文章目录
- 一、底层存储结构是什么
- 二、初始容量
- 三、构造方法
- 四、扩容原理
- 五、读写速度比较
- 六、克隆为深克隆还是浅克隆
- 七、多线程环境下是否安全
- 八、增强遍历时添加或删除元素会发生什么事情
- 九、为什么数组被transient修饰
- 十、通过`subList()`获得的集合能否转为`ArrayList`
- 十一、使用`SubList`时有哪些注意事项
- 十二、如何将集合转为数组
- 十三、使用`Arrays.asList`有什么注意事项
推荐往期文章: ArrayList源码解读
一、底层存储结构是什么
答:数组
transient Object[] elementData;
二、初始容量
答:默认初始容量为10,也可以通过构造方法指定初始容量。
-
默认初始容量为10
private static final int DEFAULT_CAPACITY = 10;
-
通过构造方法指定初始容量
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; }
-
指定初始容量的构造方法
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(Collection<? extends E> c) { Object[] a = c.toArray(); if ((size = a.length) != 0) { if (c.getClass() == ArrayList.class) { elementData = a; } else { elementData = Arrays.copyOf(a, size, Object[].class); } } else { // replace with empty array. elementData = EMPTY_ELEMENTDATA; } }
四、扩容原理
当需要向ArrayList集合中添加元素时,采用先扩容,后插入的方式。
ArrayList的扩容逻辑由grow()
方法实现,扩容后底层数组的新长度=原长度*1.5,即容量扩展为原来的1.5倍,该计算方法是通过位运算实现的,且最大长度不得超过int的最大值,即
2
23
−
1
2^{23}-1
223−1。
当确定新的数组长度后,通过Arrays.copyOf
获取一个具有新长度的数组,该方法底层是通过调用jvm本地方法System.arraycopy()
方法实现的。
五、读写速度比较
答:读数据快,写数据慢。由于底层采用数组作为存储元素的结构,而数组支持根据下标读取元素。
当根据下标操作ArrayList时,读取和修改操作快,添加和删除操作慢(因为这两个操作需要移动数组中的元素)。特殊情况是在数组尾部添加元素时,操作快。
当根据元素操作ArrayList时,读取、修改、添加、删除四个操作都慢,因为数组本身只支持根据下标操作元素,因此当根据元素操作时,需要从头遍历数组。
六、克隆为深克隆还是浅克隆
答:浅克隆。
ArrayList实现了Cloneable
接口并实现了clone()
方法,因此具有克隆的功能。但是其clone()
方法的实现中仅克隆了ArrayList
对象,却没有克隆其内部数组中的元素(通过Arrays.copyOf()
方法),结果是克隆后的ArrayList
对象与克隆前的ArrayList
对象为两个对象,内部数组中的元素却是相同的对象,指向同一个内存地址。
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
代码演示如下:
public static void cloneDemo() {
ArrayList<Person> personArrayList = new ArrayList<>();
personArrayList.add(new Person("张三"));
personArrayList.add(new Person("李四"));
personArrayList.add(new Person("王五"));
System.out.println("克隆前person集合,内存地址:" + System.identityHashCode(personArrayList) + ",元素:" + personArrayList);
Object personListClone = personArrayList.clone();
ArrayList<Person> newPersonList = (ArrayList<Person>) personListClone;
System.out.println("克隆后person集合,内存地址:" + System.identityHashCode(newPersonList) + ",元素:" + newPersonList);
newPersonList.get(0).setName("我修改了名字");
System.out.println("修改元素==============");
System.out.println("克隆前person集合,内存地址:" + System.identityHashCode(personArrayList) + ",元素:" + personArrayList);
System.out.println("克隆后person集合,内存地址:" + System.identityHashCode(newPersonList) + ",元素:" + newPersonList);
}
输出:
七、多线程环境下是否安全
答:多线程不安全
因为在ArrayList
中没有使用synchronized
或其他加锁操作,因此线程不安全。
在多线程环境下,可以通过以下方法避免线程不安全的问题:
- 在操作
ArrayList
的方法或代码块上加锁,无论是通过synchronized
还是Lock
。 - 使用
Vector
- 使用
CopyOnWriteArrayList
。
八、增强遍历时添加或删除元素会发生什么事情
答:会抛出ConcurrentModificationException
异常,但仅限于增强型遍历 或 forEach()
方法。
如下代码所示
private ArrayList<Integer> list = new ArrayList<>();
for(Integer i : list) {
list.remove(i);
}
在遍历时通过添加或删除元素会导致其底层数组结构发生变化,ArrayList通过使用expectedModCount
和modCount
两个变量判断结构是否发生变化,如果有变化,则将抛出ConcurrentModificationException
异常。
解决办法:
- 使用ArrayList内部类迭代器
Itr
或ListItr
提供的add()
和remove()
方法对集合进行添加和删除操作。在迭代器的方法中会控制expectedModCount
和modCount
两个变量的值,使其避免抛出ConcurrentModificationException
。 - 使用
CopyOnWriteArrayList
。
九、为什么数组被transient修饰
答:使用关键字transient
来修饰数组并不表示不将其序列化,ArrayList在序列化时,最主要的是将其内部元素进行序列化而不是数组,再加上数组中可能存在大量空元素,若将数组进行序列化,对时间和空间无疑都是一种消耗。
另外我们可以通过源码看一下他的序列化和反序列化过程。
-
序列化
在序列化时,通过遍历数组中的元素,将元素进行序列化,而非数组。
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ // Write out element count, and any hidden stuff int expectedModCount = modCount; s.defaultWriteObject(); // Write out size as capacity for behavioural compatibility with clone() s.writeInt(size); // Write out all elements in the proper order. for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } }
-
反序列化
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { elementData = EMPTY_ELEMENTDATA; // Read in size, and any hidden stuff s.defaultReadObject(); // Read in capacity s.readInt(); // ignored if (size > 0) { // be like clone(), allocate array based upon size not capacity int capacity = calculateCapacity(elementData, size); SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity); ensureCapacityInternal(size); Object[] a = elementData; // Read in all elements in the proper order. for (int i=0; i<size; i++) { a[i] = s.readObject(); } } }
十、通过subList()
获得的集合能否转为ArrayList
答:不能。
subList()
方法返回的是ArrayList
的内部类SubList
的实例,并不是 ArrayList 本身,而是 ArrayList 的一个视图,对于 SubList 的所有操作最终会反映到原列表上。因此无法转为ArrayList
。
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
在《阿里巴巴Java开发手册》嵩山版中是这样描述的:
十一、使用SubList
时有哪些注意事项
SubList
是ArrayList
的一个内部类,表示为ArrayList
集合的一个视图,对子集合的操作会影响父集合,对父集合的操作也会影响子集合。
- 修改父集合元素的值,会影响子集合
- 修改父集合的结构,会引起子集合在遍历、增加、删除操作中出现
ConcurrentModificationException
异常 - 修改子集合元素的值,会影响父集合
- 修改子集合的结构,会影响父集合
在《阿里巴巴Java开发手册》嵩山版中是这样描述的:
十二、如何将集合转为数组
答:使用toArray()
方法或toArray(T[] a)
方法。
但是一般来说,我们都使用toArray(T[] a)
方法实现,且传入的是类型完全一致、长度为0的空数组。至于为什么,在《阿里巴巴Java开发手册》嵩山版中是这样描述的:
十三、使用Arrays.asList
有什么注意事项
答:使用Arrays.asList()
方法得到的List实例为ArrayList 实例,但需要注意的是,此处返回的ArrayList 实例为Arrays的内部类,其全限定类路径为java.util.Arrays.ArrayList
,而我们常用的ArrayList 的全限定类路径为java.util.ArrayList
,这是两个不同的类。
当我们通过Arrays.asList()
方法得到一个集合时,只能对其进行读取、遍历、修改操作,而不能使用修改集合结构相关的方法如增加、删除等,否则会抛出异常UnsupportedOperationException
。
且该内部类体现的是适配器的设计模式,其内部依然是一个数组,只是通过asList()
方法改变我们操作数组的方式而已。
在《阿里巴巴Java开发手册》嵩山版中是这样描述的:
纸上得来终觉浅,绝知此事要躬行。
————————————————我是万万岁,我们下期再见————————————————