手撕ArrayList

ArrayList 能够存储元素,并支持按索引访问。它的底层结构是数组,那么它的扩容策略是怎样?

初始化

查看构造函数:

	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(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

在执行构造函数时:

  1. 指定合法的初始化容量值,则会立即创建数组;
  2. 未指定合法的初始化容量值,则将数组赋予给一个带有语义的空数组-尚未初始化过的空数组。
  3. 指定集合作为构造函数参数,则将数组赋予给一个带有语义的空数组-已经初始化过的空数组。

2,3 的做了区分的主要用处是在于添加第一个元素时,能够知道要初始化的大小。并且自己指定初始化容量大小,尽量别小于默认值 (10),除非你认为存储元素数量就是很小。


既然构造时,未初始化,那么在添加时,肯定会进行检测,查看 add 方法:

    public boolean add(E e) {
        // 确认容量
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

	 private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
	
	// 计算所需最小容量
	private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 仅仅当尚未初始化过时,才可能得到赋予默认容量大小的机会(上文所讲的2,3的区分的作用)
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

	private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

	private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        // 扩容为原数组的 1.5 倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 如果新的容量大小仍然小于要求的最小容量,则使用所要求的最小容量作为新的容量大小
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 最大为 Integer.MAX_VALUE (2的31次方减一)
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        // 复制数组,没有元素的索引位置用 null 填充
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

所以,扩容策略为原始大小的 1.5 倍,add 方法还有一个重载方法:

	public void add(int index, E element) {
        // 索引检测
        rangeCheckForAdd(index);
		
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 数组的当前索引所在的元素以及其后的所有元素都向后移动一个位置
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

将指定的元素插入此列表中的指定位置。将当前在该位置的元素(如果有的话)和任何后续元素向右移动(向其索引添加一个)。使用这种方式,会导致一次数组复制操作的发生,所以,如果没其它必要,推荐使用 add(E) 方法


存取操作

关于存值操作,还有个 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);
    }

	@SuppressWarnings("unchecked")
	E elementData(int index) {
        return (E) elementData[index];
    }

上面分为两步,大概是因为数组不支持泛型,需要增加 @SuppressWarnings("unchecked") 来告诉编译器忽略指定的警告,而由于在许多方法中都需要从指定索引出取出指定类型的元素,所以提取一个公共方法会更好。


删除操作

删除有 remove(int)remove(Object)

先看 remove(int)

	public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        // 将后续元素向前移动一个位置
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

再看看 remove(object)

	public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

上述操作大概分为两步:

  1. 找到元素所在位置
  2. 移除该元素,重新调整数组(相当于一个压缩的过程)

:由于这里的重载,可能导致你在使用 Integer 作为存储元素时,错误的调用了该方法。例如,你想删除 元素值为 2 的元素,你写了下列语句,并期望得到正确的结果:

list.remove(2);

实际上,上述调用将删除索引为 2 的元素;你的修改语句如下,才能得到正确结果。

list.remove(new Integer(2));


迭代器

List 支持两种迭代器,一种是基本的迭代器,一种是基本的 Iterator,一种是 ListIteratorListIterator 拥有更丰富的功能,支持向前遍历,并支持遍历过程中支持 add 或者 set 元素,关于 add 或者 set 有诸多限制,使用过程中应仔细浏览文档,确保正确使用了这些操作。

关于迭代器的源码,就不在这里列出;需要提醒的是,迭代器接口在 1.8 版本,新增了 forEachRemaining(Consumer<? super E> action) 方法,该方法能够帮助我们简化使用迭代器遍历时的代码:

// 原始:
while(iterator.hasNext()){
    System.out.println(iterator.next());
}

// 现在:
iterator.forEachRemaining(System.out::println); // 方法引用

关于 1.8 新增的 Lambda 表达式非常好用。


总结

  1. ArrayList 底层数据结构是数组;
  2. 默认扩容策略是 1.5 倍,最大存储容量是 Integer.MAX_VALUE
  3. ArrayList 基于索引的操作(新增,删除),都会触发数组的复制;
  4. ArrayList 还支持特殊的 ListIterator 迭代器,该迭代器支持向前遍历,并支持新增元素等操作。

推荐博文


手撕LinkedList


我与风来


认认真真学习,做思想的产出者,而不是文字的搬运工
错误之处,还望指出

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值