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.