JDK源码解析[1]-Object
从本文开始讲陆续对JDK关键源码进行解析和总结。先从Java最为核心的类Object入手。
1.总述
java.lang.Object,是Java所有类的父类,当你在编写一个Java类的时候没有显式的指定extends一个父类,编译器会默认的添加Object为该类的父类。
如果类A有指定父类,那么一直往上追溯,一定能找到最顶层的类没有指定父类,那么,最顶层的类就是Object。
本文基于JDK1.7进行分析讲解,如果有差异请确认JDK版本。
本文是我从我个人目前的水平出发进行的分析,更多内容请结合JDK源码进行进一步的解读。
2.正文
2.1 Object源码全览
public class Object {
//本地方法,C/C++在DLL中实现,通过JNI调用
private static native void registerNatives();
//类初始化调用此方法
static {
registerNatives();
}
//返回此Object的运行时类(每个类的Class类对象)
public final native Class<?> getClass();
//获得该对象的hash值
public native int hashCode();
//对比两对象的内存地址,如果不重写,equals方法比较的是对象地址
public boolean equals(Object obj) {
return (this == obj);
}
//本地clone方法,用于对象的赋值
protected native Object clone() throws CloneNotSupportedException;
//返回对象的的字符串表示,默认是:类名+@+hash值
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
//notify()/notifyAll()/wait()以及wait两个重载方法都是线程同步相关方法
public final native void notify();
public final native void notifyAll();
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 >= 500000 || (nanos != 0 && timeout == 0)) {
timeout++;
}
wait(timeout);
}
public final void wait() throws InterruptedException {
wait(0);
}
//对象被回收时调用,不管如何,一个对象只调用一次
protected void finalize() throws Throwable { }
2.2 详述
Object是Java所有类的祖先类,所以Java所有类都有Object中的方法,因此这些方法是Java所有类所共有的。
既然是所有类共有,代表的也是所有类的共性,比如:equals方法就是用来比较任意两个相同类型对象是否相等的,toString方法是用来将任意对象转换成String,方便打印查看。
当然,以上方法的实现都是默认的,想要实现自己的逻辑需要在自己类中覆写。
以上的native方法在Oracle的jdk是看不到的,但在OpenJDK或其他开源JDK是可以找到对应的C/C++代码的。
2.3 详解
2.3.1 构造方法
源码中并没有Object的构造方法,但是,编译器在编译期会给Object一个默认的空的构造方法:
public Object(){}
事实上,所有的Java类只要类中没有构造方法,编译器都会默认给一个空构造方法,若已有构造方法,则不会添加
2.3.2 registerNatives
带有native修饰的都是本地方法,所谓的本地方法是不通过Java语言实现的方法,但可以通过JNI像调用Java方法一样调用这些方法。详细的可以搜索查看JNI。
这个方法的作用是对Object以下几个本地方法(hashCode/clone/notify等)进行注册(可以理解为,这个方法是告诉JVM这几个本地方法的实现映射),
每一个有本地方法的都会有这个方法,但其内容不一样(因为注册的方法不一样)。
2.3.3 getClass
类在被加载的时候,都会生成一个Class类实例,而getClass这个方法就可以在运行时获得对象的Class对象(这里的对象是堆里的那个对象,也就是获得的是动态类型的那个类),Class对象主要用于反射。
class A{}
class B extends A{}
class C extends B{}
A a = new C(); //对象new C()的静态类型是A,动态类型是C
B b = (B)a; //引用b指向的还是new C(),动态类型还是C
C c = (C)b;
System.out.println(a.getClass().getName());
System.out.println(b.getClass().getName());
System.out.println(c.getClass().getName());
//打印结果均是:com.xxx.test.C
//对象的动态类型是不会变的,即new后面那个类型(构造对象的那个类型),但是静态类型是由指向它的引用决定的,事实上可以这样理解对象只有动态类型,引用类型才是静态类型
//以上说的对象指的是堆里对象,而不是泛指Object o = new Object()中的o
//不明白静态类型,动态类型的可以自行百度
2.3.4 hashCode
该方法主要用途为获得对象的hash值,Java虚拟机规范并没有规定这个方法的具体实现,只是规定了同一个对象多次调用这个方法返回的int值要相等,但并没有规定两个不同对象hash值一定不相同。
具体实现由各个JVM厂商自己实现,所以返回的值意义并不一定(这里特指Object的hashCode方法),有可能返回的是对象的内存地址,也有可能是某个特定计算公式计算出来的值。
2.3.5 equals
equals的作用是用来比较两个对象是否相等,相等的含义为:两个对象其内容是否相等,而不是两个对象是否是同一个对象,即比较其内存地址;
如果想比较两个对象是否是同一个对象即两个引用是否指向同一个对象,直接用==比较即可。因为==比较的就是对象的内存地址。
重要的是,对于Object来说,它并不能知道子类是如何判断他们的两个实例是如何equals的,所以,默认的equals实现,比较的是两对象内存地址,即,若子类不重写equals方法,其作用等同于==。
public boolean equals(Object obj) {
return (this == obj);
}
因此对象比较需要我们重写equals方法。
那么,如何重写equals方法实现判断内容相等呢?关键点取决于你的逻辑,你想让两个对象在什么时候相等,你逻辑上就怎么写。
class A {
public int a;
public String b;
public D d;
@Override
public boolean equals(Object o) {
if (this == o) return true;//如果指向同一个对象,当然equals
//如果o为null或两个对象类型都不相同,当然不equals
if (o == null || getClass() != o.getClass()) return false;
//动态类型相同,强制转换
A a1 = (A) o;
/*下面是自己的逻辑,判断两个对象是否相同,逻辑1 Begin*/
if (a != a1.a) return false;
if (b != null ? !b.equals(a1.b) : a1.b != null) return false;
return d != null ? d.equals(a1.d) : a1.d == null;
//全部字段相同,则equals。如果对象越复杂,想要实现全部字段相同,也就越复杂
/* 逻辑1 End */
/* 逻辑2 begin*/
//只要字段a相同,就认为两个对象equals
if(a == a1.a) return true;
/* 逻辑2 end*/
}
@Override
public int hashCode() {
int result = a;
result = 31 * result + (b != null ? b.hashCode() : 0);
result = 31 * result + (d != null ? d.hashCode() : 0);
return result;
}
}
class D{
public int a;
}
至于别的资料所述的重写equals方法,必重写hashCode,其实不然,若确定所有地方都没有用到类似Map的地方,就不必重写hashCode,因为Map的诸多方法是有用到hashCode方法判断两对象是否相等,而若你仅仅是自己用来判断两个对象是否equals,也就不必重写hashCode。
当然,还要确定其他地方不会用到hashCode,如果不能确定那么还是重写hashcode方法。这样保证任何地方都不会因此出错。
hash相等与对象相等的关系
若hash值不相等,则两个对象肯定不等(不equals);
若hash值相等,两个对象不一定相等(不一定equals)。
equals相等,hash值肯定相等,也就是说,hash值相等是equals相等的必要条件。
hashCode方法一般是用来判断两个对象是否equals的前置条件,这样做的原因是hashCode方法执行速度快,hashcode不相等的可快速否决掉,若hash相同,则再调用equals判断。
2.3.6 clone
克隆方法用于克隆一个与原先对象所有字段值均相等的对象,从而获得一个新的对象,需要注意的是:
- 想要使用这个方法,对象类型必须实现Cloneable接口,否则会报错,原因是Object的clone方法会对对象类型验证,如没实现则报错抛异常;
- clone方法返回的是一个新的对象,这个对象的创建不是通过new关键字创建的,而是JVM通过其他指令创建的;
clone有深clone和浅clone,这主要是针对类中间具有引用类型而言划分的,详情自行查找深拷贝浅拷贝机制。
class A{} A a = new A(); a.clone(); //报错,没有实现Cloneable接口即抛CloneNotSupportedException异常 class A implements Cloneable{} //这样才不会抛异常 //但,若你重写clone方法,并且在这个方法中没有调用父clone(也就是Object)方法 class A{ @Override public Object clone() throws CloneNotSupportedException{ return new A(); } } a.clone();//这个时候调用clone方法即使没有实现Cloneable方法也不会报错 //说白了,你要理解为什么调用clone方法要实现Cloneable的原因,而不是仅仅是记住 //当你理解了,你就能熟练掌握这些规则,而不是记住他们
2.3.7 toString
toString这个方法算是Object比较常用的方法了,它的意义是提供将类的字段以String形式格式化输出这一功能,当然,Object不可能知道子类的字段信息,所以默认toString输出的是:全路径类名+@+hash值。
一般如果我们要输出类的字段信息,需要重写toString方法,输出我们想要的自定义的格式。
2.3.8 notify/notifyAll/wait
这三个方法适用于线程同步,这里只简单介绍其作用,详细请参考JDK源码中关于notify/notifyAll/wait的注释。
- notify:随机唤醒等待(wait)队列中一个对象 通知可能等待该对象的对象锁的其他线程。由JVM(与优先级无关)随机挑选一个处于wait状态的线程。
- 在调用notify()之前,线程必须获得该对象的对象级别锁
- 执行完notify()方法后,不会马上释放锁,要直到退出synchronized代码块,当前线程才会释放锁
- notify()一次只随机通知一个线程进行唤醒
- notifyAll:使所有正在等待池中等待同一共享资源的全部线程从等待状态退出,进入可运行状态 。让它们竞争对象的锁,只有获得锁的线程才能进入就绪状态 。每个锁对象有两个队列:就绪队列和阻塞队列
- 就绪队列:存储将要获得锁的线程
- 阻塞队列:存储被阻塞的线程
- wait:wait方法会把当前线程T放置在对应的object上的等到队列中,在这个对象上的所有同步请求都不会得到响应。需要该对象的线程将不能再继续执行,直到该对象由其他线程调用notify/notifyAll方法唤醒。
2.3.9 finalize
在对象被GC之前被JVM主动调用,用户可以重写这个方法,然后在这个对象回收之前做某些动作,这个方法对于这个对象来说只能调用一次。
而且finalize()被调用不意味着gc会立即回收该对象,所以有可能调用finalize()后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会调用finalize(),产生问题。
所以,推荐不要使用finalize()方法,它跟析构函数不一样。
finalize()是在java.lang.Object里定义的,也就是说每一个对象都有这么个方法。这个方法在gc启动,该对象被回收的时候被调用。其实gc可以回收大部分的对象(凡是new出来的对象,gc都能搞定,一般情况下我们又不会用new以外的方式去创建对象),所以一般是不需要程序员去实现finalize的。
特殊情况下,需要程序员实现finalize,当对象被回收的时候释放一些资源,比如:一个socket链接,在对象初始化时创建,整个生命周期内有效,那么就需要实现finalize,关闭这个链接。
因为GC是不确定性的(这跟JVM相关),所以finalize方法的执行具有不可预知性。
对于finalize而言,若finalize代码中若出现异常,异常会被忽略。
finalize的问题
一些与finalize相关的方法,由于一些致命的缺陷,已经被废弃了,如System.runFinalizersOnExit()方法、Runtime.runFinalizersOnExit()方法
System.gc()与System.runFinalization()方法增加了finalize方法执行的机会,但不可盲目依赖它们
Java语言规范并不保证finalize方法会被及时地执行、而且根本不会保证它们会被执行
finalize方法可能会带来性能问题。因为JVM通常在单独的低优先级线程中完
成finalize的执行
对象再生问题:finalize方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的
finalize方法至多由GC执行一次(用户当然可以手动调用对象的finalize方法,但并不影响GC对finalize的行为)
class D{
public static D d111;
@Override
protected void finalize() throws Throwable {
super.finalize();
d111 = this;//这个时候该对象第一次回收将失败,而以后将不会在执行该方法
System.out.println("finalize a = " + this.a);
}
}
D d = new D();
d = null;
//程序结束
//这个时候,虽然程序结束了,new D()对象也是可回收对象了,但是并不会执行finzlize,
因为对于JVM来说GC的触发条件是内存不足,所以不会执行GC也就不会调用finzlize方法