【java第58集】java Map双列集合详解

一. Map接口概述

在这里插入图片描述

1.1 Map的核心概念

Java的Map是集合框架中的核心接口之一,用于存储键值对(Key-Value Pairs),通过键(Key)快速查找对应的值(Value)。与ListSet不同,Map是一种双列集合,其核心特点是:

  • 键(Key)的唯一性:同一个Map中不能有两个相同的键。
  • 值(Value)的可重复性:不同的键可以映射到相同的值。
  • 键值对的无序性(除非使用特定实现类如LinkedHashMapTreeMap)。

1.2 Map与 Collection的区别

特性MapCollection
数据结构键值对(Key-Value Pairs)单列集合(仅存储值)
唯一性键唯一,值可重复Set 唯一,List 可重复
存取方式通过键快速查找值通过索引或迭代器访问元素
排序通常无序(TreeMap 可排序)List 有序,Set 无序(TreeSet 可排序)

二、Map的常见实现类

2.1 HashMap

  • 底层结构:基于哈希表(数组 + 链表/红黑树)。

  • 特点

    • 非线程安全:适合单线程环境。
    • 允许 null 键和 null
    • 无序性:键值对的存储顺序与插入顺序无关。
    • 性能高:平均时间复杂度为 O(1)
  • 适用场景:对性能要求高且不需要线程安全的场景(如缓存、数据映射)。

  • HashMap 的优化:

    • 链表转红黑树:JDK 8 中,当链表长度超过阈值(默认 8)且哈希表容量达到 64 时,链表会转换为红黑树,查询效率从 O(n) 提升至 O(log n)

    • 初始容量与负载因子

      • 初始容量:默认 16,可以通过构造函数指定。
      • 负载因子:默认 0.75,表示当元素数量超过 容量 * 负载因子 时扩容。
    • 示例代码:

      Map<String, Integer> map = new HashMap<>();
      map.put("apple", 5);   // 添加键值对
      map.put("banana", 3);
      System.out.println(map.get("apple"));  // 输出 5
      

2.2 TreeMap

  • 底层结构:基于红黑树(平衡二叉搜索树)。

  • 特点

    • 按键自然顺序排序(或自定义比较器)。
    • 不允许 null(因为 null 无法比较大小)。
    • 操作时间复杂度为 O(log n)
  • 适用场景:需要按键排序的场景(如排行榜、区间查询)。

  • 自定义排序:

    Map<String, Integer> treeMap = new TreeMap<>(Comparator.reverseOrder());
    treeMap.put("apple", 5);
    treeMap.put("banana", 3);
    System.out.println(treeMap);  // 输出 {banana=3, apple=5}
    

2.3 LinkedHashMap

  • 底层结构:继承自 HashMap,维护一个双向链表。

  • 特点

    • 保持插入顺序(或访问顺序)。
    • 性能接近 HashMap
  • 适用场景:需要保持插入顺序或实现 LRU 缓存的场景。

  • LRU 缓存示例:

    Map<String, Integer> lruCache = new LinkedHashMap<>(16, 0.75f, true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {
            return size() > 3;  // 保留最多 3 个元素
        }
    };
    lruCache.put("apple", 5);
    lruCache.put("banana", 3);
    lruCache.put("cherry", 2);
    lruCache.put("date", 1);  // 超出容量,删除最久未使用的 "apple"
    

2.4 Hashtable

  • 底层结构:基于哈希表。
  • 特点
    • 线程安全:所有方法都同步。
    • 不允许 null 键和 null
    • 性能较低:由于全表加锁。
  • 适用场景:遗留代码或需要线程安全但性能要求不高的场景。

2.5 ConcurrentHashMap

  • 底层结构:基于分段锁(Java 8 后优化为 CAS + synchronized)。

  • 特点

    • 线程安全:支持高并发。
    • 允许 null(但不允许 null 键)。
    • 性能优于 Hashtable
  • 适用场景:多线程环境下需要高性能的并发访问。

  • 并发场景示例:

    Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
    concurrentMap.put("apple", 5);
    concurrentMap.computeIfAbsent("banana", k -> 3);  // 如果键不存在,则计算并插入
    

2.6 EnumMap

  • 底层结构:

    • 基于数组EnumMap 的底层是一个数组,数组的索引由枚举常量的 ordinal() 值决定(即枚举声明的顺序)。
    • 类型绑定EnumMap 的所有键必须是同一个枚举类型,数组长度等于该枚举类型的常量数量。
    • 高效存储:无需计算哈希码或处理哈希冲突,直接通过 ordinal() 值定位数组下标。
  • 特点:

    • 高性能
      • 插入/查找时间复杂度为 O(1):通过枚举的 ordinal() 值直接访问数组。
      • 内存紧凑:数组长度等于枚举常量数量,无冗余空间。
    • 有序性
      • 键值对按照枚举常量的声明顺序存储。
    • 类型安全
      • 键必须是预定义的枚举类型,编译期检查避免运行时错误。
    • 不允许 null
      • 插入 null 键会抛出 NullPointerException
        -非线程安全
      • HashMap 一样,EnumMap 不是线程安全的。
  • 适用场景:

    • 枚举键的高效映射:当键是枚举类型且需要快速访问时(如状态机、配置映射)。
    • 需要保持枚举顺序:例如,按枚举声明顺序遍历数据(如报表生成、流程步骤)。
    • 资源受限环境:因内存效率高,适合嵌入式系统或性能敏感场景。
  • 示例代码:

    enum Season { WINTER, SPRING, SUMMER, FALL }
    
    public class EnumMapExample {
        public static void main(String[] args) {
            EnumMap<Season, String> seasonMap = new EnumMap<>(Season.class);
            seasonMap.put(Season.WINTER, "Cold");
            seasonMap.put(Season.SPRING, "Warm");
            seasonMap.put(Season.SUMMER, "Hot");
            seasonMap.put(Season.FALL, "Cool");
    
            // 按枚举声明顺序遍历
            for (Season season : Season.values()) {
                System.out.println(season + ": " + seasonMap.get(season));
            }
        }
    }
    

2.7 Properties

  • 底层结构:

    • 继承自 Hashtable(Java 8 及之前),Java 9+ 改为基于 HashMap
    • 键值对类型限制
      • 键和值必须是 String 类型(否则会抛出 ClassCastException)。
    • 支持属性文件操作
      • 通过 load(InputStream).properties 文件加载键值对。
      • 通过 store(OutputStream, String) 将键值对保存到文件。
  • 特点:

    • 线程安全(Java 8 及之前):
      • 继承自 Hashtable,所有方法同步。Java 9+ 改用 HashMap,但 Properties 仍通过同步方法保证线程安全。
    • 支持注释
      • 属性文件中可使用 #! 添加注释。
    • 默认值支持
      • 可通过 defaults 属性设置默认值,键不存在时自动查找。
    • 键值对格式
      • 支持 key=valuekey:valuekey value 等格式。
    • 不允许 null 键或值
      • 插入 null 键/值会抛出 NullPointerException
  • 适用场景:

    • 配置管理
      • 存储和读取应用程序配置(如数据库连接、日志配置、国际化资源)。
    • 持久化属性
      • 需要将键值对保存到文件并读取的场景(如用户偏好设置)。
    • 跨平台兼容性
      • 属性文件为纯文本,适合跨语言或跨平台的配置共享。
  • 示例代码:

    import java.io.*;
    import java.util.Properties;
    
    public class PropertiesExample {
        public static void main(String[] args) throws IOException {
            Properties props = new Properties();
    
            // 从文件加载属性
            try (InputStream in = new FileInputStream("config.properties")) {
                props.load(in);
            }
    
            // 获取属性值
            String dbUrl = props.getProperty("database.url");
            System.out.println("Database URL: " + dbUrl);
    
            // 设置新属性并保存到文件
            props.setProperty("new.key", "new.value");
            try (OutputStream out = new FileOutputStream("config.properties")) {
                props.store(out, "Updated config");
            }
        }
    }
    

三、Map的常用方法

3.1 添加/更新键值对

  • V put(K key, V value)
    插入键值对,若键存在则覆盖旧值,返回旧值。

    map.put("apple", 5);
    
  • void putAll(Map<? extends K, ? extends V> m)
    合并另一个 Map 的键值对。

    map.putAll(anotherMap);
    

3.2 获取值

  • V get(Object key)
    返回指定键对应的值,若键不存在则返回 null

    int count = map.get("banana");
    
  • V getOrDefault(Object key, V defaultValue)
    返回指定键对应的值,若键不存在则返回默认值。

    int count = map.getOrDefault("grape", 0);
    

3.3 删除键值对

  • V remove(Object key)
    删除指定键的键值对,返回被删除的值。

    map.remove("apple");
    
  • void clear()
    清空所有键值对。

    map.clear();
    

3.4 查询操作

  • boolean containsKey(Object key)
    判断是否包含指定键。

    boolean hasApple = map.containsKey("apple");
    
  • boolean containsValue(Object value)
    判断是否包含指定值。

    boolean hasFive = map.containsValue(5);
    
  • int size()
    返回键值对数量。

    int size = map.size();
    
  • boolean isEmpty()
    判断是否为空。

    boolean isEmpty = map.isEmpty();
    

3.5 遍历操作

  • Set<K> keySet()
    返回所有键的集合。

    for (String key : map.keySet()) {
        System.out.println(key);
    }
    
  • Collection<V> values()
    返回所有值的集合。

    for (Integer value : map.values()) {
        System.out.println(value);
    }
    
  • Set<Map.Entry<K, V>> entrySet()
    返回所有键值对的集合。

    for (Map.Entry<String, Integer> entry : map.entrySet()) {
        System.out.println(entry.getKey() + " -> " + entry.getValue());
    }
    

3.6 Java 8 新增方法

  • default void forEach(BiConsumer<? super K, ? super V> action)
    对每个键值对执行操作。

    map.forEach((key, value) -> System.out.println(key + ": " + value));
    
  • default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
    如果键不存在,则计算并插入值。

    map.computeIfAbsent("pear", k -> 7);
    

四、使用Map的注意事项

4.1 键的唯一性和一致性

  • 键必须正确实现 hashCode()equals() 方法,否则可能导致哈希冲突或查找失败。
  • 自定义键的示例
    class CustomKey {
        private String name;
        private int id;
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            CustomKey that = (CustomKey) o;
            return id == that.id && Objects.equals(name, that.name);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(name, id);
        }
    }
    

4.2 线程安全

  • 非线程安全的 Map(如 HashMap)在多线程环境下需使用 Collections.synchronizedMap 包装或选择 ConcurrentHashMap
    Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
    

4.3 性能优化

  • 初始容量和负载因子:预估数据量,设置合适的初始容量和负载因子以减少扩容次数。
  • 避免哈希冲突:确保键的 hashCode()equals() 方法合理,减少链表或红黑树的使用。

4.4 遍历顺序

  • HashMapTreeMap 的遍历顺序与插入顺序无关。
  • LinkedHashMap 保持插入顺序或访问顺序(通过构造方法指定)。

五、遍历Map的方式

5.1 使用 keySet() 遍历

for (String key : map.keySet()) {
    System.out.println(key + ": " + map.get(key));
}

5.2 使用 entrySet() 遍历

for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + " -> " + entry.getValue());
}

5.3 使用 Iterator 遍历 吗,

Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<String, Integer> entry = iterator.next();
    System.out.println(entry.getKey() + " -> " + entry.getValue());
}

5.4 使用 Java 8 的 Stream API

map.entrySet().stream().forEach(entry -> 
    System.out.println(entry.getKey() + ": " + entry.getValue())
);

六、常见问题与解决方案

6.1 如何保证键的唯一性?

  • Map 自动检查键的唯一性,重复键会覆盖旧值。确保键的 equals()hashCode() 方法正确实现。

6.2 如何处理 null 键和 null 值?

  • HashMap 允许 null 键和 null,但 TreeMap 不允许 null(因为无法比较大小)。

6.3 如何在多线程环境下安全使用 Map

  • 使用 ConcurrentHashMap 或通过 Collections.synchronizedMap 包装普通 Map

6.4 如何遍历时修改 Map

  • 应当使用迭代器的 remove() 方法。如果直接修改 Map 会抛出 ConcurrentModificationException

七、总结

7.1 选择实现类

  • HashMap:高性能、无序、允许 null
  • TreeMap:按键排序。
  • LinkedHashMap:保持插入顺序或实现 LRU 缓存。
  • ConcurrentHashMap:高并发环境下的线程安全。

7.2 方法使用

  • 添加/更新putputAll
  • 获取/删除getremove
  • 遍历keySetentrySetforEach

7.3 性能优化

  • 合理设置初始容量和负载因子
  • 避免哈希冲突
  • 使用不可变对象作为键

7.4 实际应用场景

  • 缓存:使用 HashMapConcurrentHashMap
  • 数据统计:使用 HashMap 统计频率。
  • 配置管理:使用 TreeMap 按键排序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值