小蔡学Java

Java集合相关操作之避免踩坑

2023-03-28 17:40 932 0 Java基础 Java集合

我们都知道Java集合是基础中重点知识点,因为在开发过程中无时无刻都在使用着集合进行相关逻辑操作。但是我今天的总结并不是面向于八股文面试的,

这种文章已经很多了,也不会总结的很基础入门相关操作啥的,这里主要讲讲在真正工作开发过程对集合操作踩过的坑和高效处理操作,帮助大家避免踩坑,对集合玩得飞起。

1.概述

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

  1. 层次结构: Java集合框架提供了一种层次结构,核心接口包括Collection和Map。Collection接口是所有集合类的根接口,而Map接口表示键-值对的映射。
  2. 主要接口: 一些主要的集合接口包括List、Set和Queue。List是有序集合,Set是无序且不包含重复元素的集合,Queue是一种特殊的集合,按照先进先出(FIFO)的原则进行操作。
  3. 具体实现类: Java提供了多个具体的集合类,其中一些常用的包括ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等。每个类都有其特定的用途和性能特点。
  4. 泛型: Java集合框架引入了泛型,允许你指定集合中存储的元素类型。这提高了类型安全性并减少了在使用集合时的强制类型转换。
  5. 迭代器: 集合框架提供了迭代器接口,允许以统一的方式遍历集合中的元素。这简化了遍历集合的过程,使代码更加清晰。
  6. 并发集合: Java 5及更高版本引入了并发集合,如ConcurrentHashMap和CopyOnWriteArrayList,用于支持多线程环境下的高效和安全的操作。
  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方式:

	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);
		}

控制台报错如下:

	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循环模式:

	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);
		}

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

[1, 3]

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

	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

	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:

	@Data
	@Builder
	@AllArgsConstructor
	@NoArgsConstructor
	public class User {
		private Long id;
		private String userNo;
		private String name;
		private Integer age;
	}

toMap操作:

	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);
		}

报错如下:

	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)

解决方案:

	//解决方案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 异常。


示例:

 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);
    }

报错如下:

	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 的处理策略。

	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 体现的是适配器模式,只是转换接口,后台的数据仍是数组。

示例如下:

	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);
		}

报错如下:

	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 的所有操作最终会反映到原列表上。

	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);
		}

报错如下:

	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 的所有操作最终会反映到原列表上:

	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);
		}

执行结果:

[1, 2, 4, 3]

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

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

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

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

3.1 Hutool

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

	<dependency>
		<groupId>cn.hutool</groupId>
		<artifactId>hutool-all</artifactId>
		<version>5.7.11</version>
	</dependency>

Hutool 集合操作示例:

	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 类提供了很多有用的静态方法,如合并集合、查找最大/最小值等

	<dependency>
		<groupId>org.apache.commons</groupId>
		<artifactId>commons-collections4</artifactId>
		<version>4.4</version>
	</dependency>

示例如下:

创建不可变集合

	List<String> mutableList = new ArrayList<>();
	mutableList.add("A");
	mutableList.add("B");

	List<String> immutableList = UnmodifiableList.unmodifiableList(mutableList);

	// 尝试修改不可变集合将抛出 UnsupportedOperationException
	// immutableList.add("C"); // 抛出异常

创建同步集合

	List<String> synchronizedList = SynchronizedList.synchronizedList(new ArrayList<>());

	// 现在synchronizedList可以在多线程环境中安全使用

使用 Predicate 过滤集合

	List<String> list = Arrays.asList("Apple", "Banana", "Orange", "Grapes");

	// 过滤出以'A'开头的元素
	CollectionUtils.filter(list, s -> s.startsWith("A"));

	System.out.println("Filtered List: " + list);

转换集合类型

	List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

	// 将每个元素转换为字符串
	CollectionUtils.transform(numbers, Object::toString);

	System.out.println("Transformed List: " + numbers);

Map 操作

	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 库提供了丰富的集合工具类,包括不可变集合、新的集合类型、集合操作等。例如,ImmutableList、Sets 类等

	<dependency>
	    <groupId>com.google.guava</groupId>
		<artifactId>guava</artifactId>
		<version>30.1-jre</version>
	</dependency>

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

RangeSet

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

	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 允许一个键对应多个值,非常方便地处理一对多的关系。

	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 是一种特殊的映射,保证值和键都是唯一的,可以通过值找到键。

	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 类似于一个二维表,可以有两个键来索引数据。

	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 提供了不可变的集合,它们在创建后不能被修改,具有线程安全性和性能优势。

	ImmutableList<String> immutableList = ImmutableList.of("A", "B", "C");

	// 以下操作将抛出 UnsupportedOperationException
	// immutableList.add("D");
	// immutableList.remove("A");

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

4.总结

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

评论( 0 )

  • 博主 Mr Cai
  • 坐标 河南 信阳
  • 标签 Java、SpringBoot、消息中间件、Web、Code爱好者

文章目录