Java函数式编程之【Stream终止操作】【下】【二】【收集器toMap()】【叁参数收集操作collect()】

发布于:2025-08-03 ⋅ 阅读:(16) ⋅ 点赞:(0)

一、收集操作collect()的收集器toMap()用法

  • 收集器Collectors.toMap()的用法
    收集器Collectors.toMap(),可将流中的元素收集到映射Map实例中。toMap()有两个参数分别表示键和值,都是函数接口。收集器Collector.toMap()有三种重载形式:
	 toMap(Function, Function) //最简单的toMap()收集器形式(双参数)
	 /***处理键冲突的toMap()收集器形式(叁参数)。当可能出现键冲突(重复键key)时,可指定合并函数mergeFunction来处理冲突。***/
     toMap(Function, Function, BinaryOperator)
     /***定制Map容器的toMap()收集器形式(四参数)。如果需要特定的 Map 实现(如 TreeMap、LinkedHashMap),可以进行容器定制。***/
     toMap(Function, Function, BinaryOperator, Supplier)

下面是这三种重载形式的在Java核心库中定义的源代码:

public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
	    Function<? super T, ? extends U> valueMapper) 
{	//两个参数toMap()重载形式
	return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}
	    
public static <T, K, U>  Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
	    Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction) 
{	//叁个参数toMap()重载形式
	 return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}

public static <T, K, U, M extends Map<K, U>> Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
	    Function<? super T, ? extends U> valueMapper,
	    BinaryOperator<U> mergeFunction,Supplier<M> mapSupplier) 
{ 	//四个参数toMap()重载形式
	BiConsumer<M, T> accumulator = (map, element) -> map.merge(keyMapper.apply(element),
	            valueMapper.apply(element), mergeFunction);
	return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}	//tomap()重载方法,Java核心库中定义的源代码结束。

说明:收集器 Collectors.toMap() 把处理应用于流中的每个元素上,从而在映射表map中生成一个键/值项。第一个keyMapper参数用于从流中的元素提取映射键key;valueMapper参数用于提取与对应的键相关联的值value;第三个参数mergeFunction是合并函数,此函数提供key冲突时的合并策略;第四个参数mapSupplier是 map 容器供给器,它用于表示自定义的map容器(默认map容器是HashMap)。

先看 两个参数的toMap()示例:

	  Map<String, Integer> nameToAgeMap = personList . stream()
          .collect(Collectors.toMap(Person::getName, Person::getAge)); 

当值(value)为流Stream的元素值时valueMapper可以用”e->e“表示,亦可用Function.identity()表示。下面这两种写法是等价的:

	Map<String, Integer> sMap = Stream.of("World", "me", "you")
		.collect(toMap(String::length, Function.identity()));
	Map<String, Integer> sMap = Stream.of("World", "me", "you").collect(toMap(String::length, s->s));

再来看一些应用示例:

List<Employee> employees = Arrays.asList(
    new Employee(1, "张永昌", "人力资源部"),
    new Employee(2, "钱奋斗", "IT科技部"),
    new Employee(3, "李明浩", "产品销售部"),
    new Employee(4, "王振华", "产品销售部"),
    new Employee(5, "戚永明", "IT科技部")
);

// 将员工列表转换为 ID->Name 的映射
Map<Integer, String> idToName = employees.stream()
    .collect(Collectors.toMap(
        Employee::getId,
        Employee::getName
    ));

// 按部门分组,值为部门员工数
Map<String, Long> deptCount = employees.stream()
    .collect(Collectors.toMap(
        Employee::getDepartment,
        e -> 1L,
        Long::sum
    ));

// 按部门分组,值为员工姓名列表
Map<String, List<String>> deptToNames = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.mapping(Employee::getName, Collectors.toList())
    ));

Collectors.toMap()方法出现重复key(主键)冲突时的处理策略:
两个参数的toMap(),默认情况下,映射表当多个键/值项有相同键key时,将会产生冲突,收集器就会抛出IllegalStateException异常。
另外两种toMap()重载形式,都根据合并函数mergeFunction作为重复key冲突的处理策略。

使用Collectors.toMap()时,默认情况下,当两个元素产生相同的键(重复的key)时,会抛出一个IllegalStateException异常。例如:

	Map<Integer, String> sMap = Stream.of("World", "me", "you","Hello")
			.collect(toMap( String::length,Function.identity() ));

上面的代码,两个字符串的key=5有冲突,就会抛出异常如下图:
在这里插入图片描述

为了避免这个问题,我们可以根据合并策略编写mergeFunction(合并函数)来合并相同键的值,例如,下面的合并策略是用新值替换旧值,旧值被丢弃:

	BinaryOperator<String> mergeFun= (o, n)->n;//o表示旧值,n表示新值
	Map<Integer, String> sMap = Stream.of("World", "me", "you","Hello")
		.collect(toMap( String::length,Function.identity(),mergeFun ));

请看一个完整的例程:电话簿的合并策略例程CollectWithToMap
【例程10-28】电话簿的合并策略例程CollectWithToMap
这个toMap()的演示例程包含两部分:还包括自定义的Clerk类

/*** 例程中用到的自定义类Clerk***/
package test;
public class Clerk {
	private String name;
	private String telephone;
	private int age;
	public Clerk(String name,int age,String phone) {
		this.name = name;
		this.age = age;
		telephone = phone;
	}
	public String getName() { return name; }
	public String getTelephone() { return telephone; }
	public int getAge() { return age; }	
}	

例程主程序:

/***电话簿的合并策略例程CollectWithToMap***/
package test;
import static java.util.stream.Collectors.toMap;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Stream;
/***
 * @author QiuGen
 * @description 电话簿的合并策略例程CollectWithToMap
 * @date 2024/8/16
 * ***/
public class CollectWithToMap {
	public static void main(String[] args) {
		List<Clerk> clerks = Arrays.asList(
				new Clerk("张明", 32, "13207540001"),
				new Clerk("Mary", 16, "13777943886"),
				new Clerk("Bob", 18,"13507542156"),
				new Clerk("张明", 18, "13777943886"),
				new Clerk("Bob", 18,"15307092679"));
		
		BinaryOperator<String> mergeFun= (o, n)->n;//o表示旧值,n表示新值
		Map<Integer, String> map = Stream.of("World", "me", "you","Hello")
			.collect(toMap(String::length,Function.identity(),mergeFun ));
		map.forEach((k,v)->System.out.println(v)); //打印map
		mergeFun= (s1, s2)-> s1+","+s2; //电话号码簿的合并策略
		Map<String, String> phoneBook = clerks.stream()
			.collect(toMap(Clerk::getName,Clerk::getTelephone,mergeFun));
		for (String name : phoneBook.keySet()) { //换一种展示方式
			System.out.println("键="+name+"\t值:"+phoneBook.get(name));
		}
		/*** 会抛出IllegalStateException异常
		Map<String, Clerk> clerksMap = clerks.stream()
			.collect(toMap(Clerk::getName, Function.identity()));
		***/
		System.out.println("-------toTreeMap()----------");
		Map<String, Clerk> clerksMap = clerks.stream()
			.collect(toMap(Clerk::getName,e->e, (v1,v2)->v2 , TreeMap::new));
		clerksMap.forEach((k,v)->System.out.println(v));
	}
}

并发型的映射表toConcurrentMap()
收集器Collectors.toMap()有个兄弟Collectors.toConcurrentMap(),也有三种重载形式:
toConcurrentMap(Function, Function)
toConcurrentMap(Function, Function, BinaryOperator)
toConcurrentMap(Function, Function, BinaryOperator, Supplier)

收集器Collector.toMap()和Collector.toConcurrentMap()两者的区别:
Collector.toMap()是普通的Map,默认是HashMap,是线程不安全的,适用于顺序流。
Collector.toConcurrentMap()是并发型的Map,默认是ConcurrentHashMap,是线程安全的,适用于并行流。

  • 收集器Collectors.collectingAndThen()用法
    收集器collectingAndThen()在有些场景非常有用,它可在收集操作后再执行另一个函数,对收集器结果再次进行转换处理。它的原型如下:
	static<T,A,R,RR> Collector<T,A,RR> collectingAndThen(Collector<T,A,R> downstream, Function<R,RR> finisher)

collectingAndThen()方法接收两个参数:
第一个参数Collector:是收集器,用于收集流中的元素。
第二个参数Function:这是对收集结果进行转换处理的函数。

请看一个应用示例,其功能是对流中的元素进行去重和排序,最终返回一个列表。版本一使用收集器collectingAndThen()实现;版本二亦实现相同功能,但版本一有更好的效率,尤其当数据量很大时。

	List<Integer> list = Arrays.asList(5, 9, 4, 3, 7, 4);
	List<Integer> result = list.stream()   //版本一
		.collect(collectingAndThen(toCollection(() -> new TreeSet<>()),ArrayList::new));
	System.out.println(result); // 输出 [3, 4, 5, 7, 9]

	List<Integer> rtn = list.stream().distinct().sorted().collect(toList());  //版本二
	System.out.println(rtn); // 输出 [3, 4, 5, 7, 9]

二、三参数 collect() 收集方法的用法

以上主要介绍的是单参数的收集操作collect()方法的用法,下面来看三参数的收集操作collect()方法的用法。
collect()比reduce()更具通用性,对于三参数的收集操作collect()方法重载形式:
collect(Supplier,BiConsumer,BiConsumer)
叁个参数的collect()方法,其方法签名是:

	<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T>  accumulator, BiConsumer<R, R> combiner)   

叁参数collect()方法的三个参数说明如下:

  • 第一个参数 supplier 是供给器,为累加器提供初始化的容器,它可构造一个指定类型的容器。它可以是收集器的构造器参数,例如:ArrayList::new,BitSet::new
  • 第二个参数BiConsumer类型的accumulator是一个累加器,它可把元素累加起来,或将元素添加到已构建的容器里;
  • 第三个参数BiConsumer类型的combiner是组合器,对于并行流它可将并行处理的多个子流的累加器结果组合起来。如果是串行顺序流,就用不到combiner。

**叁参数collect()方法与收集器Collector的关系:**三参数 collect() 实际上相当于自己定制实现一个 Collector。

请看一个与单参数collect()对比的示例:

 	// 使用单参数collect(),其与下面的三参数collect()是等效形式
    List<String> list1 = stream.collect(Collectors.toList());
    // 使用三参数collect()
    List<String> list2 = stream.collect( ArrayList::new, ArrayList::add, ArrayList::addAll );

示例一,将流中所有字符串元素,收集到字符串的数组列表中,两个版本的写法:

	//Lambda版本
	List<String> lst = Stream.of("Bob","John","赵云")
		.collect(()->new ArrayList<>(), (list, s)->list.add(s), (lst1, lst2)->lst1.addAll(lst2));
	lst.forEach(System.out::println);
	//方法引用版本
	List<String> list = Stream.of("Bob","John","赵云")
		.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
	list.forEach(System.out::println);

示例二,将流中所有字符串联接为一个长字符串:

	String merged = Stream.of("World", "me", "you")
		.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();

示例三:字符串与流之间的转换,将 String 转为流有两种方法,分别是java.lang.CharSequence 接口定义的默认方法 chars() 和 codePoints() ,本例使用方法 chars()来演示。

	String str = "江山如此多娇!".chars()    //转换成流
        .collect(StringBuffer::new,StringBuffer::appendCodePoint,StringBuffer::append)
		.toString();  //将流转换为字符串

与叁参数的约简操作reduce() 有些相似,都适用于并行流。
三参数的收集操作collect()方法比reduce()方法更通用。我们以位集的合并为例进行说明,位集BitSet是线程不安全的,一般不能用于多线程程序中。如果要在并行流中收集位集BitSet结果,用reduce操作就不合适,因为reduce操作只允许提供一个初始值,在并行流中使用线程不安全的BitSet,也会有更新丢失,每次运算可能会有不同的结果。这种情形可使用三参数的collect()方法。
下面的代码演示并行流中用三个参数collect()合并位集BitSet:

	List<Integer> indexs = Arrays.asList(5,8,9,17,26,35,48); 
	BitSet bitSet = indexs.parallelStream().collect(BitSet::new,BitSet::set,BitSet::or);

【例程10-29】并行流用三参数collect合并位集BitSet例程CollectMergeBitSet
并行流合并位集BitSet的“CollectMergeBitSet.java”例程源代码:

package stream;		//例程CollectMergeBitSet.java源码开始:
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;
/***
 * @author QiuGen
 * @description  并行流三参数collect合并位集BitSet例程CollectMergeBitSet
 * @date 2024/8/26
 * ***/
public class CollectMergeBitSet {
	public static void PrintBitSet(BitSet bSet) { /***打印比特图***/
		System.out.println("BitSet: " + bSet);
		StringBuilder bits = new StringBuilder();
		for(int i = 0; i< bSet.size() ; i++)
			bits.append( bSet.get(i)  ?  '1'  :  '0' );
		System.out.println("比特位向量图: " + bits); 
		System.out.print("BitSet Size: " + bSet.size());
		System.out.println("\t BitSet lenght(): " +bSet.length());
	}			//PrintBitset()方法源码结束。
	
	public static void main(String[] args) {
		/***使用并行流、位集BitSet演示三个参数的collect()***/
		List<Integer> indexs = Arrays.asList(5,8,9,17,26,35,48); 
		BitSet bitSet = indexs.parallelStream().collect(BitSet::new,BitSet::set,BitSet::or);
		PrintBitSet(bitSet);
	}
}	//例程CollectMergeBitSet

测试效果图:
在这里插入图片描述

【例程10-30】透视并行流三参数collect收集到toList例程ParallelCollectToList
为了更好地理解三参数的collect()收集操作工作原理,请自行调试此例程:

package stream;  //例程ParallelCollectToList.java源码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ParallelCollectToList {
	static final String[] strs = {"Bob","John","Mary","Sophia","Emily","Peter"};
	public static void main(String[] args) {
        System.out.println("并行流:打印toList()详细调试信息: " );
        List<String> strList = Arrays.stream(strs).parallel().collect(
                () -> {
                    ArrayList<String> arrayList = new ArrayList<>();
                    System.out.println("创建list, size: " + arrayList.size());
                    return arrayList;
                },
                (list, item) -> {
                    list.add(item);
                    System.out.println("list.add,size: " + list.size());
                },
                (lstA, lstB) -> {
                    System.out.println("合并前lstA,size: " + lstA.size());
                    System.out.println("合并前lstB,size: " + lstB.size());
                    lstA.addAll(lstB);
                    System.out.println("合并后lstA,size: " + lstA.size());
                    System.out.println("合并后lstB,size: " + lstB.size());
                }
        );
        strList.forEach(System.out::println);
	}
}	//例程ParallelCollectToList.java源码结束。

并行流的Collect收集操作的测试结果比较混乱,合并时需要用到第三个参数combiner组合器。
如果删除代码“.parallel()”可作为串行流进行测试,测试结果比较清晰,可以观察到,串行流的收集Collect操作用不到combiner组合器。