Effective java 总结6-Lambda和Stream

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::parseIntstr -> Integer.parseInt(str)
有限制Instant.now()::isAfterInstant then = Instant.now(); t->then.isAfter(t);
无限制String::toLowerCasestr -> str.toLowerCase()
类构造器TreeMap<K,V>::new() -> new TreeMap< K, V>
数组构造器int[]::newlen -> 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提供了大量标准的函数接口

接口函数签名范例
UnaryOperatorT apply(T t)String::toLowerCase
BinaryOperatorT apply(T t1, T t2)BigInteger:: add
Predicateboolean test(T t)Collection:: isEmpty
Function<T, R>R apply(T t)Arrays:: asList
SupplierT get()Instant:: now
Consumervoid accept(T t)System.out:: println
  • Operator接口代表结果与参数类型一致的函数

  • Predicate接口代表有一个参数并返回一个boolean的函数

  • Function接口代表其参数与返回类型不一致的函数

  • Supplier接口没有参数并且返回(提供)一个值的函数

  • Consumer接口代表一个函数不返回任何值,消费掉了参数的函数

现有的大多数标准接口都只支持基本类型,千万不用带包装类型的基础函数接口来代替基本函数接口, 包装类的批量操作可能带来致命的性能问题

什么时候使用自己编写的函数接口? eg: Comparator

始终使用@FunctionalInterface 标注自己编写的函数接口

  1. 通用,且受益于描述性名称
  2. 具有与其关联的严格契约
  3. 受益于定制的缺省方法

第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();
}

总结:尽量不要使用并行,除非能保证在计算正确的情况下提高性能


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值