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类:
测试类,将Person作为键:
在Test类中的第15行设置断点:
由上图可以看到,put到HashMap的key-value对是以封装在HashMap的内部类Node的形式存储的,Node的源码如下:
每当往HashMap里存放key-value对时,都会为它们实例化一个Node对象,这个Node对象存储在名为table的数组中。其根据key的hashCode方法计算出来的hash值来决定存放在数组的哪个索引上。如果两个元素有相同的hashcode,它们会被放在table数组同一个索引上,逻辑上以链表(LinkedList)的形式来进行存储的。
查看HashMap的put方法,源码如下:
为了防止调用者所编写key的hashCode方法不够好而引起哈希值分布不均匀,JDK中将key包装在hash方法中,在将其作为参数和key以及value一同传给putVal方法中,putVal方法的源码如下:
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方法,源码如下:
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的一部分进行上锁。
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的一部分进行上锁。
相关文章推荐
- 010-http-2.4 new features
- oracle笔记整理13——性能调优之SQL优化
- Python 第三篇(上):python文件基础操作、json模块、lambda、map、filter、reduce和函数位置参数
- python的tab自动补全
- MFC使用ado连接SQLserver
- zookeeper学习笔记———《zookeeper-3.4.6单机伪集群配置》
- intellij idea直接编译spark源码及问题解决
- 把所有特权给root '%'所有IP
- 代理
- WinSCP和PuTTY的安装与配置(Ubuntu ssh)
- 009-httpd_ooenssl_cnf
- Python 4.1 类和实例
- Android TV-Building Layouts for TV
- 鼠标划过DIV层切换
- 008-httpd_http over ssl(https)
- linux c函数获取系统IP地址
- 燃气热水器微动开关
- 美麗的村落
- windows8.1下使用U盘安装Ubuntu双系统
- 2016/1/16学习笔记