一文带你真正玩转Java集合操作,避免工作中踩坑

我们都知道Java集合是基础中重点知识点,因为在开发过程中无时无刻都在使用着集合进行相关逻辑操作。但是我今天的总结并不是面向于八股文面试的,这种文章已经很多了,也不会总结的很基础入门相关操作啥的,这里主要讲讲在真正工作开发过程对集合操作踩过的坑和高效处理操作,帮助大家避免踩坑,对集合玩得飞起。

1.概述

Java集合是在处理对象组的数据时提供的一组框架和接口。它们被设计用于存储、检索和操作数据。以下是对Java集合的一些总结理解:

  1. 层次结构: Java集合框架提供了一种层次结构,核心接口包括CollectionMapCollection接口是所有集合类的根接口,而Map接口表示键-值对的映射。
  2. 主要接口: 一些主要的集合接口包括ListSetQueueList是有序集合,Set是无序且不包含重复元素的集合,Queue是一种特殊的集合,按照先进先出(FIFO)的原则进行操作。
  3. 具体实现类: Java提供了多个具体的集合类,其中一些常用的包括ArrayListLinkedListHashSetTreeSetHashMapTreeMap等。每个类都有其特定的用途和性能特点。
  4. 泛型: Java集合框架引入了泛型,允许你指定集合中存储的元素类型。这提高了类型安全性并减少了在使用集合时的强制类型转换。
  5. 迭代器: 集合框架提供了迭代器接口,允许以统一的方式遍历集合中的元素。这简化了遍历集合的过程,使代码更加清晰。
  6. 并发集合: Java 5及更高版本引入了并发集合,如ConcurrentHashMapCopyOnWriteArrayList,用于支持多线程环境下的高效和安全的操作。
  7. 不可变集合: Java 9引入了不可变集合,如List.of()Set.of(),这些集合一旦创建就不可更改,提供了更强的安全性和性能。
  8. Lambda 表达式和流: Java 8引入了Lambda表达式和流(Stream)API,使集合操作更加简洁和功能强大。使用流API可以进行复杂的集合操作,如过滤、映射、归约等。
  9. 性能和选择: 在选择集合类时,需要根据具体的需求和性能要求来选择合适的实现类。不同的集合类在性能上有所差异,因此在使用时需要根据具体场景进行选择。

2.踩坑点

2.1 不要在 foreach / for循环里进行元素的 remove / add 操作。remove 元素请使用 iterator 方式

foreach方式:

ini 复制代码
   public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.forEach(v -> {
            if (v == 2) {
                list.remove(v);
            }
        });
        System.out.println(list);
    }

控制台报错如下:

php 复制代码
Exception in thread "main" java.util.ConcurrentModificationException
  at java.util.ArrayList.forEach(ArrayList.java:1260)
  at com.shepherd.basedemo.Coll.CollTest.main(CollTest.java:19)

改为for循环模式:

ini 复制代码
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        for (Integer v : list) {
            if (v == 2) {
                list.remove(v);
            }
        }
        System.out.println(list);
    }

控制台竟然没有报错,输出如下

csharp 复制代码
[1, 3]

但是我们把删除元素的判断条件改为 v==1 或者 v==3就报错了

php 复制代码
Exception in thread "main" java.util.ConcurrentModificationException
  at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
  at java.util.ArrayList$Itr.next(ArrayList.java:859)
  at com.shepherd.basedemo.Coll.CollTest.main(CollTest.java:24)

是不是很诡异,具体原因可以自行跟一下代码看看为啥出现这种情况,总的来说,无论是foreach还是for循环里面进行元素的 remove / add 操作都是不靠谱的,正确方式是使用迭代器iterator

ini 复制代码
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()) {
            Integer v = iterator.next();
            if (v == 1) {
                iterator.remove();
            }
        }
        System.out.println(list);
    }

Set,Map操作和List是一样的,这里不一一展示了。

2.2 在使用 java.util.stream.Collectors 类的 toMap() 方法转为 Map 集合时,一定要注意当 value为 null 时会抛 NPE 异常。

示例如下:平时我们根据一个对象list提取一个map是很常见的事情,这里先建一个类User

less 复制代码
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long id;
    private String userNo;
    private String name;
    private Integer age;
}

toMap操作:

ini 复制代码
    public static void main(String[] args) {
        List<User> list = new ArrayList<>();
        User u1 = User.builder().id(1L).userNo("001").name("张三").build();
        User u2 = User.builder().id(2L).userNo("002").name("李四").build();
        User u3 = User.builder().id(3L).userNo("003").build();   // name为空
        list.add(u1);
        list.add(u2);
        list.add(u3);
        Map<Long, String> userNameMap = list.stream().collect(Collectors.toMap(User::getId, User::getName));
        System.out.println(userNameMap);
    }
​

报错如下:

php 复制代码
Exception in thread "main" java.lang.NullPointerException
  at java.util.HashMap.merge(HashMap.java:1225)
  at java.util.stream.Collectors.lambda$toMap$58(Collectors.java:1320)
  at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
  at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1382)
  at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
  at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
  at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
  at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
  at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
  at com.shepherd.basedemo.Coll.CollTest.main(CollTest.java:24)

解决方案:

ini 复制代码
        //解决方案1 用filter过滤value为null的
        Map<Long, String> userNameMap = new HashMap<>();
        userNameMap = list.stream().filter(user -> user.getName() != null).collect(Collectors.toMap(User::getId, User::getName));
        System.out.println(userNameMap);
​
        //解决方案2 手动实现重载方法
        userNameMap = list.stream().collect(HashMap::new, (map, user) -> map.put(user.getId(), user.getName()), HashMap::putAll);
        System.out.println(userNameMap);
​
        //解决方案3 使用原来的for循环或者foreach循环
        Map<Long, String> finalUserNameMap = userNameMap;
        list.forEach(user -> finalUserNameMap.put(user.getId(), user.getName()));
        System.out.println(finalUserNameMap);
​
        //解决方案4 使用Optional包装value
        Map<Long, Optional<String>> coll = list.stream().collect(Collectors.toMap(User::getId, user -> Optional.ofNullable(user.getName())));
        System.out.println(coll);

2.3 在使用 java.util.stream.Collectors 类的 toMap() 方法转为 Map 集合时,一定注意当出现相同 key 时会抛出IllegalStateException 异常。

示例:

ini 复制代码
    public static void main(String[] args) {
        List<User> list = new ArrayList<>();
        User u1 = User.builder().id(1L).userNo("001").name("张三").build();
        User u2 = User.builder().id(2L).userNo("002").name("李四").build();
        User u3 = User.builder().id(3L).name("张三").build();   // userNo为空
        list.add(u1);
        list.add(u2);
        list.add(u3);
        Map<String, Long> map = list.stream().collect(Collectors.toMap(User::getName, User::getId));
        System.out.println(map);
    }

报错如下:

php 复制代码
Exception in thread "main" java.lang.IllegalStateException: Duplicate key 1
  at java.util.stream.Collectors.lambda$throwingMerger$0(Collectors.java:133)
  at java.util.HashMap.merge(HashMap.java:1254)
  at java.util.stream.Collectors.lambda$toMap$58(Collectors.java:1320)
  at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
  at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1382)
  at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
  at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
  at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
  at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
  at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
  at com.shepherd.basedemo.Coll.CollTest.main(CollTest.java:24)

解决方案:调用toMap()方法时一定要使用参数类型为 BinaryOperator,参数名为 mergeFunction 的方法,参数 mergeFunction 的作用是当出现 key 重复时,自定义对 value 的处理策略。

rust 复制代码
  Map<String, Long> map = list.stream().collect(Collectors.toMap(User::getName, User::getId, (v1, v2)->v2));

2.4 使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法,它的 add/ remove / clear 方法会抛出 UnsupportedOperationException 异常。

asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。

示例如下:

ini 复制代码
    public static void main(String[] args) {
        Integer[] nums = new Integer[]{1,2,3};
        List<Integer> list = Arrays.asList(nums);
        list.add(4);
        System.out.println(list);
    }

报错如下:

php 复制代码
Exception in thread "main" java.lang.UnsupportedOperationException
  at java.util.AbstractList.add(AbstractList.java:148)
  at java.util.AbstractList.add(AbstractList.java:108)
  at com.shepherd.basedemo.Coll.CollTest.testArrayAsList(CollTest.java:103)
  at com.shepherd.basedemo.Coll.CollTest.main(CollTest.java:17)

同时还需要注意改变数组nums, list中的元素也会随之修改,反之亦然。

Collections 类返回的对象,如:emptyList() / singletonList() 等都是 immutable list,也是不可对其进行添加或者删除元素的操作。

2.5 ArrayList 的 subList 结果不可强转成 ArrayList,否则会抛出 ClassCastException 异常

subList() 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList 本身,而是 ArrayList 的一个视图,对于SubList 的所有操作最终会反映到原列表上。

ini 复制代码
   public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        List<Integer> subList = list.subList(1, 2);
        ArrayList l = (ArrayList)subList;
        System.out.println(l);
    }

报错如下:

php 复制代码
Exception in thread "main" java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList
  at com.shepherd.basedemo.Coll.CollTest.main(CollTest.java:22)

对于SubList 的所有操作最终会反映到原列表上:

ini 复制代码
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        List<Integer> subList = list.subList(1, 2);
        subList.add(4);
        System.out.println(list);
    }

执行结果:

csharp 复制代码
[1, 2, 4, 3]

平时我们对list中的元素过多进行分批处理,这时候就用到了subList(),此时如果我们对subList进行编辑增删改,都会导致原集合list跟着变化,容易出现意想不到的逻辑错误。

subList() 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList 本身,而是 ArrayList 的一个视图,所以对对父集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生 ConcurrentModificationException 异常。

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址github.com/plasticene/...

Gitee地址gitee.com/plasticene3...

微信公众号Shepherd进阶笔记

交流探讨qun:Shepherd_126

3.操作集合的好用工具类依赖包

由于我们平时对集合高频使用操作,所以出现了很多好用的封装工具类依赖包供我们使用。

3.1 Hutool

Hutool 是一个国产的Java工具包,提供了许多实用的工具类,包括对集合的操作,这里强调下集合操作只是hutool工具包的一小部分,它还很多其他的工具类,比较实用的,现在很多项目开发都在使用,可以去关注下,别自己瞎折腾封装工具类了。

xml 复制代码
​
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.11</version>
</dependency>

Hutool 集合操作示例:

typescript 复制代码
​
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
​
public class HutoolExample {
    public static void main(String[] args) {
        // List操作
        List<String> list1 = CollUtil.newArrayList("A", "B", "C");
        List<String> list2 = CollUtil.newArrayList("B", "C", "D");
        List<String> union = CollUtil.union(list1, list2);
        System.out.println("Union: " + union);
​
        // Map操作
        Map<String, Object> map1 = MapUtil.newHashMap();
        map1.put("name", "John");
        map1.put("age", 25);
        Map<String, Object> map2 = MapUtil.newHashMap();
        map2.put("gender", "Male");
        Map<String, Object> resultMap = MapUtil.join(map1, map2);
        System.out.println("Joined Map: " + resultMap);
    }
}

3.2 Apache Commons Collections

这是 Apache Commons 项目的一部分,提供了一系列实用的集合类和操作。例如,CollectionUtils 类提供了很多有用的静态方法,如合并集合、查找最大/最小值等

xml 复制代码
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.4</version>
</dependency>

示例如下:

创建不可变集合

ini 复制代码
​
List<String> mutableList = new ArrayList<>();
mutableList.add("A");
mutableList.add("B");
​
List<String> immutableList = UnmodifiableList.unmodifiableList(mutableList);
​
// 尝试修改不可变集合将抛出 UnsupportedOperationException
// immutableList.add("C"); // 抛出异常

创建同步集合

arduino 复制代码
​
List<String> synchronizedList = SynchronizedList.synchronizedList(new ArrayList<>());
​
// 现在synchronizedList可以在多线程环境中安全使用

使用 Predicate 过滤集合

ini 复制代码
​
List<String> list = Arrays.asList("Apple", "Banana", "Orange", "Grapes");
​
// 过滤出以'A'开头的元素
CollectionUtils.filter(list, s -> s.startsWith("A"));
​
System.out.println("Filtered List: " + list);

转换集合类型

ini 复制代码
​
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
​
// 将每个元素转换为字符串
CollectionUtils.transform(numbers, Object::toString);
​
System.out.println("Transformed List: " + numbers);

Map 操作

arduino 复制代码
Map<String, Integer> map = new HashMap<>();
map.put("One", 1);
map.put("Two", 2);
​
// 使用 MapUtils 获取值,如果键不存在则返回默认值
int value = MapUtils.getIntValue(map, "Three", 0);
​
System.out.println("Value for 'Three': " + value);

集合工具

CollectionUtils 类提供了众多实用的集合操作方法,如 addAll, subtract, union, intersection 等,可根据实际需要选择使用。

3.3 Guava

Google 的 Guava 库提供了丰富的集合工具类,包括不可变集合、新的集合类型、集合操作等。例如,ImmutableListSets 类等

xml 复制代码
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1-jre</version>
</dependency>
​

Guava 提供了一些比较特有且强大的集合操作,以下是一些示例:

RangeSet

RangeSet 表示一组不相交的、非空的区间。这对于表示和操作一组非连续的元素范围很有用。

csharp 复制代码
​
RangeSet<Integer> rangeSet = TreeRangeSet.create();
​
// 添加区间 [1, 5)
rangeSet.add(Range.closedOpen(1, 5));
// 添加区间 [5, 10)
rangeSet.add(Range.closedOpen(5, 10));
​
// 查询值是否在某个区间内
System.out.println("Contains 3: " + rangeSet.contains(3)); // true
System.out.println("Contains 10: " + rangeSet.contains(10)); // false

Multimap

Multimap 允许一个键对应多个值,非常方便地处理一对多的关系。

arduino 复制代码
​
Multimap<String, String> multimap = ArrayListMultimap.create();
​
multimap.put("Fruits", "Apple");
multimap.put("Fruits", "Banana");
multimap.put("Fruits", "Orange");
multimap.put("Vegetables", "Carrot");
​
// 获取键对应的所有值
Collection<String> fruits = multimap.get("Fruits");
System.out.println("Fruits: " + fruits);

BiMap

BiMap 是一种特殊的映射,保证值和键都是唯一的,可以通过值找到键。

ini 复制代码
​
BiMap<String, Integer> biMap = HashBiMap.create();
​
biMap.put("One", 1);
biMap.put("Two", 2);
​
// 根据值获取键
String key = biMap.inverse().get(2);
System.out.println("Key for value 2: " + key);

Table

Table 类似于一个二维表,可以有两个键来索引数据。

less 复制代码
​
Table<String, String, Integer> table = HashBasedTable.create();
​
table.put("Row1", "Column1", 1);
table.put("Row1", "Column2", 2);
table.put("Row2", "Column1", 3);
​
// 获取指定行和列的值
int value = table.get("Row1", "Column2");
System.out.println("Value at Row1, Column2: " + value);

Immutable 集合

Guava 提供了不可变的集合,它们在创建后不能被修改,具有线程安全性和性能优势。

ini 复制代码
​
ImmutableList<String> immutableList = ImmutableList.of("A", "B", "C");
​
// 以下操作将抛出 UnsupportedOperationException
// immutableList.add("D");
// immutableList.remove("A");

这些操作展示了 Guava 在集合处理方面的独特功能,使得它成为处理特定问题和需求的强大工具。 Guava 的集合库提供了更高级别的抽象,使得处理复杂的集合结构变得更为简单

4.总结

Java 集合框架是一组用于存储、操作和检索对象的类和接口。提供了一套强大而灵活的工具,适用于各种不同的编程需求。选择合适的集合类型和操作方式可以极大地提高代码的效率和可读性。

相关推荐
魔道不误砍柴功8 分钟前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2348 分钟前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨11 分钟前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
Chrikk2 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man2 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
测开小菜鸟2 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity3 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天3 小时前
java的threadlocal为何内存泄漏
java