文章目录
List-有序可重复的集合接口
List
接口继承自Collection
接口,集合内元素有序、可重复。
ArrayList
,LinkedList
,Vrctor
都是其实现类,区别在于内部的具体实现上。不同的实现开销不一样,适用的场景也不一样。
抽象实现类-AbstractList
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
// ... ...
}
AbstractList
是List
的抽象类,实现了部分方法,定义了部分方法。
定义的抽象方法:
Iterator iterator()//迭代器
int size()//返回集合长度
boolean add(E e)
… …
!add方法的方法体里面只抛出了异常,具体的实现需要在继承的子类中重写。
已经实现的方法:
boolean isEmpty() //是否为空
boolean contains(Object o);//包含
Object[] toArray();//转数组
String toString()
… …
数组结构实现类-ArrayList
源码👇
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final int DEFAULT_CAPACITY = 10;//默认容量
/*
空实例,在第一次添加元素后将值赋值给elementData
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/*
长度为0 的空实例,无参构造的时候会将这个空数组赋值给 elementData数组。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//数组缓存区
transient Object[] elementData;
//size表示集合长度 = 数组已插入元素的个数
private int size;
// ... ... 具体方法省略 ... ...
}
ArrayList常用方法代码分析
add
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保内部数组容量可以增加一个元素
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
/*
计算容量
1、空集合,elementData的长度就是0,给一个默认长度10;
2、不是空集合,返回最小容量。
这个最小容量是指集合需要容纳元素的数量,注意是需要!!比如集合单个添加元素,
最小容量就是当前的size+1;批量添加4元素,最小容量就是当前size+4.
*/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
/*
minCapacity = 集合需要容纳的最小数量
*/
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 最小容量小于数组容量,进行扩容。
//--------------------------------------------------------------------------
//解释:比如你批量插入15个元素,已经插入了10个元素,那么最小容量应该是10+15=25;
// 如果elementData.length = 10;显然需要新添加进去的15个元素根本没地方插入,
// 所以这里就需要扩容数组才行!!
//--------------------------------------------------------------------------
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {//minCapacity = 扩容的最小容量
// overflow-conscious code
int oldCapacity = elementData.length;//当前数组可容纳元素的长度
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);
}
👆
注意!
1、代码中申明的
size
是集合长度,就是已经插入到elementData
数组中的元素的个数。
2、理解长度和下标,长度是从1开始,下标是从0开始。
3、数组添加元素是添加在最后,所以它是有次序性的,size
可以看作是对elementData
数组添加元素操作的次序。
👆
例:
比如,
size=8
表示数组中已经存在了8个元素,这仅表示数组中已经有元素的个数。数组插入数据是有次序性的,那么第5个插入的元素其实就是数组下标为5-1的元素。
remove
public E remove(int index) {
rangeCheck(index);//检查下标是否在数组长度内,超出则抛出下标越界异常
modCount++;//记录结构性变化的次数
E oldValue = elementData(index);
/*
index = 下标,是从0开始的,操纵数组要用下标;
size = 缓存数组的容量也是集合长度,是从1 开始的;
numMoved(删除该元素后的剩余元素的个数) = 集合长度 - 要删除元素的下标 - 1;
⭐⭐这个地方有点绕脑子,比如String[] strArr = new String[]{"a","b","c","d","e","f"};
size=6,要删除元素c,元素c的index=2;所以删后边剩余的元素个数就是6-2-1;为什么要-1,
因为6是集合长度,index是下标,长度本来就比下标多了1,所以这里需要-1;
*/
int numMoved = size - index - 1;//剩余元素的个数
if (numMoved > 0)
//整个删除的逻辑就是将删除元素后边是剩余元素复制到原数组
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//最后一位重复,所以设为null
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
set
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;//直接操作数组替换原来的内容就行,逻辑很简单
return oldValue;
}
get
public E get(int index) {
rangeCheck(index);
//没什么复杂的逻辑,不再说明
return elementData(index);
}
ArrayList总结
源码解析只分析了增、删、改、查、功能的源码逻辑,其他的没操作无非是遍历集合然后进行操作没什么复杂的逻辑。从源码分析中可以看出,add
、remove
操纵涉及到数组的赋值,要是你向一个集合中插入一万条数据,就意味着需要频繁操作数组一万次,在庞大的插入量之下这个开销还是很大的。但是由于他可以直接操作下标,查询性能反而比较好一些。
纵观来看,整个类的代码逻辑中比较复杂的地方是数组的扩容,其他操作倒是没有特别复杂的地方。
比较重要的几点👇
- ⭐
ArrayList
内部是通过数组实现的,数组在内存中是一块连续的区域,数组有下标有数据,一般是使用下标来操作数据。所以说他的增删开销大也是跟他内部的数据存储结构有关系。 - ⭐由于其内部是基于数组实现的所以它的增删性能开销大,因为它每次添加元素或者删除元素都要重新计算下标、扩容。好处是它的查找元素的效率很高。
ArrayList
不是线程安全的容器,在两个线程同时修改的话就会出现线程安全问题。集合容器的工具类提供了线程安全ArrayList
-Collections.synchronizedList
。这个方法的具体实现是写了一个名字叫synchronizedList
的内部类,然后实现了List接口给方法都加上了synchronizedList
关键字。- 遍历集合优先使用迭代器或者增强for循环(内部也是迭代器实现),效率会比普通的的for循环高。
- ⭐ 初始大小是10
链表结构实现类-LinkedList
AbstractSequentialList
AbstractSequentialList
是LinkedList
的抽象类,实现了部分功能,也定义了部分功能。LinkedList
继承自该类。
public abstract class AbstractSequentialList<E> extends AbstractList<E> {
// ... 具体内容略 ...
// 看源码的话可以发现,其实内部功能都是通过迭代器来操作的
}
LinkedList源码👇
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
//size表示集合元素的数量,也表示元素插入的次序
transient int size = 0;
/*
第一个节点的地址
1、集合中的链表结构是双向链表,有前驱和后继。
2、链表的第一个元素是没有前驱的,有后续。(就像站队一样,排头前边没有人)
*/
transient Node<E> first;
/*
最后一个节点的地址
1、最后一个节点只有前驱没有后继。(就像站队一样,排尾后边没有人)
*/
transient Node<E> last;
// ... ...
}
LinkedList常用方法解析
add
源码👇
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
/*
1、链表添加元素是往最后一个元素后边添加。
2、添加元素之前new一个新的节点,这个节点的上一个节点(prev)地址就是最后一个节点的内存地址,
下一个节点的地址(next)默认为null。因为最后一个元素的后续没有元素,所以默认为null没有问题
*/
final Node<E> l = last;//注意这里,用了关键字final,初始化之后就不变了
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;//注意,将添加的值赋值给列最后后一个节点,每次new完新节点后last节点的值会变
if (l == null)
first = newNode;//注意,如果是第一次添加元素的话,第一个节点个最后一个节点的值是一样的
else
l.next = newNode;//注意这里的l,它是插入元素前的最后一个节点
size++;
modCount++;
}
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
//如果是链表的第一个元素,那么他的上个元素的地址肯定是null
//如果是链表的最后一个元素,那么他的下一个元素的地址肯定是null
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;//插入的元素
this.next = next;//下个元素地址
this.prev = prev;//上个元素地址
}
}
remove
源码👇
!LinkedList无参的删除方法默认删除的是第一个节点的元素
public E remove() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// assert f == first && 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;
}
set
源码👇
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
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;
}
}
get
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
LinkedList总结
源码只详看了添加、删除、修改、查找,其他的操作都是基于这些基本操纵实现的没有再详细的解析。
从源码来看,每次插入都是往最后边插入,不用关心具体的容量,没有太多的逻辑操纵,效率还是比较高的。但是修改查询的时候都是需要遍历,从头或者从尾部挨个取出元素,从前往后找的话是找它的next,就是后边的那个元素的地址,从后往前找的话是找它前边元素,prev的地址。
再细说一下链表根据下标取元素, 链表中定义了first、last、size,虽然它没有实际意义上的下标,但是它有插入动作的次序,里面定义的 size这个字段实际上就是它的插入次序。一共插入了5次,那么它就有5个元素。同样的道理,第五个元素一定是在第五次操作的时候添加的,所以,先找出第一个节点元素,然后往后next5次就可以找到第五次添加的这个节点元素对象。
比较重要的几点:
LinkedList
内部是链表结构- 不是线程安全的,多线程同时操作会出现问题
线程安全实现类-Vector
跟 ArrayList
差别不大,只不过里面的方法家里同步锁,实际用到的场景不是很多!
比较重要的点
- ⭐ 初始大小是10
- ⭐同步,是线程安全的
高效并发实场景现类-CopyOnWriteArrayList
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
概述
是一个线程安全的变体ArrayList ,支持高效率并发。读操作无锁,其中所有可变操作( add , set ,等等)通过对底层数组的最新副本实现。可变操作完成后将实体地址指向最新副本地址,实体设为null等待GC回收处理。
⭐该类适用于超大数据量下读操作大于写操纵的场景,原因是:一方面其add、set操纵需要将整个数组赋值,作为副本操作,在数据量特别大的情况下,复制副本这个开销会很大;另一方面复制副本后,实体还是存在的,副本add、set操作完成后才会将实体设为null等待回收。GC并不是立马回收,所以在这个过程中内存开销也很大。
⭐该类不能用于实时读的场景,add、set操作都是需要先复制出来一个副本,然后操作副本进行add、set操纵,在这个操纵没有完成之前,读操作依然是实体,这里的时差性导致实时读写会出问题。
⭐ 因为可以并发读取,所以读取效率高。
CopyOnWriteArrayList常用方法源码解析
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/*
同步锁,跟synchronized一样都是独占锁,但是又和synchronized有所区别。
ReentrantLock需要手动加锁解锁,比较灵活,等待线程的分配更公平。
*/
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
// ... ...
}
void add(E e)
public boolean add(E e) {
//块锁,块锁内的同步操作,多线程情况下只允许一哥线程进行操作
final ReentrantLock lock = this.lock;
lock.lock();//加锁
try {
Object[] elements = getArray();
int len = elements.length;
//注意区分数组的长度和下标,长度从1开始,下标从0开始
//newElements的长度就是len+1(原数组+1,因为要插入元素,长度+1)
//newElements的最后一个元素的下标就是它的长度-1
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;//将值插入到最后一个元素
setArray(newElements);//替换
return true;
} finally {
lock.unlock();//解锁
}
}
👆
!CopyOnWriteArrayList.add()方法跟ArrayList.add()相比,加了块锁,处理逻辑上也有区别。将整个数组复制然后将元素插入到最后。没有扩容数组的操作!
add
public void add(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
int numMoved = len - index;//插入元素后边剩余元素的个数
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len + 1);
else {
newElements = new Object[len + 1];
//#1将插入元素之前的所有元素复制到副本数组
System.arraycopy(elements, 0, newElements, 0, index);
//#2将插入元素之后的所有元素复制到副本元素
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
//替换元素
//复制后,新数组的 newElements[index]和 newElements[index+1]是重复的。
//所以set相当于是将插入index后边的元素整体后移了
newElements[index] = element;
setArray(newElements);
} finally {
lock.unlock();
}
}
set
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
E oldValue = get(elements, index);
if (oldValue != element) {
int len = elements.length;
//没什么逻辑,就是先复制再修改
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
remove
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 {
Object[] newElements = new Object[len - 1];
//复制需要删除元素之前的元素到新数组
System.arraycopy(elements, 0, newElements, 0, index);
//复制需要删除元素之后的元素,(不包括删除元素)到新数组
System.arraycopy(elements, index + 1, newElements, index,numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
get
private E get(Object[] a, int index) {
return (E) a[index];
}
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}
get没有太多逻辑!
遍历
!
优先使用迭代器遍历,不能使用迭代器的情况下使用基本的for循环遍历。
ArrayList<String> stringArrayList = new ArrayList<String>();
stringArrayList.add("a");
stringArrayList.add("b");
stringArrayList.add("c");
stringArrayList.add("d");
Demo1:for迭代器👇
for (Iterator it = stringArrayList.iterator();it.hasNext();){
System.out.println(it.next());
}
Demo2:for each循环👇
!for each 内部是迭代器实现的,效率比普通for循环要高!
for (String s: stringArrayList) {
System.out.println(s);
}
Demo3:for循环👇
for (int i = 0; i < stringArrayList.size(); i++) {
System.out.println(stringArrayList.get(i));
}