【JAVA】【集合7】Java中的ArrayList

  1. 【集合】ArrayList

一、ArrayList定义

ArrayList在java.util.ArrayList中定义。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    ...    
    private static final int DEFAULT_CAPACITY = 10;
    private static final Object[] EMPTY_ELEMENTDATA = {};    
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};    
    transient Object[] elementData;     
    private int size;   
}

二、ArrayList的构造方法

(1)创建空的ArrayList

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

指定初始创建的ArryList存储长度:

public ArrayList(int initialCapacity)

(2)通过集合创建ArrayList

public ArrayList(Collection<? extends E> c) 

样例:

List<String> list = new ArrayList<String>(Arrays.asList(args)); 

三、遍历ArrayList元素

1. 通过普通for循环遍历

List<String> array = new ArrayList<String>(Arrays.asList("10","11","12","13","14"));

for(int i = 0; i < array.size(); i++) {
   System.out.println(array.get(i));
}

2. 通过foreach循环遍历

List<String> array = new ArrayList<String>(Arrays.asList("10","11","12","13","14"));

for(String str: array) {
    System.out.println(str);
}

3. 通过迭代器遍历

List<String> array = new ArrayList<String>(Arrays.asList("10","11","12","13","14"));

Iterator strIteator = array.iterator();
while(strIteator.hasNext()) {
    System.out.println(strIteator.next());
}

四、几种遍历访问方式的效率

参考:

https://blog.csdn.net/qq_28605513/article/details/84981338

测试方法:

创建包含100000个元素的ArryList和LinkedList结合,分别采用如上三种方式遍历10遍,其效率如下:

  • ArrayList集合的遍历效率

    for循环100次时间:15 ms
    foreach循环100次时间:25 ms
    迭代器循环100次时间:20 ms
    
  • LinkedList集合的遍历效率

    for循环100次时间:59729 ms
    foreach循环100次时间:18 ms
    迭代器循环100次时间:14 ms
    

分析原因之前,先解答几个概念:

(1)随机访问 和 迭代访问
  • 随机访问:就是指定位置即可随机的定位到集合中的需要操作的元素。不用把集合遍历一遍来定位需要操作的元素。
  • 迭代访问:只能依次访问集合中的每个元素,这种方式叫迭代。如果需要定位一个元素,就是需要迭代+提交判断来定位。
(2)如何判断一个集合能随机访问

我们一般期望List具备高校的随机访问能力。但是,不是所有列表都能高效地随机访问任意索引上的元素。哪些List的实现类具备随机访问能力呢?

提供高效随机访问的类都实现了标记接口 RandomAccess。因此,判断一个集合是否能随机访问,可以使用 instanceof 运算符测试是否实现了这个接口。如下:

// 随便创建一个列表,供后面的代码处理 
List<?> list = ...; 

// 测试能否高效随机访问 
// 如果不能,先使用副本构造方法创建一个支持随机访问的副本,然后再处理 
if (!(list instanceof RandomAccess)) {
    l = new ArrayList<?>(list);
}

(3)如何判断一个集合能迭代访问(迭代器)

为了深入理解遍历循环处理集合的方式,我们要了解两个接口:java.util.Iterator 和 java.lang.Iterable:

public interface Iterator<E> {      
   boolean hasNext();      
   E next();      
   void remove(); 
}

Iterator 接口定义了一种迭代集合或其他数据结构中元素的方式。迭代的过程是这样的:

  • 只要集合中还有更多的元素(hasNext() 方法返回 true),就调用 next() 方法获取集合中的下一个元素。
  • 有序集合(例如列表)的迭代器一般能保证按照顺序返回元素。
  • 无序集合 (例如 Set)只能保证不断调用 next() 方法返回集中的所有元素,没有遗漏也没有重复,不过没有特定的顺序。
(4)如上3种遍历效率解析
  • ArrayList实现了RandomAccess接口,支持随机访问。ArrayList存放的内容用的是transient Object[],在内存中是连续的,通过get(i)本质上是通过[ ]访问,相当于直接操作内存地址,所以随机访问的效率较高。使用普通for循环 比 forEach循环、迭代器效率高。
  • 而LinkedList是一个双向链表,链表只能顺序访问,LinkedList中的get(i) 方法是按照顺序从列表的一端开始检查,直到找到要找的地址。所以,遍历LinkedList使用forEach、迭代器的效率高,使用普通for循环会每次都从头开始遍历、效率较差。

五、ArrayList的扩容

通过add()等方法向ArrayList集合中添加元素时,如果空间不够,ArrayList会自动扩容。如add()方法的调用关系如下:

add()
     |--ensureCapacityInternal(size + 1)
          |--ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
               |--ensureExplicitCapacity(int minCapacity)
                    |--grow(minCapacity);

最终调用到grow(int minCapacity) 方法,扩容公式是:int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity为偶数就是1.5倍,否则是1.5倍左右)。 奇偶不同,比如 :10+10/2 = 15, 33+33/2=49。如果是奇数的话会丢掉小数。l

private void grow(int minCapacity) {
   // overflow-conscious code
   int oldCapacity = elementData.length;
   int newCapacity = oldCapacity + (oldCapacity >> 1);
   if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
   if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
   // minCapacity is usually close to size, so this is a win:
   elementData = Arrays.copyOf(elementData, newCapacity);
}

六、ArrayList遍历中remove()错误用法

参考:

https://www.jianshu.com/p/2c3c4bb1eca0

https://www.jb51.net/article/177791.htm

E remove(int index);

ArrayList常见如下几种遍历删除:

  • for循环,通过index删除
  • foreach指定对象删除
  • 通过迭代器删除
  • 通过removeif()方法删除

我们以如下ArrayList为例,看每种操作方式的删除效果:

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
(1)for循环,通过index删除(错误用法)
  private static void forRemove(List<String> list) {
       for (int i = 0; i < list.size(); i++) {
           if ("b".equals(list.get(i)) || "c".equals(list.get(i))) {
               list.remove(i);
           }
       }
   }

如上代码,期望删除ArrayList中内容为“b”、“c”的元素。

我们查看下remove(int index)方法的源码,可以看出其实现就是将给定index位置之后的元素都向前移动一位,达到删除给定位置元素的目的。

  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;
    }

接下来,我们分析下上面删除元素的代码:

  • 当i=1时,我们删除了元素b,这时list变成了[a,c,d,e],size变成了4。
  • for循环继续往下进行,i=2,for循环找到了第三个元素d,发现不匹配我们的条件,没有进行删除。

这样就跳过了我们想删除的c。

所以,第此种方式最后结果是[a, c, d, e],并没有达到我们的程序预期

(2)foreach遍历删除(错误用法)
    private static void foreachRemove(List<String> list) {
        for (String s : list) {
            if ("b".equals(s) || "c".equals(s)) {
                list.remove(s);
            }
        }
    }

foreach循环,编译器编译后,也是一种迭代器的方式循环,我们看一下编译后什么样子:

   private static void foreachRemove(List<String> list) {
        Iterator var1 = list.iterator();
        while(true) {
            String s;
            do {
                if (!var1.hasNext()) {
                    return;
                }
                s = (String)var1.next();
            } while(!"b".equals(s) && !"c".equals(s));
            list.remove(s);
        }
    }

接下来我们看一下这里调的remove(Object o)方法:

   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;
    }
    
    private void fastRemove(int index) {
        modCount++;
        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
    }

fastRemove(int index)对比之前的remove(int index)方法,两个方法操作其实一样,除了fastRemove(int index)没有返回原来的值。

这里还有一个重要的点,就是modCount++,这个modCount是干嘛用的呢?我们继续往下看。

上面编译器编译后的文件中,我们看到获取元素是通过迭代器的next()方法去获取的,我们来看下迭代器的几个关键方法:

    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;

        public boolean hasNext() {
            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();

            cursor = i + 1;
            return (E) elementData[lastRet = i];

        }

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

我们看到迭代器其实就是维护一个游标cursor,不断往下遍历集合。然后我们注意到next()方法首先会调用checkForComodification(),检查集合是否被修改过。

我们看checkForComodification()方法,就是判断modCount是否等于expectedModCount,这里的expectedModCount就是我们迭代器初始化的时候赋值的(expectedModCount = modCount),回到刚才我们提到的fastRemove(index)方法,里面有一个modCount++,所以集合每次删除元素,这个modCount值就会发生变化,下次再调用next()方法,就会抛出ConcurrentModificationException异常

这里抛出ConcurrentModificationException异常,是一种快速失败(fail-fast)机制,就是两个线程一起遍历操作集合时,如果修改了集合数据,那么另一个地方再次操作集合时,直接抛出异常。(当然多线程操作集合也不建议使用线程不安全的ArrayList)

所以,foreach方式遍历删除,结果是抛出ConcurrentModificationException。

(3)迭代器删除(正确用法)
private static void iteRemove(List<String> list){
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()){
            String next = iterator.next();
            if ("b".equals(next) || "c".equals(next)) {
                iterator.remove();
            }
        }
    }

这种方式和第二种方式编译后类似,不同的地方是:这里用的迭代器的删除方法iterator.remove()。

    public void remove() {
          if (lastRet < 0)
              throw new IllegalStateException();

          checkForComodification();
          try {
              ArrayList.this.remove(lastRet);
              cursor = lastRet;
              lastRet = -1;
              expectedModCount = modCount;
          } catch (IndexOutOfBoundsException ex) {
              throw new ConcurrentModificationException();
          }
      }

删除当前元素,并且把游标回到当前位置,这样就避免了第一种方式出现的跳过一个元素的结果。

  • lastRet = -1:如果连着两次调用remove()则会抛出非法参数异常(lastRet会在调用next()方法时被赋值为cursor的值,可以看上面贴的next源码)。

所以,此种方式遍历删除ArrayList中元素是可行的。

(4)通过removeIf()方法删除(正确用法)

在JDK1.8中,Collection以及其子类新加入了removeIf()方法,作用是按照一定规则过滤集合中的元素。如:

list.removeIf(item -> "1".equals(item))

ArrayLsit中对removeIf()的重写如下:

    public boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        // figure out which elements are to be removed
        // any exception thrown from the filter predicate at this stage
        // will leave the collection unmodified

        int removeCount = 0;
        final BitSet removeSet = new BitSet(size);
        final int expectedModCount = modCount;
        final int size = this.size;

        for (int i=0; modCount == expectedModCount && i < size; i++) {
            @SuppressWarnings("unchecked")
            final E element = (E) elementData[i];
            if (filter.test(element)) {
                removeSet.set(i);
                removeCount++;
            }
        }

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

        // shift surviving elements left over the spaces left by removed elements
        final boolean anyToRemove = removeCount > 0;
        if (anyToRemove) {
           final int newSize = size - removeCount;
           for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
              i = removeSet.nextClearBit(i);
              elementData[j] = elementData[i];
           }

           for (int k=newSize; k < size; k++) {
              elementData[k] = null; // Let gc do its work
           }

           this.size = newSize;
           if (modCount != expectedModCount) {
              throw new ConcurrentModificationException();
           }
           modCount++;
          }
          return anyToRemove;
    }

removeIf()的入参是一个过滤条件,用来判断需要移除的元素是否满足条件。方法中设置了一个removeSet,把满足条件的元素索引坐标都放入removeSet,然后统一对removeSet中的索引进行移除。是安全的方法。

(5)把需删除内容加入临时ArrayList,然后通过removeAll()方法删除 (正确的方法)

这种方法思路是for循环内使用一个集合存放所有满足移除条件的元素,for循环结束后直接使用removeAll()方法进行移除。

   List<Long> removeList = new ArrayList<>();
   for (int i = 0; i < list.size(); i++) {
      if (i % 2 == 0) {
         removeList.add(list.get(i));
      }
   }
   list.removeAll(removeList);

removeAll源码如下:

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

  private boolean batchRemove(Collection<?> c, boolean complement) {
      final Object[] elementData = this.elementData;
      int r = 0, w = 0;
      boolean modified = false;
      try {
         for (; r < size; r++)
            if (c.contains(elementData[r]) == complement)
               elementData[w++] = elementData[r];
      } finally {
          if (r != size) {
             System.arraycopy(elementData, r, elementData, w, size - r);
             w += size - r;
          }

          if (w != size) {
             // clear to let GC do its work
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            size = w;
            modified = true;
         }
     }
     return modified;
  }

定义了两个数组指针r和w,初始都指向列表第一个元素。循环遍历列表,r指向当前元素,若当前元素没有满足移除条件,将数组[r]元素赋值给数组[w],w指针向后移动一位。这样就完成了整个数组中,没有被移除的元素向前移动。遍历完列表后,将w后面的元素都置空,并减少数组长度。至此完成removeAll移除操作。

(6) for循环,通过index删除变种:从后向前(正确用法)

同1,也是for循环,为啥从后往前遍历就是正确的呢?

因为每次调用remove(int index),index后面的元素会往前移动,如果是从后往前遍历,index后面的元素发生移动,跟index前面的元素无关,我们循环只去和前面的元素做判断,因此就没有影响。如:

   for (int i = list.size() - 1; i >= 0; i--) {
       if (list.get(i).longValue() == 2) {
           list.remove(i);
       }
   }

五、子类的ArrayList和父类的ArrayList之间是否存在继承关系?

样例:

ArrayList<String>  arrayList1  =  new  ArrayList<String>();
arrayList1.add(new String());

ArrayList<Object>  arrayList2  =  arrayList1;   //编译错误

解析:ArrayList类型值不能直接赋给ArrayList

posted @ 2021-05-21 14:36  小拙  阅读(234)  评论(0编辑  收藏  举报