Map的computeIfAbsent的使用场景和使用方法

本文介绍了如何使用Java中的Map接口提供的computeIfAbsent方法来简化复杂映射操作,特别是在需要确保某个键对应的值是一个集合,并在集合中添加元素的场景下。通过对比传统的if条件判断和put操作,展示了computeIfAbsent方法如何使代码更加简洁和高效。示例中,将一个整数数组中的重复数字归并到对应的List中,computeIfAbsent确保了每个数字只创建一个List,并自动将相同数字添加到对应的List中。

Map的computeIfAbsent的使用场景和使用方法

default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
    ...
}

我们在复杂map操作(put操作)时候有的时候不知道此时当前key对应的value值是否存在,这里,我们如果使用常规的代码编写,代码量比较大

例如我们定义一个场景:存在一个数组,我们需要将当前数组中相同的数存储到一个对应List集合中

常规实现:

    @Test
    public void test02() {
        //复杂map的使用场景:首先我们说复杂map,即map的value值为一个list集合或者是一个Set集合,对象或者是其他的集合
        //给定一个场景:现在存在一个数组,我们需要将当前数组中相同的数存储到一个对应List集合中
        int[] nums = {1,2,3,1,3,4,6,7,9,9,1,3,4,5};
        Map<Integer, List<Integer>> map = new HashMap<>();
        //普通的写法
        for (int i = 0; i < nums.length; i++) {
            if (!map.containsKey(nums[i])) {
                ArrayList<Integer> list = new ArrayList<>();
                list.add(nums[i]);
                map.put(nums[i],list);
            }else {
                map.get(nums[i]).add(nums[i]);
            }
        }
        map.forEach((key,value) -> {
            System.out.print(key + ": ");
            System.out.println(value);
        });
    }

使用computeIfAbsent方法实现:

    @Test
    public void  test03() {
        int[] nums = {1,2,3,1,3,4,6,7,9,9,1,3,4,5};
        Map<Integer, List<Integer>> map = new HashMap<>();
        //我们使用map的computeIfAbsent解决
        for (int i = 0; i < nums.length; i++) {
            //返回值是该key对应的集合list
            map.computeIfAbsent(nums[i], key -> new ArrayList<Integer>());
            map.get(nums[i]).add(nums[i]);
        }
    
        map.forEach((key,value) -> {
            System.out.print(key + ": ");
            System.out.println(value);
        });
    }

输出结果:

1: [1, 1, 1]
2: [2]
3: [3, 3, 3]
4: [4, 4]
5: [5]
6: [6]
7: [7]
9: [9, 9]
<think> 我们专注于Java中的Map集合。根据引用内容,Map是一种键值对集合,与Object的区别在于键可以是任意类型(而Object的键只能是字符串或Symbols),且Map中的键值是有序的(ES6 Map的特性,但注意在JavaMap接口本身不保证顺序,具体实现类决定顺序)。 引用[3]中提到: Map集合内含多组键值对,集合中每个元素分别存放着可访问的键名它对应的值,Map集合经常被用于缓存频繁取用的数据。 引用[4]中提到了TreeSet底层用TreeMap实现(红黑树),是有序的。 在Java中,Map接口的实现类主要有:HashMap, TreeMap, LinkedHashMap, Hashtable等。 接下来我将从以下几个方面详细讲解Java中的Map: 1. Map的核心特性 2. 主要实现类对比 3. 使用场景 4. 注意事项 注意:用户要求的是Java环境,所以我们将以Java为标准,但引用中提到了ES6C++的内容,我们只参考其思想,具体以Java为准。 一、Map的核心特性 1. 键值对存储:每个元素包含一个键(Key)一个值(Value),通过键来访问值。 2. 键唯一性:一个Map中不能有重复的键(根据键的equals方法判断是否重复)。 3. 值可重复:不同的键可以映射到同一个值。 4. 访问效率:根据不同的实现,通过键访问值的效率不同。 二、主要实现类对比 1. HashMap: - 基于哈希表(数组+链表/红黑树,Java 8以后链表长度超过8时转为红黑树) - 无序(不保证插入顺序) - 允许null键null值 - 线程不安全 - 平均时间复杂度:增删改查$O(1)$(在哈希冲突较少的情况下) 2. LinkedHashMap: - 继承自HashMap - 维护一个双向链表来记录插入顺序或访问顺序(可选) - 迭代顺序可预测(插入顺序或最近最少使用顺序) - 允许null键null值 - 线程不安全 3. TreeMap: - 基于红黑树(自平衡二叉查找树) - 有序(根据键的自然顺序或Comparator进行排序) - 不允许null键(因为需要比较),允许null值 - 线程不安全 - 时间复杂度:增删改查$O(\log n)$ 4. Hashtable: - 遗留类,基于哈希表 - 线程安全(方法使用synchronized修饰) - 不允许null键null值 - 无序(但会维持一个固定的顺序,但不可预测) - 性能较低,不推荐使用,通常用ConcurrentHashMap替代 5. ConcurrentHashMap: - 线程安全(分段锁或CAS+synchronized,Java 8以后) - 不允许null键null值 - 无序 - 高并发环境下性能优于Hashtable 三、使用场景 1. 快速键值查找(使用HashMap): 适用于需要快速通过键查找值的场景,如缓存实现。 ```java // 缓存用户信息 Map<String, User> userCache = new HashMap<>(); userCache.put("user1", new User("Tom")); User user = userCache.get("user1"); ``` 2. 需要保持插入顺序(使用LinkedHashMap): 如记录用户操作历史,需要按操作顺序展示。 ```java Map<String, String> history = new LinkedHashMap<>(); history.put("login", "2024-01-01"); history.put("edit", "2024-01-02"); // 遍历时会按put的顺序输出 ``` 3. 需要键排序(使用TreeMap): 如需要按照键的字母顺序或自定义顺序排列的场景。 ```java // 按照字典序排列单词及其出现次数 TreeMap<String, Integer> wordCount = new TreeMap<>(); wordCount.put("apple", 10); wordCount.put("banana", 5); // 输出:apple=10, banana=5(按字母升序) ``` 4. 高并发环境(使用ConcurrentHashMap): 多线程共享数据时,保证线程安全。 ```java ConcurrentHashMap<String, AtomicInteger> counters = new ConcurrentHashMap<>(); counters.computeIfAbsent("key", k -> new AtomicInteger()).incrementAndGet(); ``` 5. 数据分组:可以使用Map来对元素进行分组,例如根据某个属性分组。 ```java // 按照城市分组人员 List<Person> persons = ...; Map<String, List<Person>> groupByCity = new HashMap<>(); for (Person p : persons) { groupByCity.computeIfAbsent(p.getCity(), k -> new ArrayList<>()).add(p); } ``` 四、注意事项 1. 键的不可变性:作为键的对象应该是不可变的(或者至少保证用于计算hashCodeequals的关键属性不可变),否则可能导致无法正确访问。 2. 线程安全: - 非线程安全的Map(如HashMap)在多线程环境下可用`Collections.synchronizedMap`包装,但高并发推荐使用ConcurrentHashMap。 3. 性能选择: - 大多数情况使用HashMap(无顺序要求,快速访问)。 - 需要顺序则考虑LinkedHashMap(插入顺序或访问顺序)或TreeMap(排序)。 4. 哈希冲突:合理设计键的hashCode方法,减少冲突,提高HashMap性能。 五、与其他集合的区别 1. List:有序可重复,按索引访问。 2. Set:无序且唯一(不重复)。 3. Map:键值对,键唯一。 根据引用[4]中提到的TreeSet底层是TreeMap,实际上Java中Set的很多实现都是基于Map的(如HashSet底层是HashMap,TreeSet底层是TreeMap)。 生成相关问题: 1. HashMapConcurrentHashMap在实现线程安全上有何不同? 2. 如何设计一个对象的hashCode方法以避免HashMap的哈希碰撞? 3. LinkedHashMap如何实现LRU缓存? 4. TreeMap的红黑树结构是如何维持平衡的? 请根据以上内容生成回答。 </think> ### JavaMap的详细解析与使用场景 #### 一、Map的核心特性 1. **键值对存储** 每个元素包含唯一的键(Key)对应的值(Value),通过键快速访问值: ```java Map<String, Integer> map = new HashMap<>(); map.put("apple", 10); // 键="apple", 值=10 System.out.println(map.get("apple")); // 输出 10 ``` 2. **键的唯一性** 键不可重复,重复插入会覆盖旧值: ```java map.put("banana", 5); map.put("banana", 8); // 覆盖原有值 System.out.println(map.get("banana")); // 输出 8 ``` 3. **值的可重复性** 不同键可指向相同的值: ```java map.put("pear", 10); // 值"10"与apple相同 ``` 4. **动态容量** 自动扩容机制(如HashMap的负载因子$0.75$),无需手动管理容量[^4]。 --- #### 二、主要实现类对比 | **实现类** | **底层结构** | **特点** | **适用场景** | |----------------------|-------------------|--------------------------------------------------------------------------|--------------------------| | **HashMap** | 数组+链表/红黑树 | 最高效的随机访问,线程不安全 | 高频查询、缓存 | | **LinkedHashMap** | 链表+哈希表 | 保持插入顺序或访问顺序 | 需要顺序访问的场景 | | **TreeMap** | 红黑树 | 按键的自然顺序或Comparator排序,支持范围查询(如`subMap()`) | 需要排序的场景[^4] | | **ConcurrentHashMap**| 分段锁+CAS | 线程安全,高并发性能优于Hashtable | 多线程环境 | | **Hashtable** | 同步哈希表 | 线程安全但性能差(已过时) | 旧系统兼容 | --- #### 三、典型使用场景 1. **高频数据缓存** 存储频繁访问的数据(如数据库查询结果): ```java Map<UserId, UserProfile> userCache = new ConcurrentHashMap<>(); userCache.put(userId, profile); // 缓存用户数据 ``` 2. **数据分组统计** 按属性分组(如按城市分组用户): ```java Map<String, List<Employee>> groupByCity = new HashMap<>(); employees.forEach(emp -> groupByCity.computeIfAbsent(emp.getCity(), k -> new ArrayList<>()).add(emp) ); ``` 3. **键值动态关联** 键名不确定或需动态生成时(如解析JSON): ```java Map<String, Object> dynamicConfig = new LinkedHashMap<>(); dynamicConfig.put("timeout", 30); dynamicConfig.put("retry_count", 3); // 键名运行时确定 ``` 4. **排序需求** 需要按键排序时(如按字母序展示词汇表): ```java TreeMap<String, Definition> dictionary = new TreeMap<>(); dictionary.put("zebra", new Definition("非洲条纹马")); dictionary.put("apple", new Definition("水果")); // 自动排序为: apple -> zebra ``` 5. **去重计数** 统计元素出现次数(如词频统计): ```java Map<String, Integer> wordCount = new HashMap<>(); words.forEach(word -> wordCount.merge(word, 1, Integer::sum)); ``` --- #### 四、注意事项 1. **键对象设计** - 键需重写`equals()``hashCode()`方法(否则无法正确比较) - 推荐使用不可变对象(如String、Integer)作为键[^3] 2. **线程安全选择** - 单线程用`HashMap`,多线程用`ConcurrentHashMap` - 避免使用过时的`Hashtable` 3. **性能权衡** | **操作** | **HashMap** | **TreeMap** | |---------------|------------|------------| | 插入/删除 | $O(1)$ | $O(\log n)$ | | 按键查询 | $O(1)$ | $O(\log n)$ | | 范围查询 | 不支持 | $O(\log n)$ | 4. **内存占用** HashMap需维护数组+链表/红黑树,TreeMap需存储红黑树节点指针,大数据量时内存开销需评估。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值