1. 抽象类
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描述对象的,如果一个类中没有包含足够的信息来描绘一个具体对象,那么这样的类称为抽象类。
可能以上对于抽象类的定义还比较模糊,这里举例详细说明一下。如果我们要定义一个Square类,那么可以定义出
length
属性、Perimeter
方法以及Area
方法。但是如果我们定义的是一个Figure类,我们无法将该类定义得如此具体(当然还是可以定义成员变量和方法),那么这种图形类便称为抽象类。
在实际问题中,一般将父类定义为抽象类,我们需要使用这个父类进行继承和多态处理。回顾之前类继承的内容,我们发现继承树种,越在上方的类越抽象。
总结一下,抽象类具备以下特性:
- 抽象类不能被实例化成对象
- 抽象类依旧可以定义成员变量、成员方法和和构造方法,这些与普通类完全一样
- 抽象类必须被继承,才能被使用,所以通常在设计阶段便应该思考要不要实现抽象类
- 一个类只能继承一个抽象类,但是一个类是可以实现多个接口的。
💡 关于一个好玩的比喻就是,抽象类就像是你老板布置的任务,只负责发布不负责完成,而你想要和老板沟通就必须先完成布置的任务。
1.1 抽象类的实现
根据阿里巴巴 JAVA 开发手册,抽象类命名建议使用Abstract
或者Base
开头,这在阿里巴巴 JAVA 开发手册中是强制项。
| ```java public class Animal { public void eat() {} }
| ```java
public abstract class AbstractAnimal {
abstract void eat();
}
| | —- | —- |
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() {}; }
| ```java
public class Dog extends AbstractAnimal {
@Override
public void eat() {
System.out.println("Dog is eating bones.");
}
}
| | —- | —- |
我们知道抽象类是不能被实例化的,也就说 AbstractAnimal animal = new AbstractAnimal();
这样的写法是错误的。如果我们非要在主程序中实例化(此处仅为演示,并不建议在抽象类创建主程序入口),可以借助子类和向下转型来完成,当然我们更建议使用Dog animal = new Dog();
这样的方式:
public abstract class AbstractAnimal {
public abstract void eat();
public static void main(String[] args) {
AbstractAnimal animal = new Dog();
animal.eat();
}
}
1.4 继承多个抽象类
之前介绍过,抽象类是可以被另一个抽象类所继承,因为都是抽象类的原因,所以抽象方法并不用重写。
| ```java public abstract class AbstractAnimal { public abstract void eat(); }
| ```java
public abstract class AbstractAnimal2
extends AbstractAnimal {
public abstract void run();
}
| | —- | —- |
如果我们类继承的是AbstractAnimal2
抽象类,那么就必须重写eat
和run
两个方法:
public class Dog2 extends AbstractAnimal2 {
@Override
void eat() {}
@Override
void run() {}
}
1.5 抽象类的构造方法
既然抽象类不可以被实例化,那么它可以有构造器么?答案是可以有的,在其子类实例化时,继承的机制会默认调用抽象父类的无参构造方法。
| ```java public abstract class AbstractAnimal { public AbstractAnimal() { System.out.println(“A1”); }
abstract void eat();
}
| ```java
public class Dog extends AbstractAnimal {
public Dog() {
System.out.println("D1");
}
@Override
public void eat() {}
public static void main(String[] args) {
Dog dog = new Dog();
}
}
// A1
| | —- | —- |
如果子类本身有自己的无参构造,那么会先调用父类的构造方法,然后再调用自己的构造方法。
| ```java abstract class AbstractAnimal { public AbstractAnimal() { System.out.println(“A1”); }
abstract void eat();
}
| ```java
public class Dog extends AbstractAnimal {
public Dog() {
System.out.println("D1");
}
@Override
public void eat() {}
public static void main(String[] args) {
Dog dog = new Dog();
}
}
// A1
// D1
| | —- | —- |
那么问题来了,如果抽象类存在有参构造方法时,子类中需要使用super
来调用父类的构造器,如果还需要自己的构造器内容,可以在super(i);
后面进行添加。
| ```java public abstract class AbstractAnimal { public AbstractAnimal() { System.out.println(“A1”); }
public AbstractAnimal(int i) {
System.out.println("A2");
}
abstract void eat();
}
| ```java
public class Dog extends AbstractAnimal {
public Dog() {
System.out.println("D1");
}
public Dog(int i) {
super(i);
System.out.println("D2");
}
@Override
public void eat() {}
public static void main(String[] args) {
Dog dog = new Dog(2);
}
}
// A2
// D2
| | —- | —- |
1.6 定义成员变量
抽象类中依旧可以添加成员变量,子类通过this
来访问。
| ```java abstract class AbstractAnimal { String name;
public AbstractAnimal() {
this.name = "Snoopy";
}
abstract void eat();
}
| ```java
public class Dog extends AbstractAnimal {
@Override
public void eat() {
System.out.println(this.name + "~~~");
}
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat();
}
}
// Snoopy~~~
| | —- | —- |
2. 接口
接口是比抽象类更高级的抽象,接口中的所有方法只有定义,而不能有方法体。可以这样理解,如果一个抽象类中全是抽象方法,那么将该抽象类声明成接口。
接口中能定义抽象方法,不能有实例字段,不能有方法实现(静态的可以)。编写接口的目的在于对类的某些能力进行约定和规范,接口不能够被实例化,且没有构造器。
接口中的方法默认是public
,推荐使用默认的权限修饰符(即可以不写),但是因为接口是一种约定,是约定子类必须具备的能力,是需要子类去实现的,所以我们在编写接口时推荐使用 Javadoc 的方式给接口添加注释。
2.1 开发规范
- JAVA 中使用
interface
来声明一个接口,由于方法都是抽象的,所以这里abstract
也不需要了。 - 接口类中的方法和属性不要加任何修饰符(
public
也不要加),保持代码的简洁性,并加上有效的 Javadoc 注释。 - 尽量不要在接口里定义变量,如果一定要定义变量,必须要与接口方法相关,并且是整个应用的基础常量。
- JDK8+ 中接口允许有默认实现,那么这个
default
方法,是对所有实现类都有价值的默认实现。 - 如果是形容能力的接口名称,取对应的形容词做接口名(通常是
-able
的形式)。2.2 接口继承接口
接口与接口之间必须使用extends
来继承:
| ```java interface AnimalImpl { void eat(); void run(); }
| ```java
interface Animal2Impl extends AnimalImpl{
void fly();
}
| | —- | —- |
2.3 实现多个接口
JAVA 中使用implements
来实现一个或多个接口,接口之间使用逗号间隔。
public class Pet implements AnimalImpl, Animal2Impl{
@Override
public void eat() {}
@Override
public void run() {}
@Override
public void fly() {}
}
2.4 继承抽象类并实现接口
public class Pet extends BaseAnimal implements AnimalImpl, Animal2Impl{}
2.5 抽象类 VS. 接口
| ```java public abstract class BaseAnimal { public abstract void eat(); public abstract void run(); }
| ```java
interface AnimalImpl {
void eat();
void run();
}
| | —- | —- |
共性与区别:
- 继承更多是一种从属的关系,或者类似于集合与其子集。而接口更多是一种能力关系,通过实现多个接口从而获取多个能力。
- 抽象类是模板式的设计,而接口类是约定式设计
- 抽象类设计时往往将相同实现方法抽象在父类,由子类独立实现各自不同的实现。
- 抽象类和接口存在的意义在于做好顶层设计。
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(); }
```java
public class Eagle extends BaseAnimal implements Flyable {
@Override
public void eat(BaseAnimal animal) {
System.out.println(this.getName() + " like eating" + animal.getClass().getSimpleName());
}
public static void main(String[] args) {
Tigger tigger = new Tigger();
tigger.eat(tigger);
}
@Override
public String getName() {
return Eagle.class.getSimpleName();
}
@Override
public void fly() {
System.out.println(this.getName() + " can fly high!");
}
}
public class Tigger extends BaseAnimal implements Runnable {
@Override
public void eat(BaseAnimal animal) {
System.out.println(this.getName() + " like eating " + animal.getClass().getSimpleName() + "!");
}
@Override
public String getName() {
return Tigger.class.getSimpleName();
}
@Override
public void run() {
System.out.println(this.getName() + " can run fast!");
}
public static void main(String[] args) {
Tigger tigger = new Tigger();
Eagle eagle = new Eagle();
tigger.eat(eagle);
tigger.run();
eagle.fly();
}
}
// Tigger like eating Eagle!
// Tigger can run fast!
// Eagle can fly high!
3. 抽象类与接口设计
- 在设计抽象类时,应该将子类共有的特性举行抽象,也就是子类都必须重写的方法,比如所有
BasesAnimal
类都应该有eat
方法,至于吃的是啥,有可能是动物,也有植物,这需要子类自己去完成。 - 在设计接口时,更多关注不同子类的独有特性,比如爬行动物应该有
run
方法,于是设计Runnable
接口;鸟类应该有fly
方法,于是设计Flyable
接口。