Java函数式编程之【Stream终止操作】【下】【二】【收集器toMap()】【叁参数收集操作collect()】
一、收集操作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)); //打印mapmergeFun= (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组合器。