Jakob Jenkov 2015-09-04
Java 中的继承(Inheritance)是指一个类可以从另一个类继承属性和行为。在 Java 中,这也被称为“扩展”(extending)一个类。当一个类继承另一个类时,这两个类就分别扮演了特定的角色:
- 子类(subclass):执行继承操作的类,也称为派生类。
- 父类(superclass):被继承的类,也称为基类。
换句话说,子类扩展了父类,或者说子类从父类继承而来。
另一个常用于描述继承关系的术语是特化(specialization)与泛化(generalization):
- 子类是对父类的一种特化;
- 父类是对一个或多个子类的泛化。
继承是一种代码复用的方法
继承是一种有效的方式,用于在具有某些共同特征但又存在差异的类之间共享代码。
下图展示了一个名为 Vehicle(车辆)的类,它有两个子类:Car(小汽车)和 Truck(卡车)。
Vehicle是Car和Truck的父类;Car和Truck是Vehicle的子类。
Vehicle 类可以包含所有车辆共有的字段和方法(例如车牌号、车主等),而 Car 和 Truck 则可以各自包含其特有的字段和方法。
注意:有些人认为继承是根据“是什么”来对类进行分类的。例如,“小汽车是一种车辆”,“卡车也是一种车辆”。但在实际开发中,是否使用继承通常不是由这种语义决定的,而是取决于你在应用程序中如何处理这些对象。
例如,你是否需要将
Car和Truck对象统一视为Vehicle对象?是否需要以相同方式处理它们?如果是,那么为它们定义一个公共的Vehicle父类是有意义的。如果从未以相同方式处理它们,那么除了避免重复代码外,就没有必要创建公共父类。
类层次结构(Class Hierarchies)
父类和子类构成了一个继承结构,也称为类层次结构(class hierarchy)。在该结构的顶部是父类,底部是子类。
类层次结构可以有多层:一个子类本身也可以作为其他类的父类,从而形成多级继承。
Java 继承基础
当一个类从父类继承时,它会继承父类的部分方法和字段。子类还可以重写(override)继承来的方法。字段不能被重写,但可以在子类中被“隐藏”(shadowing)。具体机制将在后文详细说明。
哪些内容会被继承?
在 Java 中,当子类继承父类时:
- 所有
protected和public的字段和方法都会被继承; - 被继承的成员在子类中就像子类自己声明的一样,可以直接访问;
- 具有默认(包级)访问权限的成员,只有在子类与父类位于同一包中时才能被访问;
private字段和方法永远不会被子类直接访问,但可以通过父类中可访问的方法(如protected或public方法)间接访问。
构造函数不会被继承。但是,子类的构造函数必须调用父类的某个构造函数(这将在后文详述)。
Java 仅支持单继承
Java 的继承机制只允许一个类继承自一个父类(即单继承)。某些语言(如 C++)支持多继承(一个类可继承多个父类),但由于多继承可能引发命名冲突等问题(例如多个父类中有同名同参的方法),Java 有意不支持多继承。
在 Java 中声明继承
在 Java 中,使用 extends 关键字声明继承关系。示例如下:
public class Vehicle {
protected String licensePlate = null;
public void setLicensePlate(String license) {
this.licensePlate = license;
}
}
public class Car extends Vehicle {
int numberOfSeats = 0;
public String getNumberOfSeats() {
return this.numberOfSeats;
}
}
在这个例子中,Car 类继承自 Vehicle 类,因此它自动拥有 licensePlate 字段(因为它是 protected 的)。
虽然上面的代码没有在 Car 中直接使用 licensePlate,但我们完全可以这样做:
public class Car extends Vehicle {
int numberOfSeats = 0;
public String getNumberOfSeats() {
return this.numberOfSeats;
}
public String getLicensePlate() {
return this.licensePlate; // 访问继承自父类的字段
}
}
注意:通常更合理的做法是将
getLicensePlate()方法放在Vehicle类中,这里只是为了演示子类可以访问继承的字段。
继承与类型转换(Type Casting)
你可以将子类的实例当作其父类的实例来使用。例如:
Car car = new Car();
Vehicle vehicle = car; // 合法:Car 是 Vehicle 的子类
这个过程称为类型转换(type casting)。
向上转型(Upcasting)
将子类对象赋值给父类引用,称为向上转型。这是总是安全且自动的:
Car car = new Car();
Vehicle vehicle = car; // 向上转型
向下转型(Downcasting)
将父类引用转回子类类型,称为向下转型。但前提是该对象实际是该子类的实例,否则会在运行时抛出 ClassCastException。
✅ 正确示例:
Car car = new Car();
Vehicle vehicle = car; // 向上转型
Car car2 = (Car) vehicle; // 向下转型,成功
❌ 错误示例:
Truck truck = new Truck();
Vehicle vehicle = truck; // 向上转型
Car car = (Car) vehicle; // 运行时抛出 ClassCastException!
重写方法(Overriding Methods)
子类可以重写(override)父类中定义的方法。例如:
public class Vehicle {
String licensePlate = null;
public void setLicensePlate(String licensePlate) {
this.licensePlate = licensePlate;
}
}
public class Car extends Vehicle {
@Override
public void setLicensePlate(String license) {
this.licensePlate = license.toLowerCase(); // 转为小写
}
}
要成功重写,子类方法的签名(方法名、参数类型和顺序)必须与父类完全一致。否则会被视为一个新方法,而非重写。
注意:
private方法无法被重写。即使子类定义了同名同参的private方法,父类内部调用仍会使用自己的private方法。
@Override 注解
使用 @Override 注解可以明确表示该方法意在重写父类方法。如果父类中该方法被删除或签名变更,编译器会报错,帮助你及时发现问题:
public class Car extends Vehicle {
@Override
public void setLicensePlate(String license) {
this.licensePlate = license.toLowerCase();
}
}
这是一个良好的编程实践。
调用父类方法
即使重写了父类方法,你仍然可以通过 super 关键字调用父类的实现:
public class Car extends Vehicle {
@Override
public void setLicensePlate(String license) {
super.setLicensePlate(license); // 调用父类方法
// 可在此添加额外逻辑
}
}
你可以在子类的任何方法中使用 super,不一定非要在重写的方法中。
instanceof 操作符
Java 提供 instanceof 操作符,用于判断一个对象是否是某个类(或其子类)的实例:
Car car = new Car();
boolean isCar = car instanceof Car; // true
boolean isVehicle = car instanceof Vehicle; // true(因为 Car 继承自 Vehicle)
即使变量声明为父类类型,instanceof 仍会检查实际对象的类型:
Vehicle vehicle = new Car();
boolean isCar = vehicle instanceof Car; // true
但如果实际对象不是目标类型,则返回 false:
Vehicle vehicle = new Truck();
boolean isCar = vehicle instanceof Car; // false
字段与继承
字段不能被重写。如果子类定义了与父类同名的字段,该字段会隐藏(shadow)父类字段。
示例:
public class Vehicle {
String licensePlate = null;
public String getLicensePlate() { return licensePlate; }
public void setLicensePlate(String s) { licensePlate = s; }
}
public class Car extends Vehicle {
protected String licensePlate = null; // 隐藏父类字段
@Override
public void setLicensePlate(String license) {
super.setLicensePlate(license); // 设置父类字段
}
@Override
public String getLicensePlate() {
return super.getLicensePlate(); // 读取父类字段
}
public void updateLicensePlate(String license) {
this.licensePlate = license; // 设置子类字段!
}
}
测试代码:
Car car = new Car();
car.setLicensePlate("123");
car.updateLicensePlate("abc");
System.out.println(car.getLicensePlate()); // 输出 "123"
原因:getLicensePlate() 返回的是父类的 licensePlate(值为 "123"),而 updateLicensePlate() 修改的是子类的 licensePlate(值为 "abc"),两者是不同的字段。
建议:尽量避免在子类中定义与父类同名的字段,以免造成混淆。
构造函数与继承
构造函数不会被继承。但子类的构造函数必须在第一行调用父类的某个构造函数(显式或隐式)。
显式调用:
public class Vehicle {
public Vehicle() { }
}
public class Car extends Vehicle {
public Car() {
super(); // 调用父类构造函数
}
}
隐式调用:
如果子类构造函数未显式调用 super(),Java 编译器会自动插入对父类无参构造函数的调用。
如果父类没有无参构造函数,则子类必须显式调用父类的某个带参构造函数,否则编译失败。
嵌套类与继承
嵌套类(Nested Classes)同样遵循继承规则:
private嵌套类不会被继承;- 默认(包级)访问权限的嵌套类,仅当子类与父类在同一包中时才可访问;
protected或public嵌套类总是可被继承。
示例:
class MyClass {
class MyNestedClass { }
}
public class MySubclass extends MyClass {
public static void main(String[] args) {
MySubclass subclass = new MySubclass();
MyNestedClass nested = subclass.new MyNestedClass(); // 合法
}
}
final 类与继承
使用 final 修饰的类不能被继承:
public final class MyClass { }
// 以下代码非法:
// public class SubClass extends MyClass { } // 编译错误
抽象类与继承
抽象类(abstract class)是专门设计用来被继承的类。它不能被实例化,但可以包含抽象方法(无实现)和具体方法。
子类继承抽象类时,必须实现所有抽象方法(除非子类也是抽象类)。
抽象类的继承规则与其他类相同。
更多关于抽象类的内容,请参阅 Java 抽象类教程。