想必大家对Java中的实体类都比较熟悉。实体类通过属性和方法去描述一系列相似对象的特征。例如我们要写一个程序去计算图形的面积,需要定义一个长方形的类:
public class Rect {
private double width;
private double height;
public Rect(double width, double height, String color) {
this.width = width;
this.height = height;
this.color = color;
}
public double area() {
return width * height;
}
}
Rect类中有着实例字段去描述长方形的宽高以及图形的颜色,area()方法用来计算长方形的面积。我们再定义一个圆形的类:
public class Circle {
private double radius;
private String color;
public Circle(double radius, String color) {
this.radius = radius;
this.color = color;
}
public void area() {
return Math.PI * radius * radius
}
}
我们可以发现,Rect类和Circle类有一部分代码是相似的,例如color字段和area()方法。然而,由于计算面积的公式不同,area()的具体实现在这两个类中是不同的。这时,我们就可以创建一个新的类将相似的代码抽象出来。
public abstract class Shape {
private String color;
public Shape(String color) {
this.color = color;
}
public abstract double area();
}
可以看到Shape类和Rect类以及Circle类的一个明显不同是,class前面多了一个abstract修饰符,这代表着Shape是一个抽象类而非实体类。抽象类和实体类的一个明显的区别就是,抽象类中包含有抽象方法(没有具体实现的方法)。换句话说,包含有抽象方法的类被称为抽象类。在Shape类中,area()就是一个抽象方法。它比普通方法多了一个abstract修饰符,并且没有由花括号包裹起来的方法体。
抽象类不能被实例化,但可以被继承。抽象类的子类必须实现父类的抽象方法(重写并使其有着具体的实现),否则子类也应该被定义为抽象类(继承了父类的抽象方法而未实现)。在这个例子中,Shape类的子类必须实现area()方法。
可能有人会问,抽象方法的存在只是为了简化代码吗?其实并不是的。抽象方法实际上定义了子类必须实现的“规范”。上层代码只定义规范,具体的业务逻辑由不同的子类实现,调用者并不关心子类的具体实现。同时,由于抽象方法的存在,在编写代码的时候不需要子类就可以实现业务逻辑(正常编译不报错)。例如我们只有一个抽象类Shape,要写一个ShapeUtil类,里面有计算图形面积之和的方法。
public class ShapeUtil {
public double sum(Shape[] shapes) {
double s = 0;
for (Shape shape : shapes) {
s += shape.area();
}
return s;
}
}
可以看到,在计算面积和时,我们并不关心每一个图形的面积是如何计算的,我们只负责处理面积求和的业务逻辑,而把计算每个图形面积的具体实现交给了子类处理。也就是说,抽象方法定义了规范(area()方法需要返回图形的面积),而具体的实现交给了子类。
接下来我们使用抽象类来实现Rect类和Circle类:
public class Rect extends Shape {
private double width;
private double height;
public Rect(double width, double height, String color) {
super(color);
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
public class Circle extends Shape{
private double radius;
public Circle(double radius, String color) {
super(color);
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
可以看出,除了拥有抽象方法外,抽象类和普通类一样,同样可以拥有成员变量和普通的成员方法。抽象类和普通类在用法上也是相似的,在继承抽象类时使用extends
关键字,一个类只能继承一个抽象类。
如果一个抽象类没有字段,所有方法全部是抽象方法,就可以把该抽象类改写为接口。接口是一种声明的集合,是一种声明的规范,里面包含了很多抽象方法。需要明确的一点是,接口不是类,类描述对象的属性和方法,接口则包含类要实现的方法。
接口用interface
来声明,它无法实例化。它可以被类实现(implements
),无法被继承。当接口被类实现时,除非该类为抽象类,否则该类需要实现接口中所规定的所有抽象方法。我们将以上代码改写,使用interface来实现:
public interface Shape {
double area();
}
public class Rect implements Shape {
private double width;
private double height;
public Rect(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
public class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
接口和抽象类是有相似之处的。接口可以继承自另一个接口(使用extends
关键字),相当于扩展类接口的方法。接口定义的方法默认是public abstract
(不需要写)。接口与抽象类的一些区别如下:
抽象类 | 接口 | |
---|---|---|
继承 | 一个子类只能继承一个抽象类 | 一个子类可以实现多个接口 |
成员变量类型 | 各种类型均可 | 只能是 public static final
|
抽象方法 | 可以定义抽象方法 | 可以定义抽象方法 |
非抽象方法 | 可以定义非抽象方法 | 可以定义default方法* |
static方法 | 可以有static方法/代码块 | 不能含有static修饰的方法 |
*:default方法是一个非抽象方法,所有实现接口的类都不需要实现这个方法,任何子类都可以重写这个方法来实现它自己的逻辑。
接口的作用主要是作为一个上层的规范。比如我们要开发一个无人驾驶汽车的软件。开发过程中,我们需要GPS服务提供商提供GPS相关的服务(例如,定位、导航),我们还需要汽车制造厂商提供操控汽车的服务(例如,加速、刹车、转弯等)。这两种服务是不相关的,GPS公司和汽车厂商不需要了解对方公司是如何提供服务的(具体的代码实现)。在这种情况下,我们可以使用接口。我们写出GPS服务的接口和汽车操控的接口,根据接口来处理业务逻辑。与此同时,GPS提供商和汽车厂商可以根据我们提供的接口规范去实现具体功能。
当以下情况适用于你时,可以考虑使用抽象类:
- 你希望在一些紧密相关的类中共享代码
- 你需要被继承的类拥有一些共同的方法、字段,或者需要除了public以外的访问修饰符(例如protected/private)
- 你希望声明一些非静态/非常量的字段(你可以定义一些方法去获取/修改对象的属性)
当以下情况适用于你时,可以考虑使用抽象类:
- 你希望让许多不相关的类来实现你的接口。例如,Java中
Comparable
和Cloneable
接口被许多不相关的类所实现。 - 你希望具体指明某一特定数据类型的某个具体行为,但并不关心是谁实现了这个行为
- 你想利用对于类型的多重继承