Object类是所有类的父类,任何类都默认继承Object,位于java.lang包中,Object类主要实现了哪些方法呢?
toString方法
toString方法可以将任何一个对象转换成字符串返回,返回值的生成算法为:getClass().getName() + '@' + Integer.toHexString(hashCode())。不过,一般子类都会重写该方法。
equals方法
Object类中的equals方法,用来比较两个引用的虚地址。当且仅当两个引用在物理上是同一个对象时,返回值为true,否则将返回false。不过,一般子类都会重写该方法。
hashCode方法
获取对象的哈希码值,为16进制。
clone方法
保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。
getClass方法
final方法,获取类的class对象。
wait方法
导致线程进入等待状态,直到它被其他线程通过notify()或者notifyAll唤醒。该方法只能在同步方法中调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。wait(long timeout)可以设定一个超时间隔,调用该方法后当前线程进入睡眠状态,直到以下事件发生:
(1)其他线程调用了该对象的notify方法。
(2)其他线程调用了该对象的notifyAll方法。
(3)其他线程调用了interrupt中断该线程。
(4)时间间隔到了。
此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。
notify方法
该方法唤醒在该对象上等待的某个线程。该方法只能在同步方法或同步块内部调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。
notifyAll方法
解除所有那些在该对象上调用wait方法的线程的阻塞状态。该方法只能在同步方法或同步块内部调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。
为什么重写equals方法必须重写hashcode方法
因为如果只重写了equals方法而没有重写hashCode方法,则两个不同的实例a和b虽然equals结果相等(业务逻辑上,比如:两个对象的值全部相等),但却会有不同的hashcode,这样hashmap里面会同时存在a和b,而实际上我们需要hashmap里面只能保存其中一个,因为从业务逻辑方向看它们是相等的。
equals方法和hashCode方法如果不同时按你自己逻辑重写的话,HashMap就会出问题。比如你只重写了equals方法而没有重写hashCode方法,那么HashMap在第一步寻找链表的时候会出错,有同样值的两个对象a和b并不会指向同一个链表或桶,因为你没有提供自己的hashCode方法,那么就会使用Object的hashCode方法,该方法是根据内存地址来比较两个对象是否一致,由于a和b有不同的内存地址,所以会指向不同的链表,这样HashMap会认为b不存在,虽然我们期望a和b是同一个对象;反之如果只重写了hashCode方法而没有覆盖equals方法,那么虽然第一步操作会使a和b找到同一个链表,但是由于equals没有覆盖,那么在遍历链表的元素时,a.equals(b)也不为true(事实上Object的equals方法也是比较内存地址),从而HashMap认为不存在b对象,这同样也是不正确的。
代码测试:
没有覆盖equals方法和hashCode方法。
package com.redistext.equals;
/**
* @author: maorui
* @description: TODO
* @date: 2021/10/27 16:53
* @description: V1.0
*/
public class Student {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
package com.redistext.equals;
import java.util.HashSet;
import java.util.Set;
/**
* @author: maorui
* @description: TODO
* @date: 2021/10/27 16:54
* @description: V1.0
*/
public class EqualsTest {
public static void main(String[] args) {
Set<Student> set = new HashSet<Student>();
Student stu1 = new Student("张三",3);
Student stu2 = new Student("张三",3);
set.add(stu1);
set.add(stu2);
System.out.println("stu1 == stu2 : "+(stu1 == stu2));
System.out.println("stu1.equals(stu2) : "+stu1.equals(stu2));
System.out.println("set size:"+ set.size());
}
}
测试结果:
Student类没有覆盖equals方法,stu1调用equals方法实际上调用的是Object的equals方法。所以采用对象内存地址是否相等来判断对象是否相等。因为是两个新对象所以对象的内存地址不相等,所以stu1.equals(stu2) 是false。
但是从业务逻辑出发,我们认为stu1和stu2的属性值相等,那这两个对象就是一样的,那么我们就需要重写equals方法。
package com.redistext.equals;
/**
* @author: maorui
* @description: TODO
* @date: 2021/10/27 16:53
* @description: V1.0
*/
public class Student {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
if (age != student.age) return false;
return name != null ? name.equals(student.name) : student.name == null;
}
}
测试结果:
因为Student两个对象的age和name属性相等,而且又是通过重写的equals方法来判断的,所示stu1.equals(stu2) 为true。但是还有个问题,我们知道Set集合是不保存重复元素的,但是set集合的size还是2,说明在set集合中认为stu1和stu2还是不同的。
这就和我们没有重写hashCode方法有关系了,我们重写一下hashCode方法。
package com.redistext.equals;
/**
* @author: maorui
* @description: TODO
* @date: 2021/10/27 16:53
* @description: V1.0
*/
public class Student {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
if (age != student.age) return false;
return name != null ? name.equals(student.name) : student.name == null;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
}
测试结果:
stu1和stu2通过equals方法比较相等,而且返回的hashCode值一样,所以放入set集合中时只放入了一个对象。
wait、notify方法使用场景
在某些情况下,线程需要交替执行。比如一个线程向一个存储单元执行存放数据,而另一个操作执行取值操作,线程间同步完成这个存取任务,需要将这些线程同步。要解决线程交替执行但是又要保证共享资源安全,这需要使用到wait()、notify()方法。
package com.redistext.wait;
/**
* @author: maorui
* @description: TODO
* @date: 2021/10/27 17:35
* @description: V1.0
*/
public class WaitNotifyTest {
public static void main(String[] args) {
Object lock = new Object();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程A等待获取lock锁");
synchronized (lock){
try {
System.out.println("线程A获取到了lock锁");
Thread.sleep(1000);
System.out.println("线程A将要运行lock.wait()方法进行等待");
lock.wait();
System.out.println("线程A等待结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程B等待获取lock锁");
synchronized (lock){
try {
System.out.println("线程B获取到了lock锁");
Thread.sleep(1000);
System.out.println("线程B将要运行lock.notify()方法进行通知");
lock.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
测试结果:
在上文中,我们说了wait()方法导致线程进入等待状态,直到它被其他线程通过notify()或者notifyAll唤醒。该方法只能在同步方法中调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。
测试代码:
package com.redistext.wait;
/**
* @author: maorui
* @description: TODO
* @date: 2021/10/27 17:35
* @description: V1.0
*/
public class WaitNotifyTest {
public static void main(String[] args) {
Object lock = new Object();
Object lock1 = new Object();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程A等待获取lock锁");
synchronized (lock){
try {
System.out.println("线程A获取到了lock锁");
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("线程A将要运行lock.wait()方法进行等待");
try {
lock1.wait(1000);
} catch (Exception e) {
System.out.println("线程A lock1.wait()方法调用异常");
e.printStackTrace();
}
System.out.println("线程A等待结束");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程B等待获取lock锁");
synchronized (lock){
try {
System.out.println("线程B获取到了lock锁");
Thread.sleep(1000);
System.out.println("线程B将要运行lock.notify()方法进行通知");
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
lock1.notify();
} catch (Exception e) {
System.out.println("线程B lock1.notify()方法调用异常");
e.printStackTrace();
}
}
}
}).start();
}
}
测试结果:
为什么wait、notify必须强制要求放在synchronized中?
在日常开发,我们都知道wait/notify有着非常固定的一套模板,就像上面的例子,synchronized同步块包裹着Object.wait()
方法,如果不通过同步块包住的话JVM会抛出IllegalMonitorStateException
异常。
假设Object.wait()/notify不需要同步,我们来实现一个BlockingQueue的代码。
class BlockingQueue {
Queue<String> buffer = new LinkedList<String>();
public void give(String data) {
buffer.add(data);
notify(); // 往队列里添加的时候notify,因为可能有人在等着take
}
public String take() throws InterruptedException {
while (buffer.isEmpty()) // 用while,防止spurious wakeups(虚假唤醒)
wait(); // 当buffer是空的时候就等着别人give
return buffer.remove();
}
}
上面的代码在多线程的环境下执行,可能会出现这种情况:
- 当前buffer是空的,这时来了一个take的请求,尚未执行到wait语句,执行了buffer.isEmpty()为true
- 同时又来了一个give请求,完整执行完了整个give方法并且发送了notify
- 此时take方法才走到wait,因为它错过了上一个notify,所以会在明明buffer不空的情况下挂起线程,take方法挂起。假如再没有人调用过give方法了,在业务上的表现就会是这个take线程永远也取不到buffer中的内容。
因为你只要用notify,那就是为了在多线程环境下同步,notify/wait机制本身就是为了多线程的同步而存在的,那就只能配套synchronized,所以为了防止上面情况的发生,就直接强制抛异常来限制开发的代码模式了。
为什么wait、notify定义在Object中,而不是Thread中?
参考文档:
java中Object类中有哪些常用方法以及作用_gradonisis的博客-CSDN博客_object有哪些常用方法