跳转至

第5章 继承

访问控制修饰符

  • private :仅对本类可见。
  • 默认(不加修饰符符) :对本包可见。
  • protected :对本包和所有子类可见。
  • public :对外部可见。
当前类 当前package 子类 外部package
public ✅ ✅ ✅ ✅
protected ✅ ✅ ✅ ❌
默认 ✅ ✅ ❌ ❌
private ✅ ❌ ❌ ❌

继承

多态与动态绑定

一个对象变量可以指示多种实际类型的现象称为 多态

例如下面代码中, Employee 类型的 e 变量即可指示 Employee 类型,也可以指示 Manager 类型。反过来则不行,即子类型的变量不能指示父类型。

class Employee {
    private String name;
    public Employee(String n) {
        name = n;
    }
    public String getName() {
        return name;
    }
    public void printProfile() {
        System.out.println("Employee: " + getName());
    }
}

class Manager extends Employee {
    public Manager(String n) {
        super(n);
    }
    public void printProfile() {
        System.out.println("Manager: " + super.getName());
    }
}

public class Main {
    public static void main(String[] args) {
        Employee e = new Employee("E1");
        e.printProfile(); // Employee: E1
        e = new Manager("M1"); // 也是可以的
        e.printProfile(); // Manager: M1
    }
}

运行时,例如e.printProfile()JVM 能够根据 e 实际指示的类型自动选择适当的方法,称为 动态绑定

动态绑定 相对的是 静态绑定静态绑定 的方法在编译期间就能准确的知道应该调用哪个方法。private 方法、static 方法、final 方法会使用 静态绑定

静态绑定

静态变量和静态方法

如果子类没有定义与父类同名的 静态变量静态方法,则调用的就是父类的 静态变量静态方法

class Employee {
    public static int idx = 123;
    public static void Hello() {
        System.out.printf("Employee: Hello! idx = %d\n", idx);
    }
}

class Manager extends Employee {
}

public class Main {
    public static void main(String[] args) {
        System.out.println(Employee.idx); // 123
        System.out.println(Manager.idx);  // 123
        Employee.Hello(); // Employee: Hello! idx = 123
        Manager.Hello();  // Employee: Hello! idx = 123
    }
}

如果在子类中定义和父类同名的 静态变量静态方法,和父类是独立的,指定子类名时会调用子类的 静态变量静态方法,指定父类名时会调用父类的 静态变量静态方法

class Employee {
    public static int idx = 123;
    public static void Hello() {
        System.out.printf("Employee: Hello! idx = %d\n", idx);
    }
}

class Manager extends Employee {
    public static int idx = 456;
    public static void Hello() {
        System.out.printf("Manager: Hello! idx = %d\n", idx);
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(Employee.idx); // 123
        System.out.println(Manager.idx);  // 456
        Employee.Hello(); // Employee: Hello! idx = 123
        Manager.Hello();  // Manager: Hello! idx = 456

        Employee e = new Manager();
        // static一定是静态绑定
        e.Hello(); // Employee: Hello! idx = 123
    }
}

使用 final 阻止继承

  • final 可以防止类被派生或方法被重写。
  • 一个 final 类的所有方法会自动成为 final 方法。
  • final 方法使用 静态绑定

private

定义和父类同名对 私有变量私有方法 也是独立的。私有方法也是 静态绑定

抽象类

使用 abstract 定义抽象类和抽象方法。

abstract class Person {
    ...
    public abstract String getDescription();
}

抽象方法不需要具体的实现,只需要在抽象类中定义。具体方法由继承了抽象父类的子类实现。

抽象类不能实例化,只有定义具体子类之后,实例化具体子类,例如:

class Employee extends Person {
    public String getDescription() {
        return "I am an Employee!";
    }
}

public class Main {
    public static void main(String[] args) {
        // 不能用 Person p = new Person();
        Person p = new Employee();
        System.out.println(p.getDescription());
    }
}

Object 类

Object 类是所有类的父类。ObjectsObject 的一个子类,Objects 类中包装了一些静态方法。

equals 方法

Object.equals() 方法用于检测一个对象是否等于另外一个对象。在 Object 中的 equals 的实现是:

public class Object {
    ......
    public boolean equals(Object obj) {
        return (this == obj);
    }
    ......
}

可见,如果没有在子类中重写 equals,默认的 equals 是比较两个对象引用是否相同。

下面是一个在 Employee 类中重写 equals 方法的例子:

public class Employee {
    private String name;
    private double salary;
    LocalDate hireDay;
    ......
    public boolean equals(Object otherObject) {
        if (this == otherObject) // 先比较引用是否相等
            return true;
        if (otherObject == null) // 然后判断 otherObject 是否为 null 
            return false;

        // 如果所有的子类都有相同的相等性语义,就使用 instanceof 检测
        // 这种情况下应该将 equals 方法设置为 final 
        // if (!(otherObject instanceof Employee))
        //     return false;
        // 这里因为 equals 的语义可以在子类中改变,所以用了 getClass 检测
        if (getClass() != otherObject.getClass()) // 判断是否属于同一个类
            return false;

        Employee other = (Employee) otherObject; // 强制转换
        return Objects.equals(name, other.name) // 然后比较所有变量是否相等
                && salary == other.salary
                && Objects.equals(hireDay, other.hireDay);
    }
    ......
}

在实际使用时,通常不会用 a.equals(b) 来判断两者是否是否相等,因为 a 可能为 null。通常使用 Objects 类封装的 equals 静态方法,它的实现是:

public final class Objects {
    ......
    public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }
    ......
}

可见,Objects.equals() 会先判断 a == null 的情况,然后再调用 a.equals(b)

hashCode 方法

Object.hashCode() 用于返回一个 int 值作为哈希值。在没有重写这个方法时,默认会用对象的存储地址计算出一个哈希值。

使用 HashMap 时,会用到 hashCode() 计算出来的值作为索引。如果要用 HashMap,除了重写 hashCode() 之外,通常还需要重写 equals 方法,哈希表插入值时首先判断 hashCode 是否相等,如果哈希值相等,再用 equals() 来判断两个索引是否相等。如果哈希值不等,则这两个索引一定不等。

可以借助 Objects.hash() 方法来计算 hashCode(),例如:

class Employee {
    private String name;
    private double salary;
    LocalDate hireDay;
    private int[] a;

    ......
    public int hashCode() {
        return Objects.hash(name, salary, hireDay, Arrays.hashCode(a));
    }
    ......
}

Objects.hash() 接受可变长度的参数,会组合所有参数计算哈希值。如果实例存在数组类型的字段,使用 Ayyays.hashCode() 计算数组的哈希值。

通常也不使用 a.hashCode() 计算哈希值,因为 a 可能为 null,可以使用 Objects.hashCode() 方法,当 a==null 时会返回 0

toString 方法

Object.toString() 方法将一个对象转化成 String,默认的实现为:

public class Object {
    ......
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
    ......
}

其中 getClass().getName() 表示所属类名的字符串,Integer.toHexString() 将一个数字转化成 16 进制串,hashCode() 则是 Object.hashCode(),如果没有在子类中重写 hashCode(),就是使用存储地址计算哈希值。

toString() 方法提供了一个很有用的特性:当对象与其他字符串通过 + 拼接,或者将对象单独作为参数传入 System.out.println() 时,会自动调用 toString() 方法得到的字符串。

动态数组 ArrayList

定义

下面三种写法都可以定义 ArrayList:

ArrayList<Employee> a = new ArrayList<Employee>();
var b = new ArrayList<Employee>(); // 使用 var 自动推导变量类型
ArrayList<Employee> c = new ArrayList<>(); // 第一个 <> 有类型,后面可以不填

使用 ensureCapacity() 方法可以分配容量,类似于 C++vectorreserve()

ArrayList<Employee> a = new ArrayList<Employee>();
a.ensureCapacity(100);

也可以在定义时直接分配容量,上述代码等价于:

ArrayList<Employee> a = new ArrayList<Employee>(100);

java 不能查看 ArrayList 的容量,只能使用 size() 查看大小。

可以使用 trimToSize() 将容量缩减到当前的大小。

增删改查

ArrayList 不能使用中括号 [] 来访问和修改元素。

  • boolean add(T obj):在末尾追加一个元素。永远返回 true
  • void add(int index, T obj):在index 位置插入元素。
  • T set(int index, T obj):更改 index 位置的元素。返回原先在 index 的元素。
  • T get(int index):查询 index 位置的元素。
  • T remove(int index):删除 index 位置的元素。返回原先在 index 的元素。

对象包装器与自动装箱

ArrayList 的存放类型必须是对象,因此存放基本类型 ArrayList<int> 是不行的。需要使用包装器,例如 IntegerLongFloatDoubleShortByteCharacterBoolean(前6个类派生于公共的超类 Number)。包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,包装器类还是 final,不能派生它们的子类。

访问包装器的值需要使用包装器的方法,不过,在使用ArrayList 等数据结构时会自动将我们的语法变换成调用包装器方法的形式,这个特性称为 自动装箱

变长参数方法

可以用 T... 作为变长数组传入方法中,在遍历时,可以用 for-each 语法或者下标遍历。

public class Main {
    public static int max(int... a) {
        int M = Integer.MIN_VALUE;
        System.out.printf("len = %d\n", a.length);
        for (int x : a)
            if (x > M)
                M = x;
        return M;
    }
    public static void main(String[] args) {
        System.out.printf("max = %d", max(2, 8, 5));
    }
}

java 允许将数组作为最后一个参数传给有可变参数的方法。


最后更新: 2023-12-03
创建日期: 2023-10-09