java知识点---哈希表

Java 中的哈希表(Hash table)是一种非常重要的数据结构,它实现了键值对(Key-Value Pair)的存储和检索,提供了高效的数据访问能力。在 Java 标准库中,有两种常用的哈希表实现:

  1. java.util.HashMap
  2. java.util.HashTable

其中,HashMap 是最常用的一种,它允许使用 null 键和 null 值,并且是非线程安全的。而 HashTable 是遗留类,它是线程安全的,但不允许使用 null 键或 null 值。在现代 Java 开发中,除非有特别的线程安全需求且不打算使用并发包中的 ConcurrentHashMap,否则一般推荐使用 HashMap

以下是关于 Java 中哈希表的一些关键特性与原理:

基本原理

哈希函数:哈希表的核心在于使用哈希函数(hashCode())将键(Key)转换为一个哈希码(整数值),这个哈希码将决定键值对在内部数据结构中的存储位置。理想情况下,哈希函数应该能均匀分布键值对,使得不同的键经过哈希后得到的哈希码尽可能不同。

数组与链表/红黑树:Java 中的哈希表通常由一个固定长度的数组和一系列链表(或在某些条件下变为红黑树)组成。哈希码经过计算后通常会进行取模运算,得到数组的索引位置。数组的每个元素(桶)可以是一个链表的头节点(在 Java 8 及之后版本,如果链表过长,会转化为红黑树以优化查找性能),链表中存储了所有哈希到同一索引位置的键值对。这种设计允许通过一次哈希计算和数组索引定位,快速找到可能包含所需键值对的链表或红黑树,然后在链表/红黑树中通过键的 equals() 方法精确查找或插入元素。

哈希冲突:由于哈希函数可能存在输出值的碰撞(即不同的键得到相同的哈希码),即使理想的哈希函数也不能完全避免这种情况。当两个键哈希到同一位置时,就发生了哈希冲突。Java 中的哈希表通常采用开放寻址法(不常见)或链地址法(常见)来解决冲突。HashMapHashtable 采用链地址法,即在同一索引位置的桶中形成链表(或红黑树)来存储多个冲突的键值对。

主要方法

哈希表提供了丰富的操作方法,包括:

  • 添加put(key, value) 添加一个键值对到哈希表中。如果键已经存在,旧值将被新值替换。
  • 查找get(key) 根据键查找对应的值。如果键不存在,返回 null(对于 HashMap)或抛出异常(对于 Hashtable)。
  • 删除remove(key) 删除指定键的键值对。
  • 是否存在containsKey(key) 判断哈希表是否包含指定的键。
  • 大小size() 返回哈希表中键值对的数量。
  • 迭代:通过 iterator()entrySet()keySet()values() 等方法遍历哈希表的所有元素。

扩容机制

随着哈希表中元素数量的增长,为了维持良好的查找性能,当负载因子(已存储元素数量与总容量的比例)达到一定阈值时,哈希表会自动扩容。扩容时,会创建一个新的、更大的数组,并将所有现有元素重新哈希到新的数组中。这个过程通常伴随着性能开销,但能确保哈希表的性能不会因元素过多而严重下降。

并发处理

如前所述,HashMap 是非线程安全的,如果在多线程环境下使用,需要外部进行同步控制。对于并发环境下的哈希表操作,Java 提供了 java.util.concurrent.ConcurrentHashMap 类,它在内部实现了高效的并发控制,能够在多线程环境中安全地进行读写操作,且通常比同步的 Hashtable 或外部同步的 HashMap 具有更好的性能。

1. HashMap

  • 特点

    • 非线程安全,适用于单线程环境或外部同步控制的多线程环境。
    • 允许使用 null 键和 null 值。
    • 底层实现基于数组+链表/红黑树(Java 8及更高版本),通过哈希函数计算键的哈希码,然后取模得到数组索引。当哈希冲突发生时,使用链表或红黑树存储冲突键值对。
    • 提供了良好的性能,平均时间复杂度为 O(1)(理想情况下,取决于哈希函数的质量和负载因子)。
  • 典型用例

    • 需要快速查找、插入和删除键值对的场景。
    • 允许使用 null 键值,并且不需要线程安全控制的场合。

2. Hashtable

  • 特点

    • 线程安全,使用 synchronized 关键字同步方法,但性能较低,不推荐在现代多线程编程中使用。
    • 不允许使用 null 键和 null 值。
    • 底层实现与 HashMap 类似,也是基于数组+链表,但其方法同步,确保了多线程环境下的数据一致性。
  • 典型用例

    • 早期多线程环境下需要线程安全的哈希表,但现在通常被 ConcurrentHashMap 替代。

3. LinkedHashMap

  • 特点

    • 继承自 HashMap,保持了其大部分特性,包括非线程安全和允许 null 键值。
    • 除了哈希表功能外,还维护了一个双向链表,用于记录元素的插入顺序或访问顺序(取决于构造方法参数)。
    • 提供了迭代器按插入顺序或访问顺序遍历键值对。
  • 典型用例

    • 需要按照插入或访问顺序遍历键值对的场景,如缓存(LRU策略)或需要保持历史插入顺序的应用。

4. TreeMap

  • 特点

    • 底层实现基于红黑树(自平衡二叉搜索树),键必须实现 Comparable 接口或提供 Comparator 对象,以确保键的可排序性。
    • 提供了有序的键值对视图,可以根据键的自然顺序或自定义顺序进行排序。
    • 非线程安全,允许 null 值但不允许 null 键(因为无法比较)。
  • 典型用例

    • 需要键值对按特定顺序排列或快速进行键的范围查询的场景,如词典、优先级队列等。

5. ConcurrentHashMap

  • 特点

    • 线程安全的哈希表,设计用于高并发环境,提供了优于 Hashtable 的性能。
    • 使用分段锁(Segment Lock)或 CAS(Compare-and-Swap)等技术实现细粒度的并发控制,减少了锁竞争。
    • 允许 null 值但不允许 null 键。
  • 典型用例

    • 多线程环境中需要高效、线程安全的键值对存储,尤其是在频繁读写操作的情况下。

总结来说,Java 中的这五种哈希表各有特点,适用于不同的场景。在选择时,应考虑是否需要线程安全、是否需要保持键的顺序、是否允许 null 键值、以及对性能的需求等因素。在大多数情况下,HashMap 作为通用选择,LinkedHashMap 用于保持顺序,TreeMap 用于排序,而 ConcurrentHashMap 用于多线程环境。Hashtable 已经逐渐被弃用,仅在特定的历史遗留代码或兼容性要求下使用。

当然可以。下面分别给出 HashMapHashtableLinkedHashMapTreeMapConcurrentHashMap 的使用示例。

1. HashMap

import java.util.HashMap;

public class HashMapExample {
    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<>();

        // 添加键值对
        map.put("Apple", 1);
        map.put("Banana", 2);
        map.put("Cherry", 3);

        // 查找值
        int appleCount = map.get("Apple"); // 输出:1

        // 更新值
        map.put("Banana", 4); // 替换原有值

        // 判断是否存在键
        boolean hasOrange = map.containsKey("Orange"); // 输出:false

        // 删除键值对
        map.remove("Cherry");

        // 遍历键值对
        for (String fruit : map.keySet()) {
            System.out.println(fruit + ": " + map.get(fruit));
        }

        // 输出:Apple: 1, Banana: 4
    }
}

2. Hashtable

import java.util.Hashtable;

public class HashtableExample {
    public static void main(String[] args) {
        Hashtable<String, Integer> map = new Hashtable<>();

        // 添加键值对(不允许使用 null 键或值)
        map.put("Apple", 1);
        map.put("Banana", 2);

        // 查找值
        int appleCount = map.get("Apple"); // 输出:1

        // 更新值
        map.put("Banana", 4); // 替换原有值

        // 判断是否存在键
        boolean hasOrange = map.containsKey("Orange"); // 输出:false

        // 删除键值对
        map.remove("Banana");

        // 遍历键值对
        for (String fruit : map.keySet()) {
            System.out.println(fruit + ": " + map.get(fruit));
        }

        // 输出:Apple: 1
    }
}

3. LinkedHashMap

import java.util.LinkedHashMap;
import java.util.Map;

public class LinkedHashMapExample {
    public static void main(String[] args) {
        LinkedHashMap<String, Integer> map = new LinkedHashMap<>();

        // 添加键值对
        map.put("Apple", 1);
        map.put("Banana", 2);
        map.put("Cherry", 3);

        // 保持插入顺序
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }

        // 输出:Apple: 1, Banana: 2, Cherry: 3

        // 也可以根据访问顺序保留
        // LinkedHashMap<String, Integer> map = new LinkedHashMap<>(16, 0.75f, true); // 第三个参数设为 true

        // 遍历键值对(按访问顺序)
        for (String fruit : map.keySet()) {
            map.get(fruit); // 触发访问
        }
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

4. TreeMap

import java.util.TreeMap;

public class TreeMapExample {
    public static void main(String[] args) {
        TreeMap<String, Integer> map = new TreeMap<>();

        // 添加键值对(键自动排序)
        map.put("Apple", 1);
        map.put("Banana", 2);
        map.put("Cherry", 3);

        // 查找值
        int appleCount = map.get("Apple"); // 输出:1

        // 更新值
        map.put("Banana", 4); // 替换原有值

        // 判断是否存在键
        boolean hasOrange = map.containsKey("Orange"); // 输出:false

        // 删除键值对
        map.remove("Cherry");

        // 遍历键值对(按键的自然顺序)
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }

        // 输出:Apple: 1, Banana: 4
    }
}

5. ConcurrentHashMap

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // 添加键值对(多线程安全)
        map.put("Apple", 1);
        map.put("Banana", 2);

        // 在另一个线程中同时修改
        new Thread(() -> map.put("Cherry", 3)).start();

        // 查找值
        int appleCount = map.get("Apple"); // 输出:1

        // 更新值(多线程安全)
        map.put("Banana", 4); // 替换原有值

        // 判断是否存在键
        boolean hasOrange = map.containsKey("Orange"); // 输出:false

        // 删除键值对
        map.remove("Cherry");

        // 遍历键值对(多线程安全)
        for (String fruit : map.keySet()) {
            System.out.println(fruit + ": " + map.get(fruit));
        }
    }
}

以上示例展示了如何创建、添加、查找、更新、删除键值对以及遍历各个哈希表类型的数据。注意,对于 ConcurrentHashMap 示例,虽然在代码中模拟了多线程操作,但实际上在单线程环境中运行时无法观察到其并发控制的效果。在实际多线程应用中,ConcurrentHashMap 能确保在并发访问下的正确性和一致性。

  • 27
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值