您的位置 首页 java

Java核高基之:深入剖析Java继承的实现和应用(一)

0.摘要

java 通过继承(inheritance)和组合(composition)支持类重用。本教程由两部分组成,教你如何在Java程序中使用继承。在第1部分中,你将学习如何使用extends关键字从父类派生子类、调用父类构造函数和方法以及覆盖方法。在第2部分中,你会了解java.lang.Object,它是Java的超类,其他所有类都继承自该类。

要完成关于继承的学习,请参阅我的Java技巧,它解释了 何时使用组合与继承 。你会明白为什么组合是继承的重要补充,以及如何使用它来防止Java程序中的封装问题。

1. Java的两例继承

继承是一种编程结构,软件开发人员使用它来建立类别之间的 is-a 关系。继承使我们能够从更一般的类别派生出更具体的类别。更具体的类别是一种更一般的类别。例如,支票账户是一种你可以存取款的账户。同样,卡车是一种运输大型物品的具体交通工具。

继承可以通过多个层次下降,导致更加特定的类别。例如,图1显示了从vehicle(交通工具)继承的car和 truck ;旅行车wagon继承自汽车car;垃圾车继承自卡车。箭头从更具体的“子”类别(向下)指向不太具体的“父”类别(向上)。

本例演示了单继承,其中子类别继承一个直接父类别的状态和行为。相反, 多重继承 使子类别能够从两个或多个直接父类别继承状态和行为。图2中的层次结构演示了多重继承。

Java核高基之:深入剖析Java继承的实现和应用(一)

图1:继承层次图例

类别由类来描述。Java通过类扩展支持单继承,其中一个类通过扩展另一个类直接继承可访问的字段和方法。但是,Java不支持通过类扩展进行多重继承。在查看继承层次结构时,你可以通过菱形解构模式的存在轻松地检测到多重继承。图2显示了车辆、陆地车辆、水上车辆和气垫船上下文中的这种模式。

Java核高基之:深入剖析Java继承的实现和应用(一)

图2 继承关系解构

类别由类来描述。Java通过类扩展支持单继承,其中一个类通过扩展另一个类直接继承可访问的字段和方法。但是,Java不支持通过类扩展进行多重继承。在查看继承层次结构时,你可以通过菱形解构模式的存在形式而轻松地检测到多重继承。图2显示了车辆、陆地车辆、水上车辆和气垫船的上下文中的这种解构模式。

2.关键字extends

Java通过extends关键字来支持类扩展。当存在时,extends指定两个类之间的父-子关系。下面我使用extend来建立类Vehicle和Car之间的关系,然后是 Account 和SavingsAccount之间的关系:

清单-1:extends标识出父子关系

class Vehicle{
 // 成员声明
}
class Car extends Vehicle{
 // 从Vehicle继承了可访问成员
 // 这里提供自有的成员声明
}
class Account{
 // 成员声明
}
class SavingsAccount extends Account{
 // 从Account继承的可访问成员
 //这里提供自有的成员声明
}
 

extends关键字在类名之后和另一个类名之前指定。在extends之前的类名标识出子类,在extends之后的类名标识出父类。extends后不可能指定多个类名,因为Java不支持基于类的多重继承。

这些例子编写了is-a关系:Car是一个专门的车辆,SavingsAccount是一个专门的帐户。Vehicle和Account被称为基类、父类或超类。Car和SavingsAccount被称为派生类、子类或子类。

子类从父类和其他祖先继承可访问的字段和方法。但是,它们从不继承构造函数。相反,子类声明它们自己的构造函数。此外,它们可以声明自己的字段和方法,以区别于它们的父类。考虑清单2所示。

清单2:一个Account父类

class Account{
 private String name;
 private long  amount ;
 Account(String name, long amount) {
 this.name = name;
 setAmount(amount);
 }
 void deposit(long amount) {
 this.amount += amount;
 }
 String getName() {
 return name;
 }
 long getAmount() {
 return amount;
 }
 void setAmount(long amount) {
 this.amount = amount;
 }
}
 

清单2描述了一个具有名称name和初始金额amount的通用银行帐户类,它们都在构造函数中设置。此外,它还允许用户存款。(你可以通过存入负数来取款,但我们将忽略这种可能性。)注意,帐户名必须在创建帐户时设置。

注意:

货币量表示。 在表示货币值的数量时,你可能更喜欢使用double或float来存储货币值,但是这样做可能会导致不准确。要获得更好的解决方案,推荐使用BigDecimal,它是Java标准类库的一部分。

清单3显示了一个SavingsAccount子类,它扩展了它的Account父类。

请单3:继承自Account的子类SavingsAccout

class SavingsAccount extends Account{
 SavingsAccount(long amount) {
 super("savings", amount);
 }
}
 

SavingsAccount类很简单,因为它不需要声明额外的字段或方法。但是,它声明了一个构造函数来初始化Account超类中的字段。初始化发生在通过Java的super关键字调用Account的构造函数时,后面跟着一个圆括号括起来的参数列表。

何时何地调用super()

正如this()必须是调用同一类中另一个构造函数的构造函数中的第一个元素一样,super()也必须是调用其超类中的构造函数的构造函数中的第一个元素。如果违反这条规则,编译器将报告错误。如果编译器检测到方法中有super()调用,它也会报告错误。只能在构造函数中调用super()。

清单4使用CheckingAccount类进一步扩展了Account。

清单4:CheckingAccount子类扩展自Account父类

class CheckingAccount extends Account{
 CheckingAccount(long amount) {
 super("checking", amount);
 }
 void withdraw(long amount) {
 setAmount(getAmount() - amount);
 }
}
 

CheckingAccount比SavingsAccount更重要一些,因为它声明了一个withdraw()方法。注意这个方法对setAmount()和getAmount()的调用,CheckingAccount继承自Account。您不能直接访问Account中的amount字段,因为该字段被声明为私有的(private)(请参见清单2)。

super()和无参构造函数

如果子类构造函数中没有指定super(),并且超类没有声明无参数构造函数,那么编译器将报告错误。这是因为子类构造函数必须在super()不存在时调用无参超类构造函数。

3.类层次结构的例子

我创建了一个AccountDemo应用程序类,来验证Account类层次结构。首先看一下AccountDemo的源代码。

清单5:AccountDemo演示了account类层次结构

class AccountDemo
{
 public static void main(String[] args) {
 SavingsAccount sa = new SavingsAccount(10000);
  System .out.println("account name: " + sa.getName());
 System.out.println("initial amount: " + sa.getAmount());
 sa.deposit(5000);
 System.out.println("new amount after deposit: " + sa.getAmount());
 CheckingAccount ca = new CheckingAccount(20000);
 System.out.println("account name: " + ca.getName());
 System.out.println("initial amount: " + ca.getAmount());
 ca.deposit(6000);
 System.out.println("new amount after deposit: " + ca.getAmount());
 ca.withdraw(3000);
 System.out.println("new amount after withdrawal: " + ca.getAmount());
 }
}
 

清单5中的main()方法首先演示了SavingsAccount,然后是CheckingAccount。如果Account.java、SavingsAccount.java、 CheckingAccount.java和AccountDemo.java源文件位于同一个目录中,执行以下命令之一编译所有这些源文件:

javac AccountDemo.java
javac *.java
 

执行下面命令运行程序:

java AccountDemo
 

你将看到如下输出结果:

account name: savings
initial amount: 10000
new amount after deposit: 15000
account name: checking
initial amount: 20000
new amount after deposit: 26000
new amount after withdrawal: 23000
 

4.方法覆盖(和 方法重载 )

子类可以覆盖(替换)继承的方法,从而调用子类版本的方法。重写方法必须指定与被重写的方法相同的名称、参数列表和返回类型。为了演示,我在下面的Vehicle类中声明了一个print()方法。

清单6:声明要重写的print()方法

class Vehicle{
 private String make;
 private String model;
 private int year;
 Vehicle(String make, String model, int year) {
 this.make = make;
 this.model = model;
 this.year = year;
 }
 String getMake() {
 return make;
 }
 String getModel() {
 return model;
 }
 int getYear() {
 return year;
 }
 void print() {
 System.out.println("Make: " + make + ", Model: " + model + ", Year: " +year);
 }
}
 

下面在Truck类中重写print()方法:

清单7:重新print方法

class Truck extends Vehicle{
 private double tonnage;
 Truck(String make, String model, int year, double tonnage) {
 super(make, model, year);
 this.tonnage = tonnage;
 }
 double getTonnage() {
 return tonnage;
 }
 void print() {
 super.print();
 System.out.println("Tonnage: " + tonnage);
 }
}
 

Truck的print()方法具有与Vehicle的print()方法相同的名称、返回类型和参数列表。还要注意,Truck的print()方法首先通过前缀super.调用Vehicle的print()方法。

推荐:通常先执行超类逻辑,然后执行子类逻辑是一个好编码习惯。

提示:从子类方法调用超类方法

为了从覆盖子类方法中调用超类方法,要在方法的名称前面加上保留字super和成员访问操作符来调用。否则,最终将递归调用子类的覆盖方法。在某些情况下,子类将通过声明同名字段来屏蔽非私有超类字段。可以使用super和成员访问操作符访问非私有超类字段。

为了完成这个例子,我摘录了一个vehicle演示类的main()方法:

Truck truck = new Truck("Ford", "F150", 2008, 0.5);
System.out.println("Make = " + truck.getMake());
System.out.println("Model = " + truck.getModel());
System.out.println("Year = " + truck.getYear());
System.out.println("Tonnage = " + truck.getTonnage());
truck.print();
 

最后一行,truck.print();,调用了truck的print()方法。该方法首先调用Vehicle的print()来输出卡车的出厂、型号和年份;然后输出卡车的吨位。这部分输出如下所示:

Make: Ford, Model: F150, Year: 2008
Tonnage: 0.5
 

使用final来阻止方法覆盖

有时候,出于安全或其他原因,可能需要声明一个不应该被重写的方法。此时你可为此使用final关键字。为了防止覆盖,只需在方法头前面加上final,就像在最后一个字符串getMake()中一样。如果有人试图在子类中重写此方法,编译器将报告错误。

5.方法 重载 和覆盖

假设用下面的方法替换了清单7中的print()方法:

void print(String owner){
 System.out.print("Owner: " + owner);
 super.print();
}
 

修改后的Truck类现在有两个print()方法:前面明确声明的方法和继承自Vehicle的方法。void print(String owner)方法不覆盖车辆的print()方法。相反,它会重载它。

通过在子类的方法头部加上@ Override 注释,可以在编译时检测到是重载还是重写的尝试:

@Override
void print(String owner){
 System.out.print("Owner: " + owner);
 super.print();
}
 

指定@Override告诉编译器,给定的方法覆盖(重写)另一个方法。如果有人试图重载该方法,编译器将报告一个错误。如果没有这个注释,编译器就不会报告错误,因为方法重载是合法的。

何时使用@Override

养成用@Override前缀覆盖方法的习惯。这个习惯将帮助你更快地发现重载错误。

6.方法覆盖和 protected 方法

Java提供protected关键字,以便在方法重写上下文中使用(有一定指示性)。你还可以对字段使用protected。此关键字通常用于标识设计为要重写的方法。但不能偷懒都是这样设置所有方法以方便重写。

当声明protected的方法或字段时,方法或字段可被同一包中声明的任何类中的所有代码访问。子类也可以访问它,不管它们的包是什么。(我将在以后的文章中讨论包。)

结论

继承介绍的第二部分介绍Object类及其方法。每个Java类都继承自Object,因此熟悉这个超类是理解Java类层次结构的基础。还可请参阅我后续的另外一篇介绍 组合与继承 的Java编程技巧。组合为创建类之间的关系提供了一个重要的继承替代方案。它还恰好解决了继承的最大挑战之一。敬请期待。

文章来源:智云一二三科技

文章标题:Java核高基之:深入剖析Java继承的实现和应用(一)

文章地址:https://www.zhihuclub.com/169655.shtml

关于作者: 智云科技

热门文章

网站地图