所在包:java.util
在编程的过程中,但凡遇到与数据结构相关的问题时,都离不开Collection接口与Map接口,两者是整个集合类库中最基本的根接口。而Collection接口主要负责实现一些线性结构,如线性表(顺序表)、链表、栈、队列等。
集合类库的关系图:
这里主要表现了部分与集合类库有关的接口与实现类们,其中粗体黑框是我们最为常用、尤其重要的实现类。
这张是完整的集合接口、子接口和实现类们的关系图,其中拓展了Vector、Stack、LinkedHashMap等实现或继承的子类们,关系网还是十分庞大的。
Collection接口声明的常用方法有:
1、 添加元素
- boolean add(E e)
- boolean addAll(Collection<? extends E> c)
2、 删除元素
- boolean remove(Object o)
- boolean removeAll(Collection<?> c)
3、 查找元素
- boolean contains(Object o)
- boolean containsAll(Collection<?> c)
4、 判断集合是否为空
- boolean isEmpty()
5、 清空集合所有元素
- void clear()
6、 获取集合元素的个数
- int size()
7、 获取迭代对象Iterator (用于foreach)
- Iterator iterator()
8、 将集合中所有元素转化为数组返回
- Object[] toArray()
- T[] toArray(T[] a)
由此,Collection接口又引申出了两个子接口:List和Set,两者主要区别在于 元素是否可以重复 。
List接口
List接口是 允许元素可以重复的 ,故又扩充了一些方法。
List接口扩充方法有:
1、 指定位置添加元素
- void add(int index,E element)
- boolean addAll(int index,Collection<? extends E> c)
2、 通过索引获取元素
- E get(int index)
- List subList(int fromIndex,int toIndex) (返回子集)
3、 通过对象查找索引
- int indexOf(Object o) (前往后找)
- int lastIndexOf(Object o) (后往前找)
4、 获取ListIterator迭代对象 (此迭代对象可往前往后迭代)
- ListIterator listIterator()
- ListIterator listIterator(int index) 指定位置
5、 删除指定位置元素
- E remove(int index)
6、 修改指定位置元素
- E set(int index,E element)
实现List接口的类有三,分别为ArrayList、Vector、LinkedList。
ArrayList 与 Vector
ArrayList 与 Vector 均是实现了顺序表(线性表)的数据结构,查找、修改元素的时间复杂度为 O(1),添加、删除元素的时间复杂度为 O(n)(若操作的是最后一个元素的话为 O(1)),其中 n 为指定位置之后元素的个数。两者主要的区别如下:
此处的 Enumeration 迭代器是老款的迭代器,与Iterator功能上无异。值得注意的是 ArrayList 使用频次 远远高于 Vector,主要原因在于 ArrayList 效率高。
如何动态实现扩容?
这个问题需要阅读源代码解决,此处也是面试常常出题的地方。
// 创建两个 ArrayList 集合
ArrayList<String> array1 = new ArrayList<>();
ArrayList<String> array2 = new ArrayList<>(5);
// 创建两个 Vector 集合
Vector<String> vector1 = new Vector<>();
Vector<String> vector2 = new Vector<>(5, 5);
ArrayList的实现和Vector的实现差别挺大的。
ArrayList的关键属性有五个,构造函数有三个:
// ArrayList 关键属性
// 默认容量
private static final int DEFAULT_CAPACITY = 10;
// 空元素数组,用于给 elementData 数组初始化
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认容量的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 实际装元素的数组
transient Object[] elementData; // non-private to simplify nested class access
// 装有元素的个数
private int size;
// ArrayList 构造函数
// 无参构造
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 有参构造 指定容量
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// 根据提供的集合构造
public ArrayList(Collection<? extends E> c);
从上面可以看出,ArrayList 的无参构造方法是将一个空Object数组赋值给elementData,此时数组的容量为0 ,即使存在默认容量为10,这一点值得探究;而 ArrayList 的指定容量 initialCapacity 的有参构造方法是new一个指定容量的数组赋值给 elementData,此时容量为指定容量 initialCapacity。
接下来我们来看看 Vector的关键属性和构造方法:
// Vector 关键属性
// 实际装元素的数组
protected Object[] elementData;
// 装有元素的个数(与ArrayList的size一样)
protected int elementCount;
// 单次扩容数量
protected int capacityIncrement;
// Vector 构造函数
// 无参构造
public Vector() { this(10); }
// 有参构造 指定容量
public Vector(int initialCapacity) { this(initialCapacity, 0); }
// 有参构造 指定容量 单次扩容数量
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
Vector 的关键属性较少,只有装元素的容器(数组)elementData、元素个数 elementCount 和 单次扩容数量 capacityIncrement ,最后一个主要用于指定当集合需要扩容时,每次扩容的数量,可以控制扩容速度,需要考虑内存使用的效益。再看 Vector 的构造方法,与 ArrayList 差别不大。
两者在初始化空集合时,均只是指定了容量以及分配了内存空间,并未涉及扩容的部分,接下来我们再看看添加元素的部分,观察当集合添加溢出时,集合内部是如何处理的。
程序执行如下操作:
// 为各个集合添加一个元素 "a"
array1.add("a");
array2.add("a");
vector1.add("a");
vector2.add("a");
从添加第一个元素可以看出,只有 array1 触发了扩容机制,其中经过了 add → grow → newCapacity 三个阶段。
// ArrayList 扩容流程
// 添加元素源码
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
// 扩容源码
private Object[] grow() {
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity));
}
// 生成新容量数组源码
// 输入的最小容量为最小需要扩大的容量,一般为 size + 1
private int newCapacity(int minCapacity) {
// overflow-conscious code
// 旧数组容量
int oldCapacity = elementData.length;
// 新数组容量 = 旧数组容量 * 2
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 判断计算出来的新数组容量是否满足最小要求
// 不满足
if (newCapacity - minCapacity <= 0) {
// 判断是否为空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// ※ 若默认容量大,返回默认容量 DEFAULT_CAPACITY
// 否则返回最小容量 minCapacity
return Math.max(DEFAULT_CAPACITY, minCapacity);
// 判断最小容量是否小于0
// 此情况存在于多个线程同时对集合进行操作
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 各种情况都不满足,返回最小容量
return minCapacity;
}
// 满足
// 再判断新数组容量是否超过了最大数组容量
// 没超过就返回新数组容量
// 超过了就返回最大数组容量(不细看)
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
经过程序的跟踪,我们知道 array1 走的是程序的 ※ 出口,即此时集合的容量为10,之前均为0。另外,ArrayList 实现扩容的方法通常为 原来的数组容量*2 ,而初始化空集合时,初始容量为0。
接下来再看看Vector扩容过程:
// Vector 扩容流程
// 添加元素源码
public synchronized boolean add(E e) {
modCount++;
add(e, elementData, elementCount);
return true;
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
elementCount = s + 1;
}
// 扩容源码
private Object[] grow() {
return grow(elementCount + 1);
}
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity));
}
// 生成新容量数组源码
// 输入的最小容量为最小需要扩大的容量,一般为 elementCount + 1
private int newCapacity(int minCapacity) {
// overflow-conscious code
// 旧容量
int oldCapacity = elementData.length;
// 新容量 = 旧容量 + 单次扩容数量
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
// 判断计算出来的新数组容量是否满足最小要求
// 不满足
if (newCapacity - minCapacity <= 0) {
// 判断最小容量是否小于0
// 此情况存在于多个线程同时对集合进行操作
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
// 满足
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
经过程序跟踪,Vector 实现扩容的方式与 ArrayList 的差别在于 Vector 可以指定单次扩容数量 capacityIncrement ,而 ArrayList 一律扩容为原来的两倍。
LinkedList
LinkedList 实现的是 链表 的数据结构,查找、修改元素的时间复杂度为 O(n),添加、删除元素的时间复杂度为 O(1)。另外,由于 LinkedList 还实现了 Deque 接口,它还能实现 栈、队列 这样的数据结构,可谓是多功能集合。
LinkedList扩展的方法有:
1、实现栈的方法:
- void push(E e) 入栈
- E pop() 出栈
2、实现(双向)队列的方法
- boolean offerFirst(E e) 在队头添加元素
- boolean offerLast(E e) 在队尾添加元素
- E peekFirst() 检索队头元素
- E peekLast() 检索队尾元素
- E pollFirst() 队头元素出队
- E pollLast() 队尾元素出队
LinkedList 的关键属性有:
// LinkedList 关键属性
// 元素的个数
transient int size = 0;
// 双向链表队头结点
transient Node<E> first;
// 双向链表的队尾结点
transient Node<E> last;
// 结点 静态内部类
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;
}
}
内部实现主要就是通过 Node 类进行增删查改结点,具体细节也就不一一列举了。
Set接口
Set接口则 不允许元素重复,其实现方式是用到之后要提到的 Map接口,以确保其中的元素不重复。
// 添加元素时,只存 key,不存 value
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// PRESENT 是其内部自己定义的一个空对象,用于存入value
private static final Object PRESENT = new Object();
要做到集合内的元素不重复,得按顺序确保以下几点:
- 元素的编码值不同,即元素的 HashCode() 方法获得的值不同
- 元素内对应的属性均不相同,可以通过 equals() 方法进行比较
所以,运用 Set 接口存储的元素类 必须实现 HashCode() 和 equals() 方法。
Set接口的实现类有:
- HashSet (由HashMap实现)
- 继承 HashSet 的 LinkedHashSet (与LinkedHashMap对应)
- TreeSet (由TreeMap实现)
// Set 的实现类们的无参构造函数
public HashSet() { map = new HashMap<>(); }
public TreeSet() { this(new TreeMap<>()); }
每个 Set 的实现类都有一个 Map 的实现类与之对应,具体实现原理与方法我们在后面的 Map 集合再展开。
HashSet 与 TreeSet
两者具体区别如下:
Set集合 | HashSet | TreeSet |
数据结构 | 哈希表 | 二叉树 |
元素是否需要实现Comparable | 否 | 是 |
元素是否有序 | 无 | 有 |
是否可以放入null值 | 可以,但只能放入一个 | 不可以 |
两者内部都是由一个一个包含 keyEntry 键值对的 Node 结点元素组成,只是存储与连接的方式不同。
而 LinkedHashSet 则额外比 HashSet 多提供了可预测的 迭代顺序 ,因为插入元素时是 有序 插入的,即元素之间有前后关系。
参考文献:JDK 11 API中文帮助文档