很多人都不喜欢看看源代码,我也不喜欢看,我甚至不喜欢别人写的代码,事实说明,千万不要指望别人能写出多么优秀的代码,更不要指望自己能写出多么优秀的代码。看了HashMap的源文件以后,被这本无需代码注释的代码,却注释的如此详细明了所折服。
那这个源代码从哪看起呢?平时我用的最多的就是put和get,貌似从put开始看起更切合实际。来自1.8的HashMap。
public V put(K key, V value) { return putVal(hash(key), key, value, false, true);}
还是那句话注释写的太棒了,put会返回key之前关联的value,如果key是首次进入则返回null。这里顺便比较一下HashTable的源代码,HashTable的put的方法多了一个synchronized的关键字,所以它是线程安全的,而HashMap是非线程安全的,正因为这个全局put操作上锁导致操作频繁时效率低小,所以有了ConcurrentHashMap。
有点扯远了,先继续看HashMap的putVal方法,里面有一个Node类型的数组,且数组的初始化工作也在这里,初步可以看出HashMap就是一个Node数组。再看一下Node这个内部类,有四个属性,hash,key,value,还有一个next属性也是Node类型,初步可以看出HashMap的每个节点下可能存在一个链表的数据结构。
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);
开始初始化时,按照默认16的大小,增长因子为0.75,创建Node数组,这个增长因子是增长到16*0.75=12时,就会double数组大小。初始化以后就可以在数组占位了,这里使用key的hash值和数组length-1做&运算,这样就确定了每一个Node都可以放到这个数组中。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
当然了这个hash是使用的key值的hashCode异或key的无符号右移16位,为了尽力避免碰撞发生。如果key值hash还是重复,就要使用链表来存放重复hash的Node节点,并保存next节点信息。当链表的节点数大于8时,会进行treeify操作,就是树形化,提高索引性能。当节点小于6时,树形结构又会被untreeify操作。
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value;}
同理,get时也是根据key值去拿Node,如果key的hash值相等且key值不为null且equals或key值相等,就get到目标节点。如果next是树节点,直接查找树。如果是链表,则循环查找直到满足上述条件。
HashMap可以看出是一个线程不安全,可以容纳null元素的,最好可以根据实际情况初始化数组大小,数组长度始终是一个2的n次幂的数据结构。