Java源码通俗理解之Object类


参考文章:

https://fangjian0423.github.io/2016/03/12/java-Object-method/

Object类简介

object是java中最基本的类,所有的java类全部继承自object类,哪怕创建一个新类的时候不用extend声明继承自哪个类也会默认继承object类,所以哪怕只是写了一个空类也最少会有11个方法,那就是他的超类object类里的11个方法.

方法总览

  1. public final native Class<?> getClass()
  2. public native int hashCode()
  3. public boolean equals(Object obj)
  4. protected native Object clone() throws CloneNotSupportedException
  5. public String toString()
  6. public final native void notify()
  7. public final native void notifyAll()
  8. public final native void wait(long timeout) throws InterruptedException
  9. public final void wait(long timeout, int nanos) throws InterruptedException
  10. public final void wait() throws InterruptedException
  11. protected void finalize() throws Throwable { }

1.public final native Class<?> getClass()

从getClass()方法的声明可以看出此方法不是用java语言实现的并且不允许子类进行重写.返回运行时的类型信息

通过这个方法可以获得该类的类型信息,其实就是类型名,但这个是类型时运行时的类型.当需要判断一个对象的当前类型时就可以使用它来获取当前的类型.而class方法不行.

需要注意得是,这个方法返回值是在运行时才可以确定的,而不像class()方法一样在编译时就确定了,所以getClass()在出现继承和多态时能返回出运行到这一刻时的真正所属类型.

2.public native int hashCode()

hashCode()方法也不是用java语言实现的.返回对象的哈希码

这个方法主要用在哈希表中,比如HashMap,HashSet之类的地方,当我们向哈希表中添加对象时,哈希码就起到定位的作用.如果哈希码对应的位置没有对象,则可以直接将对象插入进去.

通过这个方法可以获得该对象的哈希码,如果不重写的话也可以调用,系统会帮你用默认方法生成一个.如果想提高性能的话也可以自己重写,但必须额遵守以下规则:

 1. 在java程序执行过程中,在一个对象没有被改变的前提下,无论这个对象被调用多少次,hashCode方法都会返回相同的整数值。对象的哈希码没有必要在不同的程序中保持相同的值。


 2. 如果2个对象使用equals方法进行比较并且相同的话,那么这2个对象的hashCode方法的值也必须相等。


 3. 如果根据equals方法,得到两个对象不相等,那么这2个对象的hashCode值不需要必须不相同。但是,不相等的对象的hashCode值不同的话可以提高哈希表的性能。

这个声明可能有点绕,但挺通俗的了.但这里面也出现了一个问题,通常情况下不同对象产生的哈希码是不相等的,但有的时候总有那么巧两个不同的对象hashCode值相同了,如果向哈希表插入的时候出现这个情况岂不是明明没重复也插不进去了吗.
其实不然,不是还有equals()方法保证准确性,equals()方法一定能判断出是否是同一个对象.但每一次都调用equals()方法进行比较的话会消耗较多的系统资源,所以这也是为什么第三条规则中说不相等的对象hashCode值不同可以提高性能的原因,比hashCode值就比出不同了就不用在进行equals比较了.这顺便也解释了为什么equals比较相同hashCode值也必须相同了.哈希表在比较时都是先比较hashCode值,hashCode相同才进行equals比较,hashCode不同的话就不会再进行equals比较了.

《Effective Java》中提出了一种简单通用的hashCode算法,参考一下就好:

A、初始化一个整形变量,为此变量赋予一个非零的常数值,比如int result = 17;
B、选取equals方法中用于比较的所有域(之所以只选择equals()中使用的域,是为了保证上述原则的第1条),然后针对每个域的属性进行计算:

(1) 如果是boolean值,则计算f ? 1:0
(2) 如果是byte\char\short\int,则计算(int)f
(3) 如果是long值,则计算(int)(f ^ (f >>> 32))
(4) 如果是float值,则计算Float.floatToIntBits(f)
(5) 如果是double值,则计算Double.doubleToLongBits(f),然后返回的结果是long,再用规则(3)去处理long,得到int
(6) 如果是对象应用,如果equals方法中采取递归调用的比较方式,那么hashCode中同样采取递归调用hashCode的方式。否则需要为这个域计算一个范式,比如当这个域的值为null的时候,那么hashCode 值为0
(7) 如果是数组,那么需要为每个元素当做单独的域来处理。java.util.Arrays.hashCode方法包含了8种基本类型数组和引用数组的hashCode计算,算法同上。

C、最后,把每个域的散列码合并到对象的哈希码中。 

如果要进行重写从而提高性能的话建议一定要把equals方法一起重写,以防出现不符合规则的情况出现,从而导致无法意料的错误产生.

3.public boolean equals(Object obj)

这个方法是用java语言实现的,返回比对是否成功

比较准确率最高的方法其实代码只有这么点
从上面的图片可以看到,Object类的实现采用了区分度最高的算法,即只要两个对象内存地址不一样,那么equals()一定返回false

当然我们在写代码的时候如果需要同样可以重写这个方法,但也要遵循一些原则:

 1. 自反性:x.equals(x)必须返回true.
 2. 对称性:x.equals(y)与y.equals(x)的返回值必须相等.
 3. 传递性:x.equals(y)为true,y.equals(z)也为true,那么x.equals(z)必须为true.
 4. 一致性:如果对象x和y在equals()中使用的信息都没有改变,那么x.equals(y)值始终不变.
 5. 非null:x不是null,y为null,则x.equals(y)必须为false

这五条原则也挺通俗的,就不解释了,说一下可能要重写的应用场景,举个栗子:

都知道String类型在进行比较时要使用equals方法进行比较,而不能用"=".因为"=“在比较两个对象时比较的是比较两个对象的内存地址是否相同.两个String类型的数据内存地址肯定是不相同的,但内容却不一定.这个时候就需要重写equals方法了,否则按照object类中的默认实现其实还是用”=“进行比较.而字符串的比较没有谁是去对比内存地址是否一致,都是比较内容是否相同,所以要进行重写.
同理,如果设计某个类的时候可能会将两个实现的某个或某几个成员变量进行比较,就可以通过重写这个方法来实现.至于比较两个对象是否相同的活交给”="好了.

同样,如果要进行重写从而提高性能的话建议一定要把hashCode方法一起重写,以防出现不符合规则的情况出现,从而导致无法意料的错误产生.

4.protected native Object clone() throws CloneNotSupportedException

这个方法不是用java语言实现的,也是少有的protected权限的方法,作用是将当前对象拷贝一份,返回拷贝的对象,可能抛出CloneNotSupportedException异常

一般情况下,任何对象的拷贝都不等于原对象,也就是 obj.clone()==obj 的结果是false.但他俩的类型应该是一致的,也就是 obj.clone().getClass()==obj.getClass() 的结果应该是true.

如果想要使用这个方法的话一定要记得实现Cloneable接口,CloneNotSupportedException异常就是为不重写得人准备的.Cloneable接口只是用来声明Object类中的clone方法可以用来合法进行克隆,简单讲就是实现这个接口就不抛异常了.

至于为啥需要这个方法嘞?
当然是因为传统的"="在进行对象的传递时用的不是值传递而是引用传递.相当于只是给对象起了个别名,在修改任意一个对象的值得时候两个对象的值都会被修改,所以在需要只修改一个对象的时候就用到这个方法了.

当然克隆和克隆还有区别,主要分为深克隆和浅克隆两种:

两者的共同点是被复制对象的所有变量与原来的对象值都相同.
不同点是浅克隆的对象里面所有的对其他对象的引用都和原对象指向同一个地方,深克隆连引用的对象都一块复制了一份,典型的例子就是序列化然后接着反序列化.这其实就是一次深克隆.
也就是说浅克隆其实也没有克隆全,当修改克隆对象的非基本数据类型时原对象的相关数据也会变化,而深克隆不会.

5.public String toString()

这个方法的默认实现是输出类的名字@实例的哈希码的16进制,实际上应该输出一个简明但易于读懂的字符串

toString方法一样很短
正常情况下我们调用这个方法主要是将对象按字符串的方式输出出来,用白话说就是:使用文字描述这个对象里各个变量是什么值 ,这个变量是什么类型的变量等.但Object类中的默认实现显然不是这么想的,所以建议每个类都重写一个toString方法,否则在调用这个方法的时候得到的将是一个看不懂的内存地址.

这个方法经常跟equals在一起使用,如果想要比较两个对象的成员变量值是否全部相同可以用toString方法将所有变量的值按照某种格式转变成字符串在进行字符串比较.

6.public final native void notify()

这个方法不是用java实现的,也不让子类重写,也没有返回值,作用是唤醒一个正在这个对象监视器上等待的线程

我们常说的对象说白了就是一块内存地址,各种对对象的操作就是操作那块内存地址罢了,但这会出现一个问题,如果同时有两个线程操作那一块内存的话可能会有一个线程出现错误结果(并发的问题),这个时候如果有多个线程要操作这块内存的话就只能都在休息室等着.一次只能进一个的控制方式就是独占锁,而notify方法就是通知一个等待中的线程到他工作了,当然,这个通知是随机的.

一次只能有一个线程拥有这个锁,正规点的叫法是一次只能有一个线程拥有对象的监视器,监视器跟锁的概念差不多.

通过上面说的我们可以判断,这个方法不出意外必须跟synchronized一起用,就是说这个东西必须放到synchronized的那个花括号中.事实上也确实如此,类似的还有notifyAll方法,三个wait方法

必须注意虽然这个方法没有throws,但他也是会抛出异常的:

如果当前线程不是此对象监视器的所有者的话会抛出IllegalMonitorStateException异常

因为notify只能在拥有对象监视器的所有者线程中调用,否则会抛出IllegalMonitorStateException异常

7.public final native void notifyAll()

这个方法和上面那个方法一样的,为一个区别就是这个方法会唤醒所有等待的线程
.

.
这个方法除了唤醒数量之外连抛出异常都跟notify方法一模一样的,作用看notify方法就好了
同样,这个方法和notify,wait一样要与synchronized一起用,详情请看notify里的介绍
.
.

8.public final native void wait(long timeout) throws InterruptedException

这个方法不是用java语言实现的,不允许子类重写,作用是让当前线程等待直到另外一个线程调用对象的notify或notifyAll方法,可能抛出InterruptedException异常

这个方法一般是和notify或者notifyAll方法配合使用的,这个方法会让当前的线程进入等待状态,直到调用那两个方法后恢复,或者等到超时自动恢复,如果想设置不唤醒的话一直等到天荒地老可以试试传入0.

要注意一下,线程有时候还会唤醒一个所谓的虚假唤醒(spurious wakeup).虽然很少发生,但也应该预防一下,比如将等待放在while循环当中,只要不满足条件就再通知一遍进入休眠.

同样,这个方法和notify,notifyAll一样要与synchronized一起用,详情请看notify里的介绍

9.public final void wait(long timeout, int nanos) throws InterruptedException

是用java语言实现的,不允许子类重写,作用是让当前线程等待直到另外一个线程调用对象的notify或notifyAll方法,可能抛出InterruptedException异常

其实就是多了一个参数
这个方法看代码就能发现其实是糊弄事的,原来还有个四舍五入,现在连四舍五入也省了,实际上还是调用的上一个方法,所以功能看上面那个方法就好了.

10.public final void wait() throws InterruptedException

这个方法可能抛出InterruptedException异常,作用和前两个一样

看源码就知道什么情况了
这个方法看源码就知道了,就是一个天荒地老版的wait方法罢了.详情请看上上个方法介绍

11.protected void finalize() throws Throwable { }

这个方法也是少有的protected权限,作用就是没有作用

这个方法Object类里面没有实现,空方法就不贴代码了

这个方法在实例被垃圾回收器回收前会被触发一下,如果想让实例在死前进行最后一波挣扎的话,你值得拥有,但官方说不建议子类覆盖这个方法,防止父类的垂死挣扎失败.

总结

虽然越到后面感觉写的越少,但偷懒这个锅我不背.后面的方法基本都是重载前面的方法或者和前面的方法进行配套,不过也能学到很多东西,比如wait方法怎么永久等待其他人是不知道的,你必须考虑到这点,比如添加一个专门的重载方法处理这个情况,而不是指望别人去你的代码里看注释.其他收获就看怎么理解了.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值