Java容器(一)——Collection体系详解及源码分析

本文深入探讨了Java集合框架中的ArrayList、Vector和LinkedList,分析了它们的底层数据结构、线程安全性和操作性能。ArrayList线程不安全但效率高,适合查找;Vector线程安全但效率低;LinkedList适合增删操作。此外,还介绍了HashSet、LinkedHashSet和TreeSet的特性,以及它们的底层实现和排序机制。最后,简要概述了Queue和Deque接口及其在Java容器中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

Java的集合框架十分强大,它将常用的数据结构和算法进行封装,使得Java开发者无需精通底层数据结构和算法就可以轻松使用集合框架的API。例如最常见的数组、链表以及队列,栈再到二叉树,红黑树,JDK都将其进行不同程度的封装,极大提升了开发人员的开发效率。不过虽然我们在项目中无需直接接触此类底层逻辑,但我们还是需要熟练掌握其内容。因为在极端的业务场景下,我们可能需要对其底层数据结构进行调试甚至优化,所以熟练掌握底层结构也是必备技能。

以下是一张Collection接口图。

Collection接口图
从图中我们可以看到,Collection接口拥有三个主要的子接口,分别是:SetListQueue

详解-List

我们首先来讲解List接口。

List的特点

  • 存储的元素是有序(存储和取出的顺序)的。
  • 同一元素可重复存储(可重复)。
  • 可通过索引值操作元素。

List接口下包含有ArrayListVectorLinkedList三个类分别实现了List接口。

接下来我们来详细讨论其的区别。

我们根据底层数据结构的不同来将其分为两大类:

  1. 底层是数组。数组查询快,增删慢。
  2. 底层是链表。链表查询慢,增删快。

ArrayList和Vector属于底层是数组的分类。

ArrayList和Vector的区别?

  • ArrayList线程不安全,但效率高,适用于频繁的查找工作。
  • Vector线程安全,但效率低。其也是Java早期提供线程安全的动态数组,不推荐在实际生产环境中使用。

源码分析

ArrayList.java


    transient Object[] elementData; // non-private to simplify nested class access

    private int size;

    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中使用的是Object[]数组,并且查阅ArrayList源码,发现类中没有使用到synchronized和其他任何锁的关键字,故ArrayList是线程不安全。


    public synchronized void copyInto(Object[] anArray) {
        System.arraycopy(elementData, 0, anArray, 0, elementCount);
    }

    public synchronized void trimToSize() {
        modCount++;
        int oldCapacity = elementData.length;
        if (elementCount < oldCapacity) {
            elementData = Arrays.copyOf(elementData, elementCount);
        }
    }

    public synchronized void ensureCapacity(int minCapacity) {
        if (minCapacity > 0) {
            modCount++;
            ensureCapacityHelper(minCapacity);
        }
    }

Vector类中,同样使用了数组的结构,方法中都带有synchronized关键字,这就使得Vector天生线程安全。但大量使用同步锁造成在高并发场景下,将会有大量阻塞的线程等待,效率地下。

LinkedList

我们再来看看LinkedList。LinkedList底层是链表结构,线程不安全,效率高。

接下来我们来探讨ArrayList和LinkedList的区别。

ArrayList和LinkedList的区别

  • ArrayList底层采用的是Object数组,而LinkedList底层采用的是双向链表的结构。

注:JDK1.6(包含)和之前采用的是双向循环链表,JDK1.7开始取消了循环。

引用 - 双向链表:包含两个指针,一个 prev 指向前一个节点,一个 next 指向后一个节点。

引用 - 双向循环链表:最后一个节点的 next 指向 head,而 head 的 prev 指向最后一个节点,构成一个环。

  • ArrayList 插入和删除的时间复杂度受元素位置的影响。原因如下:ArrayList会将元素追加到末尾,此时,时间复杂度为O(1)。这时我们通过索引在指定的位置i插入或删除元素,那么时间复杂度则为O(n-i) (n为数组的长度)。这是因为数组中第i个和第i个之后的(n-i)个元素都要跟着向后或向前移动一位。
  • LinkedList 插入和删除的时间复杂度不受元素位置影响。因为其数据结构为双向链表,所以在add(E e)addFirst(E e)addLast(E e)removeFirst()removeLast()操作中,其时间复杂度为O(1),而通过索引下标i进行插入或删除操作时间复杂度为O(n)。这是因为链表指针需要移动到指定位置后插入。
  • ArrayList支持RandomAccess(即高效的随机访问),而LinkedList则不支持。

源码分析

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

上图为LinkedList的部分源码。LinkedList类定义了firstlast两个Node结点,分别指向头元素和尾元素,是典型的链表结构。

LinkedList类没有用到锁,故其也是线程不安全的。

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

ArrayList实现了RandomAccess,这个接口与Cloneable一样,只是作为一个标识。说明ArrayList支持高效的随机访问,实现Cloneable说明该类支持克隆。同时,实现了Serializable接口说明该类可被序列化。

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{...}

可以看到LinkedList并没有支持高效的随机访问。其实现了一个双端队列Deque,其余部分与ArrayList同理。

ArrayList的扩容机制

ArrayList是动态数组,与传统数组相比,它的容量能够动态增长。在源码中,扩容的核心方法是grow(),此处我们详细分析grow()方法,若要了解详细的扩容机制,可自行查阅源码或搜索相关资料。

源码分析

    // minCapacity: 最小需要容量
    private void grow(int minCapacity) {
        // oldCapacity: 旧容量,newCapacity: 新容量
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 此处MAX_ARRAY_SIZE常量值为:Integer.MAX_VALUE - 8
        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);
    }

解释:int newCapacity = oldCapacity + (oldCapacity >> 1);此语句的意思是将oldCapacity右移一位的运算,其效果相当于oldCapacity / 2。此处使用为位运算而不直接使用除法运算目的在于:位运算速度比除法运算快得多,有助于提升效率,节省资源。

grow方法的逻辑:先将oldCapacity右移一位,然后判断新容量是否小于最小需要容量,若条件满足(即newCapacity < minCapacity),则将新容量设为传入的最小需要容量。

若新容量大于MAX_ARRAY_SIZE,则调用hugeCapacity()方法来比较minCapacityMAX_ARRAY_SIZE
hugeCapacity代码如下:

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

即:如果minCapacity大于最大容量,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8

最后,使用Arrays.copyOf();方法生成一个新的扩容好的数组,并将其赋值给原始数组即elementData。这就实现了数组的扩容操作。

此外,ArrayList还提供了可供外部调用的扩容方法ensureCapacity

    public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

通过源码可以看出,ensureCapacity方法使用的是public修饰符,可供开发者进行调用。在添加大量元素前,开发者可以通过ensureCapacity动态地为数组扩展容量,这样可以避免频繁地扩容带来的性能损失。此处不是此篇的重点,具体感兴趣可自行查看源码。

详解-Set

Set也是Collection的重要子接口之一。从文章开头的框架图可以看出,Set下包含HashSetLinkedHashSetTreeSet等子接口。

Set的特点

  • 无序性:即存储的元素是无序(存储和取出的顺序)的。
  • 唯一性:所有的元素都是唯一的。

Set适合应用在需要去重的场景中。

Set进行分类,我们同样能够将其分为两大类:

  1. 底层是哈希表,即HashMap,在下文源码中可以看出。具体HashMap的实现,我将于下一篇进行介绍。
  2. 底层是二叉树。

HashSetLinkedHashSet属于第一种分类,即底层是哈希表(LinkedHashSet底层是哈希表+链表)。TreeSet则属于底层是二叉树的分类。

HashSet使用hashCode()equals()来保证元素的唯一性。

源码分析

我们来看HashSet的两个源码片段。

public HashSet(int initialCapacity) {
    map = new HashMap<>(initialCapacity);
}
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

从源码可以看出HashSet在初始化时创建了一个HashMap对象。在添加元素的时候,我们可以看到,元素其实是以键的形式存储在HashMap中,对应的值为一个PRESENT的常量。PRESENT的值实际上是一个finalObject对象。这就解释了HashSet如何保证元素的唯一性。

HashSet常用于保证元素唯一性的场景,而TreeSet常用于对排序有要求的场景。在不需要排序的场景下使用HashSet可以获得更好的性能。

谈起排序,我们来具体讨论一下ComparableComparator接口。

  • Comparable接口在java.lang包,它有一个 compareTo(Object obj)方法用于排序。
  • Comparator接口在java.util包,它有一个compare(Object obj1, Object obj2)方法用于排序。

Comparable和Comparator接口的区别

  • Comparable接口是一个自然排序接口,它用于实现自然排序,compareTo也称为自然比较方法。当开发者调用Collections中的sort方法时,对象可按照自身实现的Comparable进行自然排序。这个对象必须实现Comparable接口。
  • Comparator接口可以理解为定制化排序接口,开发者可对没有实现Comparable接口的类进行定制化排序,而无需修改类的源码。

TreeSet源码分析

TreeSet类就同时提供了ComparableComparator两种排序的方式。具体我们追溯到源码。

public TreeSet() {
    this(new TreeMap<E,Object>());
}

public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
}

通过源码可以看出,TreeSet底层是通过TreeMap实现的,所以我们需要进一步进入到TreeMap中查看具体排序的逻辑。TreeSet的构造函数提供了一个无参构造函数和一个有参构造函数。无参构造函数在TreeMap中将会使用Comparable的自然排序,而有参构造函数传入了一个Comparator对象。

现在我们进入到TreeMap中进行查看。

final Entry<K,V> getEntry(Object key) {
    // Offload comparator-based version for sake of performance
    if (comparator != null)
        return getEntryUsingComparator(key);
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
    Entry<K,V> p = root;
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < 0)
            p = p.left;
        else if (cmp > 0)
            p = p.right;
        else
            return p;
    }
    return null;
}

通过源码可以看出,在comparator不为空的时候,getEntry方法会调用getEntryUsingComparator,进而就调用了我们自定义的排序方法。默认则是通过Comparable接口进行自然排序。

HashSet、LinkedHashSet、TreeSet比较

  • HashSetLinkedHashSetTreeSet都是Set接口的实现类,都能保证元素唯一,都不是线程安全的。
  • HashSet底层数据结构为哈希表,基于HashMap进行实现。LinkedHashSet的底层数据结构是链表+哈希表。元素的插入和取出顺序是先进先出TreeSet的底层数据结构是红黑树(自平衡二叉查找树)。
  • HashSet适用于不需要保证元素插入和取出顺序和不关心排序的场景,LinkedHashSet适用于保证元素的插入和取出顺序满足先进先出的场景,TreeSet适用于需要自定义排序规则的场景。

补充-Queue

Queue是单端队列,即元素只能从一段插入,另一端删除,一般遵循FIFO即先进先出原则。它扩展了Collection接口。

Deque是双端队列,即队列两队均可插入或删除元素。它扩展了Queue接口,并且提供pushpop等特殊方法,可用于模拟栈结构。

Deque接口下用得较多的就是LinkedList类,具体可查看前文内容。

总结

在本文的末尾,我总结了一个表格对比了上文讲述的对象的特点。

对象底层数据结构是否线程安全
ArrayList数组
Vector数组
LinkedList链表
HashSet哈希表
LinkedHashSet哈希表+链表
TreeSet红黑树

Java容器中的Collection是一个面试常考/必考的知识点,并且在开发中也十分常用。熟练掌握Collection体系内容是极其必要的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值