龙空技术网

一道算法题的思考之List集合

开心果果咚 263

前言:

此刻咱们对“list比较java”大约比较注重,姐妹们都需要学习一些“list比较java”的相关资讯。那么小编在网上收集了一些有关“list比较java””的相关文章,希望咱们能喜欢,朋友们一起来学习一下吧!

上一篇我们通过一道算法题了解了数组,数组有一个明显的缺点是需要指定容量且不能改变,而且我们不能明确数组中存了多少元素。开发中的场景很少明确知道有多少数据,所以就有了比较高级的容器,今天我们来学习List集合中的ArrayList。

java.util.List继承了Collection接口,定义了对元素增删改的方法,由其不同的子类实现,它的所有实现类:AbstractList, AbstractSequentialList, ArrayList, AttributeList, CopyOnWriteArrayList, LinkedList, RoleList, RoleUnresolvedList, Stack, Vector。最常用的子类如下图所示:

今天我们先学习ArrayList,这是开发中最常用的数据结构。

成员变量

//默认的初始化容量private static final int DEFAULT_CAPACITY = 10;//空数组private static final Object[] EMPTY_ELEMENTDATA = {};//默认初始容量空数组private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//ArrayList存放元素的数组容器transient Object[] elementData; // non-private to simplify nested class access//存储的元素的数量private int size;//最大容量private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
构造函数

ArrayList提供了3种构造函数:

ArrayList():构造一个初始容量为DEFAULT_CAPACITY(10)的空列表,其中elementData成员变量初始化为{},不包含任何元素。源码如下:

public ArrayList() {    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}

相信看了源码的你肯定有个疑问,空参构造函数里面不就是给elementData赋值了一个空的数组吗,你怎么就说它是一个初始化容量为10的数组呢?我们后面讲添加元素的时候会讲,在这里它还是一个空数组,只是第一次添加元素的时候会将容量扩充到10.

ArrayList(int initialCapacity):构造指定初始容量的空列表。initialCapacity需满足大于等于0,如果小于0则抛出异常IllegalArgumentException。

public ArrayList(int initialCapacity) {    //指定的容量大于0,则elementData初始化为initialCapacity大小的数组    if (initialCapacity > 0) {        this.elementData = new Object[initialCapacity];    } else if (initialCapacity == 0) {        //指定的容量等于0,则elementData初始化为{},不包含任何元素        this.elementData = EMPTY_ELEMENTDATA;    } else {        //小于0则抛出异常        throw new IllegalArgumentException("Illegal Capacity: "+                                           initialCapacity);    }}

这里要注意initialCapacity=0得到的elementData和我们无参得到的是不一样的,这里得到的是初始容量为0的空数组,毕竟ensureCapacityInternal方法中只对DEFAULTCAPACITY_EMPTY_ELEMENTDATA做了特殊处理,所以虽然我们看到DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA都是{},但是处理得到的结果还是不一样的。

ArrayList(Collection<? extends E> c):构造一个包含指定集合的元素列表。

public ArrayList(Collection<? extends E> c) {    //将构造函数参数列表转换为数组并赋值给elementData    elementData = c.toArray();    //如果元素个数不为0    if ((size = elementData.length) != 0) {        // 如果elementData元素的class类不是Object类型,则重新复制elementData中的元素        if (elementData.getClass() != Object[].class)            elementData = Arrays.copyOf(elementData, size, Object[].class);    } else {        // 元素个数为0,依然把elementData初始为空数组        this.elementData = EMPTY_ELEMENTDATA;    }}
添加元素add

add添加元素方法有很多重载方法,我们先学习比较基础也是比较常用的add(E e),在尾部添加元素。其源码和注释如下:

public boolean add(E e) {    //保证足够的容量,如果有必要则扩容,并且增加modCount    ensureCapacityInternal(size + 1);  // Increments modCount!!    //将添加的元素存在最后,并将size+1    elementData[size++] = e;    return true;}

勾勾在添加注释的时候以无参构造时解释各个变量的值,主要为了解释无参构造时为何初始化了一个容量为10的空数组。

private void ensureCapacityInternal(int minCapacity) {    //判断是不是成员变量DEFAULTCAPACITY_EMPTY_ELEMENTDATA,如果是就取你传入的minCapacity和DEFAULT_CAPACITY的最大值,你传的是什么?size+1=1,所以最大值就是DEFAULT_CAPACITY=10    //如果不是无参构造初始化的elementData,那么minCapacity就是指定的容量,是0它就是0,不会做特殊化    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);    }    ensureExplicitCapacity(minCapacity);}
//我们已经得到了minCapacity为10了,那么此时elementData还是一个{},它的length为0,此时就会进入grow方法private void ensureExplicitCapacity(int minCapacity) {    modCount++;    // 10-0 = 10    if (minCapacity - elementData.length > 0)        grow(minCapacity);}
//这个方法我们又称为扩容 private void grow(int minCapacity) {     // 获取elementData的长度,现在还是0     int oldCapacity = elementData.length;     //0再怎么位运算也还是0     int newCapacity = oldCapacity + (oldCapacity >> 1);     //minCapacity为10,执行减数小于0,得到newCapacity=10     if (newCapacity - minCapacity < 0)         newCapacity = minCapacity;     if (newCapacity - MAX_ARRAY_SIZE > 0)         newCapacity = hugeCapacity(minCapacity);     // elementData就得到了一个长度为10的空数组了     elementData = Arrays.copyOf(elementData, newCapacity); }

add方法添加元素关键步骤:

1)首先通过size+1明确需要的容量

2)对DEFAULTCAPACITY_EMPTY_ELEMENTDATA进行特殊处理,初始容量为10

3)判断所需要的容量是否大于elementData数组可容纳的元素的数量,如果大于length,则表明位置不够了需要扩容

4)扩容时根据elementData的length属性扩容至原来的1.5倍,并不是根据你需要的容量来的,不要混淆了

5)扩容后将elementData按照新的长度拷贝新的数组并赋值给elementData,此时就得到了足够容量的数组

6)将需要添加的元素e放置在size+1的位置,并将size数量+1

添加元素除了可以在容器尾部添加元素之外,还可以在指定的下标位置添加元素,在添加的时候需要移动index之后的所有元素,如果index之后的元素特别多,性能会比较差。

public void add(int index, E element) {    //校验下标值,不能大于size,不能小于0,否则IndexOutOfBoundsException异常    rangeCheckForAdd(index); //保证有足够的容量    ensureCapacityInternal(size + 1);  // Increments modCount    //将index之后的元素统一向后移动一个位置,此时index的位置就空出来了    System.arraycopy(elementData, index, elementData, index + 1,                     size - index);    //将元素存放在index的位置    elementData[index] = element;    size++;}

添加元素还提供了addAll方法可以将集合添加到另外一个集合中,方法类似,大家可以自行查看源码。

在这里我们补充一个知识点System.arraycopy和Arrays.copyOf的区别:

Arrays.copyOf是jdk提供的工具类,它可以按照给定的源数组和长度复制指定长度的数组System.arraycopy是Java标准类库提供的工具类,用它复制数组比for循环要快很多。它需要提供的参数:源数组、从哪个位置开始复制的偏移量、目标数组、目标数组从哪个位置开始复制的偏移量、需要复制的元素个数。获取元素get

获取元素的方法比较简单,我们只需要从elementData的下标位置取出来返回即可,因数组只提供了根据下标获取元素,所以ArrayList获取元素也只能通过下标获取。

public E get(int index) {    //校验获取元素的下标位置是否合法,如果index>=size则IndexOutOfBoundsException    rangeCheck(index);    return elementData(index);}
删除元素remove

remove删除元素方法也有很多重载方法,我们可以按照下标删除指定下标位置的元素,也可以按照元素删除容器中所有指定的元素,我们还可以删除某一段下标范围内的元素,最基础开发中最常用的是根据下标删除元素,这个方法也是我们理解其他重载方法的基础。我们接下来分析源码:

public E remove(int index) { //依然先校验传入的下标是否合法    rangeCheck(index); //这里对modCount进行+1,这是第二次出现,get方法没有    modCount++;    //获取下标位置的元素    E oldValue = elementData(index); //计算需要移动的元素,这里元素要删除,其实是将后面的元素向前移动把index位置的元素覆盖掉    int numMoved = size - index - 1;    if (numMoved > 0)        System.arraycopy(elementData, index+1, elementData, index,                         numMoved);    //把最后一个位置置为null,便于GC回收,且对size--    elementData[--size] = null; // clear to let GC do its work //返回删除的元素    return oldValue;}

看过remove方法我们要注意一点,如果在循环中删除元素,下标的位置要及时跟着变动喔。

遍历元素for

我们可以使用for循环遍历元素,也可以使用增强for循环和foreach遍历元素,或者使用迭代器遍历元素。增加for循环的底层其实也是迭代器,那我们就以迭代器为主学习ArrayList的遍历。

我们写一个遍历元素的示例:

public static void main(String[] args) {    ArrayList<String> list = new ArrayList<>(10);    list.add("Java");    list.add("Code");    //获取迭代器    Iterator<String> iterator = list.iterator();    //判断是否还有下一个元素,如果有,可以通过next方法获取    while (iterator.hasNext()){        System.out.println(iterator.next());    }}

调用ArrayList的iterator()方法,得到的是ArrayList的内部类对象Itr。

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

而hasNext()方法则就是判断指针是否移动到了最后一个元素之后的下标size的位置。

public boolean hasNext() {    return cursor != size;}

next()方法则是根据当前的指针下一个位置获取元素:

public E next() {    //校验modCount值是否被改变了    checkForComodification();    int i = cursor;    //i不能大于size    if (i >= size)        throw new NoSuchElementException();    Object[] elementData = ArrayList.this.elementData;    //如果i大于了elementData容器的容量,则抛出异常    if (i >= elementData.length)        throw new ConcurrentModificationException();    cursor = i + 1;    //返回下一个指针的元素    return (E) elementData[lastRet = i];}//校验modCount是否等于expectedModCount,不等于则抛出ConcurrentModificationExceptionfinal void checkForComodification() {    if (modCount != expectedModCount)        throw new ConcurrentModificationException();}

这个频繁出现的modCount是做什么的呢?那我们需要了解一个概念fail-fast快速失败。

快速失败fail-fast

在遍历集合的过程中,如果对集合中的元素做了增加、修改和删除操作,则会抛出异常ConcurrentModificationException,这就是快速失败。java.util包下的工具类都是fail-fast。

对应的就有另一个概念称为fail-save安全失败,在遍历集合时候,不是在原有的容器中遍历元素,而是先把原来的元素复制出来,在新的集合上遍历,原有容器中元素的增加、修改和删除并不会影响数据的遍历。java.util.concurrent包下的类都是fail-safe的。

我们看一个单线程下快速失败的例子:

public static void main(String[] args) {    ArrayList<String> list = new ArrayList<>(10);    list.add("a");    list.add("b");    list.add("c");    list.add("d");    Iterator<String> iterator = list.iterator();    while (iterator.hasNext()){        String item = iterator.next();        iterator.remove();        //随便删除一个元素        if (item.equals("a")){            //注意这里是list的删除方法,不是迭代器的删除方法            list.remove(0);        }    }}

运行结果:

Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList$Itr.next(ArrayList.java:851) at com.study.test.code.girl.base.datastruct.ListTest.main(ListTest.java:22)

我们在分析ArrayList的增加和remove的方法时,都对modCount进行了修改,所以会报错。但是如果使用的是iterator的删除方法则不会报错,因为它做了处理,我们看源码:

public void remove() {    if (lastRet < 0)        throw new IllegalStateException();    checkForComodification();    try {        ArrayList.this.remove(lastRet);        cursor = lastRet;        lastRet = -1;        //这里修改了expectedModCount的值        expectedModCount = modCount;    } catch (IndexOutOfBoundsException ex) {        throw new ConcurrentModificationException();    }}

所以我们如果有必须要删除元素的场景,不要使用ArrayList的remove方法,而是要使用迭代器的remove方法来保证线程安全性,但是更好的是使用JUC包下的CopyOnWriteArrayList。

所以下一篇我们继续学习线程安全的集合CopyOnWriteArrayList

我是勾勾,一直在努力的程序媛,感谢您的点赞、关注和转发!

我们下篇文章见!

标签: #list比较java