java集合框架(五):ArrayList

开发十年,就只剩下这套架构体系了! >>>   hot3.png

        java集合框架中被使用最多的可能就是ArrayList了,用它代替数组确实给我们开发带来了很大的便利。很多人都知道它是自动扩容的数组,使用的时候不用考虑数组的容量大小,可以往里面添加无数的元素,这样的理解是否正确,我们通过源码来分析。

        java集合框架包括了很多接口和类,为了最大程度地提高代码的复用性和易用性,作者是运用了一些软件设计原则或者设计模式,我们可以从下面ArrayList的类图中看到其中的妙处。

174158_kpSu_3092714.png

        从上图中,可以看到ArrayList是接口Collection和接口List的实现类,这里运用了对接口编程的设计原则(也可以说是依赖倒置)。这里也可以符合开闭原则,就是假如要添加一个新的集合类,可以让它直接实现Collection接口。这里还考虑了代码复用的原则,AbstractCollection,AbstractList封装了部分共用的代码,子类只要继承这些抽象类就可以获得,简化了编程。

一、类的定义

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 序列化id
    private static final long serialVersionUID = 8683452581122892189L;

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

    // 空数组对象
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 数组对象,就是存储ArrayList元素的数组
    transient Object[] elementData;

    // ArrayList元素个数
    private int size;
}

        从以上代码和类图中可以看到,ArrayList继承自AbstractList,实现了List接口。其中父类AbstractList中也是实现了List的,为什么还要在ArrayList中实现List接口呢?这个估计是作者为了让ArrayList支持动态代理,具体可以点击这里查看原因分析。

        从代码中也能看到,ArrayList其实就是Object[]数组,对ArrayList的操作其实就是对数组的操作,其数据结构如下所示:

192949_kylX_3092714.png

二、构造器

// 无参构造器
public ArrayList() {
    super();
    // 把数组赋值为空数组
    this.elementData = EMPTY_ELEMENTDATA;
}
// 把其他集合的元素加到ArrayList
public ArrayList(Collection<? extends E> c) {
        // 把c返回的数组直接加到ArrayList数组中
        elementData = c.toArray();
        size = elementData.length;
        // 如果c.toArray不是返回Object[],再做进一步处理,转为Object[]
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
}
// 指定数组的初始容量
public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        // 根据指定的大小创建数组
        this.elementData = new Object[initialCapacity];
}

三、存储的实现

1.add(E e)方法。往数组后面插入元素,保持先进先出的特性。

// 把新元素加到ArrayList
public boolean add(E e) {
        // 检查是否需要扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 新元素加入数组
        elementData[size++] = e;
        return true;
}
// 保证数组容量
private void ensureCapacityInternal(int minCapacity) {
        // 如果数组为空数组,则数组容量取默认容量和需要插入位置的最大值
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        // 确保数组容量
        ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
        // fast-fail机制
        modCount++;

        // 如果插入位置超过数组容量就扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
}
// 扩容方法
private void grow(int minCapacity) {      
        int oldCapacity = elementData.length;
        // 扩容新容量是旧容量的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 如果1.5倍不够大,就使用指定的容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 如果新容量超过了设定的最大数组大小,则新容量使用int的最大值,hugeCapacity方法返回的最大值为int的最大值,
        // 因此ArrayList的最大容量也就是int的最大值
        // MAX_ARRAY_SIZE=Integer.MAX_VALUE - 8
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 扩容操作,把就数组的数据复制到新数组,比较耗性能
        elementData = Arrays.copyOf(elementData, newCapacity);
}

2.add(int index, E element)方法。指定位置插入元素。

public void add(int index, E element) {
        // 检查index是否在数组范围
        rangeCheckForAdd(index);
        // 确保数组有足够的容量
        ensureCapacityInternal(size + 1);
        // index位置后面的元素往后移动
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // index位置插入新元素
        elementData[index] = element;
        // ArrayList大小加一
        size++;
}
private void rangeCheckForAdd(int index) {
        // 如果index不在数组范围内抛出异常
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

3.get(int index)方法。根据index获取ArrayList元素。

public E get(int index) {
        // index范围检查
        rangeCheck(index);
        // 直接返回数组对应元素
        return elementData(index);
}
E elementData(int index) {
        return (E) elementData[index];
}

4.remove(int index)方法。根据index删除元素,返回删除的元素。

public E remove(int index) {
        // 检查index范围
        rangeCheck(index);
        // fast-fail机制
        modCount++;
        // 要删除的元素
        E oldValue = elementData(index);

        // index后面的元素往前移动,耗性能
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 数组最后的元素赋值为null,以便垃圾回收器回收。并且size减一
        elementData[--size] = null;
        // 返回删除的元素,返回null表示index位置为null
        return oldValue;
}

5.remove(Object o)方法。删除指定的对象。

public boolean remove(Object o) {
        // 删除的对象为空
        if (o == null) {
            // 在数组范围内查询是否有为空的元素,有就删除
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    // 删除指定位置的元素
                    fastRemove(index);
                    // 数组有指定的对象,并删除完毕返回true
                    return true;
                }
        } 
        // 删除的对象不为空
        else {
            for (int index = 0; index < size; index++)
                // 根据equals方法判断指定对象是否在数组中,所以如果数组里面的元素是我们自定义的对象,最好重写equals方法
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        // 数组中没有指定删除的对象返回false
        return false;
}
// 快速删除元素方法,省去了判断index范围,不用返回删除的元素
private void fastRemove(int index) {
        // fast-fail机制
        modCount++;
        // index后面的元素往前移动
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null;
}

6.size()方法。直接返回数组中装载元素的大小,注意这里是数组中被使用的容量大小,而不是整个数组容量的大小。

public int size() {
        return size;
}

7.其他存储操作方法。

  • boolean addAll(Collection<? extends E> c) :把参数传进来的集合加到数组中,如果添加的元素大于0返回true,否则返回false
  • boolean addAll(int index, Collection<? extends E> c) :在指定位置添加元素,如果添加的元素大于0返回true,否则返回false
  • E set(int index, E element) :设定index位置的元素值,返回原来index位置的值
  • boolean removeAll(Collection<?> c) :在数组中删除集合c中包含的元素,如果有删除元素则返回true,否则返回false
  • void removeRange(int fromIndex, int toIndex) :删除指定范围内的元素
  • boolean retainAll(Collection<?> c) :保留参数集合c中有的元素,如果数组中元素个数发生变化返回true,否则返回false

四、判断对象是否存在

         判断ArrayList中是否包含了对象是用contains方法,这个方法在主接口Collection中就有定义,实现类可以自定义实现。

public boolean contains(Object o) {
        // 数组中包含对象返回true,否则返回false
        return indexOf(o) >= 0;
}
public int indexOf(Object o) {
        // 如果为空,就判断size范围内是否有插入空对象
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } 
        // 不为空的话遍历数组,用equals方法判断是否有相等的对象
        else {
            for (int i = 0; i < size; i++)
                // 有相等的对象就返回index值
                if (o.equals(elementData[i]))
                    return i;
        }
        // 数组里没有相等的值,返回-1
        return -1;
}

五、迭代器实现

        ArrayList的迭代器是在内部类Itr中实现的,这样实现的好处是只要不改变元素,ArrayList可以同时提供多个迭代器给客户端遍历元素。此外,迭代器要求每次调用的remove方法之前需要先调用next方法,在源码中可以看到这种限制。

public Iterator<E> iterator() {
        return new Itr();
}
private class Itr implements Iterator<E> {
        int cursor;       // 迭代器游标
        int lastRet = -1; // 上一次遍历的index位置,返回-1表示没有上一次位置
        int expectedModCount = modCount; // fast-fail机制,如果迭代过程中数组有变动会抛异常

        public boolean hasNext() {
            // 游标没有到最后返回true
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            // 检测数组是否有变动
            checkForComodification();
            int i = cursor;
            // 如果游标大于等于数组装载元素大小则抛出异常
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            // 如果游标大于等于数组容量则抛出异常
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            // 游标加1
            cursor = i + 1;
            // 返回当前遍历的元素
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            // 确保调用remove方法之前要先调用next方法
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                // 删了元素的话lastRet为-1,要再次删除的话就要先调用next方法了
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
        
        // 检查数组是否有改变
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
}

六、总结

        ArrayList的内部实现是数组,在使用过程中,我们一般不用考虑ArrayList的容量大小,因为它会自动扩容,但是扩容不是无限扩容的。数组的好处是方便随机访问每个元素,也可以提供“先进先出”的特性。从上文的分析中,可以对ArrayList做个总结:

  1. 扩容机制。ArrayList默认容量是10,也可以在构造器中指定容量(如果能够预计元素的总数最好指定容量,这样就省去了每次扩容的开销),当元素的个数大于数组容量后会进行扩容,扩容到原来的1.5倍。不是无限扩容,最多扩到int的最大值。
  2. 线程安全。ArrayList不是线程安全的,应该避免多线程去改变ArrayList。
  3. 两个对象的比较。判断ArrayList是否包括给定的对象是基于对象的equals方法作比较,如果是自定义的对象最好重写超类的equals方法。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值