wu-Chiyu

1. 集合 Collection

1.1 Java 集合框架

​ ​ ​ ​ ​ ​ ​ ​ Java 集合框架位于 java.util 包中。Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。

​ ​ ​ ​ ​ ​ ​ ​ 集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:

  • 接口:Collection,List,Set,Map 等。用于以不同的方式操作集合对象
  • 实现类:是集合接口的具体实现。如 ArrayList、LinkedList、HashSet、HashMap
  • 算法:是实现集合接口的对象里的方法执行的一些有用的计算,如搜索和排序。这些算法被称为多态,那是因为相同的方法可以在相似的接口上有着不同的实现
    ​ ​ ​ ​ ​ ​ ​ ​

集合和数组的区别:

  • 集合长度可变,数组长度固定
  • 集合中只能是引用类型,数组中既可以是基本类型也可以是引用类型
  • 集合中可以存储不同类型的元素(通常只存放同种类型),数组只能存储同一种类型
    ​ ​ ​ ​ ​ ​ ​ ​

1.2 集合接口

接口 描述
Collection 最基本的接口,一个 Collection 代表一组对象,Java 不提供直接继承自 Collection 的类,只提供其子接口。Collection 接口存储一组不唯一,无序的对象。
List List 接口是一个有序的 Collection,可以精确的控制每个元素的插入位置,通过索引访问元素,允许有相同的元素。List 接口存储一组不唯一,有序的对象。
Set 具有与 Collection 完全一样的接口,但不保存重复元素。Set 接口存储一组唯一,无序的对象。
SortedSet 继承于 Set,保存有序的集合。
Map 存储一组键值对象,提供 key 到 value 的映射
SortedMap 继承与 Map,使 key 保持升序排列

​ ​ ​ ​ ​ ​ ​ ​

1.3 Collection 集合的方法

方法 描述
boolean add(E e) 在集合末尾添加元素
boolean remove(E e) 删除值与 e 相等的元素,返回 true,若不存在,返回 false
void clear() 清除本类集中所有元素
boolean contains(E e) 判断集合中是否包含 e
boolean isEmpty() 判断集合是否为空
int size() 返回集合中的元素个数
boolean addAll(Collection c) 将一个类集 c 中所有元素添加到另一个类集
Object[] toArray() 返回一个包含本类集中所有元素的数组,数组类型为 Object[]
Iterator iterator() 迭代器,集合专用的遍历方式

​ ​ ​ ​ ​ ​ ​ ​

2. List

​ ​ ​ ​ ​ ​ ​ ​ List 接口是一个有序的 Collection,可以精确的控制每个元素的插入位置,通过索引访问元素,可以存储相同元素。List 接口存储一组不唯一,有序的对象。

​ ​ ​ ​ ​ ​ ​ ​ List 共有三个实现类,ArrayList,Vector,LinkedList。

2.1 ArrayList

​ ​ ​ ​ ​ ​ ​ ​ ArrayList 是最常用的 List 实现类,内部通过数组实现,允许堆元素进行快速随机访问。

​ ​ ​ ​ ​ ​ ​ ​ 由于数组存储需要整块内存空间,中间不能有间隔,当数组大小不足需要扩张时,需要将已有数组的数据全部复制到新存储空间中。当从 ArrayList 中间插入或删除元素时,需要对数组进行复制、移动,代价很高。因此只适合查找遍历,不适用于频繁的插入删除。

特点:

  • 底层数据结构为数组
  • 查询快,增删慢
  • 线程不安全,效率高
  • 可以存储重复元素

​ ​ ​ ​ ​ ​ ​ ​

2.1.1 常用方法

方法 描述
boolean remove(E e) 删除指定元素,返回是否删除成功
E remove(int index) 删除指定索引处元素,返回被删除元素
E set(int index, E e) 修改指定索引处元素,返回被修改的元素
E get(int index) 返回指定索引处的元素
int size() 返回元素个数
boolean add(E e) 将指定元素追加到集合末尾
void add(int index, E e) 在集合中指定位置插入元素

​ ​ ​ ​ ​ ​ ​ ​

2.2 LinkedList

​ ​ ​ ​ ​ ​ ​ ​ LinkedList 是一个继承于 AbstractSequentialList 的双向链表,同时也可以被当做栈、队列或双端队列操作。

​ ​ ​ ​ ​ ​ ​ ​ LinkedList 实现了 List 接口,能对它进行队列操作。

​ ​ ​ ​ ​ ​ ​ ​ LinkedList 实现了 Deque 接口,即能将LinkedList当作双端队列使用。
​ ​ ​ ​ ​ ​ ​ ​ LinkedList 实现了 Cloneable 接口,即覆盖了函数clone(),能克隆。
​ ​ ​ ​ ​ ​ ​ ​ LinkedList 实现 java.io.Serializable 接口,这意味着 LinkedList 支持序列化,能通过序列化去传输。

​ ​ ​ ​ ​ ​ ​ ​ LinkedList 包含两个重要成员 header 和 size。 header 是双向链表的表头,是双向链表节点对应的类 Entry 的实例,Entry 中包含成员变量:previous,next,element,element 是该节点中存放的数据。

特点:

  • 底层数据结构是双向链表

  • 查询慢,增删快

  • 线程不安全,效率高

  • 可以存储重复元素

​ ​ ​ ​ ​ ​ ​ ​

2.2.1 常用方法

方法 描述
boolean add(E e) 在链表末尾添加新节点
void add(int index, E e) 在链表指定位置添加新节点
void addFirst/push(E e) 在链表表头添加新节点
void addLast/offer(E e) 在链表表尾添加新节点
E removeFirst/pop() 删除第一个节点,并返回该对象
E removeLast() 删除最后一个节点,并返回该对象
E remove(int index) 删除指定位置节点
E get(int index) 得到指定位置节点
int indexOf(E e) 返回对象在链表中首次出现的位置,如没有返回 -1
int lastIndexOf(E e) 返回对象在链表中最后一次出现的位置,如没有返回 -1

​ ​ ​ ​ ​ ​ ​ ​

2.3 Vector

​ ​ ​ ​ ​ ​ ​ ​ 与 ArrayList 基本相同,通过数组实现,最大的区别是支持线程同步,某一时刻只有一个线程能够修改 Vector。但线程同步需要很高的消费,访问比 ArrayList 慢。

特点:

  • 底层数据结构是数组
  • 查询快,增删慢
  • 线程安全,效率低
  • 可以存储重复元素
  • 使用 synchronized 方法保证线程安全
    ​ ​ ​ ​ ​ ​ ​ ​

3. Set

​ ​ ​ ​ ​ ​ ​ ​ Set 具有与 Collection 完全一样的接口,Set 中的对象不可重复,除此之外可以把 Set 当做 Collection 使用。该集合用于存储无序元素,对象是否重复根据 hashCode 判断。
​ ​ ​ ​ ​ ​ ​ ​ 如果想让两个不同的对象被视为相等,就必须覆盖 Object 的 hashCode 方法和 equals 方法。

​ ​ ​ ​ ​ ​ ​ ​

3.1 HashSet

​ ​ ​ ​ ​ ​ ​ ​ HashSet 底层数据结构采用哈希表实现。元素的唯一性通过该元素类型是否重写 hashCode() 和 equals() 方法来保证

​ ​ ​ ​ ​ ​ ​ ​ HashSet 首先判断两个元素的哈希值,如果哈希值相同,会接着通过 equals 方法比较,如果 equals 也为 true,则视为同一个元素。

​ ​ ​ ​ ​ ​ ​ ​ HashSet 通过 hashCode 的值来确定元素在内存中的位置,一个 hashCode 位置上可以存放多个元素(多个元素存放在一个哈希桶中)。

特点:

  • 底层数据结构为哈希表
  • 元素无序且唯一
  • 线程不安全
  • 效率高
  • 可存储 null 元素

​ ​ ​ ​ ​ ​ ​ ​

3.2 LinkedHashSet

​ ​ ​ ​ ​ ​ ​ ​ LinkedHashSet 继承于 HashSet,底层使用 LinkedHashMap 保存所有元素。方法操作上与 HashSet 相同。

特点:

  • 底层使用 LinkedHashMap 保存元素
  • 元素顺序与存储顺序一致
  • 元素唯一
  • 线程不安全,效率高

​ ​ ​ ​ ​ ​ ​ ​

3.3 TreeSet

​ ​ ​ ​ ​ ​ ​ ​ TreeSet 的底层数据结构为红黑树。实现了 SortedSet 接口,可以随时确保集合中的元素处于有序状态。

​ ​ ​ ​ ​ ​ ​ ​ TreeSet 每插入一个对象都会进行排序,将其插入红黑树的指定位置,Integer 和 String 对象都可以进行默认的 TreeSet 排序,但自定义对象不可以。自定义类必须实现 Comparable 接口,并重写 compareTo() 方法才能使用 TreeSet

​ ​ ​ ​ ​ ​ ​ ​ 重写 compareTo() 方法时,比较不同的值,需要根据小于、等于、大于来返回相应的负整数,零或正整数。

特点:

  • 底层数据结构为红黑树
  • 元素有序且唯一
  • 只能添加同种类型对象
  • 不支持快速随机访问
  • 不允许元素为 null

​ ​ ​ ​ ​ ​ ​ ​

3.3.1 常用方法

方法 描述
E higher(E e) 返回集合中大于等于给定元素的最小元素
E lower(E e) 返回严格小于给定元素的最大元素
E higher(E e) 返回严格大于给定元素的最小元素
SortedSet headSet(E e) 返回此 Set 中所有小于 e 的子集
SortedSet tailSet(E e) 返回此 Set 中所有大于 e 的子集

4. Map

​ ​ ​ ​ ​ ​ ​ ​ List 和 Set 都继承自 Collection 接口,Map 不是。Map 用于保存具有映射关系的数据 key 和 value,通过键 key 查找值 value,它们可以是任何引用类型的数据,但 key 不能重复。

​ ​ ​ ​ ​ ​ ​ ​

4.1 Map 的主要方法

方法 描述
void clear() 删除全部映射
boolean containsKey(E key) 查询 Map 中是否包含指定 key
boolean containsValue(E value) 查询 Map 中是否包含指定 value
boolean isEmpty() 查询 Map 是否为空
E get(E key) 返回指定 key 对应的 value
E put(E key, E value) 添加一对映射,如果有相同的 key 覆盖 value
void putAll(Map m) 将指定 Map 中映射复制到 Map 中
E remove(E key) 删除指定 key 对应的映射,返回关联的 value
int size() 返回 Map 中的映射对数
Set entrySet() 返回 Map 中所包含映射组成的 Set 集合,每个集合元素都是 Map.Entry 对象
Set keySet() 返回 Map 中所有 key 组成的 Set 集合
Collection values() 返回 Map 中所有 value 组成的 Colllection

​ ​ ​ ​ ​ ​ ​ ​

4.2 HashMap

​ ​ ​ ​ ​ ​ ​ ​ HashMap 的主干是一个 Entry 数组。Entry 是 HashMap 的基本组成单元,每一个 Entry 包含一个 key-value 键值对。为了解决哈希冲突 HashMap 采用了链地址法,也就是数组+链表的方式。
​ ​ ​ ​ ​ ​ ​ ​ 因此大多数情况下 HashMap 可以直接定位到值,访问速度极快,遍历顺序不确定。HashMap 只允许一个键为 null,允许多条值为 null。且 HashMap 非线程安全。
​ ​ ​ ​ ​ ​ ​ ​ JDK 8 对 HashMap 进行了修改,最大的不同是引入了红黑树提升 HashMap 的查找效率。在 JDK 8 中,为解决哈希冲突的链表中的元素超过 8 个以后,会将链表转换为红黑树,在此处查找是可以把时间复杂度降低至 O(logN)。

​ ​ ​ ​ ​ ​ ​ ​

HashMap的构造函数

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

​ initialCapacity 为 HashMap 的初始大小,loadFactor 为负载因子。

​ 如果不填入构造参数,默认初始大小为16,负载因子为0.75。

特点:

  • 底层数据结构:数组代表一对映射,链表/红黑树解决哈希冲突
  • 访问速度快,效率极高
  • 遍历顺序不确定
  • 非线程安全

​ ​ ​ ​ ​ ​ ​ ​

4.3 LinkedHashMap

​ ​ ​ ​ ​ ​ ​ ​ LinkedHashMap 是 HashMap 的一个子类,底层数据结构是用双向链表连接起来的 HashMap,可以保留插入顺序,其存取过程基本与 HashMap 相同。
​ ​ ​ ​ ​ ​ ​ ​ LinkedHashMap 通过维护一个额外的双向链表保证了迭代顺序。该迭代顺序可以是插入顺序,也可以是访问顺序。因此,根据链表中元素的顺序可以将其分为:保持插入顺序的 LinkedHashMap保持访问顺序的 LinkedHashMap,其中 LinkedHashMap 的默认按插入顺序排序。
​ ​ ​ ​ ​ ​ ​ ​ 因此 LinkedHashMap 可以很好的支持 LRU 算法。

特点:

  • 底层数据结构:双向链表连接 HashMap 中的 Entry 数组
  • 可按插入顺序或访问顺序存储
  • 可以很好的支持 LRU 算法
  • 非线程安全

​ ​ ​ ​ ​ ​ ​ ​

4.4 ConcurrentHashMap

​ ​ ​ ​ ​ ​ ​ ​ ConcurrentHashMap 支持并发操作,是安全高效的 Map 集合,广泛应用于多线程开发。并发控制使⽤ synchronized 和 CAS 来操作,synchronized 只锁定当前链表或红⿊⼆叉树的首节点,这样只要 hash 不冲突,就不会产⽣并发,效率极大的提升。
​ ​ ​ ​ ​ ​ ​ ​ JDK 8 后 ConcurrentHashMap 的数据结构改为 Node 数组+链表/红黑树的结构。
​ ​ ​ ​ ​ ​ ​ ​ 红黑树结构略有不同,HashMap 的红黑树中的节点叫做 TreeNode,TreeNode 不仅仅有属性,还维护着红黑树的结构,比如说查找,新增等等。ConcurrentHashMap 中红黑树被拆分成两块,TreeNode 仅仅维护的属性和查找功能,新增了 TreeBin,来维护红黑树结构,并负责根节点的加锁和解锁。

  • Node:Node 对象是链表上的一个节点,内部存储 key、value,以及下一个节点的引用。节点中的 value 和 next 都用 volatile 修饰,保证并发的可见性

  • ForwardingNode:当扩容时,要把链表迁移到新的哈希表,在做这个操作时,会在把数组中的头节点替换为 ForwardingNode 对象。ForwardingNode 中不保存 key 和 value,只保存扩容后哈希表(nextTable)的引用。此时查找相应 node 时,需要去 nextTable 中查找

  • TreeBin:当链表转为红黑树后,数组中保存的引用为 TreeBin,TreeBin 内部不保存 key 或 value,它保存了 TreeNode 的 list 以及红黑树 root

    ​ ​ ​ ​ ​ ​ ​ ​

ConcurrentHashMap 中添加映射的过程(put 方法)

public V put(K key, V value) {
        return putVal(key, value, false);
    }

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key, value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

具体流程:

  • 给输入的 key 分配 hashcode,分配好后依次判断
  • 判断是否需要初始化
  • 判断 key 对应的 Node 数组是否为空,如果为空则用 CAS 写入数据
  • 判断是否需要扩容
  • 如果都不满足,则利用 synchronized 锁写入数据
  • 如果链表长度大于 TREEIFY_THRESHOLD,则将链表改为红黑树
    ​ ​ ​ ​ ​ ​ ​ ​

特点:

  • 底层数据结构:Node数组+链表/红黑树
  • 线程安全高效,多线程使用
  • 并发控制使用 synchronized 和 CAS,synchronized 只锁定当前链表或红⿊⼆叉树的首节点

​ ​ ​ ​ ​ ​ ​ ​

4.5 HashTable

​ ​ ​ ​ ​ ​ ​ ​ 线程安全的 HashMap,同一时间只有一个线程能够修改 HashTable,并发性远远不如 ConcurrentHashMap,效率低下,不建议在代码中使用。

4.6 TreeMap

​ ​ ​ ​ ​ ​ ​ ​ TreeMap 实现了 SortedMap 接口。能够把保存的记录根据键排序,默认为键值升序,也可以指定排序的比较器。如果使用排序的映射,建议使用 TreeMap。
​ ​ ​ ​ ​ ​ ​ ​ TreeMap 底层为红黑树,将代表一对对映射的 Entry 数组作为节点连接成红黑树,不需要调优,红黑树自动平衡。
​ ​ ​ ​ ​ ​ ​ ​ 在使用 TreeMap 来保存自定义对象时,与 TreeSet 相同必须让自定义对象的类实现 Comparable 接口,并重写 compareTo() 方法。

特点:

  • 底层数据结构为红黑树
  • 存入映射按键值排序,默认升序
  • 非线程安全
    ​ ​ ​ ​ ​ ​ ​ ​

4.6.1 常用方法

方法 描述
Map.Entry firstEntry() 返回最小 key 对应键值对
Map.Entry lastEntry() 返回最大 key 所对应的键值对
E firstKey() 返回最小 key
E lastKey() 返回最大 key
Map.Entry higherEntry(E key) 返回位于 key 后一位的键值对
Map.Entry lowerEntry(E key) 返回位于 key 前一位的键值对
E lowerKey(E key) 返回位于 key 前一位的 key
E higherKey(E key) 返回位于 key 后一位的 key

相关文章: