JavaSE——八股文
1. Object类有哪些方法?
- hashCode
- equals
- clone
- toString
- notify/notifyAll
- wait
- finalize
@IntrinsicCandidate
public native int hashCode();public boolean equals(Object obj) {return (this == obj);
}@IntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;public String toString() {return getClass().getName() + "@" + Integer.toHexString(hashCode());
}@IntrinsicCandidatepublic final native void notify();@IntrinsicCandidate
public final native void notifyAll();public final void wait() throws InterruptedException {wait(0L);
}public final native void wait(long timeoutMillis) throws InterruptedException;@Deprecated(since="9")
protected void finalize() throws Throwable { }
(1)hashCode方法
@IntrinsicCandidate
public native int hashCode();
@IntrinsicCandidate
是 Java 虚拟机(JVM)中的一个注解,主要用于标记那些可能会被 JVM 进行特殊优化(通常是内联或使用特定的、更高效的本地代码实现)的底层方法。native
关键字表示这个方法的实现不是用 Java 语言编写的,而是由 JVM 用 C、C++ 或汇编等本地语言实现的。JVM 负责提供这个方法的具体功能。
int
是返回值类型,hashCode()
方法返回一个整数,该整数通常用作哈希表(如HashMap
)中的哈希码。当你调用hashCode()
这个方法时,它一定会给你返回一个 整数(integer)。这个整数的范围是 Java 中int
类型的范围,即从 -2,147,483,648 到 2,147,483,647。- 相等的对象必须有相同的哈希码:这是
hashCode()
和equals()
合约的铁律。如果a.equals(b)
为true
,那么a.hashCode() == b.hashCode()
必须为true
。违反这一点会导致HashMap
等集合行为异常(比如get()
找不到刚put()
进去的对象)。
通过HashMap举例子。我们算出KEY的hashcode,再通过特定的取模算法,就能确定KEY在数组的索引下标。
(2)equals方法
public boolean equals(Object obj) {return (this == obj);
}
1,所有类都从Object类中继承equals()方法
2,Object中的equals方法是直接判断this和obj本身的值是否相等,即用来判断调用equals的对象和形参obj所引用的对象是否是同一对象,所谓同一对象就是指内存中同一块存储单元,如果this和obj指向的hi同一块内存对象,则返回true,如果this和obj指向的不是同一块内存,则返回false,注意:即便是内容完全相等的两块不同的内存对象,也返回false。
3,如果是同一块内存,则object中的equals方法返回true,如果是不同的内存,则返回false
4,如果希望不同内存但相同内容的两个对象equals时返回true,则我们需要重写父类的equals()方法(类似于String类中重写的toString()方法)
5,String类已经重写了object中的equals方法
(3)clone方法
要让一个类支持克隆,你需要:
- 实现
Cloneable
接口。 - 重写
clone()
方法,并通常将其访问级别改为public
(以便外部调用)。 - 处理
CloneNotSupportedException
(尽管在实现了Cloneable
的类中,这个异常理论上不会抛出,但重写时语法上需要处理)。 - (可选)实现深拷贝:
Object
的clone()
只做浅拷贝。如果你的对象包含可变对象的引用,你可能需要在重写的clone()
方法中手动复制这些内部对象,以实现深拷贝。
public class MyClass implements Cloneable {private int value;private String name;private int[] data; // 假设这是一个需要深拷贝的数组// ... 构造函数、getter、setter ...@Overridepublic MyClass clone() {try {// 调用父类 Object 的 clone(),进行浅拷贝MyClass cloned = (MyClass) super.clone();// 对于需要深拷贝的字段,手动复制if (this.data != null) {cloned.data = this.data.clone(); // 数组的 clone() 会创建新数组}return cloned;} catch (CloneNotSupportedException e) {// 因为实现了 Cloneable,这个异常不应该发生throw new AssertionError(); // 或者根据需要处理}}
}// 使用
MyClass obj1 = new MyClass(10, "Test", new int[]{1, 2, 3});
MyClass obj2 = obj1.clone(); // 创建一个副本
- 默认Object类的clone方法是浅拷贝
- 如果想让clone方法变成深拷贝,需要自定义一个类实现Clonenable接口并重写clon
(4)toString方法
- 声明:
public String toString() {return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
- 作用:
- 返回对象的字符串表示形式。
- 主要用于调试、日志记录和信息展示。
- 默认实现:
- 返回类的全限定名 +
@
+ 对象哈希码的无符号十六进制表示。 - 例如:
com.example.MyClass@1a2b3c4d
。
- 返回类的全限定名 +
- 为什么需要重写:
- 默认的字符串信息通常没有业务意义。
- 重写后可以提供对象的有意义的、可读性强的信息。
- 重写建议:
- 包含对象的关键状态信息。
- 格式清晰,易于理解。
- 例如:
"Person{name='John', age=30}"
。
- 自动调用:
- 当对象被用于字符串拼接 (
"obj: " + obj
) 或被System.out.println(obj)
直接打印时,会自动调用toString()
。
- 当对象被用于字符串拼接 (
(5)wait方法
- 声明:
public final void wait() throws InterruptedException {wait(0L); } public final native void wait(long timeoutMillis) throws InterruptedException; public final void wait(long timeoutMillis, int nanos) throws InterruptedException { ... }
- 作用:
- 使当前线程进入等待状态 (WAITING/Timed Waiting),并释放该对象的监视器锁(monitor lock)。
- 线程会一直等待,直到以下情况之一发生:
- 其他线程调用了该对象的
notify()
或notifyAll()
方法。 - 等待时间
timeoutMillis
到期(对于带超时的版本)。 - 当前线程被其他线程中断 (
interrupt()
)。
- 其他线程调用了该对象的
- 关键点:
final
: 不能被重写。- 必须在同步块中调用: 调用
wait()
前,当前线程必须拥有该对象的锁(即在synchronized
块或方法内)。否则会抛出IllegalMonitorStateException
。 - 释放锁: 这是
wait()
的核心特性。它允许其他线程获取锁来修改对象状态。 - 重新获取锁: 当线程被唤醒(或超时)后,它不会立即继续执行。它必须重新竞争该对象的锁,只有获得锁后才能从
wait()
调用中返回并继续执行。
- 使用场景:
- 生产者-消费者模式、线程间协调、条件等待。
(6)notify和notifyAll方法
- 声明:
@IntrinsicCandidate public final native void notify(); @IntrinsicCandidate public final native void notifyAll();
- 作用:
- 唤醒在该对象监视器上等待的一个 (
notify()
) 或所有 (notifyAll()
) 线程。 - 被唤醒的线程不会立即执行。它们需要重新竞争该对象的锁,获得锁后才能继续。
- 唤醒在该对象监视器上等待的一个 (
- 关键点:
final
: 不能被重写。@IntrinsicCandidate
: 可被 JIT 优化。- 必须在同步块中调用: 和
wait()
一样,调用前必须持有对象的锁。 - 不释放锁: 调用
notify()
/notifyAll()
的线程不会立即释放锁。它通常会在完成当前同步块的逻辑后才释放锁。 notify()
vsnotifyAll()
:notify()
: 随机唤醒一个等待线程。如果多个线程等待不同的条件,使用notify()
可能唤醒错误的线程,导致死锁或逻辑错误。通常更安全的做法是使用notifyAll()
。notifyAll()
: 唤醒所有等待线程。虽然可能唤醒不需要的线程(“惊群效应”),但能确保满足条件的线程有机会运行。被唤醒的线程需要在循环中检查条件是否真正满足。
- 使用场景:
- 与
wait()
配合,实现线程间的精确通信和协作。
- 与
(7)finalize方法
- 声明:
@Deprecated(since="9") protected void finalize() throws Throwable { }
- 作用:
- 在垃圾回收器回收对象之前,由 JVM 调用此方法。
- 理论上可用于执行对象销毁前的清理工作(如释放非 Java 资源:文件句柄、网络连接、本地内存等)。
- 关键点:
@Deprecated
: 从 Java 9 开始被标记为过时。强烈不推荐使用。- 不可靠:
- 无法保证
finalize()
会被调用(程序可能在对象回收前就终止了)。 - 无法保证调用的时机(可能在对象不可达后很久才调用)。
- 甚至无法保证会被调用(JVM 退出时可能不执行)。
- 无法保证
- 性能开销: 启用
finalize()
会显著增加垃圾回收的开销和复杂性。 - 危险:
finalize()
中抛出的未捕获异常会被忽略,可能导致资源清理失败。
- 替代方案:
try-with-resources
: 用于管理实现了AutoCloseable
接口的资源(如InputStream
,OutputStream
,Connection
)。这是最推荐的方式。- 显式
close()
方法: 在不再需要资源时,手动调用其close()
方法。 Cleaner
类 (Java 9+): 提供了一种更灵活、更安全的替代finalize()
的机制,用于清理非堆资源。
2. equals方法和hashCode方法的关系。
相等的对象必须有相同的哈希码:这是 hashCode() 和 equals() 合约的铁律。如果 a.equals(b) 为 true,那么 a.hashCode() == b.hashCode() 必须为 true。违反这一点会导致 HashMap 等集合行为异常(比如 get() 找不到刚 put() 进去的对象)。
这是一个关于 hashCode()
和 equals()
方法之间核心契约(Contract) 的极其重要的规则。理解它对于正确使用 Java 集合框架(尤其是基于哈希的集合如 HashMap
, HashSet
, Hashtable
)至关重要。
我们来详细解释为什么这是“铁律”,以及违反它会导致什么灾难性的后果。
1. 这条规则的含义
这条规则包含两个层面:
- 正向要求:如果两个对象通过
equals()
方法比较是相等的(a.equals(b) == true
),那么它们的hashCode()
方法必须返回相同的整数值(a.hashCode() == b.hashCode()
)。 - 反向不要求:反过来不成立。如果两个对象的
hashCode()
相同(a.hashCode() == b.hashCode()
),它们不一定相等(a.equals(b)
可能为false
)。这就是我们之前讨论的“哈希冲突”,是允许且正常的。
简单说:相等 ⇒ 同哈希码。同哈希码 ⇏ 相等。
2. 为什么是“铁律”?—— 以 HashMap 为例
HashMap
的工作原理完全依赖于这个契约。它的 put(K key, V value)
和 get(Object key)
操作流程如下:
put
操作:
- 计算
key
的哈希码:int hash = key.hashCode();
- 根据这个哈希码,通过内部算法(如
hash & (table.length - 1)
)确定该键值对应该存放在内部数组的哪个“桶”(bucket)位置,记为index
。 - 将
(key, value)
这个键值对放入table[index]
处的链表(或红黑树)中。
get
操作:
- 计算传入的
key
的哈希码:int hash = key.hashCode();
- 根据这个哈希码,计算出它应该在哪个“桶”里,记为
index
。 - 关键步骤:遍历
table[index]
处的链表(或树)中的每一个键。 - 对于链表中的每一个键
k
,HashMap
会进行双重检查:- 首先检查哈希码:
hash == k.hashCode()
。如果哈希码不同,直接跳过。 - 然后检查相等性:如果哈希码相同,再调用
key.equals(k)
。只有当equals()
也返回true
时,才认为找到了匹配的键。
- 首先检查哈希码:
3. 违反契约的灾难性后果
假设我们有一个 Student
类,但我们错误地实现了 hashCode()
和 equals()
,违反了“相等的对象必须有相同的哈希码”这条规则。
public class Student {private String name;private int age;public Student(String name, int age) {this.name = name;this.age = age;}// 错误的实现!违反了 hashCode-equals 契约!@Overridepublic boolean equals(Object obj) {if (this == obj) return true;if (obj == null || getClass() != obj.getClass()) return false;Student student = (Student) obj;// 只根据名字判断相等return name.equals(student.name);}@Overridepublic int hashCode() {// 错误!只根据年龄生成哈希码return Integer.hashCode(age);// 注意:这里没有考虑 name!}// ... toString(), getters 等
}
问题分析:
- 两个
Student
对象,只要name
相同,就被认为是equals
的。 - 但是,它们的
hashCode()
只依赖于age
。
灾难场景:
import java.util.HashMap;public class DisasterExample {public static void main(String[] args) {HashMap<Student, String> studentMap = new HashMap<>();// 创建两个名字相同但年龄不同的学生Student alice1 = new Student("Alice", 20);Student alice2 = new Student("Alice", 21); // 名字相同,但年龄不同// 根据我们的 equals 方法,它们是相等的!System.out.println("alice1.equals(alice2): " + alice1.equals(alice2)); // 输出: true// 但是,它们的哈希码不同!(因为年龄不同)System.out.println("alice1.hashCode(): " + alice1.hashCode()); // 输出: 20System.out.println("alice2.hashCode(): " + alice2.hashCode()); // 输出: 21// **PUT 操作**// 将 alice1 作为键,存入学号 "S001"studentMap.put(alice1, "S001");// HashMap 内部:// 1. 计算 alice1.hashCode() -> 20// 2. 根据 20 计算出桶的位置,比如 index = 5// 3. 将 (alice1, "S001") 存入 table[5] 的链表中// **GET 操作 - 试图用 alice2 去查找**// 注意:alice2.equals(alice1) 是 true,它们逻辑上是同一个学生!String result = studentMap.get(alice2);System.out.println("Result: " + result); // 输出: null !!!! (灾难发生)// 为什么会是 null?// 1. HashMap 计算 alice2.hashCode() -> 21// 2. 根据 21 计算出桶的位置,比如 index = 7// 3. HashMap 去 table[7] 查找,但那里是空的!因为 (alice1, "S001") 被存到了 index=5!// 4. 所以,即使 alice1 和 alice2 在逻辑上是相等的,HashMap 也找不到它们。// 因为 `get` 操作从错误的“桶”开始查找。// **更糟糕的情况:PUT 同一个“逻辑对象”两次**studentMap.put(alice2, "S002"); // 这应该更新 alice1 的值,但...// HashMap 会:// 1. 计算 alice2.hashCode() -> 21 -> index = 7// 2. 在 table[7] 处,因为是空的,直接把 (alice2, "S002") 放进去。// 现在,逻辑上相等的键 alice1 和 alice2 分别存在于不同的桶中!// HashMap 的大小变成了 2,但实际上它只应该存储一个 "Alice"。System.out.println("Map size: " + studentMap.size()); // 输出: 2 (逻辑错误)}
}
4. 正确的做法
正确的 hashCode()
实现必须包含所有参与 equals()
比较的字段。
@Override
public int hashCode() {// 包含所有 equals 中用到的字段return Objects.hash(name, age); // 或者手动计算: name.hashCode() * 31 + age
}
这样,当 alice1.equals(alice2)
为 true
时(意味着 name
相同,age
也必须相同),alice1.hashCode()
和 alice2.hashCode()
也必然相等,HashMap
就能正常工作。
总结
“相等的对象必须有相同的哈希码”是 hashCode()
和 equals()
之间的根本性契约。HashMap
等集合类依赖这个契约来保证其正确性和效率:
hashCode()
用于快速定位数据所在的“桶”(高效)。equals()
用于在同一个“桶”内精确识别是哪个对象(准确)。
如果这个契约被破坏,hashCode()
的“快速定位”功能就会失效,导致 HashMap
找不到它本应找到的对象,或者存储重复的逻辑对象,从而使整个集合的行为变得不可预测和错误。因此,每当重写 equals()
时,必须重写 hashCode()
,并且确保它们使用相同的字段集。