Map接口常用实现类学习
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实例。
Map接口常用实现类学习的更多相关文章
- Java基础 - Map接口的实现类 : HashedMap / LinkedHashMap /TreeMap 的构造/修改/遍历/ 集合视图方法/双向迭代输出
Map笔记: import java.util.*; /**一:Collection接口的 * Map接口: HashMap(主要实现类) : HashedMap / LinkedHashMap /T ...
- Map 接口有哪些类
Map接口 Map提供了一种映射关系,其中的元素是以键值对(key-value)的形式存储的,能够实现根据key快速查找value:Map中的键值对以Entry类型的对象实例形式存在:建(key值)不 ...
- Java基础Map接口+Collections工具类
1.Map中我们主要讲两个接口 HashMap 与 LinkedHashMap (1)其中LinkedHashMap是有序的 怎么存怎么取出来 我们讲一下Map的增删改查功能: /* * Ma ...
- Map接口、HashMap类、LinkedHashSet类
java.util.Map<K, V>接口 双列集合,key不可以重复 Map方法: 1.public V put(K key, V value):键值对添加到map,如果key不重复返回 ...
- Java中关于 ArrayList 和 Map 的常用遍历方法 (学习笔记,便于以后查询)
一.学习ArrayList与Map时,关于常用遍历方法的记录如下: 二.附源码如下: package com.study.in.myself; import java.util.ArrayList; ...
- Map的常用实现类及Entry的用法
public static void main(String[] args) { //map 键值对 json格式根据你的键名来获取对应的值 //特点 :无序.以键值对的形式添加元素,键不 ...
- 集合-List接口常用实现类的对比
1.collection接口:单列集合,用来存储一个一个的对象 2. list接口:存储有序的.可重复的数据. --->"动态数组",替换原有的数组 (1) Arraylis ...
- List接口常用实现类对比
相同点 都实现了List接口 储存了有序 可重复的数据 不同点 ArrayList 线程不安全 但是效率高 底层使用 Object[] elementData 实现 LinkedList 底层使用双向 ...
- 双列集合Map接口 & Collections工具类
HashMap 常用方法 遍历方式 iterator迭代器 ITIT HashTable 继承字典 Hashtable--Properties 文件读写 总结 Collections工具类
随机推荐
- Linux下的多线程下载工具mwget
之前在做项目的时候,遇到一个难题,需要一个多线程下载器,于是阴差阳错的看到了这款工具--mwget,之所以是阴差阳错,是因为mwget的多线程下载功能,并不是我们想要的多线程. wget大家都知道吧, ...
- MongoDB整理笔记の性能监控
方法一:Mongostat 此工具可以快速查看某组运行中的mongodb实例的统计信息,用法如下: [root@localhost bin]# ./mongostat insert query upd ...
- MongoDB整理笔记のGridFS
GridFS 是一种将大型文件存储在MongoDB 数据库中的文件规范.所有官方支持的驱动均实现了GridFS 规范. GridFS是MongoDB中的一个内置功能,可以用于存放大量小文件. 官网学习 ...
- XE中FMX操作ListBox,添加上千条记录(含图片)
我之前是想在ListBox的每个Item上添加一个图片,Item上所有的内容都是放在Object里赋值,结果发现加载一百条记录耗时四五秒: procedure TMainForm.AddItem; v ...
- springcloud集成swaggerui做动态接口文档
1.配置文件pom,一定要使用2.4以上的,2.4默认请求方式是json,会导致getmapping设置参数类型对对象时,swaggerui界面不能指定为其他类型,反正就是各种坑,不建议用 <d ...
- C#Task学习
简介: Task 对象是一种的中心思想基于任务的异步模式首次引入.NET Framework 4 中. 因为由执行工作Task对象通常以异步方式执行线程池线程上而不是以同步方式在主应用程序线程中,可以 ...
- 复制构造函数被调用的三种情况------新标准c++程序设计
1.当用一个对象去初始化同类的另一个对象时,会引发复制构造函数被调用.例如,下面的两条语句都会引发复制构造函数的调用,用以初始化c2. C c2 (c1); C c2=c1; 这两条语句是等价的.注意 ...
- jquery选取自定义属性为已知值的元素
$("div[myattr='value']") //选取自定义myattr属性为value的div
- 20165219 2017-2018-2 《Java程序设计》第8周学习总结
20165219 2017-2018-2 <Java程序设计>第8周学习总结 教材学习内容总结 进程与线程 线程是比进程更小的单位:线程间可以共享进程中的某些内存单元 java的多线机制 ...
- Codeforces Round #545 (Div. 2)D(KMP,最长公共前后缀,贪心)
#include<bits/stdc++.h>using namespace std;const int N=1000007;char s1[N],s2[N];int len1,len2; ...