ArrayList

  • ArrayList是实现了List接口的动态数组。实现了所有的可选列表操作(add, remove, set, get, size, indexOf, contains …),允许所有的元素,包括null。
  • ArrayList是线程不安全,如果多个线程访问同一个ArrayList实例,并且至少有一个线程修改了列表的结构(添加或删除一个或多个元素,或显式调整后备数组的大小都会造成结构修改;仅设置元素的值不是结构修改。),则必须在外部进行同步。通常采用另外一种方法解决这个问题:List list = Collections.synchronizedList(new ArrayList(…));
  • fail-fast机制1,如果在迭代器创建之后发生列表的结构修改(除非通过迭代器自己创建的remove或者add方法),迭代器会抛出ConcurrentModificationException。
  • size,isEmpty,get,set,iterator和listIterator方法以恒定的时间运行。add方法以“分摊后恒定的时间” (amortized constant time2 )运行,就是说,增加n个元素的时间复杂度为O(n)。所有其他的方法以线性时间运行。

1. 首先看下ArrayList的继承关系:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{

着重提一下RandomAccess接口,这是一个标记接口,没有任何方法,实现此接口表明支持快速随机访问,比如对ArrayList使用for循环遍历就比iterator迭代器要快3

2. 变量

/**
 * 默认初始容量
 */
private static final int DEFAULT_CAPACITY = 10;

/**
 * 用于空实例的共享空数组实例
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * 用于默认大小空实例的共享空数组实例。我们将此与EMPTY_ELEMENTDATA区分开来,
 * 以便在添加第一个元素时知道要扩容多少。
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * 存储ArrayList元素的数组缓冲区。 ArrayList的容量是此数组缓冲区的长度。
 * 添加第一个元素时,任何带有elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA的
 * 空ArrayList都将扩展为DEFAULT_CAPACITY。
 */
transient Object[] elementData; // 非私有,以简化嵌套类访问

/**
 * ArrayList的大小(它包含的元素个数)
 */
private int size;

elementData就是ArrayList底层维护的Object数组(transient4)。可以看到EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA都是由static和final修饰的,这也是一种尽量不浪费资源的设计5

3. 方法

方法无疑是基于elementData的操作,我觉得如果掌握了add这个方法,基本就把ArrayList的结构理清楚了。

// 将指定的元素追加到此列表的末尾。
public boolean add(E e) {
	ensureCapacityInternal(size + 1);  // Increments modCount!!
	elementData[size++] = e;
	return true;
}

// 保证内部容量是充足的。即保证ArrayList内部维护的elementData数组不会发生溢出
private void ensureCapacityInternal(int minCapacity) {
	ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 计算本次操作所需的最小容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
	if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
		return Math.max(DEFAULT_CAPACITY, minCapacity);
	}
	return minCapacity;
}

// 保证明确的容量
private void ensureExplicitCapacity(int minCapacity) {
	// 继承自AbstactList的属性,用来实现fail-fast机制(可参考文末注解)
	modCount++;

	// overflow-conscious code
	// 考虑溢出的代码
	if (minCapacity - elementData.length > 0)
		grow(minCapacity);
}

// 实现elementData数组的自增长
private void grow(int minCapacity) {
	int oldCapacity = elementData.length;
	int newCapacity = oldCapacity + (oldCapacity >> 1); // oldCapacity >> 1右移一位相当于oldCapacity/2
	if (newCapacity - minCapacity < 0)
		newCapacity = minCapacity;
	if (newCapacity - MAX_ARRAY_SIZE > 0)
		newCapacity = hugeCapacity(minCapacity);
	// minCapacity总是接近于size,所以到此为止:
	elementData = Arrays.copyOf(elementData, newCapacity);
}

可以看出grow方法是add操作最关键的一步,增长因子为1.56
另,MAX_ARRAY_SIZE为Integer.MAX_VALUE - 8,hugeCapacity方法是为了限制分配更大的数组而导致OutOfMemoryError;Arrays.copyOf方法的底层是本地方法System.arraycopy。(从源数组某个位置开始复制指定个数元素到目标数组,同时需指定目标数组的接收起始位置)

/**
 * @param      src      源数组
 * @param      srcPos   源数组的起始位置
 * @param      dest     目标数组
 * @param      destPos  目标数组的起始位置
 * @param      length   准备复制的元素个数
 */
public static native void arraycopy(Object src,  int  srcPos,
								Object dest, int destPos,
								int length);

其他方法相比就较为简单,在此记录下我平时比较少用但又觉得必须掌握的几个方法:

  • trimToSize 最小化ArrayList实例的存储,使内部elementData数组的大小等于size
  • ensureCapacity 在添加大量元素之前,可以使用这个方法主动扩充ArrayList的容量,相比频繁的自动增长肯定效率要高
  • toArray 充当数组和集合API之间的桥梁
  • clear 清空列表所有元素(将每个元素置为null)

注:

  • 1:fail-fast 快速失败机制
    通过该类的iterator()和listIterator(int)方法返回的迭代器具有fail-fast机制:
    不管什么时候,如果在迭代器创建之后发生列表的结构修改(除非通过迭代器自己创建的remove()或者add(Object)方法),迭代器会抛出ConcurrentModificationException。
// expectedModCount为迭代器创建时对modCount的拷贝。
int expectedModCount = modCount;
......
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

因此,在并发修改的情况下,迭代器快速利索地失败,而不是在 未来某个不确定的时间发生不确定的行为上随意冒险。
请注意,迭代器的fail-fast机制无法保证,一般来说,在非同步并发修改的情况下不可能做出任何严格的保证。迭代器的fail-fast机制会尽可能的抛出ConcurrentModificationException。因此,编写依赖于此异常的程序以确保其正确性是错误的,迭代器的快速失败行为应该仅用于检测错误。

  • 2:amortized constant time 分摊后恒定的时间
    比如说new一个ArrayList,前10次add操作所需时间是恒定的,第11次add操作时发现容量不够了需要扩容。我们发现前n次操作的时间总是恒定的,时间复杂度为O(1),但紧接着一次需要O(n),那么我们可以取个平均:O(n/n+1)=O(1),就认为ArrayList的add操作时间在分摊后是恒定的。
  • 3:关于for循环与迭代器iterator的对比,与使用场景相关,可参考:
    https://www.cnblogs.com/redcoatjk/articles/4863340.html
    关于Iterator和ListIterator 迭代器的使用,可参考:
    https://blog.youkuaiyun.com/u013087513/article/details/52232731
  • 4:transient关键字
    被修饰的成员属性变量不会被序列化。细心的朋友一定注意到了ArrayList实现了Serializable接口,这又是咋回事儿呢,最重要的一个属性居然不能被序列化。ArrayList提供了writeObject和readObject这两个方法来实现序列化和反序列化,这么折腾主要是为了节省空间和时间,elementData的数组长度通常是大于ArrayList的实际容量size的,如果直接将elementData序列化就会产生不必要的浪费。 更过细节可参考:
    https://www.jianshu.com/p/14876ef38721
  • 5:DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    当我们使用new ArrayList()返回实例中的elementData实际上都指向{}(DEFAULTCAPACITY_EMPTY_ELEMENTDATA),此时size为0,当第一次使用add方法(或其他添加元素方法)之后,elementData才会指向一个长度为10的数组。而new ArrayList(initialCapacity)则是直接返回一个长度为initialCapacity的数组(只要initialCapacity大于0)。 很多时候我们只是new一个ArrayList用来接收某个方法返回的对象,如果在new的时候就分配一个长度为10的数组就显得有点浪费了。
### Java `ArrayList` 使用指南 #### 创建 `ArrayList` 创建一个 `ArrayList` 可以通过调用其无参构造函数完成。这使得初始化一个能够存储特定类型对象的动态数组变得简单。 ```java ArrayList<String> list = new ArrayList<>(); ``` 此代码片段展示了如何声明并实例化一个新的字符串类型的 `ArrayList` 对象[^1]。 #### 数组转 `ArrayList` 对于已有数据存在于数组中的情况,Java 提供了一个便捷的方式将这些固定大小的数据结构转换成更灵活的 `ArrayList` 类型: ```java String[] array = {"apple", "banana", "orange"}; ArrayList<String> arrayListFromArr = new ArrayList<>(Arrays.asList(array)); ``` 这段代码先定义了一个字符串数组,接着利用 `Arrays.asList()` 方法配合 `ArrayList` 构造器完成了从数组到 `ArrayList` 的转变[^2]。 #### 多线程环境下的同步处理 由于标准的 `ArrayList` 不支持多线程间的并发访问,在需要共享同一个列表的情况下应当考虑对其进行同步保护。可以通过 `Collections.synchronizedList()` 函数来获取经过同步增强后的版本: ```java List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>(list)); ``` 上述语句返回的是被同步过的 `List` 接口实现类的对象,从而确保了在多个线程间的安全性[^3]。 #### 基础操作演示 下面是一些常见的针对 `ArrayList` 进行的操作示例,包括添加元素、移除指定位置上的项以及遍历整个集合等动作。 - **增加新成员** ```java list.add("grape"); ``` - **删除某个索引处的内容** ```java int indexToRemove = 0; String removedItem = list.remove(indexToRemove); System.out.println("Removed item was: " + removedItem); ``` - **迭代打印所有条目** ```java for (String fruit : list) { System.out.println(fruit); } ``` 以上就是有关于 `ArrayList` 的一些基础知识点介绍及其实际应用案例说明。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值