Java Map

Java Map

Map 概述

Map 是一种依照键/值对(key/value)存储元素的容器,提供通过键快速获取、删除和更新键/值对的功能。

Map 的核心特性如下:

  • 键(key)类似数组的下标,但数组下标固定为整数,Map 的键可以是任意类型的对象。
  • 不允许重复键,若插入已存在的键,对应的值会被覆盖。
  • 一个键与对应的值构成一个条目(Entry),条目整体存储在 Map 中。
    image

Map 类型及关系

Map 主要有三种实现类,其通用特性定义在 Map 接口中,类关系及具体实现如下:

核心接口与类结构

  • 顶层接口:Map<K, V>
  • 子接口:SortedMap<K, V>NavigableMap<K, V>
  • 抽象类:AbstractMap<K, V>
  • 具体实现类:HashMap<K, V>LinkedHashMap<K, V>TreeMap<K, V>
    image

各实现类特性

实现类 核心特性 排序方式 关键说明
HashMap 高效的增删改查,底层为散列表(数组+链表/红黑树) 无序 允许 null 键和 null 值,初始容量 16
LinkedHashMap 继承 HashMap,扩展链表实现 插入顺序或访问顺序 无参构造默认插入顺序,指定 accessOrder=true 为访问顺序
TreeMap 基于红黑树实现,遍历键时高效 自然排序或自定义排序 键需实现 Comparable 接口或通过 Comparator 指定排序规则,不允许 null 键,运行 null 值

关键接口方法

  • SortedMap 接口额外提供:
    • firstKey():返回第一个键
    • lastKey():返回最后一个键
    • headMap(toKey):返回键小于 toKey 的子 Map
    • tailMap(fromKey):返回键大于 fromKey 的子 Map
  • NavigableMap 接口额外提供:
    • floorKey(key):返回小于等于 key 的最大键
    • ceilingKey(key):返回大于等于 key 的最小键
    • lowerKey(key):返回小于 key 的最大键
    • higherKey(key):返回大于 key 的最小键
    • pollFirstEntry():移除并返回第一个条目
    • pollLastEntry():移除并返回最后一个条目
      image

简单示例代码

package com.map;

import java.util.*;

/**
 * @author Jing61
 */
public class MapDemo {
    public static void main(String[] args) {
        // HashMap,默认无序
        Map<Integer, String> map1 = new HashMap<>();
        map1.put(1, "Peppa");
        map1.put(2, "Emily");
        map1.put(3, "Candy");
        map1.put(4, "Jorge");
        map1.put(5, "Pedro");
        System.out.println(map1);
        // 如果重复的键值,则覆盖
        map1.put(1, "Pedro");
        System.out.println(map1);
        // 如果键不存在,则添加,否则不添加
        map1.putIfAbsent(2, "Jorge");
        // 获取key的value
        System.out.println(map1.get(2));
        System.out.println(map1.get(1));
        // 获取不存在的key的value,则返回null
        System.out.println(map1.get(3));
        // 获取不存在的key的value,则返回默认值
        System.out.println(map1.getOrDefault(3, "Not Found"));
        // 允许存入空键值
        map1.put(null, null);
        System.out.println(map1);
        // 删除键值对
        map1.remove(null);
        // 获取键值对个数
        System.out.println(map1.size());
        // 判断map是否为空
        System.out.println(map1.isEmpty());
        // 是否包含某个键
        System.out.println(map1.containsKey(1));
        // 是否包含某个值
        System.out.println(map1.containsValue("Pedro"));
        // 修改某个键值对,可以通过put覆盖,也可以通过replace
        map1.replace(1, "Dannie");

        /*
        迭代map
        1.获取map中所有的key,通过key迭代
         */
        Set<Integer> keys = map1.keySet();
        for (Integer key : keys) {
            System.out.println(key + ":" + map1.get(key));
        }
        /*
        2.获取map中所有的value,通过value迭代
         */
        Collection<String> values = map1.values();
        for (String value : values) {
            System.out.println(value);
        }
        /*
        3.获取map中所有的键值对,通过键值对迭代
         */
        Set<Map.Entry<Integer, String>> entries = map1.entrySet();
        for (Map.Entry<Integer, String> entry : entries) {
            System.out.println(entry.getKey() + ":" + entry.getValue());
        }
        /*
        4.forEach
         */
        map1.forEach((key, value) -> System.out.println(key + ":" + value));
        // 清空
        map1.clear();

        // LinkedHashMap,插入有序
        Map<String, Integer> map2 = new LinkedHashMap<>();
        map2.put("Peppa", 5);
        map2.put("Emily", 6);
        map2.put("Candy", 12);
        map2.put("Jorge", 3);
        map2.put("Pedro", 6);
        map2.put("Dannie", 4);
        System.out.println(map2);

        // LinkedHashMap,按照最后一次访问的顺序进行排序
        Map<String, Integer> map3 = new LinkedHashMap<>(16, 0.75f, true);
        map3.put("Peppa", 5);
        map3.put("Emily", 6);
        map3.put("Candy", 12);
        map3.put("Jorge", 3);
        map3.put("Pedro", 6);
        map3.put("Dannie", 4);

        System.out.println(map3.get("Emily"));
        System.out.println(map3.get("Pedro"));
        System.out.println(map3);

        // TreeMap,默认有序,不能插入null键,但允许插入null值
        Map<String, Integer> map4 = new TreeMap<>();
        map4.put("Peppa", 5);
        map4.put("Emily", 6);
        map4.put("Candy", 12);
        map4.put("Jorge", 3);
        map4.put("Pedro", 6);
        map4.put("Dannie", 4);

        System.out.println(map4);
    }
}

实战:统计单词出现次数

package com.map;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * @author Jing61
 */
public class HashMapDemo {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"))){
            //循环读取每一行
            String line = null;
            while((line = reader.readLine()) != null) {
                String[] words = line.split("[^a-zA-Z]+");
                for(String src : words) {
                    if(map.containsKey(src)) {//表示map中已经存在key为当前迭代的单词,需要对该key对应的value+1
                        int count = map.get(src);
                        map.replace(src, count + 1);
                    }else
                        map.put(src, 1);
                }
            }
            //1.如果map中存储该单词,就对该单词次数进行+1,else 就将该单词存放进map中
        } catch (IOException e) {
            e.printStackTrace();
        }
        /**
         * 遍历Map:
         *     1.可以获取所有的key=====>keySet
         * 2.可以获取所有的values====>values
         * 3.遍历所有的条目
         * 4.JDK8增强:forEach
         */
//        Set<String> keys = map.keySet();
//        for(String key : keys)
//            map.get(key);
        
        //map.values();
        Set<Map.Entry<String, Integer>> entries = map.entrySet();
        for(Map.Entry<String, Integer> entry : entries) {
            System.out.println(entry.getKey() + ":" + entry.getValue());
        }
        //jdk 8增强
        //map.forEach((key,value)->System.out.println(key + ":" + value));
    }
}

HashMap 原理概述

底层数据结构

HashMap 底层采用散列表(哈希表),本质是“数组+链表”的结合,兼具两者优点。

  • 数组:占用空间,连续寻址容易,查询速度快,但是,增加和删除效率非常低。
  • 链表:占用空间不连续,寻址困难,查询速度慢,但是,增加和删除效率非常高。
  • 哈希表:即查询快,增删效率也高。
  • JDK8 优化:当链表长度大于 8 时,链表转换为红黑树,进一步提升查找效率。

核心源码解析

关键常量与属性

public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 初始容量 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认负载因子

    /**
     * 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; // 核心数组(位桶数组)
    ...
}

Node 类结构

Node[] table 就是 HashMap 的核心数组结构,我们也称之为“位桶数组”。

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;

        return o instanceof Map.Entry<?, ?> e
                && Objects.equals(key, e.getKey())
                && Objects.equals(value, e.getValue());
    }
}

Node 对象存储结构

一个Node对象存储了:

  1. key:键对象value值对象。
  2. next:下一个节点。
  3. hash:键对象的hash值。

显然每一个Entry对象就是一个单向链表结构,我们使用图形表示一个Entry对象的典型示意:
image
然后,我们画出 Node[] 数组的结构(这也是HashMap的结构):
image

存储数据过程(put 方法)

  1. 获得 key 的 hashCode:调用 key 对象的 hashCode() 方法得到哈希码。
  2. 计算 hash 值:通过两次散列优化分布,最终通过位运算 hash = hashcode & (数组长度-1) 得到数组索引(数组长度为 2 的整数幂)。
    散列源码:
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
  3. 生成 Node 对象:封装 key、value、hash 值和 next 指针(初始为 null)。
  4. 存入数组:
    • 若索引位置无 Node,则直接存入。
    • 若索引位置已有 Node,则将新 Node 作为链表尾节点(JDK8 中链表长度超 8 转为红黑树)。

image

总结如上过程:当添加一个元素(key-value)时,首先计算key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,就形成了链表,同一个链表上的Hash值是相同的,所以说数组存放的是链表。JDK8中,当链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率。

获取数据过程(get 方法)

  1. 计算 hash 值:通过 key 的 hashCode 经散列算法得到索引位置。
  2. 遍历查找:在对应索引的链表/红黑树中,通过 equals() 方法对比 key,直到碰到返回 true 的节点对象为止,找到匹配的 Node。
  3. 返回结果:返回匹配 Node 的 value。

扩容机制

  • 触发条件:当数组中元素个数达到 负载因子(0.75)* 数组长度 时,触发扩容。
  • 扩容规则:数组长度变为原来的 2 倍,重新计算所有 Node 的 hash 值并迁移到新数组。
  • 注意:扩容操作耗时,需尽量避免频繁扩容。

注意

Java 规定:两个通过 equals() 判定相等的对象,必须具有相同的 hashCode();若 equals() 为 true 但 hashCode() 不同,会导致 HashMap 中存储和查找异常。两个具有相同的 hashCode()的对象,不一定 equals() 判定相等。

posted @ 2025-11-13 12:22  Jing61  阅读(13)  评论(0)    收藏  举报