List小总结

300 阅读7分钟

List

List是Java集合Collection的一个子接口,用于表示有序可重复的集合,通常开发中使用较多的就是ArrayList和LinkedList。

ArrayList

ArrayList底层是通过对象数组存储元素

内部定义:

 transient Object[] elementData;

初始化大小以及扩容规则

ArrayList的初始化大小为10,但是它并不是在创建ArrayList的时候就出初始化出一个容量为10的数组,在1.7以后,每次new ArrayList();将会将当前elementData指向一个空数组。

构造方法初始化

源码如下:

无参构造方法

 // 定义的默认空数组,用于初始化
 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
 // 无参构造方法,指向默认的空数组
 public ArrayList() {
     this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
 }

指定容量的构造方法

 // 指定容量的构造方法,会在创建的时候就初始化指定容量的数组
 public ArrayList(int initialCapacity) {
     if (initialCapacity > 0) {
         this.elementData = new Object[initialCapacity];
     } else if (initialCapacity == 0) {
         this.elementData = EMPTY_ELEMENTDATA;
     } else {
         throw new IllegalArgumentException("Illegal Capacity: "+
                                            initialCapacity);
     }
 }

将集合转换成ArrayList

 // 将其它集合按照迭代器顺序转为ArrayList
 public ArrayList(Collection<? extends E> c) {
     // 将元素转为数组的
     elementData = c.toArray();
     if ((size = elementData.length) != 0) {
         // 防止返回
         // defend against c.toArray (incorrectly) not returning Object[]
         // (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
         if (elementData.getClass() != Object[].class)
             elementData = Arrays.copyOf(elementData, size, Object[].class);
     } else {
         // replace with empty array.
         this.elementData = EMPTY_ELEMENTDATA;
     }
 }

扩容机制

ArrayList默认无参构造函数,初始化大小为10;扩容是以1.5倍扩容。

计算新容量源码:

 int newCapacity = oldCapacity + (oldCapacity >> 1);

扩容原理是基于Arrays.copyOf方法,将会新创建一个大小为newCapacity的数组,将原来的数组元素拷贝进去,注意Arrays.copyOf底层调用native方法System.arrayCopy。

 private Object[] grow(int minCapacity) {
     return elementData = Arrays.copyOf(elementData,
                                        newCapacity(minCapacity));
 }

ArrayList扩容时机是size达到了数组长度的时候扩容

  private void add(E e, Object[] elementData, int s) {
      // s就是size,当元素数量达到了数组长度的时候,执行grow扩容
      if (s == elementData.length)
          elementData = grow();
      elementData[s] = e;
      size = s + 1;
  }

新增和删除

ArrayList的底层是数组存储,数组不支持在原有基础上扩容,因此增是通过新建数组,将原来的数组元素拷贝到新的数组实现的,调用的是Arrays.copyOf()方法,底层是调用System.arrayCopy方法(native方法)实现。删除元素就是将数组中原来的数组索引处的值置为null。

ArrayList关于容量总结

根据前面分析的ArrayList的大小和扩容机制,在使用ArrayList的时候如果预先能够估算到存储元素大小,最好在创建ArrayList的时候指定初始化的大小,减少扩容次数,每次扩容都会重新创建新的数组和复制元素有很大的性能消耗,如果不知道的情况下可以采用默认大小,即直接new ArrayList()使用初始化大小10。

ArrayList的toArray

toArray():返回Object[]数组,list调用该方法会自动将list中的元素转为Object[]数组,这也就正好证明了泛型擦除。

toArray(T[]a):返回T[]数组,支持泛型返回。这里有个长度问题要注意,查看源码可知。

 public <T> T[] toArray(T[] a) {
     if (a.length < size)
         // Make a new array of a's runtime type, but my contents:
         return (T[]) Arrays.copyOf(elementData, size, a.getClass());
     System.arraycopy(elementData, 0, a, 0, size);
     if (a.length > size)
         a[size] = null;
     return a;
 }

当传入的数组长度小于集合size的时候,会创建一个新数组进行返回;

当传入的数组长度大于等于size的时候,会将list里面的元素复制到传入的数组里面,然后进行返回;在数组长度大于size的时候,会将a[size]置为null。

总结:当我们调用toArray需要支持泛型的时候,最好传和list的size相同长度的数组,这样既不会创建新数组又不会浪费空间。

 ArrayList<Object> list = new ArrayList<>(1);
 ​
 list.add(1);
 ​
 list.add(2);
 ​
 Integer[] its=new Integer[5];
 ​
 its[4]=4;
 ​
 Integer[] itsRes=list.toArray(its);
 ​
 System.out.println(Arrays.toString(itsRes));//[1,2,null,null,4]

通过以上代码可知,调用、toArray只会把array[size]的值置为null,后续值不会

LinkedList

LinkedList底层采用双向链表作为数据存储结构,链表分别用first和last变量指向链表的头部和尾部,添加的时候直接添加到链表尾部。因此LinkedList不单单可以作为List使用也可以使用Queue使用。

查询实现方式

LinkedList的查询类似于二分查找思路,但是它只能进行一次二分就是根据传入的索引值,查找索引是大于size/2则从last往前找,否则从first查找。

查找流程是节点遍历的方式,因此LinkedList的查询效率低于ArrayList,因为ArrayList底层是数组,可以直接通过数组下标查找元素。

 public E get(int index) {
     checkElementIndex(index);
     return node(index).item;
 }
 Node<E> node(int index) {
     // assert isElementIndex(index);
     // 如果查询元素索引小于size/2则从first节点向后开始查询,否则从last向前查询
     if (index < (size >> 1)) {
         Node<E> x = first;
         for (int i = 0; i < index; i++)
             x = x.next;
         return x;
     } else {
         Node<E> x = last;
         for (int i = size - 1; i > index; i--)
             x = x.prev;
         return x;
     }
 }

总结

ArrayList和LinkedList对比

插入效率分析

在尾部插入数据,数据量较小时LinkedList比较快,因为ArrayList要频繁扩容,当数据量大时ArrayList比较快,因为ArrayList扩容是当前容量*1.5,大容量扩容一次就能提供很多空间,当ArrayList不需扩容时效率明显比LinkedList高,因为直接数组元素赋值不需newNode大约在150万的时候arraList效率就大于了linkedLlist这个看电脑也有关系。

在首部插入数据,LinkedList较快,因为LinkedList遍历插入位置花费时间很小,而ArrayList需要将原数组所有元素进行一次System.arraycopy。

插入位置越往中间,LinkedList效率越低,因为它遍历获取插入位置是从两头往中间搜,index越往中间遍历越久,因此ArrayList的插入效率可能会比LinkedList高插入位置越往后,ArrayList效率越高,因为数组需要复制后移的数据少了,那么System.arraycopy就快了,因此在首部插入数据LinkedList效率比ArrayList高,尾部插入数据ArrayList效率比LinkedList高。

LinkedList可以实现队列,栈等数据结构,这是它的优势。

删除效率分析

删除尾部数据两个差不多。

删除头部数据ArrayList效率远远低于LinkedList,因为ArrayList删除头部数据后,需要将后面的元素向前移,而LinkedList只需要改变两个指针就可以了。

删除位置越往中间,LinkedList效率越低,因为它需要遍历最差能达到n/2,ArrayList只需要找到移动一遍。

使用分析

通常开发中,一般ArrayList使用得比较多,因为业务开发通常List作为一个存放已存在数据的集合,不会做太多的增删操作,反而有一定随机访问场景,如分页查询结果等,而且也不常会造成扩容的场景。但是当对集合数据要频繁增删的时候使用LinkedList就比较合适,还有就是LinkedList通常用于在队列这些场景中使用。

扩展

Arrays.asList

Arrays.asList返回的是Arrays的一个内部类ArrayList,该ArrayList和java.util.ArrayList不同,它内部虽然也是使用数组维护数据,但是不支持remove/add等方法,只提供简单的get/indexOf/set等方法,也就是说该ArrayList返回的是一个长度不可变的List,涉及到的所有要改变List长度的方法都不支持,它本质还是一个数组,只是用了内部的ArrayList将数组适配成了List。

subList

List的subList返回的是一个不可操作的SubList对象,它底层维护的是调用者的索引信息,保持了对原List的引用,是无法序列化的,如果修改了SubList中元素的值主List也会被修改。

同样修改了主List元素,数据更新会影响SubList,修改了主List结构,此时子SubList会抛出ConcurrentModificationException。

 List<Integer> mainList = new ArrayList<>();
 for (int i = 0; i < 10; i++) {
     mainList.add(i);
 }
 List<Integer> subList = mainList.subList(0, 10);
 mainList.add(11);
 subList.add(12);// 抛出ConcurrentModificationException异常
 System.out.println("mainList:"+mainList);
 System.out.println("subList:"+subList);

这个也很好理解,因为SubList源于主List一个区间,如果改变了主List长度,则SubList无法准确的得出区间。

List迭代器

当我们使用forEach迭代List集合的时候,在集合被修改后会引发并发修改异常ConcurrentModificationException。

如,继续拿上面的subList作为例子,当我们改变了主list的结构后,迭代子list会抛出ConcurrentModificationException异常。

 List<Integer> mainList = new ArrayList<>();
 for (int i = 0; i < 10; i++) {
     mainList.add(i);
 }
 List<Integer> subList = mainList.subList(0, 10);
 mainList.add(11);
 for (Integer item : subList) {
     System.out.println(item);
 }

我就有点好奇,我们只是简单的进行了一次迭代而已,并没有做任何的get操作为什么会抛出异常,然后分析异常链

 at java.base/java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1282)
     at java.base/java.util.ArrayList$SubList.listIterator(ArrayList.java:1151)
     at java.base/java.util.AbstractList.listIterator(AbstractList.java:311)
     at java.base/java.util.ArrayList$SubList.iterator(ArrayList.java:1147)
     at com.example.demo.blogs.HashMapDemo.main(HashMapDemo.java:24)

发现异常抛出在迭代器,最后将字节码反编译后发现

 List<Integer> mainList = new ArrayList();
 ​
 for(int i = 0; i < 10; ++i) {
     mainList.add(i);
 }
 ​
 List<Integer> subList = mainList.subList(0, 10);
 mainList.add(11);
 // forEach代码
 Iterator var3 = subList.iterator();
 while(var3.hasNext()) {
     Integer item = (Integer)var3.next();
     System.out.println(item);
 }

经过反编译后,发现使用forEach语法糖迭代的时候,底层调用的是迭代器,而迭代器都是会检查modCount变量的,这也就是为什么每次修改了表机构后对modCount进行加1。

List的equals比较规则

List的equals主要使用的是AbstractList重写了equals,按照顺序使用迭代器取出List的值相互比较,最后如果都相等,并且长度相同就返回true否则返回false,并且在比较的时候是调用对象equals比较。

 public boolean equals(Object o) {
     if (o == this)
         return true;
     if (!(o instanceof List))
         return false;
 ​
     ListIterator<E> e1 = listIterator();
     ListIterator<?> e2 = ((List<?>) o).listIterator();
     while (e1.hasNext() && e2.hasNext()) {
         E o1 = e1.next();
         Object o2 = e2.next();
         if (!(o1==null ? o2==null : o1.equals(o2)))
             return false;
     }
     return !(e1.hasNext() || e2.hasNext());
 }

因此在要比较集合是否相等的时候,可以直接使用equals比较,不用单独提出来。