Java 基础教程

Java 面向对象

Java 高级教程

Java 笔记

Java FAQ

original icon
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.knowledgedict.com/tutorial/java-inheritance.html

Java 继承


Java 中利用继承(inheritance),可以基于已存在的类构造一个新类。继承已存在的类就是复用(继承)这些类的方法和域。在此基础上,还可以添加一些新的方法和域,以满足新的需求。

类、超类和子类

我们假设一个雇员类 Employee 类。假设你在某个公司工作,这个公司中经理的待遇与普通雇员的待遇存在着一些差异。不过,他们之间也存在着很多相同的地方,例如,他们都领取薪水。只是普通雇员在完成本职任务之后仅领取薪水,而经理在完成了预期的业绩之后还能得到奖金。这种情形就需要使用继承。这是因为需要为经理定义一个新类 Manager,以便增加一些新功能。但可以重用 Employee 类中已经编写的部分代码,并将其中的所有域保留下来。从理论上讲,在 Manager 与 Employee 之间存在着明显的“is-a”(是)关系,每个经理都是一名雇员:“is-a”关系是继承的一个明显特征。

定义子类

下面是由继承 Employee 类来定义 Manager 类的格式,关键字 extends 表示继承。

public class Manager extends Employee {
    // field and method
}

注释:Java 与 C++ 定义继承类的方式十分相似。Java 用关键字 extends 代替了 C++ 中的冒号(:)。在 Java 中,所有的继承都是公有继承,而没有 C++ 中的私有继承和保护继承。

关键字 extends 表明正在构造的新类派生于一个已存在的类。已存在的类称为超类(superclass)、基类(base class)或父类(parent class);新类称为子类(subclass)、派生类(derived class)或孩子类(child class)。超类和子类是 Java 程序员最常用的两个术语,而了解其他语言的程序员可能更加偏爱使用父类和孩子类,这些都是继承时使用的术语。

尽管 Employee 类是一个超类,但并不是因为它优于子类或者拥有比子类更多的功能。实际上恰恰相反,子类比超类拥有的功能更加丰富。

覆盖方法

超类中的有些方法对子类 Manager 并不一定适用。具体来说,Manager 类中的 getSalary 方法应该返回薪水和奖金的总和。为此,需要提供一个新的方法来覆盖(override)超类中的这个方法:

public class Manager extends Employee {

    public double getSalary() {
        // get salary
    }
}

那具体如何实现呢?尽管每个 Manager 对象都拥有一个名为 salary 的域,但在 Manager 类的 getSalary 方法中并不能够直接地访问 salary 域。只有 Employee 类的方法才能够访问私有部分。如果 Manager 类的方法一定要访问私有域,就必须借助于公有的接口,Employee 类中的公有方法 getSalary 正是这样一个接口。

具体操作是对 salary 域的访问替换成调用 getSalary 方法。需要注意的是,我们希望调用超类 Employee 中的 getSalary 方法,而不是当前类的这个方法。为此,可以使用特定的关键字 super 解决这个问题。

public class Manager extends Employee {

    public double getSalary() {
        double baseSalary = super.getSalary();
        return baseSalary + bonus;
    }
}

注释:有些人认为 super 与 this 引用是类似的概念,实际上,这样比较并不太恰当。这是因为 super 不是一个对象的引用,不能将 super 赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。在子类中可以增加域、增加方法或覆盖超类的方法,然而绝对不能删除继承的任何域和方法。

子类构造器

子类调用超类的构造器,通过 super 关键字调用超类的构造器。

public class Manager extends Employee {

    private double bonus;

    public Manager(String name, double salary, int year, int month, int day) {
        super(name, salary, year, month, day);
        bonus = 0;
    }
}

注释:关键字 this 有两个用途:一是引用隐式参数,二是调用该类其他的构造器,同样,super 关键字也有两个用途:一是调用超类的方法,二是调用超类的构造器。在调用构造器的时候,这两个关键字的使用方式很相似。调用构造器的语句只能作为另 一个构造器的第一条语句出现。构造参数既可以传递给本类(this)的其他构造器,也可以传递给超类(super)的构造器。

继承层次

继承并不仅限于一个层次。例如,可以由 Manager 类派生 Executive 类。由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy)。在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链(inheritance chain)。

java 继承层次图

通常,一个祖先类可以拥有多个子孙继承链。例如,可以由 Employee 类派生出子类 Programmer 或 Secretary,它们与 Manager 类没有任何关系(有可能它们彼此之间也没有任何关系)。必要的话,可以将这个过程一直延续下去。

多态

有一个用来判断是否应该设计为继承关系的简单规则,这就是“is-a”规则,它表明子类的每个对象也是超类的对象。例如,每个经理都是雇员,因此,将 Manager 类设计为 Employee 类的子类是显而易见的,反 之不然,并不是每一名雇员都是经理。“is-a”规则的另一种表述法是置换法则。它表明程序中出现超类对象的任何地方都可以用子类对象置换。

例如,可以将一个子类的对象赋给超类变量。

Employee e;
e = new Employee(); //Employeeobjectexpected
e = new Manager(); // OK, Manager can be used as well

在 Java 程序设计语言中,对象变量是多态的。一个 Employee 变量既可以引用一个 Employee 类对象,也可以引用一个 Employee 类的任何一个子类的对象(例如,Manager、Executive、Secretary 等)。

在 Java 中,子类数组的引用可以转换成超类数组的引用,而不需要采用强制类型转换。例如,下面是一个经理数组。

Manager[] managers = new Manager[10];

将它转换成 Employee[] 数组完全是合法的:

Employee[] staff = managers; // OK

阻止继承:final 类和方法

有时候,可能希望阻止人们利用某个类定义子类。不允许扩展的类被称为 final 类。如果在定义类的时候使用了 final 修饰符就表明这个类是 final 类。例如,假设希望阻止人们定义 Executive 类的子类,就可以在定义这个类的时候,使用 final 修饰符声明。声明格式如下所示:

publi final class Executive extends Employee {
    // field and method
}

类中的特定方法也可以被声明为 final。如果这样做,子类就不能覆盖这个方法。

public class Employee {

    public final String getName(){
         // to do
    }
}

注释:域也可以被声明为 final。对于 final 域来说,构造对象之后就不允许改变它们的值了。不过,如果将一个类声明为 final,只有其中的方法自动地成为 final,而不包括域。

将方法或类声明为 final 主要目的是:确保它们不会在子类中改变语义。例如,Calendar 类中的 getTime 和 setTime 方法都声明为 final。这表明 Calendar 类的设计者负责实现 Date 类 与日历状态之间的转换, 而不允许子类处理这些问题。同样地,String 类也是 final 类,这意味着不允许任何人定义 String 的子类。换言之,如果有一个 String 的引用,它引用的一定是一个 String 对象,而不可能是其他类的对象。

抽象类

如果自下而上在类的继承层次结构中上移,位于上层的类更具有通用性,甚至可能更加抽象。从某种角度看,祖先类更加通用,人们只将它作为派生其他类的基类,而不作为想使用的特定的实例类。例如,考虑一下对 Employee 类层次的扩展。一名雇员是一个人,一名学生也是一个人。下面将类 Person 和类 Student 添加到类的层次结构中。

java 抽象结构图示例

为什么要花费精力进行这样高层次的抽象呢?每个人都有一些诸如姓名这样的属性。学生与雇员都有姓名属性,因此可以将 getName 方法放置在位于继承关系较高层次的通用超类中。

现在,再增加一个 getDescription 方法,它可以返回对一个人的简短描述。

在 Employee 类和 Student 类中实现这个方法很容易。但是在 Person类中应该提供什么内容呢?除了姓名之外,Person 类一无所知。当然,可以让 Person.getDescription() 返回一个空字符串。然而,还有一个更好的方法,就是使用 abstract 关键字,这样就完全不需要实现这个方法了。

public abstract String getDescription();

为了提高程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的

public abstract class Person {

    public abstract String getDescription();
}

除了抽象方法之外,抽象类还可以包含具体数据和具体方法。例如,Person 类还保存着姓名和一个返回姓名的具体方法。

public abstract class Person {

    private String name;

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

    public abstract String getDescription();

    public String getName() {
        return name;
    }
}

抽象方法充当着占位的角色,它们的具体实现在子类中。扩展抽象类可以有两种选择。一种是在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽象类;另一种是定义全部的抽象方法,这样一来,子类就不是抽象的了。

类即使不含抽象方法,也可以将类声明为抽象类。抽象类不能被实例化。也就是说,如果将一个类声明为 abstract,就不能创建这个类的对象。

需要注意,可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象。例如:

Person p = new Student("knowledge", "dict");

这里的 p 是一个抽象类 Person 的变量,Person 引用了一个非抽象子类 Student 的实例。

受保护访问

最好将类中的域标记为 private,而方法标记为 public。任何声明为 private 的内容对其他类都是不可见的。这对于子类来说也完全适用,即子类也不能访问超类的私有域。

然而,在有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。为此,需要将这些方法或域声明为 protected。

在实际应用中,要谨慎使用 protected 属性。假设需要将设计的类提供给其他程序员使用,而在这个类中设置了一些受保护域,由于其他程序员可以由这个类再派生出新类,并访问其中的受保护域。在这种情况下, 如果需要对这个类的实现进行修改,就必须通知所有使用这个类的程序员。这违背了 OOP 提倡的数据封装原则。

受保护的方法更具有实际意义。如果需要限制某个方法的使用,就可以将它声明为 protected。这表明子类(可能很熟悉祖先类)得到信任,可以正确地使用这个方法,而其他类则不行。

Object:所有类的超类

Object 类是 Java 中所有类的始祖,在 Java 中每个类都是由它扩展而来的。如果没有明确地指出超类,Object 就被认为是这个类的超类。由于在 Java 中,每个类都是由 Object 类扩展而来的,所以,熟悉这个类提供的所有服务十分重要。

可以使用 Object 类型的变量引用任何类型的对象:

Object obj = new Employee("Knowledge Dict", 35000);

当然,Object 类型的变量只能用于作为各种值的通用持有者。要想对其中的内容进行具体的操作,还需要清楚对象的原始类型,并进行相应的类型转换:

Employee e = (Employee) obj;

在 Java 中,只有基本类型(primitive types)不是对象,例如,数值、字符和布尔类型的值都不是对象。所有的数组类型,不管是对象数组还是基本类型的数组都扩展了 Object 类。

Employee[] staff = new Employee[10];
obj = staff; // OK
obj = new int[10]; // OK

equals 方法

Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。在 Object 类中,这个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用,它们一定是相等的。从这点上看,将其作为默认操作也是合乎情理的。然而,对于多数类来说,这种判断并没有什么意义。例如,采用这种方式比较两个 PrintStream 对象是否相等就完全没有意义。然而,经常需要检测两个对象状态的相等性,如果两个对象的状态相等,就认为这两个对象是相等的。

相等测试与继承

如果隐式和显式的参数不属于同一个类,equals 方法将如何处理呢?这是一个很有争议的问题。在前面的例子中,如果发现类不匹配,equals 方法就返冋 false。但是,许多程序员却喜欢使用 instanceof 进行检测:

if (!(otherObject instanceof Employee)) return false;

这样做不但没有解决 otherObject 是子类的情况,并且还有可能会招致一些麻烦。这就是建议不要使用这种处理方式的原因所在。Java 语言规范要求 equals 方法具有下面的特性:

  • 自反性:对于任何非空引用 x,x.equals(x) 应该返回 true。
  • 对称性:对于任何引用 x 和 y,当且仅当 y.equals(x) 返回 true,x.equals(y) 也应该返回 true。
  • 传递性:对于任何引用 x、y 和 z,如果 x.equals(y) 返回 true,y.equals(z) 返回 true,x.equals(z) 也应该返回 true。
  • 一致性:如果 x 和 y 引用的对象没有发生变化,反复调用 x.equals(y) 应该返回同样的结果。
  • 对于任意非空引用 x,x.equals(null) 应该返回 false。

注释:在标准 Java 库中包含 150 多个 equals 方法的实现,包括使用 instanceof 检测、调用 getClass 检测、捕获 ClassCastException 或者什么也不做。

下面给出编写一个完美的 equals 方法的建议:

1)显式参数命名为 otherObject,稍后需要将它转换成另一个叫做 other 的变量。

2)检测 this 与 otherObject 是否引用同一个对象:

if (this = otherObject) return true;

这条语句只是一个优化。实际上,这是一种经常采用的形式。因为计算这个等式要比一个一个地比较类中的域所付出的代价小得多。

3)检测 otherObject 是否为 null,如果为 null,返回 false。这项检测是很必要的。

if (otherObject = null) return false;

4)比较 this 与 otherObject 是否属于同一个类。如果 equals 的语义在每个子类中有所改变,就使用 getClass 检测:

if (getClass() != otherObject.getCIass()) return false;

如果所有的子类都拥有统一的语义,就使用 instanceof 检测:

if (!(otherObject instanceof ClassName)) return false;

5)将 otherObject 转换为相应的类类型变量:

ClassName other = (ClassName) otherObject

6)现在开始对所有需要比较的域进行比较了。使用 = 比较基本类型域,使用 equals 比较对象域。如果所有的域都匹配,就返回 true;否则返回 false。

return fieldl == other.field && Objects.equa1s(fie1d2, other.field2) && ... ;

如果在子类中重新定义 equals,就要在其中包含调用 super.equals(other)。

hashCode 方法

散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的。如果 x 和 y 是两个不同的对象,x.hashCode() 与 y.hashCode() 基本上不会相同。

由于 hashCode 方法定义在 Object 类中,因此每个对象都有一个默认的散列码,其值为对象的存储地址。

toString 方法

在 Object 中还有一个重要的方法,就是 toString 方法,它用于返回表示对象值的字符串。

随处可见 toString 方法的主要原因是:只要对象与一个字符串通过操作符“+”连接起来,Java 编译就会自动地调用 toString 方法,以便获得这个对象的字符串描述。例如:

Point p = new Point(10, 20);
String message = "The current position is " + p;
// automatically invokes p.toStringO