背景:面試時,有些問題經常被問。現在來一探究竟。。。。
一、== 和 equals
默認情況,對於基本數據類型,==比較的是兩個變量的值。對於引用對象,==比較的是兩個對象的地址
Object 類
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
String 類 重寫equals,hashcode
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
//value -拆字符串,累加 hash
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
HashMap
// key-value 取hash, Objects.hashCode(key)->object.hashCode();
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
// 重寫hashcode方法爲了將數據存入HashSet/HashMap/Hashtable 類時進行比較
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
分析可發現:
1、object 中,對於基本數據類型,==比較的是兩個變量的值。對於引用對象,==比較的是兩個對象的地址 ,equals比較的是對象地址
2、string重寫後,string類型equals 比較的是值
二、一旦重寫了equals方法,就一定要重寫hashCode方法。爲什麼?
首先認識下啥是hashcode,
hashcode就是通過hash函數得來的,通俗的說,就是通過某一種算法得到的,hashcode就是在hash表中有對應的位置
對象的內部地址(也就是物理地址)轉換成一個整數,然後該整數通過hash函數的算法就得到了hashcode
HashCode的存在主要是爲了查找的快捷性、hashcode代表對象就是在hash表中的位置
JDK API中關於Object類的equals和hashCode方法中扯了這麼多,總結起來就是兩句話:equals相等的兩個對象的hashCode也一定相等,但hashCode相等的兩個對象不一定equals相等。爲啥要重寫,原因一、官方建議
接着我們通過代碼證明,爲啥要重寫
public class Person {
private String name;
private int age;
private String sex;
Person(String name,int age,String sex){
this.name = name;
this.age = age;
this.sex = sex;
}
@Override
public boolean equals(Object obj)
{
if(obj instanceof Person){
Person person = (Person)obj;
return name.equals(person.name);
}
return super.equals(obj);
}
@Override
public int hashCode()
{
return name.hashCode();
}
public static void test(){
HashMap<Person, Integer> map = new HashMap<Person, Integer>();
Person p = new Person("jack",22,"男");
Person p1 = new Person("jack",22,"男");
System.out.println("p的hashCode:"+p.hashCode());
System.out.println("p1的hashCode:"+p1.hashCode());
System.out.println(p.equals(p1));
System.out.println(p == p1);
map.put(p,888);
map.put(p1,888);
map.forEach((key,val)->{
System.out.println(key);
System.out.println(val);
});
}
public static void main(String[] args) {
test();
}
}
// 不重寫hashcode運行結果:
p的hashCode:1205044462
p1的hashCode:761960786
true
false
com.gz.springboot_simple.Person@2d6a9952
888
com.gz.springboot_simple.Person@47d384ee
888
// 重寫hashcode運行結果:
p的hashCode:3254239
p1的hashCode:3254239
true
false
com.gz.springboot_simple.Person@31a7df
888
分析發現:
1、不重寫hashcode,map存2個對象、重寫了存1個對象
2、我們重寫equal 就是爲了滿足name相同,兩個對象就是相同。很顯然不重寫hashcode出現了錯誤的預期
3、當我們用於存放在Hash相關的集合類中時,在重寫equals時,需要重寫hashCode,不然會出現與預期不符的結果
4、不重寫取的都是object的hashcode,存在數組不同位置–會出現2個對象
爲啥hash集合類就的重寫hashcode,這得益於存儲的數據結構—hash表
一個最簡單的hash結構-
從上圖我們可以發現哈希表是由數組+鏈表組成的,一個長度爲16的數組中,每個元素存儲的是一個鏈表的頭結點。那麼這些元素是按照什麼樣的規則存儲到數組中呢。一般情況是通過hash(key)%len獲得,也就是元素的key的哈希值對數組長度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存儲在數組下標爲12的位置。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; // 當數組table爲null時, 調用resize生成數組table, 並令tab指向數組table
if ((p = tab[i = (n - 1) & hash]) == null) // 如果新存放的hash值沒有衝突
tab[i] = newNode(hash, key, value, null); // 則只需要生成新的Node節點並存放到table數組中即可
else { // 否則就是產生了hash衝突
Node<K, V> e;
K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 如果hash值相等且key值相等, 則令e指向衝突的頭節點
else if (p instanceof TreeNode) // 如果頭節點的key值與新插入的key值不等, 並且頭結點是TreeNode類型,說明該hash值衝突是採用紅黑樹進行處理.
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value); // 向紅黑樹中插入新的Node節點
else { // 否則就是採用鏈表處理hash值衝突
for (int binCount = 0;; ++binCount) { // 遍歷衝突鏈表, binCount記錄hash值衝突鏈表中節點個數
if ((e = p.next) == null) { // 當遍歷到衝突鏈表的尾部時
p.next = newNode(hash, key, value, null); // 生成新節點添加到鏈表末尾
if (binCount >= TREEIFY_THRESHOLD - 1) // 如果binCount即衝突節點的個數大於等於 (TREEIFY_THRESHOLD(=8) - 1),便將衝突鏈表改爲紅黑樹結構, 對衝突進行管理,
// 否則不需要改爲紅黑樹結構
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 如果在衝突鏈表中找到相同key值的節點, 則直接用新的value覆蓋原來的value值即可
break;
p = e;
}
}
if (e != null) { // 說明原來已經存在相同key的鍵值對
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) // onlyIfAbsent爲true表示僅當<key,value>不存在時進行插入, 爲false表示強制覆蓋;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; // 修改次數自增
if (++size > threshold) // 當鍵值對數量size達到臨界值threhold後, 需要進行擴容操作.
resize();
afterNodeInsertion(evict);
return null;
}
分析可得:
1、hash表其實是一個數組,transient Node<K,V>[] table;
2、數據結構中有數組和鏈表來實現對數據的存儲,但數組存儲區間是連續的,佔用內存嚴重,故空間複雜的很大。但數組的二分查找時間複雜度小,爲O(1);數組的特點是:尋址容易,插入和刪除困難。
鏈表存儲區間離散,佔用內存比較寬鬆,故空間複雜度很小,但時間複雜度很大,達O(N)。鏈表的特點是:尋址困難,插入和刪除容易。
3、哈希–綜合兩者的特性。如上圖所示:
4、i = (n - 1) & hash 發現 hash & 位運算後,i 確定了數組下標
哈希表有多種不同的實現方法,最常用的一種方法—— 拉鍊法,我們可以理解爲“鏈表的數組”
三、