抽象类
2.1 为什么需要抽象类
本文为书籍《Java编程的逻辑》1和《剑指Java:核心原理与应用实践》2阅读笔记
随着子类越来越多,或者继承层次越来越多,我们会发现父类变得更通用。父类要体现所有子类的共同特征,在设计某些方法(行为特征或功能)时,我们发现在父类中无法给出合理的具体实现,而应该交由子类来实现,那么这样的方法就应该设计为抽象方法,而包含抽象方法的类就必须为抽象类。从另一个角度说,当父类表现为更通用的概念类,以至于创建它的实例对象没有实际意义,那么这样的父类就算没有抽象方法,也应该设计为抽象类。
顾名思义,抽象类就是抽象的类。抽象是相对于具体而言的,一般而言,具体类有直接对应的对象,而抽象类没有,它表达的是抽象概念,一般是具体类的比较上层的父类。比如,狗是具体对象,而动物则是抽象概念;樱桃是具体对象,而水果则是抽象概念;正方形是具体对象,而图形则是抽象概念。
抽象类看上去是多余的,对于抽象方法,不知道如何实现,定义一个空方法体不就行了吗?而抽象类不让创建对象,看上去只是增加了一个不必要的限制。引入抽象方法和抽象类,是Java
提供的一种语法工具,对于一些类和方法,引导使用者正确使用它们,减少误用。使用抽象方法而非空方法体,子类就知道它必须要实现该方法,而不可能忽略,若忽略Java
编译器会提示错误。使用抽象类,类的使用者创建对象的时候,就知道必须要使用某个具体子类,而不可能误用不完整的父类。无论是编写程序,还是平时做其他事情,每个人都可能会犯错,减少错误不能只依赖人的优秀素质,还需要一些机制,使得一个普通人都容易把事情做对,而难以把事情做错。抽象类就是Java
提供的这样一种机制。
2.2 抽象方法和抽象类
现在有一个图形类Shape
,它有一个方法draw()
。Shape
其实是一个抽象概念,它的draw()
方法其实并不知道如何实现,只有子类才知道。这种只有子类才知道如何实现的方法,一般被定义为抽象方法。抽象方法是相对于具体方法而言的,具体方法有实现代码,而抽象方法只有声明,没有实现。接口中的方法(非Java 8
引入的静态和默认方法)就都是抽象方法。抽象方法和抽象类都使用abstract
这个关键字来声明,语法如下所示:
public abstract class Shape {
//其他代码
public abstract void draw();
}
定义了抽象方法的类必须被声明为抽象类,不过,抽象类可以没有抽象方法。抽象类和具体类一样,可以定义具体方法、实例变量等,它和具体类的核心区别是,抽象类不能创建对象(比如,不能使用new Shape
),而具体类可以。
抽象类不能创建对象,要创建对象,必须使用它的具体子类。一个类在继承抽象类后,必须实现抽象类中定义的所有抽象方法,除非它自己也声明为抽象类。圆类的实现代码,如下所示:
public class Circle extends Shape {
//其他代码
@Override
public void draw() {
//主体代码
}
}
圆实现了draw()
方法。与接口类似,抽象类虽然不能使用new
,但可以声明抽象类的变量,引用抽象类具体子类的对象,如下所示:
Shape shape = new Circle();
shape.draw();
shape
是抽象类Shape
类型的变量,引用了具体子类Circle
的对象,调用draw()
方法将调用Circle
的draw
代码。
2.3 抽象类和接口
抽象类和接口有类似之处:都不能用于创建对象,接口中的方法其实都是抽象方法。如果抽象类中只定义了抽象方法,那抽象类和接口就更像了。但抽象类和接口根本上是不同的,接口中不能定义实例变量,而抽象类可以,一个类可以实现多个接口,但只能继承一个类。抽象类和接口是配合而非替代关系,它们经常一起使用,接口声明能力,抽象类提供默认实现,实现全部或部分方法,一个接口经常有一个对应的抽象类。比如,在Java
类库中,有:
Collection
接口和对应的AbstractCollection
抽象类。List
接口和对应的AbstractList
抽象类。Map
接口和对应的AbstractMap
抽象类。
对于需要实现接口的具体类而言,有两个选择:一个是实现接口,自己实现全部方法;另一个则是继承抽象类,然后根据需要重写方法。
继承的好处是复用代码,只重写需要的部分即可,需要编写的代码比较少,容易实现。不过,如果这个具体类已经有父类了,那就只能选择实现接口了。