Java 基础学习笔记 —— 集合中的List

引言

在上一篇文章中,我们对集合做了一个简要对分析,讲述了我们使用集合时需要注意对问题。而在接下来对这一系列文章里,我们将对Java集合中一些具体的类,如List列表,Set集合,Queue队列,进行更具体的剖析。

这篇文章主要针对集合中可能是最为常用的类,List列表进行分析。分析包含三部分,第一部分意图从其类继承图以及具体的方法分配,来剖析其设计原理。第二部分则会针对几个重要的列表实现进行具体分析。

List类图分析

列表类继承图

关于类图

如果你知道CopyOnwirteArrayList是在java.util.concurrent包中实现的话,就不难发现所有在java.util的包中的列表实现,都继承了AbstractList。这样的类结构实际上是跟设计模式中的开闭原则是有关的。

继承开放,修改封闭。实际上就是指,在AbstractList中,已经提供了一部分列表的默认行为,而如果我们需要实现一个自定义的列表,只需要实现那些未被实现的方法就可以了。

RandomAccess接口

其次,在列表里面,不难发现列表主要分为两种,一种是继承了RandomAccess接口的,而另外一种则没有继承这个接口的。这区分了两种具体的类实现,一种采用数组形式进行存储,查询时间复杂度为O(1),插入/删除时间复杂度为O(n),另外一种则采用了链表形式进行存储,查询时间复杂度为O(n),插入/删除时间复杂度为O(1)。

具体的实现

自定义一个List

如果想要快速了解List的具体实现的差异,最好的方式就是自定义一个List,并且实现相应的功能。我们首先实现在AbstractList中的抽象方法,其次,我们还需要实现在AbstractList中抛出了UnsupportedOperationException的方法。具体需要实现的方法如下:

public class AbstractListImpl<E> extends AbstractList<E> {

    @Override
    public E get(int index) { return null; }

    @Override
    public int size() { return 0; }

    public E set(int index, E element) { return null; }

    public E remove(int index) { return null;}

    public void add(int index, E element) {}
}

不难发现,其实我们要实现的,就是curd方法,另外再加上一个获取列表大小的size()。在这里可能大家会有疑惑,为什么在AbstractList中会把setremove方法实现并且抛出UnsupportedOperationException呢。其实是因为有部分列表的实现的确是不能进行这些修改操作的,如UnmodifableArrayList,因此就把这些实现直接放到抽象类里作为默认实现了,只保留两个必须实现的方法。

ArrayList的CURD实现

在了解ArrayList的CURD实现之前,我们先看一下其内部成员变量

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
	//序列化时使用到的UID
    private static final long serialVersionUID = 8683452581122892189L;
	
	//默认容量,注意这个默认容量并不是创建一个ArrayList之后的默认容量,而是在创建一个ArrayList且进行了一个元素添加后的默认容量。
    private static final int DEFAULT_CAPACITY = 10;
    
    //目前暂时没有用处,只有显示地声明一个容量为0的列表,才会将elementData指向这个变量
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    //我们直接通过无参构造函数创建列表时,elementData指向的值
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
	
	//具体存放元素的数组(也是因为这个数据结构,ArrayList支持随机访问)
    transient Object[] elementData; 
	
	//列表大小
    private int size;
}

通过对内部成员变量的查看,我们知道了如果简单地使用ArrayList list = new ArrayList();这样的语句去创建列表,实际上是会创建一个空列表,然后在我们进行元素添加的时候,就会触发扩容操作。

接下来我们看一下具体的CURD实现。

add方法

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

具体执行的流程图如下图所示:

Array 添加元素

所以在添加元素的时候,若列表的空间不足,则会扩容至MAX(原有1.5倍容量,所需最小空间)。若发现已经超过了整型能够表示的最大值(溢出),则会抛出OOM异常。

remove 移除某个元素

	public E remove(int index) {
        rangeCheck(index);//首先检查是否已经超出范围

        modCount++;//版本号+1,处理并发的情况
        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;
    }

可以看到,删除元素操作,时间复杂度为O(n),且需要调用系统的arrayCopy来将原来的值拷贝到新的值上。

get方法,支持随机访问,时间复杂度O(1)

	 public E get(int index) {
        rangeCheck(index);
		//支持随机访问,可以直接返回某个元素
        return elementData(index);
    }

从以上代码片段我们能够得知,ArrayList通过一个Object数组来对元素进行保存,如果直接使用无参构造函数进行对象创建,其初始化的数组容量为0,并且会在首次进行元素添加时将数组的容量扩增为10。如果数组中添加的元素超过10个,那么就会在每次添加的时候使用MAX(oldLength* 1.5, MinLength)的策略进行扩容,其最多能够存放的元素数量为Integer.MAX_VALUE(若元素数量过多,首先会采用Integer.MAX_VALUE - 8,然后才是Integer.MAX_VALUE)。

Vector的CURD实现

同样,在了解Vector的内部实现时,我们也需要先了解一下Vector的内部成员变量。

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
	protected Object[] elementData;//仍然是以数组的形式来对元素进行存放
	protected int elementCount;//数组元素数量
	protected int capacityIncrement;//可以显式地定义每次扩容的大小
	private static final long serialVersionUID = -2767605614048989439L;//用于序列化的UID
}

接下来还是看一下add方法

	public synchronized boolean add(E e) {
        modCount++;//版本号+1
        ensureCapacityHelper(elementCount + 1);//确保容纳量
        elementData[elementCount++] = e;
        return true;
    }

Vector添加元素

具体容量确认过程如上图所示。与ArrayList不同,Vector可以选择每次扩容的大小,若未设置(初始化值未0),则默认扩容为原来的一倍。

然后就是查询方法,get

	public synchronized E get(int index) {
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
		//支持随机访问,可以直接返回某个序号的元素
        return elementData(index);
    }

解析来是删除元素方法remove。

	public synchronized E remove(int index) {
	    modCount++;
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
        E oldValue = elementData(index);

        int numMoved = elementCount - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--elementCount] = null; // Let gc do its work

        return oldValue;
    }

从以上代码片段能够了解到,Vector与ArrayList的实现基本一致,但是在细节上又有细微差别,如Vector可自定义扩容量,默认扩大一倍。另外,还有一个重要区别就是,Vector的所有方法都使用了sychronized关键字进行修饰,也就是说Vector是线程安全的。

而Stack则只是Vector的一个子类,并没有对CURD方法进行改写,因此在这里就不展开描述了。

LinkedList的CURD

首先还是来看看其内部变量。

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;
    //头指针
    transient Node<E> first;
    //尾指针
    transient Node<E> last;
}

以及具体Node的结构,有头指针和尾指针,也就是说我们能够从前到后遍历这个链表,也能够从后向前遍历这个链表。

	private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

实际上这个类的构造函数也就表明了这个链表的插入方式。
在这个子类中我们能够发现一些提高效率的地方,如对插入位置的判断,若插入位置在列表的前半段,则通过头指针进行遍历插入。

接下来看一下add方法。

	public boolean add(E e) {
        linkLast(e);//将节点放到最后,时间复杂度O(1)
        return true;
    }

然后是get方法

	public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

再深入看一下node方法

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

不难发现,这里也做了一点小的优化,就是首先判断获取的node位于前半段还是后半段,若是再前半段,则从头指针开始遍历,若再后半段,则从尾指针开始遍历。

最后是remove方法

	public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

remove方法相当于是首先找到需要移除的节点,然后再对该节点进行unlink。

从上面的代码片段可以看出,对于LinkedList来说,其优势就是插入效率非常高(不需要进行扩容),而劣势就是查询的时候时间复杂度为O(n)。

小结

在本文中,主要分析了集合中的List,其中又针对了三个具体的实现类——ArrayList, Vector,LinkedList进行了较为深入的分析。List的具体实现主要分为两种,是否支持随机访问。这个选择决定了它们存储元素的数据结构(数组和链表),也影响了它们的查询/插入/删除效率。

在使用随机访问列表的时候,我们需要注意的是,列表不应该简单通过无参构造函数进行创建。实际上我们应该对这个随机访问列表的容量有一个估算,并且配以适合的初始化参数,使得列表扩容的次数尽可能的少(减少对空间的占用以及时间的开销)。

而对于非随机访问的列表,实际上如果查询次数不多,反而会是一个更优的选择。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值