Java 中的 java.util.Map 接口,核心作用是建立键(Key)与值(Value)之间的映射关系,本质上是一种专门用于存储 “键值对”(Key-Value Pair)的数据结构。在这一结构中,每个键(Key)都具有唯一性,且仅与一个对应的值(Value)绑定;当键值对被存入 Map 后,后续只需通过指定的键,即可快速定位并获取其关联的值,无需遍历整个集合,这也是 Map 区别于其他集合的核心特性。
注意,Map 接口并非 java.util.Collection 接口的子类型 —— 这一设计决定了它的用法与 List、Set 等典型 Collection 实现类存在明显差异。例如,Collection 接口的核心是“存储单个元素的集合”,而 Map 则聚焦于 “键与值的关联映射”,二者在元素存储逻辑、核心方法(如 Map 的 put()/get() 与 Collection 的 add()/contains())的设计目标上均不相同。
由于 Map 是一个接口,你需要实例化 Map 接口的一个具体实现才能使用它。Java 集合 API 包含以下Map 实现:
java.util.HashMap 基于哈希表实现的 Map 接口实现类,存储键值对时不保证顺序,允许使用 null 作为键和值,但线程不安全,是日常开发中最常用的 Map 实现之一。
java.util.Hashtable 早期的哈希表实现,与 HashMap 类似但属于遗留类,不允许使用 null 作为键或值,且方法是同步的(线程安全),但性能相对较低,通常推荐用 HashMap 替代
java.util.EnumMap 专门为枚举类型设计的 Map 实现,要求所有键必须是同一个枚举类的枚举常量,内部按枚举的自然顺序存储,查询和操作效率高。
java.util.IdentityHashMap 特殊的 Map 实现,判断键是否相等时使用引用相等(==)而非对象内容相等(equals () 方法),适用于需要严格区分对象引用的场景。
java.util.LinkedHashMap 继承自 HashMap,通过双向链表维护键值对的插入顺序或访问顺序,可实现有序遍历,性能略低于 HashMap 但保留了顺序特性。
java.util.Properties 继承自 Hashtable,主要用于处理配置文件,键和值都必须是字符串类型,提供了加载和存储配置的便捷方法,常用于读取.properties 文件。
java.util.TreeMap 基于红黑树实现的有序 Map,键会按照自然顺序或自定义比较器进行排序,支持根据键的范围进行高效查询,如获取子映射、首尾元素等。
java.util.WeakHashMap 键使用弱引用的 Map 实现,当键对象不再被其他强引用指向时,会被垃圾回收器回收,对应的键值对也会从 Map 中自动移除,适合用作临时缓存。
根据我的经验,最常用的 Map 实现是 HashMap 和 TreeMap。
HashMap 映射一个键和一个值。它不保证映射内部存储的元素有任何顺序。
TreeMap 同样映射一个键和一个值。此外,它保证了键或值的迭代顺序 —— 即键或值的排序顺序。
HashMap 实现通常是这两种 Map 实现中速度最快的,所以只要你不需要对 Map 中的元素进行排序,就可以直接使用 HashMap。否则,就使用 TreeMap。
要创建一个 Map,你必须创建一个实现了 Map 接口的类的实例。例如:
Map mapA = new HashMap(); Map mapB = new TreeMap();
默认情况下,你可以将任何对象放入映射(Map)中,但从 Java5 开始,Java 泛型使得限制映射(Map)中键和值所能使用的对象类型成为可能。例如:
Map<String, MyObject> map = new HashMap<String, MyObject>();
现在这个 Map 只能接受 String 对象作为键,接受 MyObject 实例作为值。这样你就可以直接访问和迭代键与值,而无需对它们进行强制类型转换。示例如下:
for(MyObject anObject : map.values()){ // 对 anObject 执行一些操作... } for(String key : map.keySet()){ MyObject value = map.get(key); // 对 value 执行一些操作 }
在声明和创建 Map 时,若已知要存储的对象类型,始终指定泛型类型是被广泛认可的最佳实践。泛型类型不仅能在编译阶段阻止错误类型的对象被插入,避免运行时异常,还能让阅读代码的人直观了解 Map 中键值对的具体类型,显著提升代码的可读性与可维护性。
要向 Map 中添加元素,需调用其 put() 方法。例如:
Map<String, String> map = new HashMap<>(); map.put("key1", "element 1"); map.put("key2", "element 2"); map.put("key3", "element 3");
上述代码,put() 方法将字符串值映射到字符串键。然后,你可以使用该键获取对应的值,这一点我们将在下一节中看到。
在 Map 中,只有 Java 对象才能用作键(Key)和值(Value)。如果你向 Map 传递基本类型值(例如 int、double 等)作为键或值,这些基本类型值在作为参数传递之前会被自动装箱。例如:
map.put("key", 123);
在上面的示例中,传递给 put() 方法的值是一个基本类型 int。不过,Java 会将其自动装箱到一个 Integer 实例中,因为 put() 方法要求键和值都必须是 Object 实例。如果你将一个基本类型作为键传递给 put() 方法,也会发生自动装箱。
一个给定的键在 Map 中只能出现一次。这意味着,在 Map 中同一时间对于每个键只能存在一个键值对。换句话说,对于键“key1”,在同一个 Map 实例中只能存储一个值。当然,你可以在不同的 Map 实例中为同一个键存储值。
如果你多次使用相同的键调用 put() 方法,那么为该键传递给 put() 方法的最新值将会覆盖 Map 中该键已存储的值。换句话说,最新的值会替换给定键的现有值。
相当令人惊讶的是,你可以使用值 null 作为 Map 中的键。例如:
Map map = new HashMap(); map.put(null, "value for null key");
要获取由 null 键存储的值,你可以调用 get() 方法,并将 null 作为参数值传入。例如:
Map<String, String> map = new HashMap<>(); String value = map.get(null);
存储在 Map 中的键值对的值可以为null —— 因此以下操作是有效的:
map.put("D", null);
只需记住,稍后使用该键调用 get() 方法时,你会得到一个 null 值 —— 所以下面的代码会返回 null:
Object value = map.get("D");
如果之前已为该键插入了 null 值(如前面的示例所示),那么在执行此代码后,value 变量的值将为 null。
Map 接口还有一个名为 putAll() 的方法,它可以将另一个 Map 实例中的所有键值对(条目)复制到自身中。在集合论中,这也被称为两个 Map 实例的并集。例如:
Map<String, String> mapA = new HashMap<>(); mapA.put("key1", "value1"); mapA.put("key2", "value2"); Map<String, String> mapB = new HashMap<>(); mapB.putAll(mapA);
运行此代码后,变量 mapB 所引用的 Map 将包含代码示例开头插入到 mapA 中的两个键值对条目。
条目的复制是单向的。调用 mapB.putAll(mapA) 只会将条目从 mapA 复制到 mapB,而不会从 mapB 复制到 mapA。要按另一个方向复制条目,你必须执行代码 mapA.putAll(mapB)。
要获取存储在 Map 中的特定元素,需调用其 get() 方法,并将该元素的键作为参数传入。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); System.out.println("id=" + map.get("id")); //id=1000 System.out.println("name=" + map.get("name")); //name=Tom System.out.println("email=" + map.get("email")); //email=tom@gmail.com
Map 接口还有一个 getOrDefault() 方法,该方法可以返回你提供的默认值 —— 当 Map 中没有存储给定键对应的值时。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); System.out.println("id=" + map.getOrDefault("id", "0000")); //id=1000 System.out.println("age=" + map.getOrDefault("age", "0")); //age=0 返回的默认值
你可以使用 containsKey() 方法来检查 Map 中是否包含特定的键。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); System.out.println("包含`name`键吗?" + map.containsKey("name")); //包含`name`键吗?true System.out.println("包含`age`键吗?" + map.containsKey("age")); //包含`age`键吗?false
Map 接口还提供了一个用于检查 Map 中是否包含指定值的方法,该方法名为 containsValue()。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); System.out.println("包含`Tom`值吗?" + map.containsValue("Tom")); //包含`Tom`值吗?true System.out.println("包含`38`值吗?" + map.containsValue("38")); //包含`38`值吗?false
有几种方法可以迭代存储在 Map 中的键。最常用的键迭代方法有:
你可以通过 Map 的 keySet() 方法迭代其所有键。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); Iterator<String> iterator = map.keySet().iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); }
如你所见,键迭代器(Iterator)会逐个返回存储在 Map 中的每个键(每次调用 next() 方法返回一个)。一旦获得了键,你就可以使用 Map 的 get() 方法获取该键所存储的元素。
从 Java5 开始,你也可以使用 for-each 循环来迭代 Map 中存储的键。具体例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); for(String key : map.keySet()) { System.out.println(key + "=" + map.get(key)); }
从 Java 8 开始,你可以使用 Java Stream 来迭代 Map 的键。Stream 接口是 Java 8 中新增的 Java Stream API 的一部分。你首先从 Map 中获取键的 Set,然后可以从中得到一个 Stream。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); map.keySet().stream().forEach(k -> { System.out.println(k + "=" + map.get(k)); });
你也可以只迭代 Map 中存储的值。通过 values() 方法可获取 Map 中所有值组成的 Collection,然后按以下方式迭代该 Collection 中的值:
迭代 Map 中存储的所有值的第一种方法是从值集合(Set)中获取一个值迭代器(Iterator)实例,然后对其进行迭代。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); Iterator<String> iterator = map.values().iterator(); while(iterator.hasNext()) { System.out.println(iterator.next()); }
由于集合(Set)是无序的,所以对于值集合中值的迭代顺序,你无法得到任何保证。不过,如果你使用的是 TreeSet,仍然可以控制这个顺序。
迭代 Map 中存储的值的第二种方法是通过 Java 的 for-each 循环。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); for(String value : map.values()) { System.out.println(value); }
迭代 Map 中存储的值的第三种方法是使用值流(value Stream),借助 Java Stream API。首先从 Map中获取值集合(value Set),然后可以从这个值集合中获取流(Stream)。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); map.values().stream().forEach(v -> { System.out.println(v); });
你也可以迭代 Map 中的所有条目。这里的 "条目" 指的是键值对,每个条目都包含对应的键和值。前面我们要么只迭代键,要么只迭代值,而通过迭代条目,就能同时处理键和值。
与迭代键或值类似,迭代 Map 条目有两种方式:
迭代 Map 条目的第一种方法是通过从条目 Set 获取的条目 Iterator。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); Iterator<Map.Entry<String,String>> iterator = map.entrySet().iterator(); while(iterator.hasNext()) { System.out.println(iterator.next()); }
运行结果:
name=Tom id=1000 email=tom@gmail.com
迭代 Map 条目的第二种方法是使用增强型 for 循环。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); for(Map.Entry<String,String> entry : map.entrySet()) { System.out.println(entry); }
你可以调用 remove (Object key) 方法移除条目,这样就能删除与该键匹配的(键,值)对。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); map.remove("name"); System.out.println(map); //{id=1000, email=tom@gmail.com}
执行上述代码后,map 所引用的 Map 将不再包含键 name 对应的条目(键值对)。
你还可以使用 clear() 方法移除 Map 中的所有条目。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); map.clear(); // 清空 Map System.out.println(map); //{}
可以使用 replace () 方法替换 Map 中的元素。只有当指定键已映射了某个值时,才会用新值替换旧值;若给定的键不存在映射关系,则不会进行任何插入操作。这与 put () 方法的行为不同,put () 方法无论键是否已存在映射,都会插入或更新对应的值。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); map.replace("age", "48"); // 没有 “age” 的映射关系,不执行替换操作 map.replace("name", "Helen"); // 包含 “name“ 映射关系,执行替换 System.out.println(map); //{name=Helen, id=1000, email=tom@gmail.com}
运行此代码后,该 Map 实例将键为“name”的条目值替换为了“Helen”。
你可以通过 size () 方法获取 Map 中的条目数量。由于 Map 中的条目数量也被称为 Map 的大小,因此该方法被命名为 size ()。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); System.out.println("map 大小:" + map.size()); //map 大小:3
Java 的 Map 接口提供了一个专门用于检查 Map 是否为空的方法 ——isEmpty (),该方法返回布尔值(true 或 false)。当 Map 实例包含一个或多个条目时,isEmpty () 返回 false;若 Map 中没有任何条目(即包含 0 个条目),则 isEmpty () 返回 true。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); System.out.println("map 为空?" + map.isEmpty()); // map 为空?false map.clear(); System.out.println("map 为空?" + map.isEmpty()); // map 为空?true
从 Java 8 开始,Map 接口新增了一些函数式操作。这些函数式操作使得能够以更具函数式风格的方式与 Map 进行交互。例如,你可以将 Lambda 表达式作为参数传递给这些函数式风格的方法。
函数式操作方法如下:
compute()
computeIfAbsent()
computeIfPresent()
merge()
这些函数式方法中的每一个都将在接下来的内容中进行更详细的描述。
Map 的 compute() 方法接收一个键对象和一个 lambda 表达式作为参数。该 lambda 表达式必须实现java.util.function.BiFunction 接口。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); map.compute("name", new BiFunction<String, String, String>() { @Override public String apply(String key, String value) { // key=name, value=Tom System.out.println("key=" + key + ", value=" + value); return value; } });
或者
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); map.compute("name", (key, value) -> { // key=name, value=Tom System.out.println("key=" + key + ", value=" + value); return value; });
compute() 方法会在内部调用 lambda 表达式,将键对象以及该键对象在 Map 中存储的任何值作为参数传递给 lambda 表达式。
lambda 表达式返回的任何值都会被存储,以替代该键当前存储的值。如果 lambda 表达式返回 null,则该条目会被移除。Map 中不会存储“键-> null”的映射。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); // 如果 name 存在值,则转换成大写 map.compute("name", (key, value) -> { return Objects.isNull(value) ? null : value.toUpperCase(); }); map.compute("email", (key, value) -> { return null; // email 将会被删除 }); System.out.println(map); //{name=TOM, id=1000}
在上面的示例中,你可以看到 lambda 表达式在对给定键“name”所映射的值调用 toString().toUpperCase() 之前,会先检查该值是否为 null。如果 lambda 表达式抛出异常,该条目也会被移除。
Map 的 computeIfAbsent() 方法的工作方式与 compute() 方法类似,但只有当给定键不存在对应的条目时,才会调用 lambda 表达式。
lambda 表达式返回的值会被插入到 Map 中。如果返回 null,则不会插入任何条目。如果 lambda 表达式抛出异常,也不会插入任何条目。
例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); // 由于 name 键已经存在,lambda 表达式不会执行 map.computeIfAbsent("name", new Function<String, String>() { @Override public String apply(String key) { System.out.println("key=" + key); return map.getOrDefault(key, "unknown").toUpperCase(); } }); // 由于 age 键不存在,lambda 表达式会执行 map.computeIfAbsent("age", (key) -> { System.out.println("key=" + key); // 为 age,即键 return "38"; }); System.out.println(map);
运行结果:
key=age {name=Tom, id=1000, age=38, email=tom@gmail.com}
Map 的 computeIfPresent() 方法与 computeIfAbsent() 方法的作用相反。只有当 Map 中已存在该键对应的条目时,它才会调用作为参数传递的 lambda 表达式。例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); // 由于 name 键已经存在,lambda 表达式会执行 map.computeIfPresent("name", new BiFunction<String, String, String>() { @Override public String apply(String key, String value) { System.out.println("key=" + key + ", value=" + value); return Objects.isNull(value) ? null : value.toUpperCase(); } }); // 由于 age 键不存在,lambda 表达式不会执行 map.computeIfPresent("age", (key, value) -> { System.out.println("key=" + key + ", value=" + value); return "38"; }); System.out.println(map);
运行结果:
key=name, value=Tom {name=TOM, id=1000, email=tom@gmail.com}
注意,lambda 表达式返回的值将被插入到 Map 实例中。如果 lambda 表达式返回 null,则会移除给定键对应的条目。如果 lambda 表达式抛出异常,该异常会被重新抛出,并且给定键的当前条目保持不变。
Map 的 merge() 方法接收一个键、一个值以及一个实现了 BiFunction 接口的 lambda 表达式作为参数。
如果映射中没有该键对应的条目,或者该键的值为 null,那么作为参数传递给 merge() 方法的值将被插入到该键下。
但是,如果某个现有值已映射到给定的键,则会调用作为参数传递的 lambda 表达式。这样,该 lambda 表达式就有机会将现有值与新值进行合并。然后,lambda 表达式返回的值会被插入到该给定键对应的Map 中。如果 lambda 表达式返回 null,则会移除给定键对应的条目。
例如:
Map<String,String> map = new HashMap<>(); map.put("id", "1000"); map.put("name", "Tom"); map.put("email", "tom@gmail.com"); // 由于 name 键已经存在,lambda 表达式会执行 map.merge("name", "unknown", new BiFunction<String, String, String>() { @Override public String apply(String oldValue, String newValue) { System.out.println("oldValue=" + oldValue + ", newValue=" + newValue); return Objects.isNull(oldValue) ? newValue : (oldValue + newValue).toUpperCase(); } }); // 由于 age 键不存在,lambda 表达式不会执行,直接使用默认值 0 map.merge("age", "0", (oldValue, newValue) -> { System.out.println("oldValue=" + oldValue + ", newValue=" + newValue); return "38"; }); System.out.println(map);
运行结果:
oldValue=Tom, newValue=unknown {name=TOMUNKNOWN, id=1000, age=0, email=tom@gmail.com}
本示例会在键 age 没有映射值或者映射值为 null 的情况下,将值 0 插入到 Map 中。name 键已映射了一个非 null 值,则会调用这个 lambda 表达式。此 lambda 表达式返回“旧值+新值”转大写,即 TOMUNKNOWN。
如果 lambda 表达式抛出异常,该异常会被重新抛出,并且给定键的当前映射保持不变。
更多信息参考 https://docs.oracle.com/javase/8/docs/api/java/util/Map.html API 文档。