完全明白equals和hashCode

equals和hashCode方法

equals

咱們知道equals是用來比較兩個對象是否相等的,好比咱們經常使用的String.equals方法

@Test
public void test() {
  String str1=new String("abc");
  String str2=new String("abc");
  boolean equals = str1.equals(str2);
  System.out.println(equals);//true
}

hashCode方法

hashCode方法是經過必定的算法獲得一個hash值,通常配合散列集合一塊兒使用,如HashMap、HashSet都是不能夠存放重複元素的,那麼當容器中元素個數不少時,你要添加一個元素時,難道一個一個去equals比較?固然這是能夠的,可是不免效率很低,而HashMap和HashSet的底層都是使用數組+鏈表的方式實現的,這樣有什麼好處呢,當一個對象要加入集合,直接用hashCode進行一些運算獲得保存的數組下標,再去數組下標對應的鏈表中一個一個元素比較(equals),這樣顯然減小了比較次數,提升了效率java

那Object的hashCode方法的默認實現是怎樣的呢?node

public native int hashCode();

能夠看到它是一個本地方法,實際上Object的hashCode方法返回是元素的地址(不一樣的虛擬機可能不同,但Hotspot的是)程序員

class Emp{
	String idCord;
	String name;
	int age;
	public Emp(String idCord, String name, int age) {
		super();
		this.idCord = idCord;
		this.name = name;
		this.age = age;
	}
	
}

@Test
public void test2() {
  Emp e=new Emp("0101001","zhangsan",20);
  System.out.println(e.hashCode());//1717159510
  System.out.println(e);//com.moyuduo.test.Emp@6659c656
}

6659c656轉換成十進制也就是1717159510算法

哈希集合的使用

咱們不少時候這樣使用HashMap數組

@Test
public void test3() {
  HashMap<String,Emp> map=new HashMap<>();
  map.put(new String("zhangsan"), new Emp("01001","zhangsan",20));
  map.put(new String("lisi"), new Emp("01002","lisi",22));
  map.put(new String("zhangsan"), new Emp("01003","zhangsan",23));
  Emp emp = map.get("zhangsan");
  System.out.println(emp);//Emp [idCord=01003, name=zhangsan, age=23]
}

額?不對呀,編號爲01001的張三呢?並且咱們知道new出來的String的hashCode是地址必定是不相同的,那麼爲何後一個張三仍是把前一個張三覆蓋了呢?app

是由於String重寫了hashCode方法和equals方法this

public int hashCode() {
  //默認爲0
  int h = hash;
  if (h == 0 && value.length > 0) {
    char val[] = value;
	//一個一個遍歷String的char[]
    for (int i = 0; i < value.length; i++) {
      //hash值等於當前字符前字符的hash*31+當前字符的Unicode
      h = 31 * h + val[i];
    }
    hash = h;
  }
  return h;
}

public boolean equals(Object anObject) {
  //判斷兩個對象的地址是否相同
  if (this == anObject) {
    return true;
  }
  //判斷傳入的對象是否是String類型
  if (anObject instanceof String) {
    String anotherString = (String)anObject;
    int n = value.length;
    //判斷兩個String的char[]的長度是否一致
    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;
}

那麼當咱們使用本身的對象做爲鍵時code

@Test
public void test4() {
  HashMap<Emp,Integer> map=new HashMap<>();	
  map.put(new Emp("01001","zhangsan",20),6000);
  map.put(new Emp("01002","lisi",22),8000);
  Integer integer = map.get(new Emp("01001","zhangsan",20));
  System.out.println(integer);//null
}

能夠看到輸出的是null,這是爲何呢,就是由於咱們自定義的類沒有從新寫hashCode方法,get的時候新new出來的Emp對象的hashCode(也就是地址)確定和存的時候的hashCode不同,因此拿不到,因此當咱們自定義的類要使用散列集合存儲時,必定要重寫equals方法和hashCode方法對象

HashMap的底層原理

爲何當咱們要使用自定義對象做爲key存放在HashMap中時,必定要重寫equals和hashCode呢?get

咱們去看看HashMap底層是怎麼存鍵值對和獲得值的

HashMap的put方法

public V put(K key, V value) {
  return putVal(hash(key), key, value, false, true);
}

//計算鍵的hash
static final int hash(Object key) {
  int h;
  //若是鍵爲null那麼hash爲0,這也是爲何HashMap只能存放一個鍵爲null的元素,不然hash爲hashCode與上hashCode無符號右移16位
  return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
  		//若是當前的Node數組還未初始化或長度爲0
        if ((tab = table) == null || (n = tab.length) == 0)
            //進行擴容
            n = (tab = resize()).length;
  		//節點存放的下標是數組長度-1與上鍵的hash
        if ((p = tab[i = (n - 1) & hash]) == null)
          	//運算獲得的下標的位置沒有存放元素,那麼直接保存
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
              //若是下標位置元素的hash和鍵的hash相等而且下標元素的key和鍵的地址相同或equals那麼直接覆蓋
                e = p;
            else if (p instanceof TreeNode)
              //若是下標元素位置存放的元素原本就是紅黑色節點,那麼按照紅黑樹的規則插入
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else { //下標位置有元素並且還沒轉化爲紅黑樹,說明是鏈表存儲
                for (int binCount = 0; ; ++binCount) {
            		//讓e指向鏈表下一個節點
                    if ((e = p.next) == null) {//當找到最後e等於null了說明鏈表中沒有元素的key和當前插入的key相同
                      //直接把節點掛到鏈表尾
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                  	//若是找到已存入元素的key和插入key的hash相同而且兩key地址相等或equals,那麼e就是要替換的元素
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                  //替換舊值
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
          //插入後元素大小超過閾值進行擴容
            resize();
        afterNodeInsertion(evict);
        return null;
    }

HashMap的get方法

public V get(Object key) {
  Node<K,V> e;
  //若是經過key拿到的鍵值對節點爲null就返回null,不然返回節點的value
  return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  		//Node[]是否已經初始化而且長度>0而且經過hash運算獲得的下標已經有元素
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
          //判斷下標第一個位置節點的hash和查詢key的hash一致而且兩key地址同樣或equals
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
          //下標節點還有next
            if ((e = first.next) != null) {
              //節點是紅黑樹,那麼按照紅黑樹的查找規則進行
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                  //是鏈表,那麼依次遍歷
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

能夠看到存的時候是經過hashCode獲得hash再用hash獲得存放下標,而後存入鍵值對

取的時候是經過hashCode獲得hash再獲得下標元素,下標元素再根據hash&&(地址相等||equals)獲得鍵值對

Object規範

說了這些再來講說Object規範

  • 兩對象equals那麼hashCode值必定要相同
  • 兩對象hashCode值相等,對象不必定equals,這主要是由於hashCode是根據對象的特徵值生成的,hashCode的算法是程序員本身實現的,在某些狀況下可能兩對象在邏輯上也不一樣也能生成相同的hashCode

equals和hashCode聯繫

  1. 當咱們自定義類不須要充當key來在散列表中存儲對象時,equals和hashCode根本沒有關係,你也不必重寫hashCode方法
  2. 當咱們會用自定義類充當key在散列表中存對象,這時候你必定要重寫equals和hashCode
相關文章
相關標籤/搜索