Effective Java-Lambda和Stream笔记

Effective Java Lambda和Stream

Lambda和Stream

java8中,增加了函数接口function interface,Lambda和方法引用method reference,使得创建函数对象function object变得容易,同时,还增加了StreamAPI

42.Lambda优先于匿名类

java8以前函数对象的主要方式是通过匿名类anonymous class(单个抽象方法的接口作为函数类型,它们的实例称作函数对象,表示函数或者要采取的动作)

匿名类满足了传统的面向对象的设计模式对函数对象的需求,即策略模式Strategy

java8形成了"带有单个抽象方法的接口是特殊的,值得特殊对待"的观念,这些接口现在被称作函数接口functional interface,java允许利用Lambda表达式创建这些接口的实例。

Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));

参数类型和返回值类型以及Lambda的类型都没有出现在代码中,编译器利用类型推导type inference的过程,根据上下文推断出这些类型
故而删除所有Lambda参数的类型吧,除非它们的存在能够使程序变得更加清晰
编译器是从泛型获取到得以执行类型推导的大部分类型信息的

Collections.sort(words, comparingInt(String::length));
java8在List接口中添加的sort方法更简短
words.sort(comparingInt(String::length));

利用Lambda优化34条的Operation枚举类型

给每个枚举常量的构造器传递一个实现其行为的Lambda,构造器将Lambda保存在一个实例域中,apply方法再将调用转给Lambda
public enum Operation {
	PLUS("+", (x, y) -> x + y),
	...;
	
	private final String symbol;
	private final DoubleBinaryOperator op;
	
	Operation(String symbol, DoubleBinaryOperator op) {
		this.symbol = symbol;
		this.op = op;
	}
	
	public double apply(double x, double y) {
		return op.applyAsDouble(x, y);
	}
}
DoubleBinaryOperator接口表示带有两个double参数的函数,并返回一个double结果
注意:传入枚举构造器的参数是在静态的环境中计算的,因而枚举构造器中的Lambda无法访问枚举的实例成员

Lambda没有名称和文档;如果一个计算本身不是自描述的,或者超出了几行,那就不要把它放在一个Lambda中。对于Lambda而言,一行是最理想的,三行是合理的极限
Lambda无法获得对自身的引用,Lambda中this是指外围实例而匿名类中,this指匿名类实例

尽可能不要序列化一个Lambda或者匿名类实例,若想要可序列化的函数对象,如Comparator,就使用私有静态嵌套类的实例

千万不要给函数对象使用匿名类,除非必须创建非函数接口的类型的实例

43.方法引用优先于Lambda

java提供了生成比Lambda更简洁函数对象的方法:方法引用method reference

以下,当这个键不在映射中时,将数字1和键关联起来;或者当这个键已经存在,就负责递增该关联值:
map.merge(key, 1, (count, incr) -> count + incr);      
merge方法,是java8 Map接口中添加的,若指定的key没有映射,该方法就会插入指定值;若映射存在,merge方法就会将指定函数应用到当前值count和指定值incr上,并用结果覆盖当前值

java8 Integer(以及所有其他的数字化基本包装类型)提供了sum的静态方法

map.merge(key, 1, Integer::sum);
更为简洁

若Lambda太长,或者过于复杂,还有另一种选择:从Lambda中提取代码,放到一个新的方法中,并用该方法的一个引用代替lambda

许多方法引用都指向静态方法,但其中4种没有这么做。其中两个是有限制bound和无限制unbound的实例方法引用。在有限制的引用中,接收对象是在方法引用中指定的。有限制的引用本质上类似于静态引用:函数对象与被引用方法带有相同的参数。在无限制的引用中,接收对象是在运用函数对象时,通过在该方法的声明函数前面额外添加一个参数来指定的。无限制的引用经常用在流管道Stream pipeline中作为映射和过滤函数。最后还有两种构造器引用,分类针对类和数组。构造器引用充当工厂对象

方法引用类型范例Lambda等式
静态Integer::parseIntstr -> Integer.parseInt(str)
有限制Instant.now()::isAfterInstant t = new Instant.now(); i->t.isAfter(i)
无限制String::toLowerCasestr -> str.toLowerCase
类构造器TreeMap<K, V> :: new()->new TreeMap<K, V>
数组构造器int[]::newlen -> new int[len]

只要方法引用更加简洁,清晰,就用方法引用;若方法引用并不简洁,就坚持使用Lambda

44.坚持使用标准的函数接口

函数接口取代模板方法模式

只要标准的函数接口能够满足需求,通常应该优先考虑,而不是专门再构建一个新的函数接口
许多标准的函数接口都提供了有用的默认方法,如Predicate接口提供了合并断言的方法

函数接口有43个接口,其中有6个基础接口,可以推断出其余接口。基础接口作用于对象引用类型。

Operator接口代表其结果与参数类型一致的函数
Predicate接口代表带有一个参数并返回一个boolean的函数
Function接口代表其参数与返回类型不一致的函数
Supplier接口代表没有参数并且返回或提供一个值的函数
Consumer代表的是带有一个函数但不返回任何值的函数,消费
接口函数签名范例
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)Arrays::asList
SupplierT get()Instant::now
Consumervoid accept(T t)System.out::println

如今大多数标准函数接口都只支持基本类型,千万不要用带包装类型的基础函数接口来代替基本函数接口

若你所需要的函数接口与Comparator一样具有一项或者多项以下特征,则需要考虑自己编写专用的函数接口,而不是使用标准的函数接口:

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

@FunctionalInterface注解标注自定义函数接口
必须始终用@FunctionalInterface注解对自己编写的函数接口进行标注

45.谨慎使用Stream

java8的Stream API,简化了串行或并行的大批量操作。这个API提供了两个关键抽象:Stream代表数据元素有限或无限的顺序,Stream pipeline流管道则代表这些元素的一个多级计算。Stream中的元素可能来自任何位置。常见的来源包括集合,数组,文件,正则表达式模式匹配器,伪随机数生成器,以及其他Stream。Stream中的数据元素可以是对象引用,或者基本类型值。它支持三种基本类型:int,long,double

一个Stream pipeline中包含一个源Stream,接着是0或多个中间操作intermediate operation和一个终止操作terminal operation。每个中间操作都会对Stream进行转换,从一个Stream转换成另一个Stream,其元素类型可能与输入的Stream一样,也可能不一样(filter,map中间操作)。终止操作会在最后一个中间操作产生的Stream上执行一个最终的运算(foreach,collect,findFirst)

Stream pipeline通常是lazy懒加载:直到调用终止操作时才会开始计算,对于完成终止操作不需要的数据元素,将永远都不会被计算,无限Stream成为可能。注意,没有终止操作的Stream pipeline将是一个静默的无操作指令,故而不要忘记终止操作。

Stream API是流式的fluent:所有包含pipeline的调用可以链接成一个表达式。
默认情况下,Stream pipeline是按顺序运行的。要使pipeline并发执行,只需在该pipeline的任何Stream上调用parallel方法即可,通常不建议这样做(48条)

java8 Map新增方法compute,computeIfAbsent,computeIfPresent

computeIfAbsent:在映射中查找键,存在返回关联值,不存在对该键运用指定的函数对象算出一个值,将这个值与键关联起来并返回计算得到的值,此方法简化了将多个值与每个键关联起来的映射实现

滥用Sream会使程序代码难以读懂和维护

public class Anagrams {
    public static void main(String[] args) {
        File dictionary = new File(args[0]);
        int minGroupSize = Integer.parseInt(args[1]);

        Map<String, Set<String>> groups = new HashMap<>();
        try (Scanner s = new Scanner(dictionary)) {
            while (s.hasNext()) {
                String word = s.next();
                groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>()).add(word);
            }
        } catch (Exception e) {

        }
    }

    private static String alphabetize(String s) {
        char[] a = s.toCharArray();
        Arrays.sort(a);
        return new String(a);
    }
}

不好的示例:

public class Anagrams {
    public static void main(String[] args) {
        Path dictionary = Paths.get(args[0]);
        int minGroupSize = Integer.parseInt(args[1]);

        try (Stream<String> words = Files.lines(dictionary)) {
            words.collect(
                    Collectors.groupingBy(word -> word.chars().sorted()
                            .collect(StringBuilder::new,
                                    (sb, c) -> sb.append((char) c),
                                    StringBuilder::append).toString()))
                    .values().stream()
                    .filter(group -> group.size() >= minGroupSize)
                    .map(group -> group.size() + ":" + group)
                    .forEach(System.out::println);
        } catch (Exception e) {

        }
    }
}

好的示例:

public class Anagrams {
    public static void main(String[] args) {
        Path dictionary = Paths.get(args[0]);
        int minGroupSize = Integer.parseInt(args[1]);
        
        try (Stream<String> words = Files.lines(dictionary)) {
            words.collect(Collectors.groupingBy(word -> alphabetize(word)))
                    .values().stream()
                    .filter(group -> group.size() >= minGroupSize)
                    .forEach(g -> System.out.println(g.size() + ":" + g));
        } catch (Exception e) {
            
        }    
    }

    private static String alphabetize(String s) {
        char[] a = s.toCharArray();
        Arrays.sort(a);
        return new String(a);
    }
在try-with-resources中打开词典文件,获得一个包含了文件中所有代码的Stream。Stream变量命名为words,该Stream中的每个元素均为单词。这个Stream中的pipeline没有中间操作;它的终止操作将所有的单词集合到一个映射中,按照它们的字母排序形式对单词进行分组。随后在映射的values视图中打开一个新的Stream<List<String>>.Stream过滤将所有单词长度小于minGroupSize的单词都去掉了

在没有显式类型的情况下,仔细命名Lambda参数,这对于Stream pipeline的可读性至关重要
在Stream pipeline中使用helper方法,对于可读性而言,比在迭代化代码中使用更为重要
最好避免利用Stream来处理char值,因为Java不支持基本类型的char Stream,char会自动转换为int基本类型,除非强制转换为char

重构现有代码来使用Stream,并且只在必要的时候才在新代码中使用
以下工作只能通过代码块,而不能通过函数对象来完成:

  • 从代码块中,可以读取或者修改范围内的任意局部变量;Lambda中只能读取final或者有效地final变量,且不能修改任何local变量
  • 从代码块中,可以从外围方法中return, break或continue外围循环,或者抛出该方法声明要抛出的任何受检异常

Stream使用场景:

  • 统一转换元素的序列
  • 过滤元素的序列
  • 利用单个操作如添加,连接或计算其最小值 合并元素的顺序
  • 将元素的序列存放到一个集合中,比如分组
  • 搜索满足某些条件的元素的序列

若不确定用Stream还是迭代,那么就两种都试试,选择好理解,简洁的

46.优先选择Stream中无副作用的函数

forEach是显式迭代,不适合并行
forEach操作应该只用于报告Stream计算的结果,而不是执行计算

静态导入Collectors的所有成员是惯例也是明智的,因为这样可以提升Stream pipeline的可读性

import static java.util.stream.Collectors.*;

Collectors中的大部分方法是为了便于将Stream集合到映射中而存在,每个Stream元素都有一个关联的键和值,多个Stream元素可以关联同一个键。
最简单的映射收集器是toMap(keyMapper, valueMapper),它带有两个函数,其中一个是将Stream元素映射到键,另一个是将它映射到值。

private static final Map<String, Operation> stringToEnum = Stream.of(values()).collect(
	toMap(Object::toString, e -> e));

此时多个Stream元素映射到同一个键,pipeline会抛出IllegalStateException异常并终止

比toMap更复杂的是groupingBy方法,提供更多处理此类冲突的策略,如给toMap提供键,值,合并函数即BinaryOperator,V为映射的值类型。

Map<Artist, Album> topHits = albums.collect(
	toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));

以上比较器使用了静态工厂方法maxBy,从BinaryOperator静态导入的,该方法将Comparator转换成一个BinaryOperator,用于计算指定比较器产生的最大值。
三个参数的toMap还有一种用途,即生成一个收集器,有冲突时保留最后更新的

toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)

toMap的最后一种形式是带有第四个参数,映射工厂,使用时要指定特殊的映射实现,如EnumMap或TreeMap

toMap的前三种还有另外的变换形式,命名为toConcurrentMap,能并行运行,并生成ConcurrentHashMap实例

除了toMap方法,Collectors还提供了groupingBy方法,根据分类函数将元素分组,分类函数带有一个元素,并返回所属的类别。这个类别就是元素的映射键。

words.collect(groupingBy(word -> alphabetize(word)));
键为alphabetize(word),值为包含word元素的List集合

两个参数的groupingBy,第二个参数为指定的一个下游收集器downstream collector

下游收集器,指定返回的集合类型,toSet()或toCollection(collectionFactory)允许创建存放各元素类别的集合 如toCollection(HashSet::new))
下游收集器,指定返回的值而不是列表的映射如下
Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting()));

groupingBy的第三个版本:
第二个参数指定映射工厂mapFactory
第三个参数指定下游收集器

注意:groupingByConcurrent方法提供了groupingBy所有三种重载的变体,可并发运行生成ConcurrentHashMap实例

通过在Stream上的count方法可以替代collect(counting()),这个属性还有15种Collectors方法。其中包含9种方法其名称以summing,averaging和summarizing开头(相应的Stream基本类型上就有相同的功能)。它们还包括reducing,filtering,mapping,flatMapping和collectingAndThen方法。
Collectors的joining方法,只在CharSequence实例的Stream中操作,如字符串。(可被String.join()替代),它以参数的形式,返回一个简单地合并元素的收集器。其中一种参数形式带有一个delimiter分解符的CharSequence参数,它返回一个连接Stream元素并在相邻元素之间插入分隔符的收集器。注意分隔符和字符串本身有的字符歧义。三个参数的joining除了分隔符delimiter还有前缀prefix,后缀suffix。

    public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
                                              CharSequence prefix,
                                               CharSequence suffix)

最重要的收集器工厂是toList,toSet,toMap,groupingBy,joining

47.Stream要优先用Collection作为返回类型

java8新增的Stream本质上导致给序列化返回的方法选择适当返回类型的任务变得复杂。
Stream并没有淘汰迭代:要编写优秀的代码必须巧妙地将Stream与迭代结合起来使用

若一个API只返回Stream,则无法直接用for-each循环遍历。因为Stream接口只在Iterable接口中包含了唯一一个抽象方法,Stream对于该方法的规范也适用于Iterable的。

for (ProcessHandle ph : ProcessHandle.addProcesses()::iterator) {
	statement
}
以上代码编译器报错
以下不报错
// 为了使代码能够进行编译,必须将方法引用转换成适当参数化的Iterable
for (ProcessHandle ph : (Iterable<ProcessHandle>) ProcessHandle.addProcesses()::iterator) {
	statement
}

以上代码使用适配器方法进行优化:

在适配器方法中没有必要进行转换,因为java的类型引用在这里派上了用场
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
	return stream::iterator;
}
利用以上适配器可以利用for-each遍历任何Stream:
for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) {
	statement
}

反过来,想要利用Stream pipeline处理序列,面对Iterable也需要适配器

public static <E> Stream<E> streamOf(Iterable<E> iterable) {
	// Spliterator用于并行遍历元素
	// public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) 是否并行
	return StreamSupport.stream(iterable.spliterator(), false);
}

而对于想要编写Stream pipeline和Iterable的人来说,应该考虑返回Collection接口
Collection接口是Iterable的一个子类型,它有一个stream方法,因此提供了迭代和stream访问。
对于公共的,返回序列的方法,Collection或者适当的子类型通常是最佳的返回类型
数组也通过Arrays.asList和Stream.of方法提供了简单地迭代和stream访问
千万别在内存中保存巨大的序列,将它作为集合返回即可

Collection相比于Stream或Iterable作为返回类型的缺点:Collection有一个返回int类型的size方法,它限制返回的序列长度为Integer.MAX_VALUE或者2e31 -1 。

48.谨慎使用Stream并行

stream并行即调用parallel方法,要考虑安全性和活性失败

import static java.math.BigInteger.*;
public static void main(String[] args) {
    final BigInteger TWO = new BigInteger("2");
    // 返回2的幂为素数 然后减1  且是素数的前20条数据
	primes.map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
		.filter(mersenne -> mersenne.isProbablePrime(50))
		.limit(20)
		.forEach(System.out::println);
}
static Stream<BigInteger> primes() {
	// 根据函数迭代产生初始为TWO,无限素数组成的stream流
	return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}

对以上stream调用parallel方法导致活性失败,程序无法打印
即便是最佳环境下,若调用parallel的Stream源头是来自Stream.iterate,或者使用了中间操作的limit,那么并行pipeline也不可能提升性能,更糟糕的是默认的并行策略在处理limit的不可预知性。
千万不要任意地并行Stream pipeline 其结果可能是灾难性的
**在Stream上通过并行获得的性能,最好是通过ArrayList,HashMap,HashSet和ConcurrentHashMap实例,数组,int范围和long范围等。**这些数据结构的共性是,都可以被精确,轻松地分成任意大小的子范围,使并行线程中的分工变得更加轻松。
Stream类库用来执行这个任务的抽象是分割迭代器spliterator,它是由Stream和Iterable中的spliterator方法返回的。
这些数据结构共有的另一项重要特性是,在进行顺序处理时,它们提供了优异的引用局部性locality shof reference:序列化的元素引用一起保存在内存中。而那些对象是分散保存在内存中,而基本类型数组相邻地保存在内存中

Stream pipeline的终止操作本质上也影响了并发执行的效率(大量的工作在终止操作符完成而不是在pipeline中完成,则并行受到限制)。并发的最佳终止操作是做减法reduction,用一个stream的reduce方法,将所有从pipeline产生的元素都合并在一起,或者预先打包像min,max,count,sum这类方法。驟死式操作如anyMatch,allMatch,和noneMatch也都可以并行。由Stream的collect方法执行的操作,都是可变的减法,不是并行的最好选择,因为合并集合的成本高

若是自己编写Stream,Itereable或者Collection实现,且想要得到适当的并行性能,就必须覆盖spliterator方法
并行Stream不仅可能降低性能,包括活性失败,还可能导致结果出错,以及难以预料的行为如安全性失败

程序中所有并行Stream pipeline都是在一个通用的fork-join池中运行的。只要有一个pipeline运行异常,都会损害到系统中其他不相关部分的性能

在适当的条件下,给Stream pipeline添加parallel调用,确实可以在多处理器的情况下实现近乎线性的倍增

parallel有效地例子:

static long pi(long n) {
	return LongStream.rangeClosed(2, n)
		.parallel()
		.mapToObj(BigInteger::valueOf)
		.filter(i -> i.isProbablePrime(50))
		.count();
}

如果要并行一个随机数的Stream,应该从SplittableRandom实例开始,而不是从ThreadLocalRandom或实际上已经过时的Random开始
ThreadLocalRandom只用于单线程,它将自身当做一个并行的Stream源运用到函数中
Random在每个操作都进行同步,扼杀了并行的优势

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值