HashMap
1.8的HashMap:数组加链表加红黑树结构
最重要的内部类有2个:
Node,实现了Map.Entry接口。有4个成员变量:int hash,K key,V value,Node next,有且只有一个4个参数的构造器:Node(int hash, K key, V value, Node<K,V> next)。
TreeNode,继承了LinkedHashMap.Entry,而LinkedHashMap.Entry又继承了HashMap.Node。LinkedHashMap.Entry除了继承自HashMap.Node的4个成员变量外,还有2个成员变量:LinkedHashMap.Entry before,LinkedHashMap.Entry after。TreeNode除了继承自LinkedHashMap.Entry的6个成员变量外,还有5个成员变量:TreeNode parent,TreeNode left,TreeNode right,TreeNode prev,boolean red,也有且只有一个4个参数的构造器:TreeNode(int hash, K key, V val, Node<K,V> next),构造器参数同HashMap.Node的构造器参数一样。
常用方法实现:
put()方法实现:
内部调用putVal()方法。putVal()方法第一个参数是hash(key),是根据key计算出的一个hash值,如果key为null,则hash值为0,否则根据key的hashCode值计算出来一个值。第二个参数是key,第三个参数是value,第四个参数是boolean类型变量,是否仅在key不存在时生效,写死false。第五个参数是boolean类型变量,是否驱逐,写死true。
putVal方法首先判断当前Node数组是否为null,或者为空,如果是的话,就调用resize()方法扩容,扩容后容量为大于等于初始容量的最小的2次幂。之后,通过hash值找到在数组中的位置。索引值是(n - 1) & hash,其中n是数组的长度,即hash值与数组长度-1做位与运算,计算的结果小于等于n - 1。找到位置后,看此位置有没有元素,如果没有元素,则新建一个Node实例并放到这个位置上,注意该Node实例的next属性值为null。如果有元素,则先判断该元素的hash属性值与传进来的hash值是否相等以及该元素的key属性值是否==或者equals传进来的key(从这里可以看出,HashMap判断两个key是否一样,是依赖key的hashCode()方法与equals()方法的)。如果返回true,则说明 就拿新的value值替换老的value值。之后,再把modcount变量值加1。再把size变量值加1,再判断size是否大于threshold变量值,如果大于,则要调用resize()扩容。
我们常用的类,都重写了hashCode()和equals()方法,如7个原始类型对应的包装类、ArrayList/LinkedList(AbstractList重写了)、HashSet/LinkedHashSet/TreeSet(AbstractSet重写了)、HashMap/LinkedHashMap/TreeMap(AbstractMap重写了)、String、Date、BigDecimal。如果key是自定义类型,则在定义这个类时,必须要重写hashCode()与equals()方法。Set的元素的类型,也要求重写hashCode()方法和equals()方法。
get()方法实现:
参考另一篇文章:https://www.cnblogs.com/koushr/p/5873371.html
remove()方法实现:
内部调用removeNode()方法,第一个参数是key的hash值,第二个参数是key,其他参数不重要。首先根据hash值找到索引位置,如果索引位置上没有元素,则返回null。如果有元素,则判断这个元素的hash属性值是否和入参key的hash值一样,并且这个元素的key属性值是否==或者equals入参key,如果返回true,则说明。如果是TreeNode,则调用TreeNode的removeTreeNode()方法,。再把modCount值加1,size值减1。
clear()方法实现:
先把modCount值加1,然后如果Node数组不为null且长度大于0,则把size置为0,并遍历数组,把所有位置都赋值为null。
size()方法实现:
返回size变量的值。put()、remove()、putAll()、clear()方法都会维护size值,putxxx是加1,remove是减1,clear直接清0。
containsKey()方法实现:
内部调用getNode()方法,然后判断getNode()返回值是否为null。
containsValue()方法实现:
遍历数组以及链表,直到某一个Node实例的value属性值==或者equals入参value,如果找到就返回true,否则返回false。从这里可以看出,HashMap判断value相同的依据是两个value==或者equals返回为true,跟value的hashCode()没关系。
entrySet()方法实现:
HashMap有一个Set类型的entrySet变量,如果是第一次调用,会把entrySet变量初始化为一个新生成的EntrySet实例并返回。此后再调用的话,就直接返回这个EntrySet实例。EntrySet是HashMap的内部类,继承了AbstractSet,重写了Set接口的很多方法,如size()方法直接返回当前HashMap实例的size变量值。clear()方法内部是调用当前HashMap实例的clear()方法。iterator()方法会返回一个EntryIterator实例。EntryIterator继承了HashIterator,其next()方法内部调用HashIterator的nextNode()方法。从nextNode()方法的实现中可以看出在迭代时不能对HashMap进行增删操作的原因。nextNode()方法内部会比较当前modCount与创建EntryIterator实例时的modCount是否相同,如果不同,就会抛ConcurrentModificationException,而增删操作会使得modCount+1,所以在调用nextNode()方法前不能对HashMap实例进行增删操作,但是可以在nextNode()方法之后对HashMap实例进行增删操作,这时假如还没迭代完的话,就又会调用nextNode()方法,就会抛ConcurrentModificationException异常,所以只有在最后一次迭代的时候,对HashMap实例进行增删操作不会抛异常。
示例:
public static void main(String[] args) { Map<Integer, Integer> map = new HashMap<>(); map.put(1, 2); map.put(2, 2); Set<Entry<Integer, Integer>> entrySet = map.entrySet(); System.out.println(entrySet); Iterator<Map.Entry<Integer, Integer>> iterator = entrySet.iterator(); int i = 0; while (iterator.hasNext()) { // map.put(3, 3); System.out.println(iterator.next()); i++; if (i == map.size()) { map.put(4, 4); } } System.out.println(entrySet); }
map.put(3, 3);之后调用iterator.next()会抛异常,map.put(4, 4);之后,会跳出迭代,所以不会抛异常。
两次打印entrySet的结果不一样,这是为啥呢?
打印entrySet,其实是打印entrySet调用toString()方法的返回值。EntrySet的toString()方法继承自AbstractCollection,
public String toString() { Iterator<E> it = iterator(); if (! it.hasNext()) return "[]"; StringBuilder sb = new StringBuilder(); sb.append('['); for (;;) { E e = it.next(); sb.append(e == this ? "(this Collection)" : e); if (! it.hasNext()) return sb.append(']').toString(); sb.append(',').append(' '); } }
toString()内部会先调用iterator()方法拿到迭代器,接着去遍历。
什么情况下要扩容?有以下几种情况:
1、size > threshold的情况。包括第一次putVal时数组初始化以及之后putVal时size > threshold的情况。threshold是容量*负载因子取整后的值。
2、链表长度达到8时,每增加一个链表元素,就会扩容,直到数组长度大于等于64。数组长度达到64之后,就要把链表结构转为红黑树结构了。TreeNode上场。
if (binCount >= TREEIFY_THRESHOLD - 1) { treeifyBin(tab, hash); }
在treeBin()方法中会调用resize()方法。
resize()方法中,老数组元素在新数组中如何放,也是很复杂的。以后再研究。
LinkedHashMap
1.8的LinkedHashMap:数组加双向链表加红黑树结构
类似于HashMap中Node内部类的作用,LinkedHashMap有一个内部类Entry,继承了HashMap.Node。LinkedHashMap.Entry有2个额外的属性,Entry类型的before、Entry类型的after。每插入一个新的Entry实例时,都要维护下和上一次插入的Entry实例的先后顺序。当前Entry实例的before属性被赋值为上一个Entry实例,上一个Entry实例的after属性被赋值为当前Entry实例。
TreeMap
1.8的TreeMap:红黑树实现
内部类Entry代表红黑树节点,有6个成员变量:K key,V value,Entry<K,V> left,Entry<K,V> right,Entry<K,V> parent,boolean color(初始值是true,黑色节点)。
构造器:
有4个构造器,无参构造器,参数为Comparator实例的构造器,参数为Map实例的构造器,参数为SortedMap实例的构造器。
TreeMap内部是由红黑树实现的,见https://www.cnblogs.com/koushr/p/5873421.html。
TreeMap是依据Comparator实例排序的,可以在构造TreeMap实例的时候传进去。如果用入参是SortedMap实例构造,则Comparator实例是SortedMap实例对应的Comparator实例。如果用无参构造器或者非SortedMap类型的Map实例,则Comparator实例是null,这时要求键类型K必须实现Comparable接口,从而调用key的compareTo方法来比较。如果K没有实现Comparable接口,就会报类型转换异常 K cannot be cast to java.lang.Comparable。
TreeMap第一个放进去的TreeMap.Entry实例是初始根节点,之后放进去的就会依据Comparator实例或者key的compareTo方法找到自己的初始位置,之后会检查是否还满足红黑树的5个特性,如果不满足的话,就会有节点变色、旋转等操作(见fixAfterInsertion方法),以满足红黑树的5个特性。
需要指出的是,TreeMap的有序和LinkedHashMap的有序是两种截然不同的意思。LinkedHashMap的序指的是插入顺序,如果accessOrder值是默认值false的话,而TreeMap的序指的是自排序后的顺序,而非插入顺序。举个简单的例子,往new LinkedHashMap()和new TreeMap()中分别放入5个元素,key依次是1、4、3、2、5,则LinkedHashMap实例的KeySet实例的Iterator实例遍历时,key值依次是1、4、3、2、5,和插入顺序一致。TreeMap实例的KeySet实例的Iterator实例遍历时,key值依次是1、2、3、4、5,因为内部排序时采用的是Integer实例的int compareTo(Integer anotherInteger)方法,单纯比较key的大小。如果我们想自定义取出顺序时,可以传入自定义的Comparator实例。