1、HashMap
HashMap是Map接口最常见的实现,HashMap是非线程安全的,其内部实现是一种基于一个数组和链表的结合体,如下table为HashMap中存储数据的字段:
transient Entry[] table;
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; ...... }
上面的Entry就是数组中的元素,它持有一个指向下一个元素的引用,这就构成了链表。
加载因子:
final float loadFactor;
默认加载因子:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
默认容量:
static final int DEFAULT_INITIAL_CAPACITY = 16;
最大容量:
static final int MAXIMUM_CAPACITY = 1 << 30;
实际扩展容量边界:
int threshold;
当实际数据大小超过threshold时,HashMap会将容量扩容,threshold=容量*加载因子
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // Find a power of 2 >= initialCapacity int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity]; init(); }
HashMap的初始容量为capacity而不是参数initialCapacity:
int capacity = 1; while (capacity < initialCapacity) capacity <<= 1;//左移并赋值 table = new Entry[capacity];
如果执行new HashMap(9,0.75);那么HashMap的初始容量是16,而不是9。
当 我们往hashmap中put元素的时候,先根据key的hash值得到这个元素 在数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表 的形式存放,新加入的放在链头,最先加入的放在链尾。从hashmap中get元素时,首先计算key的hashcode,找到数组中对应位置的某一元 素,然后通过key的equals方法在对应位置的链表中找到需要的元素。如果每个位置上的链表只有一个元素,那么 hashmap的get效率将是最高的。
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); 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; }
HashMap充许key为null,put对象时,对于key为null的情况,HashMap的做法为获取Entry数组的第一个Entry对象,并基于Entry对象的next属性遍历,当找到了其中的Entry对象的key为null时,则将其中value赋值为新的value然后返回,如果没有key为null的Entry,则增加一个Entry对象:
private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; }
hashmap 中要找到某个元素,需要根据key的hash值来求得对应数组中的位 置。如何计算这个位置就是hash算法。前面说过hashmap的数据结构是数组和链表的结合,所以我们当然希望这个hashmap里面的元素位置尽量的 分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再 去遍历链表。
所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式那?java中时这样做的:
static int indexFor(int h, int length) { return h & (length-1); }HashMap的数组长度永远都是2的n次方, 数组的长度-1的二进制永远全部都是1,再与 key的hashcode值 做 一次“与”运算 (&)。看上去很简单,其实比较有玄机。比如数组的长度是2的4次方,那么hashcode就会和2的4次方-1做“与”运算。很多人都有这个疑 问,为什么hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高,我以2的4次方举例,来解释一下为什么数组大小为2的幂时 hashmap访问的性能最高。
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
说到这里,我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,看到上面 annegu的解释之后我们就清楚了吧,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询 的效率。 所以,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始 化的,代码如下(HashMap的构造方法中):
int capacity = 1; while (capacity < initialCapacity) capacity <<= 1;
在数组某个位置的对象可能并不是唯一的,它是一个链表结构,根据哈希值找到链表后,还要对链表遍历,找出key相等的对象,替换它,并且返回旧的值:
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; } }
如果遍历完了该位置的链表都没有找到有key相等的,那么将当前对象增加到链表的表头去:
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); if (size++ >= threshold) resize(2 * table.length); }
当 hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行 扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了, 而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
那么hashmap什么时候进行扩容呢?当put一个元素时,如果达到了容量限制,也就是threshold的值,数组大小*loadFactor,就会进行数组扩容,新的容量永远是原来的2倍:
if (size++ >= threshold) resize(2 * table.length);
loadFactor 的 默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那 么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。
2、TreeMap
TreeMap是一个支持排序的Map实现,是基于红黑树来实现的,TreeMap是非线程安全的。
3、HashTable
HashTable也是Map接口的一个实现,它是线程安全的,跟HashMap的主要区别有下面几点:
3.1 历史原因,Hashtable是基于陈旧的Dictionary类的,HashMap是Java 1.2引进的Map接口的一个实现
3.2 HashTable是线程安全的,HashMap是非线程安全的
3.3 在HashMap中,null可以作为键 ,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示 HashMap中没有该键,也可以表示该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。HashTable中key和value都不能为null。
public synchronized V put(K key, V value) { // Make sure the value is not null if (value == null) { throw new NullPointerException(); } // Makes sure the key is not already in the hashtable. Entry tab[] = table; int hash = key.hashCode();//key为空将抛出异常 int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { V old = e.value; e.value = value; return old; } } modCount++; if (count >= threshold) { // Rehash the table if the threshold is exceeded rehash(); tab = table; index = (hash & 0x7FFFFFFF) % tab.length; } // Creates the new entry. Entry<K,V> e = tab[index]; tab[index] = new Entry<K,V>(hash, key, value, e); count++; return null; }
3.4 HashTable使用Enumeration,HashMap使用Iterator。
3.5 HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。
3.6 哈希值的使用不同,HashTable直接使用对象的hashCode ,代码是这样的:
int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length;
而HashMap重新计算hash值,而且用与代替求模 :
static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } static int indexFor(int h, int length) { return h & (length-1); }
HashMap经验:
由于HashMap扩容会影响性能,在已经知道Map大小的情况下,实例化Map时最好要指定其大小,但不是直接指定,因为Map扩容时其容量并没有满,而是到其容量乘加载因子(0.75),所以这里指定其容量其是用已知的大小除以加载因子,但这里也不要除以.075,最好是除以0.7,这样能确保结果大于除以0.75的。
如把一个List里的元素全部放到HashMap里时:
HashMap<String,Foo> map; void addObjects(List<Foo> input){ map = new HashMap<String, Foo>(); for(Foo f: input){ map.put(f.getId(), f); } }
优化后:
HashMap<String,Foo> _map; void addObjects(List<Foo> input){ map = new HashMap<String, Foo>((int)Math.ceil(input.size() / 0.7)); for(Foo f: input){ map.put(f.getId(), f); } }