一、收集操作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组合器。