大話-那些年常被問的java題

背景:面試時,有些問題經常被問。現在來一探究竟。。。。
一、== 和 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 確定了數組下標

哈希表有多種不同的實現方法,最常用的一種方法—— 拉鍊法,我們可以理解爲“鏈表的數組”
在這裏插入圖片描述
三、

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章