JavaSE - 集合类-双列集合框架

JavaSE - 集合类-双列集合框架

本节学习目标:

  • Java双列集合框架结构;
  • 了解并掌握Map接口及其方法;
  • 了解并掌握Map的实现类及其方法;
  • 了解并掌握双列集合实现类的特性和优缺点。

1. 双列集合框架结构

双列集合指有两列数据的集合,双列集合中的每个数据被称为键值对(Entry)。

键值对由两个数据组成:

  • (Key):使用泛型K限制数据类型,键不可重复唯一),数据结构也只对键有效;
  • (Value):使用泛型V限制数据类型,值可以重复

Java双列集合框架由Map接口及其多种实现类构成:

JavaSE - 集合类-双列集合框架_第1张图片

2. Map 接口

Map接口为Java双列集合框架中的根接口,它预先定义了双列集合中的基本方法,双列集合中的常用方法如下:

方法 返回值类型 功能
size() int 返回Map集合中的键值对数量
isEmpty() boolean 返回Map集合是否为
containsKey(Object key) boolean 返回Map集合中是否存在指定
containsValue(Object value) boolean 返回Map集合中是否存在指定
get(Object key) V 返回Map集合中指定
put(K key, V value) V 向Map集合中添加指定键值对,并返回旧值(如果没有该键的映射则返回null
remove(Object key) V 移除Map集合中的指定键,并返回此键的
putAll(Map m) void 将指定Map集合的所有键值对添加至当前Map集合中
keySet() Set Set集合形式返回Map集合的所有
values() Collection Collection集合形式返回Map集合的所有
entrySet() Set> Set集合形式返回Map集合的所有键值对
remove(Object key, Object value) boolean 指定键对应指定值时,移除此键值对,返回是否移除成功
replace(K key, V oldValue, V newValue) boolean 当键key对应值oldValue时,将值替换为newValue,返回是否替换成功
replace(K key, V value) V 将指定键的替换为指定值,并返回旧值(如果没有该键的映射则返回null
clear() void 移除Map集合中的所有键值对

Entry接口为Map接口中的内部接口,它预先定义了双列集合中键值对的基本方法,键值对的常用方法如下:

方法 返回值类型 功能
getKey() K 返回当前键值对的
getValue() V 返回当前键值对的
setValue(V value) V 将当前键值对的替换为指定值,并返回旧值

编写代码进行测试:

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class Test {
     
    public static void main(String[] args) {
     
        Map<Integer, String> map = new HashMap<>();
        System.out.println("集合是否为空:" + map.isEmpty());
        map.put(1, "A");
        map.put(2, "A");
        map.put(3, "C");
        System.out.println("集合当前键值对:" + Arrays.toString(map.entrySet().toArray()));
        System.out.println("集合中键值对数量:" + map.size());
        System.out.println("集合中是否存在键2:" + map.containsKey(2));
        System.out.println("集合中是否存在值D:" + map.containsValue("D"));
        System.out.println("获取集合中键1的值:" + map.get(1));
        map.remove(1);
        System.out.println("移除集合中键为1的键值对:" + Arrays.toString(map.entrySet().toArray()));
        map.putAll(Collections.singletonMap(4, "D"));
        System.out.println("向集合中添加Map集合[4=D]:" + Arrays.toString(map.entrySet().toArray()));
        System.out.println("获取集合中的所有键:" + Arrays.toString(map.keySet().toArray()));
        System.out.println("获取集合中的所有值:" + Arrays.toString(map.values().toArray()));
        map.remove(2, "B");
        System.out.println("移除集合中键值对2=B:" + Arrays.toString(map.entrySet().toArray()));
        map.replace(2, "A", "B");
        System.out.println("将集合中键值对2=A的值替换为B:" + Arrays.toString(map.entrySet().toArray()));
        System.out.println("将集合中键为3的键值对的值替换为E,它的旧值:" + map.replace(3, "E"));
        Map.Entry<Integer, String> entry = map.entrySet().iterator().next();
        System.out.println("获取集合中的第一个键值对:" + entry);
        System.out.println("它的键:" + entry.getKey());
        System.out.println("它的值:" + entry.getValue());
        System.out.println("将它的值替换为C,它的旧值:" + entry.setValue("C"));
        map.clear();
        System.out.println("移除集合中的所有键值对,此时集合中的键值对数量:" + map.size());
    }
}

运行结果:

集合是否为空:true
集合当前键值对:[1=A, 2=A, 3=C]
集合中键值对数量:3
集合中是否存在键2:true
集合中是否存在值D:false
获取集合中键1的值:A
移除集合中键为1的键值对:[2=A, 3=C]
向集合中添加Map集合[4=D]:[2=A, 3=C, 4=D]
获取集合中的所有键:[2, 3, 4]
获取集合中的所有值:[A, C, D]
移除集合中键值对2=B:[2=A, 3=C, 4=D]
将集合中键值对2=A的值替换为B:[2=B, 3=C, 4=D]
将集合中键为3的键值对的值替换为E,它的旧值:C
获取集合中的第一个键值对:2=B
它的键:2
它的值:B
将它的值替换为C,它的旧值:B
移除集合中的所有键值对,此时集合中的键值对数量:0

3. Map 接口的实现类

Map接口有很多种实现类,这些实现类各自有各自的优缺点,可以根据实际需求挑选使用。

对Map集合结构的理解:

  • Map中的无序的不可重复的。所有的组成的集合是一个Set集合keySet);
  • Map中的无序的可重复的。所有的组成的集合是一个Collection集合values);
  • Map中的,以及它们之间的映射关系组成了键值对对象(Entry);
  • Map中的键值对也是无序的不可重复的。所有的键值对组成的集合是一个Set集合(entrySet)。

3.1 HashMap 集合类

HashMap类是Map接口的主要实现类之一,并且继承于AbstractMap抽象类。它是Map集合的主要实现,一般情况下都使用HashMap集合作为Map集合的实现。

HashMap集合的核心结构为节点Node),它实现了Map接口中的Entry内部接口,也就是键值对

// java.util.HashMap.Node
// HashMap.java jdk1.8.0_202 Line:279~317

static class Node<K,V> implements Map.Entry<K,V> {
     
    final int hash; // 由键的哈希值(hashCode()返回值)计算得来的值,它不一定等于键的哈希值
    final K key;    // 键值对的键
    V value;        // 键值对的值
    Node<K,V> next; // 发生哈希冲突时,连接旧键值对的引用
    // 使用有参构造方法替代无参构造方法,避免空指针
    Node(int hash, K key, V value, Node<K,V> next) {
     
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    // 省略其他代码
}

HashSet集合内部使用了HashMap对象提供支持。

1. 成员变量

HashMap类的成员变量

成员变量名 类型 说明
table Node[] 键值对数组,数组的长度必须为2的N次幂
entrySet Set> 所有键值对的Set集合
size int 键值对的个数,即集合的长度
modCount int 对集合进行操作次数计数
threshold int 扩容临界值阈值),默认值为当前容量乘以负载因子
loadFactor float 负载因子,默认为DEFAULT_LOAD_FACTOR(即0.75F

继承于AbstractMap类的成员变量

成员变量名 类型 说明
keySet Set 所有的Set集合
values Collection 所有的Collection集合

Node(键值对)类的成员变量

成员变量名 类型 说明
hash int 由键的哈希值经过hash()方法计算得来的值,它不一定等于键的哈希值
key K 键值对的
value V 键值对的
next Node 发生哈希冲突时,连接旧键值对引用

2. 构造方法

HashMap类的构造方法

构造方法 说明
HashMap() 默认构造方法创建集合,键值对数组长度和负载因子均为默认
HashMap(int initialCapacity) 以指定数组长度创建集合,使用默认负载因子
HashMap(int initialCapacity, float loadFactor) 以指定数组长度和负载因子创建集合
HashMap(Map m) 以某Map集合m转换为HashMap集合

3. 键值对添加与扩容原理(源码)

调用put()方法进行添加操作,开始调试,进入put()方法:

// java.util.HashMap.put
// HashMap.java jdk1.8.0_202 Line:611~613

public V put(K key, V value) {
     
    // 调用hash()方法得出键值对的hash:
    // static final int hash(Object key) {
     
    //     int h;
    //     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    // }
    // 传入hash,键和值,进入putVal()方法
    return putVal(hash(key), key, value, false, true);
}

继续深入调试,进入putVal()方法:

// java.util.HashMap.putVal
// HashMap.java jdk1.8.0_202 Line:625~666

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
     
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果键值对数组还未初始化(table引用为null)或键值对数组长度为0
    if ((tab = table) == null || (n = tab.length) == 0)
        // 调用resize()方法对键值对数组进行初始化并获取键值对数组长度
        n = (tab = resize()).length;
    // 根据下面的计算方式算出新键值对在数组中的存储位置,判断这个存储位置上有没有键值对(即是否为null)
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 如果为null,则直接添加
        tab[i] = newNode(hash, key, value, null);
    // 否则获取存储位置上的原键值对引用p
    else {
     
        Node<K,V> e; K k;
        // 判断原键值对和新键值对的hash以及键是否都相同
        if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
            // 如果相同,将原键值对的引用赋给e
            e = p;
        // 如果键值对类型为红黑树节点(TreeNode)类型
        else if (p instanceof TreeNode)
            // 使用红黑树的添加方法putTreeVal()进行新键值对的添加操作,在此不再深入分析
            // 如果e不为null,就为添加后返回的原键值对引用
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 如果键值对类型是节点(Node)类型
        else {
     
            // for循环遍历哈希表中键值对数组当前索引的链表
            for (int binCount = 0; ; ++binCount) {
     
                // 将原键值对的下一个键值对引用赋给e,同时判断是否为null(即已到达链表末尾)
                if ((e = p.next) == null) {
     
                    // 将新键值对添加至链表末尾
                    p.next = newNode(hash, key, value, null);
                    // 为了加快操作效率
                    // 如果链表长度大于或等于TREEIFY_THRESHOLD(8)
                    // 则调用treeifyBin()方法将链表转换为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果原键值对的下一个键值对和新键值对的hash以及键都相同
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    // 说明是重复,直接跳过
                    break;
                // 将原键值对的下一个键值对引用赋给p,继续循环遍历
                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()方法进行扩容
        resize();
    afterNodeInsertion(evict);
    // 返回null,说明没有旧值
    return null;
}

链表转换为红黑树的treeifyBin()方法:

// java.util.HashMap.treeifyBin
// HashMap.java jdk1.8.0_202 Line:755~774

final void treeifyBin(Node<K,V>[] tab, int hash) {
     
    int n, index; Node<K,V> e;
    // 如果键值对数组没有初始化或者数组长度还未达到MIN_TREEIFY_CAPACITY(64)
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        // 调用resize()方法进行扩容,而不是转换为红黑树
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
     
        // 具体转换逻辑,在此省略
    }
}

总结(简述)HashMap集合在JDK1.8中添加操作的步骤:

  • 判断键值对数组是否已经初始化,如果没有先调用resize()扩容方法进行初始化
  • 根据某计算方式算出新键值对键值对数组中的存储索引,判断索引处是否已经存在键值对
    • 如果不存在键值对,直接在此索引位置添加新键值对,并返回null结束添加操作
    • 如果存在键值对,判断索引位置的键值对新键值对hash以及是否都相同
      • 如果相同,则将索引位置的键值对替换为新键值对,并返回原值结束添加操作
      • 如果不相同,判断索引位置的键值对是否为红黑树键值对类型TreeNode类型):
        • 如果,则调用红黑树的添加方法putTreeVal()进行添加操作,并返回原值(或者为null),结束添加操作
        • 如果不是,循环遍历当前索引处的链表,判断链表是否存在新键值对hash相同键值对
          • 如果,则将这个键值对的替换为新键值对,并返回原值结束添加操作
          • 如果没有,即已经循环到链表末尾,在链表末尾添加新键值对,并返回null结束添加操作
          • 如果链表的长度(循环的次数)已经达到TREEIFY_THRESHOLD(8)且键值对数组的长度已达MIN_TREEIFY_CAPACITY(64),则将链表转换为红黑树treeifyBin()方法)。
  • 添加操作结束后判断键值对数组长度table.length)是否大于或等于阈值threshold),如果超了则调用resize()方法进行扩容

扩容与初始化方法resize()

// java.util.HashMap.resize
// HashMap.java jdk1.8.0_202 Line:677~749

final Node<K,V>[] resize() {
     
    // 获取原键值对数组的引用并赋给oldTab
    Node<K,V>[] oldTab = table;
    // 获取原键值对数组的长度并赋给oldCap,如果没有初始化则为0
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 获取原阈值并赋给oldThr
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 判断原键值对数组的长度是否大于0
    if (oldCap > 0) {
     
        // 如果原键值对数组的长度已经超过HashMap集合最大长度MAXIMUM_CAPACITY
        if (oldCap >= MAXIMUM_CAPACITY) {
     
            // 将阈值提升到int类型最大值
            threshold = Integer.MAX_VALUE;
            // 返回原键值对数据引用,方法结束
            return oldTab;
        }
        // 计算扩容后的长度newCap,即原长度的2倍
        // 判断扩容后的长度是否小于HashMap集合最大长度MAXIMUM_CAPACITY且小于等于默认长度16
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 计算扩容后的阈值newThr,即原阈值的2倍
            newThr = oldThr << 1; // double threshold
    }
    // 如果原阈值oldThr大于0
    else if (oldThr > 0) // initial capacity was placed in threshold
        // 扩容至原阈值大小
        newCap = oldThr;
    // 如果原阈值oldThr小于等于0,即还未初始化
    else {
                    // zero initial threshold signifies using defaults
        // 扩容至默认长度16
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 扩容后的阈值为默认负载因子0.75F乘以默认长度16,即12
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 如果原键值对长度小于等于0且原阈值大于0
    // 比如将某个Map集合转换为HashMap,这时键值对数组table为null,但阈值threshold可能不为0(在putMapEntries()方法中被设置)
    // 即通过了上面的else if语句,此时newCap与原阈值oldThr相等
    if (newThr == 0) {
     
        // 计算扩容后的阈值,即原阈值乘以负载因子
        float ft = (float)newCap * loadFactor;
        // 如果原阈值和扩容后的阈值都小于HashMap集合最大长度MAXIMUM_CAPACITY,则扩容后的阈值就为计算结果
        // 否则扩容后的阈值为int类型最大值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 将阈值设为扩容后的阈值
    threshold = newThr;
    @SuppressWarnings({
     "rawtypes","unchecked"})
    // 以扩容后的长度创建新的键值对数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    // 将table引用指向新键值对数组
    table = newTab;
    // 如果原键值对数组不为null
    if (oldTab != null) {
     
        // 此处省略了将原键值对数组中的键值对重新计算存储位置并放到新键值对数组中的操作
    }
    // 返回新键值对数组
    return newTab;
}

总结(简述)HashMap集合在JDK1.8中扩容操作的步骤:

  • 判断键值对数组table是否已经初始化(即table引用是否为null):
    • 如果已初始化扩容后的长度调整为原来的2倍阈值调整至原来的2倍
    • 如果未初始化(即table引用为null),判断阈值threshold是否大于0
      • 如果阈值大于0扩容后的长度调整为阈值阈值调整至原阈值乘以负载因子后的值。
      • 如果阈值小于或等于0扩容后的长度调整为默认长度16阈值调整至默认阈值12(int) (16 * 0.75F))。
  • 扩容后的长度创建一个新的空键值对数组,如果原键值对数组中已有数据,计算这些数据的存储位置并按此插入新的数组;
  • 将引用table指向新数组并返回。

HashMap集合在JDK1.7和JDK1.8中实现方式的不同:

  • JDK1.7中只使用哈希表(数组+链表)结构进行存储,JDK1.8中使用哈希表(数组+链表)和红黑树进行存储;
    • 红黑树进行遍历次数只有链表的一半,所以JDK1.8中链表长度大于或等于8将被转换红黑树以提高遍历效率。
  • 如果发生哈希冲突,需要在链表中进行存储时,JDK1.7是头插法(New.next = Old),JDK1.8中是尾插法(Old.next = New);

4. 常量

HashMap类提供的常量

常量名 类型 说明
DEFAULT_INITIAL_CAPACITY int 1 << 4(16) 键值对数组初始化的默认长度(HashMap集合默认容量
MAXIMUM_CAPACITY int 1 << 30(1073741824) 键值对数组最大长度(HashMap集合最大容量
DEFAULT_LOAD_FACTOR float 0.75F 默认负载因子
TREEIFY_THRESHOLD int 8 链表转换为红黑树时所需链表长度阈值
UNTREEIFY_THRESHOLD int 6 红黑树转换为链表时所需的红黑树长度阈值
MIN_TREEIFY_CAPACITY int 64 链表转换为红黑树时所需键值对数组长度阈值

5. 特性

  • HashMap集合底层采用哈希表红黑树存储数据(JDK1.7中只使用哈希表存储数据);
  • 由于数据结构采用了哈希表红黑树,操作效率较高
  • HashMap集合允许键值对的null,但只能有一个键值对的键为null
  • HashMap集合的操作方法没有同步,因此为非线程安全的,不适用于多线程环境;
  • HashMap集合初始容量16(即DEFAULT_INITIAL_CAPACITY),每次扩容为原来的2倍,扩容后容量一定为2的N次幂
  • 添加操作结束后才会触发扩容,有可能会造成无效扩容(即扩容后没有新的添加操作)。
  • HashMap集合的容量必定是2的N次幂(使用tableSizeFor()方法重新计算容量),所以会造成指定的容量与实际容量不符的情况。
// java.util.HashMap.tableSizeFor
// HashMap.java jdk1.8.0_202 Line:378~386

static final int tableSizeFor(int cap) {
     
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

6. 扩展延伸

什么是哈希表?什么是哈希冲突(哈希碰撞)?


哈希表是基于数组的一种存储方式,它主要由哈希函数数组组成。当存储数据时,会先用哈希函数计算出数据的哈希值
之后根据哈希值存储进数组指定索引

哈希冲突是指两个不同的数据由哈希函数计算出的哈希值相同,它们必然会存储进哈希表中的同一个索引,引发冲突碰撞)。
Java中解决哈希冲突的方式是链地址法,将发生哈希冲突的数据构成一个单链表,并将单链表的头节点存储进数组

什么是哈希表?什么又是哈希冲突?哈希冲突的解决方法?- JavaShuo

HashMap集合扩容后的容量为什么一定为2的N次幂?


HashMap集合根据哈希值计算存储位置时使用按位与运算代替了取模运算,以提升效率。当数组长度为2的N次幂时,会使数据在哈希表上分布均匀
减少哈希冲突,减少链表的长度,提升了性能。

为什么hashMap的容量扩容时一定是2的幂次 - 平凡之路无尽路/CSDN

为什么JDK1.8中HashMap集合使用哈希表+红黑树?为什么要在链表长度超过8且容量超过64时才将链表转换为红黑树?


Java使用链地址法来解决哈希冲突,但链表的长度很长时将严重降低效率。由于红黑树的特性使得它的性能比链表更优越,所以在链表很长时会转换为红黑树
由于红黑树构造效率不是很高,所以要在链表长度超过8时才会转换红黑树,避免大量使用红黑树。如果容量低于64而存在长度超过8的链表,说明发生哈希冲突的概率较高,
这时候会进行扩容操作,将链表中的数据重新计算存储位置,使数据分布更均匀。从而避免使用红黑树。

hashmap中为何链表长度大于8才转换成红黑树 - JerryCode/知乎

什么是负载因子?默认负载因子为什么是0.75?


负载因子决定了阈值的大小,当已用容量超过阈值时会进行扩容,增加容量并消除部分哈希冲突(减少链表的长度)。

负载因子越小,阈值越小,触发扩容越频繁,容量越大,空间利用率越低;哈希冲突越少(链表长度越小),操作效率越高。

负载因子越大,阈值越大,触发扩容次数越少,容量越小,空间利用率越高,哈希冲突越多(链表长度越大),操作效率越低。

可以看出负载因子是用于平衡空间利用率和操作效率。

(源码)作为一般规则,默认负载因子 (.75) 在时间和空间成本之间提供了很好的权衡。 较高的值会减少空间开销,但会增加查找成本(反映在HashMap类的大多数操作中,包括getput)。

// HashMap.java jdk1.8.0_202 Line:68~77

/* As a general rule, the default load factor (.75) offers a good
 * tradeoff between time and space costs.  Higher values decrease the
 * space overhead but increase the lookup cost (reflected in most of
 * the operations of the HashMap class, including
 * get and put).  The expected number of entries in
 * the map and its load factor should be taken into account when
 * setting its initial capacity, so as to minimize the number of
 * rehash operations.  If the initial capacity is greater than the
 * maximum number of entries divided by the load factor, no rehash
 * operations will ever occur.
 */

HashMap的负载因子为什么默认是0.75 - 火麒马/CSDN

3.2 LinkedHashMap 集合类

LinkedHashMap类也是Map接口的实现类之一,并且继承于HashMap类。

LinkedHashMap集合相当于HashMap集合加双向链表,由双向链表根据键值对的插入顺序排序。

LinkedHashMap集合的核心结构Entry(双向链表中的一个节点,继承于HashMap类中的Node类):

// java.util.LinkedHashMap.Entry
// LinkedHashMap.java jdk1.8.0_202 Line:192~197

static class Entry<K,V> extends HashMap.Node<K,V> {
     
    Entry<K,V> before, after; // 分别指向上一个和下一个键值对的引用
    Entry(int hash, K key, V value, Node<K,V> next) {
     
        super(hash, key, value, next);
    }
}

LinkedHashSet集合内部使用了LinkedHashMap对象提供支持。

1. 成员变量

LinkedHashMap类继承于HashMap类,所以拥有HashMap类的非私有成员变量,它自身的成员变量:

成员变量名 类型 说明
accessOrder boolean 遍历顺序,true则按照哈希表遍历顺序false则按照插入顺序
head Entry 双向链表的头节点(first)
tail Entry 双向链表的尾节点(last)

Entry类的成员变量:

成员变量名 类型 说明
before Entry 指向上一个节点的引用(prev)
after Entry 指向下一个节点的引用(next)

2. 构造方法

LinkedHashMap类的构造方法

构造方法 说明
LinkedHashMap() 默认构造方法,accessOrderfalse
LinkedHashMap(int initialCapacity) 以指定容量创建集合,accessOrderfalse
LinkedHashMap(int initialCapacity, float loadFactor) 以指定容量和负载因子创建集合,accessOrderfalse
LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) 以指定容量和负载因子创建集合,遍历顺序为指定顺序
LinkedHashMap(Map m) 将集合m转换为LinkedHashMap集合

3. 特性

  • LinkedHashMap集合底层是在HashMap集合的存储结构上加入了双向链表,为键值对的插入顺序进行排序;
  • 数据结构采用哈希表+红黑树+双向链表。操作效率较高。
  • LinkedHashMap允许键值对的null,但只能有一个键值对的null
  • LinkedHashMap集合内部使用了HashMap集合的操作方法,没有进行同步,所以也是非线程安全的。

3.3 Hashtable 集合类

Hashtable类继承于Dictionary抽象类,是Map接口的古老实现类(类似ArrayList集合和Vector集合的关系)。

Dictionary类是Map接口未出现前用来存储键值对的抽象类,现已过时

1. 成员变量

Hashtable类的成员变量:

成员变量名 类型 说明
count int 键值对的个数,即集合的长度
entrySet Set> 所有键值对的Set集合
keySet Set 所有的Set集合
values Collection 所有的Collection集合
table Entry[] 键值对数组
threshold int 扩容临界值阈值),默认值为当前容量乘以负载因子
loadFactor float 负载因子,默认为0.75F
modCount int 对集合进行操作次数计数

2. 构造方法

Hashtable类的常用构造方法:

构造方法 说明
Hashtable() 默认构造方法
Hashtable(int initialCapacity) 指定容量创建Hashtable集合
Hashtable(int initialCapacity, float loadFactor) 指定容量负载因子创建Hashtable集合
Hashtable(Map t) 将Map集合t转换为Hashtable集合

3. 常量

Hashtable类提供的常量

常量名 类型 说明
MAX_ARRAY_SIZE int Integer.MAX_VALUE - 8 键值对数组最大长度(Hashtable集合的最大容量)

4. 特性

  • Hashtable集合底层使用哈希表存储数据;
  • 由于使用哈希表存储数据,但Hashtable类年代久远,优化较差,现已弃用
  • Hashtable集合内部实现和HashMap集合在JDK1.7的实现相似;
  • Hashtable集合不允许键值对的null
  • Hashtable集合的操作方法使用了同步,因此是线程安全的,适用于多线程环境。

3.4 TreeMap 集合类

TreeMap类为SortedMap接口的实现类之一,间接实现了Set接口。TreeMap是一种有序Map集合,按照键值对的某一属性进行排序。

TreeMap集合底层使用红黑树存储数据。它的节点Entry实现:

// java.util.TreeMap.Entry
// TreeMap.java jdk1.8.0_202 Line:2053~2119

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; // 颜色

    Entry(K key, V value, Entry<K,V> parent) {
     
        this.key = key;
        this.value = value;
        this.parent = parent;
    }
    // 省略其他代码
}

TreeSet集合内部使用了TreeMap对象提供支持。

1. 成员变量

TreeMap类的成员变量:

成员变量名 类型 说明
comparator Comparator 比较器,如果为null则使用键的自然排序
modCount int 对集合进行操作次数计数
root Entry 红黑树的根节点
size int 键值对的个数,即集合的长度

2. 构造方法

TreeMap类的常用构造方法:

构造方法 说明
TreeMap() 默认构造方法,使用键的自然排序
TreeMap(Comparator comparator) 使用比较器创建一个TreeMap集合,使用比较器的定制排序
TreeMap(Map m) 将Map集合m转换为TreeMap集合

3. 红黑树概述

红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。

红黑树是在1972年由Rudolf Bayer发明的,当时被称为平衡二叉B树(symmetric binary B-trees)。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的“红黑树”。

红黑树是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。

它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。

红黑树 - 百度百科

红黑树的五个性质

  1. 节点是红色黑色
  2. 节点(root)是黑色
  3. 所有叶子节点都是黑色
  4. 每个叶子到根的所有路径上不能有两个连续红色节点;
  5. 任意节点其每个叶子的所有路径都包含相同数目黑色节点。

这些性质使红黑树大致上是平衡的,并且最长路径最短路径2倍

关于实现源码:Java提高篇(二七)-----TreeMap - chenssy/CSDN

4. 特性

  • TreeMap底层使用红黑树存储数据;
  • 由于数据结构使用红黑树存储数据,操作效率较高
  • TreeMap集合不允许键值对的null,但允许键值对的null
  • TreeMap集合的操作方法没有同步,因此为非线程安全,不适用于多线程环境。

3.6 Map 接口实现类之间的异同和优缺点

Map接口的四大实现类:HashMapLinkedHashMapHashtableTreeMap

Map集合 键可为null 值可为null 操作效率 是否线程安全 存储结构 说明
HashMap 是(只有一个) 较高 哈希表+红黑树 Map集合的主要实现,键值对为无序
LinkedHashMap 是(只有一个) 较高 哈希表+红黑树+双向链表 键有序(按照插入顺序)
Hashtable 较低 哈希表 现已弃用
TreeMap 较高 红黑树 键有序(按照键的某个属性排序)

如果在多线程环境中需要使用Map集合,使用Collection工具类提供的synchronizedMap()方法获取线程安全的Map集合,或使用ConcurrentHashMap集合。

你可能感兴趣的