Java集合框架常用知识点总结

基本知识

  1. Java中,集合用于存储一组对象的集。集合类存放的都是对象的引用,而非对象本身,出于表达上的便利,我们称集合中的对象就是指集合中对象的引用。
  2. Java中,集合类存放于java.util包中。
  3. Java中集合类的高层接口有Collection和Map接口,其中Collection用于存储普通的对象类型,Map用于存储key-value形式的对象。
  4. 继承Collection接口的主要有List,Set,Queue,实现Map接口的主要有HashMap、TreeMap等
  5. 集合框架的简单架构关系如下图所示
    avatar

Collection接口相关内容

  • Collection 是最基本的集合接口,一个 Collection 代表一组 Object,即 Collection 的元素。
  • Java不提供直接继承自Collection的类,只提供实现其子接口的类。
  • Collection 接口存储一组不唯一的,无序的对象。

List接口相关内容

  • List接口用于存储一组不唯一的,有序的对象,其顺序为元素插入集合的顺序。
  • List集合可以通过元素的索引去访问一个元素,第一个元素的索引为0
  • List集合中允许存储相同的元素

ArrayList

  • 底层基于Object数组实现,具有自动扩容机制的动态数组
  • 随机访问元素的速度较快,但增加和删除元素较慢
  • 在ArrayList默认的无参构造方法中,不指定集合的容量,则默认创建一个空的数组,长度为0。在第一次向集合中添加元素时,默认创建一个长度为10的Object数组
  • 在的扩容机制中,当目前的数组容量无法满足新添加的元素时,默认将数组容量扩充至当前容量的1.5倍,最后使用数组拷贝的方法,将所有数据拷贝至新的数组中。自动扩容机制的代码如下:
 /**
     * Increases the capacity of this ArrayList instance, if
     * necessary, to ensure that it can hold at least the number of elements
     * specified by the minimum capacity argument.
     *
     * @param   minCapacity   the desired minimum capacity
     */
    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);
        }
    }

    private static int calculateCapacity(Object[] elementData, int 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
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    private void grow(int minCapacity) {
     
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1); // 新的数组容量为当前容量的1.5倍
        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);
    }
  • ArrayList中元素的数目不超过 Integer.MAX_VALUE

LinkedList

  • 底层基于双向循环链表实现,链表中每个节点由以下三个部分组成:前指针(指向前面的节点的位置),数据,后指针(指向后面的节点的位置)。同时,最后一个节点的后指针指向第一个节点,从而形成一个环。
  • 随机访问元素的速度较慢,增加和删除元素较快

Vector

  • ArrayList是线程操作不安全的,Vector中所有方法都使用synchronized 关键词修饰
  • 使用Vector时的资源消耗较多,synchronized的开销是巨大的
  • 因此,在单线程、不考虑并发的情况下使用ArrayList,在并发的情况下使用Vector。
  • 对于单个方法的调用,Vector是线程安全的,但对于多个方法的调用,Vector的线程安全也存在一定的问题,需要对整个vector对象进行线程安全的处理。
  • Vector可以设置默认数组每次增长的大小,默认情况下,数组每次增长为原有容量的2倍。其自动扩容代码如下:
    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);
    }

三种List的区别与特点

  • ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素
  • LinkedList:底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素
  • Vector:底层数据结构是数组,查询快,增删慢,操作方式使用synchronized修饰,效率低,开销大,可以存储重复元素

Set接口相关内容

  • Set集合用于存储唯一的、不重复的、无序的元素。
  • 通过重写hashCode()和equals()方法来保证元素的唯一性

HashSet

  • HashSet底层是用HashMap实现的,其成员变量持有一个HashMap
  • HashSet将所有的存储元素存在HashMap的key中,而Value则存储同一个相同的元素
// Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();
  • 判断新增元素是否与现有元素相同的过程:存储元素首先会使用hash()算法函数生成一个int类型hashCode散列值,然后已经的所存储的元素的hashCode值比较,如果hashCode不相等,则所存储的两个对象一定不相等,此时存储当前的新的hashCode值处的元素对象;如果hashCode相等,存储元素的对象还是不一定相等,此时会调用equals()方法判断两个对象的内容是否相等,如果内容相等,那么就是同一个对象,无需存储;如果比较的内容不相等,那么就是不同的对象,则存储该原色。因此,要采用哈希的解决地址冲突算法,在当前hashCode值处类似一个新的链表, 在同一个hashCode值的后面存储存储不同的对象,这样就保证了元素的唯一性。

TreeSet

  • TreeSet是一个有序的集合,它的作用是提供有序的Set集合。
  • TreeSet是基于TreeMap实现的,其成员变量中持有一个TreeMap。实际上是将待保存的元素作为TreeMap的key,而所有的value存储固定相同的Object对象。
  • TreeSet的元素支持2种排序方式:自然排序或者根据提供的Comparator进行排序
  • 如果要在TreeSet中存储自定义类型的元素,则该类需要实现Comparable,用于比较元素之间的大小,以实现排序

LinkedHashSet

  • LinkedHashSet是基于LinkedHashMap实现的,其底层实现代码中,成员变量维护一个LinkedHashMap
  • 使用hashCode()和equals()方法来判断两个元素是否相等
  • 与HashSet的区别:LinkedHashSet使用双向链表维护元素的顺序,可以按照元素插入的顺序来迭代遍历LinkedHashSet,即元素插入和取出的顺序是一致的。其迭代遍历的性能优于HashSet,但元素插入的性能逊色于HashSet。

Queue接口相关内容

  • 队列是一种很重要的数据结构,它是只允许从一端插入,而从另一端删除的线性结构。
  • 队列的特点是先进先出,与栈的结构相反
  • 队列的使用场景很广泛,在树的按层遍历、图的广度优先搜索中,都使用队列作为辅助的数据结构。

单向队列

单向队列是从队尾添加元素,从对头删除元素,其在java中定义的接口为java.util.Queue。\

PriorityQueue
PriorityQueue又叫做优先级队列,保存队列元素的顺序不是按照及加入队列的顺序,而是按照队列元素的大小进行重新排序。因此当调用peek()或pool()方法取出队列中头部的元素时,并不是取出最先进入队列的元素,而是取出队列的最小元素。

底层实现:PriorityQueue使用了一个高效的数据结构:堆。底层是使用数组保存数据。还会进行排序,优先将元素的最小值存到队头。

双端队列

双端队列是从队列的两端都可以插入或删除元素,其在java中定义的接口为java.util.Deque。LinkedList也是Deque的一个实现类

常用的队列实现类

LinkedBlockingQueue:的容量是没有上限的(说的不准确,在不指定时容量为Integer.MAX_VALUE,不要然的话在put时怎么会受阻呢),但是也可以选择指定其最大容量,它是基于链表的队列,此队列按 FIFO(先进先出)排序元素。

ArrayBlockingQueue:在构造时需要指定容量, 并可以选择是否需要公平性,如果公平参数被设置true,等待时间最长的线程会优先得到处理(其实就是通过将ReentrantLock设置为true来 达到这种公平性的:即等待时间最长的线程会先操作)。通常,公平性会使你在性能上付出代价,只有在的确非常需要的时候再使用它。它是基于数组的阻塞循环队 列,此队列按 FIFO(先进先出)原则对元素进行排序。

PriorityBlockingQueue:是一个带优先级的 队列,而不是先进先出队列。元素按优先级顺序被移除,该队列也没有上限(看了一下源码,PriorityBlockingQueue是对 PriorityQueue的再次包装,是基于堆数据结构的,而PriorityQueue是没有容量限制的,与ArrayList一样,所以在优先阻塞 队列上put时是不会受阻的。虽然此队列逻辑上是无界的,但是由于资源被耗尽,所以试图执行添加操作可能会导致 OutOfMemoryError),但是如果队列为空,那么取元素的操作take就会阻塞,所以它的检索操作take是受阻的。另外,往入该队列中的元 素要具有比较能力。

DelayQueue:(基于PriorityQueue来实现的)是一个存放Delayed 元素的无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且poll将返回null。当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于或等于零的值时,则出现期满,poll就以移除这个元素了。此队列不允许使用 null 元素。

Map接口相关内容

  • Map类型的集合是最常用的数据结构,用于存储key-Value型的数据
  • 比较常用的有HashMap、HashTable、TreeMap、ConcurrentHashMap

HashMap

HashMap是目前最常用的集合类,其内部结构和实现也比较复杂,接下来我通过阅读源代码(JDK1.8),按照自己的理解,尽可能详细的讲解其内部工作原理。
首先,有几个重要的知识点需要掌握:

  • HashMap用于存储key-value型的数据,但它存储的是对象的引用,而非对象本身
  • HashMap使用hashCode()和equals()方法,判断两个对象的key是否相同
  • HashMap底层使用 数组+链表+红黑树 的结构存储数据,其中红黑树是在JDK1.8中加入的,我们会在接下来的源码分析中具体看到。

首先看一下类的继承结构,其结构相对比较简单:
avatar

其次,我们需要了解几个重要的成员变量

    /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node<K,V>[] table;
  • table是实际用于存储数据的数组,也就是之前提到的数组+链表+红黑树结构中的数组,数组的每个元素可能是链表的头节点或红黑树的根节点,同时节点的数据部分存储同一个Key-Value对(Node对象)。数组初始默认大小:16
    /**
     * The number of key-value mappings contained in this map.
     */
    transient int size;
  • 这个变量比较简单,就是HashMap中实际存储的键值对的数目
    /**
     * The load factor for the hash table.
     *
     * @serial
     */
    final float loadFactor;
  • 加载因子,一般取值在0到1之间,当存储的数据超过一定的比例时,会进行Map的动态扩容操作,我们会在接下来的分析中具体讲解。loadFactor实际是在空间开销和时间开销之间做出权衡,loadFactor过大,会节约一定的空间,但会使put和get操作产生较大的时间开销。反之loadFactor过小,会频繁的进行动态扩容,从而产生较大的空间开销。默认取值:0.75,是实验获得的,HashMap能具有比较好的性能,在没有特殊应用的场景下,一般不建议修改。
    /**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;
  • threshold = table.length * loadFactor
  • threshold是Map进行动态扩容的阈值,当键值对的数目超过该阈值时,会进行resize()操作。

这样,我们就梳理出了HashMap底层的数据结构,首先是一个table数组,数组中的每个元素是一个链表的头结点或红黑树的根节点。当一个元素链接的节点数小于阈值时(默认为8),该节点为一个链表,当节点数大于阈值时,会执行treefy操作,将链表转化为红黑树。即当节点数目较多时,可以利用到红黑树的查询和修改效率较高的优势,以提高HashMap的性能。因此,HashMap底层是 数组+链表+红黑树 的结构。

接下来,在了解几个重要的操作方法
1. put方法

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with key, or
     *         null if there was no mapping for key.
     *         (A null return can also indicate that the map
     *         previously associated null with key.)
     */
    public V put(K key, V value) {
     
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
     
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
     
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
     
                for (int binCount = 0; ; ++binCount) {
     
                    if ((e = p.next) == null) {
     
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) {
      // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

简单描述一下思路:这里首先计算key的哈希值,然后判断若table数组为空或空间不足,则进行resize()操作,进行动态扩容。如果key所对应的节点已经存在,则用value覆盖原有的值,否则将新的值添加到哈希计算的节点中,如果是红黑树,则插入到红黑树的对应位置。如果是链表,则插入到链表的尾部,判断该元素的节点数目是否超过了treefy的阈值(默认为8),如果超过阈值,则进行treefy操作,将链表转换为红黑树。最后再次判断++size > threshold,如果成立,则进行resize()动态扩容,为下一次put操作做好准备。

2. get方法

    /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * 

More formally, if this map contains a mapping from a key * {@code k} to a value {@code v} such that {@code (key==null ? k==null : * key.equals(k))}, then this method returns {@code v}; otherwise * it returns {@code null}. (There can be at most one such mapping.) * *

A return value of {@code null} does not necessarily * indicate that the map contains no mapping for the key; it's also * possible that the map explicitly maps the key to {@code null}. * The {@link #containsKey containsKey} operation may be used to * distinguish these two cases. * * @see #put(Object, Object) */ public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } /** * Implements Map.get and related methods. * * @param hash hash for key * @param key the key * @return the node, or null if none */ final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }

get操作的逻辑相对简单一些:首先判断table表是否为空,如果为空则返回null。然后根据key的哈希值,确定元素的位置,检查头结点是否与key相同,如果相同则返回该节点。否则判断该元素是链表的头结点还是红黑树的根节点,分别进行链表的遍历或者二叉树的查找操作,返回对应的节点。

注意一个细节:这里拿到一个元素后,先判断头结点的key是否为待查找的节点,然后再判断该元素是链表还是红黑树。如果头结点能够满足要求,则直接返回,不进行后续的操作。

3. 动态扩容(resize)
resize操作主要完成两件事情:
(1) 建立一个长度为原有数组2倍的新的数组
(2) 将原有数组中的数据拷贝到新数组中,并同时根据新数组的长度进行rehash的操作
具体的实现细节大家可以参考源代码 resize() 方法。

一个小Tips:
我们注意到HashMap的源码中使用了大量的位运算,包括取模等操作都是通过位运算实现的,这有效提高了HashMap的执行效率,这是我们在写代码的时候也可以借鉴的地方。

TreeMap

  • TreeMap也用于存储key-value形式的数据,与HashMap不同的是,TreeMap按照key的大小有序存储的,通过对TreeMap进行迭代,就可以得到按照key排序的有序数据集合。
  • 底层实现:TreeMap是使用红黑树存储数据的,通过对树的中序遍历得到有序的集合。同时,利用红黑树的特点,保证了树的平衡性,提高了操作的效率。
  • 通过使用红黑树,TreeMap保证了containsKey, get, put and remove操作的时间复杂度均为log(n)。具有非常好的性能。
  • 使用TreeMap进行排序,有以下两种方式:
  1. 在TreeMap的构造方法中传入Comparator,并实现compare(T o1,T o2)方法。
  2. 在TreeMap存储的元素对象实现了Comparable接口,或者为JDK原生的可以比较大小的类的对象。
  • 与HashMap不同,在JDK8中,TreeMap存储元素的基本单元仍然是一个Entry,每个Entry是红黑树的一个节点,该类的签名和成员变量如下:
    /**
     * Node in the Tree.  Doubles as a means to pass key-value pairs back to
     * user (see Map.Entry).
     */

    static final class Entry<K,V> implements Map.Entry<K,V> {
     
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;
    }

注意一下TreeMap类的继承结构
avatar
TreeMap主要实现了NavigableMap接口,并继承自AbstractMap类。更多源码层面的东西,大家有兴趣可以自己阅读一下TreeMap的源代码。

HashTable

HashTable也是用于key-value形式的数据存储,因此这里我们主要来对比一下HashTable和HashMap之间的区别:

  1. 它们的父类不同,HashMap继承自AbstractMap抽象类,而HashTable继承自Dictionary抽象类。

  2. 它们底层封装的数据结构不同
    HashMap底层是Node类型的数组(JDK1.8)

    /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node<K,V>[] table;

HashTable底层是Entry类型的数组

    /**
     * The hash table data.
     */
    private transient Entry<?,?>[] table;
  1. HashMap是非线程安全的,HashTable是线程安全的,所有公共方法都有synchronized修饰。

  2. 由于HashTable的方法有synchronized修饰,在单线程环境下其开销较大,而HashMap的性能更好。

  3. 初始容量和扩容机制不同:

  • HashMap的默认初始容量为16,加载因子为0.75,当插入第13个元素的时候执行扩容,容量扩容为原来的2倍。
  • HashTable的默认初始容量为11,加载因子为0.75,因此当插入第9个元素的时候执行扩容,容量扩充为原来的2n+1。
  1. HashMap可以接受为null的key和value,HashTable不接受空值。这是因为HashMap在实现时对null做了特殊处理,将null的hashCode值定为了0,从而将其存放在哈希表的第0个bucket中(数组中下标为0的元素)。

  2. 在迭代方面,HashTable使用Enumeration,HashMap使用Iterator。Iterator其实与Enmeration功能上很相似,只是多了删除的功能。

你可能感兴趣的:(JAVA)