HashMap详解

Map API

Map 是集合中一个重要接口,是双列集合的根接口

其内部存储结构为 key-value 键值对

  • 无重复键值对
  • key 可为 null(限一个)
  • value 可为 null(多个)
方法说明
void clear ()清除所有键值对
boolean containsKey (Object key)如果此映射包含指定键的映射,则返回 true
boolean containsValue (Object value)
Set<Map, Entry<K,V>> entrySet ()
boolean equals (Object o)
default void forEach (BiConsumer action)
V get (Object key)
int hashCode ()返回此集合的哈希码值
boolean isEmpty ()
Set<K> keySet ()返回所有 key 的 set
V put (K key, V value)添加键值对
void putAll (Map<? extends K,? extends V> m)
V remove (Object key)删除键值对
default boolean remove (Object key, Object value)仅当指定的key映射到指定的值时删除
default V replace (K key, V value)只有当目标映射到某个值时,才替换
boolean replace (K key, V oldValue, V newValue)
int size ()
Collection<V> values ()返回此 Map 的 Collection

HashMap API

HashMap 是 Map 的一个实现类,其没有实现线程同步,是非线程安全的集合

public static void main(String[] args) {
    Map<Integer, Integer> map = new HashMap<>();
    map.put(1, 1);
    map.put(2, 2);
    map.put(3, 3);

    Integer integer = map.get(1);
    Integer remove = map.remove(1);

    boolean key = map.containsKey(1);
    boolean value = map.containsValue(1);
    boolean empty = map.isEmpty();

    // 获取所有 key
    Set<Integer> integers = map.keySet();

    // 获取所有 value
    Collection<Integer> values = map.values();

    // 获取所有键值对
    Set<Map.Entry<Integer, Integer>> entries = map.entrySet();
	for (Map.Entry<Integer, Integer> entry : entries) {
        Integer k = entry.getKey();
        Integer v = entry.getValue();
    }
    
    
    map.clear();
}

底层分析

在 JDK 1.8 中,HashMap 底层采用 数组 + 链表 + 红黑树 来实现,JDK 1.7 之前的底层为 数组 + 链表

HashMap 内部维护了一个链表,用来存放键值对

该节点内部类继承了 Map.Entry<K,V> 接口

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    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;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

用一个 table 链表数组来存放数据

transient Node<K,V>[] table;

为了方便遍历,HashMap 底层维护了一个 EntrySet 集合,该集合存放的元素类型是 Entry

该集合中的 key 指向了 Node 结点中的 key,entrySet 定义的类型是 Map.Entry,实际类型是 HashMap$Node

transient Set<Map.Entry<K,V>> entrySet;

创建

Map<Integer, Integer> map = new HashMap<>();

当使用无参构造器创建 HashMap 时,会对加载因子进行初始化为 0.75

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

创建完成后,此时的 table 是空的(HashMap$Node [] table = null)


在创建的时候,还可以自定义容器的大小,自定义加载因子等

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 (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);
}

在传入参数后,底层会对其进行校验,如果校验失败则会抛出异常

情景:传入的加载因子 > 1

当传入的加载因子大小超过1的时候,系统不会抛出异常,因为这是合理的。但在容器想要扩容的时候,扩容的阈值是通过容器现有容量与加载因子的乘积计算得出的,当加载因子超过1的时候,扩容阈值会大于当前的容器容量值,会导致容器一直无法扩容,添加超过容器大小的容器后,会一直添加在节点链表的后面,即 容器的大小定死了

Put

HashMap 底层的存储数据结构为 Node<K,V>[] table,这是一个 哈希表,初始默认为 null

当第一次调用 put 方法进行添加键值对的时候,会先对基本数据类型进行自动装箱,之后进入 put 方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
  1. 先进入 hash 方法计算哈希值
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  1. 计算出 hash 值后,把 hash 值,key、value 传入核心方法 putVal
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;
}
  1. 当第一次添加键值对时,会对集合进行扩容,初始扩容大小为16,临界值为 12(16 * 0.75)
  2. 以后每次进行扩容时,扩容倍数为2,临界值为容器扩容后的大小 * 加载因子
  3. 在添加键值对的时候,先根据计算处的 hash 值查看 hash 对应的索引位置是否有元素存在,如果有元素存在,则对该位置的链表进行树的判断;如果其是一颗红黑树,则根据树的方式进行查找
  4. 如果不是树,则对链表的每个元素都进行遍历查看,如果节点的整条链表中没有存在 put 的键值对,则进行 newNode 并加入链表
  5. 加入链表后进行树化判断,即当链表的长度超过8,且集合的大小超过64时,会对该链表进行红黑树化
  6. 如果链表的长度超过了8,但集合大小没超过64,则对集合进行扩容,直到64为止才可以进行树化

初始容量大小

HashMap 的第一次扩容时,初始大小为 16,这个大小的 2的整数次幂

16 = 2^4

16、32、64 …

当使用有参构造创建 HashMap,传入指定容器大小时,会对容器大小进行扩容,返回一个 2 的整数幂的数值

如 输入一个 13,返回一个 16

把容器的大小设置为 2 的整数幂的目的:是为了让哈希表中的数据能够尽可能地均匀分布,并尽可能地减少哈希冲突

遍历

Map 集合不能直接使用迭代器或者 foreach 进行遍历,但是转成 Set 之后就可以使用了

方式一:通过 keySet 函数获取所有键,再使用 for 循环遍历

public static void main(String[] args) {
    Map<Integer, Integer> map = new HashMap<>();

    map.put(1, 1);
    map.put(2, 2);

    Set<Integer> keySet = map.keySet();
    for (Integer key : keySet) {
        System.out.println(map.get(key));
    }
    
    // 通过迭代器
    Iterator<Integer> iterator = keySet.iterator();
    while (iterator.hasNext()) {
        Integer next = iterator.next();
        System.out.println(map.get(next));
    }
}

方式二:通过 entrySet 函数获取所有键值对,再使用 for 循环遍历

public static void main(String[] args) {
    Map<Integer, Integer> map = new HashMap<>();

    map.put(1, 1);
    map.put(2, 2);

    Set<Map.Entry<Integer, Integer>> entries = map.entrySet();
    for (Map.Entry<Integer, Integer> entry : entries) {
        Integer key = entry.getKey();
        Integer value = entry.getValue();

        System.out.println(key);
        System.out.println(value);
    }
    
    // 迭代器
    Iterator<Map.Entry<Integer, Integer>> iterator = entries.iterator();
    while (iterator.hasNext()) {
        Map.Entry<Integer, Integer> next = iterator.next();
        Integer key = next.getKey();
        Integer value = next.getValue();
        System.out.println("key = " + key + ",value = " + value);
    }
}

HashTable

特点:

  • 键值都不能为 null
  • 线程安全

底层有数组 HashTable$Entry[],初始化大小为 11

临界值 8 = 11 * 0.75*

扩容:11 * 2 + 1 = 23

private transient Entry<?,?>[] table;

HashMap 与 HashTable 的区别

  • HashTable 继承自 Dictionary,HashMap 继承自 AbstractMap,二者都实现了 Map 接口
  • HashTable 不允许 null key 和 null value,HashMap 允许
  • HashMap 的使用几乎与 HashTable 相同,不过 HashTable 是 synchronized 同步的;HashMap 是线程不安全
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值