ArrayList是一种List实现,它的内部用一个动态数组来存储元素,因此ArrayList能够在添加元素的时候进行动态的扩容。
数组
我们知道ArrayList查找效率高,这个都是基于数组来实现的,那么数组为什么查找效率高呢?
- Java的数组中存储的每个元素类型一致,每个元素占用的空间大小相同。
- Java数组中存储的每个元素,内存地址是连续的。
- 通常首元素的内存地址作为整个数组对象的内存地址。
- 数组中的元素是有下标的,有下标就可以计算出被查找的元素和首元素的偏移量。
实际上数组中查找元素是可以通过计算被查找元素的内存地址,通过内存地址可以直接定位元素。也就是说数组中有100个元素和有100万个元素,实际上在查找方面效率是一样的。
数组是线性表,就是数据排成像一条直线一样的结构,除了数组,链表、队列、栈都是线性结构,而非线性表就是二叉树、堆、图等。
借用别人的几个动图
下图展示往数组末尾添加元素,时间复杂度没什么影响,只会验证一个扩容操作(在扩容过程中也是耗费性能的操作)。
指定位置添加元素,每一次添加元素的时候,指定位置以及后面的元素都要往后移动,移动的过程是影响性能的。
删除元素跟指定位置添加元素是同样的道理。
如何扩容
当ArrayList中添加元素的时候,ArrayList的存储容量如果满足新元素的容量要求,则直接存储;如果不满足,ArrayList会自动扩容。
初始化
先看一下ArrayList的两个构造方法
/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
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);
}
}
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
在无参构造中,我们看到了在用无参构造来创建对象的时候其实就是创建了一个空数组,长度为0
在有参构造中,传入的参数是正整数就按照传入的参数来确定创建数组的大小,否则异常
确保内部容量
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
// 确保内部容量
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
① ensureCapacityInternal方法名的英文大致是“确保内部容量”,size表示的是执行添加之前的元素个数,并非ArrayList的容量,容量应该是数组elementData的长度。ensureCapacityInternal该方法通过将现有的元素个数与数组的容量比较,如果需要扩容,则扩容。
②是将要添加的元素放置到相应的数组中。
可以看出扩容逻辑在ensureCapacityInternal()方法中,参数为当前元素个数+1,进入此方法后:
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//如果传入的是个空数组则最小容量取默认容量与minCapacity之间的最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
// 如果最小需要空间比elementData的内存空间要大,则需要扩容
if (minCapacity - elementData.length > 0)
//扩容
grow(minCapacity);
}
扩容
ArrayList扩容的关键方法是grow()方法:
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
// 获取到ArrayList中elementData数组的内存空间长度
int oldCapacity = elementData.length;
// 扩容至原来的1.5倍
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:
// 调用Arrays.copyOf方法将elementData数组指向新的内存空间newCapacity的连续空间
// 并将elementData的数据复制到新的内存空间
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
int newCapacity = oldCapacity + (oldCapacity >> 1);
oldCapacity >> 1 右移运算符 原来长度的一半 再加上原长度也就是每次扩容是原来的1.5倍,之前的所有都是确定新数组的长度,确定之后就是把老数组copy到新数组中,这样数组的扩容就结束了。
扩容导致性能下降,如何处理
首先复制分为两种;深度复制、浅复制。深度复制可以将对象的值和对象的内容复制,而浅复制是指对对象引用的复制,也就是浅复制共用同一份内容。
数组数据复制使用到了Arrays.copyOf方法,该方法最终使用的是System.arraycopy方法来复制数组,而System.arraycopy是Native方法。
System源码中的arraycopy()标识为native意味JDK的本地库,不可避免的会进行IO操作,如果频繁的对ArrayList进行扩容,毫不疑问会降低ArrayList的使用性能,因此当我们确定添加元素的个数的时候,我们可以事先知道并指定ArrayList的可存储元素的个数,这样当我们向ArrayList中加入元素的时候,就可以避免ArrayList的自动扩容,从而提高ArrayList的性能。
LinkedList比较
我们知道ArrayList由数组实现,而LinkedList由链表实现。链表分为单向链表和双向链表。
-
单向链表
只有一个指向下一个节点的指针
优点:增加删除节点简单,遍历时候不会死循环
缺点:只能从头到尾遍历,只能找到后面的值,无法找到前一个值。
-
双向链表
有两个指针,一个指向前一个节点,一个指向下一个节点
优点:可以找到上一个和下一个的节点
缺点:增加删除节点复杂,需要多分配一个指针存储空间
查找
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
/**
* Returns the (non-null) Node at the specified element index.
*/
Node<E> node(int index) {
// assert isElementIndex(index);
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;
}
}
上面是LinkedList获取元素的源码,代码中利用了双向链表的特性,如果查找的元素位置离链表头比较近,就从节点头部往后遍历,否则就从节点尾部向前开始遍历。
对于LinkedList查找要么从头开始找,要么多后开始找。所以要查找的数据靠近两端的话,查找耗时相对较少,位置越在链表中间,查找所需要消耗的时间就越长。
而ArrayList是基于数组来实现的,数组查找任意元素耗时几乎没有什么差别。
插入
LinkedList插入数据的时候,主要耗时操作是寻址时间和创建节点时间,越靠近两端,所需要的寻址时间越短。
只需要找到插入位置,然后修改插入元素前后节点的prev、next值即可。因为在前面部分,所以查找插入位置耗时不多,随着插入位置往后,所需要的查找时间会越多。
ArrayList插入数据的时候,主要耗时操作是数据移动和扩容。越是靠前插入,需要移动的数据就越多,耗时也就越多。
总结:当数据量小的时候,LinkedList主要耗时在创建节点,而ArrayList主要耗时在扩容上面。ArrayList在数据量小的时候扩容相对会比较频繁。在单论插入的情况下,LinkedList在数据量小的时候效率相对占住优势,而ArrayList在数据量大的时候效率相对占优势。
删除
LinkedList删除主要耗时在寻址上面,数据越是靠近链表中间,所需要的寻址时间就越长。ArrayList主要耗时在移动数据上面,数据越靠前,所要移动的数据就越多,耗时就越长。
总结
LinkedList和ArrayList在效率上的问题不是一定的,所以我们不要一位的追求一定的答案,要结合实际情况,任何事物都有两面性。
复制ArrayList
package com.doaredo.test.proxy;
import org.junit.Test;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class ArrayListCopy {
public ArrayList list;
{
list = new ArrayList();
list.add("copy");
}
@Test
public void copy1(){
ArrayList copyList = (ArrayList) list.clone();
System.out.println(list == copyList);
System.out.println(list.equals(copyList));
}
@Test
public void copy2(){
ArrayList copyList = new ArrayList();
copyList.addAll(list);
System.out.println(list == copyList);
System.out.println(list.equals(copyList));
}
@Test
public void copy3(){
List<Object> objects = Arrays.asList(new Object[list.size()]);
ArrayList copyList = new ArrayList(objects);
Collections.copy(copyList, list);
System.out.println(list == copyList);
System.out.println(list.equals(copyList));
}
/**
* 深拷贝
* @throws Exception
*/
@Test
public void copy4() throws Exception {
ArrayList deepList = new ArrayList();
Info info = new Info();
info.setName("deep");
deepList.add(info);
List copy = deepCopy(deepList);
System.out.println(deepList == copy);
Info coInfo = (Info) copy.get(0);
info = (Info) deepList.get(0);
coInfo.setName("copyInfo");
System.out.println(coInfo.getName());
System.out.println(info.getName());
}
private List deepCopy(List deepList) throws Exception {
// 序列化
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(deepList);
//反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream bis = new ObjectInputStream(bais);
List copy = (List) bis.readObject();
return copy;
}
}
class Info implements Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
ArrayList去重
package com.doaredo.test.proxy;
import org.junit.Test;
import java.util.*;
import java.util.stream.Collectors;
public class ArrayListRepeat {
/**
* 借助set去重
* Set本身具有去重的功能
* 先将集合放入Set中,再将Set转换为List就去重了
*
*/
@Test
public void repeat1() {
ArrayList<String> list = new ArrayList();
list.add("F");
list.add("A");
list.add("D");
list.add("A");
list.add("F");
HashSet set = new HashSet(list);
list.clear();
list.addAll(set);
}
/**
* 先用stream方法将集合转换成流,然后distinct去重
* 最后将stream流用collect收集为List
*
*/
@Test
public void repeat2() {
ArrayList<String> list = new ArrayList();
list.add("F");
list.add("A");
list.add("D");
list.add("A");
list.add("F");
List<String> newList = list.stream().distinct().collect(Collectors.toList());
}
}
上面是整个对象去重,如果是要对对象中的某个属性去重,显然上面的方式不行。
package com.doaredo.test.proxy;
import org.junit.Test;
import java.util.*;
import java.util.stream.Collectors;
public class ArrayListRepeat {
@Test
public void repeat3() {
User u1 = new User("aa", 11);
User u2 = new User("bb", 22);
User u3 = new User("aa", 11);
User u4 = new User("cc", 33);
ArrayList<User> list = new ArrayList();
list.add(u1);
list.add(u2);
list.add(u3);
list.add(u4);
//Set<User> set = new TreeSet<>((o1, o2) -> o1.getName().compareTo(o2.getName()));
Set<User> set = new TreeSet<>(Comparator.comparing(User::getName));
set.addAll(list);
new ArrayList<>(set).forEach(item -> {
System.out.println(item.getName());
}
);
}
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
}
比较和排序
实现Comparable接口
package com.doaredo.test.proxy;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class ArrayListSort {
@Test
public void sort(){
ArrayList<String> list = new ArrayList();
list.add("F");
list.add("A");
list.add("D");
list.add("B");
list.add("H");
Collections.sort(list);
for (String s : list){
System.out.print(s + ",");
}
System.out.println("");
Collections.sort(list, Collections.reverseOrder());
for (String s : list){
System.out.print(s + ",");
}
}
@Test
public void sort2(){
Info o1 = new Info("aaa", 3);
Info o2 = new Info("bbb", 1);
Info o3 = new Info("ccc", 2);
ArrayList<Info> list = new ArrayList();
list.add(o1);
list.add(o2);
list.add(o3);
Collections.sort(list);
for (Info info : list){
System.out.print(info.getAge() + ",");
}
}
class Info implements Comparable<Info> {
private String name;
private int age;
public Info(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int compareTo(Info o) {
return this.getAge() < o.getAge() ? -1 : (this.getAge() == o.getAge() ? 0 : 1);
}
}
}
自定义Comparator
package com.doaredo.test.proxy;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class ArrayListSort {
@Test
public void sort3(){
User o1 = new User("aaa", 3);
User o2 = new User("ccc", 1);
User o3 = new User("bbb", 2);
ArrayList<User> list = new ArrayList();
list.add(o1);
list.add(o2);
list.add(o3);
Collections.sort(list, new SortByAge());
for (User user : list){
System.out.print(user.getAge() + ",");
}
Collections.sort(list, new SortByName());
for (User user : list){
System.out.print(user.getName() + ",");
}
}
class SortByName implements Comparator<User> {
@Override
public int compare(User o1, User o2) {
return o1.getName().compareTo(o2.getName());
}
}
class SortByAge implements Comparator<User> {
@Override
public int compare(User o1, User o2) {
return o1.getAge() < o2.getAge() ? 1 : (o1.getAge() == o2.getAge() ? 0 : -1);
}
}
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
}