Bootstrap

java溯本求源之基础(二十七)之--Map常用子类及源码分析(6000字长文)

目录

1.引言

2.Map接口概述

3.常用Map子类详细分析

3.1 HashMap

3.1.1 概述

3.1.2 关键特性

3.1.3源码分析

3.1.3.1构造函数

3.1.3.2 主要方法分析

put 方法

get 方法 

resize 方法

3.2 TreeMap

3.2.1 概述

3.2.2 关键特性

3.2.3 源码分析

3.2.3.1构造函数

3.2.3.2主要方法分析

3.3 LinkedHashMap

3.3.1 概述

3.3.2 关键特性

3.3.3 源码分析

3.3.3.1 构造函数

3.3.3.2 主要方法分析

removeEldestEntry 方法

4. 其他Map子类

5. 性能比较

6.使用场景

7. 总结


1.引言

        在Java编程中,数据结构的选择至关重要。Map接口作为Java Collections Framework的一部分,提供了一种通过键值对存储数据的方式。本文将深入探讨Map接口的常用子类及其实现源码,帮助读者在实际开发中选择合适的数据结构,提高代码的性能和可维护性。

2.Map接口概述

        Map接口定义了一种映射关系,其中每个键(Key)都映射到一个值(Value)。Map的主要特性包括:

  • 键唯一:一个键只能对应一个值。
  • 不保证顺序:不同实现类对键值对的存储顺序有不同的特性。

常用的Map实现包括:

  • HashMap:基于哈希表实现,提供快速的插入和查找。
  • TreeMap:基于红黑树实现,支持自然排序或定制排序。
  • LinkedHashMap:结合了HashMap和LinkedList,维护插入顺序。

3.常用Map子类详细分析

3.1 HashMap
3.1.1 概述

        HashMap是Java中最常用的Map实现,底层使用哈希表存储键值对。它的主要优点是快速的插入和查找,时间复杂度为O(1)。

3.1.2 关键特性
  • 不保证顺序:HashMap中的元素顺序与插入顺序无关。
  • 线程不安全:在多线程环境下,HashMap需要外部同步。
3.1.3源码分析
3.1.3.1构造函

  HashMap 提供了多种构造函数,主要有以下三种:

  • 默认构造函数
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // 负载因子为默认值 0.75
}
  • 指定初始容量的构造函数

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
  • 指定初始容量和负载因子的构造函数

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

        在构造 HashMap 时,我们可以指定初始容量和负载因子,负载因子的默认值为 0.75,它表示当哈希表填充度达到 75% 时进行扩容。

3.1.3.2 主要方法分析
  • put 方法

 put 方法用于向 HashMap 中插入键值对:

//put 方法用于向 HashMap 中插入键值对:
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
//计算键的哈希值
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

//查找桶的位置:通过哈希值对数组长度取模,找到对应的桶。

//处理冲突:如果桶中已经存在节点,则通过链表(或红黑树)解决冲突。

//插入新节点:如果桶为空,直接插入;如果有冲突则添加到链表末尾。

!!!put方法的大致流程 这里可以算作一个面试题

  • 根据 Key 计算数组下标
    通过 Key 的哈希值,结合位运算,计算出数组中的位置(下标)。

  • 判断该下标是否为空

    • 如果下标对应的数组位置为空,说明还没有元素占用这个位置。此时,会将 Key 和 Value 封装成一个 Entry 对象(在 JDK 1.7 中)或 Node 对象(在 JDK 1.8 中),并将其放入该位置。
  • 下标位置不为空时的处理
    如果该下标位置已经有元素存在,就需要根据不同的情况进行处理:

    a. JDK 1.7 的处理

        首先检查是否需要扩容,如果需要扩容就进行扩容操作。扩容完成后,重新插入元素。

如果不需要扩容,则会生成一个新的 Entry 对象,并使用头插法将这个新对象插入到当前链表的头部。

  • b. JDK 1.8 的处理

       首先会检查当前 Node 的类型,判断该位置上的节点是红黑树节点还是链表节点。

       如果是红黑树节点:将 Key 和 Value 封装为红黑树的节点,并将其插入到红黑树中。在插入过程中,会检查红黑树中是否已经存在相同的 Key。如果存在,则更新对应的 Value。  

       如果是链表节点:将 Key 和 Value 封装为链表节点,并通过尾插法将新节点添加到链表的末尾。在这个过程中会遍历整个链表,检查是否有相同的 Key。如果找到相同的 Key,则更新其对应的 Value。 - 插入完成后,会检查链表的节点数量。如果链表中的节点数量大于或等于 8,则会将该链表转换为红黑树,以提高性能。

        扩容判断:不论是插入到链表还是红黑树中,最后都会再次检查是否需要扩容。如果需要扩容,则进行扩容操作。如果不需要扩容,则 put() 方法执行完毕。

  • get 方法 
//get 方法用于根据键查找对应的值:
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
//通过计算哈希值找到桶的位置。

//遍历链表或红黑树,查找与键匹配的节点。
  • resize 方法
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // 阈值翻倍
    }
    else if (oldThr > 0) // 初始容量存放在阈值中
        newCap = oldThr;
    else {               // 零初始阈值表示使用默认值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        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 = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // 保持顺序
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
3.2 TreeMap
3.2.1 概述

        TreeMap基于红黑树实现,自动对键进行排序。它适合需要有序键的场景。

3.2.2 关键特性
  • 支持自然排序或定制排序。
  • 迭代时返回的键是有序的。
3.2.3 源码分析
3.2.3.1构造函数
  • 默认构造函数
public TreeMap() {
    comparator = null; // 默认使用自然顺序
}
  • 使用指定比较器的构造函数
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}
  • 根据现有映射构造
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}
3.2.3.2主要方法分析
  • put 方法

//        put 方法用于向 TreeMap 中插入键值对:
public V put(K key, V value) {
      return putVal(key, value, false);
   }

//        在插入过程中,首先检查键是否已经存在。
//        如果键不存在,则创建一个新节点,并根据红黑树的插入规
  • get 方法
//        get 方法用于根据键查找对应的值:
        public V get(Object key) {
            Entry<K,V> e = getEntry(key);
            return (e == null) ? null : e.value;
        }
//        通过比较器找到键对应的节点。
//        如果找到,返回对应的值;否则返回 null。
  • remove 方法

        public V remove(Object key) {
            Entry<K,V> p = getEntry(key);
            if (p == null) return null;
            V oldValue = p.value;
            deleteEntry(p);
            return oldValue;
        }

//        找到要删除的节点后,进行删除操作,并调整树的结构以保持红黑树的性质。
3.3 LinkedHashMap
3.3.1 概述

        LinkedHashMap结合了HashMap和双向链表,维护插入顺序或访问顺序。

3.3.2 关键特性
  • 保持元素的插入顺序或访问顺序。
  • 适用于需要顺序的遍历场景。
3.3.3 源码分析

LinkedHashMap在HashMap的基础上增加了双向链表的逻辑。

3.3.3.1 构造函数
  • 默认构造函数
public LinkedHashMap() {
    super(); // 调用父类 HashMap 的构造函数,默认初始容量为 16,负载因子为 0.75
    accessOrder = false; // 默认按插入顺序
}
  • 指定初始容量的构造函数
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity); // 调用父类 HashMap 的构造函数
    accessOrder = false; // 默认按插入顺序
}
  • 指定初始容量和访问顺序的构造函数
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
    super(initialCapacity, loadFactor); // 调用父类 HashMap 的构造函数
    this.accessOrder = accessOrder; // 指定访问顺序
}
3.3.3.2 主要方法分析
  • put 方法
public V put(K key, V value) {
    return super.put(key, value); // 调用父类 HashMap 的 put 方法
}
  • get 方法
public V get(Object key) {
    Node<K,V> e;
    // 通过计算哈希值找到对应节点
    if ((e = getNode(hash(key), key)) == null) {
        return null; // 如果未找到,返回 null
    }
    // 如果是按访问顺序,则更新访问节点的顺序
    if (accessOrder) {
        afterNodeAccess(e);
    }
    return e.value; // 返回节点的值
}
  • removeEldestEntry 方法

//此方法用于控制在插入新元素时是否移除最老的元素,可以通过重写此方法来实现自定义的容量限制。
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false; // 默认不移除
}

4. 其他Map子类

除了上述常用子类,Java还提供了一些其他Map实现:

  • ConcurrentHashMap:线程安全的HashMap实现,采用分段锁机制,提高并发性能。
  • WeakHashMap:允许垃圾回收器回收的Map实现,适用于缓存场景。

5. 性能比较

在选择Map实现时,应考虑以下性能指标:

  • HashMap:适合快速查找和插入,使用广泛。
  • TreeMap:适合需要排序的场景,性能相对较慢。
  • LinkedHashMap:在保持顺序的情况下提供合理性能,适用于需要遍历的场景。

6.使用场景

  • HashMap:缓存、查找表,或者在做业务的时候为了避免重复for循环查询某个表,也可以全量查询出来封装成Map。
  • TreeMap:有序数据存储,如字典。
  • LinkedHashMap:实现LRU缓存策略。

7. 总结

        本文详细分析了Java中Map的常用子类及其源码实现。理解这些实现的特性和适用场景,对于提升Java开发者的编码能力至关重要。希望读者能够根据具体需求选择合适的Map实现,优化程序性能。

;