深入浅出LinkedList、ArrayList

1. ArrayList底层实现

ArrayList底层是一个动态数组,即容量可变,ArrayList继承了抽象List实现了诸如以下的接口。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

实现了RandomAccess可以使ArrayList实现快速随机访问(通过数组下表标),实现了Cloneable可以使ArrayList被克隆,Serializable可以使ArrayList实现序列化,其中最关键的部分:

transient Object[] elementData;

即ArrayList底层是一个transient 修饰的Object数组

问题1:为什么用transient 修饰?

transient:Java关键字,变量修饰符,如果用transient声明一个实例变量,当对象存储时,用transient关键字标记的成员变量不参与序列化过程

问题2:为什么用transient:关键字使其不支持序列化?

假如elementData的长度为10,而其中只有5个元素,那么在序列化的时候只需要存储5个元素,而数组中后面5个元素是不需要存储的**,即存在一定的空间浪费!**。于是将elementData定义为transient,避免了Java自带的序列化机制,但是其自定义了两个方法,实现了自己可控制的序列化操作。

private void writeObject(java.io.ObjectOutputStream s)  
private void readObject(java.io.ObjectInputStream s)  

2. ArrayList构造函数

ArrayList默认的无参构造方法:

    public ArrayList() {
        //无参构造器默认构造一个空数组
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

可以在设置时指定初始值大小initialCapacity的值即

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

3. ArrayList重要方法

首先就是add添加的方法:

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

这里我们看到每一次添加的时候先去调用ensureCapacityInternal的方法,我们点进去看看:

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
         // 数据结构发生改变,和fail-fast机制有关(下面会讲),在使用迭代器过程中,只能通过迭代器的方法(比如迭代器中add,remove等),修改List的数据结构,
    // 如果使用List的方法(比如List中的add,remove等),修改List的数据结构,会抛出ConcurrentModificationException
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

这里ensureExplicitCapacity又调用了grow的方法,我们再次点进去:

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

即add方法中,ArrayList每次新增元素都会进行容量大小检测判断,若新增的后元素的个数会超过ArrayList的容量,就会调用ensureCapacity->进而调用grow的扩容方法进行扩容满足新增元素的需求

为什么是扩容成1.5倍?

假设现在我有11个元素,扩容成15之后有四个是浪费掉的即

在这里插入图片描述

浪费了26%还多

如果是2.5倍呢?此时就会浪费更多:56%!

3.5、4.5就更多了,但是假设现在是1.1呢,我们知道扩容调用的是Arrays.copyOf(elementData, newCapacity)即复制的方法,每一次复制都要从头到尾复制一遍,太多的复制次数影响性能,所以1.5倍是一个折中的办法:即减少了空间的浪费也减少了性能的损耗!

4. 快速失败

java.util下的包都是快速失败的,何为快速失败?

即内部维护了一个modCount的值,上面也反复提到了,每一次遍历的时候都会讲modCout的值与内存值进行比较,假设内存值!=modCount,即其他线程对当前的集合进行了修改,就直接抛出一个ConcurrentModificationException

ArrayList源码中大量运用了checkForComodification方法

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

安全失败

与快速失败相对应的是安全失败,采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历,这样就不会直接扔出一个异常了。

1. LinkedList
2. ArrayList

那么这两者有什么异同呢
首先是代码测试:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import bean.Answer;
import bean.User;

/**
 * 测试类
 * 
 * @author hxz
 *
 */
public class MyTestUtil {
	
	public static void addTest(List<String> list) {
		System.out.println(list.getClass().getName() + "开始查询");
		long start = System.currentTimeMillis();
		System.out.println("开始时间:" + start);
		list.add("需要查找的数据");
		list.add("eeee");
		list.add("aaee");
		list.add("abbb");
		for (int i = 0; i < 10000000; i++) {
			for (int j = 0; j < list.size(); j++) {
				list.get(j).contains("e");
				System.out.print("");
			}
		}
		long end = System.currentTimeMillis();
		System.out.println("结束时间:" + end);
		System.out.println("总耗时:" + (end - start));
	}

	public static void main(String[] args) {
		List<String> a = new ArrayList<>();
		List<String> b = new LinkedList<>();
		addTest(a);
		addTest(b);
	}
}


在这里插入图片描述

???不是说ArrayList查询快于LinkedList么?

然后我们测试增加
在这里插入图片描述
???简直颠覆我的认知!
难道我学的都是错的吗?
事情的真相只有一个!
首先我们需要了解
linkedLIst是双向链表结构
在这里插入图片描述
元素之间的所有关系是通过引用关联的,就好比最近特别火的从袖子里撤出棒棒糖来的情景,想要撤出下一个就必须撤出上一个。它在查询的时候,只能一个一个的遍历查询,所以他的查询效率很低,如果我们想删除一节怎么办呢?就相当于自行车的链子,有一节坏了,我们是不是直接把坏的那节仍掉,然后让目标节的上一节指向目标节的下一节,但是
ArrayList是数组结构
在这里插入图片描述
就是有相同特性的一组数据的箱子,比如说我有一个能容下10个苹果的箱子,我现在只放了5个苹果,那么放第6个是不是直接放进去就行了?呢我要放11个呢?这个箱子是不是放不下了?所以我是不是需要换个大点的箱子?这就是**数组的扩容!**同样,我们一般放箱子里面的东西是不是按照顺序放的?假如说是按abcd的顺序放的,我突然想添加一个e,这个e要放到c的后面,你是不是需要把d先拿出来,再把e放进去,再把d放进去?假如说c后面有10000个呢?你是不是要把这10000个都拿出来,把e放进去,再放这10000个?效率是不是很低了?所以,理论上它的增删比较慢!但是前面也说了,我们箱子里面放东西,都是按照顺序放的,所以我知道其中一个"地址",是不是就知道所有元素的地址?所以它的查询在理论上比较快!
注意:只是在在list容量较大情况下,ArrayList查询数据要远优于LinkedList

在这里插入图片描述
在这里插入图片描述
总结
ArrayList和LinkedList在性能上各有优缺点,都有各自所适用的地方,总的说来可以描述如下:
对ArrayList而言,主要是在内部数组中增加一项数据,指向所添加的元素,偶尔复杂度为O(n)可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,复杂度为O(1),分配一个内部对象。
在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。

  1. LinkedList不支持高效的随机元素访问。
  2. 只是在在list容量较大情况下,ArrayList查询数据要远优于LinkedList
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章