Java 中实现哈希表

今天也要加油呀

    • 1. 基本用法
      • 1.1 创建 HashMap
      • 1.2 插入数据(put)
      • 1.3 获取数据(get)
      • 1.4 判断是否包含键或值
      • 1.5 删除数据(remove)
      • 1.6 更新数据
      • 1.7 其他方法
    • 2. 遍历方式
    • 3. HashMap 的内部工作原理(简要)
    • 4. 线程安全与 ConcurrentHashMap
    • 5. Hashtable 与 HashMap 的区别
    • 6. 总结

在 Java 中,实现哈希表功能的数据结构主要有以下两种:

  1. Hashtable

    • 这是 Java 1.0 时就存在的“传统”哈希表实现,位于 java.util.Hashtable 类中。
    • 它是线程安全的,多线程环境下可以直接使用,但效率相比后来的 HashMap 较低。
    • 不允许 null 作为键或者值。
  2. HashMap

    • 这是 Java 1.2 引入的,位于 java.util.HashMap 类中,使用更广泛。
    • 它在大多数情况下是哈希表的首选实现。
    • 不是线程安全的,如果需要线程安全,可以使用 Collections.synchronizedMap(...) 进行包装,或者使用并发包下的 ConcurrentHashMap
    • 允许键或者值为 null(最多允许一个 null 键,可以有多个 null 值)。

一般而言,如果没有特别的线程安全需求,推荐使用 HashMap。以下以 HashMap 为例介绍其常见用法和注意事项。


1. 基本用法

1.1 创建 HashMap

// 无参构造函数,使用默认初始容量(16)和加载因子(0.75)
Map<String, Integer> map = new HashMap<>();

// 指定初始容量
Map<String, Integer> mapWithCapacity = new HashMap<>(32);

// 指定初始容量和加载因子
Map<String, Integer> mapWithCapacityAndFactor = new HashMap<>(32, 0.8f);
  • 初始容量:HashMap 底层维护一个哈希桶数组,初始容量就是数组的长度。默认是 16,当键值对数量超过一定阈值时(容量 * 加载因子)会自动扩容。
  • 加载因子(Load Factor):默认值为 0.75,代表当容器中元素数量达到容量的 75% 时会进行扩容。可以根据实际需要进行调节,但通常使用默认值即可。

1.2 插入数据(put)

map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);
  • 如果插入相同的键,会覆盖对应的值,并返回被覆盖的旧值。
  • 如果插入的是一个新键,则返回 null

1.3 获取数据(get)

Integer value = map.get("apple");
System.out.println(value); // 1
  • 如果键不存在,则返回 null
  • 注意区分 “key 不存在” 返回 null 和 “key 存在但 value 为 null” 的情况,可以先调用 containsKey() 判断是否包含该键。

1.4 判断是否包含键或值

boolean hasApple = map.containsKey("apple"); // true
boolean hasValue1 = map.containsValue(1);    // true

1.5 删除数据(remove)

map.remove("banana"); // 移除 key = "banana" 的条目
// 或者移除 key-value 都匹配的条目
map.remove("orange", 3);
  • map.remove(key) 会返回被删除的 value,如果 key 不存在则返回 null
  • map.remove(key, value) 会在 key-value 同时满足时才删除,返回 true / false 表示是否成功删除。

1.6 更新数据

更新数据其实和插入共用 put 方法,如果 map 中已存在对应的键,就会覆盖旧值。

或使用 putIfAbsent 方法确保仅在键不存在时才放入数据:

map.putIfAbsent("apple", 10); // 如果 "apple" 不存在则写入 10,如果存在则不做任何操作

1.7 其他方法

  • size():获取键值对数量。
  • isEmpty():判断是否为空。
  • clear():清空所有条目。

2. 遍历方式

在实际开发中,遍历一个 Map 比较常见,常用方式包括:

  1. 遍历 keySet(只遍历键)

    for (String key : map.keySet()) {
        System.out.println("Key: " + key);
    }
    
  2. 遍历 values(只遍历值)

    for (Integer val : map.values()) {
        System.out.println("Value: " + val);
    }
    
  3. 遍历 entrySet(同时遍历键和值)

    for (Map.Entry<String, Integer> entry : map.entrySet()) {
        String key = entry.getKey();
        Integer value = entry.getValue();
        System.out.println(key + " -> " + value);
    }
    
    • 这是最常见的完整遍历方式,可以同时获取键和值。

3. HashMap 的内部工作原理(简要)

  1. 底层数据结构:HashMap 底层是基于数组 + 链表(JDK 1.8 以后,当链表长度超过阈值时会转为红黑树,极大地提高了查询效率)。
  2. 哈希计算:存储时会根据 key 的 hashCode() 计算哈希值,决定该 key-value 将存放到数组的哪一个槽(bucket)中。
  3. 碰撞处理(Collision)
    • 当哈希值相同或散列到相同索引时,会将新的键值对挂到已有节点的后面(形成链表或树)。
    • JDK 1.8 的改进:链表长度超过某个阈值(默认为 8)时,会将链表转化为红黑树,以提升查询效率。
  4. 扩容
    • 当存储的元素个数超过 capacity * loadFactor 时,会进行扩容,新的容量通常是原来的 2 倍,并将原有元素重新哈希分布到新的数组中。
    • 扩容开销相对较高,实际使用中如果能预估数据规模,最好在创建时指定一个合适的初始容量,减少扩容带来的性能损失。

4. 线程安全与 ConcurrentHashMap

  • HashMap 在多线程环境下并不安全,可能导致数据不一致或者产生死循环等问题。
  • 如需在多线程环境中使用,可以考虑:
    1. 使用 Collections.synchronizedMap(new HashMap<>()) 获取一个同步包装类,但是在并发量较大时性能较差。
    2. 使用 ConcurrentHashMap,这是并发包 (java.util.concurrent) 下提供的线程安全且高效的哈希表实现。
    3. 如果读远多于写,也可以考虑使用只读的 Map 或者其他并发优化手段。

5. Hashtable 与 HashMap 的区别

  1. 线程安全
    • Hashtable 的所有方法基本都被 synchronized 修饰,是线程安全的;
    • HashMap 非线程安全,需要额外手段来保证多线程环境下的安全。
  2. 是否允许 null
    • Hashtable 不允许 null 作为键或值;
    • HashMap 允许 1 个 null 键和多个 null 值。
  3. 扩容机制
    • Hashtable 扩容时,容量变为原来的 2 倍 + 1;
    • HashMap 扩容时,容量变为原来的 2 倍。
  4. 使用场景
    • Hashtable 是早期遗留类,除非有特殊原因,一般不再推荐使用;
    • HashMap 是现代开发中更常用的哈希表实现。

6. 总结

  • HashMap 是 Java 中最常用的哈希表实现,用来存储 key-value 键值对,内部通过哈希码快速定位和查找。
  • 日常开发中,如果没有线程安全的需求,使用 HashMap;如果需要线程安全且并发量较大,优先考虑 ConcurrentHashMap
  • 根据数据规模合理设置初始容量并了解其扩容机制,能够提升性能并减少内存浪费。
  • 注意在多线程环境下使用非线程安全的集合类会导致数据不一致或者其他异常,需结合使用同步手段或者并发包下的专用数据结构。

通过以上内容,相信你对 Java 中哈希表(尤其是 HashMap)的使用、原理及注意事项有了较为清晰的认识。祝学习愉快!

你可能感兴趣的:(Java算法笔记,java,散列表,开发语言)