1. 抽象类

在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描述对象的,如果一个类中没有包含足够的信息来描绘一个具体对象,那么这样的类称为抽象类

可能以上对于抽象类的定义还比较模糊,这里举例详细说明一下。如果我们要定义一个Square类,那么可以定义出length属性、Perimeter方法以及Area方法。但是如果我们定义的是一个Figure类,我们无法将该类定义得如此具体(当然还是可以定义成员变量和方法),那么这种图形类便称为抽象类

在实际问题中,一般将父类定义为抽象类,我们需要使用这个父类进行继承多态处理。回顾之前类继承的内容,我们发现继承树种,越在上方的类越抽象。

总结一下,抽象类具备以下特性:

  • 抽象类不能被实例化成对象
  • 抽象类依旧可以定义成员变量、成员方法和和构造方法,这些与普通类完全一样
  • 抽象类必须被继承,才能被使用,所以通常在设计阶段便应该思考要不要实现抽象类
  • 一个类只能继承一个抽象类,但是一个类是可以实现多个接口的。

💡 关于一个好玩的比喻就是,抽象类就像是你老板布置的任务,只负责发布不负责完成,而你想要和老板沟通就必须先完成布置的任务。

1.1 抽象类的实现

根据阿里巴巴 JAVA 开发手册,抽象类命名建议使用Abstract或者Base开头,这在阿里巴巴 JAVA 开发手册中是强制项。

| ```java public class Animal { public void eat() {} }

  1. | ```java
  2. public abstract class AbstractAnimal {
  3. abstract void eat();
  4. }

| | —- | —- |

1.2 抽象方法

抽象类具备以下特性:

  • 使用abstract关键字定义的类为抽象类,使用abstract定义的方法为抽象方法
  • 抽象方法没有方法体,抽象方法本身没有意义,除非它被重写
  • 如果一个类中的存在抽象方法,那么该类必须声明为抽象类,这也表示不可能在非抽象类中获取抽象方法
  • 抽象类被继承后,需要实现该抽象类中的所有方法,即保证相同的方法名、参数列表和相同返回类型创建出非抽象方法
  • 抽象方法默认权限修饰符是public,也可以使用protected
  • 抽象类也可以被另一个抽象类继承

:::info 🔎 问题一:抽象方法没有方法体,不能被直接调用,那么它存在的意义在哪?
——————————————————————————————————
抽象方法存在的主要意义在于对子类进行约定,所有继承抽象类的普通类,都必须实现抽象方法的重写

🔎 问题二:抽象方法的权限修饰符为什么不可以是private
——————————————————————————————————
因为private修饰的方法不可以被子类使用,更不用谈重写了。 :::

1.3 继承抽象类

继承AbstractAnimal抽象类的Dog子类必须采用重写eat方法,并且所有重写的方法都要加上@Override注解。

| ```java public abstract class AbstractAnimal { public abstract void eat(); public void swing() {}; }

  1. | ```java
  2. public class Dog extends AbstractAnimal {
  3. @Override
  4. public void eat() {
  5. System.out.println("Dog is eating bones.");
  6. }
  7. }

| | —- | —- |

我们知道抽象类是不能被实例化的,也就说 AbstractAnimal animal = new AbstractAnimal(); 这样的写法是错误的。如果我们非要在主程序中实例化(此处仅为演示,并不建议在抽象类创建主程序入口),可以借助子类向下转型来完成,当然我们更建议使用Dog animal = new Dog();这样的方式:

  1. public abstract class AbstractAnimal {
  2. public abstract void eat();
  3. public static void main(String[] args) {
  4. AbstractAnimal animal = new Dog();
  5. animal.eat();
  6. }
  7. }

1.4 继承多个抽象类

之前介绍过,抽象类是可以被另一个抽象类所继承,因为都是抽象类的原因,所以抽象方法并不用重写。

| ```java public abstract class AbstractAnimal { public abstract void eat(); }

  1. | ```java
  2. public abstract class AbstractAnimal2
  3. extends AbstractAnimal {
  4. public abstract void run();
  5. }

| | —- | —- |

如果我们类继承的是AbstractAnimal2抽象类,那么就必须重写eatrun两个方法:

  1. public class Dog2 extends AbstractAnimal2 {
  2. @Override
  3. void eat() {}
  4. @Override
  5. void run() {}
  6. }

1.5 抽象类的构造方法

既然抽象类不可以被实例化,那么它可以有构造器么?答案是可以有的,在其子类实例化时,继承的机制会默认调用抽象父类的无参构造方法。

| ```java public abstract class AbstractAnimal { public AbstractAnimal() { System.out.println(“A1”); }

  1. abstract void eat();

}

  1. | ```java
  2. public class Dog extends AbstractAnimal {
  3. public Dog() {
  4. System.out.println("D1");
  5. }
  6. @Override
  7. public void eat() {}
  8. public static void main(String[] args) {
  9. Dog dog = new Dog();
  10. }
  11. }
  12. // A1

| | —- | —- |

如果子类本身有自己的无参构造,那么会先调用父类的构造方法,然后再调用自己的构造方法。

| ```java abstract class AbstractAnimal { public AbstractAnimal() { System.out.println(“A1”); }

  1. abstract void eat();

}

  1. | ```java
  2. public class Dog extends AbstractAnimal {
  3. public Dog() {
  4. System.out.println("D1");
  5. }
  6. @Override
  7. public void eat() {}
  8. public static void main(String[] args) {
  9. Dog dog = new Dog();
  10. }
  11. }
  12. // A1
  13. // D1

| | —- | —- |

那么问题来了,如果抽象类存在有参构造方法时,子类中需要使用super来调用父类的构造器,如果还需要自己的构造器内容,可以在super(i);后面进行添加。

| ```java public abstract class AbstractAnimal { public AbstractAnimal() { System.out.println(“A1”); }

  1. public AbstractAnimal(int i) {
  2. System.out.println("A2");
  3. }
  4. abstract void eat();

}

  1. | ```java
  2. public class Dog extends AbstractAnimal {
  3. public Dog() {
  4. System.out.println("D1");
  5. }
  6. public Dog(int i) {
  7. super(i);
  8. System.out.println("D2");
  9. }
  10. @Override
  11. public void eat() {}
  12. public static void main(String[] args) {
  13. Dog dog = new Dog(2);
  14. }
  15. }
  16. // A2
  17. // D2

| | —- | —- |

1.6 定义成员变量

抽象类中依旧可以添加成员变量,子类通过this来访问。

| ```java abstract class AbstractAnimal { String name;

  1. public AbstractAnimal() {
  2. this.name = "Snoopy";
  3. }
  4. abstract void eat();

}

  1. | ```java
  2. public class Dog extends AbstractAnimal {
  3. @Override
  4. public void eat() {
  5. System.out.println(this.name + "~~~");
  6. }
  7. public static void main(String[] args) {
  8. Dog dog = new Dog();
  9. dog.eat();
  10. }
  11. }
  12. // Snoopy~~~

| | —- | —- |

2. 接口

接口是比抽象类更高级的抽象,接口中的所有方法只有定义,而不能有方法体。可以这样理解,如果一个抽象类中全是抽象方法,那么将该抽象类声明成接口

接口中能定义抽象方法,不能有实例字段,不能有方法实现(静态的可以)。编写接口的目的在于对类的某些能力进行约定和规范,接口不能够被实例化,且没有构造器

接口中的方法默认是public,推荐使用默认的权限修饰符(即可以不写),但是因为接口是一种约定,是约定子类必须具备的能力,是需要子类去实现的,所以我们在编写接口时推荐使用 Javadoc 的方式给接口添加注释。

2.1 开发规范

  • JAVA 中使用interface来声明一个接口,由于方法都是抽象的,所以这里abstract也不需要了。
  • 接口类中的方法和属性不要加任何修饰符(public也不要加),保持代码的简洁性,并加上有效的 Javadoc 注释。
  • 尽量不要在接口里定义变量,如果一定要定义变量,必须要与接口方法相关,并且是整个应用的基础常量。
  • JDK8+ 中接口允许有默认实现,那么这个default方法,是对所有实现类都有价值的默认实现。
  • 如果是形容能力的接口名称,取对应的形容词做接口名(通常是-able的形式)。

    2.2 接口继承接口

    接口与接口之间必须使用extends来继承:

| ```java interface AnimalImpl { void eat(); void run(); }

  1. | ```java
  2. interface Animal2Impl extends AnimalImpl{
  3. void fly();
  4. }

| | —- | —- |

2.3 实现多个接口

JAVA 中使用implements来实现一个或多个接口,接口之间使用逗号间隔。

  1. public class Pet implements AnimalImpl, Animal2Impl{
  2. @Override
  3. public void eat() {}
  4. @Override
  5. public void run() {}
  6. @Override
  7. public void fly() {}
  8. }

2.4 继承抽象类并实现接口

  1. public class Pet extends BaseAnimal implements AnimalImpl, Animal2Impl{}

2.5 抽象类 VS. 接口

| ```java public abstract class BaseAnimal { public abstract void eat(); public abstract void run(); }

  1. | ```java
  2. interface AnimalImpl {
  3. void eat();
  4. void run();
  5. }

| | —- | —- |

共性与区别:

  • 继承更多是一种从属的关系,或者类似于集合与其子集。而接口更多是一种能力关系,通过实现多个接口从而获取多个能力。
  • 抽象类是模板式的设计,而接口类是约定式设计
  • 抽象类设计时往往将相同实现方法抽象在父类,由子类独立实现各自不同的实现。
  • 抽象类和接口存在的意义在于做好顶层设计。

    2.6 参考案例

    ```java interface Runnable { void run(); }

interface Flyable { void fly(); }

public abstract class BaseAnimal { public abstract void eat(BaseAnimal animal); public abstract String getName(); }

  1. ```java
  2. public class Eagle extends BaseAnimal implements Flyable {
  3. @Override
  4. public void eat(BaseAnimal animal) {
  5. System.out.println(this.getName() + " like eating" + animal.getClass().getSimpleName());
  6. }
  7. public static void main(String[] args) {
  8. Tigger tigger = new Tigger();
  9. tigger.eat(tigger);
  10. }
  11. @Override
  12. public String getName() {
  13. return Eagle.class.getSimpleName();
  14. }
  15. @Override
  16. public void fly() {
  17. System.out.println(this.getName() + " can fly high!");
  18. }
  19. }
  1. public class Tigger extends BaseAnimal implements Runnable {
  2. @Override
  3. public void eat(BaseAnimal animal) {
  4. System.out.println(this.getName() + " like eating " + animal.getClass().getSimpleName() + "!");
  5. }
  6. @Override
  7. public String getName() {
  8. return Tigger.class.getSimpleName();
  9. }
  10. @Override
  11. public void run() {
  12. System.out.println(this.getName() + " can run fast!");
  13. }
  14. public static void main(String[] args) {
  15. Tigger tigger = new Tigger();
  16. Eagle eagle = new Eagle();
  17. tigger.eat(eagle);
  18. tigger.run();
  19. eagle.fly();
  20. }
  21. }
  22. // Tigger like eating Eagle!
  23. // Tigger can run fast!
  24. // Eagle can fly high!

3. 抽象类与接口设计

  • 在设计抽象类时,应该将子类共有的特性举行抽象,也就是子类都必须重写的方法,比如所有BasesAnimal类都应该有eat方法,至于吃的是啥,有可能是动物,也有植物,这需要子类自己去完成。
  • 在设计接口时,更多关注不同子类的独有特性,比如爬行动物应该有run方法,于是设计Runnable接口;鸟类应该有fly方法,于是设计Flyable接口。