zoukankan      html  css  js  c++  java
  • 彻底明白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),这样显然减少了比较次数,提高了效率

    那Object的hashCode方法的默认实现是怎样的呢?

    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是地址一定是不相同的,那么为什么后一个张三还是把前一个张三覆盖了呢?

    是因为String重写了hashCode方法和equals方法

    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;
    }
    

    那么当我们使用自己的对象作为键时

    @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呢?

    我们去看看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
  • 相关阅读:
    Wine 的安装与解决微信文字不可见,无法聚焦问题。#Wine教程
    Utuntu / CentOS设置环境变量 #java #maven
    真机安装Linux系统 (CentOS与Ubuntu)
    scrollUtils.js #触底触发上拉加载更多 #越线 #上下滚动事件
    微信小程序 bus.js
    svchost.exe占网速的解决办法
    Linux常用的解压缩命令
    本博客标题指示灯代码
    Jenkins持续集成
    jenkins.plugins.publish_over.BapPublisherException: Failed to connect and initialize SSH connection. Message: [Failed to connect session for config [master_server]. Message [Auth fail]]
  • 原文地址:https://www.cnblogs.com/moyuduo/p/12581879.html
Copyright © 2011-2022 走看看