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比较,不用单独提出来。