【搞定Java基础-集合】第十篇:Java 集合类总结篇

目录

序言:Collection

一、List 总结篇

 1、List 接口描述

2、使用场景

3、区别

3.1  Aarraylist 和 Linkedlist

3.2  Vector 和 ArrayList 的区别

二、Map 总结篇

2.0 HashMap 和 TreeMap的不同点

2.1、Map 概述

2.2、内部哈希:哈希映射技术

2.3  Map 优化

2.3.1  调整容器初始化的大小

2.3.2  调整负载因子

2.4、HashMap 面试“明星”问题汇总

1、size 必须是 2 的整数次方原因;

2、get 和 put 方法流程;

3、resize 方法;

4、影响 HashMap 的性能因素(key 的 hashCode 函数实现、loadFactor、初始容量);

5、HashMap key 的 hash 值计算方法以及原因(见上面 hash 函数的分析);1.8的

6、1.8 HashMap 内部存储结构:Node 数组 + 链表或红黑树;

7、1.8 table[i] 位置的链表什么时候会转变成红黑树;

8、HashMap 主要成员属性:threshold、loadFactor、HashMap 的懒加载;

9、HashMap 的 get 方法能否判断某个元素是否在 map 中;【不能:null】

10、HashMap 线程安全吗,哪些环节最有可能出问题,为什么?

11、HashMap 的 value 允许为 null,但是 HashTable 和 ConcurrentHashMap 的 value 都不允许为 null,试分析原因?

12、HashMap 中的 hook 函数

三、Set 总结篇

四、对集合的选择

4.1  对 List 的选择

4.2  对 Set 的选择

4.3  对 Map 的选择

五、Comparable 和 Comparator



 

一、List 总结篇

前面已经充分介绍了有关于 List 接口的大部分知识,如 ArrayList、LinkedList、Vector、Stack,通过这几个知识点可以对List 接口有了比较深的了解了。只有通过归纳总结的知识才是你的知识。所以下面就List接口做一个总结。推荐阅读:

【搞定Java基础-集合篇】第二篇 源码ArrayList、LinkedList和Vector的区别

  • 1、ArrayList、LinkedList、HashMap中都有一个字段叫modCount(表示list结构上被修改的次数。add,remove这些都会改变modcount)
    • 1.1 Fail-Fast 机制: java.util.ArrayList 不是线程安全的,如果出现线程不安全,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。【通过modCount来实现,该字段被Iterator以及ListIterator的实现类所使用,如果该值被意外更改,Iterator或者ListIterator 将抛出ConcurrentModificationException异常】
  • 一、ArrayList源码分析
    • 1.1 ArrayList构造方法初始化一个空数组,直到第一次add的时候才初始化这个数组(因为第一次扩充时默认大小10一定是现在大小0*1.5的,所以第一次扩充是扩充到10)
    • 1.2 调用add方法插入数据【1、当使用add方法的时候首先调用ensureCapacityInternal方法,传入size+1进去,检查是否需要扩充elementData数组的大小。2、检查完毕之后再将e赋值给elementData数组 ,size再自增1。】【newCapacity = 扩充数组为原来的1.5倍(不能自定义)----->ArrayList中copy数组的核心就是System.arraycopy方法,将original数组的所有数据复制到copy数组中,这是一个本地方法】
    • 1.3 add(int index, E element)指定位置插入数据【1、ensureCapacityInternal检查是否要扩充数组,2、数据index及以后都后移一位,index位置插入数据System.arraycopy】
    • 1.4 remove 指定位置移除元素[1、numMoved=size-1-index表示删除元素之后要移动元素的总数,将elementData数组从index+1开始的numMoved个元素,往前移动1位(覆盖index位置的元素)。2、接着将elementData数组的最后一个元素设置为空,方便GC回收内存。 System.arraycopy],返回被删除的值
    • 1.5 get(int index)查找操作
  • 二、Vector源码分析
    • 2.1 Vector构造方法一开始就初始化长度为10,和数组扩增时容量为原来的2倍的object数组
    • 2.2 add方法插入数据。比起ArrayList,Vector许多对外公开的方法都加上了synchronized关键字声明,方法基本和ArrayList一样,比较明显不同就是grow方法数组容量的扩增算法,oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity)。 如果你指定了capacityIncrement变量的值,那么数组容量按你设置的值来扩增,否则每次默认容量就只是扩增一倍
  • 三、LinkedList 源码【双向链表】
    • 3.1 构造方法[为空,此时first和last都为null]
    • 3.2 add插入数据
    • 3.3 add 根据下标插入数据【1.node()方法找到该下标的Node; 2、在该Node前插入】
    • 3.4 remove 删除某个下标的数据【1.node()方法找到该下标的Node; 2、删除】
    • 3.5 get 查找数据
  • 四、总结
    • 1) 从使用方法的角度分析。ArrayList属于非线程安全,而Vector则属于线程安全。如果是开发中没有线程同步的需求,推荐优先使用ArrayList。因为其内部没有synchronized,执行效率会比Vector快很多。
    • 2) 从数据结构的角度分析。ArrayList是一个数组结构(Vector同理),数组在内存中是一片连续存在的片段,在查找元素的时候数组能够很方便的通过内存计算直接找到对应的元素内存。但是它也有很大的缺点。我们假设需要往数组插入或删除数据的位置为i,数组元素长度为n,则需要搬运数据n-i次才能完成插入、删除操作,导致其效率不如LinkedList。
    • 3) LinkedList的底层是一个双向链表结构,在进行查找操作的时候需要花费非常非常多的时间来遍历整个链表(哪怕只遍历一半),这就是LinkedList在查找效率不如ArrayList快的原因。但是由于其链表结构的特殊性,在插入、删除数据的时候,只需要修改链表节点的前后指针就可以完成操作,其的效率远远高于ArrayList。
  • 五、Stack

 1、List 接口描述

List 接口,称为有序的 Collection,也就是序列。该接口可以对列表中的每一个元素的插入位置进行精确的控制,同时用户可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。

【搞定Java基础-集合】第十篇:Java 集合类总结篇_第1张图片

Collection:Collection 层次结构中的根接口。它表示一组对象,这些对象也称为 Collection 的元素。对于 Collection 而言,它不提供任何直接的实现,所有的实现全部由它的子类负责;

AbstractCollection: 提供 Collection 接口的骨干实现,以最大限度地减少了实现此接口所需的工作(contains,toArray等)。对于我们而言要实现一个不可修改的 Collection,只需扩展此类,并提供 iterator 和 size 方法的实现。但要实现可修改的 Collection,就必须另外重写此类的 add 方法(否则,会抛出 UnsupportedOperationException),iterator 方法返回的迭代器还必须另外实现其 remove 方法;

Iterator: 迭代器;

ListIterator: 列表迭代器,允许程序员按任一方向遍历列表. 迭代期间可修改列表,并获得迭代器在列表中的当前位置;

List: 继承于Collection的接口。它代表着有序的队列;

AbstractList: List 接口的骨干实现,以最大限度地减少实现“随机访问”数据存储(如数组)支持的该接口所需的工作;

Queue: 队列。提供队列基本的插入、获取、检查操作;

Deque: 一个线性 Collection,支持在两端插入和移除元素。大多数 Deque 实现对于它们能够包含的元素数没有固定限制,但此接口既支持有容量限制的双端队列,也支持没有固定大小限制的双端队列;

AbstractSequentialList: 提供了 List 接口的骨干实现,从而最大限度地减少了实现受“连续访问”数据存储(如链接列表)支持的此接口所需的工作。从某种意义上说,此类与在列表的列表迭代器上实现“随机访问”方法;

LinkedList: List 接口的链接列表实现。它实现所有可选的列表操作;

ArrayList: List 接口的大小可变数组的实现。它实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小;

Vector: 实现可增长的对象数组。与数组一样,它包含可以使用整数索引进行访问的组件;

Stack: 后进先出(LIFO)的对象堆栈。它通过五个操作对类 Vector 进行了扩展 ,允许将向量视为堆栈;

Enumeration: 枚举,实现了该接口的对象,它生成一系列元素,一次生成一个。连续调用 nextElement 方法将返回一系列的连续元素;

【搞定Java基础-集合】第十篇:Java 集合类总结篇_第2张图片

二、Map 总结篇

在前面文章中已经详细介绍了 HashMap、HashTable、TreeMap 的实现方法,从数据结构、实现原理、源码分析三个方面进行阐述,对这个三个类应该有了比较清晰的了解,下面就对 Map 做一个简单的总结。

推荐阅读:

【搞定Java基础 - 集合篇】第三篇、源码Java7 -HashMap、HashTable、ConCurrentHashMap  【java7都是从头插入,且先扩容再插入,只有hashMap默认容量16允许key为null,hashTable默认容量11和ConCurrentHashMap默认并发数16,entry数组2的key和value都不为null】【HashMap和ConCurrentHashMap都是2倍扩容,hashTable是2倍+1】

  • 一、java7-HashMap源码
  • 1.1 数据结构:hashMap :Entry数组+链表 【扩容后数组大小为当前的 2 倍。】
  • 1.2 put(K key, V value)添加数据,是插入表头,key 为 null,放到 table[0] 中,会modcount++
    • 1)当插入第一个元素的时候,需要先初始化数组大小,保证数组大小一定是 2 的 n 次方。
    • 2)求 key 的 hash 值
    • 3)找到对应的数组下标
    • 4)遍历一下对应下标处的链表,看是否有重复的 key 已经存在,如果有,直接覆盖,put 方法返回旧值就结束了【hash相同,key的equal相同】
    • 5)不存在重复的 key,将此 entry 添加到链表的表头[返回null]
      • 5.1 如果当前 HashMap 大小已经达到了阈值,并且新值要插入的数组位置已经有元素了,那么要扩容 resize(2 * table.length);,扩容后,数组大小为原来的 2 倍
  • 1.4 V get(Object key)
    • 1)如果key时null,则遍历 table[0] 处的链表,如果不为null,则根据 key 计算 hash 值
    • 2)找到相应的数组下标:hash & (length – 1)
    • 3)遍历该数组位置处的链表,直到找到相等(hash值相等且自身==或equals)的 key。 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
  • 二、HashTable【 HashTable,只是在 HashMap 的基础上,给各个操作都加上了 synchronized 关键字而已,HashTable 中的键 key 和值 value 都不可为空。】
  • 2.1 构造方法:默认构造器,容量为:11,加载因子为:0.75,可以指定初始容量和默认加载因子
  • 3.2 put 方法:HashTable 中的键 key 和值 value 都不可为空。【在 put 方法中,如果需要向 table[ ] 中添加 Entry 元素,会首先进行容量校验,如果容量已经达到了阀值,HashTable 就会进行扩容处理 rehash()
    • 3.2.1 扩容rehash():容量扩大两倍+1,同时需要将原来 HashTable 中的元素一 一复制到新的 HashTable 中,同时还需要重新计算 hashSeed 的,毕竟容量已经变了。
  • 3.3 get 方法
  • 三、Java7-ConcurrentHashMap源码【segment数组+Entry数组+链表】【segment对象数组,一个segment对象里有个属性是Entry数组,Entry数组的每个位置放的是链表(结点是Entry)】
  • 3.1 数据结构: ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁(获取同步状态),所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
    • 1)initialCapacity:初始容量,这个值指的是整个 ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment。
    • 2)loadFactor:负载因子,Segment 数组不可以扩容,所以这个负载因子是给每个 Segment 内部使用的。
    • 3)concurrencyLevel:并行级别、并发数、Segment 数,默认是 16。【。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。】
  • 3.2 初始化得到segment数组
    • 1)根据你的设置计算并行级别 ssize,要保持并行级别是 2 的 n 次方,默认是16,一旦确定后就不可以扩容
    • 2)根据 initialCapacity【整个map初始大小】 计算 Segment 数组中每个位置可以分到的大小,Segment[i] 的默认大小为 2,负载因子是 0.75,得出初始阈值为 1.5,因为这样的话,对于具体的槽上,插入一个元素不至于扩容,插入第二个的时候才会扩容,segment 数组不能扩容,扩容是 segment 数组某个位置内部的数组 HashEntry[] 进行扩容,扩容后,容量为原来的 2 倍
    • 3)创建 Segment 数组(),并创建数组的第一个元素 segment[0](因为之后其余位置的初始化要利用segment[0]的参数)
      • 移位数segmentShift 的值为 32 – 4 = 28,掩码segmentMask 为 16 – 1 = 15;移位数用于得到hash的高几位,掩码用于限制下标不越界
        • hash 是 32 位,无符号右移 segmentShift(28) 位,剩下高 4 位, 然后和 segmentMask(15) 做一次与操作,也就是说 j 是 hash 值的高 4 位,也就是槽的数组下标【哪一个segment, int j = (hash >>> segmentShift) & segmentMask】,至于Entry里的下标是利用 hash 值,求应该放置的数组下标int index = (tab.length - 1) & hash;【即用hash的高几位作为segment数组的下标,用hash的低几位作为entry数组的下标】
  • 3.3 put添加元素【不支持value为null】
    • 一)找到元素所在的 segment 段s,然后放到该segment段 s.put()
      • 1)计算 key 的 hash 值
      • 2)根据 hash 值找到 Segment 数组中的位置 j【int j = (hash >>> segmentShift) & segmentMask;】
      • 3)ensureSegment(j) 对 segment[j] 进行初始化,用的CAS循环【因为存在并发,所以只要有一个 初始化成功即可】
    • 二)在 s 这个segment段中的插入元素,segment对象内部的put方法
      • 1)获取锁:在往该 segment 写入前,需要先获取该 segment 的独占锁,如果tryLock()快速获取锁失败,就要scanAndLockForPut获取锁【此方法有两个出口:一个是 tryLock() 成功了,循环终止,另一个就是重试次数超过了 MAX_SCAN_RETRIES,进到 lock() 方法,此方法会阻塞等待,直到成功拿到独占锁。】
      • 2)利用 hash 值,求应该放置的数组下标int index = (tab.length - 1) & hash;
      • 3)看该位置有无链表,如果有,遍历它找是否有重复元素,重复就覆盖,如果没有重复或者不存在链表,就将它设置为链表表头【都是从头插入】【在插入前要先判断是否超过了该 segment 的阈值,超过了则这个 segment 需要扩容,扩容后再进行插入】
      • 4)解锁
  • 3.4 get 获取元素
    • 1)计算 hash 值,找到 segment 数组中的具体位置【哪个槽】
    • 2)槽中也是一个数组,根据 hash 找到数组中具体的位置
    • 3)到这里是链表了,顺着链表进行查找即可
  • 3.5 并发问题分析
  • JDK1.7中ConcurrentHashMap与HashMap相比,有以下不同点
    • 1、ConcurrentHashMap线程安全,而HashMap非线程安全
    • 2、HashMap允许Key和Value为null,而ConcurrentHashMap不允许
    • 3、HashMap不允许通过Iterator遍历的同时通过HashMap修改,而ConcurrentHashMap允许该行为,并且该更新对后续的遍历可见
  • HashTable 与 HashMap 的区别
    • 第一: HashTable 基于 Dictionary 类,而 HashMap 是基于AbstractMap。Dictionary 是什么?它是任何可将键映射到相应值的类的抽象父类,而 AbstractMap 是基于 Map 接口的骨干实现,它以最大限度地减少实现此接口所需的工作。
    • 第二: HashMap 可以允许存在一个为 null 的 key 和任意个为 null 的 value,但是 HashTable 中的 key 和 value 都不允许为null。
    • 第三: Hashtable 的方法是同步的,而 HashMap 的方法不是。所以有人一般都建议如果是涉及到多线程同步时采用 HashTable,没有涉及就采用 HashMap,但是在 Collections 类中存在一个静态方法:synchronizedMap(),该方法创建了一个线程安全的 Map 对象,并把它作为一个封装的对象来返回,所以通过 Collections 类的 synchronizedMap 方法是可以同步访问潜在的HashMap。
  • Hashtable 与 Collections.synchronizedMap(HashMap) 的区别
    • 1、默认 Hashtable 和 synchrnizedMap 都是锁 类实例,synchrnizedMap 可以选择锁其他的 Object(mutex)
    • 2、Hashtable 的 synchronized 是方法级别的;synchrnizedMap 的 synchronized 的代码块级别的
    • 3、两者性能相近,但是 synchrnizedMap 可以用 null 作为 key 和 value

 

【搞定Java基础-集合篇】第四篇 Java8-HashMap和ConCurrentHashMap

  • 一、Java8-HashMap源码【2倍扩容】
    • 1.1 数据结构【数组+链表+红黑树,当put导致桶的数组长度>=64并且链表元素>8 个(插入的是第九个),会将链表转换为红黑树,降低查找的时间复杂度为 O(logN)。当红黑树节点数量<=6时,会转变为链表结构】【Java7: Entry,Java8 使用 Node来代表每个 HashMap 中的数据节点,,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。我们根据数组元素中,第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树的】
    • 1.2 put【Java7先扩容再插入到链表最前面,Java8先插入再扩容,插入到链表最后面】【key和value可以为null】
      • 1)第一次 put 值的时候,resize()初始化数组长度从 null 初始化到默认的 16 或自定义的初始容量,默认阈值 0.75
      • 2)找到具体的数组下标(n - 1) & hash,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了
      • 3)如果数组该位置有数据,判断是红黑树还是链表【插到链表最后面,重复就覆盖旧值,如果插入后个数是9个,则将链表转为红黑树】
      • 4)如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容,每次扩容数组和阈值都为原来的 2 倍
    • 1.3 HashMap 的 resize 函数源码分析 【重点中的重点】
      • 有两种情况会调用当前函数:
      • HashMap的容量为什么一定得是 2 的整数次幂呢?【面试重点】。
    • 1.4 get
      • 1) 第一个节点就匹配到了,直接返回,否则进行搜索
      • 2) 判断是否是红黑树,是,就找红黑树
      • 3)说明是链表,遍历链表来找
  • 二、Java8-ConCurrentHashMap
    • 2.1 数据结构【结构上和 Java8 的 HashMap 基本上一样,不过它要保证线程安全性,所以在源码上确实要复杂一些。也引入了红黑树】
    • 2.2 初始化【如果你有提供长度initialCapacity,那么数组长度为sizeCtl = 【 (1.5 * initialCapacity + 1),然后向上取最近的 2 的 n 次方】,但一般我们使用的默认构造器,不指定长度,默认数组长度是16】
    • 2.3 put【key和value都不能为null】
      • 1)如果数组"空",进行数组初始化
      • 2)找该 hash 值对应的数组下标【i = (n - 1) & hash)】,得到第一个节点 f
      • 3)if: 数组该位置f为空,用一次 CAS 操作将这个新值放入其中即可,如果 CAS 失败,那就是有并发操作,进到下一个循环就好了(1,2,3,4,5步骤都在for循环里,直到我们某一步成功放入,才break)
      • 4)else if: 容量不够,扩容,扩容后的数组容量为原来的两倍
      • 5)else: 数组该位置f不为空,获取数组该位置的头结点的监视器锁synchronized (f)【对数组的每个位置操作时都会这样加锁头结点】 ,如果是treeNode,则用红黑树的方法插入,如果是链表,判断是否有重复,重复覆盖,不重复则加到链表末尾,如果链表长度大于8,与HashMap不同的是它不一定会转换为红黑树,比如当前数组长度如果小于64,那会选择进行数组扩容,而不是转换为红黑树
    • 2.5 初始化数组:initTable
      • 1) sc = sizeCtl,然后CAS将 sizeCtl 设置为 -1,代表抢到了锁,如果本身就是-1了,说明初始化这件事被其他线程抢去了,我就只需Thread.yield();就好
      • 2)如果由我抢到了锁,初始化数组,长度为 16(默认长度) 或初始化时根据你提供的长度计算出来的长度,负载因子为0.75,table 是 volatile 的
    • 2.6 get
      • 1)计算 hash 值
      • 2)根据 hash 值找到数组对应位置: (n – 1) & h
      • 3)根据该位置处结点性质进行相应查找【如果该位置为 null,那么直接返回 null ->如果该位置处的节点刚好就是我们需要的,返回该节点的值即可->如果该位置节点的 hash 值小于 0,说明正在扩容,或者是红黑树,后面我们再介绍 find 方法->如果以上 3 条都不满足,那就是链表,进行遍历比对即可】

【搞定Java基础-集合】第七篇 TreeMap 和红黑树

【搞定Java基础-集合】第六篇:深入理解 LinkedHashMap 和 LRU 缓存

摘要:HashMap 和双向链表合二为一即是 LinkedHashMap

友情提示

1、LinkedHashMap 概述 

2、LinkedHashMap 在 JDK 中的定义

2.1  类结构定义

2.2  成员变量定义:增加了两个独有属性:双向链表头结点 header 和 迭代顺序标志位accessOrder【true=按访问顺序排序,false=按插入顺序排序(默认)】

2.3  成员方法定义

2.4  基本元素 Entry:重新定义了Entry,增加了两个指针 before 和 after用于维护双向链表

2.5  LinkedHashMap 的构造函数

2.6  LinkedHashMap 的数据结构

2.7  LinkedHashMap 的快速存取

LinkedHashMap 的存储实现 : put(key, vlaue):在 LinkedHashMap 中向哈希表中插入新 Entry 的同时,还会通过 Entry 的 addBefore(head) 方法将其链入到双向链表中。其中,addBefore 方法本质上是一个双向链表的插入操作

LinkedHashMap 的扩容操作 : resize(),扩容为原来的2倍

LinkedHashMap 的读取实现 :get(Object key)

2.8 LinkedHashMap 存取小结

1、LinkedHashMap 的存取过程基本与 HashMap 类似,只是在细节实现上稍有不同,这是由 LinkedHashMap 本身的特性所决定的,因为它要额外维护一个双向链表用于保持迭代顺序。

2、在 put 操作上,虽然 LinkedHashMap 完全继承了 HashMap 的 put 操作,但是在细节上还是做了一定的调整,比如,在LinkedHashMap 中向哈希表中插入新 Entry 的同时,还会通过 Entry 的 addBefore 方法将其链入到双向链表中。

3、在扩容操作上,虽然 LinkedHashMap 完全继承了 HashMap 的 resize 操作,但是鉴于性能和 LinkedHashMap 自身特点的考量,LinkedHashMap 对其中的重哈希过程(transfer方法)进行了重写(照着双向链表的顺序来重哈希)。在读取操作上,LinkedHashMap 中重写了HashMap 中的 get 方法(增加了 recordAccess方法,如果链表中元素的排序规则是按照插入的先后顺序排序的话,该方法什么也不做;如果链表中元素的排序规则是按照访问的先后顺序排序的话,则将 e 移到链表的末尾处),通过 HashMap 中的 getEntry 方法获取 Entry 对象,在此基础上,进一步获取指定键对应的值。

3、LinkedHashMap 与 LRU

3.1  put 操作与标志位 accessOrder:recordAccess 提供了 LRU 算法的实现,它将最近使用的 Entry 放到双向循环链表的尾部。也就是说,当 accessOrder 为 true 时,get 方法和 put 方法(如果不存在一样的key是插入链表尾部,若已经存在一样的key,就是更新,更新后会挪到链表的尾部)都会调用 recordAccess 方法使得最近使用的Entry移到双向链表的末尾;当 accessOrder 为默认值false 时,从源码中可以看出 recordAccess 方法什么也不会做。

3.2  get 操作与标志位 accessOrder

3.3  LinkedListMap 与 LRU 小结【访问标志accessOrder是决定put和get时要不要按访问顺序,removeEldestEntry方法是决定何时删除最近最久未访问节点,默认是返回false,即不会删除,若要删除即要实现LRU,你只需要重写这个方法】

1、使用 LinkedHashMap 实现 LRU 的必要前提是将 accessOrder 标志位设为 true 以便开启按访问顺序排序的模式。我们可以看到,无论是 put 方法还是 get 方法,都会导致目标 Entry 成为最近访问的 Entry,因此就把该 Entry 加入到了双向链表的末尾:get 方法通过调用 recordAccess 方法来实现。

2、put 方法在插入新的 Entry 时,通过createEntry 中的 addBefore 方法来实现插入链表尾部;在覆盖已有 key 的情况下,通过 recordAccess 方法来实现将更新的entry放到链表尾部。get操作也通过recordAccess 方法将该entry放到链表尾部。多次操作后,双向链表前面的 Entry 便是最近没有使用的。

3、在每次put插入新的Entry时,都会根据你重写的removeEldestEntry方法来决定是否要删除最近最久未访问元素(默认返回false,你可以重写成当节点个数大于多少时返回true),这样当节点个数大于某个数时,就会删除最前面的 Entry(head后面的那个Entry),因为它就是最近最久未使用的 Entry。

4、使用 LinkedHashMap 实现 LRU 算法

5、LinkedHashMap 有序性原理分析【利用双向链表进行迭代输出】

6、LinkedHashMap 【JDK1.8】

6.1 构造函数【增加了双向链表的head和tail,以及访问标志accessOrder】

二、get函数

三、afterNodeXXXX命名格式的三个函数在HashMap中只是一个空实现,是专门用来让LinkedHashMap重写实现的hook函数

3.1  afterNodeAccess(Node p) { }  //处理元素被访问后的情况:其功能为如果accessOrder为true,则将刚刚访问的元素移动到链表末尾

3.2 afterNodeInsertion(boolean evict) { }  //处理元素插入后的情况:即是否要删除最久未访问元素【根据你重写的removeEldestEntry()默认返回false,无需删除,如果你重写的返回true,则在元素插入后会删除最近最久未访问元素。】

3.3 afterNodeRemoval(Node p) { }  //处理元素被删除后的情况:在HashMap.removeNode()的末尾处调用, 将e从LinkedHashMap的双向链表中删除

7、总结

1、LinkedHashMap 在 HashMap 的数组加链表结构的基础上,将所有节点连成了一个双向链表。

2、put 方法在插入新的 Entry 时,通过createEntry 中的 addBefore 方法来实现插入链表尾部;在覆盖已有 key 的情况下,通过 recordAccess 方法来实现将更新的entry放到链表尾部。get操作也通过recordAccess 方法将该entry放到链表尾部。多次操作后,双向链表前面的 Entry 便是最近没有使用的。在每次put插入新的Entry时,都会根据你重写的removeEldestEntry方法来决定是否要删除最近最久未访问元素(默认返回false,你可以重写成当节点个数大于多少时返回true),这样当节点个数大于某个数时,就会删除最前面的 Entry(head后面的那个Entry),因为它就是最近最久未使用的 Entry。【实现 LRU 可以直接实现继承 LinkedHashMap 并重写removeEldestEntry 方法来设置缓存大小。JDK 中实现了 LRUCache 也可以直接使用。】

3、LinkedHashMap 的扩容比 HashMap 来的方便,因为 HashMap 需要将原来的每个链表的元素分别在新数组进行插入链化,而 LinkedHashMap 的元素都连在一个链表上,可以直接迭代然后插入。

2.0 HashMap 和 TreeMap的不同点

1、HashMap 通过 hashCode 对其内容进行快速查找,而 TreeMap 中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。HashMap 中元素的排列顺序是不固定的)。

2、在 Map 中插入. 删除和定位元素,HashMap 是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap 会更好。使用 HashMap 要求添加的键类明确定义了 hashCode() 和 equals() 的实现。 这个 TreeMap 没有调优选项,因为该树总处于平衡状态。

 

2.1、Map 概述

首先先看 Map 的结构示意图:

【搞定Java基础-集合】第十篇:Java 集合类总结篇_第3张图片

Map: “键值对” 映射的抽象接口。该映射不包括重复的键,一个键对应一个值;

SortedMap: 有序的键值对接口,继承 Map 接口;

NavigableMap: 继承 SortedMap,具有了针对给定搜索目标返回最接近匹配项的导航方法的接口;

AbstractMap: 实现了 Map 中的绝大部分函数接口。它减少了 “Map的实现类” 的重复编码;

Dictionary: 任何可将键映射到相应值的类的抽象父类。目前被 Map 接口取代;

TreeMap: 有序散列表,实现 SortedMap 接口,底层通过红黑树实现;

HashMap: 是基于“拉链法”实现的散列表。底层采用 “数组+链表” 实现;

WeakHashMap: 基于“拉链法”实现的散列表;

HashTable: 基于“拉链法”实现的散列表。

它们之间的区别:

【搞定Java基础-集合】第十篇:Java 集合类总结篇_第4张图片

2.2、内部哈希:哈希映射技术

几乎所有通用 Map 都使用哈希映射技术。对于我们程序员来说我们必须要对其有所了解。

哈希映射技术是一种将元素映射到数组的非常简单的技术。由于哈希映射采用的是数组结构,那么必然存在一种用于确定任意键访问数组的索引机制,该机制能够提供一个小于数组大小的整数,我们将该机制称之为哈希函数。在 Java 中我们不必为寻找这样的整数而大伤脑筋,因为每个对象都必定存在一个返回整数值的 hashCode 方法,而我们需要做的就是将其转换为整数,然后再将该值除以数组大小取余即可。如下

int hashValue = Maths.abs(obj.hashCode()) % size;

2.3  Map 优化

首先我们这样假设,假设哈希映射的内部数组的大小只有1,所有的元素都将映射该位置(0),从而构成一条较长的链表。由于我们更新、访问都要对这条链表进行线性搜索,这样势必会降低效率。我们假设,如果存在一个非常大数组,每个位置链表处都只有一个元素,在进行访问时计算其 index 值就会获得该对象,这样做虽然会提高我们搜索的效率,但是它浪费了空间。诚然,虽然这两种方式都是极端的,但是它给我们提供了一种优化思路:使用一个较大的数组让元素能够均匀分布。 在 Map 有两个会影响到其效率,一是容器的初始化大小、二是负载因子

2.3.1  调整容器初始化的大小

在哈希映射表中,内部数组中的每个位置称作 “存储桶” (bucket),而可用的存储桶数(即内部数组的大小)称作容量 (capacity)。我们为了使 Map 对象能够有效地处理任意数的元素,将 Map 设计成可以调整自身的大小。我们知道当 Map 中的元素达到一定量的时候就会调整容器自身的大小,但是这个调整大小的过程其开销是非常大的。调整大小需要将原来所有的元素插入到新数组中。我们知道 index = hash(key) % length。这样可能会导致原先冲突的键不在冲突,不冲突的键现在冲突的,重新计算、调整、插入的过程开销是非常大的,效率也比较低下。所以,如果我们开始知道 Map 的预期大小值,将 Map调整的足够大,则可以大大减少甚至不需要重新调整大小,这很有可能会提高速度。 下面是 HashMap 调整容器大小的过程,通过下面的代码我们可以看到其扩容过程的复杂性:【1.7】

void resize(int newCapacity) {
	Entry[] oldTable = table;                  // 原始容器
	int oldCapacity = oldTable.length;         // 原始容器大小
	if (oldCapacity == MAXIMUM_CAPACITY) {     // 是否超过最大值:1073741824
		threshold = Integer.MAX_VALUE;
		return;
	}
 
	// 新的数组:大小为 oldCapacity * 2
	Entry[] newTable = new Entry[newCapacity];    
	transfer(newTable, initHashSeedAsNeeded(newCapacity));
	table = newTable;
   /*
	* 重新计算阀值 =  newCapacity * loadFactor >  MAXIMUM_CAPACITY + 1 ?
	*                         newCapacity * loadFactor :MAXIMUM_CAPACITY + 1
	*/
	threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);   
}
 
// 将元素插入到新数组中
void transfer(Entry[] newTable, boolean rehash) {
	int newCapacity = newTable.length;
	for (Entry e : table) {
		while(null != e) {
			Entry next = e.next;
			if (rehash) {
				e.hash = null == e.key ? 0 : hash(e.key);
			}
			int i = indexFor(e.hash, newCapacity);
			e.next = newTable[i];
			newTable[i] = e;
			e = next;
		}
	}
}

2.3.2  调整负载因子

为了确认何时需要调整 Map 容器,Map 使用了一个额外的参数并且粗略计算存储容器的密度。在 Map 调整大小之前,使用”负载因子”来指示 Map 将会承担的“负载量”,也就是它的负载程度,当容器中元素的数量达到了这个“负载量”,则 Map 将会进行扩容操作。

例如:如果负载因子大小为 0.75,默认容量为11(HashTable),则 11 * 0.75 = 8.25 = 8,所以当我们容器中插入第 8 个元素的时候,Map 就会调整容器的大小。

负载因子本身就是在空间和时间之间的折衷:

当我使用较小的负载因子时,虽然降低了冲突的可能性,使得单个链表的长度减小了,加快了访问和更新的速度,但是它占用了更多的空间,使得数组中的大部分空间没有得到利用,元素分布比较稀疏,同时由于 Map 频繁的调整大小,可能会降低性能。

但是如果负载因子过大,会使得元素分布比较紧凑,导致产生冲突的可能性加大,从而访问、更新速度较慢。所以我们一般推荐不更改负载因子的值,采用默认值 0.75。

2.4、HashMap 面试“明星”问题汇总

你知道 HashMap 吗,请你讲讲 HashMap?

这个问题不单单考察你对 HashMap 的掌握程度,也考察你的表达、组织问题的能力。个人认为应该从以下几个角度入手(所有常见 HashMap 的考点问题总结):

1、size 必须是 2 的整数次方原因;

2、get 和 put 方法流程;

3、resize 方法;

4、影响 HashMap 的性能因素(key 的 hashCode 函数实现、loadFactor、初始容量);

5、HashMap key 的 hash 值计算方法以及原因(见上面 hash 函数的分析);

6、HashMap 内部存储结构:Node 数组 + 链表或红黑树;

7、table[i] 位置的链表什么时候会转变成红黑树(上面源码中有讲);

8、HashMap 主要成员属性:threshold、loadFactor、HashMap 的懒加载;

9、HashMap 的 get 方法能否判断某个元素是否在 map 中;【不能:null】

10、HashMap 线程安全吗,哪些环节最有可能出问题,为什么?

11、HashMap 的 value 允许为 null,但是 HashTable 和 ConcurrentHashMap 的 value 都不允许为 null,试分析原因?

12、HashMap 中的 hook 函数(在后面讲解 LinkedHashMap 时会讲到,这也是面试时拓展的一个点)

1、size 必须是 2 的整数次方原因;

原因是:

  •  1、* CPU对位运算支持较好,即位运算速度很快当 n 是 2 的整数次幂时:hash & (n - 1) 与 hash % n 是等价的,但是两者效率来讲是不同的,位运算的效率远高于取余 % 运算。****所以,HashMap中使用的是 hash & (n - 1)
  •  2、在1.8中,这还带来了一个好处,就是将旧数组中的 Node 迁移到扩容后的新数组中的时候有一个很方便的特性:【索引为 i 的节点,rehash后的索引只可能是 i 或者 i+oldcap,也就是我们可以这样处理:把 table[i] 这个桶中的 node 拆分为两个链表 l1 和 l2:如果hash & n == 0,那么当前这个 node 被连接到 l1 链表;否则连接到 l2 链表。这样下来,当遍历完 table[i] 处的所有 node 的时候,我们得到两个链表 l1 和 l2,这时我们令 newtab[i] = l1,newtab[i + n] = l2,这就完成了 table[i] 位置所有 node 的迁移(rehash),这也是 HashMap 中容量一定的是2的整数次幂带来的方便之处。 (因为Java8 是尾插,如果你一个一个的来放置的话,那么每个位置你都要遍历到该位置链表的尾部才能插入,耗时长【自己理解的】)】

2、get 和 put 方法流程;

java1.7的hashmap

  • 1.2 put(K key, V value)添加数据,是插入表头,key 为 null,放到 table[0] 中,会modcount++[达到阈值后,先扩容再插入]
    • 1)当插入第一个元素的时候,需要先初始化数组大小,保证数组大小一定是 2 的 n 次方。
    • 2)求 key 的 hash 值,找到对应的数组下标
    • 3)遍历一下对应下标处的链表,看是否有重复的 key 已经存在,如果有,直接覆盖,put 方法返回旧值就结束了【hash相同,key的equal相同】
    • 4)不存在重复的 key,将此 entry 添加到链表的表头[返回null]
      • 5.1 如果当前 HashMap 大小已经达到了阈值,并且新值要插入的数组位置已经有元素了,那么要扩容 resize(2 * table.length);,扩容后,数组大小为原来的 2 倍
  • 1.4 V get(Object key)
    • 1)如果key时null,则遍历 table[0] 处的链表,如果不为null,则根据 key 计算 hash 值
    • 2)找到相应的数组下标:hash & (length – 1)
    • 3)遍历该数组位置处的链表,直到找到相等(hash值相等且自身==或equals)的 key。 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))

Java1.8的hashMap【实质扩容的条件和1.7一样的,1.7是插入之前>=阈值就扩容再插入,1.8是插入之后发现 此时size > 阈值就扩容,所以都是此次会超过阈值就扩容】

  • 1.1 数据结构【数组+链表+红黑树,当put导致桶的数组长度>=64并且链表元素>8 个(插入的是第九个),会将链表转换为红黑树,降低查找的时间复杂度为 O(logN)。当红黑树节点数量<=6时,会转变为链表结构】【Java7: Entry,Java8 使用 Node来代表每个 HashMap 中的数据节点,,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。我们根据数组元素中,第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树的】
  • 1.2 put【Java7先扩容再插入到链表最前面,Java8先插入再扩容,插入到链表最后面】【key和value可以为null】
    • 1)第一次 put 值的时候,resize()初始化数组长度从 null 初始化到默认的 16 或自定义的初始容量,默认阈值 0.75
    • 2)找到具体的数组下标(n - 1) & hash
    • 3)如果数组该位置有数据,判断是红黑树还是链表【插到链表最后面,重复就覆盖旧值,如果插入后个数是9个,则将链表转为红黑树】
    • 4)如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容,每次扩容数组和阈值都为原来的 2 倍
  • 1.4 get
    • 1) 第一个节点就匹配到了,直接返回,否则进行搜索
    • 2) 判断是否是红黑树,是,就找红黑树
    • 3)说明是链表,遍历链表来找

3、resize 方法;

有两种情况会调用resize:

  • 1、之前说过 HashMap 是懒加载,第一次调用 HashMap 的 put 方法的时候 table 还没初始化,这个时候会执行 resize,进行table 数组的初始化,table 数组的初始容量保存在 threshold 中(如果从构造器中传入的一个初始容量的话),如果创建HashMap 的时候没有指定容量,那么 table 数组的初始容量是默认值:16。即,初始化 table 数组的时候会执行 resize 函数。
  • 2、扩容的时候会执行 resize 函数,插入元素后,当 size 的值 > threshold 的时候会触发扩容(1.8,先插入后发现>threshold),即执行 resize 方法,这时 table 数组的大小会翻倍。
  • 注意我们每次扩容之后容量都是翻倍( * 2),所以HashMap的容量一定是2的整数次幂。

4、影响 HashMap 的性能因素(key 的 hashCode 函数实现、loadFactor、初始容量);

  • 如果我们开始知道 Map 的预期大小值,将 Map调整的足够大,则可以大大减少甚至不需要重新调整大小,这会提高速度【避免了扩容】。
    • 假如你预先知道最多往 HashMap 中存储 64 个元素,那么你在创建 HashMap 的时候:如果选用无参构造器:默认容量16,在存储 16*loadFactor 个元素之后就要进行扩容(数组扩容涉及到连续空间的分配,Node 节点的 rehash,代价很高,所以要尽量避免扩容操作)。如果给构造器传入的参数是 64,这时 HashMap 中在存储 64 * loadFactor 个元素之后就要进行扩容;但是如果你给构造器传的参数为:(int)(64/0.75) + 1,此时就可以保证 HashMap 不用进行扩容,避免了扩容时的代价。
  • 负载因子本身就是在空间和时间之间的折衷:最好还是采用默认0.75

当我使用较小的负载因子时,虽然降低了冲突的可能性,使得单个链表的长度减小了,加快了访问和更新的速度,但是它占用了更多的空间,使得数组中的大部分空间没有得到利用,元素分布比较稀疏,同时由于 Map 频繁的调整大小,可能会降低性能。

但是如果负载因子过大,会使得元素分布比较紧凑,导致产生冲突的可能性加大,从而访问、更新速度较慢。所以我们一般推荐不更改负载因子的值,采用默认值 0.75。

5、HashMap key 的 hash 值计算方法以及原因(见上面 hash 函数的分析);1.8的

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

HashMap 允许 key 为null,null 的 hash 为 0。非 null 的 key 的 hash 高 16 位和低 16 位分别由:key 的 hashCode 高 16 位 和 hashCode 的高 16 位异或 hashCode 的低16位组成。主要是为了增强 hash 的随机性,减少 hash & (n - 1) 的随机性,即减小 hash 冲突,提高 HashMap 的性能。

  • (因为如果直接使用 hashCode & (n - 1) 来计算 index,此时 hashCode 的高位随机特性完全没有用到,因为 n 相对于hashCode 的值很小,计算 index 的时候只能用到低 16 位。基于这一点,把 hashCode 高 16 位的值通过异或混合到hashCode 的低 16 位,由此来增强 hashCode 低 16 位的随机性。)

6、1.8 HashMap 内部存储结构:Node 数组 + 链表或红黑树;

7、1.8 table[i] 位置的链表什么时候会转变成红黑树;

MIN_TREEIFY_CAPACITY值是64,也就是当链表长度>8的时候(插入后再转),有两种情况:

  1. 如果table数组的长度<64,此时进行扩容操作;

  2. 如果table数组的长度>=64,此时进行链表转红黑树结构的操作.

8、HashMap 主要成员属性:threshold、loadFactor、HashMap 的懒加载;

  •  HashMap 是懒加载,第一次调用 HashMap 的 put 方法的时候 table 还没初始化,这个时候会执行 resize,进行table 数组的初始化
  • threshold:threshold 也是比较重要的一个属性:创建 HashMap 时,该变量的值是:初始容量(2 的整数次幂),之后 threshold的值是 HashMap 扩容的门限值,即当前 Nodetable 数组的长度 * loadfactor。
  • loadFactor:是空间和时间的一个平衡点。
        * DEFAULT_LOAD_FACTOR较小时,需要的空间较大,但是 put 和 get 的代价较小;
        * DEFAULT_LOAD_FACTOR较大时,需要的空间较小,但是 put 和 get 的代价较大;

9、HashMap 的 get 方法能否判断某个元素是否在 map 中;【不能:null】

因为HashMap是可以存放值为null的,你并不能分辨到底是 不存在返回null 还是本身值是null;

// 入口,返回对应的value
public V get(Object key) {
	Node e;
		
	// hash函数上面分析过了
	return (e = getNode(hash(key), key))== null ? null : e.value;
}


public boolean containsKey(Object key) {
	// 注意与get函数区分,我们往map中put的所有的都被封装在Node中,
	// 如果Node都不存在显然一定不包含对应的key
	return getNode(hash(key), key) != null;
}

10、HashMap 线程安全吗,哪些环节最有可能出问题,为什么?

HashMap 在并发时可能出现的问题主要是两方面:

  • 如果多个线程同时使用 put 方法添加元素,而且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 一样),那么根据 HashMap 的实现,这两个 key 会添加到数组的同一个位置,这样最终就会发生其中一个线程 put 的数据被覆盖

    • 1.7 (实际是Entry e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e);)      大致意思就是e.next=table[i];table[i] =e; 

    • 同样1.8 在尾部插入的时候同时进行也会覆盖另一个线程的put

  • 如果多个线程同时检测到元素个数超过数组大小 * loadFactor,这样就会发生多个线程同时对 Node 数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给 table,也就是说其他线程的都会丢失,并且各自线程 put 的数据也丢失【这是1.8,如果是1.7头插还会导致死循环】

    • 1.8对table[i]是拆分成两个链表,再挂到table[i]和table[i+oldCap]下,一个线程拆分j位置会让 oldTab[j] = null; 另一个线程可能就认为该位置无元素,该线程把它扩容后的数组赋给table,就会丢失其他线程的扩容

    • 如果扩容的同时也put元素,比如put正在迁移的位置,那么因为最后会把拆分的两个链表赋给那两个位置,所以会导致各自线程put的数据也丢失。

11、HashMap 的 value 允许为 null,但是 HashTable 和 ConcurrentHashMap 的 value 都不允许为 null,试分析原因?

首先要明确 ConcurrentHashMap 和 Hashtable 从技术从技术层面讲是可以允许 value 为 null 。但是它们实际上是不允许的,这肯定是为了解决一些问题,为了说明这个问题,我们看下面这个例子(这里以 ConcurrentHashMap 为例,HashTable 也是类似)。

HashMap 由于允 value 为 null,get 方法返回 null 时有可能是 map 中没有对应的 key;也有可能是该 key 对应的 value 为 null。所以 get 不能判断 map 中是否包含某个 key,只能使用 contains 判断是否包含某个 key。

看下面的代码段,要求完成这个一个功能:如果 map 中包含了某个 key ,则返回对应的 value,否则抛出异常

if (map.containsKey(k)) {
   return map.get(k);
} else {
   throw new KeyNotPresentException();
}

1、如果上面的 map 为HashMap,那么没什么问题,因为 HashMap 本来就是线程不安全的,如果有并发问题应该用ConcurrentHashMap,所以在单线程下面可以返回正确的结果。【单线程下,hashmap包含key,返回null或其他值,不包含就抛出异常,能反应真实情况;而ConCurrentHashMap是用于多线程的,在判断key存在后,key能被别的线程删掉了,它会返回null,这就表示不存在能反应真实的情况,所以ConcurrentHashMap不能存放null】

2、如果上面的 map 为ConcurrentHashMap,此时存在并发问题:在 map.containsKey(k) 和 map.get 之间有可能其他线程把这个 key 删除了,这时候 map.get 就会返回 null,而 ConcurrentHashMap 中不允许 value 为 null,也就是这时候返回了 null,一个根本不允许出现的值?

但是因为 ConcurrentHashMap 不允许 value 为 null,所以可以通过 map.get(key) 是否为 null 来判断该 map 中是否包含该 key,这时就没有上面的并发问题了。

12、HashMap 中的 hook 函数

三、afterNodeXXXX命名格式的三个函数在HashMap中只是一个空实现,是专门用来让LinkedHashMap重写实现的hook函数

3.1  afterNodeAccess(Node p) { }  //处理元素被访问后的情况:其功能为如果accessOrder为true,则将刚刚访问的元素移动到链表末尾

3.2 afterNodeInsertion(boolean evict) { }  //处理元素插入后的情况:即是否要删除最久未访问元素【根据你重写的removeEldestEntry()默认返回false,无需删除,如果你重写的返回true,则在元素插入后会删除最近最久未访问元素。】

3.3 afterNodeRemoval(Node p) { }  //处理元素被删除后的情况:在HashMap.removeNode()的末尾处调用, 将e从LinkedHashMap的双向链表中删除

 

三、Set 总结篇

Set 就是 HashMap 将 value 固定为一个object,只存 key 元素包装成一个 entry 即可,其他和 Map 基本一样。

所有 Set 几乎都是内部用一个 Map 来实现,因为 Map 里的 KeySet 就是一个Set,而 value 是假值,全部使用同一个Object 即可。

Set 的特征也继承了那些内部的 Map 实现的特征。

HashSet:内部使用 HashMap 来存储元素和操作元素。

LinkedHashSet:内部使用 LinkedHashMap 来存储元素和操作元素。

TreeSet:内部是TreeMap 的 SortedSet。

ConcurrentSkipListSet:内部是 ConcurrentSkipListMap 的并发优化的 SortedSet。

CopyOnWriteArraySet:内部是 CopyOnWriteArrayList 的并发优化的 Set,利用其 addIfAbsent() 方法实现元素去重,如前所述该方法的性能很一般。

好像少了个 ConcurrentHashSet,本来也该有一个内部用 ConcurrentHashMap 的简单实现,但JDK偏偏没提供。Jetty就自己简单封了一个,Guava 则直接用 java.util.Collections.newSetFromMap(new ConcurrentHashMap())  实现。


四、对集合的选择

4.1  对 List 的选择

1、对于随机查询与迭代遍历操作,数组比所有的容器都要快。所以在随机访问中一般使用 ArrayList

2、LinkedList 使用双向链表对元素的增加和删除提供了非常好的支持,而 ArrayList 执行增加和删除元素需要进行元素位移。

3、对于 Vector 而已,我们一般都是避免使用。

4、将 ArrayList 当做首选,毕竟对于集合元素而已我们都是进行遍历,只有当程序的性能因为List的频繁插入和删除而降低时,再考虑 LinkedList。

4.2  对 Set 的选择

1、HashSet 由于使用 HashCode 实现,所以在某种程度上来说它的性能永远比 TreeSet 要好,尤其是进行增加和查找操作。

2、虽然 TreeSet 没有 HashSet 性能好,但是由于它可以维持元素的排序,所以它还是存在用武之地的。

4.3  对 Map 的选择

1、HashMap 与 HashSet 同样,支持快速查询。虽然 HashTable 速度的速度也不慢,但是在 HashMap 面前还是稍微慢了些,所以 HashMap 在查询方面可以取代 HashTable。

2、由于TreeMap 需要维持内部元素的顺序,所以它通常要比 HashMap 和 HashTable 慢。


五、Comparable 和 Comparator

实现 Comparable 接口可以让一个类的实例互相使用 compareTo 方法进行比较大小,可以自定义比较规则;

Comparator 则是一个通用的比较器,比较指定类型的两个元素之间的大小关系。

 

 

 

你可能感兴趣的:(Java集合)