Java 中的 Map 接口定义了一种非常通用的数据结构,用于存储键值对。在具体的开发实践中,我们会根据不同的需求选择不同的实现。HashMap 和 Hashtable 是两个最常见的实现,但你真的了解它们之间的细微差别,以及如何在其他特定场景下选择 TreeMap、Properties 或 LinkedHashMap 吗?
在 云栈社区 的 Java 技术板块,关于集合框架的讨论总是热度不减,其性能与正确使用是保证应用稳定高效的基础。
HashMap 和 Hashtable
HashMap 和 Hashtable 都是 Map 接口的典型实现类。但 Hashtable 是一个古老的 Map 实现类,自 Java 1.0 就有了(那时还没有 Map 接口)。它最初只包含了两个方法:elements()(类似于 values())和 keys()(类似于 keySet())。
它们的核心区别主要有以下几点:
- 线程安全性:
Hashtable 是线程安全的 Map 实现,而 HashMap 是线程不安全的。因此,在单线程环境下,HashMap 通常有更好的性能。但当多个线程需要访问同一个 Map 对象时,使用 Hashtable 会更稳妥。
- null 值处理:
Hashtable 不允许使用 null 作为 key 和 value,如果试图放入 null,将会引发 NullPointerException 异常。而 HashMap 则允许使用 null 作为 key 或 value。
下面的代码演示了 HashMap 对 null 的处理:
import java.util.*;
import static java.lang.System.*;
public class NullInHashMap
{
public static void main(String[] args)
{
var hm = new HashMap();
// 试图将两个key为null的key-value对放入HashMap中
hm.put(null, null);
hm.put(null, null);
// 将一个value为null的key-value对放入HashMap中
hm.put("a", null);
// 输出Map对象
out.println(hm);
}
}
无论是 HashMap 还是 Hashtable,要成功地在其中存储和获取对象,用作 key 的对象必须正确实现 hashCode() 和 equals() 方法。与 HashSet 集合一样,它们也不能保证其中 key-value 对的顺序。判断两个 key 相等的标准也是:两个 key 通过 equals() 方法比较返回 true,并且两个 key 的 hashCode 值也相等。
来看一个更复杂的例子,它定义了自定义类作为 key:
import java.util.*;
import static java.lang.System.*;
class A
{
int count;
public A(int count)
{
this.count = count;
}
// 根据count的值来判断两个对象是否相等。
public boolean equals(Object obj)
{
if (obj == this)
return true;
if (obj != null && obj.getClass() == A.class)
{
var a = (A) obj;
return this.count == a.count;
}
return false;
}
// 根据count来计算hashCode值。
public int hashCode()
{
return this.count;
}
}
class B
{
// 重写equals()方法,B对象与任何对象通过equals()方法比较都返回true
public boolean equals(Object obj)
{
return true;
}
}
public class HashtableTest
{
public static void main(String[] args)
{
var ht = new Hashtable();
ht.put(new A(60000), "自动化平台测试开发");
ht.put(new A(87563), "SpringBoot实战");
ht.put(new A(1232), new B());
out.println(ht);
// 只要两个对象通过equals比较返回true,
// Hashtable就认为它们是相等的value。
// 由于Hashtable中有一个B对象,
// 它与任何对象通过equals比较都相等,所以下面输出true。
out.println(ht.containsValue("测试字符串"));
// 只要两个A对象的count相等,它们通过equals比较返回true,且hashCode相等
// Hashtable即认为它们是相同的key,所以下面输出true。
out.println(ht.containsKey(new A(87563)));
// 下面语句可以删除最后一个key-value对
ht.remove(new A(1232));
out.println(ht);
}
}
这段程序定义了 A 和 B 两个类。A 类判断相等的标准是 count 变量,只要 count 相等,equals() 返回 true,hashCode() 也相等。Hashtable 判断 value 相等的标准则是:只要 value 与另一个对象通过 equals() 方法比较返回 true 即可。
这里有一个非常重要的实践警示:如果使用可变对象作为 HashMap 或 Hashtable 的 key,并且在程序运行中修改了这个 key 对象,可能会导致你无法再准确访问到 Map 中已存在的对应键值对。
import java.util.*;
import static java.lang.System.*;
public class HashMapErrorTest
{
public static void main(String[] args)
{
var ht = new HashMap();
// 此处的A类与前一个程序的A类是同一个类
ht.put(new A(60000), "Spring实战");
ht.put(new A(87563), "SpringBoot实战");
// 获得Hashtable的key Set集合对应的Iterator迭代器
var it = ht.keySet().iterator();
// 取出Map中第一个key,并修改它的count值
var first = (A) it.next();
first.count = 87563;
// 输出{A@1560b=Spring实战, A@1560b=SpringBoot实战}
out.println(ht);
// 只能删除没有被修改过的key所对应的key-value对
ht.remove(new A(87563));
out.println(ht);
// 无法获取剩下的value,下面两行代码都将输出null。
out.println(ht.get(new A(87563)));
out.println(ht.get(new A(60000)));
}
}
因此,尽量不要使用可变对象作为 HashMap 或 Hashtable 的 key。如果必须使用,也要确保不在使用过程中修改该 key 对象。
LinkedHashMap 实现类
正如 HashSet 有一个 LinkedHashSet 子类,HashMap 也有一个 LinkedHashMap 子类。LinkedHashMap 使用双向链表来维护 key-value 对的次序(即插入顺序)。因此,在迭代访问 Map 里的全部元素时,它能按照添加顺序输出,性能较好,但维护链表会带来轻微的性能开销。
import java.util.*;
import java.lang.System.*;
public class LinkedHashMapTest
{
public static void main(String[] args)
{
var scores = new LinkedHashMap();
scores.put("语文", 80);
scores.put("英文", 82);
scores.put("数学", 76);
// 调用forEach方法遍历scores里的所有key-value对
scores.forEach((key, value) -> System.out.println(key + "-->" + value));
}
}
使用 Properties 读写属性文件
Properties 类是 Hashtable 的子类,专门用于处理属性文件(如 .properties 或 Windows 的 .ini 文件)。因为它处理的是属性名和属性值,所以其内部的 key 和 value 都必须是字符串类型。
它提供了便捷的方法来读写属性:
String getProperty(String key) / String getProperty(String key, String defaultValue)
Object setProperty(String key, String value)
void load(InputStream inStream): 从输入流加载属性。
void store(OutputStream out, String comments): 将属性保存到输出流。
import java.util.*;
import java.io.*;
import static java.lang.System.*;
public class PropertiesTest
{
public static void main(String[] args)
throws Exception
{
var props = new Properties();
// 向Properties中增加属性
props.setProperty("username", "yeeku");
props.setProperty("password", "123456");
// 将Properties中的key-value对保存到a.ini文件中
props.store(new FileOutputStream("a.ini"),
"comment line");
// 新建一个Properties对象
var props2 = new Properties();
// 向Properties中增加属性
props2.setProperty("gender", "male");
// 将a.ini文件中的key-value对追加到props2中
props2.load(new FileInputStream("a.ini"));
out.println(props2);
}
}
此外,Properties 也支持以 XML 文件格式保存和加载键值对。
SortedMap 接口和 TreeMap 实现类
Map 接口派生出了 SortedMap 子接口,其实现类是 TreeMap。TreeMap 基于红黑树数据结构,能够保证所有的键值对处于有序状态。它的排序方式有两种:
- 自然排序:
TreeMap 的所有 key 必须实现 Comparable 接口,且是同一类的对象。
- 定制排序:创建
TreeMap 时,传入一个 Comparator 对象。
TreeMap 判断两个 key 相等的标准是:通过 compareTo()(或 compare())方法比较返回 0。这里有一个关键点:如果使用自定义类作为 key,那么重写 equals() 方法和 compareTo() 方法时应保持逻辑一致,否则会和 Map 接口的基本规则产生冲突。
TreeMap 提供了丰富的方法用于顺序访问,如 firstKey(), lastKey(), higherKey(), subMap() 等,这使其在需要范围查询或排序输出的场景下非常有用。这体现了其在 算法与数据结构 中的独特价值。
import java.util.*;
import static java.lang.System.*;
class R implements Comparable
{
int count;
public R(int count)
{
this.count = count;
}
public String toString()
{
return "R[count:" + count + "]";
}
// 根据count来判断两个对象是否相等。
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj != null && obj.getClass() == R.class)
{
var r = (R) obj;
return r.count == this.count;
}
return false;
}
// 根据count属性值来判断两个对象的大小。
public int compareTo(Object obj)
{
var r = (R) obj;
return count > r.count ? 1 :
count < r.count ? -1 : 0;
}
}
public class TreeMapTest
{
public static void main(String[] args)
{
var tm = new TreeMap();
tm.put(new R(3), "SpringBoot实战");
tm.put(new R(-5), "Spring实战");
tm.put(new R(9), "Spring");
out.println(tm);
// 返回该TreeMap的第一个Entry对象
out.println(tm.firstEntry());
// 返回该TreeMap的最后一个key值
out.println(tm.lastKey());
// 返回该TreeMap的比new R(2)大的最小key值。
out.println(tm.higherKey(new R(2)));
// 返回该TreeMap的比new R(2)小的最大的key-value对。
out.println(tm.lowerEntry(new R(2)));
// 返回该TreeMap的子TreeMap
out.println(tm.subMap(new R(-1), new R(4)));
}
}
WeakHashMap 实现类
WeakHashMap 与 HashMap 用法相似,但关键区别在于其 key 是弱引用。这意味着,如果某个 key 对象除了在 WeakHashMap 中被引用外,没有其他强引用指向它,那么当垃圾回收发生时,这个 key 对象可以被回收,WeakHashMap 也会自动删除对应的键值对。这常用于实现缓存或监听器列表,避免内存泄漏。
import java.util.*;
import static java.lang.System.*;
public class WeakHashMapTest
{
public static void main(String[] args)
{
var whm = new WeakHashMap();
// 将WeakHashMap中添加三个key-value对,
// 三个key都是匿名字符串对象(没有其他引用)
whm.put(new String("语文"), new String("良好"));
whm.put(new String("数学"), new String("及格"));
whm.put(new String("英文"), new String("中等"));
//将 WeakHashMap中添加一个key-value对,
// 该key是一个系统缓存的字符串对象,该key是一个字符串直接量
whm.put("java", new String("中等"));
// 输出whm对象,将看到4个key-value对。
out.println(whm);
// 通知系统立即进行垃圾回收
gc();
runFinalization();
// 通常情况下,将只看到一个key-value对。
out.println(whm);
}
}
注意:如果要利用 WeakHashMap 的弱引用特性,就不要让 key 所引用的对象还有其他强引用。
IdentityHashMap 实现类
在 IdentityHashMap 中,判断两个 key 相等的标准非常严格:只有当 key1 == key2(即引用同一对象)时,才认为相等。而普通的 HashMap 是依据 equals() 和 hashCode()。它允许 null 作为 key 和 value,也不保证顺序。
import java.util.*;
import static java.lang.System.*;
public class IdentityHashMapTest
{
public static void main(String[] args)
{
var ihm = new IdentityHashMap();
// 下面两行代码将会向IdentityHashMap对象中添加两个key-value对
ihm.put(new String("语文"), 89);
ihm.put(new String("语文"), 78);
// 下面两行代码只会向IdentityHashMap对象中添加一个key-value对
ihm.put("java", 93);
ihm.put("java", 98);
System.out.println(ihm);
}
}
前两个 key 是新创建的不同 String 对象(== 比较为 false),所以被视为两个键。后两个 key 是字符串直接量,Java 常量池机制使其指向同一个对象(== 比较为 true),所以后一个 put 操作覆盖了前一个。
EnumMap 实现类
EnumMap 是一个与枚举类配合使用的 Map 实现,其所有 key 都必须是单个枚举类的枚举值。它在内部以紧凑的数组形式存储,因此性能极高。key 的顺序遵循枚举值在枚举类中的定义顺序(自然顺序)。它不允许 null 作为 key,但 value 可以为 null。
import java.util.*;
import static java.lang.System.*;
enum Season
{
SPRING, SUMMER, FALL, WINTER
}
public class EnumMapTest
{
public static void main(String[] args)
{
// 创建EnumMap对象,该EnumMap的所有key都是Season枚举类的枚举值
var enumMap = new EnumMap(Season.class);
enumMap.put(Season.SUMMER, "夏日炎炎");
enumMap.put(Season.SPRING, "春暖花开");
out.println(enumMap);
}
}
各 Map 实现类的性能分析
了解不同 Map 实现的性能特点,有助于我们做出正确的选择,这也是构建高性能 后端架构 的必备知识:
- HashMap vs Hashtable:通常
HashMap 性能优于线程安全的 Hashtable。
- TreeMap:通常比
HashMap 和 Hashtable 慢,特别是在插入和删除时,因为它要维护红黑树的平衡。但其优势在于 key 始终有序。
- LinkedHashMap:比
HashMap 略慢,因为它需要额外的链表来维护插入顺序。
- IdentityHashMap:性能与
HashMap 相似,区别在于使用 == 判断相等性。
- EnumMap:所有实现中性能最好的,但限制是 key 必须来自单一枚举类。
HashSet 和 HashMap 的性能调优
HashSet 和 HashMap(以及 Hashtable)都基于哈希表,其性能受以下几个参数影响:
- 容量 (Capacity):哈希表中“桶”(bucket)的数量。
- 初始化容量 (Initial Capacity):创建时的桶数,可在构造器中指定。
- 尺寸 (Size):当前存储的键值对数量。
- 负载因子 (Load Factor):等于
size / capacity,表示哈希表的装满程度。
- 负载极限 (Load Limit):一个 0~1 的阈值(默认 0.75)。当负载因子达到此极限,哈希表会自动进行 rehashing(扩容并重新分配元素)。
哈希表在理想情况下,每个桶只存储一个元素,查询效率为 O(1)。但在发生哈希冲突时,桶内会以链表(或红黑树,在特定条件下)存储多个元素,查询时需要遍历,效率下降。

性能权衡:
- 较高的负载极限(如 0.9):节约内存空间,但会增加查询时间(冲突概率高)。
- 较低的负载极限(如 0.5):提升查询性能,但会占用更多内存。
最佳实践:如果能预估 Map 将要存储的记录数量 N,那么在创建时指定一个大于 N / 负载极限 的初始化容量,可以避免后续的 rehashing 操作,从而获得更好的性能。例如,预计存放 1000 个元素,负载因子默认 0.75,那么初始化容量设为 2048 (1000/0.75 后向上取2的幂) 比使用默认值 16 要高效得多。