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实例。

最新文章

  1. Python导入其他文件中的.py文件 即模块
  2. HTTP服务&amp;Ajax编程知识点导图
  3. JQ JSON数据类型
  4. C++ 虚函数表与内存模型
  5. Android Service组件(1)
  6. HDU 5062 Beautiful Palindrome Number(数学)
  7. 提问!同一ajax请求获取的图片路劲,在谷歌浏览器能正确展示图片,在火狐浏览器则显示路径undefined
  8. StackExchange.Redis学习笔记(三) 数据库及密码配置 GetServer函数
  9. etectMultiScale(gray, 1.2,3,CV_HAAR_SCALE_IMAGE,Size(30, 30))
  10. Android开发利器之Data Binding Compiler V2 —— 搭建Android MVVM完全体的基础
  11. 通过linkserver不能调远程表值函数
  12. 《TypeScript 中文入门教程》
  13. Arch Linux 的休眠设置
  14. FreeRTOS任务函数
  15. C语言实现mq收发数据的函数
  16. Leetcode 1004. 最大连续1的个数 III
  17. Flex DateTime Format
  18. Xtreme8.0 - Play with GCD dp
  19. Jmeter入门3 http请求—content-type与参数
  20. mysql压力测试工具Mysqlslap

热门文章

  1. WCF把书读薄(4)——事务编程与可靠会话
  2. [GO]给导入包起别名
  3. SVG素材整理(原)
  4. layer弹出框插件参数及方法介绍
  5. IOS 防坑指南
  6. C#帮助类:Base64
  7. HBase优化实战
  8. 201621123012 《java程序设计》第2周学习总结
  9. App Store Connect Operation Error ERROR ITMS-90032: &quot;Invalid Image Path - No image found at the path referenced under key &#39;CFBundleIcons&#39;: &#39;AppIcon20x20&#39;&quot;
  10. IPython绘图和可视化---matplotlib 入门