1、概览

容器主要包括Collection和Map两种,Collection存储着对象的集合,而map存储着键值对(两个对象)的映射表

Collection

1、set

  • TreeSet:基于红黑树实现,支持有序操作,。查找效率不如HashSet,HashSet查找的时间复杂度为O(1),TreeSet则为O(logN)。
  • HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去元素的插入顺序信息,即遍历HashSet时候得到的结果是不确定的。
  • LinkeHashSet:具有HashSet的查找效率,并且内部使用双向链表维护元素的插入顺序。

2、List

  • ArrayList:基于动态数组实现,支持随机访问
  • Vector:和ArrayList类型,但它是线程安全的。
  • LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList还可以用作栈、队列和双向队列。

3、Queue

  • LinkedList:可以用它来实现双向队列
  • PriorityQueue:基于堆结构实现,可以用它来实现优先队列。

Map

  • TreeMap:基于红黑树实现。
  • HashMap:基于哈希表实现。
  • HashTable:和HashMap类似,但它是线程安全的,这意味着同一时刻多个县城同时写入HashTable不会导致数据不一致。遗留类,不应该使用,而是使用 ConcurrentHashMap 来支持线程安全,ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
  • LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。

2、设计模式

迭代器模式:

Collection继承了Iterable接口,其中的iterator()方法能够产生一个Iterator对象,通过这个对象就可以迭代遍历Collection中的元素。

适配器模式

java.Util.Arrays.asList()可以把数组类型转换为List类型。

应该注意的是 asList() 的参数为泛型的变长参数,不能使用基本类型数组作为参数,只能使用相应的包装类型数组

3、源码分析

ArrayList

1、概述

因为ArrayList是基于数组实现的,所以支持快速随机访问。

数组的默认大小为10.

private static final int DEFAULT_CAPACITY = 10;

2、扩容

添加元素时候使用ensureCapacityInternal()方法来保证容量足够,需要使用grow()方法进行扩容,新容量的大小为(oldCapacity+oldCapacity/2)。因此,新容量大约是旧容量的1.5倍左右。 (oldCapacity 为偶数就是 1.5 倍,为奇数就是 1.5 倍-0.5)。

扩容时候,需要调用操作Arrays.copyOf()把原数组整个复制到新数组中,整个操作代价高,因此最好在创建ArrayList对象时候就指定大概的容量大小,减少扩容次数。

3、删除元素

需要调用System.arraycopy()将index+1后面的元素都复制到index位置上,该操作的时间复杂度是O(N),因此ArrayList删除元素的代价是很高的。

Vector

它的实现与ArrayList类似,但是使用了synchronized进行同步。

public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
} public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index); return elementData(index);
}

2、扩容

Vector的可以传入 capacityIncrement 参数,可以使在扩容时使容量增长 capacityIncrement 。如果这个参数值小于或等于0,扩容时每次扩容两倍。

3、与ArrayList的比较

  • Vector是同步的,因此开销就比ArrayList大,访问速度更慢。最好使用ArrayList而不是Vector,因为同步操作可以由程序员控制。
  • Vector每次扩容请求其大小2倍,而ArrayList是1.5倍。

4、替代方案

可以使用Collections.synchronizedList();得到一个线程安全的ArrayList

List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);
List<String> list = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList

1、读写分离

写操作在一个复制的数组上进行,读操作是在原始数组,读写分离,互不影响。

写操作需要加锁,防止并发写入时导致写入数据丢失

写操作结束后需要把原始数组指向新的复制数组

2、适用场景

CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。

缺陷

  • 内存占用:在写操作的同时需要复制一个新的数组,内存占用为原来的两倍
  • 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。

所以CopyOnWriteArrayList不适合内存敏感以及对实时性要求很高的场景。

LinkedList

基于双向链表实现,使用Node存储链表节点信息。

与ArrayList比较

ArrayList基于动态数组实现,LinkedList基于双向链表实现。ArrayList和LinkedList的区别可以归结为数组和链表的区别:

  • 数组支持随机访问,但插入删除的代价很高,需要移动大量元素
  • 链表不支持随机访问,但插入删除只需要改变指针。

HashMap

内部包含了一个 Entry 类型的数组 table。Entry 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值和散列桶取模运算结果相同的 Entry。

2、拉链法工作原理

  • 新建一个 HashMap,默认大小为 16;
  • 插入 <K1,V1> 键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 115%16=3。
  • 插入 <K2,V2> 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。
  • 插入 <K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 <K2,V2> 前面。

应该注意到链表的插入是以头插法方式进行的,例如上面的 <K3,V3> 不是插在 <K2,V2> 后面,而是插入在链表头部。

查找需要分成两步进行:

  • 计算键值对所在的桶;
  • 在链表上顺序查找,时间复杂度显然和链表的长度成正比。

HashMap 允许插入键为 null 的键值对。但是因为无法调用 null 的 hashCode() 方法,也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。

3、扩容

设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此查找的复杂度为 O(N/M)。

为了让查找的成本降低,应该使 N/M 尽可能小,因此需要保证 M 尽可能大,也就是说 table 要尽可能大。HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。

4、扩容-重新计算桶下标

在进行扩容时,需要把键值对重新计算桶下标,从而放到对应的桶上。在前面提到,HashMap 使用 hash%capacity 来确定桶下标。HashMap capacity 为 2 的 n 次方这一特点能够极大降低重新计算桶下标操作的复杂度。

假设原数组长度 capacity 为 16,扩容之后 new capacity 为 32:

对于一个 Key,它的哈希值 hash 在第 5 位:

  • 为 0,那么 hash%00010000 = hash%00100000,桶位置和原来一致;
  • 为 1,hash%00010000 = hash%00100000 + 16,桶位置是原位置 + 16。

5、链表转红黑树

从JDK1.8开始,一个桶存储的链表长度大于等于8时会将链表转为红黑树。

6、与Hashtable的比较

  • Hashtable使用synchronized来进行同步
  • HashMap可以插入键为null的Entry
  • HashMap是无序的

ConcurrentHashMap

ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。

LinkedHashMap

存储结构

继承自HashMap,因此具有和HashMap一样的快速查找特性。

内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序。

LinkedHashMap 最重要的是以下用于维护顺序的函数,它们会在 put、get 等方法中调用。

void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
afterNodeAccess()

当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。

afterNodeInsertion()

在 put 等操作之后执行,当 removeEldestEntry() 方法返回 true 时会移除最晚的节点,也就是链表首部节点 first。

evict 只有在构建 Map 的时候才为 false,在这里为 true。

最新文章

  1. C++中debug和release的区别 . 转载
  2. [PCL]keypoint
  3. AngularJS合集
  4. YII Framework 1.0运行时序图分析过程
  5. Surround the Trees(凸包求周长)
  6. 用python 爬取网页图片
  7. Kubernetes 架构(下)- 每天5分钟玩转 Docker 容器技术(121)
  8. opencv基本图像操作
  9. maven生成项目慢解决办法
  10. 2-STM32带你入坑系列(点亮一个灯--Keil)
  11. Linux系统常用升级的基础包
  12. LVS主从部署配置和使用
  13. H5浏览器播放RTMP直播流实现切换
  14. C++ Leetcode Median of Two Sorted Arrays
  15. Flex学习笔记-自定义菜单的显示细节
  16. 12_python_生成器
  17. MAC版Eclipse的常用快捷键
  18. linux c++环境
  19. MYSQL 备份及还原数据库
  20. Elasticsearch环境安装配置

热门文章

  1. NGK.IO的智能合约是炒作还是未来商业的主流?
  2. 谷歌地球服务器&quot;失联&quot;的替代方案
  3. JMM内存模型相关笔记整理
  4. HTTP2 的前世今生
  5. MySQL5.7.29 和 Navicat ===&gt; windows窗口式按装和使用
  6. Django Admin 后台Admin继承UserAdmin增加用户密码不显示明文和用户登录不了的解决方法
  7. C++教程01:计算机系统的组成
  8. Google单元测试框架gtest之官方sample笔记2--类型参数测试
  9. HDOJ-4027(线段树+区间更新(每个节点更新的值不同))
  10. 小程序基于Token登录 示意图