第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