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());
}
@IntrinsicCandidate
public 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 ...
@Override
public 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 契约!
@Override
public 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);
}
@Override
public 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()); // 输出: 20
System.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(),并且确保它们使用相同的字段集。
更多推荐


所有评论(0)