一、Java集合介绍
Java集合是一个存储同种类型的容器,因此我们也常叫做Java容器。它类似于数组但不同于数组的是创建数组时要制定长度(也就是存储数据的个数),但是容器不需要,这是因为Java容器底层依靠数组来实现,但又会根据存储而动态扩容。
二、容器框架图
三、常用容器方法介绍
Arrylist特性及注意细节
其根据动态数组实现,里面自带扩容方法,有数组的访问特性
(随机访问效率高,随机插入,删除效率低)
其线程不安全,适合单线程环境使用。
多线程环境下可使用Collections.synchronizedList(list)函数返回一个安全的ArrayList类,也可以使用并发包下的CopyOnWriteArrayList类。
存储实际数据,使用transient修饰,序列化时不会保存
允许元素为null
在new ArrayList时不会被初始化,在调用add()函数时会判断是否为空,如果为空则创建长度为10的数组
new ArryList容器时可以指定大小,不指定则初始化为0。
LinkList特征及注意细节
LinkList设计基于双向链表实现,因此可以在任何位置不移动其它元素的插入删除元素。还可以实现栈队列/双向队列
它的线程不安全,
Vector
Vector是矢量队列,基于动态数组实现,可以自动扩容,和ArrayList的原理一样,但是相比较看它的线程是安全的,它使用了synchronized实现安全,已经被弃用,我们已经使用CopyOnWriteArrayList替代了它
允许元素为空
扩容倍数为一倍
它有个子类Stack(用来实现栈存储)
CopyOnWriteArrayList
内部原理:把原来的数组拷贝一份出来然后在新数组进行写操作,操作完以后再将原来的数组引用指向新数组。
注意:
1、读多写少
2、不能保证实时一致性,只能保证最终一致性
3、写操作会消耗大量的内存,容量大的情况下可能频繁触发GC操作
4、不适用于实时都的情况,因为读到的数据可能是旧的
5、其写操作加了锁,不然多线程操作时可能会复制多个副本;读操作并没有加锁,所以可以并发的读
6、读写操作并发的执行可能读到旧数据
ArrayList和Vector的区别
ArrayList 的数据结构是动态数组,LinkedList 的数据结构是链表;
ArrayList 不支持高效的插入和删除元素,LinkedList 不支持高效的随机访问元素;
ArrayList 的空间浪费在数组末尾预留一定的容量空间,LinkedList 的空间浪费在每一个结点都要消耗空间来存储 prev、next 等信息。
三、Map
3.1 HashMap
HashMap 是以key-value 键值对形式存储数据,允许 key 为 null(多个则覆盖),也允许 value 为 null。底层结构是数组 + 链表 + 红黑树。
主要属性:
initialCapacity:初始容量,默认 16,2 的 N 次方。
loadFactor:负载因子,默认 0.75,用于扩容。
threshold:阈值,等于 initialCapacity * loadFactor,比如:16 * 0.75 = 12。
size:存放元素的个数,非 Node 数组长度。
数据结构:数组 + 单链表,Node 结构:hash|key|value|next

特征:
只允许一个 key 为 Null(多个则覆盖),但允许多个 value 为 Null
查询、插入、删除效率都高(集成了数组和单链表的特性)
默认的初始化大小为 16,之后每次扩充为原来的 2 倍
线程不安全
使用场景:
快速增删改查
随机存取
缓存
哈希冲突的解决方案:
开放定址法
再散列函数法
链地址法(拉链法,常用)
put() 存储的流程(Java 8):
计算待新增数据 key 的 hash 值;
判断 Node[] 数组是否为空或者数据长度为 0 的情况,则需要进行初始化;
根据 hash 值通过位运算定计算出 Node 数组的下标,判断该数组第一个 Node 节点是否有数据,如果没有数据,则插入新值;
如果有数据,则根据具体情况进行操作,如下:
如果该 Node 结点的 key(即链表头结点)与待新增的 key 相等(== 或者 equals),则直接覆盖值,最后返回旧值;
如果该结构是树形,则按照树的方式插入新值;
如果是链表结构,则判断链表长度是否大于阈值 8,如果 >=8 并且数组长度 >=64 才转为红黑树,如果 >=8 并且数组长度 < 64 则进行扩容;
如果不需要转为红黑树,则遍历链表,如果找到 key 和 hash 值同时相等,则进行覆盖返回旧值,如果没有找到,则将新值插入到链表的最后面(尾插法);
判断数组长度是否大于阈值,如果是则进入扩容阶段。
resize() 扩容的流程(Java 8):
扩容过程比较复杂, 迁移算法与 Java 7 不一样,Java 8 不需要每个元素都重新计算 hash,迁移过程中元素的位置要么是在原位置,要么是在原位置再移动 2 次幂的位置。
get() 查询的流程(Java 8):
根据 put() 方法的方式计算出数组的下标;
遍历数组下标对应的链表,如果找到 key 和 hash 值同时相等就返回对应的值,否则返回 null。
get() 注意事项:Java 8 没有把 key 为 null 放到数组 table[0] 中。
remove() 删除的流程(Java 8):
根据 get() 方法的方式计算出数组的下标,即定位到存储删除元素的 Node 结点;
如果待删结点是头节点,则用它的 next 结点顶替它作为头节点;
如果待删结点是红黑树结点,则直接调用红黑树的删除方法进行删除;
如果待删结点是链表中的一个节点,则用待删除结点的前一个节点的 next 属性指向它的 next 结点;
如果删除成功则返回被删结点的 value,否则返回 null。
remove() 注意事项:删除单个 key,注意返回是的键值对中的 value。
为什么使用位运算(&)来代替取模运算(%):
效率高,位运算直接对内存数据进行操作,不需转成十进制,因此处理速度非常快;
可以解决负数问题,比如:-17 % 10 = -7。
HashMap 在 Java 7 和 Java 8 中的区别:
存放数据的结点名称不同,作用都一样,存的都是 hashcode、key、value、next 等数据:
Java 7:使用 Entry 存放数据
Java 8:改名为 Node
定位数组下标位置方法不同:
- Java 7:计算 key 的 hash,将 hash 值进行了四次扰动,再进行取模得出;
Java 8:计算 key 的 hash,将 hash 值进行高 16 位异或低 16 位,再进行与运算得出。
扩容算法不同:
- Java 7:扩容要重新计算 hash
Java 8:不用重新计算
put 方法插入链表位置不同:
- Java 7:头插法
Java 8:尾插法
Java 8 引入了红黑树,当链表长度 >=8 时,并且同时数组的长度 >=64 时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高 HashMap 的性能。
3.2 HashTable
和 HashMap 一样,Hashtable 也是一个哈希散列表,Hashtable 继承于 Dictionary,使用重入锁 Synchronized 实现线程安全,key 和 value 都不允许为 Null。HashTable 已被高性能的 ConcurrentHashMap 代替。
主要属性:
initialCapacity:初始容量,默认 11。
loadFactor:负载因子,默认 0.75。
threshold:阈值。
modCount:记录结构性修改次数,用于快速失败。
快速失败原理是在并发场景下进行遍历操作时,如果有另外一个线程对它执行了写操作,此时迭代器可以发现并抛出ConcurrentModificationException,而不需等到遍历完后才报异常。
数据结构:链表的数组,数组 + 链表,Entry 结构:hash|key|value|next
特征:
key 和 value 都不允许为 Null;
HashTable 默认的初始大小为 11,之后每次扩充为原来的 2 倍;
线程安全。
原理:
与 HashMap 不一样的流程是定位数组下标逻辑,HashTable 是在 key.hashcode() 后使用取模,HashMap 是位运算。HashTable 是 put() 之前进行判断是否扩容 resize(),而 HashMap 是 put() 之后扩容。
3.3 ConcurrentHashMap
ConcurrentHashMap 在 Java 8 版本中丢弃了 Segment(分锁段)、ReentrantLock、HashEntry 数组这些概念,而是采用CAS + Synchronized 实现锁操作,Node 改名为 HashEntry,引入了红黑树保证查询效率,底层数据结构由数组 + 链表 + 红黑树组成,Node 数组默认为 16。
数据结构(Java 8):Node[] 数组 + 单链表 + 红黑树