3. 继承

关键字 extends 表明正在构造的新类派生于一个已存在的类。 已存在的类称为超类( super class)、 基类( base class) 或父类(parent class); 新类称为子类(subclass、) 派生类( derived class) 或孩子类(child class)。 超类和子类是 Java 程序员最常用的两个术语。

前缀“ 超” 和“ 子” 来源于计算机科学和数学理论中的集合语言的术语。所有雇员组成的集合包含所有经理组成的集合。可以这样说, 雇员集合是经理集合的超集, 也可以说,经理集合是雇员集合的子集。

一个子类只能继承一个超类,而一个子类可以实现多个接口。

3.1. 继承设计的技巧

  1. 将公共操作和域放在超类

    • 这就是为什么将姓名域放在 Person类中,而没有将它放在 Employee 和 Student 类中的原因

  2. 不要使用受保护的域

    • protected 机制并不能够带来更好的保护,某个类的子类和或与其同一个包中的所有类都可以访问它 proteced 域,这样会破坏类的封装性。

  3. 使用继承实现 “is-a” 关系

    • 教师和人属于 is-a 关系,所以 Teacher 可以继承 Person ; 而学生和教师不属于 is-a 关系,所以不可继承。

  4. 除非所有继承的方法都有意义,否则不要使用继承

    • 并非所有类都适合作为超类被继承,假如继承一个类会使得其子类封装性出现问题,那么就不要使用该类作为其“子类”的超类。

  5. 在覆盖方法时,不要改变预期的行为

    • 关键在于, 在覆盖/重写子类中的方法时,不要偏离最初的设计想法

  6. 使用多态, 而非类型信息

    if (x is of type 1)
        action1();
    else if (x is of type 2)
        action2();
    /*
    * 对于以上这种情况都应当考虑使用多态。最终简化为 x.action(),
    * 以便使用多态性提供的动态分派机制执行相应的动作。
    */
    
  7. 不要过多地使用 反射

    • 反射机制使得人们可以通过在运行时查看域和方法, 让人们编写出更具有通用性的程序。这种功能对于编写系统程序来说极其实用,但是通常不适于编写应用程序。反射是很脆弱的,即编译器很难帮助人们发现程序中的错误, 因此只有在运行时才发现错误并导致异常。

3.2. instanceof 关键字 1

严格来说 instanceof 是 Java 中的一个双目运算符,由于它是由字母组成的,所以也是 Java 的保留关键字。在 Java 中可以使用 instanceof 关键字判断一个对象是否为一个类(或接口、抽象类、父类)的实例。语法格式如下所示:

boolean result = obj instanceof Class

其中,obj 是一个对象,Class 表示一个类或接口。obj 是 class 类(或接口)的实例或者子类实例时,结果 result 返回 true,否则返回 false。

Java instanceof 关键字的几种用法:

  • 声明一个 class 类的对象,判断 obj 是否为 class 类的实例对象

Integer integer = new Integer(1);
System.out.println(integer instanceof  Integer);    // true
  • 声明一个 class 接口实现类的对象 obj,判断 obj 是否为 class 接口实现类的实例对象

ArrayList arrayList = new ArrayList();
System.out.println(arrayList instanceof List);    // true
  • obj 是 class 类的直接或间接子类

Integer integer = new Integer(1);
System.out.println(integer instanceof Number);    // true
System.out.println(integer instanceof Object);    // true

警告

对于 obj instanceof class :

obj 的类型必须是引用类型或空类型,否则会编译错误。class 只能是类或者接口,当其为 null 时,会发生编译错误。

3.2.1. 三目运算

instanceof 也经常和三目(条件)运算符一起使用,代码如下:

A instanceof B ? A : C

public class Test {
    public Object animalCall(Animal a) {
        String tip = "这个动物不是牛!";
        // 判断参数a是不是Cow的对象
        return a instanceof Cow ? (Cow) a : tip;
    }

    public static void main(String[] args) {
        Sheep sh = new Sheep();
        Test test = new Test();
        System.out.println(test.animalCall(sh));
    }
}

class Animal {

}

class Cow extends Animal {

}

class Sheep extends Animal {

}

3.3. Object: 所有类的超类

Object 类是 Java 中所有类的始祖, 在 Java 中每个类都是由它扩展而来的。但是并不需要这样写:

public class Employee extends Object

如果没有明确地指出超类,Object 就被认为是这个类的超类。故而可以使用 Object 类型的变量引用任何类型的对象:

Object obj = new Employee(Harry Hacker", 35000);

由于在 Java中,每个类都是由 Object 类扩展而来的,所以, 熟悉这个类提供的所有服务十分重要。

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

3.3.1. equal()

Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。在 Object 类中,这个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用, 它们一定是相等的。然而,对于多数类来说, 这种判断并没有什么意义。

例如, 如果两个雇员对象的 ID 一样, 就认为它们是相等的(利用下面这个示例演示 equals 方法的实现机制):

public class Employee {

    private String id;
    private String name;
    private double salary;
    private LocalDate hireDay;

    /* ... */

    @Override
    public boolean equals(Object o) {
        // 快速判断;因为当两个对象有相同引用时必定是相等的
        if (this == o) {
            return true;
        }
        // 比较对象不为空且比较的对象属于同一个类时才成立
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        // 属性值比较
        Employee employee = (Employee) o;
        return Objects.equals(id, employee.id);
        /*
         * 当然不排除有特殊需求;例如子类对象比较父类对象,
         * 只要父对象拥有的属性部分相同即可判定两个对象相同, 如
        if (o instanceof Employee) {
            // 属性值比较
            Employee employee = (Employee) o;
            return Objects.equals(id, employee.id);
        } else {
            return false;
        }
         */
    }
}

对于如何实现 equals ,可以从两个截然不同的情况看一下这个问题:

  • 如果子类能够拥有自己的相等概念, 则对称性需求将强制采用 getClass 进行检测 【因为要确定比较双方是否为同一个类】

  • 如果由超类决定相等的概念,那么就可以使用 instanceof 进行检测, 这样可以在不同子类的对象之间进行相等的比较。【因为需要判断比较对象是否属于超类的实例或其子类对象】

注意

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

有特殊要求的对象比较:

Employee 作为基类,Manager 继承 Employee;Staff 类比较时使用 Employee 。(需要注意的是,Staff 类的 equals 实现是不符合 Java 语言规范的)

以下为类源码:

public class Main {
    public static void main(String[] args) {
        Employee eugene = new Employee("1244", "eugene", 2000, 2021, 8, 10);
        Manager forest = new Manager("1244", "eugene forest", 2000, 2021, 8, 10);
        forest.setBonus(5000);
        Staff staff = new Staff("1244", "eugene_forest", 2000, 2021, 8, 10);
        if (forest.equals(eugene) && eugene.equals(forest)) {
            System.out.println("forest and eugene");
        }
        if (staff.equals(eugene) && staff.equals(forest)) {
            System.out.println("equal");
        }
        if (eugene.equals(staff) || forest.equals(staff)) {
            System.out.println("eugene or forest equal staff");
        } else {
            System.out.println("eugene and forest not equal staff");
        }
    }
}

// result code
/*
forest and eugene
equal
eugene and forest not equal staff
*/

3.3.2. hashCode 方法

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

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

Equals 与 hashCode 的定义必须一致:如果 x.equals(y) 返回 true, 那么 x.hashCode( ) 就必须与 y.hashCode( ) 具有相同的值。

//hashCode 实现样例
@Override
public int hashCode() {
    return Objects.hash(id, name, salary, hireDay);
}

3.3.3. toString()

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

绝大多数(但不是全部)的 toString方法都遵循这样的格式:类的名字,随后是一对括号括起来的域值。下面是 Employee 类中的 toString 方法的实现:

@Override
public String toString() {
    return "Staff{" +
            "id='" + id + '\'' +
            ", name='" + name + '\'' +
            ", salary=" + salary +
            ", hireDay=" + hireDay +
            '}';
}

Object 类定义了 toString 方法, 用来打印输出对象所属的类名和散列码(hashCode)。例如, 调用 System.out.println(System.out); 将输出内容 java.io.PrintStream@74a14482,之所以得到这样的结果是因为 PrintStream 类的设计者没有覆盖 toString 方法。


1

Java instanceof 关键字详解 : http://c.biancheng.net/view/6346.html

2

此观点需要验证!!笔者对此有疑惑。