PS:如果事先知道要存储的元素个数,那么最好使用带
int
参数的构造函数,因为这样能够减少数组扩容的开销
ArrayList是使用数组实现的列表,具有顺序性。ArrayList内部使用了transient Object[] elementData;
来存储列表中的元素(即数组的缓存区);ArrayList的容量就是该缓存区的数组长度
private static final int DEFAULT_CAPACITY = 10
默认容量,只有在add的时候判断elementData是空数组的时候,就会初始化这个默认容量的数组赋值给elementDatamodCount
这个属性是用来记录List列表的表结构发生变化的次数(要对list的结构发生变化的操作,都会使得modCount的值进行加一;结构性操作指对列表长度进行修改的,即add/remove操作);这个属性比较重要modCount其实是fail-fast 机制的实现,即当某一个线程A通过
Iterator
去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException
异常,产生fail-fast事件
//无参构造方法
//无参的构造函数得到的是一个空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
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);
}
}
int
大小的数组IllegalArgumentException
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[]
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
构造指定元素的列表,当指定的元素为空时返回的是一个空的数组;如果元素不为空,那么直接得到一个指定元素的数组
这里有个官方的注释,这里的作用是:如果得到的element数组类型不是Obejct类型的数组,那么就要将其转换成Object类型的数组,至于这样做的原因是:
如果没有这个Object类型转换的情况会是怎么样的,我们模拟一下
List<Object> list = new ArrayList<Object>(Arrays.asList("yes", "no")); list.set(0, new Object());
注意:此处模拟指没有类型转换的情况
执行上面的代码,会调用指定元素的构造方法(推荐debug看)
- 当执行
elementData = c.toArray();
之后,elementData会是 String[] 类型的数组,而不是 Object[] 类型的- 那么执行完该构造方法之后,上面代码就会等价于
Object[] list = new String[]{"yes","no"}; list[0]=new Object();
- 然后当你再执行
list.set(0, new Object());
的时候,就会抛出ArrayStoreException
异常了根本原因是
toArray()
方法中调用的是copyOf
方法,该方法实际使用的是泛型,虽然返回的是 Object[] 类型的数组,但是实际的类型已经声明成 String[] 了
了解这两个方法对看下面几个方法很有帮助
/** * @param src 源数组 * @param srcPos 源数组中的起始位置 * @param dest 目标数组 * @param destPos 指定目标数据中的起始位置 * @param length 要复制的数组元素的数量. */
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
/** * @param original 要复制的数组 * @param newLength 要返回的副本的长度 * @param newType 要返回的副本的类型 */
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
//执行顺序1.
//size默认值是0
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
//2.其实是执行3和4的方法
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//3.
/** * 这个方法是计算list的容量的: * 上面add方法调用的时候会传入(size+1)到这个计算的方法中 * 如果当前的数组是空数组,那么就比较(size+1)与默认的容量(默认是10),取较大的值返回. * 如果不是空数组,那么直接返回(size+1) */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//默认容量DEFAULT_CAPACITY是10
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
//4.
/** * 因为是add操作,对列表结构有改动,需要modCount++ * 如果传入的(size+1)值大于当前数组的长度的时候,才对此数组进行扩容的操作 */
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//5.如果是超过当前数组长度才需要进行数组扩容的操作
/** * 如果新数组大小还是小于传入的容量的话,那就直接取传入的容量作为新数组的长度 * 如果新数组的长度要比最大定义的数组长的话 * (最大的数组长度是Integer.MAX_VALUE-8) * 那么就直接使用Integer.MAX_VALUE作为数组的长度. * 然后就将数组的元素拷贝到一个新的数组中 */
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//右移1位,此处得到新容量是旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
addAll(Collection extends E> c)
方法和普通add方法类似,主要使用了System.arraycopy
方法来拷贝指定元素
//插入到index的下标的位置,然后返回旧的值
public E set(int index, E element) {
//判断传入的下标是否越界
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
public E get(int index) {
//判断传入的下标是否越界
rangeCheck(index);
return elementData(index);
}
public E remove(int index) {
//判断传入的下标是否越界
rangeCheck(index);
//删除操作修改了列表结构
modCount++;
E oldValue = elementData(index);
//需要进行移动的元素个数
int numMoved = size - index - 1;
if (numMoved > 0)
//移动数组的元素
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//更新列表的元素个数,清除最后一个元素,让GC回收
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
public boolean remove(Object o) {
//移除null值元素
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
//for循环遍历列表,根据对象的equals方法来移除对应元素
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
//此方法是移除列表中的元素,根据下标志移除
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
public boolean removeAll(Collection<?> c) {
//如果参数为null,那么抛出异常
Objects.requireNonNull(c);
return batchRemove(c, false);
}
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
//注意此处的complement是false
//所以就是将elementData中不存在c中的元素留下
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
//这里的判断的原因是因为 contains 方法可能会抛出异常
//如果抛出异常的情况下,就需要对 elementData 进行处理
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
//这里很简单,清除元素,修改 modCount 的值,然后返回true
if (w != size) {
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
public Iterator<E> iterator() {
return new Itr();
}
ArrayList使用的迭代器是其内部类Itr
其内部类还有
ListItr
也是迭代器,只是这个迭代器可以在任意方向遍历列表,即可以向前/向后遍历。可以通过public ListIterator
方法得到该迭代器listIterator()
此处专注于迭代器Itr
,先来查看该类的属性
int cursor
:要返回的下一个元素的索引int lastRet = -1
:返回的最后一个元素的索引,如果没有返回-1int expectedModCount = modCount
:结构修改次数在遍历元素前列表都会比较结构修改次数,如果结构修改次数不对应,那么会抛出ConcurrentModificationException
异常;所以在多线程对同个列表进行迭代和增删操作的时候,很大可能会抛出异常
//是否还有下一个可遍历的元素
public boolean hasNext() {
return cursor != size;
}
public E next() {
checkForComodification();
int i = cursor;
//这里都是判断下标是否越界
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
//游标+1
cursor = i + 1;
return (E) elementData[lastRet = i];
}
//在获取元素之前,需要先判断列表结构是否被修改过
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
public void remove() {
//这个地方就是为什么需要先执行 next 方法后才能调用 remove 方法的原因
if (lastRet < 0)
throw new IllegalStateException();
//在移除元素之前,需要先判断列表结构是否被修改过
checkForComodification();
try {
//调用arrayList的remove方法
ArrayList.this.remove(lastRet);
//重置一下游标,因为 next 操作里面将游标+1了
cursor = lastRet;
lastRet = -1;
//更新结构修改次数,上面调用remove方法导致modCount增加了
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
/** * List的遍历方式 */
public void cycleList() {
Integer[] ints = {1,2,3,4,5,6,7,8,9};
// Arrays.asList(ints) 将数组转成List
List<Integer> lists = Arrays.asList(ints);
/** * 1.for循环遍历 * * foreach循环(增强for循环) */
for (int i = 0;i < lists.size();i++) {
System.out.println(lists.get(i));
}
for (Integer i : lists) {
System.out.println(i);
}
/** * 2.迭代器遍历 */
Iterator<Integer> iterator = lists.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
/** * 3.JDK8的stream流方式 */
lists.stream().forEach(System.out::println);
}
注意:
for循环遍历元素的时候,如果需要在for循环中删除元素,此时需要注意:
实现思路:
实现思路:跟遍历去重方式思路基本上一致,只是对于对象的遍历判断,是需要重写equals和hasCode方法来比较对象是否重复
list.stream().distinct().forEach(System.out::println);
注意: 如果列表中是对象的话,一定要重写对象的equals()
和hasCode()
方法来定义对象是否重复的规则
SubList方法能够返回指定下标区间的父List的视图、视图、视图
//不包含toIndex下标的元素,即左闭右开
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
要注意的地方就是:调用这个方法并不是返回一个全新的列表,而是返回一个视图。所以在操作这个方法得到的视图会影响原列表的内容。简单来说就是subList后得到的列表跟原列表是同一个对象
size >> 1
**的方式(即折半法查找)来查找index,从而提高查询的效率transient Node first
存放头结点transient Node 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;
}
}
public LinkedList() {
}
//调用addAll方法,这个下面详说
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
LinkedList的增加方法的核心是linkBefore(E e, Node
、linkFirst(E e)
、linkLast(E e)
和node(int index)
//添加元素到链表末尾
//将元素包装成Node节点,然后就是链表的操作了
// 如果尾节点是空的,那么将该节点作为头和尾节点
// 如果尾节点非空,那么将该节点作为尾节点,并且加入到链表中
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
//添加元素到链表头
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
//链表操作,将 e 插入到 succ 前面
void linkBefore(E e, Node<E> succ) {
// succ 不能为null
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
//即succ是头节点的情况
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
/** * 采用折半法得到index所在的位置(左/右区间) * 然后采用for循环遍历的方式得到index位置上的元素 */
Node<E> node(int index) {
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
//注意:这里是从后往前遍历
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
add(E e)
public boolean add(E e) {
//插入链表的末尾
linkLast(e);
return true;
}
add(int index, E element)
public void add(int index, E element) {
//检查下标是否越界
checkPositionIndex(index);
if (index == size)
//添加到链表最后,即调用上面的add(E e)方法
linkLast(element);
else
//插入node(index)节点之前
linkBefore(element, node(index));
}
//检查下标是否越界和index是否小于0
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
addAll
方法,带参构造函数中也调用此方法public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
//检查下标是否越界
checkPositionIndex(index);
//得到指定插入的元素
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
//获取插入位置的前驱和后继节点
Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
//for循环插入指定元素,并且设置好上面的前驱和后继节点
for (Object o : a) {
E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;
}
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
//返回头节点元素内容
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
//返回尾节点元素内容
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
//获取指定下标的元素
public E get(int index) {
checkElementIndex(index);
//折半for循环查找指定下标的元素
return node(index).item;
}
//检查下标是否越界和index是否小于0
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
LinkedList的删除方法的核心是unlink(Node
、unlinkFirst(Node
和unlinkLast(Node
//要求参数 x 不能为null
E unlink(Node<E> x) {
//得到需要删除节点的内容、前驱节点和后继节点
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
//该节点是头结点的情况
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
//该节点是尾节点的情况
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
private E unlinkFirst(Node<E> f) {
// 要求f是头节点并且不为null
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
//链表元素只有一个的情况
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
private E unlinkLast(Node<E> l) {
// 要求l是尾节点并且不为null
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
if (prev == null)
//链表元素只有一个的情况
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
remove()
移除第一个节点,即当前头节点remove(int index)
移除下标是index的节点,先检查下标是否越界,再利用node(int index)
方法得到节点,最后用unlink(Node x)
移除removeFirst()
和removeLast()
方法。根据名称可知作用,如果头结点/尾节点为null的情况,即链表为空的情况会抛出NoSuchElementException
,然后各自调用unlinkFirst
和unlinkLast
删除节点remove(Object o)
//删除指定元素的节点,利用for循环便利链表,找到第一个配对的元素删除
//注意:如果链表有多个相同的元素,该方法只会删除第一个
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
public E set(int index, E element) {
//检查下标是否越界或者index是否小于0
checkElementIndex(index);
//找到index下标的元素,更新该item内容后返回旧值
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
NoSuchElementException
peek和pop方法的区别是:peek方法和pop方法都是返回头节点的元素,但是peek方法不会删除头结点只返回null,而pop方法会删除头节点(相当于出栈)
synchronized
同步锁)总体来说:
注意:上面是总体而已,某些情况下不成立
CopyOnWriteArrayList
是JUC包提供的线程安全的ArrayList,底层通过复制数组的方式来实现;CopyOnWriteArrayList
在迭代器遍历的使用不会抛出ConcurrentModificationException
异常(即没有快速失败机制),并且迭代器遍历的时候就不用额外加锁
copy-on-write是指写时复制;如果有多个调用者同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变
优点是如果调用者没有修改该资源,就不会有副本(private copy)被建立,因此多个调用者只是读取操作时可以共享同一份资源
final transient ReentrantLock lock = new ReentrantLock()
该列表通过 ReentrantLock 来实现加锁操作private transient volatile Object[] array
CowArrayList用来存储数据的数组public boolean add(E e) {
final ReentrantLock lock = this.lock;
//加锁操作
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//复制一个新的数组,新增操作在新数组上完成
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
//将新数组设置到 array 上
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
CowArrayList通过复制一个新数组,并且增加操作在新数组上完成,最后将array指向到新数组中;跟ArrayList的区别就是:
CowArrayList的set()
、remove()
操作都类似add()
操作,都是在新数组上完成操作后将array指向新数组
因为CopyOnWriteArrayList
在迭代器遍历的使用不会抛出ConcurrentModificationException
异常,并且迭代器遍历的时候就不用额外加锁;这个原因就在于CopyOnWriteArrayList
在使用迭代器的时候,保存了一份原数组的副本
static final class COWIterator<E> implements ListIterator<E> {
//原数组的副本
private final Object[] snapshot;
//游标
private int cursor;
//CopyOnWriteArrayList调用 iterator() 执行的构造函数
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
public boolean hasPrevious() {
return cursor > 0;
}
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
public E previous() {
if (! hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor-1;
}
//CopyOnWriteArrayList的迭代器不支持一下方法
public void remove() {
throw new UnsupportedOperationException();
}
public void set(E e) {
throw new UnsupportedOperationException();
}
public void add(E e) {
throw new UnsupportedOperationException();
}
}
显而易见:
在调用迭代器的方法时,迭代器会保存一份原数组的副本,即snapshot
。该迭代器的所有操作都是对原数组进行操作,因此也就没有线程间的读写问题了,也就不需要快速失败机制和加锁操作了,所以CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性
add()、set()、remove()
的话,那是比较耗费内存的(因为都需要复制一个新数组进行增删改)