找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3061

积分

0

好友

427

主题
发表于 15 小时前 | 查看: 1| 回复: 0

Java 中的 Map 接口定义了一种非常通用的数据结构,用于存储键值对。在具体的开发实践中,我们会根据不同的需求选择不同的实现。HashMapHashtable 是两个最常见的实现,但你真的了解它们之间的细微差别,以及如何在其他特定场景下选择 TreeMapPropertiesLinkedHashMap 吗?

云栈社区 的 Java 技术板块,关于集合框架的讨论总是热度不减,其性能与正确使用是保证应用稳定高效的基础。

HashMap 和 Hashtable

HashMapHashtable 都是 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。

下面的代码演示了 HashMapnull 的处理:

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

这段程序定义了 AB 两个类。A 类判断相等的标准是 count 变量,只要 count 相等,equals() 返回 truehashCode() 也相等。Hashtable 判断 value 相等的标准则是:只要 value 与另一个对象通过 equals() 方法比较返回 true 即可。

这里有一个非常重要的实践警示:如果使用可变对象作为 HashMapHashtable 的 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)));   
    }
}

因此,尽量不要使用可变对象作为 HashMapHashtable 的 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 子接口,其实现类是 TreeMapTreeMap 基于红黑树数据结构,能够保证所有的键值对处于有序状态。它的排序方式有两种:

  1. 自然排序TreeMap 的所有 key 必须实现 Comparable 接口,且是同一类的对象。
  2. 定制排序:创建 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 实现类

WeakHashMapHashMap 用法相似,但关键区别在于其 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:通常比 HashMapHashtable 慢,特别是在插入和删除时,因为它要维护红黑树的平衡。但其优势在于 key 始终有序。
  • LinkedHashMap:比 HashMap 略慢,因为它需要额外的链表来维护插入顺序。
  • IdentityHashMap:性能与 HashMap 相似,区别在于使用 == 判断相等性。
  • EnumMap:所有实现中性能最好的,但限制是 key 必须来自单一枚举类。

HashSet 和 HashMap 的性能调优

HashSetHashMap(以及 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 要高效得多。




上一篇:深入理解Python绑定方法对象:揭秘self参数传递机制与实例方法本质
下一篇:微内核架构如何解耦系统核心与插件?以OSGi与规则引擎为例解析设计关键
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-7 19:23 , Processed in 0.393838 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表