IntelliJ IDEA Java

简单攻略:在 Java 中创建继承

Read this post in other languages:

在 Java 和其他编程语言中,继承是面向对象编程的基本特性之一。 借助继承,您可以创建从类(基类或超类)派生的类,并重用、扩展或修改超类的行为。 这一原则允许您构建类层次结构和重用现有代码。

Java 本身到处都使用继承:许多 JDK 类继承其他类,并且 Java 中的每个类都隐式扩展 java.lang.Object。 本文不会过多关注这一部分,而主要举例说明如何在代码中使用继承。

假设,您想要在应用程序中创建 EmployeeCustomer 类。 借助继承,您可以编写这两个类,使其从父 Person 类继承 nameaddress 属性。 在代码可重用性和模块化方面,这有多个好处,本文将具体介绍。

什么是 Java 中的继承?

仍然是前言中的示例:具有 PersonEmployeeCustomer 类的层次结构。 Person 类带有一个 name 属性、该属性的 getter 和一个构造函数:

public class Person {
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

如果要在代码中使用 Person 类,可以从中实例化一个新对象,例如,将其名称打印到控制台:

var person = new Person("Maarten");
System.out.println(person.getName());

// Prints: Maarten

Employee 类需要一个 name 和一个 employeeId 属性。 由于 Employee 与人共享许多特征(至少在机器人抢走所有工作之前),您可以重用 Person 类作为基础。

在 Java 中,基类通常被称为超类,而继承自它的类被称为子类。 您也可能遇到其他术语用法,例如使用父类指代超类。

Employee 类为例,它继承自 Person 类:

public class Employee extends Person {
    private final int employeeId;

    public Employee(String name, int employeeId) {
        super(name);
        this.employeeId = employeeId;
    }

    public int getEmployeeId() {
        return employeeId;
    }
}

有几点需要展开说明。 首先,extends 关键字用于告诉 Java 编译器 Employee extends Person。 接下来,在 Employee 构造函数中,可以看到 super(name) 语法将 name 形参传递到超类构造函数中,使底层 Person 可以存储此属性值。

有趣的是,因为我们使用继承,Employee 不必实现 name。 然而,Employee 类现在公开两个属性:nameemployeeId

var employee = new Employee("Maarten", 123);
System.out.printf("%s (%s)", employee.getName(), employee.getEmployeeId());

// Prints: Maarten (123)

类似地,您可以创建一个同样扩展 PersonCustomer 类,并添加 String 类型的 customerId 属性:

public class Customer extends Person {
    private final String customerId;

    public Customer(String name, String customerId) {
        super(name);
        this.customerId = customerId;
    }

    public String getCustomerId() {
        return customerId;
    }
}

使用 Customer 类时,您将从 Person 超类获取 name 属性及其 getter:

var customer = new Customer("Maarten", "ABC");
System.out.printf("%s (%s)", customer.getName(), customer.getCustomerId());

// Prints: Maarten (ABC)

如此示例所示,继承让您可以在任何子类中重用超类的属性和方法。

如果您使用 IntelliJ IDEA Ultimate,可以从这些类创建 UML 图并直观呈现我们刚刚创建的层次结构。 在 Windows/Linux 上使用 Ctrl+Alt+Shift+D 快捷键,或在 macOS 上使用 ⌘Сmd+⌥Opt+⇧Shift+U,或者使用 Project(项目)工具窗口中软件包或类上的上下文菜单:

提示:使用工具栏中的图标切换是否显示字段、构造函数、方法或属性。

在某些情况下,您可能想有一个无法实例化的通用超类和可以实例化的子类。 如果有通用属性或方法,这很有用,但如果没有子类,直接使用超类也没有意义。 在 Java 中,您可以使用 abstract 修饰符告诉编译器可以使用类型名称并且可以继承类,但您不会想直接创建新对象:

public abstract class Person { } // can be inherited from, but not instantiated
new Person(); // does not work
public class Employee extends Person { } // can be inherited from & instantiated
new Employee(); // works
public class Customer extends Person { } // can be inherited from & instantiated
new Customer(); // works

除了抽象类外,您还可以定义和继承接口。 虽然抽象类通常应包含其某些成员的基本实现,但接口更加轻量。 接口定义了其子类上可用的成员,但不添加实现。 它们很像一个协定。

如果将 Person 重写为接口,则可以向代码的使用者传达 Person 实现将具有 name 属性的 getter。 不过,没有该 getter 的实现,也没有包含该名称的支持字段。 相反,接口的实现必须提供:

public interface Person {
  String getName(); // consumers can expect this getter to exist, and subclasses have to provide an implementation
}

class Employee implements Person {

  @Override
  public String getName() {
      return ""; // may need a backing field, constructor, ...
  }
}

注意,这里不是使用 extends 关键字,而是使用 implements 关键字继承接口。

下面我们来点更有意思的!

重写 Java 类的方法

在类层次结构中,子类可以重写超类的行为。 我们稍微扩展一下示例,向 Employee 类添加一个 title 属性及其 getter。

提示:在 Employee 的构造函数中添加 String title 作为实参时,您可以使用 Alt+Enter (Windows/Linux) 或 ⌥Opt+Enter (macOS) 让 IntelliJ IDEA 创建字段并更新现有用法:

更新后的 Employee

public class Employee extends Person {
    private final String title;
    private final int employeeId;

    public Employee(String name, String title, int employeeId) {
        super(name);
        this.title = title;
        this.employeeId = employeeId;
    }

    public int getEmployeeId() {
        return employeeId;
    }

    public String getTitle() {
        return title;
    }
}

Employee 类现在可以代表各位员工,包含员工的头衔和 ID:

var employee1 = new Employee("Maarten", "Developer Advocate", 123);
var employee2 = new Employee("Hadi", "Developer Advocate", 456);

就层次结构而言,Employee 是一个很好的例子。 但问题是:Hadi 实际是团队主管,而不仅仅是 Employee。 怎样才能体现这一点呢? 使用更多继承! 我们来引入一个继承自 EmployeeTeamLead 子类,并且始终给 title 的 getter 返回值加上“(team lead)”后缀:

public class TeamLead extends Employee {

    public TeamLead(String name, String title, int employeeId) {
        super(name, title, employeeId);
    }

    @Override
    public String getTitle() {
        return super.getTitle() + " (team lead)";
    }
}

TeamLead 类的实现非常简单:它重用 Employee 超类中的所有代码。 不过,它会重写 getTitle() getter 实现:

  • @Override 注解通知 Java 编译器您正在重写超类中的方法。 这有助于编译器在方法不存在或不具有与基类相同的签名时发出警告。
  • 使用 super.getTitle()Employee 调用 getter,然后重写版本,在返回值之前附加一个后缀。

您可以使用 TeamLead 类并查看打印到控制台的后缀:

var employee = new TeamLead("Hadi", "Developer Advocate", 456);

System.out.printf("%s (%s) - %s", employee.getName(), employee.getEmployeeId(), employee.getTitle());

// Prints: Hadi (456) - Developer Advocate (team lead)

使用 final 修饰符防止继承

俗话说:“能力越大,责任越大”,继承也是如此。 一个超类可以有无限个子类,但这不一定是理想选择。 在我们的示例中,您可能想阻止 TeamLead 拥有更多子类或完全阻止从某个类继承。

在 Java 中,您可以使用 final 修饰符设置可扩展性的限制。 您可能已经注意到 final 被用于 Person 中的 name 属性,使其成为“只读”并避免被重新赋值。

final 修饰符也可以与继承一起使用。 当 final 应用于类时,将无法从它继承。 例如,向 Employee 类添加 final 时,我们在上一部分中创建的 TeamLead 类将不再编译,IntelliJ IDEA 还会显示一条错误消息,告诉您类不能从 Employee 继承:

您还可以将 final 修饰符添加到类层次结构中的方法。 例如,如果您想防止 getTitle() getter 被重写,您可以将其设为 final

如果要防止一个类被扩展并将继承限制为几个已知的子类,您还可以使用 sealed 修饰符和 permits 子句:

public sealed class Person permits Employee { } // no other classes than Employee can extend Person

如果您想在代码中使用 sealed 类,IntelliJ IDEA 将帮助您根据现有类层次结构生成 permits 子句,并相应地更新层次结构中的所有类:

使用 IntelliJ IDEA 密封类

继承和访问级别修饰符

Java 有多个访问修饰符,可用于控制类、构造函数、属性、方法和其他成员的作用域。

对于类,有两个访问级别修饰符:public,使类在任何位置都可用;package-private,使类仅在定义它的软件包中可用。

在继承和类层次结构上,任何位置都可以继承 public 类 – 您自己的代码库的所有软件包和模块中,以及引用代码中。 package-private 类只能在同一个软件包内继承,并且在此软件包外不可用。 也就是说,除非它们具有 final 修饰符,否则继承完全不被允许。

public class A { } // available everywhere, can be inherited from in any package
class B { } // available in the current package, can be inherited from in the current package

在成员级别,同样的原则适用:您可以使用 public 修饰符使成员在任何位置都可用,也可以使用 private 使其仅在同一个类中可用。 您也可以不使用修饰符,这将使成员在成员所在软件包中可用。

此外,您可以使用 protected 修饰符允许子类访问特定成员。

public class C { // available everywhere, can be inherited from in any package
  private int a; // available only within class C
  public void b() { } // available everywhere
  protected void c() // available only within class C and its subclasses in any package
}

以下是访问级别以及具体类成员可用位置的快速总结:

可用位置: 软件包 同一软件包的子类 另一个软件包的子类 任何地方
public
protected  
package-private(无修饰符)    
private        

回到我们的示例,您可以看到 name 字段为 private,这意味着任何子类都必须使用 getName() getter 访问它的值。 如果将其设为 protectedname 字段将在 Person 的所有子类中可用。

虽然所有访问级别修饰符都适用于定义类及其成员的可用作用域,但 protected 修饰符在通过继承创建类层次结构时非常有用。 超类可以定义 protected 字段或方法,这些字段或方法可以由子类访问,但不能由这些类的使用者访问。

继承的缺点

继承有很多好处,例如重用代码和扩展现有功能。 通用属性和行为可以在超类中定义,子类可以继承和基于它们构建,并添加或重写功能。

但也有一些缺点,例如类之间的紧密耦合。 对超类的任何更改都会影响所有子类。 拥有较大的类层次结构可能增加复杂程度,尤其是在涉及多个继承级别的大型项目中。

总结

在这篇博文中,我们将继承视为 Java 编程中的一个基本概念。 继承允许通过使用 extends 关键字从现有类(超类)扩展属性和行为来创建新类(子类)。 我们还讨论了抽象类和接口。

我们还提到了 final 修饰符,它用于防止类的继承或防止重写特定类成员。 将其与访问级别修饰符相结合,您可以定义可以继承类的位置以及类成员的可用位置。

IntelliJ IDEA 中尝试一下

本博文英文原作者:

image description

Discover more