从Object类的详解到阻塞队列的实现
一、Object类详解
Java 中的 Object 类是所有类的终极父类,任何一个类(包括数组)都直接或间接继承自它。因此,Object 类中的方法在所有 Java 对象中都可以使用。掌握这些方法,是理解 Java 基础的重要一步。
下面我们逐一介绍 Object 类的核心方法:
1.构造方法Object():
/**
* 类 {@code Object} 是类层次结构的根类。
* 每个类都以 {@code Object} 作为超类。所有对象(包括数组)都实现了此类的方法。
*
* @see java.lang.Class
* @since 1.0
*/
public class Object {
/**
* 构造一个新对象。
*/
@IntrinsicCandidate
public Object() {}
意思是当我们用new关键字创建一个对象时,构造方法会被调用。Object类的构造方法时最底层的构造方法,所有子类的构造方法都会先调用它(通过super()),从而保证对象能够被正确创建。
(通俗理解:就像盖房子需要先打地基,Object的构造方法就是所有对象的“地基”。我们不需要自己写它,但要知道它是存在的。)
2.getClass()方法
/**
* 返回此 {@code Object} 的运行时类。返回的 {@code Class} 对象是由所表示类的
* {@code static synchronized} 方法锁定的对象。
*
* <p><b>实际结果类型是 {@code Class<? extends |X|>},其中 {@code |X|} 是调用
* {@code getClass} 的表达式的静态类型擦除。</b> 例如,在以下代码片段中不需要强制转换:</p>
*
* <p>
* {@code Number n = 0; }<br>
* {@code Class<? extends Number> c = n.getClass(); }
* </p>
*
* @return 表示此对象运行时类的 {@code Class} 对象。
* @jls 15.8.2 类字面量
*/
@IntrinsicCandidate
public final native Class<?> getClass();
这个方法可以返回对象运行时的实际类型(即Class对象)。通过他可以获取类的完整名称、方法、字段等信息。
示例:
(通俗理解):问一个对象“你是什么类型的?”它会告诉你它真正的类。例如,
String str = "hello"; str.getClass()得到的是String.class。
3.hashCode()方法
/**
* {@return 此对象的哈希码值} 此方法支持哈希表(例如 {@link java.util.HashMap} 提供的哈希表)的使用。
* <p>
* {@code hashCode} 的常规约定是:
* <ul>
* <li>在 Java 应用程序执行期间,对同一对象多次调用 {@code hashCode} 方法时,只要对象的
* {@code equals} 比较中使用的信息未被修改,则该方法必须始终返回相同的整数。该整数无需在应用程序的一次执行到另一次执行之间保持一致。
* <li>如果根据 {@link #equals(Object) equals} 方法两个对象相等,则对这两个对象中的每一个调用
* {@code hashCode} 方法必须产生相同的整数结果。
* <li>不要求如果根据 {@link #equals(Object) equals} 方法两个对象不相等,那么对这两个对象中的每一个调用
* {@code hashCode} 方法必须产生不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。
* </ul>
*
* @implSpec
* 在合理可行的范围内,由 {@code Object} 类定义的 {@code hashCode} 方法对于不同的对象返回不同的整数。
*
* @apiNote
* {@link java.util.Objects#hash(Object...) hash} 和 {@link java.util.Objects#hashCode(Object) hashCode}
* 方法(属于 {@link java.util.Objects})可用于帮助构建简单的哈希码。
*
* @see java.lang.Object#equals(java.lang.Object)
* @see java.lang.System#identityHashCode
*/
@IntrinsicCandidate
public native int hashCode();
这个方法的作用是返回对象的哈希码(一个整数)。哈希码主要用于基于哈希的集合(如 HashMap、HashSet),它们利用哈希码快速定位对象存储的位置。
示例:
通俗理解:每个对象可以计算出一个“身份证号”,虽然不唯一(可能两个不同对象有相同哈希码),但好的哈希算法会让不同对象的哈希码尽量不同,从而提高集合的查找效率。
其中:
如果两个对象通过
equals()比较相等,那么它们的hashCode()必须相等。反过来不要求:哈希码相等的两个对象不一定
equals()相等。
4.equals(Object obj) 方法
/**
* 指示某个其他对象是否与此对象“相等”。
* <p>
* {@code equals} 方法在非空对象引用上实现了一个 <dfn>{@index "等价关系"}</dfn>:
* <ul>
* <li>它是<i>自反的</i>:对于任何非空引用值 {@code x},{@code x.equals(x)} 应返回 {@code true}。
* <li>它是<i>对称的</i>:对于任何非空引用值 {@code x} 和 {@code y},当且仅当 {@code y.equals(x)}
* 返回 {@code true} 时,{@code x.equals(y)} 应返回 {@code true}。
* <li>它是<i>传递的</i>:对于任何非空引用值 {@code x}、{@code y} 和 {@code z},如果 {@code x.equals(y)}
* 返回 {@code true} 且 {@code y.equals(z)} 返回 {@code true},则 {@code x.equals(z)} 应返回 {@code true}。
* <li>它是<i>一致的</i>:对于任何非空引用值 {@code x} 和 {@code y},只要对象的 {@code equals} 比较中使用的信息未被修改,
* 多次调用 {@code x.equals(y)} 始终返回 {@code true} 或始终返回 {@code false}。
* <li>对于任何非空引用值 {@code x},{@code x.equals(null)} 应返回 {@code false}。
* </ul>
*
* <p>
* 等价关系将其操作的元素划分为<i>等价类</i>;等价类的所有成员彼此相等。等价类的成员可以相互替换,至少对于某些用途是这样。
*
* @implSpec
* {@code Object} 类的 {@code equals} 方法实现了对象上最可能的等价关系;也就是说,对于任何非空引用值 {@code x} 和 {@code y},
* 当且仅当 {@code x} 和 {@code y} 引用同一个对象({@code x == y} 的值为 {@code true})时,此方法返回 {@code true}。
*
* 换句话说,在引用相等性等价关系下,每个等价类只有一个元素。
*
* @apiNote
* 通常,在重写此方法时,必须同时重写 {@link #hashCode() hashCode} 方法,以维护 {@code hashCode} 方法的常规约定,
* 该约定规定相等的对象必须具有相等的哈希码。
* <p>两个参数的 {@link java.util.Objects#equals(Object, Object) Objects.equals} 方法实现了对两个可能为空的
* 对象引用的等价关系。
*
* @param obj 要与之比较的引用对象。
* @return 如果此对象与 obj 参数相同,则返回 {@code true};否则返回 {@code false}。
* @see #hashCode()
* @see java.util.HashMap
*/
public boolean equals(Object obj) {
return (this == obj);
}
这个方法可以比较两个对象是否“相等”。默认实现比较的是内存地址,也就是判断两个变量是否指向同一个对象。
示例:
public class Main { private String name; private int age; // 构造方法 public Main(String name, int age) { this.name = name; this.age = age; } //重写equals方法 @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Main person = (Main) obj; if (age != person.age) return false; if (!Objects.equals(name, person.name)) { return false; } return true; } //重写hashCode方法 @Override public int hashCode() { return Objects.hash(name, age); } //重写toString方法 @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } public static void main(String[] args) { Main person1 = new Main("张三", 18); Main person2 = new Main("张三", 18); Main person3 = new Main("王五", 20); //比较p1和p2是否相等 System.out.println(person1.equals(person2)); System.out.println(person1==person2); //比较p1和p3是否相等 System.out.println(person1.equals(person3)); } }
通俗理解:
默认情况下,“
equals”就像“==”一样,问“你们两个是不是同一个东西?”但在实际开发中,我们经常希望比较对象的内容(比如两个字符串内容是否相同),所以需要在子类中重写
equals。
5.clone()方法
/**
* 创建并返回此对象的一个副本。“副本”的确切含义可能取决于对象的类。一般的意图是,对于任何对象 {@code x},表达式:
* <blockquote>
* <pre>
* x.clone() != x</pre></blockquote>
* 将为 true,并且表达式:
* <blockquote>
* <pre>
* x.clone().getClass() == x.getClass()</pre></blockquote>
* 将为 {@code true},但这些不是绝对要求。虽然通常情况是:
* <blockquote>
* <pre>
* x.clone().equals(x)</pre></blockquote>
* 将为 {@code true},但这不是绝对要求。
* <p>
* 按照惯例,返回的对象应该通过调用 {@code super.clone} 获得。如果一个类及其所有超类(除了 {@code Object})都遵循此约定,
* 那么将满足 {@code x.clone().getClass() == x.getClass()}。
* <p>
* 按照惯例,此方法返回的对象应独立于此对象(正在被克隆的对象)。为了实现这种独立性,可能需要在返回 {@code super.clone}
* 获得的对象之前修改它的一个或多个字段。通常,这意味着复制组成被克隆对象内部“深层结构”的任何可变对象,并将对这些对象的引用替换为对副本的引用。
* 如果一个类只包含基本类型字段或对不可变对象的引用,那么通常不需要修改 {@code super.clone} 返回的对象中的任何字段。
*
* @implSpec
* {@code Object} 类的 {@code clone} 方法执行特定的克隆操作。首先,如果此对象的类未实现接口 {@code Cloneable},
* 则抛出 {@code CloneNotSupportedException}。请注意,所有数组都被视为实现了接口 {@code Cloneable},并且数组类型
* {@code T[]} 的 {@code clone} 方法的返回类型是 {@code T[]},其中 T 是任何引用或基本类型。
* 否则,此方法创建此对象类的一个新实例,并使用此对象相应字段的内容初始化其所有字段,就像通过赋值一样;字段本身的内容不会被克隆。
* 因此,此方法执行此对象的“浅拷贝”,而不是“深拷贝”操作。
* <p>
* {@code Object} 类本身不实现接口 {@code Cloneable},因此在类为 {@code Object} 的对象上调用 {@code clone} 方法
* 将在运行时导致抛出异常。
*
* @return 此实例的一个克隆。
* @throws CloneNotSupportedException 如果对象的类不支持 {@code Cloneable} 接口。重写 {@code clone} 方法的子类
* 也可以抛出此异常以指示无法克隆实例。
* @see java.lang.Cloneable
*/
@IntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
这个方法的作用是创建并返回当前对象的一个副本。默认实现是“浅拷贝”:只复制基本类型字段和引用,而引用指向的对象不会被复制。
示例:
class Address { String city; } class User implements Cloneable { String name; Address addr; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); // 浅拷贝 } } User u1 = new User(); u1.addr = new Address(); User u2 = (User) u1.clone(); System.out.println(u1.addr == u2.addr); // true,地址相同,说明 addr 没有被复制通俗理解:
可以复制一个一模一样的对象,但默认只是复制了表面一层。如果对象内部还有引用的其他对象,则这些引用指向的还是原来的对象。
使用条件:
必须实现
Cloneable接口(标记接口,无方法),否则调用clone()会抛出CloneNotSupportedException。通常建议重写
clone()方法,并扩大访问权限为public,同时根据需要实现深拷贝。
6.toString()方法
/**
* {@return 对象的字符串表示形式}
*
* 满足此方法的约定意味着必须返回一个非 {@code null} 的结果。
*
* @apiNote
* 通常,{@code toString} 方法返回一个“以文本方式表示”此对象的字符串。结果应是一个简洁但信息丰富的表示形式,易于人们阅读。
* 建议所有子类重写此方法。字符串输出不一定随时间或 JVM 调用保持稳定。
* @implSpec
* {@code Object} 类的 {@code toString} 方法返回一个字符串,该字符串由对象所属类的名称、at 符号“{@code @}”和对象的哈希码的无符号十六进制表示组成。
* 换句话说,此方法返回一个等于以下值的字符串:
* {@snippet lang=java :
* getClass().getName() + '@' + Integer.toHexString(hashCode())
* }
* {@link java.util.Objects#toIdentityString(Object) Objects.toIdentityString} 方法返回一个对象的字符串,
* 该字符串等于如果对象的类未重写 {@code toString} 和 {@code hashCode} 方法时返回的字符串。
*/
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
该方法的作用是返回对象的字符串表示。默认格式是“类名@哈希码的十六进制”。但通常我们会重写它,让它返回更有意义的信息(比如对象的属性值),方便调试和日志输出。
示例:
通俗理解:当你直接打印一个对象时,打印的就是
toString()的结果。重写它可以让输出更友好。
7.线程相关方法:wait()、notify()、notifyAll()
这些方法用于线程之间的协作,必须在同步代码块或同步方法中调用,且调用线程必须持有当前对象的锁。
7.1 wait()
/**
* 导致当前线程等待,直到它被唤醒,通常是通过被 <em>通知</em> 或 <em>中断</em>。
* <p>
* 在所有方面,此方法的行为就像调用了 {@code wait(0L, 0)} 一样。有关详细信息,请参见 {@link #wait(long, int)} 方法的规范。
*
* @throws IllegalMonitorStateException 如果当前线程不是对象监视器的所有者
* @throws InterruptedException 如果在当前线程等待之前或等待期间任何线程中断了当前线程。
* 当抛出此异常时,当前线程的<em>中断状态</em>将被清除。
* @see #notify()
* @see #notifyAll()
* @see #wait(long)
* @see #wait(long, int)
*/
public final void wait() throws InterruptedException {
wait(0L);
}
该方法的作用是让当前线程进入等待状态,并释放它持有的改对象的锁🔒,直到其他线程使用notify()或notifyAll()将其唤醒
其中:
wait(long timeout)可以设置超时时间(毫秒),超时后自动唤醒;wait(long timeout, int nanos)更精确。
7.2 notify()
/**
* 唤醒正在此对象监视器上等待的单个线程。如果有任何线程正在此对象上等待,则会选择其中一个被唤醒。选择是任意的,由实现决定。
* 线程通过调用其中一个 {@code wait} 方法在对象的监视器上等待。
* <p>
* 被唤醒的线程在当前线程释放此对象上的锁之前无法继续。被唤醒的线程将以通常的方式与可能正在主动竞争以在此对象上同步的任何其他线程竞争;
* 例如,被唤醒的线程在成为下一个锁定此对象的线程方面没有可靠的特权或劣势。
* <p>
* 此方法只能由拥有此对象监视器的线程调用。线程通过以下三种方式之一成为对象监视器的所有者:
* <ul>
* <li>通过执行该对象的同步实例方法。
* <li>通过执行在该对象上同步的 {@code synchronized} 语句的主体。
* <li>对于 {@code Class} 类型的对象,通过执行该类的静态同步方法。
* </ul>
* <p>
* 一次只有一个线程可以拥有对象的监视器。
*
* @throws IllegalMonitorStateException 如果当前线程不是此对象监视器的所有者。
* @see java.lang.Object#notifyAll()
* @see java.lang.Object#wait()
*/
@IntrinsicCandidate
public final native void notify();
该方法的作用是随机唤醒一个正在等待该对象锁的线程。被唤醒的线程不会立即执行,需要等待当前线程释放锁后,与其他线程竞争。
7.3 notifyAll()
/**
* 唤醒正在此对象监视器上等待的所有线程。线程通过调用其中一个 {@code wait} 方法在对象的监视器上等待。
* <p>
* 被唤醒的线程在当前线程释放此对象上的锁之前无法继续。被唤醒的线程将以通常的方式与可能正在主动竞争以在此对象上同步的任何其他线程竞争;
* 例如,被唤醒的线程在成为下一个锁定此对象的线程方面没有可靠的特权或劣势。
* <p>
* 此方法只能由拥有此对象监视器的线程调用。有关线程成为监视器所有者的方式的说明,请参见 {@code notify} 方法。
*
* @throws IllegalMonitorStateException 如果当前线程不是此对象监视器的所有者。
* @see java.lang.Object#notify()
* @see java.lang.Object#wait()
*/
@IntrinsicCandidate
public final native void notifyAll();
这个方法用来唤醒所有正在等待该对象锁的线程。
(通俗理解:
想象一个餐厅的座位(对象锁),顾客(线程)想要吃饭必须先拿到座位。
wait():顾客暂时离开座位去上厕所,把座位让给别人(释放锁),等上完厕所(被唤醒)再回来等座位。
notify():服务员随机叫一个正在等待的顾客回来。
notifyAll():服务员把所有等待的顾客都叫回来,但他们需要重新抢座位。)
二、阻塞队列的实现
阻塞队列是一种线程安全的队列,它支持两种阻塞操作:
-
当队列满时,插入操作(
put)会阻塞,直到队列有空位。 -
当队列空时,取出操作(
take)会阻塞,直到队列有元素可用。
详细代码解释:
1.数据结构
public class SimpleBlockingQueue<T> {
private final Object[] items; // 存储元素的环形数组
private int putIndex; // 下一个插入位置
private int takeIndex; // 下一个取出位置
private int count; // 当前元素个数
public SimpleBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
items = new Object[capacity];
}
// ... 方法
}
环形数组:利用数组和两个指针实现队列,避免数据搬移。
putIndex:生产者下次存放元素的位置,到达数组末尾时折返到 0。
takeIndex:消费者下次取出元素的位置,同样环形处理。
count:当前队列中的元素个数,用于判断队列空/满。
2.插入元素(队列满则阻塞)
public synchronized void put(T t) throws InterruptedException {
while (count == items.length) {
wait(); // 队列满,当前线程等待
}
items[putIndex] = t;
if (++putIndex == items.length) putIndex = 0;
count++;
notifyAll(); // 唤醒可能等待的 take 线程
}
同步方法:
synchronized保证线程安全,方法内部自动持有当前对象锁。条件检查:使用
while循环判断队列是否已满。如果已满,调用wait()使当前线程等待,并释放锁。当被唤醒后,会重新竞争锁,然后再次检查条件(while重新判断),防止虚假唤醒。插入元素:将元素放入
putIndex位置,然后环形更新索引,并增加count。唤醒:插入成功后调用
notifyAll(),唤醒所有等待的线程(主要是消费者)。
3.取出元素(队列空则阻塞)
@SuppressWarnings("unchecked")
public synchronized T take() throws InterruptedException {
while (count == 0) {
wait(); // 队列空,等待
}
T t = (T) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length) takeIndex = 0;
count--;
notifyAll(); // 唤醒可能等待的 put 线程
return t;
}
4.生产者线程
for (int i = 0; i < 10; i++) {
Thread t1 = new Thread() {
public void run() {
int count = 0;
while (true) {
Thread.sleep(500);
queue.put("task" + count);
count++;
}
}
};
t1.start();
}
每个生产者每 500 毫秒生产一个任务,任务名为
"task" + 计数。注意每个生产者的count是独立的,所以可能产生重复的任务名,但这不影响队列逻辑。如果队列满,生产者会在
put方法内部阻塞,直到有空间。
5.消费者线程
Thread t2 = new Thread() {
public void run() {
while (true) {
Thread.sleep(3000);
String str = queue.take();
str += "end";
System.out.println(str);
}
}
};
每 3 秒取一个元素,处理(拼接 "end")并打印。消费速度慢于生产速度,队列会逐渐填满,生产者开始阻塞。
6.监听线程
Thread t3 = new Thread() {
public void run() {
while (true) {
Thread.sleep(1000);
System.out.println("当前队列元素数量为:" + queue.size());
}
}
};
每秒打印一次队列大小,便于观察队列积压情况。
运行结果

初始时队列为空,消费者阻塞(因为
take会等待)。生产者不断生产,队列大小逐渐增加。
消费者每 3 秒取一个元素,大小增长速度放缓。
当队列满(大小等于容量 30)时,生产者进入等待状态,队列大小不再增加。
消费者取走元素后,队列有空位,唤醒生产者继续生产。
整个过程通过
wait/notifyAll实现了线程的阻塞与唤醒。
三、总结
通过手写一个简单的阻塞队列,我们不仅复习了 Object 的线程协作方法,还实践了生产者-消费者模式,并体会了线程安全设计中的关键细节(如循环检查、notifyAll 的选择)。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐







所有评论(0)