Effective java 总结6 - Lambda和Stream
第42条 Lambda优于匿名类
带有单个抽象方法的接口作为函数类型,其实例称为函数对象
// 匿名类 Comparator 代表排序的抽象策略
Collection.sort(words, new Comparator<String>{
public int compare(String s1, String s2){
return Integer.compare(s1.length(), s2.length());
}
});
// Lambda
Collection.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length());
* // 编译器利用一个称作类型推导的过程,根据上下文推断参数类型,返回值等
* // 建议删除所有Lambda的参数类型,除非它们的存在能使程序更清晰
// 比较器构造方法
Collection.sort(words, comparingInt(String::length));
// List接口中sort方法
words.sort(comparingInt(String::length));
特定于常量的方法实现可以使用lambda(第34条)
public enum Operation{
PLUS("+", (x, y) -> x + y),
MINUS("-", (x, y) -> x - y),
TIMES("*", (x, y) -> x * y),
DIVIDE("/", (x, y) -> x / y);
private final String symbol;
private final DoubleBinaryOperator dbo;
Operation(String symbol, DoubleBinaryOperator dbo){
this.symbol = symbol;
this.dbo = dbo;
}
public double applyAsDouble(double x, double y) {
return dbo.applyAsDouble(x,y);
}
}
几点注意
- 与方法和类不同的是,Lambda没有文档,如果一个计算本身不是自描述的,或者超出几行,不使用Lambda
- 对于lambda,一行是合理的,三行是合理的最大极限
- 如果枚举类型带有难以理解的特定于常量的行为,或者无法在几行内实现,或者需要访问实例域或方法,那么特定于常量的类主体仍然是首选
- 如果想创建抽象类的实例,可以用匿名类完成,lambda不行
- Lambda无法获得对自身的引用,在Lambda中,this指的是外围实例,匿名类中,this指的是匿名类实例,如果需要从函数对象的主题内部访问,必须使用匿名类
- 尽可能不要序列化一个Lambda
- Lambda是表示小函数对象的最佳方式
第43条 方法引用优于Lambda
与匿名类相比, Lambda的优势在于简洁,java提供了更加简洁的函数对象的方法:方法引用
map.merge(key, 1, (count, incr) -> count + incr); // Lambda
map.merge(key, 1, Integer::sum) // 方法引用
方法引用类型 | 范例 | Lambda等式 |
---|---|---|
静态 | Integer::parseInt | str -> Integer.parseInt(str) |
有限制 | Instant.now()::isAfter | Instant then = Instant.now(); t->then.isAfter(t); |
无限制 | String::toLowerCase | str -> str.toLowerCase() |
类构造器 | TreeMap<K,V>::new | () -> new TreeMap< K, V> |
数组构造器 | int[]::new | len -> new int[len] |
- 有限制的引用中,接受对象是在方法引用中指定的,类似于静态引用:函数对象与被引用方法带有相同的参数
- 无限制的引用中,接受对象是在运用函数对象时,通过在该方法的声明函数前添加一个参数来指定, 常用于流管道中作为映射和过滤函数
- 两种构造器都是充当工厂对象
Lambda和方法引用谁更简洁,可读,就用谁
第44条 坚持使用标准的函数接口
模板方法中,用子类覆盖基本类型方法,来限定超类的行为,现在的替代方法是: 提供一个接受函数对象的静态工厂或者构造器。
以函数对象作为参数,需要谨慎选择正确的函数参数类型
只要标准的函数接口能够满足需求,优先考虑其使用,非构建新的函数接口
@FunctionalInterface
public interface EldestEntryRemoveFunction<K, V> {
boolean remove(Map<K,V> map, Map.Entry<K,V>, eldest);
}
@FunctionalInterface
public interface BiPredicate<T, U> {...}
// 可以替换为: BiPredicate<Map<K,V>, Map.Entry<K,V>>
java.util.function提供了大量标准的函数接口
接口 | 函数签名 | 范例 |
---|---|---|
UnaryOperator | T apply(T t) | String::toLowerCase |
BinaryOperator | T apply(T t1, T t2) | BigInteger:: add |
Predicate | boolean test(T t) | Collection:: isEmpty |
Function<T, R> | R apply(T t) | Arrays:: asList |
Supplier | T get() | Instant:: now |
Consumer | void accept(T t) | System.out:: println |
-
Operator接口代表结果与参数类型一致的函数
-
Predicate接口代表有一个参数并返回一个boolean的函数
-
Function接口代表其参数与返回类型不一致的函数
-
Supplier接口没有参数并且返回(提供)一个值的函数
-
Consumer接口代表一个函数不返回任何值,消费掉了参数的函数
现有的大多数标准接口都只支持基本类型,千万不用带包装类型的基础函数接口来代替基本函数接口, 包装类的批量操作可能带来致命的性能问题
什么时候使用自己编写的函数接口? eg: Comparator
始终使用@FunctionalInterface 标注自己编写的函数接口
- 通用,且受益于描述性名称
- 具有与其关联的严格契约
- 受益于定制的缺省方法
第45条 谨慎使用Stream
Stream: 代表数据元素有限or无限的顺序
Stream pipeline: 代表元素的多级计算
Stream中的数据元素可以是对象引用 或者 基本数据类型(int, long, double)
一个Stream pipeline中包含: 一个源stream, 0或多个中间操作,一个终止操作
Stream pipeline是 lazy的,直到调用终止操作时才会开始计算,不需要的元素不会被计算;默认情况下,是按顺序运行的,并行需要调用parallel方法创建流(通常不建议
滥用Stream会使程序代码难以读懂和维护
// 正确使用栗子
public class Anagrams{
public static void main(String args[]){
Path dictinary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try(Stream<String> words = Files.lines(dictinary)){
words.collect(groupingBy(word -> alphabetize(word)))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.forEach(System.out::println);
}
}
}
在没有显示类型的情况下,仔细命名lambda参数,对于Stream pipeline的可读性至关重要
避免使用Stream处理char值
// java不支持基本类型的char stream
"hello world".chars().forEach(System.out::println); // 打印一串数字
// 需要强制类型转换
"hello world".chars().forEach(x -> System.out.println((char) x));
使用Stream
-
统一转换元素的序的序列
-
过滤元素的序列
-
利用单个操作合并元素的顺序 (添加,连接,计算极值)
-
将元素放入集合中分组
-
搜索满足条件的元素序列
使用代码块
- 读取修改局部变量(Lambda只能读取final变量,且不能修改任何局部变量)
- 可以从外围方法中return,continue,break,或者抛出方法声明要抛出的受检异常,lambda不行
自己选择使用stream还是迭代
// 笛卡尔积
private static List<Card> newDeck(){
List<Card> result = new ArrayList<>();
for(Suit suit: Suit.values()){
for(Rank rank: Rank.values()){
result.add(new Card(suit, rank));
}
}
return result;
}
// Stream
private static List<Card> newDeck(){
List<Card> result = new ArrayList<>();
return Stream.of(Suit.values())
.flatMap(suit-> Stream.of(Rank.values()).map(rank->new Card(suit, rank)))
.collect(toList());
}
第46条 优先选择Stream中无副作用的函数
传入Stream操作的任何函数对象,无论是中间操作还是终止操作,都应该是无副作用的(不依赖且不更新任何状态
Map<String long> freq = new HashMap<>();
try( Stream<String> words = new Scanner(file).tokens() ){
words.forEach(word->{
freq.merge(word.toLowerCase(), 1L, Long::sum);
});
}
// forEach 操作应该只用于报告Stream计算的结果,而不是执行计算,修改如下
Map<String long> freq;
try( Stream<String> words = new Scanner(file).tokens() ){
freq = words.collect(groupingBy(String::toLowerCase, counting()));
}
Collectors API : 封装缩减对象,一般产生一个集
toList(), toSet(), toCollection(cooolectionFactory) 分别返回一个列表,一个集合和指定的集合类型
静态导入Collectors的所有成员惯例可以提升Stream pipeline的可读性
// 从频率表中提取排名前十的单词列表
List<String> topTen = freq.keyset().stream().
sorted(comparing(freq::get).reversed()).limit(10).collect(toList());
toMap
-
两个参数:toMap(keyMapper, valueMapper)
private static final Map<String, Operation> stringToEnum = Stream.of(values()). collect(toMap(Object::toString, e->e));
-
三个参数: toMap(keyMapper, valueMapper, MergeFunction)
Map<Artist, Album> topHits = albums.collect( toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));
-
四个参数: toMap(keyMapper, valueMapper, MergeFunction, Supplier) 带有映射工厂指定特殊的映射实现:EnumMap or TreeMap
private static final Map<Phase, Map<Phase, Transition>> m = Stream.of(values()).collect(groupingby( t -> t.from, // key () -> new EnumMap<>(Phase.class), // 修改返回值的类型 toMap(t->to, // value // key t->t, // vlaue (x,y)->x, // merge ()->new EnumMap<>(Phase.class)))); // 返回值类型
groupingBy
-
一个参数:groupingBy(Function<? super T, ? extends K> classifier) 分类器
words.collect(groupingBy(word->alphabetize(word)));
-
两个参数:groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream) 指定一个下游收集器(toSet…)
Map<String, long> freq = words.collect(groupingBy(String::toLowerCase, counting()));
-
三个参数: groupingBy(Function<? super T, ? extends K> classifier, Supplier mapFactory, Collector<? super T, A, D> downstream) , 指定一个映射工厂(toMap中四个参数例子中)
最重要的收集器工厂:toSet, toList, toMap, groupingBy, joining…
第47条 Stream要优先用Collection作为返回类型
Collection接口是Iterable的一个子类型,它有一个stream方法,因此提供了迭代和Stream访问。对于公共的,返回序列的方法,collection或者适当的子类型通常是最佳的返回类型
数组也通过Arrays.asList和Stream.of方法提供了简单的迭代和Stream访问
不要在内存中保存巨大的序列,将它作为集合返回即可
实现专用的集合(Collection)
// 幂集 {a,b,c} : {} {a} {b} {c} {ab} {ac} {bc} {abc} 2^N次
public class PowerSet{
public static final <E> Collection<Set<E>> of(Set<E> s){
List<E> src = new ArrayList<>(s);
if(src.size > 30) throw new Illegal...;
return new AbstractList<Set<E>>{
@Override public int size(){
return 1 << s.size;
}
@Override public boolean contains(Object o){
return o instanceof Set && src.containsAll((Set)o);
}
@Override public Set<E> get(int index){
Set<E> result = new HashSet<>();
for ( int i = 0; index !=0; i++, index >>= 1 ){
if( 1 == (index & 1) )
result.add(src.get(i));
}
return result;
}
};
}
}
在AbstractCollection上编写一个Collection实现,除了Iterable必须的方法外,还需要实现 size()、contains()
返回Stream
// 列表的前缀后缀 (a,b,c) : (a) (a,b) (a,b,c) -- (a,b,c) (b,c) (c)
public class SubLists{
public static <E> Stream<List<E>> of(List<E> list){
return Stream.concat(
Stream.of(Collections.emptyList()),
prefixes(list).flatMap(Sublists::suffixes));
}
private static <E> Stream<List<E>> prefixes(List<E> list){
return IntStream.rangeClosed(1, list.size()).
mapToObj(end -> list.subList(0, end));
}
private static <E> Stream<List<E>> suffixes(List<E> list){
return IntStream.range(0, list.size()).
mapToObj(start -> list.subList(start, list.size()));
}
}
编写返回一系列元素的方法时,尽量兼顾迭代,Stream。优先返回标准的集合,不行就返回定制的集合,如果不能返回集合,返回Stream或Iterable
第48条 谨慎使用Stream并行
使用parallel方法即可实现Stream并行,但千万不要任意并行 Stream pipeline, 所有的并行都是在一个通用的fork-join池中运行的,一个pipeline运行异常,可能损害其他不相关部分性能
在Stream上并行获得性能提升,最好通过ArrayList, HashMap, HashSet, ConcurrentHashMap实例,数组,int范围和long范围等,主要原因:
-
可以轻松分成任意大小的子范围(由Stream和Iterable中的spliterator抽象实现)
-
具有优异的引用局部性(序列化的元素引用一起保存在内存中,基本类型数组最佳)
stream pipeline 的终止操作也会影响并行效率,不要将大量的计算工作放在终止操作中,最佳终止操作:做减法(reduce, min, max, count, sum)或骤死式操作(anyMatch, allMatch, noneMatch), 而collect方法执行的操作集合合并成本较高,不适宜并行
并行Stream可能降低性能,可能导致活性失败或导致结果出错,更严重出现不可预计的结果,因此并行Stream是严格的性能优化,需要进行充分的性能测试比对
Stream pipeline有效的例子
// 非并行
static long pi(long n){
return LongStream.rangeClosed(2, n).
mapToObj(BigInteger::valueOf).filter(i -> i.isProbablePrime(50)).count();
}
// 并行
static long pi(long n){
return LongStream.rangeClosed(2, n).parallel().
mapToObj(BigInteger::valueOf).filter(i -> i.isProbablePrime(50)).count();
}
总结:尽量不要使用并行,除非能保证在计算正确的情况下提高性能