Collection集合
Collection接口下主要有三大子接口 List
Queue
Set
1. List
List是有序可重复集合,根据索引下标来访问元素
List接口常见的三个实现类:ArrayList
、LinkedList
、Vector
特点:
- 集合中的元素允许重复
- 集合中的元素有序,按照添加的顺序
- 集合中的元素通过索引下标快速访问
1.1 ArrayList
List集合的主要实现类,底层使用动态数组 Object[]
存储,线程不安全。如果在创建时没有指定大小,会创建一个空数组,在第一次进行添加元素时,数组容量扩充为10(默认)
动态扩容
在调用add()
方法添加元素时,会先将集合中的元素个数+1得到待添加元素的下标modCount
,然后比较待添加元素的下标与数组长度,如果等于数组长度说明数组容量不足(数组下标从0开始),执行扩容grow()
,扩容后得到一个新数组,将待添加的元素添加新数组末尾,并更新元素个数
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
// 扩容
elementData = grow();
elementData[s] = e;
size = s + 1;
}
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
在进行扩容前先求出新增一个元素后最小所需容量(比如原本有10个元素新添加1个元素,最小所需容量就是11),然后再判断数组是否非空
- 如果是空数组(还未创建)就新建一个大小为
max(DEFAULT_CAPACITY, minCapacity)
的数组。 - 非空数组,开始扩容先求扩容后数组容量,三个参数分别为 旧容量
oldCapacity
, 最小需要增加的容量
minCapacity - oldCapacity
, 旧容量的一半左右oldCapacity >> 1
。Math.max(minGrowth, prefGrowth) + oldLength
最小需要增加的容量和旧容量的一半左右两者取最大值 + 旧容量,求得扩容后数组容量newLength
,随后与Integer.MAX_VALUE - 8
进行比较,如果没有超过就返回newLength
。如果超过说明扩容后数组长度很大,此时需要调用hugeLength()
方法,判断oldLength + minGrowth
的大小 如果超过Integer.MAX_VALUE
就报OutOfMemoryError
异常 ,没有超过Integer.MAX_VALUE - 8
就返回Integer.MAX_VALUE - 8
,如果在Integer.MAX_VALUE - 8
与Integer.MAX_VALUE
之间就会返回Integer.MAX_VALUE
得到扩容后数组容量后就调用copyOf()
方法进行数组拷贝,底层是调用native
本地方法进行拷贝
private Object[] grow() {
//求最小所需容量
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
//判断数组非空
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 开始扩容 求扩容后数组容量
int newCapacity = ArraysSupport.newLength(oldCapacity,
//最小需要增加的容量
minCapacity - oldCapacity,
// 旧容量的一半左右
oldCapacity >> 1 );
//调用copyof方法进行数组拷贝
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
// 创建数组
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
// ArraysSupport类
public static final int MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8;
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
// 扩容后数组容量 = 最小需要增加的容量 和 旧容量的一半左右 两者取最大值 + 旧容量
int newLength = Math.max(minGrowth, prefGrowth) + oldLength;
if (newLength - MAX_ARRAY_LENGTH <= 0) {
// 新容量没有超过 Integer.MAX_VALUE - 8 就返回新容量
return newLength;
}
// 长度超过了Integer.MAX_VALUE - 8, 此时采取需要增加几个容量就扩几个 //如果超过 Integer.MAX_VALUE 就报OutOfMemoryError 异常
return hugeLength(oldLength, minGrowth);
}
private static int hugeLength(int oldLength, int minGrowth) {
int minLength = oldLength + minGrowth;
// 溢出
if (minLength < 0) { // overflow
throw new OutOfMemoryError("Required array length too large");
}
if (minLength <= MAX_ARRAY_LENGTH) {
return MAX_ARRAY_LENGTH;
}
return Integer.MAX_VALUE;
}
为什么在扩容是,需要与
Integer.MAX_VALUE - 8
比较,而不是Integer.MAX_VALUE
JVM虚拟机(Hotspot)在存储对象是还要存储对象头需要占用32字节。如果尝试分配更大的数组可能会导致OutOfMemoryError
请求的数组大小超过虚拟机限制
ArrayList对于内存空间浪费 体现在需要在集合尾部预留一部分空间
1.2 LinkedList
List集合中链式存储的集合,底层采用双向链表(JDK1.6之前为双向循环链表),LinkedList
还实现了Deque
接口,可以当作双端队列来使用。既可以当栈使用,也可以当队列使用
链式存储最大特点就是在元素之间插入新元素或者删除元素不需要移动之后的元素位置,但是访问元素需要遍历整个链表,不能像ArrayList
快速根据下标访问。
在日常使用集合的场景下,可以使用LinkedList
时同时也可以使用ArrayList
,且使用集合需要频繁遍历,因此ArrayList
几乎可以取代LinkedList
由于需要存储直接前驱和直接后继,因此每个元素都需要多两个指针的开销
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
1.3 Vector
Vector在List集合中不常用,底层使用Object[]
存储,最大特点是线程安全的,通过在方法上加synchronized
关键字来保证线程安全,执行效率比较低
2. Queue
队列是数据结构中比较重要的一种类型,队列的特点是先进先出FIFO,Queue
是单端队列,只能在一端添加元素,另一端移除元素,而Deque
是双端队列,在两端都可以进行增删操作。
常用实现类: ArrayDeque
、PriorityQueue
、LinkedList
队列中增删查元素时根据 因容量问题返回的结果不同 分为两类,一类操作失败后抛出异常,另一类操作失败后返回
add()
添加元素,在队列满的情况下抛异常offer()
添加元素,在队列满的情况下返回false
remove()
删除队列头元素,队列为空的情况下将抛异常poll()
删除队列头元素,在队列为空的情况下将返回null
element()
返回队列的头元素,在队列为空的情况下,将抛异常peek()
返回队列的头元素,在队列为空的情况下返回null
2.1 ArrayDeque
底层采用动态数组+双指针,且元素不支持NULL
transient Object[] elements;
transient int head;
transient int tail;
扩容过程类似于ArrayList
2.2 PriorityQueue
优先级队列,底层采用动态数组存储数组,通过二叉堆数据结构来实现优先级高的先出队。不支持存储NULL
,默认是小顶堆可以接收一个 Comparator
作为构造参数,从而来自定义元素优先级的先后。
transient Object[] queue;
int size;
private final Comparator<? super E> comparator;
3. Set
无序集合,不允许存放重复元素,常用实现类有HashSet
、LinkedHashSet
、TreeSet
3.1 HashSet
HashSet
是Set
集合最常用实现类,底层数据结构采用哈希表(数组+链表+红黑树)实现,相当于一个只有key的HashMap
,元素无序且唯一,可以存储null
元素。
无序性: 存储的元素在数组中是按照哈希值来决定位置
唯一性: 通过重写equals()
方法和 hashCode()
方法来保证元素的唯一性
public HashSet() {
map = new HashMap<>();
}
(详细过程见后续文章Map集合)
3.2 LinkedHashSet
继承HashSet
实现LinkedHashMap
接口,底层由哈希表+双向链表实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。常用的缓存淘汰策略LRU算法的普通版本就是这样实现的。
3.3 TreeSet
底层数据结构采用二叉树来实现,元素唯一且有序,唯一性需要重写hashCode
和equals()
方法,二叉树结构保证了元素的有序性。
排序分为自然排序(存储元素实现Comparable
接口)和自定义排序(创建TreeSet时,传递一个自己实现的Comparator
对象)