Object类

Object是所有类的祖宗

Java里所有的类都直接或间接继承自Object,它是类层次结构的

就像人类的”共同祖先”一样,Object定义了所有对象都应该有的基本能力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testObjectIsRoot() {
// 任何类都是Object的子类
String s = "hello";
Integer n = 42;
int[] arr = {1, 2, 3};

System.out.println(s instanceof Object); // true
System.out.println(n instanceof Object); // true
System.out.println(arr instanceof Object); // true,连数组都是!

// 你写的任何类,即使没写extends,也默认继承Object
// class Student {} 等价于 class Student extends Object {}
}

Object的核心方法一览

方法 作用 是否常重写
equals() 判断两个对象是否”相等”
hashCode() 返回对象的哈希值
toString() 返回对象的字符串表示
clone() 克隆对象 有时
getClass() 获取运行时类信息
finalize() 垃圾回收前调用(已废弃)
wait()/notify() 线程通信

equals():判断两个对象是否”相等”

默认的equals()和 == 一样,比较的是内存地址,通常需要重写来比较内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Test
public void testEquals() {
// 不重写equals的情况
class Student {
String name;
int age;
Student(String name, int age) {
this.name = name;
this.age = age;
}
}

Student s1 = new Student("张三", 18);
Student s2 = new Student("张三", 18);
System.out.println(s1.equals(s2)); // false!默认比较地址
System.out.println(s1 == s2); // false!同上
}

@Test
public void testEqualsOverride() {
// 正确重写equals的Student类
class Student {
String name;
int age;
Student(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true; // 同一个对象
if (obj == null) return false; // null肯定不等
if (getClass() != obj.getClass()) return false; // 类型不同
Student other = (Student) obj;
return this.age == other.age
&& Objects.equals(this.name, other.name); // 逐字段比较
}
}

Student s1 = new Student("张三", 18);
Student s2 = new Student("张三", 18);
System.out.println(s1.equals(s2)); // true!比较的是内容了
}

重写equals的五大原则

自反性:a.equals(a) 必须是true

对称性:a.equals(b)b.equals(a) 结果一样

传递性:a.equals(b)b.equals(c),则 a.equals(c)

一致性:多次调用结果一样(对象没变的话)

非空性:a.equals(null) 必须是false

参考 运算符==equals 的区别,以及 String类与字符串操作 中字符串比较

hashCode():对象的”身份证号”

hashCode就像人的身份证号,用来快速定位对象在哈希表中的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testHashCode() {
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");

// String已经重写了hashCode,相同内容的hashCode相同
System.out.println(s1.hashCode()); // 99162322
System.out.println(s2.hashCode()); // 99162322
System.out.println(s3.hashCode()); // 99162322

// Object默认的hashCode是根据内存地址算的
Object obj1 = new Object();
Object obj2 = new Object();
System.out.println(obj1.hashCode()); // 一个数字
System.out.println(obj2.hashCode()); // 另一个数字,跟上面不同
}

hashCode和equals的契约(必须遵守!)

equals 返回true → hashCode 必须相同

hashCode 相同 → equals 不一定为true(哈希冲突)

equals 返回false → hashCode 可以相同也可以不同

为什么重写equals必须同时重写hashCode

因为HashMap、HashSet这些集合依赖这个契约来工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@Test
public void testEqualsHashCodeContract() {
// 反面教材:只重写equals不重写hashCode
class BadStudent {
String name;
BadStudent(String name) { this.name = name; }

@Override
public boolean equals(Object obj) {
if (obj instanceof BadStudent) {
return this.name.equals(((BadStudent) obj).name);
}
return false;
}
// 没重写hashCode!
}

BadStudent s1 = new BadStudent("张三");
BadStudent s2 = new BadStudent("张三");
System.out.println(s1.equals(s2)); // true

// 放进HashSet
Set<BadStudent> set = new HashSet<>();
set.add(s1);
set.add(s2);
System.out.println(set.size()); // 2!应该是1才对!

// 原因:HashSet先用hashCode定位桶,s1和s2的hashCode不同
// 所以它们被放进了不同的桶,equals根本没机会被调用
System.out.println(s1.hashCode()); // 一个值
System.out.println(s2.hashCode()); // 另一个值(默认根据地址算)
}

@Test
public void testCorrectOverride() {
// 正确做法:equals和hashCode一起重写
class GoodStudent {
String name;
int age;
GoodStudent(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof GoodStudent)) return false;
GoodStudent other = (GoodStudent) obj;
return age == other.age && Objects.equals(name, other.name);
}

@Override
public int hashCode() {
return Objects.hash(name, age); // 用参与equals比较的字段来算
}
}

GoodStudent s1 = new GoodStudent("张三", 18);
GoodStudent s2 = new GoodStudent("张三", 18);

Set<GoodStudent> set = new HashSet<>();
set.add(s1);
set.add(s2);
System.out.println(set.size()); // 1!正确!
}

toString():对象的”自我介绍”

默认打印 类名@十六进制哈希值,一般看不懂,所以需要重写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Test
public void testToString() {
// 不重写toString
Object obj = new Object();
System.out.println(obj); // java.lang.Object@1b6d3586(看不懂)

// String已经重写了toString
String s = "hello";
System.out.println(s); // "hello"(很友好)

// 自定义类重写toString
class Student {
String name;
int age;
Student(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Student{name='" + name + "', age=" + age + "}";
}
}

Student stu = new Student("张三", 18);
System.out.println(stu); // Student{name='张三', age=18}
// System.out.println() 内部会自动调用对象的toString()
}

小技巧:实际开发中,IDE可以一键生成toString(),不用手写

clone():浅拷贝 vs 深拷贝

clone就是复制一个对象,但要注意是”浅复制”还是”深复制”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Test
public void testClone() throws CloneNotSupportedException {
// 浅拷贝示例
class Address implements Cloneable {
String city;
Address(String city) { this.city = city; }
}

class Person implements Cloneable {
String name;
Address address; // 引用类型字段

Person(String name, Address address) {
this.name = name;
this.address = address;
}

@Override
protected Person clone() throws CloneNotSupportedException {
return (Person) super.clone(); // 浅拷贝
}
}

Address addr = new Address("北京");
Person p1 = new Person("张三", addr);
Person p2 = p1.clone();

System.out.println(p1 == p2); // false,两个不同对象
System.out.println(p1.name.equals(p2.name)); // true
System.out.println(p1.address == p2.address); // true!地址是同一个对象!

// 浅拷贝的问题:修改p2的地址,p1也受影响
p2.address.city = "上海";
System.out.println(p1.address.city); // "上海"!p1也被改了!
}

浅拷贝 vs 深拷贝

类型 基本类型字段 引用类型字段 比喻
浅拷贝 复制值 复制引用(共享同一个对象) 抄了你的笔记,但图片是同一张
深拷贝 复制值 递归复制出新对象 抄了你的笔记,图片也重新画了一份

深拷贝的实现方式:手动clone引用字段,或者用序列化

getClass():获取运行时类信息

可以在运行时知道一个对象到底是什么类,这是反射的入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testGetClass() {
String s = "hello";
Integer n = 42;

System.out.println(s.getClass()); // class java.lang.String
System.out.println(s.getClass().getName()); // java.lang.String
System.out.println(s.getClass().getSimpleName()); // String

System.out.println(n.getClass()); // class java.lang.Integer

// 判断两个对象是否同一类型
String a = "hello";
String b = "world";
System.out.println(a.getClass() == b.getClass()); // true

// 反射预告:通过getClass可以获取类的所有方法、字段等信息
// 这在框架开发中非常重要(Spring、MyBatis都大量用反射)
}

常见坑总结

说明 怎么避免
只重写equals不重写hashCode HashMap/HashSet行为异常 两个必须一起重写
equals传参类型写错 equals(Student s) 不是重写而是重载 参数类型必须是Object
clone默认是浅拷贝 引用字段共享同一个对象 需要深拷贝要手动处理
toString忘记重写 打印出来看不懂的地址 养成重写toString的习惯
==equals 搞混 参考 运算符 对象比较用equals

练习题

题1:给下面的类重写equals、hashCode和toString

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Book {
String isbn;
String title;
double price;

Book(String isbn, String title, double price) {
this.isbn = isbn;
this.title = title;
this.price = price;
}

// 两本书ISBN一样就认为是同一本书
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Book)) return false;
Book other = (Book) obj;
return Objects.equals(this.isbn, other.isbn);
}

@Override
public int hashCode() {
return Objects.hash(isbn); // 只用isbn,和equals保持一致
}

@Override
public String toString() {
return "Book{isbn='" + isbn + "', title='" + title
+ "', price=" + price + "}";
}
}

@Test
public void testBook() {
Book b1 = new Book("978-1", "Java入门", 59.9);
Book b2 = new Book("978-1", "Java入门", 59.9);
Book b3 = new Book("978-2", "Python入门", 49.9);

System.out.println(b1.equals(b2)); // true
System.out.println(b1.equals(b3)); // false
System.out.println(b1); // Book{isbn='978-1', title='Java入门', price=59.9}

Set<Book> set = new HashSet<>();
set.add(b1);
set.add(b2);
System.out.println(set.size()); // 1
}

上一章 目录 下一章
包装类与自动装拆箱 java基础 Math类