6-Java 面向对象高级2

6.4 final 关键字

final 关键字的作用:

  1. 修饰变量:被他修饰的变量不可改变。一旦赋了初值,就不能被重新赋值。

    final int MAX_SPEED = 120;
    
  2. 修饰方法:该方法不可被子类重写。但是可以被重载!

    final void study() {}
    
  3. 修饰类:修饰的类不能被继承。比如:Math、String 等。

    final class A {}
    

6.5 抽象方法和抽象类

抽象方法: 使用 abstract 修饰的方法,没有方法体,只有声明。定义的是一种“规范”,就是告诉子类必须要给抽象方法提供具体的实现。

抽象类: 包含抽象方法的类必须是抽象类。通过 abstract 方法定义规范,然后要求子类必须定义具体实现。通过抽象类,我们就可以做到严格限制子类的设计,使子类之间更加通用。

  • 抽象类和抽象方法的基本用法
/**
 * 测试抽象方法
 * 抽象类的意义在于:为子类提供统一、规范的模板。子类必须实现相关的抽象方法。
 *
 * @author ZJax
 * @date 2021年05月14日 20:43
 */
public abstract class Animal {
    /*
        1. 本类中没有实现。
        2. 子类必须实现。
    */
    public abstract void shout();

    public void run() {
        System.out.println("Run!!!");
    }
}

class Dog extends Animal {

    @Override
    public void shout() {
        System.out.println("汪汪!");
    }
}
  • 抽象类的使用要点:
  1. 有抽象方法的类只能定义成抽象类
  2. 抽象类不能实例化,即不能用 new 来实例化抽象类。
  3. 抽象类可以包含属性、方法、构造方法。但是构造方法不能用来 new 实例,只能用来被子类调用。
  4. 抽象类只能用来被继承。
  5. 抽象方法必须被子类实现。

6.6 接口

6.6.1 接口的作用

  • 为什么需要接口?接口和抽象类的区别?

    接口就是比“抽象类”还“抽象”的“抽象类”,可以更加规范的对子类进行约束。全面地专业地实现了:规范和具体实现的分离。

    抽象类还提供某些具体实现,接口不提供任何实现,接口中所有方法都是抽象方法。接口是完全面向规范的,规定了一批类具有的公共方法规范。

    • 从接口的实现者角度看,接口定义了可以向外部提供的服务。
    • 从接口的调用者角度看,接口定义了实现者能提供那些服务。

    接口是两个模块之间通信的标准,通信的规范。如果能把你要设计的模块之间的接口定义好,就相当于完成了系统的设计大纲,剩下的就是添砖加瓦的具体实现了。大家在工作以后,做系统时往往就是使用“面向接口”的思想来设计系统。

    接口和实现类不是父子关系,是实现规则的关系。 比如:我定义一个接口 Runnable,Car 实现它就能在地上跑,Train 实现它也能在地上跑,飞机实现它也能在地上跑。就是说,如果它是交通工具,就一定能跑,但是一定要实现 Runnable 接口。

  • 接口的本质探讨

    接口就是规范,定义的是一组规则,体现了现实世界中“如果你是…则必须能…”的思想。如果你是天使,则必须能飞。如果你是汽车,则必须能跑。如果你是好人,则必须能干掉坏人;如果你是坏人,则必须欺负好人。

    接口的本质是契约,就像我们人间的法律一样。制定好后大家都遵守。

    面向对象的精髓,是对对象的抽象,最能体现这一点的就是接口。为什么我们讨论设计模式都只针对具备了抽象能力的语言(比如 C++、Java、C# 等),就是因为设计模式所研究的,实际上就是如何合理的去抽象。

    Java 中的接口更多的体现在对行为的抽象!

  • 区别

    1. 普通类:具体实现
    2. 抽象类:具体实现,规范(抽象方法)
    3. 接口:规范!

6.6.2 定义和使用接口

声明格式:

[访问修饰符] interface 接口名 [extends 父接口1, 父接口2...] {
    常量定义;
    方法定义;
}

说明:

  1. 访问修饰符:只能是 public 或默认。
  2. 接口名:和类名采用相同命名机制。
  3. extends:接口可以多继承。
  4. 常量:接口中的属性只能是常量,总是:public static final 修饰。不写也是。
  5. 方法:接口中的方法只能是:public abstract。 省略的话,也是 public abstract

要点:

  1. 子类通过 implements 来实现接口中的规范。
  2. 接口不能创建实例,但是可用于声明引用变量类型。
  3. 一个类实现了接口,必须实现接口中所有的方法,并且这些方法只能是 public 的。
  4. JDK1.7 之前,接口中只能包含静态常量、抽象方法,不能有普通属性、构造方法、普通方法。
  5. JDK1.8 后,接口中包含普通的静态方法。

接口的使用:

/**
 * 测试接口和实现类
 *
 * @author ZJax
 * @date 2021年05月14日 21:32
 */
public class TestInterface {
    public static void main(String[] args) {
        Volant v = new BirdMan();
        v.fly();
        Honest h = new GentleMan();
        h.helpOther();
        Angel a = new Angel();
        a.fly();
        a.helpOther();
    }
}

/**
 * 飞行接口
 *
 * @author ZJax
 */
interface Volant {
    int FLY_HEIGHT = 1000;

    void fly();
}

/**
 * 善良接口
 *
 * @author ZJax
 */
interface Honest {
    void helpOther();
}

class GentleMan implements Honest {

    @Override
    public void helpOther() {
        System.out.println("GentleMan.helpOther");
    }
}

class BirdMan implements Volant {

    @Override
    public void fly() {
        System.out.println("BirdMan.fly");
    }
}

/**
 * 一个类可以实现多个接口
 *
 * @author ZJax
 */
class Angel implements Volant, Honest {

    @Override
    public void fly() {
        System.out.println("Angel.fly");
    }

    @Override
    public void helpOther() {
        System.out.println("Angel.helpOther");
    }
}

6.6.3 接口的多继承

接口完全支持多继承。和类的继承类似,子接口扩展某个父接口,将会获得父接口中所定义的一切。

/**
 * @author ZJax
 * @date 2021年05月15日 0:14
 */
public class TestInterface2 {
    public static void main(String[] args) {
        MySubClass msc = new MySubClass();
        msc.testA();
        msc.testB();
        msc.testC();
    }
}

interface A {
    void testA();
}

interface B {
    void testB();
}

interface C extends A, B { /* 接口可以多继承 */
    void testC();
}

class MySubClass implements C {

    @Override
    public void testA() {
        System.out.println("MySubClass.testA");
    }

    @Override
    public void testB() {
        System.out.println("MySubClass.testB");
    }

    @Override
    public void testC() {
        System.out.println("MySubClass.testC");
    }
}

6.6.4 面向接口编程

面向接口编程是面向对象编程的一部分。

为什么需要面向接口编程?软件设计中最难处理的就是需求的复杂变化,需求的变化更多的体现在具体实现上。我们的编程如果围绕具体实现来展开就会陷入“复杂变化”的汪洋大海中,软件也就不能最终实现。我们必须围绕某种稳定的东西开展,才能以静制动,实现规范的高质量的项目。

** 接口就是规范,就是项目中最稳定的因素!** 面向接口编程可以让我们把握住真正核心的东西,使实现复杂多变的需求成为可能。

通过面向接口编程,而不是面向实现类编程,可以大大降低程序模块间的耦合性,提高整个系统的可扩展性和和可维护性。

面向接口编程的概念比接口本身的概念要大得多。设计阶段相对比较困难,在你没有写实现时就要想好接口,接口一变就乱套了,所以设计要比实现难!

6.7 内部类

6.7.1 内部类的概念

一般情况,我们把类定义成独立的单元。有些情况下,我们把一个类放在另一个类的内部定义,称为内部类(innerclasses)。

内部类可以使用 public、default、protected 、private 以及 static 修饰。而外部顶级类(我们以前接触的类)只能使用 public 和 default 修饰。

注意:

内部类只是一个编译时概念,一旦我们编译成功,就会成为完全不同的两个类。对于一个名为 Outer 的外部类和其内部定义的名为 Inner 的内部类。编译完成后会出现 Outer.class 和 Outer$Inner.class 两个类的字节码文件。所以内部类是相对独立的一种存在,其成员变量/方法名可以和外部类的相同。

/**
 * 外部类
 */
class Outer {
    private int age = 10;

    public void show() {
        System.out.println(age);
    }

    /**
     * 内部类
     */
    public class Inner {
        // 内部类中可以声明与外部类同名的属性与方法
        private int age = 20;
        public void show() {
            System.out.println(age);
        }
    }
}

编译后会产生两个不同的字节码文件

img

6.7.2 内部类的作用

  1. 内部类提供了更好的封装。只能让外部类直接访问,不允许同一个包中的其他类直接访问。
  2. 内部类可以直接访问外部类的私有属性,内部类被当成其外部类的成员。 但外部类不能访问内部类的内部属性。
  3. 接口只是解决了多重继承的部分问题,而内部类使得多重继承的解决方案变得更加完整。
  4. 在一个类的使用次数较少时,我们可以通过将其写成内部类来提升性能。

6.7.3 内部类的分类

在 Java 中内部类主要分为成员内部类(非静态内部类、静态内部类)、匿名内部类、局部内部类。

成员内部类(可以使用 private、default、protected、public 任意进行修饰。 类文件:外部类$内部类.class)

6.7.4 成员内部类

非静态内部类

说明:外部类里使用非静态内部类和平时使用其他类没什么不同

  1. 非静态内部类必须寄存在一个外部类对象里。因此,如果有一个非静态内部类对象那么一定存在对应的外部类对象。非静态内部类对象单独属于外部类的某个对象。

  2. 非静态内部类可以直接访问外部类的成员,但是外部类不能直接访问非静态内部类成员。

  3. 非静态内部类不能有静态方法、静态属性和静态初始化块。

  4. 外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例。

  5. 成员变量访问要点:

    1. 内部类里方法的局部变量:变量名。
    2. 内部类属性:this.变量名。
    3. 外部类属性:外部类名.this.变量名。
    class Outer {
        private int age = 10;
        class Inner {
            int age = 20;
            public void show() {
                int age = 30;
                System.out.println("内部类方法里的局部变量age:" + age); // 30
                System.out.println("内部类的成员变量age:" + this.age); // 20
                System.out.println("外部类的成员变量age:" + Outer.this.age); // 10
            }
        }
    }
    
  6. 内部类的访问:

    1. 外部类中定义内部类:new Inner()
    2. 外部类以外的地方使用非静态内部类:Outer.Inner varname = new Outer().new Inner()
    /**
     * 测试非静态内部类
     *
     * @author ZJax
     * @date 2021年05月15日 11:52
     */
    public class TestInnerClass {
        public static void main(String[] args) {
            // 外部类以外的地方使用非静态内部类
            // 方式一:先创建外部类,再创建内部类
            Outer outer = new Outer();
            Outer.Inner inner = outer.new Inner();
    
            // 方式二:直接创建
            Outer.Inner inner = new Outer().new Inner();
        }
    }
    
    class Outer {
        private int age = 10;
        public void testOuter() {
            // 外部类中定义内部类:new Inner()
            Inner inner = new Inner();
            System.out.println("Outer.testOuter");
        }
    
        class Inner {
            private int age = 20;
            public void show() {
                int age = 30;
                System.out.println("外部类的成员变量 age:" + Outer.this.age);
                System.out.println("内部类的成员变量 age:" + this.age);
                System.out.println("局部变量 age:" + age);
            }
        }
    }
    

静态内部类

定义方式:

static class ClassName {
    // 类体
}

使用要点:

  1. 当一个静态内部类对象存在,并不一定存在对应的外部类对象。 因此,静态内部类的实例方法不能直接访问外部类的实例方法。
  2. 静态内部类看做外部类的一个静态成员。 因此,外部类可以通过:“静态内部类.名字”的方式访问静态内部类的静态成员,通过 new 静态内部类() 访问静态内部类的实例。
public class TestStaticInnerClass {
    public static void main(String[] args) {
        /*
        对比非静态内部类
            Outer2.Inner2 oi2 = new Outer2().new Inner2();
         */
        Outer3.Inner3 inner3 = new Outer3.Inner3();
    }
}

class Outer3 {
    public void show() {
    }
    static class Inner3 {
    }
}

6.7.5 匿名内部类

适合那种只需要使用一次的类,匿名内部类通常是作为其他方法的参数而被创建的。比如:键盘监听操作等等。

匿名内部类的使用:

/**
 * 测试匿名内部类
 *
 * @author ZJax
 * @date 2021年05月15日 13:36
 */
public class TestAnonymousInnerClass {

    public static void main(String[] args) {
        // 匿名内部类的使用
        TestAnonymousInnerClass.test(new Animal2() {
            @Override
            public void eat() {
                System.out.println("吃饭。");
            }
        });
    }

    public static void test(Animal2 animal2) {
        animal2.eat();
    }
}

interface Animal2 {
    void eat();
}

注意

  1. 匿名内部类没有访问修饰符。
  2. 匿名内部类没有构造方法。因为它连名字都没有,那又何来构造方法呢。

6.7.6 局部内部类

还有一种内部类,它是定义在方法内部的,作用域只限于本方法,称为局部内部类。

局部内部类的的使用主要是用来解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类。局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法中被使用,出了该方法就会失效。

局部内部类在实际开发中应用很少。

public class Test2 {
    public void show() {
        // 作用域仅限于该方法
        class Inner {
            public void fun() {
                System.out.println("helloworld");
            }
        }
        new Inner().fun();
    }

    public static void main(String[] args) {
        new Test2().show();
    }
}

6.8 枚举

JDK1.5 引入了枚举类型。枚举类型的定义包括枚举声明和枚举体。枚举体就是放置一些常量。

enum  枚举名 {
    枚举体(常量列表)
}

所有的枚举类型隐性地继承自 java.lang.Enum。枚举实质上还是类!而每个被枚举的成员实质就是一个枚举类型的实例,他们默认都是 public static final 修饰的。可以直接通过枚举类型名使用它们。

enum Season {
    SPRING, SUMMER, AUTUMN, WINDER 
}

建议:

  1. 当你需要定义一组常量时,可以使用枚举类型。
  2. 尽量不要使用枚举的高级特性,事实上高级特性都可以使用普通类来实现,没有必要引入枚举,增加程序的复杂性!
import java.util.Random;
public class TestEnum {
    public static void main(String[] args) {
        // 枚举遍历
        for (Week k : Week.values()) {
            System.out.println(k);
        }
        // switch 语句中使用枚举
        int a = new Random().nextInt(4); // 生成 0,1,2,3 的随机数
        switch (Season.values()[a]) {
        case SPRING:
            System.out.println("春天");
            break;
        case SUMMER:
            System.out.println("夏天");
            break;
        case AUTUMN:
            System.out.println("秋天");
            break;
        case WINDTER:
            System.out.println("冬天");
            break;
        }
    }
}
/* 季节 */
enum Season {
    SPRING, SUMMER, AUTUMN, WINDTER
}
/* 星期 */
enum Week {
    星期一, 星期二, 星期三, 星期四, 星期五, 星期六, 星期日
}