您的位置:首页 > 其它

HashMap解析

2016-01-16 22:49 295 查看
HashMap是基于哈希表的Map接口的实现,允许key和value为null,并且不保证其内部元素的顺序,随着时间推移,同一元素的位置也可能改变(resize)。

HashMap有两个因素影响其性能:初始容量(initial capacity)和负载因子(load factor)。初始容量即哈希表创建时桶(bucket)的数量,负载因子是用来衡量在哈希表容量自动扩展之前允许的多满,过高的负载因子可以降低存储空间但会增加查找(put/get)的时间,默认为0.75。当哈希表中的条目(entry)数量超过了负载因子和当前容量的乘积,哈希表将会再哈希(rehashed)。

如果有很多映射被存储在HashMap实例中,设置一个合适的初始化容量比在运行时按需扩展哈希表更有效率。当哈希表中存在很多有相同hashCode时会降低性能,此时如果key是Comparable可以使用比较顺序来改善。

因为HsahMap是非线程安全的,可在创建时通过Collections.synchronizedMap方法来包装以实现同步。HashMap“集合视图方法”返回的迭代器都是快速失败(fail-fast):如果在迭代器创建后,有结构上的修改(增删entry,只改变entry的value的值不是),除了自身迭代器的remove方法外,就会抛出ConcurrentModificationException(但是假如已经从结构上进行了更改,再调用set方法,将会抛出IllegalArgumentException异常)。

使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择。

示例:

首先,定义一个Person类:

public class Person {
String name;

public Person(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

Person person = (Person) o;

return !(name != null ? !name.equals(person.name) : person.name != null);

}

@Override
public int hashCode() {
if (name.length()%2 == 0)
return 31;
else
return 95;
}

}


测试类,将Person作为键:

public class Test {

public static void main(String[] args) {
Person li = new Person("li");
Person su = new Person("su");
Person mao = new Person("mao");
Person han = new Person("han");

HashMap<Person, String> map = new HashMap<Person, String>();
map.put(li, "beijing");
map.put(mao, "shanghai");
map.put(su, "hangzhou");
map.put(han, "shenzhen");

Iterator<Person> itr = map.keySet().iterator();
while(itr.hasNext()) {
Person p = itr.next();
String city = map.get(p);
System.out.println(p.getName() + "----" + city);
}
}
}


在Test类中的第15行设置断点:



由上图可以看到,put到HashMap的key-value对是以封装在HashMap的内部类Node的形式存储的,Node的源码如下:

static class Node<K,V> implements Map.Entry<K,V> {
  final int hash;
  final K key;
  V value;
  Node<K,V> next;

  //...
}


每当往HashMap里存放key-value对时,都会为它们实例化一个Node对象,这个Node对象存储在名为table的数组中。其根据key的hashCode方法计算出来的hash值来决定存放在数组的哪个索引上。如果两个元素有相同的hashcode,它们会被放在table数组同一个索引上,逻辑上以链表(LinkedList)的形式来进行存储的。

查看HashMap的put方法,源码如下:

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


为了防止调用者所编写key的hashCode方法不够好而引起哈希值分布不均匀,JDK中将key包装在hash方法中,在将其作为参数和key以及value一同传给putVal方法中,putVal方法的源码如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
  Node<K,V>[] tab; Node<K,V> p; int n, i;
  if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
  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))))
  e = p;
else if (p instanceof TreeNode)
  e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
  for (int binCount = 0; ; ++binCount) {
    if ((e = p.next) == null) {
      p.next = newNode(hash, key, value, null);
  if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    treeifyBin(tab, hash);
  break;
  }
  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;
}


4000

如果HashMap中的table数组为空或者长度为0,则需要进行resize调整HashMap大小。

如果查找的数组索引指向为空,则通过newNode新建一个Node,并放入数组指定的索引中。如果不为空,且数组索引指向的Node对象的hash值与当前插入key的hash值相等,并且两者通过equals方法比较相等,则将Node对象赋给e;否则通过Node对象的next所指向的下一个Node对象迭代链表进行比较,如果找到hash值相等并且equals方法也相等,则将找到的Node对象赋给e。最后,如果e不为空,用参数中的value替换其之前的value值,并返回旧的value值。

再来看看HashMap的get方法,源码如下:

public V get(Object key) {
Node<K,V> e;
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;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
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;
}




Note:
HashMap有一个叫做Node的内部类,它用来存储key-value对,并存储在一个叫做table的Node数组中。
table的索引在逻辑上叫做“桶”(bucket),它存储了链表的第一个元素。
key的hashcode方法用来找到Node对象所在的bucket。
如果两个key有相同的hash值,他们会被放在table数组的同一个bucket。
key的equals方法用来确保key的唯一性。
value对象的equals和hashcode方法其实一点用也没有。

在哈希表容量为length的情况下,为了使每个key都能在冲突最小的情况下映射到0到length内,一般有两种方法:
让length为素数,然后用hashCode(key) mod length的方法得到索引(Hashtable)
让length为2的指数倍,然后用hashCode(key) & (length-1)的方法得到索引(HashMap)

在stackoverflow.com上有几个问题很好:
为什么String, Interger这样的wrapper类适合作为键?因为它们都是不可变的,也是final的,而且已经重写了equals和hashCode方法了,不可变性是必要的,因为要计算hashCode,就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode,就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。
我们可以使用自定义的对象作为键吗? 这是前一个问题的延伸。当然你可使用任何对象作为键,只要它遵守了equals和hashCode方法的定义规则,并且当对象插入到Map中之后将不会再改变了。如果这个自定义对象时不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了。
我们可以使用CocurrentHashMap来代替Hashtable吗?我们知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它根据同步级别对map的一部分进行上锁。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: