Java的List接口

目录

List接口简介

List实现类之一:ArrayList

List实现类之二:LinkedList

List实现类之三:Vector


List接口简介

List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。

List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。

JDK API中List接口的实现类常用的有:ArrayListLinkedListVector

List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法:

方法描述
void add(int index, Object ele)在index位置插入ele元素
boolean addAll(int index, Collection eles)从index位置开始将eles中的所有元素添加进来
Object get(int index)获取指定index位置的元素
int indexOf(Object obj)返回obj在集合中首次出现的位置
int lastIndexOf(Object obj)返回obj在当前集合中末次出现的位置
Object remove(int index)移除指定index位置的元素,并返回此元素
Object set(int index, Object ele)

设置指定index位置的元素为ele

List subList(int fromIndex, int toIndex)返回从fromIndex到toIndex 位置的子集合

List实现类之一:ArrayList

ArrayList 是 List 接口的典型实现类,本质上,ArrayList是对象引用的一个变长数组。

ArrayList 是 List 接口的可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。

ArrayList 是线程不安全的,而 Vector 是线程安全的,即使为保证 List 集合线程安全,也不推荐使用Vector。

Arrays.asList(…) 方法返回的 List 集合既不是 ArrayList 实例,也不是 Vector 实例。Arrays.asList(…) 返回值是一个固定长度的 List 集合。

ArrayList 继承了 AbstractList ,并实现了 List 接口。

 ArrayList的方法

方法描述
add()将元素插入到指定位置的 arraylist 中
addAll()添加集合中的所有元素到 arraylist 中
clear()删除 arraylist 中的所有元素
clone()复制一份 arraylist
contains()判断元素是否在 arraylist
get()通过索引值获取 arraylist 中的元素
indexOf()返回 arraylist 中元素的索引值
removeAll()删除存在于指定集合中的 arraylist 里的所有元素
remove()删除 arraylist 里的单个元素
size()返回 arraylist 里元素数量
isEmpty()判断 arraylist 是否为空
subList()截取部分 arraylist 的元素
set()替换 arraylist 中指定索引的元素
sort()对 arraylist 元素进行排序
toArray()将 arraylist 转换为数组
toString()将 arraylist 转换为字符串
ensureCapacity()设置指定容量大小的 arraylist
lastIndexOf()返回指定元素在 arraylist 中最后一次出现的位置
retainAll()保留 arraylist 中在指定集合中也存在的那些元素
constainsAll()查看 arraylist 是否包含指定集合中的所有元素
trimToSize()将 arraylist 中的容量调整为数组中的元素个数
removeRange()删除 arraylist 中指定索引之间存在的元素
repaceAll()将给定的操作内容替换掉数组中每一个元素
removeIf()删除所有满足特定条件的 arraylist 元素
forEach()遍历 arraylist 中每一个元素并执行特定操作

ArrayList的实现原理

属性

//默认容量的大小
private static final int DEFAULT_CAPACITY = 10;
//空数组常量
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认的空数组常量
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存放元素的数组,从这可以发现 ArrayList 的底层实现就是一个 Object数组
transient Object[] elementData;
//数组中包含的元素个数
private int size;
//数组的最大上限
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

ArrayList的属性非常少,就只有这些。其中最重要的莫过于elementData了,ArrayList所有的方法都是建立在 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);
    } 
}

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

从构造方法中我们可以看见,默认情况下,elementData 是一个大小为 0 的空数组, 当我们指定了初始大小的时候,elementData 的初始大小就变成了我们所指定的初始大小了。

get() 方法

// 返回此列表中指定位置上的元素。
public E get(int index) {
    RangeCheck(index);
    return (E) elementData[index];
}

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

E elementData(int index) {
    return (E) elementData[index];
}

因为 ArrayList 是采用数组结构来存储的,所以它的 get 方法非常简单,先是判断一下有没有越界,之后就可以直接通过数组下标来获取元素了,所以 get 的时间复杂度是 O(1)。 访问 ArrayList 中的元素可以使用 get() 方法。

add() 方法

// 将指定的元素添加到此列表的尾部。
public boolean add(E e) {
    ensureCapacity(size + 1);
    elementData[size++] = e;
    return true;
}
// 将指定的元素插入此列表中的指定位置。
// 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加 1)。
public void add(int index, E element) {
    if (index > size || index < 0)
          throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);

    // 如果数组长度不足,将进行扩容。
    ensureCapacity(size+1); // Increments modCount!!
    // 将 elementData 中从 Index 位置开始、长度为 size-index 的元素,
    // 拷贝到从下标为 index+1 位置开始的新的 elementData 数组中。
    // 即将当前位于该位置的元素以及所有后续元素右移一个位置。
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    elementData[index] = element;
    size++;
}
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
    grow(minCapacity);
}

ArrayList 的 add 方法也很好理解,在插入元素之前,它会先检查是否需要扩容,然后再把元素添加到数组中最后一个元素的后面。

在 ensureCapacityInternal 方法中, 我们可以看见,如果当 elementData 为空数组时,它会使用默认的大小去扩容。所以说,通过无参构造方法来创建 ArrayList 时,它的大小其实是为 0 的,只有在使用到的时候,才会通过 grow 方法去创建一个大小为 10 的数组。

第一个 add 方法的复杂度为 O(1),虽然有时候会涉及到扩容的操作,但是扩容的次数是非常少的,所以这一部分的时间可以忽略不计。如果使用的是带指定下标的 add 方法,则复杂度为 O(n),因为涉及到对数组中元素的移动,这一操作是非常耗时的。

addAll() 方法

// 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部
public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    // 如果数组长度不足,将进行扩容。
    ensureCapacity(size + numNew); // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}
// 从指定的位置开始,将指定 collection 中的所有元素插入到此列表中。
public boolean addAll(int index, Collection<? extends E> c) {
    if (index > size || index < 0)

         throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacity(size + numNew); // Increments modCount
    int numMoved = size - index;
    if (numMoved > 0)

         System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
    System.arraycopy(a, 0, elementData, index, numNew);
    size += numNew;
    return numNew != 0;
}

set() 方法

// 用指定的元素替代此列表中指定位置上的元素,并返回以前位于该位置上的元素。
public E set(int index, E element) {
    RangeCheck(index);
    E oldValue = (E) elementData[index];
    elementData[index] = element;
    return oldValue;
}
set 方法的作用是把下标为 index 的元素替换成 element ,跟 get 非常类似,所以就不再赘述了,时间复杂度度为 O(1)

remove() 方法

public E remove(int index) {
    rangeCheck(index);
    modCount++;
    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;
}
remove 方法与 add 带指定下标的方法非常类似,也是调用系统的 arraycopy 方法来移动元素,时间复杂度为 O(n)。

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);
}
grow 方法是在数组进行扩容的时候用到的,从中我们可以看见, ArrayList 每次扩容都是扩 1.5 倍,然后调用 Arrays 类的 copyOf 方法,把元素重新拷贝到一个新的数组中去。

size() 方法

public int size() {
    return size;
}
size 方法非常简单,它是直接返回 size 的值,也就是返回数组中元素的个数,时间复杂度为 O(1) 。这里要注意一下,返回的并不是数组的实际大小。

indexOf() 方法

public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1; 
}
indexOf 方法的作用是返回第一个等于给定元素的值的下标。它是通过遍历比较数组中每个元素的值来查找的,所以它的时间复杂度是 O(n)

lastIndexOf() 方法

public int lastIndexOf(Object o) {
    if (o == null) {
        for (int i = size-1; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = size-1; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1; 
}
lastIndexOf 的原理跟 indexOf 一样,而它仅仅是从后往前找起罢了。

数组(Array)和列表(ArrayList)有什么区别?

        1)Array可以包含基本类型和对象类型(引用类型), ArrayList只能包含对象类型。

        2)Array 大小是固定的, ArrayList 的大小是动态变化的。

        3)ArrayList 提供了更多的方法和特性,比如: addAll(), removeAll(), iterator()等等。

什么时候应该使用 Array 而不是 ArrayList?

对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。

List实现类之二:LinkedList

链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的地址。

与 ArrayList 相比,LinkedList 的增加和删除的操作效率更高,而查找和修改的操作效率较低。对于频繁的插入或删除元素的操作,建议使用LinkedList类,效率较高。

LinkedList 继承了 AbstractSequentialList 类。实现了 Queue 接口,可作为队列使用。 实现了 List 接口,可进行列表的相关操作。实现了 Deque 接口,可作为队列使用。实现了 Cloneable 接口,可实现克隆。实现了 java.io.Serializable 接口,即可支持序列化,能通过序列化去传输。

链表可分为单向链表和双向链表。

一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接。

 一个双向链表有三个整数值: 数值、向后的节点链接、向前的节点链接。


 

LinkedList的方法:

方法描述
public boolean add(E e)链表末尾添加元素,返回是否成功,成功为 true,失败为 false。
public void add(int index, E element)向指定位置插入元素。
public boolean addAll(Collection c)将一个集合的所有元素添加到链表后面,返回是否成功,成功为 true,失败为 false。
public boolean addAll(int index, Collection c)将一个集合的所有元素添加到链表的指定位置后面,返回是否成功,成功为 true,失败为 false。
public void addFirst(E e)元素添加到头部。
public void addLast(E e)元素添加到尾部。
public boolean offer(E e)向链表末尾添加元素,返回是否成功,成功为 true,失败为 false。
public boolean offerFirst(E e)头部插入元素,返回是否成功,成功为 true,失败为 false。
public boolean offerLast(E e)尾部插入元素,返回是否成功,成功为 true,失败为 false。
public void clear()清空链表。
public E removeFirst()删除并返回第一个元素。
public E removeLast()删除并返回最后一个元素。
public boolean remove(Object o)删除某一元素,返回是否成功,成功为 true,失败为 false。
public E remove(int index)删除指定位置的元素。
public E poll()删除并返回第一个元素。
public E remove()删除并返回第一个元素。
public boolean contains(Object o)判断是否含有某一元素。
public E get(int index)返回指定位置的元素。
public E getFirst()返回第一个元素。
public E getLast()返回最后一个元素。
public int indexOf(Object o)查找指定元素从前往后第一次出现的索引。
public int lastIndexOf(Object o)查找指定元素最后一次出现的索引。
public E peek()返回第一个元素。
public E element()返回第一个元素。
public E peekFirst()返回头部元素。
public E peekLast()返回尾部元素。
public E set(int index, E element)设置指定位置的元素。
public Object clone()克隆该列表。
public Iterator descendingIterator()返回倒序迭代器。
public int size()返回链表元素个数。
public ListIterator listIterator(int index)返回从指定位置开始到末尾的迭代器。
public Object[] toArray()返回一个由链表元素组成的数组。
public T[] toArray(T[] a)返回一个由链表元素转换类型而成的数组。

LinkedList的实现原理

属性

//链表的节点个数
transient int size = 0;
//指向头节点的指针
transient Node<E> first;
//指向尾节点的指针
transient Node<E> last;

LinkedList 的属性非常少,就只有这些。通过这三个属性,其实我们大概也可以猜测出它是怎么实现的了。

结点结构

 双向链表每个结点除了数据域之外,还有一个前指针和后指针,分别指向前驱结点和后继结点(如果有前驱/后继的话)。另外,双向链表还有一个 first 指针,指向头节点,和 last 指针,指向尾节点。

//Node 是在 LinkedList 里定义的一个静态内部类,它表示链表每个节点的结构,
//包括一个数据域 item,一个后置指针 next,一个前置指针 prev。
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;
    } 
}

添加元素

对于链表这种数据结构来说,添加元素的操作无非就是在表头/表尾插入元素,又或者在指定位置插入元素。因为 LinkedList 有头指针和尾指针,所以在表头或表尾进行插入元素只需要 O(1) 的时间,而在指定位置插入元素则需要先遍历一下链表,所以复杂度为 O(n)。

在表头添加元素的过程如下:

当向表头插入一个节点时,很显然当前节点的前驱一定为 null,而后继结点是 first 指针指向的节点,当然还要修改 first 指针指向新的头节点。除此之外,原来的头节点变成了第二个节点,所以还要修改原来头节点的前驱指针,使它指向表头节点,源码的实现如下:

private void linkFirst(E e) {
    final Node<E> f = first;
    //当前节点的前驱指向 null,后继指针原来的头节点
    final Node<E> newNode = new Node<>(null, e, f);
    //头指针指向新的头节点
    first = newNode;
    //如果原来有头节点,则更新原来节点的前驱指针,否则更新尾指针
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

在表尾添加元素跟在表头添加元素大同小异,如图所示:

当向表尾插入一个节点时,很显然当前节点的后继一定为 null,而前驱结点是 last 指针指向的节点,然后还要修改 last 指针指向新的尾节点。此外,还要修改原来尾节点的后继指针,使它指向新的尾节点,源码的实现如下:

void linkLast(E e) {
    final Node<E> l = last;
    //当前节点的前驱指向尾节点,后继指向 null
    final Node<E> newNode = new Node<>(l, e, null);
    //尾指针指向新的尾节点
    last = newNode;
    //如果原来有尾节点,则更新原来节点的后继指针,否则更新头指针
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

最后,在指定节点之前插入,如图所示:

当向指定节点之前插入一个节点时,当前节点的后继为指定节点,而前驱结点为指定节点的前驱节点。此外,还要修改前驱节点的后继为当前节点,以及后继节点的前驱为当前节点,源码的实现如下:

void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    //指定节点的前驱
    final Node<E> pred = succ.prev;
    //当前节点的前驱为指点节点的前驱,后继为指定的节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    //更新指定节点的前驱为当前节点
    succ.prev = newNode;
    //更新前驱节点的后继
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

删除元素

删除操作与添加操作大同小异,例如删除指定节点的过程如下图所示,需要把当前节点的前驱节点的后继修改为当前节点的后继,以及当前节点的后继结点的前驱修改为当前节点的前驱:

删除头节点和尾节点跟删除指定节点非常类似,就不一一介绍了,源码如下:

//删除表头节点,返回表头元素的值
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next; //头指针指向后一个节点
    if (next == null)
        last = null;
    else
        next.prev = null; //新头节点的前驱为 null
    size--;
    modCount++;
    return element;
}
//删除表尾节点,返回表尾元素的值
private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    final E element = l.item;
    final Node<E> prev = l.prev;
    l.item = null;
    l.prev = null; // help GC
    last = prev; //尾指针指向前一个节点
    if (prev == null)
        first = null;
    else
        prev.next = null; //新尾节点的后继为 null
    size--;
    modCount++;
    return element;
}
//删除指定节点,返回指定元素的值
    E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next; //当前节点的后继
    final Node<E> prev = x.prev; //当前节点的前驱
    if (prev == null) {
        first = next;
    } else {
        prev.next = next; //更新前驱节点的后继为当前节点的后继
        x.prev = null;
    }
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev; //更新后继节点的前驱为当前节点的前驱
        x.next = null;
    }
    x.item = null;
    size--;
    modCount++;
    return element;
}

获取元素

获取元素的方法一看就懂,我就不必多加解释了。

//获取表头元素
public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}
//获取表尾元素
public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}
//获取指定下标的元素
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;
    } 
}

ArrayList 和 LinkedList 的区别:

1)ArrayList 是基于索引的数据接口,它的底层是数组。它可以以 O(1)时间复杂度对元素进行随机访问。与此对应, LinkedList 是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是 O(n)。

2)相对于 ArrayList, LinkedList 的插入,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。

3)LinkedList 比 ArrayList 更占内存,因为 LinkedList 为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。

List实现类之三:Vector

Vector是一个古老的集合,JDK1.0就有了。大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。

在各种list中,最好把ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList;Vector总是比ArrayList慢,所以尽量避免使用。

Vector新增方的法:

        void addElement(Object obj)

        void insertElementAt(Object obj,int index)

        void setElementAt(Object obj,int index)

        void removeElement(Object obj)

        void removeAllElements()

Vector很多方法都跟 ArrayList 一样,只是多加了个 synchronized 来保证线程安全罢了。

Vector的属性

//Vector 比 ArrayList 多了一个属性
protected int capacityIncrement;

这个属性是在扩容的时候用到的,它表示每次扩容只扩 capacityIncrement 个空间就足够了。该属性可以通过构造方法给它赋值。

Vector的构造方法

public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}

public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}

public Vector() {
    this(10);
}

从构造方法中,我们可以看出 Vector 的默认大小也是 10,而且它在初始化的时候就已经创建了数组了,这点跟 ArrayList 不一样。

grow()方法

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

从 grow 方法中我们可以发现,newCapacity 默认情况下是两倍的 oldCapacity,而当指定了 capacityIncrement 的值之后,newCapacity 变成了oldCapacity+capacityIncrement。所以,Vector 每次扩容都以当前数组大小的 2 倍去扩容。当指定了 capacityIncrement 之 后,每次扩容仅在原先基础上增加 capacityIncrement 个单位空间。

ArrayList与Vector的区别:

比较点

ArrayList

Vector

推出时间

JDK1.2之后推出的,属于新的操作类

JDK1.0时推出的,属于旧的操作类

性能

采用异步处理方式,性能更高

采用同步处理方式,性能较低

线程安全

属于非线程安全的操作类

属于线程安全的操作类

输出

只能使用Iterator、foreach输出

可以使用Iterator、foreach、Enumeration输出

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值