java集合-集合List(二)
ArrayList
ArrayList 是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。
ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
ArrayList 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。稍后,我们会比较List的“快速随机访问”和“通过Iterator迭代器访问”的效率。
ArrayList 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。
ArrayList源码分析
ArrayList包含了两个重要的对象:elementData 和 size。
- elementData 是”Object[]类型的数组”,它保存了添加到ArrayList中的元素。实际上,elementData是个动态数组,我们能通过构造函数 ArrayList(int initialCapacity)来执行它的初始容量为initialCapacity;如果通过不含参数的构造函数ArrayList()来创建ArrayList,则elementData的容量默认是10。elementData数组的大小会根据ArrayList容量的增长而动态的增长(每次动态扩充50%)。
- size 则是动态数组的实际大小。
ArrayList初始化
jdk1.8与1.6的初始化微微有点不同,1.6初始化时以来就创建一个elementData的容量默认是10数组。而1.8则创建一个空数组出来。
1.8里面的创建
private static final Object[] 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() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
1.6里面的创建
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
// 新建一个数组
this.elementData = new Object[initialCapacity];
}
// ArrayList构造函数。默认容量是10。
public ArrayList() {
this(10);
}
ArrayList动态扩容(add的操作)
arr.add(object);
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
//将元素添加到数组中
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//数组是否为{},如果是空数组,则初始大小为10的数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//计算动态扩容的大小。old+old/2
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);
}
位移运算
右移一位=除2,右移两位=除4;左移=*2;
oldCapacity >> 1
System.arraycopy数组拷贝
Arrays.copyOf的底层实现就是System.arraycopy(t1, 0, t2, 0, 1);
参数(源数组A,要拷贝A数组的起始位置,目标数组B,将A数组拷贝到B数组的起始位置,拷贝的长度(就是拷贝多少个A数组元素))。
System.arraycopy(t1, 0, t2, 2, 10);说明:t2将拷贝t1数组,从t1的第0个元素开始拷贝10个元素,存放的时候从t2的第2个位置开始存放。
注意:System.arraycopy这个是浅拷贝(浅拷贝是指拷贝源数据的引用,深拷贝是拷贝源数据的值)。
class ArrayCoyeTest {
String name;
ArrayCoyeTest(String name) {
this.name = name;
}
@Override
public String toString() {
return "Te{" +
"name='" + name + '\'' +
'}';
}
}
public class ArrayListDome {
public static void main(String args[]) {
int a[] = new int[10];
a[0]=1;
a[1]=2;
a[1]=4;
int b[] = new int[10];
System.arraycopy(a, 0, b, 0,10);
a[1]=3;
System.out.println("b=="+b[1]);//打印为1,不为3,因为数值不存在浅深拷贝的问题,只有引用才涉及到。
ArrayCoyeTest[] t1 = new ArrayCoyeTest[1];
t1[0] = new ArrayCoyeTest("a");//new了一个新对象
ArrayCoyeTest[] t2 = new ArrayCoyeTest[1];
System.arraycopy(t1, 0, t2, 0, 1);//t2拷贝了t1的引用。
t1[0].name = "2";//t1的值改变了
System.out.println("t2:"+t2[0].name);//t2的值也跟着改变了
}
}
数组拷贝:
方法一:System.arraycopy;(浅拷贝)
方法二:ArrayList newArray = oldArray.clone();(浅拷贝)
方法三:ArrayList newArray = new ArrayList(oldArray);底层实现就是第一种方法System.arraycopy
方法四:Collection中的copy方法。(深拷贝)
浅拷贝不代表就是线程不安全的,看了篇帖子被带沟里面去了,看到上面的示例t1[0].name=”2”是改变了,但是它并不代表是线程不安全的。
线程不安全说的,如果多个线程来,多个线程的t1[0].name=”2”也是被改变的啊。所以网上所说的这里是线程不安全的不对。
说一下Collection中深拷贝是怎么拷贝的?
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
//遍历老数组,然后将老数组中的值添加到新数组中。
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
删除操作
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);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
解说:比如elementData的大小为10,现在index为2,表示移除第二个元素;我们来计算一下
int numMoved = size - index - 1;
numMoved=10-2-1=7;
System.arraycopy(elementData, 3, elementData,2,7);
拷贝elementData,从第3个元素开始,存放到elementData的第2的位置总的拷贝7个元素。
可以看到删除arraylist效率是非常低的,因为从删除的那个元素之后的所有元素都要向前移动一位。
modCount变量的用途
modCount的定义是在AbstractList类里面定义的。
protected transient int modCount = 0;
执行add操作时:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
执行移除时的操作
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);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
等等,可以看到对elementData操作的过程中,不管是添加还是删除,修改都会执行modCount++;
那么modCount++;这个是干吗的呢?
调用arrayList.iterator();迭代器的时候使用
fail-fast
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
//这里把modCount赋值给expectedModCount
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
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();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
//判断他们两个的值是否相等
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
关键就在checkForComodification这里,expectedModCount的值是modCount赋值给他的,为什么还要判断他们相等呢?
因为modCount值是在外面++的,所以如果在多线程下,A线程在调用arrayList迭代器,在遍历数组,这个时候B线程往arrayList里面添加元素或者删除元素了modCount++了,modCount != expectedModCount成立,抛出异常,A线程遍历失败。
在使用iterator便利数组过程中,如果另外一个线程对list进行了add操作或remove操作就会抛出异常java.util.ConcurrentModificationException!产生fail-fast事件!。
解决fail-fast事件
- 在删除的时候使用Iterator里面的remove删除方法,而不要使用List里面的remove方法。(详情查看源码哦)
- Iterator里面只针对了remove做了fail-fast事件,并没有对add做同样的操作,如果遍历的时候还是会抛异常,可以使用ListIterator迭代器来做,ListIterator是专门正对List集合的一个迭代器,加强版的Iterator。
- 可以使用CopyWriteArrayList来解决。
ArrayList之线程不安全
看完ArrayList源码后,整个实现中充满了线程不安全的操作。
第一:size++;成员变量,不用说也知道,如果多个线程同时操作同一个ArrayList对象,那么size++值则会不确定。
第二: elementData[size++] = e;原因同上,假如线程1和线程2同时对size++,结果会变成线程1和线程2的size的值一样。
操作后就会变成下面的情况:
elementData[0]=“a”;//线程1
elementData[0]=“b”;//线程2
希望的操作为
elementData[0]=“a”;//线程1
elementData[1]=“b”;//线程2
第三:modCount++;同上,成员变量多线程下不安全。
ArrayList总结
- ArrayList每个元素中都有索引,他的序列号就是索引如:arrayList.get(3);获取第3个元素。查询较快
- ArrayList在删除时需要向前移动删除元素之后的所有元素。不适合删除较多的操作
- ArrayList所有操作几乎都是线程不安全的。所以没发在多线程中使用,当然如果ArrayList只在局部中使用是可以的,如果作为全局变量来使用的话,多线程中是不安全的。类的全局变量中尽量不要使用ArrayList,以防止其他人使用多线程调用该类时造成多线程不安全。
线程安全的CopyOnWriteArrayList
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
CopyOnWriteArrayList只实现了List接口,并没有继承extends AbstractList,意味着需要自己实现迭代器
看下面代码
List copyArrayList=new CopyOnWriteArrayList();
copyArrayList.add(“3”);
copyArrayList.remove(2);
copyArrayList.iterator();
初始化
//2.这里可以看到CopyOnWriteArrayList的安全性是使用Lock来实现的
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
//1.实例化时默认数组大小为1;
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
1.实例化时默认数组大小为1;
2.这里可以看到CopyOnWriteArrayList的安全性是使用Lock来实现。
添加
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;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
当每次添加一个元素时才动态扩展数组大小为1.就是说来一个数组扩展+1;
删除
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
//1.定义一个新数组长度为老的数组长度-1
Object[] newElements = new Object[len - 1];
//2.把删除元素的前面的数组元素复制到新数组里面
System.arraycopy(elements, 0, newElements, 0, index);
//3.把删除元素后面的数组元素复制到新数组里面
System.arraycopy(elements, index + 1, newElements, index,numMoved);
//4.返回一个新数组
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
遍历
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
public boolean hasPrevious() {
return cursor > 0;
}
//读取下一个元素,其实就是一个迭代,里面每次cursor++,和for循环差不多
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
@SuppressWarnings("unchecked")
public E previous() {
if (! hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor-1;
}
//迭代的时候不允许remove操作,直接抛异常
public void remove() {
throw new UnsupportedOperationException();
}
//霸气,直接抛异常
public void set(E e) {
throw new UnsupportedOperationException();
}
public void add(E e) {
throw new UnsupportedOperationException();
}
@Override
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
Object[] elements = snapshot;
final int size = elements.length;
for (int i = cursor; i < size; i++) {
@SuppressWarnings("unchecked") E e = (E) elements[i];
action.accept(e);
}
cursor = size;
}
}
解说:霸气,只能用霸气来说明。这里定义了一个内部类COWIterator implements ListIterator 实现了ListIterator。
在遍历数组的时候,不能add,remove,如果外面的某个线程操作了add,直接抛异常。所以说霸气,在迭代遍历的时候禁止做一起事情,如果做了直接抛异常。
Vector介绍
Vector 是矢量队列,它是JDK1.0版本添加的类。继承于AbstractList,实现了List, RandomAccess, Cloneable这些接口。
Vector 继承了AbstractList,实现了List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功能。
Vector 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在Vector中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问(根据下标访问arr[i])。
Vector 实现了Cloneable接口,即实现clone()函数。它能被克隆。
Vector 没有实现java.io.Serializable接口,所以Vector不能被序列化。
源码分析
Vector基本和ArrayList差不多,不同的地方在于每一步操作都加锁了。
add操作
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
Vector和ArrayList不同点在于:动态扩容的时候可以指定动态扩容的大小没有指定则动态扩容原数组大小的一倍。
移除操作
public synchronized boolean removeElement(Object obj) {
modCount++;
int i = indexOf(obj);
if (i >= 0) {
removeElementAt(i);
return true;
}
return false;
}
看到了吗,每一步都是用synchronized加锁,内部操作基本和ArrayList差不多。这也是为什么Vector性能比ArrayList差的原因了。当然他也是多线程安全的。
LinkedList
LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
LinkedList 实现 List 接口,能对它进行队列操作。
LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
LinkedList 是非同步的。
添加add方法
测试代码:
public static void main(String args[]) {
LinkedList<String> link = new LinkedList<String>();
link.add("1");
link.add("2");
link.remove(1);
link.iterator();
link.iterator();
}
transient Node<E> first;
transient Node<E> last;
public LinkedList() {
}
public boolean add(E e) {
linkLast(e);
return true;
}
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 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 E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
Node<E> node(int index) {
// assert isElementIndex(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里面有一个size++了嘛,就是记录每个Node的序列号的(因为是顺序添加node,所以size++就刚好对上了)。
这个index与size/2做对比如果小于size/2则从开始查找,如果大于size/2则从尾巴查找,这样平均可以减少一半的时间复杂度。
E unlink(Node<E> x) {
// assert x != null;
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;
}
第一步:根据size索引找到要删除的node2
第二步:定义两个局部变量:prev,next;一个等于node2的前节点,一个等于node2的后续节点
第三步:将前节点的下一个节点指向next,将next的上一个节点指向prev,这样就绕过了node2.这个时候x.item=null删除x节点即可。
总结
- ArrayList:随访访问,查询较快,插入与删除操作较慢(需要复制数组,重新创建数组); ArrayList支持序列化。支持Iterator和listIterator遍历
- Vector:与ArrayList同样是使用数组实现的,所以都有较快的访问,但是Vector每个操作都加了synchronized,所以多线程下为安全的,但同样性能也就没有ArrayList效率高。不支持序列化。VectorIterator和listIterator遍历,并且还支持通过Enumeration去遍历,而ArrayList则不支持。
- LinkedList:一个双向链表,它也可以被当作堆栈、队列或双端队列进行操作快速插入,删除元素。
- Stack 是栈,它继承于Vector。它的特性是:先进后出(FILO, First In Last Out)。
网上的帖子不一定可靠,有些帖子也是些小菜学到了一点知识就写点笔记,但是某些地方理解的不到位,所描述的和他所理解的不一定正确。