一、引言
Object是java所有类的基类,是整个类继承结构的顶端,也是最抽象的一个类。大家天天都在使用toString()、equals()、hashCode()、waite()、notify()、getClass()等方法,或许都没有意识到是Object的方法,也没有去看Object还有哪些方法以及思考为什么这些方法要放到Object中。本篇就每个方法具体功能、重写规则以及自己的一些理解。
二、Object方法详解
Object中含有: registerNatives()、getClass()、hashCode()、equals()、clone()、toString()、notify()、notifyAll()、wait(long)、wait(long,int)、wait()、finalize()共十二个方法。 这个顺序是按照Object类中定义方法的顺序列举的,下面我也会按照这个顺序依次进行讲解。
1.1、registerNatives()
public class Object {
private static native void registerNatives();
static {
registerNatives();
}
}
什么鬼?哈哈哈,我刚看到这方法,一脸懵逼。从名字上理解,这个方法是注册native方法(本地方法,由JVM实现,底层是C/C++实现的)向谁注册呢?当然是向JVM,当有程序调用到native方法时,JVM才好去找到这些底层的方法进行调用。
Object中的native方法,并使用registerNatives()向JVM进行注册。(这属于JNI的范畴,9龙暂不了解,有兴趣的可自行查阅。)
static JNINativeMethod methods[] = {
{"hashCode", "()I", (void *)&JVM_IHashCode},
{"wait", "(J)V", (void *)&JVM_MonitorWait},
{"notify", "()V", (void *)&JVM_MonitorNotify},
{"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},
{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
};
为什么要使用静态方法,还要放到静态块中呢?
上一篇整理了类加载流程, 我们知道了在类初始化的时候,会依次从父类到本类的类变量及类初始化块中的类变量及方法按照定义顺序放到< clinit>方法中,这样可以保证父类的类变量及方法的初始化一定先于子类。 所以当子类调用相应native方法,比如计算hashCode时,一定可以保证能够调用到JVM的native方法。
1.2、getClass()
public final native ClassgetClass():这是一个public的方法,我们可以直接通过对象调用。
类加载的第一阶段类的加载就是将.class文件加载到内存,并生成一个java.lang.Class对象的过程。getClass()方法就是获取这个对象,这是当前类的对象在运行时类的所有信息的集合。这个方法是反射三种方式之一。
1.2.1、反射三种方式:
对象的getClass();类名.class;Class.forName();class extends ObjectTest {
private void privateTest(String str) {
System.out.println(str);
}
public void say(String str) {
System.out.println(str);
}
}
public class ObjectTest {
public static void main(String[] args) throws Exception {
ObjectTest = new ();
//获取对象运行的Class对象
Class aClass = .getClass();
System.out.println(aClass);
//getDeclaredMethod这个方法可以获取所有的方法,包括私有方法
Method privateTest = aClass.getDeclaredMethod("privateTest", String.class);
//取消java访问修饰符限制。
privateTest.setAccessible(true);
privateTest.invoke(aClass.newInstance(), "private method test");
//getMethod只能获取public方法
Method say = aClass.getMethod("say", String.class);
say.invoke(aClass.newInstance(), "Hello World");
}
}
//输出结果:
//class test.
//private method test
//Hello World
反射主要用来获取运行时的信息,可以将java这种静态语言动态化,可以在编写代码时将一个子对象赋值给父类的一个引用,在运行时通过反射可以或许运行时对象的所有信息,即多态的体现。对于反射知识还是很多的,这里就不展开讲了。
1.3、hashCode()
public native int hashCode();这是一个public的方法,所以子类可以重写它。这个方法返回当前对象的hashCode值,这个值是一个整数范围内的(-2^31 ~ 2^31 - 1)数字。
对于hashCode有以下几点约束
在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改;如果两个对象 x.equals(y) 方法返回true,则x、y这两个对象的hashCode必须相等。如果两个对象x.equals(y) 方法返回false,则x、y这两个对象的hashCode可以相等也可以不等。 但是,为不相等的对象生成不同整数结果可以提高哈希表的性能。默认的hashCode是将内存地址转换为的hash值,重写过后就是自定义的计算方式;也可以通过System.identityHashCode(Object)来返回原本的hashCode。public class HashCodeTest {
private int age;
private String name;
@Override
public int hashCode() {
Object[] a = Stream.of(age, name).toArray();
int result = 1;
for (Object element : a) {
result = 31 * result + (element == null ? 0 : element.hashCode());
}
return result;
}
}
推荐使用Objects.hash(Object… values)方法。相信看源码的时候,都看到计算hashCode都使用了31作为基础乘数, 为什么使用31呢?我比较赞同与理解result * 31 = (result<<5) - result。JVM底层可以自动做优化为位运算,效率很高;还有因为31计算的hashCode冲突较少,利于hash桶位的分布。
1.4、equals()
public boolean equals(Object obj);用于比较当前对象与目标对象是否相等,默认是比较引用是否指向同一对象。为public方法,子类可重写。
public class Object{
public boolean equals(Object obj) {
return (this == obj);
}
}
为什么需要重写equals方法?
因为如果不重写equals方法,当将自定义对象放到map或者set中时;如果这时两个对象的hashCode相同,就会调用equals方法进行比较,这个时候会调用Object中默认的equals方法,而默认的equals方法只是比较了两个对象的引用是否指向了同一个对象,显然大多数时候都不会指向,这样就会将重复对象存入map或者set中。这就 破坏了map与set不能存储重复对象的特性,会造成内存溢出 。
重写equals方法的几条约定:
自反性:即x.equals(x)返回true,x不为null;对称性:即x.equals(y)与y.equals(x)的结果相同,x与y不为null;传递性:即x.equals(y)结果为true, y.equals(z)结果为true,则x.equals(z)结果也必须为true;一致性:即x.equals(y)返回true或false,在未更改equals方法使用的参数条件下,多次调用返回的结果也必须一致。x与y不为null。如果x不为null, x.equals(null)返回false。
我们根据上述规则来重写equals方法。
public class EqualsTest{
private int age;
private String name;
//省略get、set、构造函数等
@Override
public boolean equals(Object o) {
//先判断是否为同一对象
if (this == o) {
return true;
}
//再判断目标对象是否是当前类及子类的实例对象
//注意:instanceof包括了判断为null的情况,如果o为null,则返回false
if (!(o instanceof )) {
return false;
}
that = () o;
return age == that.age &&
Objects.equals(name, that.name);
}
public static void main(String[] args) throws Exception {
EqualsTest1 equalsTest1 = new EqualsTest1(23, "9龙");
EqualsTest1 equalsTest12 = new EqualsTest1(23, "9龙");
EqualsTest1 equalsTest13 = new EqualsTest1(23, "9龙");
System.out.println("-----------自反性----------");
System.out.println(equalsTest1.equals(equalsTest1));
System.out.println("-----------对称性----------");
System.out.println(equalsTest12.equals(equalsTest1));
System.out.println(equalsTest1.equals(equalsTest12));
System.out.println("-----------传递性----------");
System.out.println(equalsTest1.equals(equalsTest12));
System.out.println(equalsTest12.equals(equalsTest13));
System.out.println(equalsTest1.equals(equalsTest13));
System.out.println("-----------一致性----------");
System.out.println(equalsTest1.equals(equalsTest12));
System.out.println(equalsTest1.equals(equalsTest12));
System.out.println("-----目标对象为null情况----");
System.out.println(equalsTest1.equals(null));
}
}
//输出结果
//-----------自反性----------
//true
//-----------对称性----------
//true
//true
//-----------传递性----------
//true
//true
//true
//-----------一致性----------
//true
//true
//-----目标对象为null情况----
//false
从以上输出结果验证了我们的重写规定是正确的。
注意:instanceof 关键字已经帮我们做了目标对象为null返回false,我们就不用再去显示判断了。
建议equals及hashCode两个方法,需要重写时,两个都要重写,一般都是将自定义对象放至Set中,或者Map中的key时,需要重写这两个方法。
1.4、clone()
protected native Object clone() throws CloneNotSupportedException;
此方法返回当前对象的一个副本。
这是一个protected方法,提供给子类重写。但需要实现Cloneable接口,这是一个标记接口,如果没有实现,当调用object.clone()方法,会抛出CloneNotSupportedException。
public class CloneTest implements Cloneable {
private int age;
private String name;
//省略get、set、构造函数等
@Override
protected CloneTest clone() throws CloneNotSupportedException {
return (CloneTest) super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
CloneTest cloneTest = new CloneTest(23, "9龙");
CloneTest clone = cloneTest.clone();
System.out.println(clone == cloneTest);
System.out.println(cloneTest.getAge()==clone.getAge());
System.out.println(cloneTest.getName()==clone.getName());
}
}
//输出结果
//false
//true
//true
从输出我们看见,clone的对象是一个新的对象;但原对象与clone对象的String类型的name却是同一个引用,这表明,super.clone方法对成员变量如果是引用类型,进行是浅拷贝。
那什么是浅拷贝?对应的深拷贝?
浅拷贝:拷贝的是引用。
深拷贝:新开辟内存空间,进行值拷贝。
那如果我们要进行深拷贝怎么办呢?看下面的例子。
class Person implements Cloneable{
private int age;
private String name;
//省略get、set、构造函数等
@Override
protected Person clone() throws CloneNotSupportedException {
Person person = (Person) super.clone();
//name通过new开辟内存空间
person.name = new String(name);
return person;
}
}
public class CloneTest implements Cloneable {
private int age;
private String name;
//增加了person成员变量
private Person person;
//省略get、set、构造函数等
@Override
protected CloneTest clone() throws CloneNotSupportedException {
CloneTest clone = (CloneTest) super.clone();
clone.person = person.clone();
return clone;
}
public static void main(String[] args) throws CloneNotSupportedException {
CloneTest cloneTest = new CloneTest(23, "9龙");
Person person = new Person(22, "路飞");
cloneTest.setPerson(person);
CloneTest clone = cloneTest.clone();
System.out.println(clone == cloneTest);
System.out.println(cloneTest.getAge() == clone.getAge());
System.out.println(cloneTest.getName() == clone.getName());
Person clonePerson = clone.getPerson();
System.out.println(person == clonePerson);
System.out.println(person.getName() == clonePerson.getName());
}
}
//输出结果
//false
//true
//true
//false
//false
可以看到,即使成员变量是引用类型,我们也实现了深拷贝。 如果成员变量是引用类型,想实现深拷贝,则成员变量也要实现Cloneable接口,重写clone方法。
1.5、toString()
public String toString();这是一个public方法,子类可重写, 建议所有子类都重写toString方法,默认的toString方法,只是将当前类的全限定性类名+@+十六进制的hashCode值。
public class Object{
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
}
我们思考一下为什么需要toString方法?
我这么理解的,返回当前对象的字符串表示,可以将其打印方便查看对象的信息,方便记录日志信息提供调试。
我们可以选择需要表示的重要信息重写到toString方法中。为什么Object的toString方法只记录类名跟内存地址呢?因为Object没有其他信息了,哈哈哈。
1.6、wait()/ wait(long)/ waite(long,int)
这三个方法是用来线程间通信用的,作用是阻塞当前线程,等待其他线程调用notify()/notifyAll()方法将其唤醒。这些方法都是public final的,不可被重写。
注意:
此方法只能在当前线程获取到对象的锁监视器之后才能调用,否则会抛出IllegalMonitorStateException异常。调用wait方法,线程会将锁监视器进行释放;而Thread.sleep,Thread.yield()并不会释放锁 。wait方法会一直阻塞,直到其他线程调用当前对象的notify()/notifyAll()方法将其唤醒;而wait(long)是等待给定超时时间内(单位毫秒),如果还没有调用notify()/nofiyAll()会自动唤醒;waite(long,int)如果第二个参数大于0并且小于999999,则第一个参数+1作为超时时间;public final void wait() throws InterruptedException {
wait(0);
}
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
1.7、notify()/notifyAll()
前面说了, 如果当前线程获得了当前对象锁,调用wait方法,将锁释放并阻塞;这时另一个线程获取到了此对象锁,并调用此对象的notify()/notifyAll()方法将之前的线程唤醒。 这些方法都是public final的,不可被重写。
public final native void notify();随机唤醒之前在当前对象上调用wait方法的一个线程public final native void notifyAll();唤醒所有之前在当前对象上调用wait方法的线程
下面我们使用wait()、notify()展示线程间通信。假设9龙有一个账户,只要9龙一发工资,就被女朋友给取走了。
//账户
public class Account {
private String accountNo;
private double balance;
private boolean flag = false;
public Account() {
}
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
/**
* 取钱方法
*
* @param drawAmount 取款金额
*/
public synchronized void draw(double drawAmount) {
try {
if (!flag) {
//如果flag为false,表明账户还没有存入钱,取钱方法阻塞
wait();
} else {
//执行取钱操作
System.out.println(Thread.currentThread().getName() + " 取钱" + drawAmount);
balance -= drawAmount;
//标识账户已没钱
flag = false;
//唤醒其他线程
notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void deposit(double depositAmount) {
try {
if (flag) {
//如果flag为true,表明账户已经存入钱,取钱方法阻塞
wait();
} else {
//存钱操作
System.out.println(Thread.currentThread().getName() + " 存钱" + depositAmount);
balance += depositAmount;
//标识账户已存入钱
flag = true;
//唤醒其他线程
notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//取钱者
public class DrawThread extends Thread {
private Account account;
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
@Override
public void run() {
//循环6次取钱
for (int i = 0; i < 6; i++) {
account.draw(drawAmount);
}
}
}
//存钱者
public class DepositThread extends Thread {
private Account account;
private double depositAmount;
public DepositThread(String name, Account account, double depositAmount) {
super(name);
this.account = account;
this.depositAmount = depositAmount;
}
@Override
public void run() {
//循环6次存钱操作
for (int i = 0; i < 6; i++) {
account.deposit(depositAmount);
}
}
}
//测试
public class DrawTest {
public static void main(String[] args) {
Account brady = new Account("9龙", 0);
new DrawThread("女票", brady, 10).start();
new DepositThread("公司", brady, 10).start();
}
}
//输出结果
//公司 存钱10.0
//女票 取钱10.0
//公司 存钱10.0
//女票 取钱10.0
//公司 存钱10.0
//女票 取钱10.0
例子中我们通过一个boolean变量来判断账户是否有钱,当取钱线程来判断如果账户没钱,就会调用wait方法将此线程进行阻塞;这时候存钱线程判断到账户没钱, 就会将钱存入账户,并且调用notify()方法通知被阻塞的线程,并更改标志;取钱线程收到通知后,再次获取到cpu的调度就可以进行取钱。反复更改标志,通过调用wait与notify()进行线程间通信。实际中我们会时候生产者消费者队列会更简单。
注意:调用notify()后,阻塞线程被唤醒,可以参与锁的竞争,但可能调用notify()方法的线程还要继续做其他事,锁并未释放,所以我们看到的结果是,无论notify()是在方法一开始调用,还是最后调用,阻塞线程都要等待当前线程结束才能开始。
为什么wait()/notify()方法要放到Object中呢?
因为每个对象都可以成为锁监视器对象,所以放到Object中,可以直接使用。
1.8、finalize()
protected void finalize() throws Throwable ;
此方法是在垃圾回收之前,JVM会调用此方法来清理资源。此方法可能会将对象重新置为可达状态,导致JVM无法进行垃圾回收。
我们知道java相对于C++很大的优势是程序员不用手动管理内存,内存由jvm管理;如果我们的引用对象在堆中没有引用指向他们时,当内存不足时,JVM会自动将这些对象进行回收释放内存,这就是我们常说的垃圾回收。但垃圾回收没有讲述的这么简单。
finalize()方法具有如下4个特点:
永远不要主动调用某个对象的finalize()方法,该方法由垃圾回收机制自己调用;finalize()何时被调用,是否被调用具有不确定性;当JVM执行可恢复对象的finalize()可能会将此对象重新变为可达状态;当JVM执行finalize()方法时出现异常,垃圾回收机制不会报告异常,程序继续执行。public class FinalizeTest {
private static FinalizeTest ft = null;
public void info(){
System.out.println("测试资源清理得finalize方法");
}
public static void main(String[] args) {
//创建FinalizeTest对象立即进入可恢复状态
new FinalizeTest();
//通知系统进行垃圾回收
System.gc();
//强制回收机制调用可恢复对象的finalize()方法
// Runtime.getRuntime().runFinalization();
System.runFinalization();
ft.info();
}
@Override
public void finalize(){
//让ft引用到试图回收的可恢复对象,即可恢复对象重新变成可达
ft = this;
throw new RuntimeException("出异常了,你管不管啊");
}
}
//输出结果
//测试资源清理得finalize方法
我们看到,finalize()方法将可恢复对象置为了可达对象,并且在finalize中抛出异常,都没有任何信息,被忽略了。
1.8.1、对象在内存中的状态
对象在内存中存在三种状态:
可达状态:有引用指向,这种对象为可达状态;可恢复状态:失去引用,这种对象称为可恢复状态;垃圾回收机制开始回收时,回调用可恢复状态对象的finalize()方法(如果此方法让此对象重新获得引用,就会变为可达状态,否则,会变为不可大状态)。不可达状态:彻底失去引用,这种状态称为不可达状态,如果垃圾回收机制这时开始回收,就会将这种状态的对象回收掉。
1.8.2、垃圾回收机制
垃圾回收机制只负责回收堆内存种的对象,不会回收任何物理资源(例如数据库连接、网络IO等资源);程序无法精确控制垃圾回收的运行,垃圾回收只会在合适的时候进行。当对象为不可达状态时,系统会在合适的时候回收它的内存。在垃圾回收机制回收任何对象之前,总会先调用它的finalize()方法 ,该方法可能会将对象置为可达状态,导致垃圾回收机制取消回收。
1.8.3、强制垃圾回收
上面我们已经说了,当对象失去引用时,会变为可恢复状态,但垃圾回收机制什么时候运行,什么时候调用finalize方法无法知道。虽然垃圾回收机制无法精准控制,但java还是提供了方法可以建议JVM进行垃圾回收,至于是否回收,这取决于虚拟机。但似乎可以看到一些效果。
public class GcTest {
public static void main(String[] args){
for(int i=0;i<4;i++){
//没有引用指向这些对象,所以为可恢复状态
new GcTest();
//强制JVM进行垃圾回收(这只是建议JVM)
System.gc();
//Runtime.getRuntime().gc();
}
}
@Override
public void finalize(){
System.out.println("系统正在清理GcTest资源。。。。");
}
}
//输出结果
//系统正在清理GcTest资源。。。。
//系统正在清理GcTest资源。。。。
System.gc(),Runtime.getRuntime().gc()两个方法作用一样的,都是建议JVM垃圾回收,但不一定回收,多运行几次,结果可能都不一致。
三、总结
本篇举例讲解了Objec中的所有方法的作用、意义及使用,从java最基础的类出发,感受java设计之美吧。我是不会高诉大家,这好像面试也会问的【摊手】。
欢迎工作一到五年的Java工程师朋友们加入Java程序员开发: 721575865
群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!