方式1(基本不用)
众所周知,在JDK8之后,List转Map的一般实现方式如下:
1 2 3 4 5 6 7 8 9 10
| ArrayList<Student> list = new ArrayList<>(3); Student student1 = new Student("张三",18); Student student2 = new Student("李四",18); Student student3 = new Student("王五",20); list.add(student1); list.add(student2); list.add(student3);
Map<String, Integer> map = list.stream() .collect(Collectors.toMap(Student::getName, Student::getAge));
|
方式一存在的问题
Collectors.toMap()
调用的方法如下:
1 2 3 4 5 6 7 8
| public static <T, K, U> Collector<T, ?, Map<K,U>> toMap( Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) { return new CollectorImpl<>(HashMap::new, uniqKeysMapAccumulator(keyMapper, valueMapper), uniqKeysMapMerger(), CH_ID); }
|
在调用CollectorImpl
的构造函数前,使用uniqKeysMapAccumulator(keyMapper, valueMapper)
对数据进行了处理,处理代码如下:
1 2 3 4 5 6 7 8 9 10
| private static <T, K, V> BiConsumer<Map<K, V>, T> uniqKeysMapAccumulator(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends V> valueMapper) { return (map, element) -> { K k = keyMapper.apply(element); V v = Objects.requireNonNull(valueMapper.apply(element)); V u = map.putIfAbsent(k, v); if (u != null) throw duplicateKeyException(k, u, v); }; }
|
Objects.requireNonNull
:对值进行了判断,如果为null则抛出NullPointerException
- 通过
map.putIfAbsent(k, v);
及下面的判断,如果key已经存在,则抛出duplicateKeyException
方式2(有缺陷)
Collectors.toMap()
还有一个重载的方法,如下所示:
1 2 3 4 5 6
| 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) { return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new); }
|
可以看到return这里调用了类中另外一个同名不同参的toMap
方法,如下所示:
1 2 3 4 5 6 7 8 9 10
| 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> mapFactory) { BiConsumer<M, T> accumulator = (map, element) -> map.merge(keyMapper.apply(element), valueMapper.apply(element), mergeFunction); return new CollectorImpl<>(mapFactory, accumulator, mapMerger(mergeFunction), CH_ID); }
|
可以看到,可以通过定义自己的mergeFunction
也就是对应map.merge的第三个remappingFunction参数
的来控制如何累加,优化后如下所示:
1 2 3 4 5 6 7 8 9 10
| ArrayList<Student> list = new ArrayList<>(3); Student student1 = new Student("张三",18); Student student2 = new Student("李四",18); Student student3 = new Student("王五",20); list.add(student1); list.add(student2); list.add(student3);
Map<String, Integer> map = list.stream() .collect(Collectors.toMap(Student::getName, Student::getAge,(v1, v2)->v2));
|
这里,传入了lambda表达式(v1, v2)->v2)
,当key出现重复时,移除原key对应的键值对,放入新key的键值对,map.merge
具体代码实现如下:
方式2存在的问题
在自定义key重复处处理逻辑后,在调用Map的merge方法时,实际上调用了HashMap
的merge方法,在其第一行就是:
1 2
| if (value == null || remappingFunction == null) throw new NullPointerException();
|
当value为null时,抛出NullPointerException
异常
方式2问题一般的处理方式
对null情况进行提前判断,并赋给默认值
1 2 3 4
| Map<String, Integer> map = list.stream() .collect(Collectors.toMap( Student::getName, student -> Optional.ofNullable(student.getAge()).orElse(1) ));
|
方式3(可以使用)
.collect()方法可以自定义自己的累加器容器
、累加器容器元素添加方式
、累加器容器合并
,如下所示:
1 2
| Map<String, Integer> map = list.stream() .collect(HashMap::new,(m,v)->m.put(v.getName(),v.getAge()),HashMap::putAll);
|
速度测试
1 2 3 4 5 6
| ArrayList<Student> list = new ArrayList<>(100000); for(int i = 0 ; i < 5000000 ;i++){ Student student= new Student("测试"+i,i ); list.add(student); }
|
测试方式
直接使用forEach
1 2 3 4 5 6
| Map<String, Integer> map3 = new HashMap<>(list.size()); for(Student student:list){ map3.put(student.getName(),student.getAge()); } Instant now4 = Instant.now(); System.out.println("直接使用forEach:"+Duration.between(now3,now4).toMillis());
|
手动判空
1 2 3 4 5 6 7
| Map<String, Integer> map2 = list.stream() .collect(Collectors.toMap( Student::getName, student -> Optional.ofNullable(student.getAge()).orElse(1) )); Instant now3 = Instant.now(); System.out.println("手动判空:"+Duration.between(now2,now3).toMillis());
|
使用自定义累加器
1 2 3 4 5 6
| Instant now1 = Instant.now(); Map<String, Integer> map = list.stream() .collect(HashMap::new,(m,v)->m.put(v.getName(),v.getAge()),HashMap::putAll); Instant now2 = Instant.now(); System.out.println("使用自定义累加器:"+Duration.between(now1,now2).toMillis());
|
测试结果
直接使用forEach:338
手动判空:591
使用自定义累加器:738
测试结论
- 直接使用forEach是时间最快的,但是需要自己维护实现细节,对性能要求高大批量数据时可以使用
使用自定义累加器
虽然能够避免了空指针及键重复问题,但是创建map和putall的行为仍然会消耗额外时间,但是和手动判空
消耗时间差距不大,数据量较小,对性能要求不高的业务场景下可以使用
附录
.collect
:流的终端操作,接收一个Collector<? super T, A, R> collector
参数,主要的作用是将流中元素汇聚成一个结果。
最终调用了CollectorImpl
类的4参数构造函数,进行了返回,所以对CollectorImpl
类进行分析,CollectorImpl
实现了Collector
接口,接口未如下所示:
1 2 3 4 5 6 7 8 9 10 11
| public interface Collector<T, A, R> { Supplier<A> supplier();
BiConsumer<A, T> accumulator();
BinaryOperator<A> combiner();
Function<A, R> finisher();
Set<Characteristics> characteristics(); }
|
supplier()
:返回一个新的累加器容器
1 2 3 4
| @FunctionalInterface public interface Supplier<T> { T get(); }
|
accumulator()
:将元素添加进累加器容器
1 2 3 4
| @FunctionalInterface public interface BiConsumer<T, U> { void accept(T t, U u); }
|
combiner()
:合并两个累加器容器
1 2 3 4 5 6 7 8 9
| @FunctionalInterface public interface BinaryOperator<T> extends BiFunction<T,T,T> {
} @FunctionalInterface public interface BiFunction<T, U, R> {
R apply(T t, U u); }
|
finisher()
:结束前对累加器容器进行转换,转换成最终结果容器
1 2 3 4
| @FunctionalInterface public interface Function<T, R> { R apply(T t); }
|
characteristics()
:定义combiner()
函数的行为
1 2 3 4 5 6 7 8
| enum Characteristics { CONCURRENT, UNORDERED, IDENTITY_FINISH }
|