论坛首页 Java企业应用论坛

JDK源代码学习系列一---java.util(1)

浏览 21266 次
精华帖 (0) :: 良好帖 (2) :: 新手帖 (3) :: 隐藏帖 (9)
作者 正文
   发表时间:2011-02-15   最后修改:2011-02-15
改hashCode以后,在map中就找不到这个对象了。具体可参见源码,刚才理解错了,,源码中有个hash == key.hash这种情况的判断,旧的hash是会被存储的,改了hashCode新hash和旧hash应该不会一致吧。。。

0 请登录后投票
   发表时间:2011-02-15  
哎,JDK编码格式可读性还真差
0 请登录后投票
   发表时间:2011-02-15  
很好,我也要一起过一遍
0 请登录后投票
   发表时间:2011-02-15   最后修改:2011-02-15
是个好问题,不过前面分析的都不太正确,我来试着说明一下。

先看代码:
//section 0 Entry的定义
        final K key;
        V value;
        Entry<K,V> next;
        final int hash; <--Entry的hash永不会变
//section 1
        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))) { <--用entry的hash在比较
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i); <--在hash位置的链表中找不到entry时,添加
//section 2
    void addEntry(int hash, K key, V value, int bucketIndex) {
	Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e); <-- 传入链表head
        if (size++ >= threshold)
            resize(2 * table.length);
    }
//section 3
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n; <-- 把当前entry设为head,把原head设为head->next
            key = k;
            hash = h;
        }

问题原因:
之前当key分别为1和2时,
假设hashcode在优化后得到的table数组下标(这个下标是根据数据power of 2的length取低位得到的)不同,则分布在table的两个不同bucket上,当2变成1以后,Person2的对象hash值发生了变化,但是entry的hash值不会发生变化,数组位置也不会变,因为不存在listener。当进行get操作时,首先数组下标根据hash值会定位在1上,而2则无法被找到,只能通过数组遍历取得,存在潜在的内存泄露风险。
假设hash算法不好,两个对象落在同一个bucket上,则根据指针变化可知p2在p1之前被定位到,此时将一直返回p2,而p1则是潜在的内存泄露
0 请登录后投票
   发表时间:2011-02-16   最后修改:2011-02-16
楼上(yangyi)说的有点问题。最开始我和你的分析一样,而且连容积率、概率等都算进去了,但是后来发现了一个细节彻底推翻了我的推论。
我们先不讨论2个对象的情况,单独对该对象在hashMap中改变hashCode是否可以被查找进行分析:

如你所说
引用
final int hash; <--Entry的hash永不会变 


而在get方法内有这样一句:
int hash = hash(key.hashCode());
//此处省略若干
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;


即便
引用
假设hash算法不好,两个对象落在同一个bucket上

如果hash改变过,e.hash == hash永远不成立(认为改变hashcode方法以后hash一定不一样)
故,无论如何,只要改过hashCode,肯定查找不到。

借用你结论用一下,我做一点修改就应该是比较正确的答案了:

第一部分没变:
引用
之前当key分别为1和2时,
假设hashcode在优化后得到的table数组下标(这个下标是根据数据power of 2的length取低位得到的[我插一句,这个地方说复杂了,CAPACITY(散列表长度)一定是2的若干次方,那么这个地方是用hashCode对散列表长度取余以确定落在哪个bucket上])不同,则分布在table的两个不同bucket上,当2变成1以后,Person2的对象hash值发生了变化,但是entry的hash值不会发生变化,数组位置也不会变,因为不存在listener。当进行get操作时,首先数组下标根据hash值会定位在1上,而2则无法被找到,只能通过数组遍历取得,存在潜在的内存泄露风险。

第二部分我修改一下:
假设hash算法不好,两个对象落在同一个bucket上,由于hashCode改变了,源代码写死了不允许hashCode不同的对象被查找,故同样查找不到,存在潜在的内存泄露风险。

结论:如果一个对象的hashcode改变了,则一定查找不到了。


下面附上测试代码:
import java.util.HashMap;
import java.util.Map;


public class TestMap {
	public static void main(String[] args) {
		TestBean bean1 = new TestBean();
		bean1.hashCode = 1;
		
		Map m = new HashMap();
		m.put(bean1, bean1);
		int j =0,total = 20000;//CAPACITY初始为16,20000个key肯定有落在同一个bucket上的。
		for(int i=0;i<total;i++) {
			bean1.hashCode = i;
			if(m.get(bean1)==bean1) {
				j++;
			}
		}
		System.out.println(j);
		System.out.println((float)j/total);
	}
}

class TestBean {
	static int count = 0;
	public int hashCode = 0;
	
	public int hashCode() {
		return hashCode;
	}
	public String toString() {
		return "TestBean"+count+"\r\n";
	}
}

PS:我把 equals 去掉了,结果是一样的
0 请登录后投票
   发表时间:2011-02-16  
skzr.org 写道
emsn 写道
youjianbo_han_87 写道
1. 先鄙视下论坛规则,我好久没上了,竟然要回答什么尿尿问题。
2. 贴出 JDK1.5_update_22中 HashMap的 get方法的源码:
/**
     * Returns the value to which the specified key is mapped in this identity
     * hash map, or <tt>null</tt> if the map contains no mapping for this key.
     * A return value of <tt>null</tt> does not <i>necessarily</i> indicate
     * that the map contains no mapping for the key; it is also possible that
     * the map explicitly maps the key to <tt>null</tt>. The
     * <tt>containsKey</tt> method may be used to distinguish these two cases.
     *
     * @param   key the key whose associated value is to be returned.
     * @return  the value to which this map maps the specified key, or
     *          <tt>null</tt> if the map contains no mapping for this key.
     * @see #put(Object, Object)
     */
    public V get(Object key) {
if (key == null)
    return getForNullKey();
        int hash = hash(key.hashCode());
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;//--关键在这里。
        }
        return null;
    }

通过代码可以得知,如果一个Key对应2个Value,看到注释的部分吗? 他按顺序找到后,直接就 Return a.value了。而不会循环Person2.

源代码一贴,神秘感就没了,哈哈



源码就是硬道理,不过[youjianbo_han_87]可能理解错了楼主的意图了,楼主是在put后,修改了key的关键域字段,人为改变了对象的hashCode和equals的行为(相对put时的状态),给我们设计提了个醒哦:key尽量使用状态不可变的类(偶一般都是Long、Integer、String做key)

 


这是个坏的例子:既然person使用id做为关键域,逻辑上关键域就不应当再修改了,否则程序或代码行为不可预知。


呵呵,以前项目中用map做缓存,都是用的String做key,就是看中了String不可变,建议缓存key对象中这样的关键域做成了final,呵呵调用的想是坏都不行(也遇到过和楼主一样的情形,调用者重用了返回的key,搞三搞四,调试了蛮久才发现,后来基本上返回的尽量new或者clone一个对象)

map只认put时的key状态,put后对key修改了关键域->改变了hashCode和equals行为,当然再次get时会按照新的hashCode和equals来定位Entry了

 

 

 

很有道理,从设计角度讲这有不合理的地方,此处仅为讨论问题而故而为之,不推荐像我这么做

0 请登录后投票
   发表时间:2011-02-16   最后修改:2011-02-16
To superobin:
当equals成立时,hashcode必须相等,don't break the contract
再看看
0 请登录后投票
   发表时间:2011-02-16   最后修改:2011-02-16
To yangyi:
我要表述的和equals貌似没有关系,代码里我不重载equals结果也是一样的。我要表述的意思是

如果一个放入HashMap的对象的hashCode改变了,则一定查找不到了(在判断的前一半就是false了,根本不会用equals比较了吧)。不管你一共放进map多少个对象、什么顺序、其他对象的key是否与改变后的key一样。

故在这个基础上你写的导致内存泄露的原因是有问题的,虽然结论貌似是一致的。

另:“当equals成立时,hashcode必须相等”这个是固定的约定吗?我怎么记得是“hashcode不同,equals一定返回false”这俩意思好像不太一样,是我记错了?
0 请登录后投票
   发表时间:2011-02-16  
superobin 写道
To yangyi:
我要表述的和equals貌似没有关系,代码里我不重载equals结果也是一样的。我要表述的意思是

如果一个放入HashMap的对象的hashCode改变了,则一定查找不到了(在判断的前一半就是false了,根本不会用equals比较了吧)。不管你一共放进map多少个对象、什么顺序、其他对象的key是否与改变后的key一样。

故在这个基础上你写的导致内存泄露的原因是有问题的,虽然结论貌似是一致的。

另:“当equals成立时,hashcode必须相等”这个是固定的约定吗?我怎么记得是“hashcode不同,equals一定返回false”这俩意思好像不太一样,是我记错了?

脱离了general contract再谈就没有意义了,或者我们讨论的不是同一个问题了
0 请登录后投票
   发表时间:2011-02-16  
好文章 持续跟进楼主 期待。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics