简介
在Java的集合中,ArrayList可以说是用的最频繁的一个,List是一个有序,可重复的集合,但是它确是线程不安全的,我么还可以通过索引来访问指定位置的集合元素。虽然我们平时经常的使用它但是对于它的底层的代码实现我们却一直没有怎么关注。
类结构图
代码实现
我们一般都是两种构造方法来创建ArrayList的。
//数组中真实存储数据的长度
int size;
//用于存储数据的数组,这也是该类的核心存储结构
transient Object[] array;
public ArrayList(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("capacity < 0: " + capacity);
}
//如果 capacity为0的话,则创建一个空数组,否则就创建指定大小的数组
array = (capacity == 0 ? EmptyArray.OBJECT : new Object[capacity]);
}
//当我们使用默认构造方法的时候,这个时候就创造一个空数组
public ArrayList() {
array = EmptyArray.OBJECT;
}
- 添加数据
public boolean add(E object) {
Object[] a = array;
int s = size;
//表示实际个数与数组长度相等,需要扩容长度的长度了
if (s == a.length) {
//如果之前的长度小于6的话,则在原有的基础上扩容 12 个位置,
//否则扩大原有基础的1.5倍也就是多增加0.5倍原来的空间
Object[] newArray = new Object[s +
(s < (MIN_CAPACITY_INCREMENT / 2) ?
MIN_CAPACITY_INCREMENT : s >> 1)];
//将之前数组的内容拷贝到新数组 newArray中
System.arraycopy(a, 0, newArray, 0, s);
array = a = newArray;
}
//将元素放到最后一个位置(注意这里的最后一个位置并不是数组的最后一个位置,因为size <= array.length的)
a[s] = object;
//数组的实际长度 + 1
size = s + 1;
//修改次数 + 1
modCount++;
return true;
}
- 查询元素
public E get(int index) {
//判断我们传入的位置是否大于等于等于数组中实际元素个数大小,千万要注意这里的 size 并不是数组的大小,而是存入元素的个数,当发生remove的时候,size 会减 1的。
if (index >= size) {
throwIndexOutOfBoundsException(index, size);
}
return (E) array[index];
}
//示例
private void arrayListTest() {
//我们往数组中 添加3个元素,但是我们取第6个元素的时候,这个是会抛异常的,因为 6 > 3了
List<String> list = new ArrayList<>(16);
list.add("123");
list.add("3");
list.add("hello");
//这里直接抛异常
System.out.println(list.get(6));
}
- 获取元素在集合中的位置
public int indexOf(Object object) {
Object[] a = array;
int s = size;
if (object != null) {
//从 索引0遍历到 size-1 位置
for (int i = 0; i < s; i++) {
//这里判断两个元素是否相等调用的是 equals 方法
//如果集合中是某个类的对象的话,我们需要重写该类的 equals方法才能判断该对象是否在集合中
if (object.equals(a[i])) {
//然后返回该元素所在的位置。注意两个集合中存在两个相同的元素的话,返回的是第一个元素的位置
return i;
}
}
} else {
//从索引 0 遍历到 size -1 位置
for (int i = 0; i < s; i++) {
if (a[i] == null) {
return i;
}
}
}
return -1;
}
- 根据索引位置删除元素
public E remove(int index) {
Object[] a = array;
int s = size;
if (index >= s) {
throwIndexOutOfBoundsException(index, s);
}
//获取数组中第index位置的元素,并且最后还需要return 该元素
E result = (E) a[index];
//这里的--s也就是 s = s - 1的意思。
//将 index 后面的元素全部向前移动一个位置,其实这个时候 s 位置
System.arraycopy(a, index + 1, a, index, --s - index);
//上面因为涉及了数组内元素的移动,所以还需要将最后一个位置[s]的元素清空(变成null,有利于内存回收)
a[s] = null; // Prevent memory leak
//size = size - 1;
size = s;
//修改次数 + 1
modCount++;
return result;
}
- 根据元素内容删除元素
//该方法并不会返回要删除的元素,只是返回删除元素的状态。如果删除成功则返回true,否则返回false。
public boolean remove(Object object) {
Object[] a = array;
int s = size;
//判断元素是否为空
if (object != null) {
for (int i = 0; i < s; i++) {
//首先需要遍历数组,找到该元素的具体位置,然后其实跟调用remove(index)方法是一样的了。
if (object.equals(a[i])) {
//将 i +1以后所有的元素都向前移动一个位置,覆盖 i 这个位置的元素。这样子就算是删除了元素。
System.arraycopy(a, i + 1, a, i, --s - i);
//同时需要将 s 位置的元素置位 null,有利于内存回收。
a[s] = null; // Prevent memory leak
size = s;
modCount++;
return true;
}
}
} else {
for (int i = 0; i < s; i++) {
//首先需要遍历数组,找到该元素的具体位置
if (a[i] == null) {
//将 i +1以后所有的元素都向前移动一个位置,覆盖 i 这个位置的元素。这样子就算是删除了元素。
System.arraycopy(a, i + 1, a, i, --s - i);
//同时需要将 s 位置的元素置位 null,有利于内存回收。
a[s] = null; // Prevent memory leak
//size = size - 1;
size = s;
//操作次数 + 1
modCount++;
return true;
}
}
}
return false;
}
- 判断是否为空
public int size() {
return size;
}
//这里我们只需要判断 size的长度是否为0就知道List中是否有元素了,注意这里不能判断数组的长度
public boolean isEmpty() {
return size == 0;
}
- 清空所有元素
public void clear() {
//判断size是否大于0
if (size != 0) {
//然后将数组中所有元素都置为null
Arrays.fill(array, 0, size, null);
//同时 真正的计数 size 需要变成0,这样子List才算是真正的清空了。
size = 0;
modCount++;
}
}
删除元素的坑
- 坑一
private void arrayListTest() {
List<Integer> integers = new ArrayList<>(5);
integers.add(1);
integers.add(2);
integers.add(2);
integers.add(4);
integers.add(5);
int size = integers.size();
for (int i = 0; i < size; i++) {
//在获取第4个位置(i = 3)元素的时候,这个时候会直接抛出异常,因为remove一个元素以后size = size - 1了。在 get()方法中有一个判断就是 index >= size的话则会抛出异常的。
if (integers.get(i)%2==0) {
integers.remove(i);
}
}
System.out.println(integers);
}
结果直接抛出异常
- 坑二
还是上面的代码,只是这次我们改变了一下。
private void arrayListTest() {
List<Integer> integers = new ArrayList<>(5);
integers.add(1);
integers.add(2);
integers.add(2);
integers.add(4);
integers.add(5);
// i < integers.zie()了,这次我们不存储变量了
for (int i = 0; i < integers.size(); i++) {
//删除完第二个元素以后,数组会进行一次的移动的,也就是会覆盖第二个(i = 1)元素的内容,第三个元素会覆盖第二个元素。这个时候i = 2这个元素就移动到 [1] 这个位置了,所以之前i = 2这个位置的元素就不会遍历到,所以这个位置的元素也就漏掉了。
if (integers.get(i) % 2 == 0) {
integers.remove(i);
}
}
System.out.println(integers);
}
最后的结果打印的是 [1, 2, 5]
- 坑三
private void arrayListTest() {
List<String> list = Arrays.asList("123", "456", "789");
list.add("hello");
System.out.println(list);
}
最后结果是直接抛出异常
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.AbstractList.add(Unknown Source)
at java.util.AbstractList.add(Unknown Source)
其实这个我觉得不能算是 ArrayList的问题,而是JDK 内部做的一个限制吧。
public class Arrays {
//这里的 ArrayList其实并不是 java.util.ArrayList 类。而是 Arrays内部定义的一个静态内部类
public static <T> List<T> asList(T... array) {
return new ArrayList<T>(array);
}
//该类跟 java.util.ArrayList的类继承是一模一样的。只是该类并没有实现 add方法,remove方法,只实现了查找 和 修改元素的操作,说白了就是一个完整的数组。
private static class ArrayList<E> extends AbstractList<E> implements
List<E>, Serializable, RandomAccess {
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;
ArrayList(E[] storage) {
if (storage == null) {
throw new NullPointerException("storage == null");
}
a = storage;
}
.......
}
//因为没有实现 add 和remove 方法。所以调用的是父类的 AbstractList类的 add 和remove方法。所以就直接抛了一个异常出来了。
}
public abstract class AbstractList {
public boolean add(E object) {
add(size(), object);
return true;
}
public void add(int location, E object) {
throw new UnsupportedOperationException();
}
}
- 坑四
public void arrayListTest() {
List<String> strings = new ArrayList<>();
strings.add("a");
strings.add("b");
strings.add("c");
strings.add("d");
for (String string : strings) {
strings.remove(string);
}
}
结果也是直接抛出一个异常(ConcurrentModificationException)
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
at java.util.ArrayList$Itr.next(Unknown Source)
- 坑五
public void test() {
List<String> strings = new ArrayList<>();
strings.add("a");
strings.add("b");
Iterator<String> iterator = strings.iterator();
while (iterator.hasNext()){
String next = iterator.next();
strings.remove(next);
}
System.out.println(strings);
}
最后结果还是抛异常(ConcurrentModificationException)
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
at java.util.ArrayList$Itr.next(Unknown Source)
通过上面一系列的操作已经令我们崩溃了,这个时候我们就会开始的喷Java 设计的太垃圾了,这是什么鬼呀,总是不对,不符合我们的逻辑呀。我们仅仅想删除集合中一些不符合条件的元素而已,这样子遍历的去删除有什么错吗?接着我们看看Java为我们设计一个 Iterator
- 正确的删除元素代码
public void arrayListTest() {
List<Integer> integers = new ArrayList<>(10);
integers.add(1);
integers.add(2);
integers.add(2);
integers.add(4);
integers.add(8);
integers.add(5);
Iterator<Integer> iter = integers.iterator();
while (iter.hasNext()){
if(iter.next() % 2 == 0) {
iter.remove();
}
}
System.out.println(integers);
}
总结
通过上面的分析我们可以得出得出ArrayList一下特点:
- ArrayList的方法中没有锁,所以不是线程安全的
- 其内部实现是一个数组结构,我们可以根据索引找到具体的内容,也是有序的
- ArrayList对于查询和修改内容效率是非常高的,但是对于删除和在某些位置中插入数据的,需要移动数组的内容,这样子个人觉得效率比较低的,特别是List 中的长度比较大时
- 我们在遍历删除某些数据的时候,需要用到迭代器(Iterator)去操作
- 需要主要 Arrays.ArrayList的与 java.util.ArrayList的区别。