1. 概述
面向对象编程的核心原则之一 – 继承 – 使我们能够重用现有代码或扩展现有类型。
简单来说,在 Java 中,一个类可以继承另一个类和多个接口,而一个接口可以继承其他接口。
在本文中,我们将从继承的必要性开始,逐步了解继承在类和接口中的工作方式。
然后,我们将涵盖变量/方法名和访问修饰符如何影响继承的成员。
最后,我们将了解继承一个类型意味着什么。
2. 继承的必要性
想象一下,作为一家汽车制造商,你向客户提供多种汽车型号。 即使不同的汽车型号可能提供不同的功能,例如天窗或防弹窗,它们都将包含通用的组件和功能,例如发动机和车轮。
有意义的是 创建一个基本设计并扩展它以创建它们的专业版本,而不是从头开始单独设计每个汽车型号。
以类似的方式,通过继承,我们可以创建一个具有基本功能和行为的类,并通过创建继承该基本类的类来创建它的专业版本。 同样,接口可以扩展现有接口。
我们会注意到使用多个术语来指被另一个类型继承的类型,特别是
- 基本类型也称为超类或父类
- 派生类型称为扩展、子类或子类型
3. 类继承
3.1. 扩展一个类
一个类可以继承另一个类并定义额外的成员。
让我们从定义一个基本类 Car 开始
public class Car {
int wheels;
String model;
void start() {
// Check essential parts
}
}
类 ArmoredCar 可以通过在声明中使用关键字 extends 来继承 Car 类的成员
public class ArmoredCar extends Car {
int bulletProofWindows;
void remoteStartCar() {
// this vehicle can be started by using a remote control
}
}
现在我们可以说 ArmoredCar 类是 Car 的子类,后者是 ArmoredCar 的超类。
Java 中的类支持单继承;ArmoredCar 类不能扩展多个类。
另外,请注意,如果没有 extends 关键字,则一个类会隐式继承类 java.lang.Object。
子类继承超类中的非静态 protected 和 public 成员。 此外,如果这两个类在同一个包中,则具有 default(包私有)访问权限的成员也会被继承。
另一方面,类的 private 和 static 成员不会被继承。
3.2. 从子类访问父成员
要访问继承的属性或方法,我们可以直接使用它们
public class ArmoredCar extends Car {
public String registerModel() {
return model;
}
}
请注意,我们不需要超类的引用来访问其成员。
4. 接口继承
4.1. 实现多个接口
虽然类只能继承一个类,但可以实现多个接口。
想象一下,我们在前一节中定义的 ArmoredCar 需要一个超级间谍。 因此,汽车制造公司考虑添加飞行和浮动功能
public interface Floatable {
void floatOnWater();
}
public interface Flyable {
void fly();
}
public class ArmoredCar extends Car implements Floatable, Flyable{
public void floatOnWater() {
System.out.println("I can float!");
}
public void fly() {
System.out.println("I can fly!");
}
}
在上面的示例中,我们注意到使用关键字 implements 从接口继承。
4.2. 多重继承的问题
Java 允许使用接口进行多重继承。
直到 Java 7,这并不是一个问题。接口只能定义抽象方法,即没有实现的任何方法。因此,如果一个类实现了具有相同方法签名的多个接口,这并不是一个问题。实现类最终只需要实现一个方法。
让我们看看这个简单的等式是如何随着 Java 8 中接口引入默认方法而改变的。
从 Java 8 开始,接口可以选择为其方法定义默认实现(接口仍然可以定义抽象方法)。这意味着如果一个类实现了多个定义了相同签名的方法的接口,子类将继承单独的实现。这听起来很复杂,是不允许的。
Java 不允许继承来自不同接口的相同方法的多个实现。
这里有一个例子
public interface Floatable {
default void repair() {
System.out.println("Repairing Floatable object");
}
}
public interface Flyable {
default void repair() {
System.out.println("Repairing Flyable object");
}
}
public class ArmoredCar extends Car implements Floatable, Flyable {
// this won't compile
}
如果我们想实现这两个接口,我们将不得不覆盖repair()方法。
如果前面的例子中的接口定义了具有相同名称的变量,例如duration,我们不能在不使用接口名称作为前缀的情况下访问它们。
public interface Floatable {
int duration = 10;
}
public interface Flyable {
int duration = 20;
}
public class ArmoredCar extends Car implements Floatable, Flyable {
public void aMethod() {
System.out.println(duration); // won't compile
System.out.println(Floatable.duration); // outputs 10
System.out.println(Flyable.duration); // outputs 20
}
}
4.3. 接口扩展其他接口
一个接口可以扩展多个接口。这是一个例子
public interface Floatable {
void floatOnWater();
}
interface interface Flyable {
void fly();
}
public interface SpaceTraveller extends Floatable, Flyable {
void remoteControl();
}
接口使用关键字extends继承其他接口。类使用关键字implements继承接口。
5. 继承类型
当一个类继承另一个类或接口时,除了继承它们的成员外,它还继承它们的类型。这也适用于继承其他接口的接口。
这是一个非常强大的概念,它允许开发人员编程到接口(基类或接口),而不是编程到它们的实现。
例如,想象一下这种情况,一个组织维护着其员工拥有的汽车列表。当然,所有员工可能拥有不同的汽车型号。那么我们如何引用不同的汽车实例呢?这里有一个解决方案
public class Employee {
private String name;
private Car car;
// standard constructor
}
由于Car的所有派生类都继承了类型Car,因此可以使用类Car的变量引用派生类实例。
Employee e1 = new Employee("Shreya", new ArmoredCar());
Employee e2 = new Employee("Paul", new SpaceCar());
Employee e3 = new Employee("Pavni", new BMW());
6. 隐藏类成员
6.1. 隐藏实例成员
如果超类和子类定义了具有相同名称的变量或方法怎么办?不用担心;我们仍然可以访问它们。但是,我们必须通过在变量或方法前加上关键字this或super,向 Java 明确我们的意图。
this关键字指的是在其使用它的实例。super关键字(顾名思义)指的是父类实例
public class ArmoredCar extends Car {
private String model;
public String getAValue() {
return super.model; // returns value of model defined in base class Car
// return this.model; // will return value of model defined in ArmoredCar
// return model; // will return value of model defined in ArmoredCar
}
}
许多开发人员使用this和super关键字来明确他们引用的变量或方法。但是,在所有成员上使用它们会使我们的代码看起来杂乱无章。
6.2. 隐藏静态成员
如果我们的基类和子类定义了具有相同名称的静态变量和方法怎么办?我们是否可以像访问实例变量一样,在派生类中从基类访问静态成员?
让我们用一个例子来了解一下
public class Car {
public static String msg() {
return "Car";
}
}
public class ArmoredCar extends Car {
public static String msg() {
return super.msg(); // this won't compile.
}
}
不,我们不能。静态成员属于类,不属于实例。因此,我们不能在msg()中使用非静态的super关键字。
由于静态成员属于类,我们可以将前面的调用修改如下
return Car.msg();
考虑以下示例,其中基类和派生类都定义了具有相同签名的静态方法msg()
public class Car {
public static String msg() {
return "Car";
}
}
public class ArmoredCar extends Car {
public static String msg() {
return "ArmoredCar";
}
}
以下是如何调用它们
Car first = new ArmoredCar();
ArmoredCar second = new ArmoredCar();
对于前面的代码,first.msg()将输出“Car“,而second.msg()将输出“ArmoredCar”。调用的静态消息取决于用于引用ArmoredCar实例的变量类型。
7. 结论
在本文中,我们介绍了一种 Java 语言的核心方面——继承。
我们了解了 Java 如何通过类支持单继承,通过接口支持多继承,并讨论了该机制在语言中的复杂性。
支持本文的代码可在 GitHub 上获取。 一旦你以 Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。















