Java核心技术1-Object类

  |  

摘要: 本文是《Java核心技术 10th》中 Object 类的要点总结

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


本文是《Java核心技术1》第10版 【Chap5 继承】中关于 Object 类的要点总结。由于此前在 C++ 中已经接触过面向对象编程,这里主要关注 Java 与 C++ 有区别的地方。


Object 所有类的超类

如果没有明确地指出超类,Object 就被认为是这个类的超类。在 Object 中有几个只在处理线程时才会被调用的方法,具体参考线程的内容。

可以用 Object 类型的变量引用任何类型的对象,Java 中只有基本类型不是对象。Object 类型的变量只能用于作为各种值的通用持有者。要想对其中的内容进行具体的操作,还需要清楚对象的原始类型,并进行相应的类型转换

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

C++ 和 Java 中关于 Object 类的区别:

在 C++ 中没有所有类的根类,不过,每个指针都可以转换成 void* 指针。

Equals 方法、相等测试与继承

Object.equals 方法用于检测一个对象是否等于另一个对象,具体是判断两个对象是否具有相同的引用。

对于具体的类如何判断相等,最好自己定义,以 Employee 为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Employee {
public boolean equals(Object otherObject) {
// 判断两个对象是否是同一个对象
if(this == otherObject)
return true;

// 判断 otherObject 是否为空
if(otherObject == null)
return false;

// 判断类型是否匹配
if(getClass() != otherObject.getClass())
return false;

// 此时 otherObject 是一个非空的 Employee 类对象
Employee other = (Employee) otherObject;

// this 和 otherObject 的各个域是否有相同值
return Object.equals(name, other.name) // String 的 equals
&& salary == other.salary // 基本类型不是 Object 类
&& Object.equals(hireDay, other.hireDay) // LocalDate 的 equals
}
}

getClass 方法返回对象所属的类,只有两个对象属于同一个类,才可能相等。

name 和 hireDay 不是基本类型,因此可能为 null。需要使用 Object.equals 方法。

  • 如果两个参数都为 null, Object.equals(a, b) 返回 true。
  • 如果其中一个参数为 null, Object.equals(a, b) 返回 false。
  • 如果两个参数都不为 null, 调用 a.equals(b)。

子类中定义 equals 时,首先调用超类的 equals。

1
2
3
4
5
6
7
8
public class Manager extends Employee {
public boolean equals(Object otherObject) {
if(!super.equals(otherObject))
return false;
Manager other = (Manager) otherObject;
return bunus == other.bonus;
}
}

相等的特性

Java 语言规范要求 equals 满足以下特性

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

getClass 与 instanceof 的适用场景

自己实现 equals 时,隐式和显式参数不属于同一个类给如何处理是主要的问题。前面的做法是如果发现类不匹配,就返回 false,例如前面的 Employee 中的 getClass() 的判断。

1
2
if(getClass() != otherObject.getClass())
return false;

除了 otherObject.getClass() 之外,还有 instanceof 的方法,如下

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

但是注意,instanceof 没有解决 otherObject 是子类的情况,并且还会引发不满足相等的对称性特性的情况。

适合 getClass 的场景

下面我们看一下 Employee.equals 用 instanceof 检测的话如何会违反对称性

考虑 e.equals(m),e 是 Employee,m 是 Manager,它们的各个域的值都一样。

如果用 m instanceof Employee,则返回 true,m.equals(e) 时,e instanceof Manager 为 false,违背对称性,抛出异常。

适合 instanceof 的场景

但是也有时,我们认为不应该用 getClass 检测,因为不符合置换原则(只要父类出现的地方子类就能够出现,而且替换为子类不会产生任何错误或异常。但是反过来,子类出现的地方,替换为父类就可能出现问题了)。

例如 AbstractSet 类的 equals 方法,它检测两个集合是否有相同的元素。它们分别使用不同的算法实现查找集合元素的操作。无论集合采用何种方式实现,都需要拥有对任意两个集合进行比较的功能。

AbstractSet 有两个子类:TreeSet 和 HashSet。也就是一个 TreeSet 实例与一个 HashSet 实例是可以相等的,此时用 instanceof 更合理。这里比较特殊,因为没有任何一个子类要重新定义集合是否相等,AbstractSet.equals 应该声明为 final,但 AbstractSet.equals 并没有声明 final,这样子类可以选择更有效的算法对集合检测是否相等。

总结

  • 如果子类能够拥有自己的相等概念(例如 Manager),则对称性要求必须用 getClass 检测。
  • 如果由超类决定相等的概念,那么可以在超类中用 instanceof 进行检测,这样可以在不同子类的对象之间进行相等的比较。

Java 标准库中包含 150 多个 equals 实现,包括用 instanceof 检测、用 getClass 检测、捕获 ClassCastException 或什么也不做。

自己实现 equals 的建议

  • 显式参数命名为 otherObject,稍后需要将它转换成另一个叫做 other 的变量。
  • 检测 this 与 otherObject 是否引用同一个对象。
    1
    2
    if(this == otherObject)
    return true;
  • 检测 otherObject 是否为 null,若为 null 返回 false。
    1
    2
    if(otherObject == null)
    return false;
  • 比较 this 与 otherObject 是否属于同一类。
    如果 equals 的语义在某个子类中有改变,则使用 getClass 检测:
    1
    2
    if(getClass() != otherObject.getClass())
    return false;
    如果所有 equals 都有统一的语义,则使用 instanceof 检测:
    1
    2
    if(!(otherObject instanceof ClassName))
    return false;
  • 将 otherObject 转换为相应的类类型变量。
    1
    ClassName other = (ClassName) otherObject;
  • 对需要比较的域进行比较,用 == 比较基本类型域,用 equals 比较对象域。

如果在子类中重新定义 equals,则要包含 super.equals(other)。且最好加 @override 标记。

1
2
3
4
5
6
public class Employee {
@override
public boolean equals(Object other) {
...
}
}

注意形参类型应该为 Object,如果不是,则加了 @override 后会报错,因为没有覆盖超类 Object 超类的任何方法。

对于数组类型的域,可以使用静态的 Arrays.equals 方法检测相应的数组元素是否相等。

1
static Boolean equals(type[] a, type[] b)

如果长度相同且对应位置元素也相同就返回 true,元素类型可以是 Object, int, long, short, char, byte, boolean, float, double。

hashCode 方法

String 类使用下列算法计算散列码。

1
2
3
4
int hash = 0;
for(int i = 9; i < length(); ++i) {
hash = 31 * hash + charAt(i);
}

hashCode 方法定义在 Object 类中,默认的散列吗是对象的存储地址。

关于默认散列码,下面是一个 String 和 StringBuilder 对比的例子。

1
2
3
4
5
6
7
8
9
10
public class StringTest {
public static void main(String[] args) {
String s = "OK";
StringBuilder sb = new StringBuilder(s);
System.out.println(s.hashCode() + " " + sb.hashCode());
String t = new String("OK");
StringBuilder tb = new StringBuilder(t);
System.out.println(t.hashCode() + " " + tb.hashCode());
}
}

结果如下

1
2
2524 225534817
2524 664740647

可以看到 s, t 的散列码一样,因为 String 定义了 hashCode 方法,是从内容算的散列码。

而 sb 和 tb 的散列码不一样,因为 StringBuilder 没有定义 hashCode 方法,其散列码是 Object 类的默认 hashCode 也就是对象地址。

如果要把对象插入到散列表,那么如果重新定义 equals 方法,就必须重新定义 hashCode 方法。

自己的类中实现 HashCode

在自己的类中实现 hashCode 方法应该返回一个整型数值(也可以是负数),注意合理组合实例域的散列码,让不同对象的散列码更均匀。下面是一些常见写法

基本写法如下

1
2
3
4
5
6
7
public class Employee {
public int hashCode() {
return 7 * name.hashCode()
+ 11 * new Double(salary).hashCode()
+ 13 * hireDay.hashCode();
}
}

还可以优化

  1. 用 null 安全的 Objects.hashCode(),如果参数为 null 返回 0,否则返回对参数调用 hashCode() 的结果。
  2. new Double(salary).hashCode() 可以改为用静态方法避免创建 Double 对象 Double.hashCode(salary)
1
2
3
4
5
6
7
8
9
import java.util.Objects

public class Employee {
public int hashCode() {
return 7 * Objects.hashCode(name)
+ 11 * new Double(salary).hashCode()
+ 13 * Objects.hashCode(hireDay);
}
}

需要组合多个散列值时,还可以调用 Objects.hash 并提供多个参数。这个方法会对各个参数调用 Objects.hashCode,并组合这些散列值。

1
2
3
4
5
public class Employee {
public int hashCode() {
return Objects.hash(name, salary, hireDay);
}
}

equals 与 hashCode 的一致性: 如果 x.equals(y) 为 true,则 x.hashCode() 和 y.hashCode() 必须有相同的值。

如果有数组类型的域,可以用 Arrays.hashCode 方法算一个散列码,由数组元素的散列码组成。

toString 方法

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

1
2
3
4
5
6
7
8
9
public class Employee {
public String toString() {
return getClass().getName()
+ "[name=" + name
+ ",Salary=" + salary
+ ",hireDay=" + hireDay
+ "]";
}
}

toString 也可以供子类调用。不过子类也可以定义自己的 toString。

1
2
3
4
5
6
7
public class Manager extends Employee {
public String toString() {
return super.toString()
+ "[bonus=" + bonus
+ "]";
}
}

定义了 toString 后,只要对象与一个字符串通过操作符 “+” 连接起来,Java 编译就会自动地调用 toString 方法,以便获得这个对象的字符串描述。

“” + x 可以用于代替 x.toString(),如果 x 是基本类型,”” + x 也是可以执行的。

println(x) 会直接调用 x.toString()。

Object 定义的 toString 方法输出对象所属类名和散列码。例如:

1
System.out.println(System.out);

PringStream 类没有设计 toString 方法,上面输出的就是类名+散列码。

1
java.io.PrintStream@6e8cf4c6

数组继承了 Object 类的 toString 方法,数组类型将按照类名+散列码打印。

1
2
3
int[] nums = {2, 3, 4};
String s = "" + nums;
System.out.println(s);

结果如下,其中 [I 表示是整型数组。

1
[I@12edcd21

应该用静态方法 Arrays.toString:

1
String s = Arrays.toString(nums);

打印多维数组的方法 Arrays.deepToString()


Share