由上图可知,HashMap的基本数据结构是数组和单向链表或红黑树。
以下内容翻译于HashMap类的注释
HashMap是map接口的基础实现类。这个实现提供了所有可选的Map接口操作。并且允许null键和null值。HashMap类和Hashtable类差不多,只是HashMap不是线程完全的,并且HashMap允许null值和null键。这个类不保证map元素的顺序,也不保证顺序会随着时间保持不变。
假如hash函数能够使元素在桶中(buckets)均匀地分散,对于基本的get,put操作HashMap的性能还是比较稳定的。集合视图的遍历(EntrySet之类的操作,也许这些方法返回的结果都是集合类型,所以叫做集合视图)需要的时间和HashMap的"capacity"(buckets的数据)乘以数量(bucket中的健值对的数量)成正比。因此如果遍历性能非常重要,那么就不要把初始的CAPACITY设置的太大(或者LOAD_FACTOR太小)。
HashMap实例有有两个属性影响它的性能:CAPACITY和LOAD_FACTOR。CAPACITY是hash表里桶的数量,并且初始的CAPACITY仅仅是hash表创建时的容量。LOAD_FACTOR是hash表在自动地增加它的CAPACITY前,允许CAPACITY有多满的测量方式。当hash表里的条目的数量超过当前CAPACITY乘以LOAD_FACTOR的数量时,hash表被重新计算hash。(也就是说内部的数据结构被重建)。以便hash表具有大概两倍于原来桶数量。
一般来说,默认的loadfactory(0.75)在时间和空间消耗上提供了一个好的折中。更高的值减小了空间压力,但是增加了查询消耗(反映在HashMap中的大部分操作,包括get和put)。为了减小rehash的操作次数,当设置它的初始capacity时应该考虑将来的map中的条目数量和它的loadfactory。如果初始capacity大于条目最大数量除以loadfactory,就不会有rehash操作发生。
如果很多映射(键值对)将被存储在HashMap中。与在需要的时候自动地执行rehash操作来扩大hash表大小相比,创建一个足够大capacity的hashMap来存储映射将是更高效的。注意,很多key具有相同的hashCode()值是降低任何hash表性能的方式。
注意这个实现不是synchronized(线程安全)的。如果多个线程同时访问hashMap,并且只要有一个线程修改map结构,它就必须在外面被加上synchronized。(结构的修改是指任何增加或删除一个或多个的映射,仅仅修改一个健的值不是结构的修改)。这通常通过在天然地包裹map的对象上同步来实现。如果没有这样的对象存在。map应该用Collections.synchronizedMap方法包装一下。为了防止对map意外的不同步的访问,最好在创建的时候完成这样的操作。例如
Map m = Collections.synchronizedMap(new HashMap(...))
被这个类的”集合视图方法”返回的所有遍历器都是快速失败的:在这个遍历器创建之后,用任何方法除了iterator自身的remove方法修改map的结构将会抛出ConcurrentModificationException。因此面对同时的修改,遍历器快速而干净利落地失败。而不是在不确定的未来冒着不确定的危险。
注意,遍历器快速失败的行为不能被用来保证它看起来的样子。换句话说,在不同步的同时修改前面不能做任何强的担保。快速失败的遍历器尽量地抛出ConcurrentModificationException。写的程序依赖这个异常来保证正确性将是错误的。iterators的快速失败行为应该只被用于检测错误。
HashMap的几个重要的字段
/**
* 默认的CAPACITY值,也就是16,这个值必须是2的幂
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/** * capacity最大值, 当在构造器中传入一个比这大的参数的时候使用。 * 也就是说,当传入的值大于这个值,就使用这个值 * 必须是2的幂 */ static final int MAXIMUM_CAPACITY = 1 << 30;
/** * 在构造器中没有指定的时候被使用的默认的加载因子. */ static final float DEFAULT_LOAD_FACTOR = 0.75f;
/** * 使用树而不是链表的bin(就是之前常说的桶(bucket))中的数量阀值,当在bin中增加数据的时候,大于这个值
* 就会把bin中的数据从链表转换成红黑树结构来表示。这个值必须大于2并且应该小
* 小于8。 */
static final int TREEIFY_THRESHOLD = 8;
/** * 在resize操作中把bin中数据变为列表结构的数量阀值,如果小于这个值,就会 * 从树结构变为列表结构。这个值应该小于TREEIFY_THRESHOLD并且最大为6。 * most 6 to mesh with shrinkage detection under removal. */ static final int UNTREEIFY_THRESHOLD = 6;
/** * 当bin中的结构转换为树的时候,CAPACITY的最小值. * 否则就会resize当bin中数据太多的时候。应该至少4 * TREEIFY_THRESHOLD * 来避免resizing和树转换阀值之间的冲突。 * between resizing and treeification thresholds. */ static final int MIN_TREEIFY_CAPACITY = 64;
节点实现类
-
Node
1 /** 2 * 基本的bin节点, 被用于表示大部分的数据条目。 3 * 4 */ 5 static class Node<K,V> implements Map.Entry<K,V> { 6 final int hash; // 这个记录的是K的hash值 7 final K key; // map的键 8 V value; // map的值 9 Node<K,V> next; // 指向下一个节点 10 11 Node(int hash, K key, V value, Node<K,V> next) { 12 this.hash = hash; 13 this.key = key; 14 this.value = value; 15 this.next = next; 16 } 17 18 public final K getKey() { return key; } 19 public final V getValue() { return value; } 20 public final String toString() { return key + "=" + value; } 21 // 节点的hashCode,key的hashCode和value的hashCode的异或 22 public final int hashCode() { 23 return Objects.hashCode(key) ^ Objects.hashCode(value); 24 } 25 26 public final V setValue(V newValue) { 27 V oldValue = value; 28 value = newValue; 29 return oldValue; 30 } 31 // 重写的equals,如果节点的key和value都相等,两个节点才相等。 32 public final boolean equals(Object o) { 33 if (o == this) 34 return true; 35 if (o instanceof Map.Entry) { 36 Map.Entry<?,?> e = (Map.Entry<?,?>)o; 37 if (Objects.equals(key, e.getKey()) && 38 Objects.equals(value, e.getValue())) 39 return true; 40 } 41 return false; 42 } 43 }
静态的工具方法
/** * 计算key的hashCode值并且把hashCode的高16位和低16位异或。 * 这是一个折中的做法。因为现在大部分情况下,hash的分布已经 * 比较分散了,而且如果冲突比较多的时候,我们会把bin中的数据转 * 换为树结构,来提高搜索速度。 */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
疑问?
为什么采用这样的算法。或者说为什么要把hashCode的高16位和低16位异或。我一开始也想不明白,看其它的文章也很难找到把这一点解决明白的。
于是我就动手做实验,来验证,如果不采用异或会怎么样。采用异或之后有什么效果。
当然,不能忘记一点,计算hash值是为了找这个key对应的table数组之中的下标的。计算下标的算法是tab[i = (n - 1) & hash]。这里的n是table数组的数量。hash就是hash()方法返回的 值。他们两个求'与'。在源码的类说明里,说了一种情况,就是几个连续的Float类型的值在一个小的table中会冲突。我就以几个连续的Float值为样例测试。代码如下
1 /** 2 * 描述: 3 * 日期:2017年11月13 4 * @author dupang 5 */ 6 public class DupangTest { 7 public static void main(String[] args) { 8 Float f1 = 1f; 9 Float f2 = 2f; 10 Float f3 = 3f; 11 Float f4 = 4f; 12 13 String f1_hashCode = Integer.toBinaryString(f1.hashCode()); 14 String f2_hashCode = Integer.toBinaryString(f2.hashCode()); 15 String f3_hashCode = Integer.toBinaryString(f3.hashCode()); 16 String f4_hashCode = Integer.toBinaryString(f4.hashCode()); 17 18 System.out.println(f1_hashCode); 19 System.out.println(f2_hashCode); 20 System.out.println(f3_hashCode); 21 System.out.println(f4_hashCode); 22 23 int size = 198; 24 int f1_index = f1.hashCode()&(size-1); 25 int f2_index = f2.hashCode()&(size-1); 26 int f3_index = f3.hashCode()&(size-1); 27 int f4_index = f4.hashCode()&(size-1); 28 29 int f1_index_1 = hash(f1)&(size-1); 30 int f2_index_2 = hash(f2)&(size-1); 31 int f3_index_3 = hash(f3)&(size-1); 32 int f4_index_4 = hash(f4)&(size-1); 33 34 System.out.println(f1_index); 35 System.out.println(f2_index); 36 System.out.println(f3_index); 37 System.out.println(f4_index); 38 System.out.println("=========华丽的分割线==========="); 39 System.out.println(f1_index_1); 40 System.out.println(f2_index_2); 41 System.out.println(f3_index_3); 42 System.out.println(f4_index_4); 43 } 44 45 static final int hash(Object key) { 46 int h; 47 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 48 } 49 }