【基本内容】
集合框架是Java编程开发中最常用的技术框架。Java的集合框架主要分为单列集合和双列集合,本章主要讲述单列集合。
单列集合的根接口是Collection接口,下分三个子接口:List、Set、Queue, 三个子接口的特征如下图表:
其中的有序无序不是指元素值的大小顺序,而是指元素存取的顺序一致,即以什么顺序存入的,取出时就是什么顺序,例如以下ArrayList代码:
ArrayList是按索引的顺序存入1、4、2、3,因而按索引顺序打印出来的也是1、4、2、3。
关于单列集合主要的继承关系如下图:
这图主要列出了三个分类接口的主线继承关系,但为了满足开发人员的各种需求,实际上Jdk提供的集合类各种数据结构繁多,继承关系远比上图复杂,还有许多分支。我们先从List接口开始一一介绍。
一、List接口
List接口主要有以下几个实现类:ArrayList、LinkedList、Vector和Statck,其中LinkedList继承关系稍复杂,后面单列。下图为List实现类的区别:
其中,ArrayList、Vector和Statck因为存储结构采用的都是数组,即顺序存储,因而元素存取的时间复杂度同顺序表: 查询时间复杂度为O(1),插入和删除时间复杂度为O(n),而LinkedList底层存储是双向链表即链式存储,因而其时间复杂度刚好相反: 查询时间复杂度为O(n),插入和删除时间复杂度为O(1)。关于存储结构和顺序表相关概念可参看作者相关文章数据结构和算法之基本概念和数据结构和算法之线性结构。
ArrayList和Vector
ArrayList虽然底层是用数组实现,但是在设计目标上,又不完全等同于数组,数组是纯粹的顺序表,存储空间是预先分配的不能动态扩容,而ArrayList允许动态扩容,那ArrayList是怎么实现的呢?我们先看看ArrayList的部分定义代码:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
@java.io.Serial
private static final long serialVersionUID = 8683452581122892189L;
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
第二段申明其构造函数的代码:
/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
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);
}
}
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
从上述两段代码可以看出,ArrayList底层是用数组来存储元素的,其中成员变量elementData是用来动态增删元素的,而其私有成员变量DEFAULTCAPACITY_EMPTY_ELEMENTDATA 则是用来初始化的缺省空数组,而数组缺省大小为DEFAULT_CAPACITY的值10。
继续看ArrayList的代码:
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
private Object[] grow() {
return grow(size + 1);
}
......
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;
}
从上述代码可以看出,当往ArrayList增加数据元素时,会校验数组的大小是否已经等于元素的个数,如果相等就会调用grow()方法进行动态扩容。而grow()方法则会调用数组辅助类Arrays.copyof()方法按指定长度创建新的数组,并把原数组拷贝拷贝过去,从而实现动态扩容。有关数组及辅助类知识请参看作者相关文章Java基础之数组。
以上是ArrayList动态扩容的代码,而Vector同样是用动态数组实现的,它的动态机制和ArrayList大同小异,这里就不再深究代码,不同的是Vector的方法基本上是用同步修饰符synchronized修饰,从而保证了线程安全,而Stack直接继承了Vector,因而也是线程安全的,只不过追加了先进后出的逻辑。
在Java后续的新版本中,因为性能问题,都不再推荐使用Vector和Stack,那如果只用ArrayList,怎么保证特定场景下的线程安全需求呢?这个方法有很多种:方法一,可参考Vector代码,使用synchronized关键字来保证;方法二,可以使用集合辅助类Collections.synchronizedList() 方法来得到一个线程安全的集合,例如:
List<E> myArrayList = Collections.synchronizedList(new ArrayList<E>());
方法三可以使用 CopyOnWriteArrayList 类替代ArrayList,该类是 Java 并发包(java.util.concurrent)中提供的一个线程安全的列表实现。它在读取操作上没有锁,并且支持在迭代期间进行修改操作,而不会抛出 ConcurrentModificationException 异常。示例如下:
CopyOnWriteArrayList<E> myArrayList = new CopyOnWriteArrayList<E>();
最后的一个方法,您还可以使用显式锁(如 ReentrantLock)来手动实现对 ArrayList 的同步, 它是java.util.concurrent.locks 包中提供的, 相关代码可以自己去尝试实现。
LinkedList
LinkedList虽然也实现了List接口,但它不是List接口的纯正血脉,它还间接实现了Queue接口,它的继承关系较为复杂;此外,它的底层结构是按双向链表结构实现的,因而特性和前面三个相反,在插入和删除操作上更高效,但在随机访问时效率较低,因为查询需要遍历链表;因而在List接口家族中,LinkedList算是另类。我们先看它的完整继承关系,如下图;
从上图的继承关系可以看出,LinkedList如前文所说,既实现了List接口,同时也实现了Queue接口的子接口双端队列接口Deque,因而LinkedList可用于双端队列的实现。那怎么确定LinkedList底层是双向链表结构呢?我们可以查看它的源代码定义,如下:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;
/**
* Pointer to first node.
*/
transient Node<E> first;
/**
* Pointer to last node.
*/
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;
}
}
......
}
从上述代码可以看出,LinkedList底层是双向链表结构,每个元素都是一个嵌套类Node结点,里面包括本身元素的值和指向前后元素的指针。LinkedList和ArrayList一样是线程非安全的,它的方法上没有使用synchronized修饰,要保证它的线程安全,可以参考前述内容ArrayList使用的方式。
List接口及其实现类的介绍就到这里,总之,我们需要深刻理解其各实现类的异同,其各自的特性,正是它们底层结构和实现方式的外在体现。
【注意事项】
1.LinkedList的归属:在有的文章中,会把LinkedList归属到Queue接口下讲解,这个无可厚非。只是作者认为Queue的显著特性是先进先出,而LinkedList虽然能用来实现队列的这一特性,但先进先出并不是LinkedList本身具有的特性,因而还是把它归类为List接口下讲解。
2.接口特性规范的说明:在文章中列出的三个接口List、Set和Queue的特征,例如元素是否可为空,元素是否有序,这些在接口的实现类中,并不会严格遵循,只能作为接口设计的初衷和一般规范;另外像LinkedList这样既实现了List接口,又实现了Queue接口,可能无法同时满足两者的规范特性。
码农爱刷题
为计算机编程爱好者和从业人士提供技术总结和分享 !为前行者蓄力,为后来者探路!
10篇原创内容
公众号
如有错误或不足,欢迎指正!