Object 类是 Java 语言中所有类都会隐式继承的类,对于自己所创建的类即使不写 extends 也是会继承 Obejct 类的,俗称上帝类,所以想翻阅 JDK 源码,首先来翻翻这个类的源码是很有必要的。
Object 类中常用方法解析
getClass() 方法
由 final 修饰,不允许子类去重写,而且也是一个本地方法,作用是返回当前运行时对象的 Class 类型对象,获取到此 Class 类型对象后,通过此对象可以获取到此类的各种信息,比如说可以调用 Class 类中的 getName 方法获取到此类的全类名。如下:
hashCode() 方法
也是一个本地方法,返回的是一个 int 类型,默认情况下使用的是以对象的内部地址做哈希运算,返回的一个 int 值,只能保证相同类的不同实例返回的值是不同的,不同类的实例返回的值可能相同(具体实现取决于JVM)。其实在数学上,hash 函数本质是一个非对称的加密算法,是不可逆的算法,明文与密文是多对一的关系,所以哈希函数也是只能做顺推,而不可以逆推的。hashCode() 在 Java 语言中是非常有用的,往往自己写的类中,很多时候也是需要重写这个方法的,HashMap 的 key 哈希表实现中也是用到了hashCode()方法的。
在重写 hashCode 方法时也需要遵守以下几点原则:
- 如果两个对象的 hashCode 相等,则这两个对象不一定是同一个对象。
- 如果两个对象的 hashCode 不相等,则这两个对象一定不是同一个对象。
- 如果两个对象是同个对象,hashCode 一定相等。
- 如果两个对象不是同个对象,hashCode 也可能相等。
其实这四个原则挺好理解的,拿 JDK 中的 String 为例子,String 也是重写了 hashCode 方法的,由于 int 类型为4个字节32位,这里假设 int 不表示负数,其能最大精度也是 2^32 = 4294967296 ,而字符串的数量是无限的,2^32 个 int 值是没法表达出无限个字符串的,所以对于字符串中不同的对象中是有可能返回相同 hashCode 值的,也就是肯定会发生 hash 冲突的情况。例如: ABCDEa123abc 和 ABCDFB123abc 就是两个 hashcode 值相同的字符串对象,而这个时候就需要靠重写下面即将讲到的 equals() 方法来区别两个字符串是不是相等了。
equals() 方法
和 hashCod() 方法一样,是一个很重要的方法,Object 源码中 equals() 是直接使用了 == 来实现的,所以对于基本类型,比较的是值是否相等,对于应用型类型对比的是对象的内存地址,只要不是同个对象,返回的就是 false 。在子类重写完 hashCode() 后,往往 equals() 方法是要一起重写的,重写后两个方法之间也有如下两个特性:
- 两个对象通过 equals() 判断为 true,则这两个对象的 hashCode() 返回值也必须相同,此时这两个对象可能是同个对象,也可能是两个不同对象(但是对象所包含的内容是相同的)。
- 两个对象的 hashCode() 返回值相同,equals() 比较不一定是 true,有可能是不同对象且两个对象说包含的内容是不相同的(就比如:ABCDEa123abc 和 ABCDFB123abc 两个字符串对象)
为什么需要同时重写 hashCode() 和 equals() 方法呢?
这是因为在编码的过程中,往往我们都需要将一些对象放进一个容器中存储起来,到需要用时再从容器中取出来, Java SE 标准库中,就提供了各种集合类来让我们使用,其中 Map作为一个 key-value 型的容器,是我们经常会用到了一种保证 key 唯一的容器,作为 key-value 型的存储容器,通过 key 值去拿到相应的 value 是这类容器必须提供的方法(Map 里的 get 方法),而对于容器而言,假如是通过遍历所有的元素去查找相应的 key 再取 value 值,效率是十分低下的。而 HashTable、HashMap 的作为 Map 容器的两种实现,里面就使用到了 hashCode() 和 equals() 方法,通过散列表的方式而得以让我们能够以更高的效率去获取相应的 value,且保证了 key 的唯一性。
HashMap、HashTable 里的哈希表实质上是一个数组 + 链表组成的数据结构,这个数组的长度是 2 的幂,默认情况下是16,翻源码可知,这个数组是一个 Node 类型的,这个 Node 就是节点类,其内部还有一个 next 指针,来指向下一个元素,也就是实现了链表。假如以 String 类(String 已经重写了 hashCode() 和 equals() 方法)作为 HashMap 的 key ,当一个字符串对象需要 put 进 Map 中时,Map 就会拿着这个字符串对象的 hash 值,通过对哈希表长度做求余运算,得出一个小于等于哈希表长度的整数,结果就会发生一下两种情况:
- 哈希表中相应位置为空,还未有元素占领这个位置。
此时就会直接将元素放在这个数组中,因为通过 hash 值就可知,这个对象说想表达的内容在 Map 中并未出现,可以安心的将此元素放入其中。
- 哈希表中相应位置为空,还未有元素占领这个位置。
- 哈希表中相应位置已经有元素占领的这个位置。
这种情况就是发生了哈希冲突,就像连续在 HashMap 中 put 两个 key 为 ABCDEa123abc 和 ABCDFB123abc 的元素(value值随意),其 key 的 hash 值是相同的,但是字符串所表达的内容是不同的意思。这时就要通过 equals() 来判断这个对象所想表达的内容在 Map 中是不是出现过,假如 equals 为 true,表明出现过了,则重新覆盖这个 key 对应 value 的值,并且返回之前存储的 value 值。equals 为 false,则会在相应数组位置所对应的链表后追加一个新元素。
- 哈希表中相应位置已经有元素占领的这个位置。
所以重写 hashCode() 是为了更快的获取到所存元素在哈希表数组的位置,从而能快速能快速获取到相应元素,而重写 equals 则是为了保证 key 值内容的唯一性,解决了 hash 冲突的问题。两者环环相扣,巧妙至极,打类型的敬佩这些计算机科学家们了,想出了那么巧妙的设计。
注:当存储的元素数量过于庞大时,hash 值重复的是必然的,假如哈希表数组长度只有默认的16,发生了 hash 冲突后,相应的元素其实就会存储于链表中,在数量大到一定程度时想要获取到相应 key 值所对应的 value,就必须进行遍历链表操作,这样效率又会变得和链表没什么区别,所以翻看源码后可知 HashMap 中哈希表的数组长度是会扩容的,具体什么时候扩容取决于扩容因子(默认0.75),超过这个扩容因子时,哈希表就会进行扩容。
clone() 方法
Java 中原型模式的一种实现,想要调用 clone() 方法,既定类就必须要实现 Clonealbe 接口,否则会报 CloneNotSupportedException ,这里有关浅复制和深复制的内容,不作展开,及时Google 即可。
toString()方法
默认实现:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
因为没有什么大的用处往往都是会去重写这个方法,在System.out.println()打印某个对象时,默认也是打印这个对象的 toString() 方法返回的字符串。
网友评论