zoukankan      html  css  js  c++  java
  • 【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

  • 相关阅读:
    解析CIDR表示的IP段表示的范围
    [Python] 使用乘号复制变量引起的问题
    [Python] 字典dict添加二级键值的问题
    [Java] [刷题] 连续自然数和
    [Java] [刷题] 多个整数连接为最大整数问题
    [CentOS] 编译安装Python3后pip3安装的库如何在命令行调用
    [CentOS] 宝塔面板与Python3的恩怨情仇
    [易语言] 两种字节序的直观比较
    [Java] [刷题] Excel地址转换
    [Java] 运算精度
  • 原文地址:https://www.cnblogs.com/yickel/p/14793690.html
Copyright © 2011-2022 走看看