作为一名java开发,肯定会知道object类,object类是所有类的基类,当一个类没有直接继承任何类时,默认继承object类,所以也被称之为“上帝类”。
目录
一、继承Object的时机
由此,我们抛出一个问题,object类时什么时候成为一个类的父类的?是编译时还是虚拟机处理的时候。
这里我们通过JDK自带的反编译工具javap来看一下就明白了
可以看到在编译时期就已经继承了object类
二、类中方法解析
这是object类中方法,其中共有7个native方法,也就是说这七个方法并不是java本身去完成的,而是通过c/c++来完成,放在.dll动态文件中
关于如何通过java语言调用c/c++的代码,则是有标准的JNI协议来定义的,感兴趣可以自行去了解一下
1.类构造器
每个类都会有类构造器,如果没有声明,则会默认创建一个无参构造
2.equals方法
这个方法相信大家再熟悉不过了,面试中也是一个高频问点,比如经典的:
- “equals()和==有什么区别”
- “有没有重写过equals”
- “什么情况下需要重写equals()以及hashcode()”
在Object中的equals()其实跟==是一样的,都是比较两个对象的引用是否相等,也就是说,如果两个对象的引用相等,那么它俩的对象一定是相等的
public boolean equals(Object obj) {
return (this == obj);
}
那肯定就有人有疑惑了,那为啥String当中的equals()也会去比较值是否相等呢,那正是因为String对equals()进行了重写。先是比较了引用,然后再遍历比较值是否相等(下图为jdk11的代码,如果是jdk8则不会将这个方法拆开分情况处理)
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
for (int i = 0; i < value.length; i++) {
if (value[i] != other[i]) {
return false;
}
}
return true;
}
return false;
}
所以,当我们自定义一个类时,如果这个类不重写equals方法,那么只会比较引用,并不会去比较值,值得注意的是,重写equals方法通常都必须重写hashcode方法,以维护hashcode方法的一般约定,该方法声明相等对象必须具有相同的哈希代码
3.hashcode方法
该方法在object中为native方法,作用就是返回对象的散列码,是int类型的数值
HashCode的存在主要是为了查找的快捷性,用来在散列存储结构中确定对象的存储地址,比如说,我们判断一个元素是否相等可以通过equals方法,但是如果集合中有10000个元素,当我们添加一个新元素时,就要调用10000次equals来比较,显然效率很低。
于是java的集合设计者就采用了哈希表来实现,就是将数据依照特定的算法产生的结果指定到一个地址上,这样当集合需要添加新元素的时候,调用其hashcode,就能计算出应该将其存放到什么位置上。
那么由此产生了几种情况:
- 该位置没有元素,则直接存放
- 该位置有元素,调用equals进行比较,如果相同则无需再存储
- 如果不相同,也就是意味着发生了哈希冲突,那这个时候则会在该位置产生一个链表,将产生相同的hashcode存放到这个链表中(如果看过HashMap源码的话就知道了)
那么hashcode到底是啥,是否为对象的内存地址?
要清楚这一点我们需要知道,内存地址是不可能重复的,所以只要产生hashcode重复的情况就能证明hashcode不为内存地址
public class HashCode_Test {
public static void main(String[] args) {
int num =0;
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 200000; i++) {
Object o = new Object();
if(list.contains(o.hashCode())){
num++;
}else{
list.add(o.hashCode());
}
}
System.out.println("重复hashcode个数为:"+num);
System.out.println("不重复hashcode个数为:"+list.size());
}
}
重复hashcode个数为:8
不重复hashcode个数为:199992
可以看到在循环20万次时,会出现重复的情况,所以先是佐证了hashcode并不是内存地址
那么hashcode的存储位置又在哪里呢?
这个时候我们需要先了解一下啊对象的内存布局
hashcode存放在java的对象头当中,对象头分为两个部分,一个是MarkWord(标记字段),另一个是Class Pointer(类型指针)。其中Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。、
下图为MarkWord的机构
当一个对象处于无锁状态时,会用31个bit来存储hashcode;当该对象有锁无论是轻量级锁、重量级锁都没有存放hashcode的空间,因为被指针占据了
如果在偏向锁的过程中,调用了hashcode方法,则会先撤销偏向锁,因为存储hashcode的bit被线程id占据了,所以得先撤销再去生成hashcode
如果有线程硬性调用对象的锁,则对象再也回不到偏向锁状态,而是升级为重量级锁,hashcode跟随mark word被移动到c的object monitor中
4.clone方法
该方法时实现对象的浅拷贝,也就是说只是将指针指向了该对象,而深拷贝则是重新创建了一个一样的对象
值得注意的是,被拷贝的对象需要实现Cloneable接口才行
5.getClass方法
作用是返回对象的运行时类
6.toString方法
作用是返回对象的全类名+十六进制的hashcode,我们打印某个类时,就是默认打印的该类的toString方法
7.finalize方法
当GC确定不再有对该对象的引用时,GC会调用对象的finalize方法来进行回收,jvm会确保一个对象的finalize方法只会被调用一次,并且程序中不能直接调用它
8.registerNatives方法
一个类如果要调用操作系统的实现,则必须要装载本地库,由于Object中用了static代码块调用该方法,所以在类加载时必定会执行该方法
以上就是Object的源码解析,重点还是在hashcode以及equals那部分