【源码系列】Java中的数据结构——数组与ArrayList2

简介: 【源码系列】Java中的数据结构——数组与ArrayList

2.3.4 get方法

我们知道ArrayList实际上就是数组的封装,所以对于具有随机访问的数组而言,根据下标访问元素实在太简单不过了。


public E get(int index) {
  //下标安全检查
       rangeCheck(index);
       return elementData(index);
}


2.3.5 set方法

修改index下标的元素为element


public E set(int index, E element) {
  //下标检查
        rangeCheck(index);
  //旧的值
        E oldValue = elementData(index);
        //更新值
        elementData[index] = element;
        //返回旧的值
        return oldValue;
    }


2.3.6 size方法

public int size() {
        return size;
    }


其实就是把size返回,但这里要注意的是size的意义,它不是指当前ArrayList的容量有多少,而是指当前ArrayList中有多少元素。


2.3.7 indexOf方法

public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }


该方法就是遍历查找是否有对象o,如果找到则返回下标。


2.3.8 toArray方法

public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

需要注意的是该方法是将其elementData对象数组从内存中复制了一份,而且长度就是元素的数量。所以不必担心修改toArray方法的得到的数组会影响原ArrayList对象。


2.3.9 removeAll和retainAll方法

①removeAll(Collection c)

删除ArrayList中所有和集合中相同的元素

public boolean removeAll(Collection<?> c) {
  //检查该集合是否为空
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }


该方法的核心在于boolean batchRemove(Collection c, boolean complement)方法,我们点进去看一下,


private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData;
        //r为快指针,w为慢指针
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement)
                  //只有符合条件时,w才自增
                    elementData[w++] = elementData[r];
        } finally {
            // 这里正常情况都是r=size,只有在程序遇到异常时,才可能会遇到r!=size的情况。
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            //当w!=size时,即有元素被抛弃了,这时需要把后面无用的元素值置为null
            if (w != size) {
                // 将后面的元素值置为null
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                //modcount加上修改过的元素
                modCount += size - w;
                //更新size
                size = w;
                //此时已经抛弃了一些元素,所以置为true
                modified = true;
            }
        }
        return modified;
    }


可以看到这里用了快慢指针的技巧,当遇到要保留的元素时w再自增,最终保留元素的下标便是0到w-1。


在遍历的过程中调用contains方法看看这个元素是否在集合c中。


这里巧妙的地方在于它把contains的返回值complement参数比较。


这样,Boolean类型的complement参数的作用就类似于一个开关,当为true时,这个函数就是保留在集合c中出现过的元素,否则保留未出现过的元素。


②retainAll(Collection c)

而retainAll也类似,只不过它把开关置为false,这样就会保留在集合c中出现过的元素。


public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, true);
    }

2.4 迭代器

2.4.1 普通迭代器Itr

private class Itr implements Iterator<E> {
  //下一个元素下标
        int cursor;       // index of next element to return
        //上一个元素下标
        int lastRet = -1; // index of last element returned; -1 if no such
        //期望修改次数(后面会讲,现在不用管)
        int expectedModCount = modCount;
        Itr() {}
  //判断是否有下一个
        public boolean hasNext() {
            return cursor != size;
        }
  //返回当前停留的元素
        @SuppressWarnings("unchecked")
        public E next() {
          //检查是否有修改,这个之后会讲,现在先不管它
            checkForComodification();
            int i = cursor;
            //如果此时下一次超出数组范围,则抛出没有NoSuchElementException异常
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            //如果此时i还能大于等于数组的长度,唯一的可能就是在遍历过程中有程序对其做了修改,所以此时应抛出并发异常ConcurrentModificationException
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            //下一个元素的下标为i+1
            cursor = i + 1;
            //返回下标为i的元素
            return (E) elementData[lastRet = i];
        }
        public void remove() {
          //如果当前的元素下标小于0,抛出IllegalStateException异常
            if (lastRet < 0)
                throw new IllegalStateException();
            //检查是否有修改
            checkForComodification();
            try {
              //删除上一个元素
                ArrayList.this.remove(lastRet);
                //指针回退
                cursor = lastRet;
                //上一个指针回归-1,这个就说明了不能连续调用remove方法
                lastRet = -1;
                //期望的修改值变为当前的修改值(这些之后会讲)
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
}



这里有个值得注意的地方——它在定义的迭代器的时候加了个private,那我们如何去创建迭代器呢?


其实它有个Iterator()方法,


public Iterator<E> iterator() {
        return new Itr();
    }


它就返回了一个迭代器的对象。


这样的好处就在于面对不同的集合类只需调用通用的接口方法iterator()便可以返回相应的迭代器。


看完上面的源码我们明白了以下几点:

1. 这个迭代器对象只能使用一次,因为它没有初始化重置的方法

2. 这个迭代器只会往前,因为它只有next方法

3. 不能连续调用remove方法,因为调用了一次后,指向上一个元素下标的lastRet变量便被赋值为-1,连续调用会抛出异常


看完上面这个迭代器的源码,你会发现——初看源码会有一种不知所云的感觉,但是去掉这些让我看起来不知所云的操作和边界条件的检查后,是不是感觉简单了起来?


没错,如果不管这些边界条件,不管并发可能带来的错误,那么我们也能写出ArrayList的迭代器。


这里,为了更好理解代码,我这里放上该迭代器使用简单例子

Iterator<Integer> iterator=list.iterator();
while (iterator.hasNext()){
    Integer i=iterator.next();
    iterator.remove();
}

看了源码后是不是更有感觉了呢?


2.4.2 迭代器的增强版ListItr

private class ListItr extends Itr implements ListIterator<E> {
        ListItr(int index) {
            super();
            cursor = index;
        }
        public boolean hasPrevious() {
            return cursor != 0;
        }
        public int nextIndex() {
            return cursor;
        }
        public int previousIndex() {
            return cursor - 1;
        }
        @SuppressWarnings("unchecked")
        public E previous() {
          //检查是否有修改
            checkForComodification();
            //cursor前移
            int i = cursor - 1;
            //检查i是否小于0
            if (i < 0)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            //如果此时i还能大于等于数组的长度,唯一的可能就是在遍历过程中有程序对其做了修改,所以此时应抛出并发异常ConcurrentModificationException
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i;
            return (E) elementData[lastRet = i];
        }
        public void set(E e) {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
    //修改当前下标的元素值
            try {
                ArrayList.this.set(lastRet, e);
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
        public void add(E e) {
            checkForComodification();
            try {
                int i = cursor;
                ArrayList.this.add(i, e);
                cursor = i + 1;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
    }


可以看到ListItr是Itr的子类,在有Itr的功能的基础上,拓展了前移指针的previous()方法、增加元素的add(E e)方法和修改当前元素的方法set(E e)。源码操作也不难理解,这里就不多介绍了。


2.4.3 干扰检测

①为什么要有干扰检测

在看迭代器的源码,你是不是很疑惑为啥会有执行checkForComodification()检查?

你是不是很疑惑为什么很多地方会有modCount++,expectedModCount = modCount这种操作?

你是不是很疑惑modCount,expectedModCount 是来用来干什么的呢?


带着这些疑问,我们来探究一下这样做的原因和原理。


首先,不知道你在写程序的时候遇到ConcurrentModificationException()这个异常,在我们用迭代器遍历过程中,有没有删除增加过集合。比如下面这样:


for (Integer i:list){
      list.add(1);
}


注意:ArrayList的foreach增强遍历本质就是调用了迭代器。


我们在遍历途中不断向集合中添加整数1,这看似好像没有什么问题,可仔细一想,如果不断添加,那么这个迭代器调用的next真的是原来所期望的next吗?看似好像不会影响结果,但实际上这往往会发生一些逻辑上的错误,如果不对这些情况做出一些提示,程序员会非常苦恼,因为他们很难发现这里面的问题。


同时在并发编程中,如果你对共享变量误用了非线程安全的ArrayList,那么有个线程正在遍历ArrayList集合,可以另一个线程对其做了修改,这会不会发生一些错误呢?尽管在这种情况下ArrayList并未做出有效解决方案,但它必须有责任告知你这个情况,所以它会抛出这个ConcurrentModificationException异常。


②干扰检测原理

那么它是如何实现的呢?


核心就modcount和expectedModCount 变量。


还记得我们调用add,remove方法出现modcount++操作吗?



q4.png


q3.png

在这里modcount就是累计修改的次数,expectedModCount 就是预期修改的次数。


这里要注意的是modcount是全局的一个属性


protected transient int modCount = 0;


而expectedModCount 是在迭代器内部的一个属性。


q2.png

言外之意就是该错误检测一定是在迭代器内部实现的。


阅读源码我们发现,在ArrayList每次进程执行add,remove方法时,都会触发一次modcount++。


同时在创建迭代器时,expectedModCount 的初值就被赋值为当时的modcount,

q1.png

而每次迭代器在进行next和remove方法时都会做一次检测


q1.png

点开方法查看

final void checkForComodification() {
       if (modCount != expectedModCount)
           throw new ConcurrentModificationException();
}


这个方法实质就是在比较modCount和expectedModCount是否相等,如果不相等就抛出ConcurrentModificationException异常。


检查完后,我们对集合进行修改时,如执行remove方法,那么就会modcount++,经过几步操作后在把modcount的值赋值给expectedModCount ,这是就保证了expectedModCount 与modcount是相等的,但是预期之外的修改可不会执行这句话,所以就会造成modCount和expectedModCount值不一致,从而检查出了错误。


到这里我们终于明白了它的核心思路:

在迭代器里的修改(主要是指add和remove方法)是被允许的,因为这是迭代器内可见的。但是在预期之外的修改是不被允许的,一旦迭代器发现了预期之外的修改(执行checkForComodification()方法检查modCount和expectedModCount是否相等),那么迭代器就会抛出ConcurrentModificationException异常告诉工程师——啊,我迭代过程中出现问题了,可能会发生一些逻辑错误。


到现在你也明白了这样做的原因和原理,那么我问你个问题——你觉得这个过程完美吗?


看着很完美是吗?

其实不是的,实际上你仔细看看这段

ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;

modCount修改到expectedModCount被更新这里中间差了这么多步,那我们假设在这个过程中其他线程修改了集合,这时modcount又被更新了,但是expectedModCount 还没更新啊。当expectedModCount 更新时,它以为它更新的值是预期中的,但是实际上有预期外的修改混了进来,而它却浑然不知。


所以说,ArrayList是个非线程安全的类,即使在迭代过程中,它有着能感知这种意外的机制,但是这种机制并不是那么灵敏,在一些情况下,它检测不出并发错误。(当然啦,这个机制最主要的是检查工程师在遍历过程中误修改操作,对于并发错误的检测也只是顺带的罢了)


当然啦,源码中也有这么一段注释:


/*
         * If ArrayLists were immutable, or structurally immutable (no
         * adds, removes, etc), we could implement their spliterators
         * with Arrays.spliterator. Instead we detect as much
         * interference during traversal as practical without
         * sacrificing much performance. We rely primarily on
         * modCounts. These are not guaranteed to detect concurrency
         * violations, and are sometimes overly conservative about
         * within-thread interference, but detect enough problems to
         * be worthwhile in practice. To carry this out, we (1) lazily
         * initialize fence and expectedModCount until the latest
         * point that we need to commit to the state we are checking
         * against; thus improving precision.  (This doesn't apply to
         * SubLists, that create spliterators with current non-lazy
         * values).  (2) We perform only a single
         * ConcurrentModificationException check at the end of forEach
         * (the most performance-sensitive method). When using forEach
         * (as opposed to iterators), we can normally only detect
         * interference after actions, not before. Further
         * CME-triggering checks apply to all other possible
         * violations of assumptions for example null or too-small
         * elementData array given its size(), that could only have
         * occurred due to interference.  This allows the inner loop
         * of forEach to run without any further checks, and
         * simplifies lambda-resolution. While this does entail a
         * number of checks, note that in the common case of
         * list.stream().forEach(a), no checks or other computation
         * occur anywhere other than inside forEach itself.  The other
         * less-often-used methods cannot take advantage of most of
         * these streamlinings.
         */



翻译过来就是:


如果 ArrayLists 是不可变的,或者结构上不可变的(没有添加、删除等),我们可以使用 Arrays.spliterator 实现它们的拆分器。相反,我们在不牺牲太多性能的情况下,在遍历过程中检测到尽可能多的干扰。我们主要依赖 modCounts。这些不能保证检测到并发冲突,并且有时对线程内干扰过于保守,但是可以检测到足够的问题以值得实践。为了实现这一点,我们(1)延迟初始化fence和expectedModCount,直到我们需要提交到我们正在检查的状态的最新点;从而提高精度。 (这不适用于创建具有当前非惰性值的拆分器的子列表)。 (2) 我们在 forEach 的末尾只执行一个ConcurrentModificationException 检查(对性能最敏感的方法)。当使用 forEach(而不是迭代器)时,我们通常只能在动作之后检测干扰,而不是之前。进一步的 CME触发检查适用于所有其他可能违反假设的情况,例如 null 或给定 size() 的太小的 elementData数组,这可能仅由于干扰而发生。这允许 forEach 的内部循环在没有任何进一步检查的情况下运行,并简化了 lambda解析。虽然这确实需要一些检查,但请注意,在 list.stream().forEach(a) 的常见情况下,除了 forEach本身之外,不会在任何地方进行检查或其他计算。其他不太常用的方法不能利用这些流线化中的大部分。


2.4.4 ArrayListSpliterator拆分迭代器

这个拆分迭代器时jdk8引入的,是为了满足流式编程的需要,用于并行流的拆分,这块我之后再补。(未完待续…)


static final class ArrayListSpliterator<E> implements Spliterator<E> {
        /*
         * If ArrayLists were immutable, or structurally immutable (no
         * adds, removes, etc), we could implement their spliterators
         * with Arrays.spliterator. Instead we detect as much
         * interference during traversal as practical without
         * sacrificing much performance. We rely primarily on
         * modCounts. These are not guaranteed to detect concurrency
         * violations, and are sometimes overly conservative about
         * within-thread interference, but detect enough problems to
         * be worthwhile in practice. To carry this out, we (1) lazily
         * initialize fence and expectedModCount until the latest
         * point that we need to commit to the state we are checking
         * against; thus improving precision.  (This doesn't apply to
         * SubLists, that create spliterators with current non-lazy
         * values).  (2) We perform only a single
         * ConcurrentModificationException check at the end of forEach
         * (the most performance-sensitive method). When using forEach
         * (as opposed to iterators), we can normally only detect
         * interference after actions, not before. Further
         * CME-triggering checks apply to all other possible
         * violations of assumptions for example null or too-small
         * elementData array given its size(), that could only have
         * occurred due to interference.  This allows the inner loop
         * of forEach to run without any further checks, and
         * simplifies lambda-resolution. While this does entail a
         * number of checks, note that in the common case of
         * list.stream().forEach(a), no checks or other computation
         * occur anywhere other than inside forEach itself.  The other
         * less-often-used methods cannot take advantage of most of
         * these streamlinings.
         */
        private final ArrayList<E> list;
        private int index; // current index, modified on advance/split
        private int fence; // -1 until used; then one past last index
        private int expectedModCount; // initialized when fence set
        /** Create new spliterator covering the given  range */
        ArrayListSpliterator(ArrayList<E> list, int origin, int fence,
                             int expectedModCount) {
            this.list = list; // OK if null unless traversed
            this.index = origin;
            this.fence = fence;
            this.expectedModCount = expectedModCount;
        }
        private int getFence() { // initialize fence to size on first use
            int hi; // (a specialized variant appears in method forEach)
            ArrayList<E> lst;
            if ((hi = fence) < 0) {
                if ((lst = list) == null)
                    hi = fence = 0;
                else {
                    expectedModCount = lst.modCount;
                    hi = fence = lst.size;
                }
            }
            return hi;
        }
        public ArrayListSpliterator<E> trySplit() {
            int hi = getFence(), lo = index, mid = (lo + hi) >>> 1;
            return (lo >= mid) ? null : // divide range in half unless too small
                new ArrayListSpliterator<E>(list, lo, index = mid,
                                            expectedModCount);
        }
        public boolean tryAdvance(Consumer<? super E> action) {
            if (action == null)
                throw new NullPointerException();
            int hi = getFence(), i = index;
            if (i < hi) {
                index = i + 1;
                @SuppressWarnings("unchecked") E e = (E)list.elementData[i];
                action.accept(e);
                if (list.modCount != expectedModCount)
                    throw new ConcurrentModificationException();
                return true;
            }
            return false;
        }
        public void forEachRemaining(Consumer<? super E> action) {
            int i, hi, mc; // hoist accesses and checks from loop
            ArrayList<E> lst; Object[] a;
            if (action == null)
                throw new NullPointerException();
            if ((lst = list) != null && (a = lst.elementData) != null) {
                if ((hi = fence) < 0) {
                    mc = lst.modCount;
                    hi = lst.size;
                }
                else
                    mc = expectedModCount;
                if ((i = index) >= 0 && (index = hi) <= a.length) {
                    for (; i < hi; ++i) {
                        @SuppressWarnings("unchecked") E e = (E) a[i];
                        action.accept(e);
                    }
                    if (lst.modCount == mc)
                        return;
                }
            }
            throw new ConcurrentModificationException();
        }
        public long estimateSize() {
            return (long) (getFence() - index);
        }
        public int characteristics() {
            return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED;
        }
    }



2.4.5 迭代器总结

除了拆分迭代器,ArrayList中的迭代器其实很简单。

如果没有那些边界检查,没有并发错误检测,就算作为初学者的我们也能出色的完成任务。但是作为jdk类库中最常用的集合类,它必须是可靠的,它必须有着极高的性能,同时拥有较高的安全性。


迭代器作为遍历集合的手段,它可以在遍历的过程中不断遍历同时对元素做出修改。在这个过程中它会尽量避免一些错误的发生。


Itr是普遍的迭代器,它只能单向遍历,同时遍历完后就没用了,也无法连续调用remove方法。

ListItr作为Itr的增强版(也是子类),在其基础上拓展了双向遍历的功能,同时支持增删改的操作。


四、总结

总结一下,

ArrayList是对数据结构中数组的封装,其核心是就是对象数组elementData。它最大的特点就是让数组看起来是可以动态扩容的,并且提供各种方便的操作,极大提升了我们日常开发的效率。


对于ArrayList,你需要记住以下几点:


ArrayList的核心是对象数组elementData,它是存放数据的地方。

size并不代表这数组的容量,而是指当前list中元素的数量。

由于elementData数组并未对外开放,你无法直接获取elementData数组,除非你用反射,同时容量也无法直接获取。

ArrayList是看起来支持动态扩容,实际依旧摆脱不了数组的特性——创建容量即确定。扩容原理就是调用System.arrayCopy方法进行数组复制,把原来数组的内容复制到另外一个容量更大的数组里。

ArrayList创建时最好给它一个初始容量来避免后续的扩容。

ArrayList适用于查询多,增删操作少的情况。

在add方法执行后,如果容量不足它会自动扩容,一般情况下扩容后的容量时原来容量的3/2倍

迭代器为了保证迭代过程中不出现预期之外的修改,会进行检查。如果出现了预期之外的修改,会抛出ConcurrentModificationException异常。

整篇源码读下来,其实好处还是很多的,

在对外方法的暴露上,在对内代码的封装和复用上,在方法间的调用上,你可以发现各种设计模式,感叹设计的巧妙。


如此复杂的方法调用在这里被组织的井然有序,这是我们日常开发中需要好好学习的。


好了,这篇的内容到这里就差不多结束了,当然本文也并非讲解所有的源码,只是挑了核心原理的一部分来讲,至于其他方法,个人认为是一些其他方面的封装,这里我就不一一赘述了,感兴趣的同学可以自己去研读

相关文章
|
22天前
|
存储 Java
Java数组07:稀疏数组
【8月更文挑战第23天】
25 2
|
14天前
|
Kubernetes jenkins 持续交付
从代码到k8s部署应有尽有系列-java源码之String详解
本文详细介绍了一个基于 `gitlab + jenkins + harbor + k8s` 的自动化部署环境搭建流程。其中,`gitlab` 用于代码托管和 CI,`jenkins` 负责 CD 发布,`harbor` 作为镜像仓库,而 `k8s` 则用于运行服务。文章具体介绍了每项工具的部署步骤,并提供了详细的配置信息和示例代码。此外,还特别指出中间件(如 MySQL、Redis 等)应部署在 K8s 之外,以确保服务稳定性和独立性。通过本文,读者可以学习如何在本地环境中搭建一套完整的自动化部署系统。
43 0
|
22天前
|
Java
|
17天前
|
Java
Java数组的应用场景
Java数组的应用场景
28 1
|
17天前
|
存储 机器学习/深度学习 Java
Java数组
Java数组
23 1
|
22天前
|
存储 Java
如何在 Java 中打印字符串数组列表
【8月更文挑战第23天】
26 2
|
22天前
|
存储 Java API
|
22天前
|
存储 安全 Java
Java 中数组和 ArrayList 的区别
【8月更文挑战第23天】
27 1
|
24天前
|
Java 索引
Java系列 之 Java复制(拷贝)数组的4种方法:arraycopy()方法、clone() 方法、copyOf()和copyOfRan
这篇文章介绍了Java中数组复制的四种方法:`Arrays.copyOf()`、`Arrays.copyOfRange()`、`System.arraycopy()`和`clone()`方法,以及它们的使用场景和示例代码。
|
14天前
|
存储 开发者 C#
WPF与邮件发送:教你如何在Windows Presentation Foundation应用中无缝集成电子邮件功能——从界面设计到代码实现,全面解析邮件发送的每一个细节密武器!
【8月更文挑战第31天】本文探讨了如何在Windows Presentation Foundation(WPF)应用中集成电子邮件发送功能,详细介绍了从创建WPF项目到设计用户界面的全过程,并通过具体示例代码展示了如何使用`System.Net.Mail`命名空间中的`SmtpClient`和`MailMessage`类来实现邮件发送逻辑。文章还强调了安全性和错误处理的重要性,提供了实用的异常捕获代码片段,旨在帮助WPF开发者更好地掌握邮件发送技术,提升应用程序的功能性与用户体验。
19 0