介绍
初学java的同学, 对于存储批量数据的时候常用数组, 但是有一个问题是如果根据查询条件从数据库中读取数据你不知道要读取出多少条, 但是呢 数组又一定要指定一个长度 , 所以我们就需要一个自动扩容的 "动态数组" List 也就是我们说的 集合 很好的解决了这个问题, 当你的数据量大于当前集合的承受范围时 , 它将自动的扩容知道把所有数据都添加进去为止。
ArrayList构造函数
-
ArrayList()
/**
* 该方法指的是构造一个默认长度为10的空集合
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
-
ArrayList(int initialCapacity)
/**
* 构造一个指定初始长度的空列表。
* @param initialCapacity 列表长度。
* @param IllegalArgumentException 非法指定长度的异常。
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//如果传入的长度 > 0 , 则构造一个为该长度的object类型的数组。
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//如果传入的长度为0 , 则把默认的空元素数组赋值给当前实例的elementData。
this.elementData = EMPTY_ELEMENTDATA;
} else {
//如果插入的长度 < 0 , 则抛出非法容量的异常。
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
-
ArrayList(Collection<? extends E> c)
/**
* 按照集合迭代器返回元素的顺序构造包含指定集合的元素的列表。 (源码上给出的注释)
* 上面注释大致意思就是 : 根据你传递进去的集合参数,来构造出一个和你传递参数一样的集合可理解为复制。
* @param c 要放入此列表中的集合。
*/
public ArrayList(Collection<? extends E> c) {
//将传递进来的集合转为数组 , 复制给当前实例的elementData。
elementData = c.toArray();
//判断当前传递进来的集合是否为空。
if ((size = elementData.length) != 0) {
//判断当前的elementData的类型是不是object[]的类型。
if (elementData.getClass() != Object[].class)
//不是的话将当前的elementData通过Arrays.copy转为object[] 并重新赋值给elementData。
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
//为空的话把当前EMPTY_ELEMENTDATA赋值给elementData , 这就相当于我们之前说的第二个构造方法 new ArrayList(0)。
this.elementData = EMPTY_ELEMENTDATA;
}
}
ArrayList常用方法
-
添加元素 add()
首先我们先来看 add() 方法的开头 , 然后逐步去分析~
add()
/**
* 将一个元素添加此列表的结尾处。
* @param e 要添加到此列表的元素。
*/
public boolean add(E e) {
//确保内部的容量
ensureCapacityInternal(size + 1);
//相当于 elementData[size] = e; size++;
elementData[size++] = e;
return true;
}
我们在这里发现了一个叫 ensureCapacityInternal(size + 1) 的方法我们在走进去看一下。
ensureCapacityInternal(size + 1)
/**
* 确保内部的容量
* @param minCapacity 最小容量
*/
private void ensureCapacityInternal(int minCapacity) {
//如果当前实例的elementData 是空数组 , 则取较大值(注: 无参构造函数实在这里在开始初始化长度10的, DEFAULT_CATPACITY = 10)
//这里为什么要判断呢? 我个人理解是如果是第一次add就要把数组的长度初始化为10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
在此方法中又发现了 ensureExplicitCapacity(minCapacity); 方法, 再次进入方法体。
ensureExplicitCapacity(int minCapacity);
/**
* 确保真正的容量增量
* @param minCapacity 最小容量
*/
private void ensureExplicitCapacity(int minCapacity) {
//改动次数+1
modCount++;
//如果最小需要的容量比当前elementData数组的长度还要大, 那么就要进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
在此方法中出现了扩容方法 grow(minCapacity) 进入方法内部。
grow(int minCapacity)
/**
* 增加容量,确保数组能够容下 最小容量的元素数量
* @param minCapacity 最小容量
*/
private void grow(int minCapacity) {
//当前elementData的旧容量
int oldCapacity = elementData.length;
//计算出elementData需要的新容量大小(注:这里的 " >> " 是java 的位移运算符,指的是将 >> 的数字转换
为二进制向右移动1位,也就是相当于除以二)
//扩容1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果新的容量还是小于最小容量,就把最小容量赋值给最新的容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果新的容量大于数组的最大容量, 就是用hugeCapacity(超大容量)来增加容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//计算出新的容量后, 拷贝数组Arrays.copy(elementData, newCapacity);
//第一个参数elementData的意思是需要拷贝的源数组
//第二个newCapacity的意思是需要拷贝的下标范围(0 - nweCapacity 个人理解,如有错误欢迎指正)
//然后用elementData引用重新接受具有新的容量的数组实例
elementData = Arrays.copyOf(elementData, newCapacity);
}
在这其中有个插曲就是我们发现了hugeCapacity(int minCapacity)方法 ,进入方法内部看看。(注:也可忽略因为很少用到,如果执行完了上一个方法add过程也就结束了。)
hugeCapacity(int minCapacity)
/**
* 超大容量的扩容
* @param minCapacity 最小容量
* @return
*/
private static int hugeCapacity(int minCapacity) {
//判断minCapacity的值
if (minCapacity < 0)
throw new OutOfMemoryError();
//minCapacity相比如果大于MAX_ARRAY_SIZE 返回 Integer的最大值,否则返回MAX_ARRAY_SIZE
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
当我们的add里面调用的其他方法全部执行完返回true的时候证明整个add过程执行结束。
-
add(int index, E element)
这个就是添加一个指定的元素,到指定的位置上去。
/**
* 在此列表中的指定位置插入指定的元素
* 将当前位于该位置的元素(如果有)和任何后续元素右移(在其索引中添加一个元素), 这是来自jdk源码的注释
* 我的理解是: 将当前元素和后续元素全部往后移动, 然后让出一个位置给新元素
* @param index 要插入指定元素的索引
* @param element 要插入的元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
//检查索引越界
rangeCheckForAdd(index);
//数组扩容
ensureCapacityInternal(size + 1);
//将当前元素和后续所有元素往后移动,让出位置
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//在指定的位置添加指定的元素
elementData[index] = element;
size++;
}
这个方法不难, 但是我个人用的比较少 , 不知道大家的情况 , 所以也写出来了。
-
addAll(int index, Collection<? extends E> c)
这个方法的作用是可以根据给出的指定位置起插入一个集合的所有元素。
/**
* 将指定集合中的所有元素插入此列表,从指定位置开始。
* 移动元素当前在该位置和任何后续元素右边
* @param index 插入第一个元素的索引
* @param c 要插入的元素集合
* @return
*/
public boolean addAll(int index, Collection<? extends E> c) {
//检查索引越界
rangeCheckForAdd(index);
//把传入的参数转为数组
Object[] a = c.toArray();
int numNew = a.length;
//数组扩容
ensureCapacityInternal(size + numNew);
//计算出需要移动的位置大小
int numMoved = size - index;
//如果不是在末尾插入
if (numMoved > 0)
//数组拷贝, 传入的集合有多少个元素,数组就往后以多少位,空出位置
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
//如果在末尾插入
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
总之大部分需要移动元素能完成的操作,大致上都差不多,理解了它的移动规则就好。
-
addAll(Collection<? extends E> c )
此方法是将参数集合添加到此集合的末尾
/**
* 按指定集合的迭代器返回元素的顺序,将指定集合中的所有元素追加到此列表的末尾。
* 如果在操作进行过程中修改了指定的集合,则此操作的行为未定义
* 这意味着,如果指定的集合是此列表,而此列表不是空的,则此调用的行为取消。
* @param c 要添加到此列表的元素的集合
*/
public boolean addAll(Collection<? extends E> c) {
//首先将传入的集合转为数组
Object[] a = c.toArray();
int numNew = a.length;
//扩容 在此之前的add()方法有详解,不懂的可以回去看下
ensureCapacityInternal(size + numNew);
//把数组拷贝到扩容后的elementData
System.arraycopy(a, 0, elementData, size, numNew);
//改变size大小
size += numNew;
return numNew != 0;
}
我们会发现, 如果弄明白了add() 到这里是非常简单的~~~
-
remove(int index)
remove方法是一个重载方法我们先介绍参数为 index 的, 也就是根据下标去删除元素的remove方法, 这
里直接上代码。
/**
* 删除此列表中指定位置的元素, 并且将所有后续元素向左移动。
* @param index 要删除的元素位置
* @return
*/
public E remove(int index) {
//检查输入的元素下标是否越界
rangeCheck(index);
//修改数 + 1
modCount++;
//取出数组中在该下标的元素
E oldValue = elementData(index);
//计算出要复制的元素数 (size:实际元素存在数量 - index:下标 - 1)
//因为下标从0开始, 所以size - index 后应该在 - 1
int numMoved = size - index - 1;
//如果移除的不是数组中的最后一个元素
if (numMoved > 0)
//拷贝数组
//elementData: 要拷贝的目标源数组
//srcPos: 源数组拷贝的起始位置
//elementData: 要拷贝到的目标数组
//index: 拷贝到的目标的数组的起始位置
//numMoved: 拷贝的元素数量
//总的来说就是 将源数组被移除的下标的后面所有元素 全部拷贝到当前数组被移除元素的下标及下标后(说白了就是被删除的元素后面的素有元素整体往左移动)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//这里设置最后一个元素为null因为最后一个元素已经被复制到elementData.length - 1 - 1 上了,可以直接让GC垃圾回收机制进行回收释放资源,并且size - 1
elementData[--size] = null;
//返回移除的元素值
return oldValue;
}
以上就是remove(int index)执行流程。
-
remove(Object o)
这个方法是根据传入的对象最匹配来进行元素的删除。
/**
* 此列表中如果出现一个和 o 匹配的元素, 则删除第一个匹配项,如果不包含此项, 则不变。
* @param o
* @return
*/
public boolean remove(Object o) {
//判断o是否为null 因为ArrayList允许存null值
if (o == null) {
//循环判断所有元素是否为null,如果有满足条件的则进入fastRemove(index);
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
//循环判断o 是否和当前数组中的元素相匹配, 匹配进入fastRemove
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
//如果没有删除掉, 或者说没有和o匹配的元素返回false
return false;
}
/**
* 快速删除方法,和remove(int index) 类似, 区别就是这里跳过了索引越界检查, 其他的这里不再做解释。
* @param index
*/
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;
}
-
removeAll(Collection<?> c)
这个方法是删除指定集合中包含的所有此集合的元素, 共包括两个方法,看代码 ↓
/**
* 删除指定集合中包含的所有此集合的元素
* @param c 要删除的元素的集合
* @return
*/
public boolean retainAll(Collection<?> c) {
//这里判断传入的参数是否为null (为什么不用 c == null 呢? 因为这个方法会自动抛出空指针异常,不用我们去写了)
Objects.requireNonNull(c);
//最关键的就是这个方法
return batchRemove(c, true);
}
/**
* 将批量删除
* @param c 要删除的元素集合
* @param complement false
* @return
*/
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
//这里循环判断集合元素和数组的元素时候相等
if (c.contains(elementData[r]) == complement)
//如果相等的话,将元素放入第一个数组位置, 换句话说就是把不删除的元素往前移,然后用w变量当删除的分界线
elementData[w++] = elementData[r];
} finally {
//这里的作用是如果c.contains(e)过程中出现异常的话,可以通过数组copy的方式完成删除操作
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
//如果有相同的元素要被删除的时候
if (w != size) {
//上面说了w就是不删除与删除元素之间的一条分界线,从这条分界线开始往后全部设置为null让GC回收
for (int i = w; i < size; i++)
elementData[i] = null;
//记录下改变次数
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
好啦, 常用的几个删除方法就介绍都这里。
-
set(int index, E element)
该方法的作用是将一个元素替换到你指定的列表位置上,并返回原先在该位置上的元素。
/**
* 将此列表中将指定位置的元素,替换为新的元素
*
* @param index 要替换的元素的索引
* @param element 要存储在指定位置的元素
* @return 以前位于该位置的元素
*/
public E set(int index, E element) {
//老规矩, 检查索引越界
rangeCheck(index);
//取出当前在此位置的元素
E oldValue = elementData(index);
//将新元素设置到该位置
elementData[index] = element;
//返回原先的此位置上的元素
return oldValue;
}
这个看着还是非常简单的~~~
-
clear()
这个方法的作用是将此集合的所有元素删除,size归0。
/**
* 删除此列表中的所有元素。此方法执行完成后,列表将为空。
*/
public void clear() {
//修改次数 + 1
modCount++;
//循环所有的元素为null GC回收
for (int i = 0; i < size; i++)
elementData[i] = null;
//设置size为0
size = 0;
}
这个也不难~~~
-
contains(Object o)
这个方法是用于判断传入的对象是否和此列表中的某个元素相同, 如果相同返回true,反之返回false
/**
* 如果此列表包含指定的元素就返回true
* @param o 要测试的元素
*/
public boolean contains(Object o) {
//如果返回> 0 的就返回true
return indexOf(o) >= 0;
}
/**
* 返回在此列表中和指定元素相同的索引, 如果全部不同返回-1
*/
public int indexOf(Object o) {
//判断是否为null
if (o == null) {
//如果有为null的返回其下标
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
//如果有和指定元素相同的则返回其下标
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
这个方法还是很常用的,了解一下也比较好最好别用多重循环嵌套的集合使用此方法(影响效率,不过问题不大~~~)。
-
isEmpty()
此方法是用来判断当前的列表有没有元素。
/**
* 如果此列表不包含元素则返回true
*/
public boolean isEmpty() {
return size == 0;
}
我知道这个没什么说的, 嗯~ 但是还是想说。
-
trimToSize()
用于裁剪数组大小的方法
/**
* 将此实例的容量调整为列表的当前大小
* 说白了就是调整成size 的大小 , 减少开销(数组要那么长干嘛......)
*/
public void trimToSize() {
//修改次数 + 1
modCount++;
//如果size 小于当前数组的长度才会进行裁剪操作
if (size < elementData.length) {
//如果当前这个数组里面并没有存储元素, 就把当前的数组设置成一个默认的空实例
//如果当前数组有元素存储,就把当前的数组设置成数组里的元素数的大小
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
-
subList(int fromIndex, int toIndex)
这个方法就是用来返回一个当前集合指定范围内的视图。
/**
* jdk源码的注释太多了总之大概意思如下:
* 根据你传入的 formIndex 和 toIndex 返回一个 subList的视图 (注意是视图,而不是新的实例)
* 改变sublist的结构会影响 父list
* @return
*/
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new ArrayList.SubList(this, 0, fromIndex, toIndex);
}
/**
* 检查索引是否符合规定
* @param fromIndex
* @param toIndex
* @param size
*/
static void subListRangeCheck(int fromIndex, int toIndex, int size) {
if (fromIndex < 0)
throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
if (toIndex > size)
throw new IndexOutOfBoundsException("toIndex = " + toIndex);
if (fromIndex > toIndex)
throw new IllegalArgumentException("fromIndex(" + fromIndex +
") > toIndex(" + toIndex + ")");
}
/**
* SubList类
*/
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size;
/**
* 构造方法
* @param parent
* @param offset
* @param fromIndex
* @param toIndex
*/
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
//parent是父list的引用
this.parent = parent;
//父list的fromIndex
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
//计算出当前视图的size
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
}
看到这里可能有人会问这里只用一个构造方法就是把父级的list引用传递过来而没有对elementData有 任何修改 , 怎么做到裁剪list呢?
/**
* SubList的get方法
* @param index
* @return
*/
public E get(int index) {
rangeCheck(index);
checkForComodification();
//注意这里是offset + index 而不是 只有index
//elementData(index) 注意区别
return ArrayList.this.elementData(offset + index);
}
大家注意这里,所有取出的数据都是在offset基础之上的 , 而这个offset正是我们之前传递过来的fromIndex , 所以在他的基础上就可以取出我们想要裁剪出的数据了。
subList(int fromIndex, int toIndex)使用注意事项
- 返回的这个子列表的幕后其实还是原列表 , 也就是说,修改这个子列表,将导致原列表也发生改变;反之亦然。
- 如果发生结构性修改的是返回的子list,那么原来的list的大小也会发生变化;
- 分裂成独立的集合 List<T> list = new ArrayList(subList);
- 清空此阶段的数据 list.subList(1,2).clear();
- 如果你在调用了sublist返回了子list之后,如果修改了原list的大小,那么之前产生的子list将会失效,变得不可使用。
- 返回的视图范围是 包括 ()fromIndex ---- toIndex - 1) 之间的数据 , 也就是说包括fromIndex而不包括toIndex
-
removeIf(Predicate<? extends E> filter)
这个方法就是删除此集合中符合表达式条件的元素。
/**
* 删除列表中符合filter条件的元素
* @param filter
* @return
*/
@Override
public boolean removeIf(Predicate<? super E> filter) {
//这里判断下参数是否为空, 为空的话抛出空指针异常
Objects.requireNonNull(filter);
//找出要删除的元素
//在此阶段从筛选器谓词引发的任何异常都将使集合保持不变
//removeCount 记录下删除的元素数
int removeCount = 0;
//bitSet 是用于数据量大的去重,排序的
final BitSet removeSet = new BitSet(size);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
@SuppressWarnings("unchecked")
//取到当前循环的元素
final E element = (E) elementData[i];
//表达式测试结果是否为true
if (filter.test(element)) {
//把当前的索引记录到bitset里面, 用于记住这个索引的元素是要被删除的
removeSet.set(i);
//删除的数量 + 1
removeCount++;
}
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
//将保留的元素移到删除元素留下的空间上
final boolean anyToRemove = removeCount > 0;
//如果有需要删除的元素, 继续往下执行
if (anyToRemove) {
//计算出移除后的数组元素数量
final int newSize = size - removeCount;
for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
//寻找出存储在bitset中 以当前i 开始至后面 第一个为false的索引并返回
i = removeSet.nextClearBit(i);
//前面把要删除的元素的索引存储在了bitset里面,所以为false的肯定就是不用删除的啊
//把不用删除的元素一次往前面移动,那么被删除的元素就被覆盖,或者依旧到最后面几位了
elementData[j] = elementData[i];
}
//这里就可以把后面几个没用的数据删除了
for (int k=newSize; k < size; k++) {
//设置为null 让gc回收
elementData[k] = null;
}
//设置新的size
this.size = newSize;
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
//变更次数 + 1
modCount++;
}
return anyToRemove;
}
大家要看这个方法的源码的时候 , 首先要对lamdba和Bitsize有些了解。这个是Bitsize的链接希望可以帮到你。
不得不承认 , 人家jdk在这个方法上实现的确实比较吊。(只是对目前的我来说 , 大神勿喷~)
附上Bitsize 的链接 https://www.cnblogs.com/angin-iit/p/8857556.html
-
get(int index)
get()方法是我们最常用的方法, 是用于取出集合中的元素的方法,下面我们就来看一看get的源码(还是相当简单的~~~)
/**
* 返回此列表中指定位置的元素
* @param index 想要取出的元素的位置
* @return 在此位置的元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
//检查是否越界
rangeCheck(index);
//直接返回在此此位置的元素
return elementData(index);
}
结语
好啦 , 关于ArrayList的常用方法基本就这么多了 , 我自己写这个文章的目的就是学习一下 , 做个笔记 , 当然能帮助大家也是更好。上面总结的东西也是我个人自己看完了(当然也有参考其他的文章) 然后写出来的,有什么不对的地方欢迎各位大佬指正 , 在此谢过了~~~
世界残酷 , 但并非一定丑陋 —— 戏命师 . 烬
在此鸣谢:https://www.cnblogs.com/angin-iit/p/8857556.html