Java集合体系结构分析与比较(五)

2014-11-24 10:29:11 · 作者: · 浏览: 7
常犯一个错误。
在HashMap中通过label查找value时,实际上是计算label对象地址的散列码来确定value的。一般情况下,我们是使用基类Object的方法hashCode()来生成散列码,它默认是使用对象的地址来计算的,因此由第一个对象new Apple(5)和第二个对象new Apple(5)生成的散列码是不同的,不能完成正确的查找。通常,我们可以编写自己的hashCode()方法来覆盖基类的原始方法,但与此同时,我们必须同时实现equals()方法来判断当前的label是否与表中存在的label相同。
正确的equals()方法满足五个条件:
(1) 自反性。对于任意的x,x.equals(x)一定返回true。
(2) 对称性。对于任意的x和y,如果y.equals(x)返回true,则x.equals(y)也返回true。
(3) 传递性。对于任意的x、y、z,如果有x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)一定返回true。
(4) 一致性。对于任意的x和y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回的结果应该保持一致,要么一直是true,要么一直是false。
(5) 对任何不是null的x,x.equals(null)一定返回false。
Equals()比较的是对象的地址,如果要使用自己的类作为HashMap的label,必须同时重载hashCode()和equals()方法。
5.4.2 HashMap的性能因子
容量(capacity): 散列表中bucket的数量。
初始化容量(initial capacity): 创建散列表时bucket的数量。可以在构造方法中指定HashMap和HashSet的初始化容量。
尺寸(size): 散列表中记录的数量。(数组的元素个数,非list中元素总和)
负载因子(load factor): 尺寸/容量。负载因子为0,表示空的散列表,0.5表示半满的散列表。轻负载的散列表具有冲突少,适宜插入与查询的特点,但是使用迭代器遍历会比较慢。较高的负载会减少所需空间大小。当负载达到指定值时,容器会自动成倍地增加容量,并将原有的对象重新分配,存入新的bucket中,这个过程称为“重散列”。
5.4.3 重写hashCode()的关键
(1) 对同一个对象调用hashCode()都应该生成同样的值。
(2) hashCode()方法不要依赖于对象中易变的数据,当数据发生变化时,hashCode()就会生成一个不同的散列码,即产生了一个不同的label。
(3) hashCode()不应依赖于具有唯一性的对象信息,例如对象地址。
(4) 散列码应该更关心速度,而不是唯一性,因为散列码不必是唯一的。
(5) 好的hashCode()应该产生分步均匀的散列码。
5.4.4 HashMap的深度分析
HashMap可谓JDK的一大实用工具,把各个Object映射起来,实现了“键--值”对应的快速存取。但实际里面做了些什么呢?
在这之前,先介绍一下负载因子和容量的属性。大家都知道其实一个 HashMap 的实际容量等于因子*容量,其默认值是 16×0.75=12; 这个很重要,对效率很一定影响!当存入HashMap的对象超过这个容量时,HashMap 就会重新构造存取表。这就是一个大问题,我后面慢慢介绍,反正,如果你已经知道你大概要存放多少个对象,最好设为该实际容量的能接受的数字。
两个关键的方法,put和get:
先有这样一个概念,HashMap是声明了 Map,Cloneable, Serializable 接口,和继承了 AbstractMap 类,里面的 Iterator 其实主要都是其内部类HashIterator 和其他几个 iterator 类实现,当然还有一个很重要的继承了Map.Entry 的 Entry 内部类,由于大家都有源代码,大家有兴趣可以看看这部分,我主要想说明的是 Entry 内部类。它包含了hash,value,key 和next 这四个属性,很重要。put的源码如下
public Object put(Object key, Object value) {
//这个就是判断键值是否为空,如果为空,它会返回一个static Object 作为键值
//这就是为什么HashMap允许空键值的原因
Object k = maskNull(key);
/*
hash 通过 key 这个Object的 hashcode 进行 hash,然后通过 indexFor 获得在Object table的索引值。
table?不要惊讶,其实HashMap也神不到哪里去,它就是用 table 来放的。最牛的就是用 hash 能正确的返回索引。
*/
int hash = hash(k);
int i = indexFor(hash, table.length);
/*
不知道大家有没有留意 put 其实是一个有返回的方法,它会把相同键值的 put 覆盖掉并返回旧的值!如下方法彻底说明了 HashMap 的结构,其实就是一个表加上在相应位置的Entry的链表:
for (Entry e = table[i]; e != null; e = e.next) {
  if (e.hash == hash && eq(k, e.key)) {
Object oldvalue = e.value;
e.value = value; //把新的值赋予给对应键值。
e.recordAccess(this); //空方法,留待实现
return oldvalue; //返回相同键值的对应的旧的值。
  }
}

modCount++; //结构性更改的次数
addEntry(hash, k, value, i); //添加新元素,关键所在!
return null; //没有相同的键值返回
}
我们把关键的方法拿出来分析:
void addEntry(int hash, Object key, Object value, int bucketIndex) {
/*
因为 hash 的算法有可能令不同的键值有相同的hash码并有相同的table索引,如:key=“33”和key=Object g的hash都是-8901334,那它经过indexfor之后的索引一定都为i,这样在new的时候这个Entry的next就会指向这个原本的table[i],再有下一个也如此,形成一个链表,和put的循环对定e.next获得旧的值。到这里,HashMap的结构,大家也十分明白了吧?
*/
table[bucketIndex] = new Entry(hash, key, value, table[bucketIndex]);

if (size++ >= threshold) //这个threshold就是能实际容纳的量
resize(2 * table.length); //超出这个容量就会将Object table重构
}
所谓的重构也不神,就是建一个两倍大的table(我在别的 论坛上看到有人说是两倍加1,把我骗了),然后再一个个indexfor进去!注意!!这就是效率!!如果你能让你的HashMap不需要重构那么多次,效率会大大提高!
说到这里也差不多了,get比put简单得多,大家,了解p