第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 类是所有类的父类。Objects 是 Object 的一个子类,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++ 中 vector 的 reserve()。
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> 是不行的。需要使用包装器,例如 Integer、Long、Float、Double、Short、Byte、Character 和 Boolean(前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-10-09