java.uti.List框架:
ArrayList
1 | public class ArrayList<E> extends AbstractList<E> |
介绍
- 继承自
AbstractList。实现了List。实现了RandomAccess标记接口表示可以随机访问。实现了Cloneable标记接口但是是浅拷贝。实现了Seriable可以进行序列化。 ArrayList是一个基于数组实现的可变数组,数组长度取决于使用的构造函数和传的初始参数,在添加元素过程中可以动态以1.5倍扩容。数组中的元素是有序的,元素可以是null。因此随机访问元素的时间复杂度为O(1),但是插入和删除元素的时间复杂度为O(n)刚好和LinkedList相反(用链表实现的)。- 非线程安全,如果需要线程安全需要使用
Vector、Collections.synchronizedList(new ArrayList())、CopyOnWriteArrayList - 数组初始容量(
DEFAULT_CAPACITY)是10
部分源码分析
ArrayList(int initialCapacity)
如果设置的初始容量为0,数组指向长度为0的数组。
1 | public ArrayList(int initialCapacity) { |
ArrayList()
如果没有指定数组长度,将默认指向另一个(区别于设置的初始长度为0)长度为0的数组。
这里和上面形参为0指向的长度为0的数组不一样,目的是可以以此为标识,在首次添加元素时,执行不同的的扩容操作。无参构造函数在首次添加元素时,数组会扩容成10(0 -> 10 -> 20),而形参为0的构造函数在首次添加元素时,只会在0的基础上1.5倍扩容(0 -> 1 -> 2 -> 3 -> 4 -> 6)。至于这么做的原因,暂时没想到性能上会带来哪些提升,唯一能想到的就是为了统一??为了区分不同的构造函数??TODO。。。 下面是Java Doc:
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.
1 | public ArrayList() { |
ArrayList(Collection<? extends E> c)
主要是第二个if,其他两个构造函数逻辑比较简单。
1 | /** |
add(E e)
添加元素,主要涉及了一些扩容的逻辑
1 | /** |
add(int index, E element)
在指定下标添加元素,主要是理解这个函数的使用System.arraycopy(elementData, index, elementData, index + 1, size - index)下同↓
1 | public void add(int index, E element) { |
remove(int index)
删除指定位置元素
1 | public E remove(int index) { |
trimToSize()
扩容的反义词:缩容。重新创建一个数组,长度为ArryList中的size。目的是为了防止进行多次remove操作以后,数组长度特别大,但是仅有几个元素导致空间浪费。可以调用此方法重新修剪数组长度。
1 | public void trimToSize() { |
boolean contains(Object o)
是否包含指定元素o
1 | public boolean contains(Object o) { |
int indexOf(Object o)
找到指定元素o第一次出现的下标
1 | public int indexOf(Object o) { |
int lastIndexOf(Object o)
找到指定元素o最后出现的下标
1 | public int lastIndexOf(Object o) { |
LinkedList
介绍
- 继承自
AbstractSequentialList抽象类(只是非常简单地实现了List相关方法) - 实现了
List接口、Deque接口(继承自Queue接口,说明LinkedList间接实现了Queue接口)、有关于栈Stack的操作(push、pop)。可以当作一个双向链表、双端队列、FIFO队列、栈。并且可以添加null,这点区别于ArrayDeque。 - 非线程安全,如果需要线程安全需要用到
List list = Collections.synchronizedList(new LinkedList(...));
部分源码分析(翻译:))
public LinkedList(Collection<? extends E> c)
接收一个Collection类型的变量构造一个LinkedList
1 | public LinkedList(Collection<? extends E> c) { |
public void clear()
清除链表中的元素,额外置空操作的意思是将节点之间的链接都断开,虽然不是必须的,但是有助于分代GC
1 | public void clear() { |
其它
实现了队列(Queue)/双端队列(Deque)接口
- 队列接口主要方法
peek();element()获取队头元素,如果队列为空,则返回null/则抛异常offer(E e)队尾入队列
- 双端队列接口主要方法
offerFirst(E e);offerLast(E e)从队头/对尾入队列peekFirst();peekLast()获取队头/队尾元素pollFirst();pollLast()获取并移除队头/队尾元素
包含了栈(Stack)操作
push(E e)入栈pop()出栈
JavaDoc建议用
ArrayQueue作为队列或者栈的接口实现,因为它有着比LinkedList更好的性能。 不再建议使用Stack stack = new Stack();
Resizable-array implementation of the Deque interface. Array deques have no capacity restrictions; they grow as necessary to support usage. They are not thread-safe; in the absence of external synchronization, they do not support concurrent access by multiple threads. Null elements are prohibited. This class is likely to be faster than Stack when used as a stack, and faster than LinkedList when used as a queue.
总结
LinkedList适合用来做链表/双向链表、双端队列的实现。当用到FIFO队列或者栈时,建议使用ArrayDeque作为实现。比如:Deque<Integer> stack = new ArrayDeque<Integer>();Queue<Integer> queue = new ArrayDeque<Integer>();
Q&A
ArrayList和LinkedList本质区别就是数组和链表的区别
ArrayList是如何扩容的?
- 当添加元素之前需要判断当前数组的容量和需要的容量的差异,当需要的容量大于数组的当前容量,就会重新申请1.5倍旧数组大小的新数组,然后将旧数组元素拷贝到新数组。注意的是扩容过程中可能会由于需要的空间大小超出最大值(
Integer.MAX_VALUE)而出现OOM。
ArrayList和LinkedList的区别?
ArrayList用数组实现,数组占据了一段连续的内存空间,支持通过首地址+偏移量进行时间复杂度为O(1)的随机访问。但是随机插入/删除元素的时间复杂度为O(n)。LinkedList用链表实现,链表中的每个节点在物理内存中并不相邻,所以不支持随机访问。随机插入/删除元素的时间复杂度为O(1)。
这里要区分一下随机访问和顺序访问,维基百科定义:
- 随机访问/存取(Random access)
In computer science, random access (more precisely and more generally called direct access) is the ability to access an arbitrary element of a sequence in equal time or any datum from a population of addressable elements roughly as easily and efficiently as any other, no matter how many elements may be in the set.
总结就是,访问任意一个元素所用的时间 time(注意是时间,不是时间复杂度),都是一样的。
- 顺序访问(Sequential access)
In computer science, sequential access means that a group of elements (such as data in a memory array or a disk file or on magnetic tape data storage) is accessed in a predetermined, ordered sequence.
总结就是,如果要访问一个元素,就需要从前向后依次访问过去,直到目标元素,明显,访问任意一个元素所用时间是不一样的。
fail-fast机制?
当使用List的迭代器Iterator的进行操作时,如果List的结构被其它线程修改了(比如添加/删除了一个元素),当前线程在迭代过程中会抛出ConcurrentModificationException。
在生成迭代器的时候会拷贝一份modcount变量到expectedModCount(modcount变量用来记录List被修改的次数),每次迭代时会判断expectedModCount是否等于modcount变量,如果不相等就说明其它线程对List做了修改,此时会抛出异常而不是继续冒险以一种不确定的行为执行下去。
if the list is structurally modified at any time after the iterator is created, in any way except through the iterator’s own remove or add methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.
fail-fast机制是用来检测BUG的,不能依赖这个异常来保证程序的正确性。抛出这个异常说明代码写的有问题。
Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.