目录

Java 抽象类与接口

1. 抽象类

多态 文中提到了 抽象 的概念:如果一个类定义了没有具体执行代码的方法(即只有定义,没有实现),这个方法就是抽象方法,并用 abstract 修饰(不能定义为 final)。因为无法执行抽象方法,因此这个类也必须定义为抽象类,但是抽象类中可以不定义抽象方法。

在阿里巴巴 Java 开发手册中抽象类的类名必须带有 Abstract 的开头。

抽象类本身被设计成 只能用于被继承,并且在其子类中 必须覆写 抽象类中的 抽象方法

之前文中的多态实现代码中,就带有一个如下的抽象类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
abstract class AbstractAnimal {
	
    String name;

    public AbstractAnimal(String name) {
        this.name = name;
    }

	/**
     * 动物叫声
     */
    public abstract void sound();
}

其中 sound() 方法即为所谓的抽象方法,在任何继承该抽象类的子类中 sound() 方法必须覆写,如果在抽象类中实现了普通的方法,则无须覆写直接调用。

1.1 为什么要使用面向抽象编程?

尽量引用高层类型,避免引用实际子类型的方式。

上句的解释是,使用向上转型引用具体的子类实例,可以忽略具体的子类型。也可以理解为,面向抽象编程使得调用者只关心抽象方法的定义,不关心子类的具体实现。

1
2
3
4
5
AbstractAnimal dog = new Dog();
dog.sound();

AbstractAnimal cat = new Cat();
cat.sound();

比如上面例子中的抽象方法 sound() 定义为动物的叫声,Dog 类 和 Cat 类都是抽象类 AbstractAnimal 的子类,通过向上转型的对象 dogcat 可以直接调用方法 sound(),并不关心 AbstractAnimal 类型变量的具体子类型(即不关心动物是如何叫的,只关心动物的叫声是什么)。

这样的好处是,假如引用一个新的子类 Fox,在上面代码中添加:

1
2
AbstractAnimal fox = new Fox();
fox.sound();

调用时不用关心新的子类是如何实现 sound() 方法,也不用修改抽象类中的任何代码。这也是实现多态的其中一种方法。

2. 接口

如果说抽象类中可以定义抽象方法,那么接口就是抽象方法的集合。

关于接口,有以下的规定:

  • 接口并不是类,类描述对象的属性和方法,接口则包含类要实现的方法。

  • 接口中 只能包含常量的声明和抽象方法,不存在构造方法,所有常量默认都是 public static final,所有方法默认都是 public abstract,以上均可省略。

  • 所有非抽象类实现一个接口时,须使用 implements 关键字,而且接口中的所有方法必须覆写实现,访问权限也必须是 public

  • 接口支持多继承,即一个类可以实现多个接口。

2.1 抽象类与接口的比较

语法维度 抽象类 接口
关系 is - a can - do
定义关键字 abstract interface
子类继承或实现关键字 extends implements
方法实现 可以有 不能有(JDK8 开始支持 default)
方法访问控制符 无限制 有限制,默认是 public abstract 类型
属性访问控制符 无限制 有限制,默认是 public static final 类型
静态方法 可以有 不能有(JDK8 开始可以有)
static{} 静态代码块 可以有 不能有
本类型之间拓展 单继承 多继承
本类型之间拓展关键字 extends extends

对于上述表格的一些解释:

  • 抽象类中如果只有一个抽象方法,那么它等同于一个接口。

  • 抽象类能继承抽象类,接口能继承接口,但是只有接口可以同时继承多个接口,表示该接口可以做很多事情,使用的关键字为 extends 而非 implements

  • 在 JDK8 开始支持 default 方法,当接口中新增一个 default 方法,可以不必影响所有的实现该接口的类,可以按需覆写。 default 方法可以在接口中实现,可以在不被覆写的情况下直接调用。

如下定义了默认方法 fly(),如果定义了 Bird 类就可以在调用或覆写该方法,其它实现该接口的类不必覆写。众所周知猫和狗不能飞,若使用抽象方法定义了 fly(),则在 DogCat 类都需要覆写,这时覆写的方法变得无意义。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
public class InterfaceDemo {

    public static void main(String[] args) {
        Dog dog = new Dog("Dog");
        dog.sound();

        Cat cat = new Cat("Cat");
        cat.sound();

        Bird bird = new Bird("Bird");
        bird.sound();
        bird.fly();
    }
}

interface Animal {

    /**
     * 获取动物的名称
     * @return name
     */
    String getName();

    /**
     * 动物叫声
     */
    void sound();

    /**
     * default 方法
     */
    default void fly() {
        System.out.println(getName() + " flies");
    }
}

class Dog implements Animal {

    private final String name;

    public Dog(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void sound() {
        System.out.println(name + " goes woof");
    }
}

class Cat implements Animal {

    private final String name;

    public Cat(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void sound() {
        System.out.println(name + " goes meow");
    }
}

class Bird implements Animal {

    private final String name;

    public Bird(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void sound() {
        System.out.println(name + " goes tweet");
    }

    @Override
    public void fly() {
        System.out.println(name + " flies on the sky");
    }
}
注意
  • 如果一个类实现了多个接口,而这些接口又有同名的 default 方法,该类必须覆写接口中的 default 方法,否则无法指明是哪个接口的 default 方法。

  • 如果子类 B 继承父类 A,父类 A 中有 a 方法,该子类同时实现的接口中也有被 default 修饰的 a 方法,那么子类 B 会继承父类 A 的 a 方法而不是继承接口中的 a 方法。

2.2 接口回调

对于上述例子中,主类是使用类的对象调用接口覆写的方法。如果使用接口变量的引用指向实现接口的类的对象,那么该接口变量就可以调用被类实现的接口方法。

将主类改为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public static void main(String[] args) {
	Animal animal;
    animal = new Dog("Dog");
    animal.sound();

    animal = new Cat("Cat");
    animal.sound();

    animal = new Bird("Bird");
    animal.sound();
    animal.fly();
}

结果一致:

1
2
3
4
Dog goes woof
Cat goes meow
Bird goes tweet
Bird flies on the sky

接口变量 animal 调用被各类实现的方法时,相当于通知相应的对象调用该方法。接口回调的作用类似向上转型对象调用子类覆写的方法。

补充:非访问修饰符 final 和 static

上文甚至之前文中的代码使用了 finalstatic 修饰符,static 变量和方法已经在前文记录,下作补充。

1. final 的三个“不能”:

  • final 修饰的类不能被继承。

  • final 修饰的方法不能被子类覆写。

  • final 修饰的字段和局部变量不能被重新赋值。

2. static 变量

  • 局部变量不能被声明为 static 变量。

  • 实例对象没有静态字段。最好通过 类名.变量 来访问类变量。

  • 一个类的所有实例下的静态字段 共享同一片空间,无论修改哪个实例下的静态字段,都会影响到所有实例的静态字段,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Person {
    
    String name;
    static int number;

    Person(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        Person person1 = new Person("马保国");
        Person person2 = new Person("妙蛙种子");
        person1.number = 17;
        System.out.println(person1.number);
        person2.number = 12;
        System.out.println(person2.number);
        System.out.println(person2.number);
    }
}

结果为:

1
2
3
17
12
12

可见对 person2 的编号赋值 12 后会将 person1 的编号从 17 改成 12。实际上,上面的主函数相当于:

1
2
3
4
5
number = 17;
System.out.println(number);
number = 12;
System.out.println(number);
System.out.println(number);

日常使用 static 变量时需要特别注意,因为不同于 final,静态字段是可以重新赋值的。