• Hash算法原理以及HashCode深入理解


    Java中的Collection有两类,一类是List,一类是Set。List内的元素是有序的,元素可以重复。Set元素无序,但元素不可重复。要想保证元素不重复,两个元素是否重复应该依据什么来判断呢?用Object.equals方法。但若每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。也就是说若集合中已有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。于是Java采用了哈希表的原理。
    当Set接收一个元素时根据该对象的内存地址算出hashCode,看它属于哪一个区间,再这个区间里调用equeals方法。【特别注意】这里需要注意的是:当俩个对象的hashCode值相同的时候,Hashset会将对象保存在同一个位置,但是他们equals返回false,所以实际上这个位置采用链式结构来保存多个对象。
          
    上面方法确实提高了效率。但一个面临问题:若两个对象equals相等,但不在一个区间,因为hashCode的值在重写之前是对内存地址计算得出,所以根本没有机会进行比较,会被认为是不同的对象。所以Java对于eqauls方法和hashCode方法是这样规定的:
    1 如果两个对象相同,那么它们的hashCode值一定要相同。也告诉我们重写equals方法,一定要重写hashCode方法,也就是说hashCode值要和类中的成员变量挂上钩,对象相同–>成员变量相同—->hashCode值一定相同。
    2 如果两个对象的hashCode相同,它们并不一定相同,这里的对象相同指的是用eqauls方法比较。

    接下来内容就是转载自:http://blog.csdn.net/jiangwei0910410003/article/details/22739953博客*********************************************************************

    下面来看一下一个具体的例子: RectObject对象:

    package com.weijia.demo;

    public class RectObject {
    public int x;
    public int y;
    public RectObject(int x,int y){
    this.x = x;
    this.y = y;
    }
    @Override
    public int hashCode(){
    final int prime = 31;
    int result = 1;
    result = prime * result + x;
    result = prime * result + y;
    return result;
    }
    @Override
    public boolean equals(Object obj){
    if(this == obj)
    return true;
    if(obj == null)
    return false;
    if(getClass() != obj.getClass())
    return false;
    final RectObject other = (RectObject)obj;
    if(x != other.x){
    return false;
    }
    if(y != other.y){
    return false;
    }
    return true;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    我们重写了父类Object中的hashCode和equals方法,看到hashCode和equals方法中,如果两个RectObject对象的x,y值相等的话他们的hashCode值是相等的,同时equals返回的是true;
    下面是测试代码:
    package com.weijia.demo;
    import java.util.HashSet;
    public class Demo {
    public static void main(String[] args){
    HashSet<RectObject> set = new HashSet<RectObject>();
    RectObject r1 = new RectObject(3,3);
    RectObject r2 = new RectObject(5,5);
    RectObject r3 = new RectObject(3,3);
    set.add(r1);
    set.add(r2);
    set.add(r3);
    set.add(r1);
    System.out.println("size:"+set.size());
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    我们向HashSet中存入到了四个对象,打印set集合的大小,结果是多少呢? 运行结果:size:2
    为什么会是2呢?这个很简单了吧,因为我们重写了RectObject类的hashCode方法,只要RectObject对象的x,y属性值相等那么他的hashCode值也是相等的,所以先比较hashCode的值,r1和r2对象的x,y属性值不等,所以他们的hashCode不相同的,所以r2对象可以放进去,但是r3对象的x,y属性值和r1对象的属性值相同的,所以hashCode是相等的,这时候在比较r1和r3的equals方法,因为他么两的x,y值是相等的,所以r1,r3对象是相等的,所以r3不能放进去了,同样最后再添加一个r1也是没有没有添加进去的,所以set集合中只有一个r1和r2这两个对象

    下面我们把RectObject对象中的hashCode方法注释,即不重写Object对象中的hashCode方法,在运行一下代码:
    运行结果:size:3
    这个结果也是很简单的,首先判断r1对象和r2对象的hashCode,因为Object中的hashCode方法返回的是对象本地内存地址的换算结果,不同的实例对象的hashCode是不相同的,同样因为r3和r1的hashCode也是不相等的,但是r1==r1的,所以最后set集合中只有r1,r2,r3这三个对象,所以大小是3

    下面我们把RectObject对象中的equals方法中的内容注释,直接返回false,不注释hashCode方法,运行一下代码:
    运行结果:size:3 这个结果就有点意外了,我们来分析一下:
    首先r1和r2的对象比较hashCode,不相等,所以r2放进set中,再来看一下r3,比较r1和r3的hashCode方法,是相等的,然后比较他们两的equals方法,因为equals方法始终返回false,所以r1和r3也是不相等的,r3和r2就不用说了,他们两的hashCode是不相等的,所以r3放进set中,再看r4,比较r1和r4发现hashCode是相等的,在比较equals方法,因为equals返回false,所以r1和r4不相等,同一r2和r4也是不相等的,r3和r4也是不相等的,所以r4可以放到set集合中,那么结果应该是size:4,那为什么会是3呢?
    这时候我们就需要查看HashSet的源码了,下面是HashSet中的add方法的源码:

    /**
    * Adds the specified element to this set if it is not already present.
    * More formally, adds the specified element <tt>e</tt> to this set if
    * this set contains no element <tt>e2</tt> such that
    * <tt>(e==null ? e2==null : e.equals(e2))</tt>.
    * If this set already contains the element, the call leaves the set
    * unchanged and returns <tt>false</tt>.
    *
    * @param e element to be added to this set
    * @return <tt>true</tt> if this set did not already contain the specified
    * element
    */
    public boolean add(E e) {
    return map.put(e, PRESENT)==null;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    这里我们可以看到其实HashSet是基于HashMap实现的,我们在点击HashMap的put方法,源码如下:
    /**
    * Associates the specified value with the specified key in this map.
    * If the map previously contained a mapping for the key, the old
    * value is replaced.
    *
    * @param key key with which the specified value is to be associated
    * @param value value to be associated with the specified key
    * @return the previous value associated with <tt>key</tt>, or
    * <tt>null</tt> if there was no mapping for <tt>key</tt>.
    * (A <tt>null</tt> return can also indicate that the map
    * previously associated <tt>null</tt> with <tt>key</tt>.)
    */
    public V put(K key, V value) {
    if (key == null)
    return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    Object k;
    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    V oldValue = e.value;
    e.value = value;
    e.recordAccess(this);
    return oldValue;
    }
    }

    modCount++;
    addEntry(hash, key, value, i);
    return null;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    我们主要来看一下if的判断条件,
    首先是判断hashCode是否相等,不相等的话,直接跳过,相等的话,然后再来比较这两个对象是否相等或者这两个对象的equals方法,因为是进行的或操作,所以只要有一个成立即可,那这里我们就可以解释了,其实上面的那个集合的大小是3,因为最后的一个r1没有放进去,以为r1==r1返回true的,所以没有放进去了。所以集合的大小是3,如果我们将hashCode方法设置成始终返回false的话,这个集合就是4了。

    最后我们在来看一下hashCode造成的内存泄露的问题:看一下代码:

    package com.weijia.demo;
    import java.util.HashSet;
    public class Demo {
    public static void main(String[] args){
    HashSet<RectObject> set = new HashSet<RectObject>();
    RectObject r1 = new RectObject(3,3);
    RectObject r2 = new RectObject(5,5);
    RectObject r3 = new RectObject(3,3);
    set.add(r1);
    set.add(r2);
    set.add(r3);
    r3.y = 7;
    System.out.println("删除前的大小size:"+set.size(http://www.my516.com));
    set.remove(r3);
    System.out.println("删除后的大小size:"+set.size());
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    运行结果:
    删除前的大小size:3
    删除后的大小size:3
    擦,发现一个问题了,而且是个大问题呀,我们调用了remove删除r3对象,以为删除了r3,但事实上并没有删除,这就叫做内存泄露,就是不用的对象但是他还在内存中。所以我们多次这样操作之后,内存就爆了。看一下remove的源码:
    /**
    * Removes the specified element from this set if it is present.
    * More formally, removes an element <tt>e</tt> such that
    * <tt>(o==null ? e==null : o.equals(e))</tt>,
    * if this set contains such an element. Returns <tt>true</tt> if
    * this set contained the element (or equivalently, if this set
    * changed as a result of the call). (This set will not contain the
    * element once the call returns.)
    *
    * @param o object to be removed from this set, if present
    * @return <tt>true</tt> if the set contained the specified element
    */
    public boolean remove(Object o) {
    return map.remove(o)==PRESENT;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    然后再看一下remove方法的源码:
    /**
    * Removes the mapping for the specified key from this map if present.
    *
    * @param key key whose mapping is to be removed from the map
    * @return the previous value associated with <tt>key</tt>, or
    * <tt>null</tt> if there was no mapping for <tt>key</tt>.
    * (A <tt>null</tt> return can also indicate that the map
    * previously associated <tt>null</tt> with <tt>key</tt>.)
    */
    public V remove(Object key) {
    Entry<K,V> e = removeEntryForKey(key);
    return (e == null ? null : e.value);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    在看一下removeEntryForKey方法源码:
    /**
    * Removes and returns the entry associated with the specified key
    * in the HashMap. Returns null if the HashMap contains no mapping
    * for this key.
    */
    final Entry<K,V> removeEntryForKey(Object key) {
    int hash = (key == null) ? 0 : hash(key);
    int i = indexFor(hash, table.length);
    Entry<K,V> prev = table[i];
    Entry<K,V> e = prev;

    while (e != null) {
    Entry<K,V> next = e.next;
    Object k;
    if (e.hash == hash &&
    ((k = e.key) == key || (key != null && key.equals(k)))) {
    modCount++;
    size--;
    if (prev == e)
    table[i] = next;
    else
    prev.next = next;
    e.recordRemoval(this);
    return e;
    }
    prev = e;
    e = next;
    }

    return e;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    我们看到,在调用remove方法的时候,会先使用对象的hashCode值去找到这个对象,然后进行删除,这种问题就是因为我们在修改了r3对象的y属性的值,又因为RectObject对象的hashCode方法中有y值参与运算,所以r3对象的hashCode就发生改变了,所以remove方法中并没有找到r3了,所以删除失败。即r3的hashCode变了,但是他存储的位置没有更新,仍然在原来的位置上,所以当我们用他的新的hashCode去找肯定是找不到了。
    ---------------------

  • 相关阅读:
    nopcommerce商城系统--文档整理
    浏览器标签页显示图标
    SQL SERVER 查询语句学习:CHARINDEX
    NHibernate3.3.3 学习笔记1
    权限管理UML设计草图
    jquery UI 跟随学习笔记——拖拽(Draggable)
    Unity3d IOS中的IGUI控件
    Unity3d ngui基础教程
    unity3d 幻灯片效果实现
    unity3d 场景间数据传递
  • 原文地址:https://www.cnblogs.com/ly570/p/11331593.html
Copyright © 2020-2023  润新知