我爱编程

Java 8 学习笔记

2017-09-21  本文已影响222人  谢随安

第一章

为什么要关心Java 8

使用Stream库来选择最佳低级执行机制可以避免使用Synchronized(同步)来编写代码,这种代码不仅容易出错,并且在多核CPU上的执行成本也很高。
因为多核CPU每个处理器内核都有独立的高速缓存。加锁需要这些高速缓存同步运行,而这又需要内核间进行缓慢的缓存一致性协议通信

Streams的作用不仅仅是把代码传递给方法,它提供了一种新的间接地表达行为参数化的方法。
比如,对于两个只有几行代码不同的方法,只需要把不同的那部分代码作为参数传递进去就可以。

流处理

流是一系列数据项,一次只生成一项。
基于Unix操作流的思想,Java 8在java.util.stream中添加了一个Stream APIStream API的很多方法可以链接起来形成一个复杂的流水线。
Java 8可以透明的把输入的不相关部分拿到几个CPU内核上去分别执行Stream操作流水线,并不需要Thread

Unix操作流示例:
cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3 将两个文件连接起来创建一个流,tr会转换流中的字符,sort会对流中的行进行排序,而tail -3则给出流的最后三行,这些程序通过管道(|)连接在一起。

用行为参数化把代码传递给方法

Java 8增加了通过API来传递代码的能力,把方法(代码)作为参数传递给另一个方法。

并行与共享的可变数据

要能够同时对不同的输入安全地执行,这意味着写代码不能访问共享的可变数据。虽然可以使用Synchronized来打破“不能有共享的可变数据”的规则,但是同步迫使代码按照顺序执行,这与并行处理相悖。

没有共享的可变数据将方法和函数(即代码)传递给其他方法的能力是函数式编程范式的基石。

“不能有共享的可变数据”的要求意味着,一个方法要通过它将参数值转换为结果的方法完全描述的,就像是一个数学函数,没有可见的副作用。

Java中的函数

Java 可能操作的值: 原始值,对象(严格来说是对象的引用)。其他的结构(二等公民)虽然有助于表示值的结构,但它们程序执行期间并不能传递。程序之间期间能传递的是(一等公民),解决办法是用方法来定义类,通过类的实例化产生值,人们又发现, 通过在运行时传递方法*能够将方法变成值来传递。

让方法等概念作为值(一等公民)会让编程变得容易很多。

方法和Lambda作为一等公民

Java 8的一个新功能是方法引用
要将方法作为值传给另一个方法,只需用方法引用::语法(即“把这个方法作为值”)将其传给另一个方法即可。
这样做的好处是代码读起来更接近问题的陈述,方法生成升为了“一等公民”。

与对象引用传递对象类似(对象引用使用new创建),当写下XXX:MethodName的时候,就创建了一个方法引用,同样也可以传递它。以前的Java版本只能把方法包裹在FileFilter对象里,然后才能传递给别的方法。

除了允许函数成为一等值外,Java 8还体现了更广义的将函数作为值的思想------Lambda

函数式编程风格,即编写把函数作为一等值来传递的程序

传递代码的例子

假设有一个Apple类,它有一个getColor方法,还有一个变量inventory保存着一个Apples的列表。要选出所有的绿苹果并返回一个列表。在Java 8之前,可能会写一个这样的方法:

public static List<Apple> filterGreenApples(List<Apple> inventory){
    List<Apple> result = new ArrayList<>();
    for (Apple apple: inventory){
        //代码会仅仅选出绿色苹果,并加入result中
        if ("green".equals(apple.getColor())) {
            result.add(apple);
        }
    }
    return result;
}

接下来,又想要选出重的苹果,比如超过150克的,于是又写了如下方法:

public static List<Apple> filterGreenApples(List<Apple> inventory){
    List<Apple> result = new ArrayList<>();
    for (Apple apple: inventory){
        //代码会仅仅选出重量大于苹果,并加入result中
        if (apple.equals(apple.getWeight() > 150 ) {
            result.add(apple);
        }
    }
    return result;
}

这两个方法只有一行不同,Java 8由于可以把条件代码作为参数传递进去,这样可以避免filter方法出现重复的代码。于是可以这样写:

    public static boolean isGreenApple(Apple apple) {
        return "green".equals(apple.getColor()); 
    }

    public static boolean isHeavyApple(Apple apple) {
        return apple.getWeight() > 150;
    }

    public static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p){
        List<Apple> result = new ArrayList<>();
        for(Apple apple : inventory){
            if(p.test(apple)){
                result.add(apple);
            }
        }
        return result;
    }       

注意filterApples的参数: Predicate<Apple> p,这里是从java.util.function.predicate导入的,它的作用是定义一个泛型接口:

public interface Predicate<T> {
    boolean test(T t);
}

通过这样,可以将方法通过谓语(Predicate)参数p传递进filterApples。

要使用 filterApples 的话,可以写成这样:

List<Apple> greenApples = filterApples(inventory, FilteringApples::isGreenApple);

或者

 List<Apple> heavyApples = filterApples(inventory, FilteringApples::isHeavyApple);

Predicate(谓语)在数学上常用来代表一个类似函数的东西,它接受一个参数值,并返回true或false。

从传递方法到Lamda

除了把方法作为值来传递以外,Java 8还引入了一套记法(匿名函数或Lambda),于是上面的代码可以写成:

List<Apple> greenApples2 = filterApples(inventory, 
(Apple a) -> "green".equals(a.getColor()));

或者

List<Apple> heavyApples2 = filterApples(inventory, 
(Apple a) -> a.getWeight() > 150);

甚至

List<Apple> weirdApples = filterApples(inventory,
 (Apple a) -> a.getWeight() < 80 || "brown".equals(a.getColor()));

通过这种方法,你甚至不需要为只用一次的方法写定义;代码更干净清晰。

Q:什么时候使用方法作为值传递,什么时候使用Lambda?

A:要是代码的长度多于几行,使用Lambda表示的效果并不是一目了然,这样还是应该使用方法引用来指向一个方法,而不是使用匿名的Lambda。简言之,使用哪种方法应该以代码的清晰度为准绳。

通过Stream和Lamdba表达式,可以将之前的筛选代码改进成如下形式:

import static java.util.stream.Collector.toList;
List<Apple> heavyApples =
  inventory.stream().filter((Apple a) -> a.getWeight() > 150).collect(toList());

并行处理:

import static java.util.stream.Collector.toList;
List<Apple> heavyApples =
  inventory.parallelstream().filter((Apple a) -> a.getWeight() > 150).collect(toList());

和Collection API相比,StreamAPI处理数据的方式非常不同,用集合需要自己去做迭代的过程。你需要用for-each循环一个个去迭代元素、处理元素,这种数据迭代的方法被称为外部迭代。而有了Stream API,数据处理完全在库内部进行,这种迭代思想被称为内部迭代。

计算集群:用高速网络连接起来的多台计算机

Colletion主要是为了存储和访问数据,而Stream则主要用于描述对数据的计算。

第二章

通过行为参数化传递代码

行为参数化是可以帮助你处理频繁变更的需求的一种软件开发模式。
将代码块作为参数传递给另一个方法,稍后再去执行它。这样这个方法就基于那块代码被参数化了。打个比方,为了实现行为参数化可能会这样处理一个集合:

这样一个方法便可以接受不同的新行为作为参数去执行。

举个栗子:假设要求你对苹果的不同属性做筛选,比如大小、形状、产地、重量等,写好多个重复的filter方法或者一个巨大的非常复杂的方法都不是好办法。为此需要一种更好的方法,来把苹果的选择标准告诉filterApples方法。更高层次的抽象这个问题,对选择标准进行建模:苹果需要根据Apple的某些属性来返回一个boolean值,这需要一个返回boolean值的函数,我们把它称为谓词

现在我们定义一个接口来对选择标准建模:

public interface ApplePredicate(){
    boolean test(Apple apple);
}

现在就可以用ApplePredicate的多个实现代表不同的选择标准了:

    static class AppleWeightPredicate implements ApplePredicate{
        public boolean test(Apple apple){
            return apple.getWeight() > 150; 
        }
    }
    static class AppleColorPredicate implements ApplePredicate{
        public boolean test(Apple apple){
            return "green".equals(apple.getColor());
        }
    }

    static class AppleRedAndHeavyPredicate implements ApplePredicate{
        public boolean test(Apple apple){
            return "red".equals(apple.getColor()) 
                    && apple.getWeight() > 150; 
        }
    }
选择苹果的不同策略

这些标准可以看做filter方法的不同行为。上面做的这些和“策略设计模式”相关,它让你定义一族算法,把它们封装起来,然后运行的时候选择一个算法,这里的算法族就是applePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。

下面的问题是,要怎么利用applePredicate的不同实现。需要让filterApples方法接受applePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为作为参数,并在内部使用,来完成不同的行为。

为此需要给filterApples方法添加一个参数,让它能接受ApplePredicate对象。这样在软件工程上带来的好处就是:**将filterApples方法迭代集合的逻辑与你要应用到集合中每个也元素的行为(这里是谓词)区分开了。

由此便可以修改ApplePredicate方法为:

    public static List<Apple> filterApples(List<Apple> inventory,
                                           ApplePredicate p){
        List<Apple> result = new ArrayList<>();
        for(Apple apple: inventory){
            if(p.test(apple)){
                result.add(apple);
            }
        }
        return result;
    }

现在便可以创建不同的ApplePredicate对象,并将它们传递给filterApples方法。filterApples方法的行为取决于你通过ApplePredicate对象传递的代码。实现了行为参数化

虽然已经可以把行为抽象出来让代码适应需求的变化了,但是这个过程很啰嗦,因为当要把新的行为传递给方法的时候,可能需要声明很多实现接口的类来实例化好几个只要实例化一次的类(划重点,这边说的是对于只要实例化一次的类)。这样又啰嗦又费时间。

对于这个问题也有对应的解决办法,Java有一个机制称为匿名类,它可以让你同时声明和实例化一个类,使代码更为简洁。

匿名类

匿名类和Java局部类差不多,但匿名类没有名字。它允许你同时声明并实例化一个类(随用随建)。

用匿名类实现的ApplePredicate对象:

List<Apple> redApples= filterApples(inventory, new ApplePredicate(){
    public boolean test(Apple apple){
        return "red".euqls(apple.getColor());
    }
});

但是这也并不完全令人满意。第一,它代码占用很多行。第二,它用起来容易让人费解。即使匿名类处理在某种程度上改善了为一个借口声明好几个实体类的啰嗦问题,让仍不能让人满意。更好的方式是通过Lambda表达式让代码更易读。

上面的代码在Java8里可以用Lambda表达式重写为下面的样子:

List<Apple> result =
        filterApples(inventory, (Apple apple) -> "red".equals(getColor()));
将List类型抽象化

目前的filterApples方法还只适用于Apple。还可以将List类型抽象化,从而超越眼前要处理的问题:

public interface Predicate<T>{
    boolean test(T t);
}

public static <T> List<T> filter(List<T> inventory, ApplePredicate<T> p){
    List<T> result = new ArrayList<>();
    for(T e : list ){
        if(p.test(e)){
            result.add(e);
        }
    }
    return result;
} 

现在可以把filter方法用在其他列表上了。

真实的例子

下面将通过两个例子来巩固传递代码的思想。

用Comparator来排序

想要根据不同的属性使用一种方法来表示和使用不同的排序行为来轻松地适应变化的需求。
在Java 8中,List自带了一个sort方法(也可以使用Collections.sort)。sort的行为可以用java.util.Comparator对象来参数化,它的接口如下:

public interface Comparator<T>{
    public int compare(T o1,T o2);
}

因此可以随时创建Comparator的实现,用sort方法表现出不同的行为。

按照重量升序对库存排序:

inventory.sort(new Comparator<Apple>(){
    public int compare(Apple a1,Apple a2){
        return a1.getWeight().compareTo(a2.getWeight());
    }
});

(这段代码并没有告诉你如何同时遍历列表中的两个元素,不要纠结遍历问题,下一章会详细的讲解如何编写和使用Lambda表达式。)

用Runnable执行代码块

线程就像是轻量级的进程:它们自己执行一个代码块。多个线程可能会运行不同的代码。为此需要一种方式来代表要执行的一段代码。在Java里,可以使用Runnable接口表示一个要执行的代码块:

public interface Runnabke{
    public void run();
}

可以像下面这样使用这个接口创建执行不同行为的线程:

Thread t = new Thread(new Runnable()){
    public void run(){
        System.out.println("Hello world");
    }
});

用Lambda表达式的话,看起来会是这样:

Thread t =new Thread(() -> System.out.println("Hello world"));

第三章

Lambda表达式

利用行为参数化这个概念,就可以编写更为灵活且可重复使用的代码。但同时,使用匿名类来表示不同的行为并不令人满意。Java 8引入了Lambda表达式来解决这个问题。它使你以一种很简洁的表示一个行为或传递代码。

可以将Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

使用Lambda的最终结果就是你的代码变得更清晰、灵活。打比方,利用Lambda表达式,可以更为简洁地自定义一个Comparator对象。

Lambda表达式由参数、箭头和主体组成

对比以下两段代码:

Comparator <Apple> byWeight = new Comparator<Apple>(){
    public int compare(Apple a1,Apple a2){
        return a1.getWeight().compareTo(a2.getWeight());
    }
};
(Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight())

代码看起来更清晰了。基本上只传递了真正需要的代码(compare方法的主体)。

Lambda表达式由三个部分,如图 Lambda表达式由参数、箭头和主体组成 所示

Lambda 的基本语法是:
(parameters) -> expression(parameters) -> { statements; }

函数式接口

之前的Predicate<T>就是一个函数式接口,因为Predicate仅仅定义了一个抽象方法:

public interface Predicate<T>{
    boolean test(T t);
}

简而言之,函数式接口就是**只定义一个抽象方法的接口。Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。

函数式接口的抽象方法的签名

把Lambda付诸实践:环绕执行模式

要从一个文件中读取一行所需的模板代码:

public static String processFile() throws IOException{
    try (BufferedReader br =
            new BufferedReader(new FileReader("data.txt"))){
        return br.readLine();
    }
}

现在这段代码的局限性在于只能读文件的第一行,如果想要返回头两行,甚至是返回使用最频繁的词。这时需要把processFile的行为参数化。
需要一个接收BufferedReader并返回String的Lambda。下面是从BufferedReader中打印两行的写法:

String result = processFile(BufferedReader br) -> br.readLine() + br.readLine());

现在需要创建一个能匹配BufferedReader -> String,还可以抛出IOException异常的接口:

@FunctionalInterface
public interface BufferedReaderProcessor{
    String process(BufferedReader b) throws IOException;
}

@FunctionalInterface 是什么?
这个标注用于表示该接口会设计成一个函数式接口。如果用 @FunctionalInterface 定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。
使用它不是必须的,但是使用它是比较好的做法。

现在就可以把这个接口作为新的processFile方法的参数,在方法主体内,对得到BufferedReaderProcessor对象调用process方法执行处理:

public static String processFile(BufferedReaderProcessor p) throws IOException {
    try (BufferedReader br =
                 new BufferedReader(new FileReader("data.txt"))){
        return p.process(br);
    }
}

现在可以通过传递不同的Lambda重用processFile方法,并以不用的方式处理文件了。
处理一行:

String oneLine =
    processFile((BufferedReader br) -> br.readLine());

处理两行:

String twoLines = 
     processFile((BufferedReader br) -> br.readLine() + br.readLine());

完整代码如下:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class test{
    @FunctionalInterface
    public interface BufferedReaderProcessor{
        String process(BufferedReader b) throws IOException;
    }
    public static String processFile(BufferedReaderProcessor p) throws IOException {
        try (BufferedReader br =
                     new BufferedReader(new FileReader("c:/tmp/data.txt"))){
            return p.process(br);
        }
    }
    public static void main(String[] args) throws IOException {
        String oneLine = processFile((BufferedReader br) -> br.readLine());
        String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
        System.out.println(oneLine);
        System.out.println(twoLines);
    }
}

现在已经能成功的利用函数式接口来传递Lambda。

使用函数式接口

函数接口定义且只定义了一个抽象方法。函数式接口的抽象方法的签名称为函数描述符。因此,为了应用不同的Lambda表达式,需要一套能够描述常见函数描述符的函数式接口。
Java 8在java.util.function包中加入了许多新的函数式接口,你可以重用它来传递多个不同的Lambda。

Predicate

java.util.function.Predicate<T>接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。在需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口。

public interface Predicate<T> {
    boolean test(T t);
}

例如:

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

public static <T> List<T> filter(List<T> list, Predicate<T> p){
    List<T> results = new ArrayList<>();
    for (T s: list){
        if(p.test(s)){
            results.add(s);
        }    
    }
    return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Consumer

java.util.function.Consumer<T>定义了一个名叫accept的抽象方法,它接受泛型T对象,没有返回。
例如:

@FunctionalInterface
public interface Comsumer<T> {
    void accept(T t);
}

public static <T> void forEach(List<T> list, Predicate<T> c){
    for (T i: list){
        c.accept(i);
    }
}
forEach(
      Arrays.asList(1,2,3,4,5),
      (Interger i) -> System.ou.println(i)
    );
Function

java.util.function.Function<T, R>定义了一个名叫apply的抽象方法,它接受一个泛型T对象,并返回一个泛型R的对象。
下面将创建一个map方法,以将一个String列表映射到包含每个String长度的Interger列表:

@FunctionalInterface
public interface Function<T, R> {
    R accept(T t);
}

public static <T, R> List<R> map(List<T> list, Function<T, R> f){
    List<R> result = new ArrayList<>();
    for (T s: list){
        result.add(f.apply(s));
    }
    return result;
}
List<Integer> l =map(Arrays.asList("lambdas","in","action"), (String s) -> s.length());
原始类型特化

Java的类型有两种: 引用类型原始类型 。但是泛型只能绑定到引用类型。这是由于泛型内部的实现方式造成的。因此Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫装箱(boxing)。相反的操作便叫拆箱(unboxing)。装箱和拆箱操作是可以由自动装箱机制来自动完成的。但是这在性能方面要付出代价,装箱后的值需要更多的内存并且需要额外的内存。

Java 8为原始类型带来了一个专门的版本,用于在输入和输出都是原始类型时避免自动装箱的操作:

public interface IntPredicate{
    boolean test(int t);
}

无装箱:

IntPredicate evenNumbers = (int i) -> i%2 ==0;
evenNumbers.test(1000);

装箱:

Predicate<Integer> oddNumbers = (Integer i) -> i%2 == 1;
oddNumbers.test(1000);

类型检查、类型推断以及限制

Lambda本身并不包含它在实现哪个函数式接口的信息。为了全面了解Lambda表达式应该知道Lambda的实际类型是什么。

类型检查

Lambda的类型是从使用Lambda的上下文推断出来的。上下文(例如接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。

同样的Lambda,不同的函数式接口

有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来。

类型推断

可以进一步简化你的代码。编译器会从上下文(目标类型)推断出痛什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,这样就可以再Lambda语法中省去标注参数类型。

没有自动类型推断:

Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

有自动类型推断:

Comparator<Apple> c =(a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
使用局部变量

之前介绍的所有Lambda表达式都只用到了其主体里面的参数。但Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。它们被称作捕获Lambda

例如:

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);

Lambda捕获了portNumber变量。但关于对这些变量可以做什么有一些限制。Lambda可以没有限制的捕获实例变量和静态变量(也就是在其主体中引用)。但是局部变量必须显示声明为final(或事实上是final)。
下面的代码无法编译,因为portNumber变量被赋值了两次:

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;

实例变量和局部变量背后的实现有一个关键的不同。实例变量都存储在堆中,而局部变量则保存在栈上。Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后去访问该变量,这回引发造成线程不安全的新的可能性。

方法引用

方法引用可以重复使用现有的方法定义,并像Lambda一样传递它们。

下面是用方法引用写的一个排序的例子:

先前:

invenstory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

之后(使用方法引用和 java.util.Comparator.comparing ):

inventory.sort(comparing(Apple::getWeight));

方法引用可以被看做仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。

方法引用就是让你根据已有的方法实现来创建Lambda表达式。

当你需要使用方法引用时,目标引用放在分隔符 :: 前,方法的名称放在后面。例如 Apple::getWeight 就是引用了Apple类中定义的方法getWeight。

方法引用也可以看做针对仅仅设计单一方法的Lambda的语法糖

构建方法引用

方法引用主要有三类。

  1. 指向静态方法的方法引用(Integer::parseInt)
  2. 指向任意类型实例方法的方法引用(String::length)
  3. 指向现有对象的实例方法的方法引用(Transaction::getValue)

第二种方法引用的思想就是你在引用一个对象的方法,而这个对象本身是Lambda的一个参数。
例如 (String s) -> s.toUpperCase() 可以写作 String::toUpperCase

第三种方法引用指的是,你在Lambda中调用一个已经存在的外部对象的方法。例如,Lambda表达式 () -> expenssiveTransaction.getValue() 可以写作 expensiveTransaction::getValue

方法引用不需要括号,是因为没有实际调用这个方法。

构造函数引用

可以利用现有构造函数的名称和关键字来创建它的一个引用 ClassName:new

例如:

List<Integer> weight = Arrays.asList(7,3,4,10);
List<Apple> apples = map(weights, Apple::new);
public static List<Apple> map(List<Integer> List, Function<Integer, Apple> f){
    List<Apple> result = new ArrayList<>();
    for(Integer e: list){
        result.add(f.apply(e))
    }
    return result;
}

Lambda和方法引用实战(用不同的排序策略给一个Apple列表排序)

第1步:传递代码

Java API已经提供了一个List可用的sort方法,要如何把排序策略传递给sort呢?sort方法的签名样子如下:

void sort(Comparator<? super E> c)

它需要一个Comparator对象来比较两个Apple。

第一个解决方案看上去是这样的:

public class AppleComparator implements Comparator<Apple>{
    public int compare(Apple a1, Apple a2){
        return a1.getWeight().compareTo(a2.getWeight());
    }
}

inventory.sort(new AppleComparator());
第2步:使用匿名类改进
inventory.sort(new AppleComparator<Apple>(){
    public int compare(Apple a1, Apple a2){
        return a1.getWeight().compareTo(a2.getWeight());
    }
});

使用匿名类的意义仅仅在于不用为了只实例化一次而实现一个Comparator。

第3步:使用Lambda表达式
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
函数复合

还可以把Function接口所代表的Lambda表达式复合起来。Function接口有两个默认方法:andThen和 compose。它们都会返回Function的一个实例。

  1. andThen 方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。

比如函数f给数字加1,另一个函数给数字乘2:

Function<Integer,Integer> f = x -> x + 1;
Function<Integer,Integer> g = x -> x * 2;
Function<Integer,Integer> h = f.andThen(g);
int result = h.apply(1);

在数学上意味着g(f(x))

  1. compose 方法先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果。
Function<Integer,Integer> f = x -> x + 1;
Function<Integer,Integer> g = x -> x * 2;
Function<Integer,Integer> h = f.compose(g);
int result = h.apply(1);

在数学上意味着f(g(x))

第四章

引入流

集合是Java中使用最多的API。几乎每个Java应用程序都会制造和处理集合。但集合的操作却远远算不上完美。

流是Java API,它允许你以声明性方式处理数据集合。此外流还可以透明性地并行处理,无需写任何多线程代码。

下面是一个Java 7实现的 返回低热量的菜肴名称并按照卡路里排序:

List<Dish> lowCaloricDishes = new ArrayList<>();
for(Dish d: dishes){
    if(d.getCalories() < 400){
        lowCaloricDishes.add(d);
    }
}
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
    public int compare(Dish d1, Dish d2){
        return Integer.compare(d1.getCalories(), d2.getCalories());
    }
});
List<String> lowCaloricDishesName = new ArrayList<>();
for(Dish d: lowCaloricDishes){
    lowCaloricDishesName.add(d.getName());
}

变量lowCaloricDishes唯一的作用就是作为一次性的中间容器。

下面是Java 8实现:

import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
List<String> lowCaloricDishesName = 
    menu.stream()
        .filter(d -> d.getCalories() < 400)
        .sorted(comparing(Dish::getCalories))
        .map(Dish::getName)
        .collect(toList());

为了利用多核架构并行执行这段代码,只需要把 stream() 换成 parallelStream()

import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
List<String> lowCaloricDishesName = 
    menu.parallelStream()
        .filter(d -> d.getCalories() < 400)
        .sorted(comparing(Dish::getCalories))
        .map(Dish::getName)
        .collect(toList());

使用新的方法 有几个显而易见的好处:

使用Java 8 的Stream API的优点:

流简介

流是什么?简短的定义就是“从支持数据处理操作的源 生成的 元素序列”。

此外,流操作有两个重要的特点:

例如:

import static java.util.stream.Collectors.toList;
List<String> threeHighCaloricDishesName = 
    menu.stream()
        .filter(d -> d.getCalories() > 300)
        .map(Dish::getName)
        .limit(3)
        .collect(toList());
    System.out.println(threeHighCaloricDishNames);

在上面示例代码中,先是对menu调用stream方法,由菜单得到一个流。数据源是menu,它给流提供一个元素序列。接下来,对流应用一系列数据处理操作:filter、map、limit和collect。除了collect之外,所有这些操作都会返回一个流,这样就可以连接成一条流水线。最后,collect操作开始处理流水线,并返回结果(它和别的操作不一样,因为它返回的是一个List)。

在调用collect之前,没有任何结果产生,实际上根本就没有从menu里选择元素,可以理解为:链中的方法调用都在排队等待,直到调用collect。

这样做的好处在于,你并没有去实现筛选、提取或截断功能,Stream库已经自带了。

流与集合

粗略的说,集合与流之间的差异就在于什么时候进行计算。集合是一个内存中的数据结构,它包含数据结构中目前所有的值,集合中每个元素都得算出来来才能添加到集合中(不管往集合里加东西或者删东西,集合中的每个元素都是放在内存里的,元素都得先算出来才能成为集合的一部分)。
流则是概念上固定的数据结构,其元素是按需计算的。从另一个角度来说,流就像是一个延迟创建的集合:只有在消费者要求的时候才会计算值。而集合则是急切创建的。

只能遍历一次

和迭代器类似,流只能遍历一次。遍历完以后,这个流就已经被消费掉了。可以从原始数据源那里再获得一个新的流来重新遍历一遍。

以下代码会抛出一个异常,提示流已被消费掉了:

List<String> title = Arrays.asList("Java8","In","Action");
Stream<String> s = title.stream();
s.forEach(System.out::println);
s.forEach(System.out::println);
外部迭代和内部迭代

集合和流的另一个关键区别在于它们遍历数据的方式。

使用Collection接口需要用户去做迭代(比如用for-each),这称为外部迭代。而Stream库使用内部迭代 ----- 它帮你把迭代做了,还把得到的流值存在了某个地方,只要给出一个函数说要干什么就可以了。

用for-each循环外部迭代:

List<String> names = new ArrayList<>();
for(Dish d: menu){
    names.add(d.getName());
}

用背后的迭代器做外部迭代:

List<String> names = new ArrayList<>();
Iterator<String> iterator =menu.iterator();
while(iterator.hasNext()) {
    Dish d = iterator.next();
    names.add(d.getName());
}

流:内部迭代:

List <String> names = menu.stream()
           .map(Dish::getName)
           .collect(toList());

流操作

java.util.stream.Stream中的stream接口定义了许多操作。它们可以被分为两大类:中间操作终端操作。可以被连接起来的流操作称为中间操作,关闭流的操作称为终端操作

中间操作

中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。更重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理。中间操作会合并起来在终端操作时一次性全部处理。

终端操作

终端操作会从流的流水线生成结果。其结果可以是任何不是流的值。

使用流

流的使用一般包括三件事:

流的流水线背后的理念类似于构建器模式。在构建器模式中有一个调用链用来设置一套配置(流的中间操作链),接着是调用built方法(流的终端操作)。


第五章

筛选和切片

用谓词筛选

Stream接口支持filter方法。该操作会接受一个谓词作为参数,并返回一个包括所有符合谓词的元素的流。

例如:

List<Dish> vegetarianMenu = menu.stream()
                                .filter(Dish::isVegetarian)
                                .collect(toList());
筛选各异的元素

流支持distinct方法,它会返回一个元素各异的流。例如,以下代码会筛选出列表中所有的偶数,并确保没有重复。

List<Integer> numbers = Arrays.asList(1,2,1,3,3,2,4);
numbers.stream()
        .filter(i -> i%2 ==0)
        .distinct()
        .forEach(System.out::println);
截短流

流支持limit(n)方法,该方法会返回一个不超过给定长度n的流。

例如:

List<Dish> dishes = menu.stream()
            .filter(d -> d.getCalories() > 300)
            .limit(3)
            collect(toList());
跳过元素

流还支持skip(n)方法,返回一个扔掉前n个元素的流。如果流元素不足n个则返回一个空流。

例如:

List<Dish> dishes = menu.stream()
            .filter(d -> d.getCalories() > 300)
            .skip(2)
            collect(toList());

映射

一个常见的数据处理套路就是从某些对象中选择信息。在SQL里,可以从表中选择一列。Stream API也通过map和flatMap方法提供了类似的工具。

对流中每一个元素应用函数

流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素。

例如:

//提取菜肴名称
List<String> dishNames = menu.stream()
            .map(Dish::getName)
            .collect(toList());
流的扁平化

对于一个单词表,如何返回一张列表,列出里面各不相同的字符?例如给定单词列表["Hello","World"],要返回列表["H","e","l","o","W","r","d"]。

最开始的版本可能是这样的:

words.stream()
    .map(word -> word.split(""))
    .collect(toList());

这样做的问题在于,传递给map方法的Lambda为每个单词返回了一个String[]。因此map返回的流实际上是Stream<String[]>类型的。而真正想要的是用Stream<String>来表示一个字符流。

  1. 尝试使用map和Arrays.stream()

首先,需要一个字符流,而不是数组流。有一个叫做Arrays.stream()的方法可以接受一个数组并产生一个流,例如

String[] arraysOfWords = {Goodbye", "World"};
Stream<String>  streamOfWords = Arrays.stream(arrayOfWords); 

将它用在前面的流水线里

words.stream()
    .map(word -> word.split(""))
    .map(Arrays::stream)
    .distinct()
    .collect(toList());

当前的解决方案仍然搞不定,因为现在得到的是一个流的列表(因为先是把每个单词转换成一个字母数组,然后把每个数组变成了独立的流

2.使用flatMap

可以像下面这样使用flatMap来解决这个问题:

List<String> uniqueCharacters =  
        words.stream()
            .map(word -> word.split(""))
            .map(Arrays::stream)
            .distinct()
            .collect(toList());

使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map()时生成的单个流都被合并起来,即扁平化为一个流。
简而言之flatMap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来称为一个流。

测验:

1.给定两个数字列表[1,2,3]和[3,4],返回综合能被3整除的数对。

List<Integer> numbers1 = Arrays.asList(1,2,3);
List<Integer> numbers2 = Arrays.asList(3,4);
List<int[]> pairs = numbers1.stream()
        .flatmap(i -> numbers2.stream()
                .filter( j -> (j+i) % 3 == 0)
                .map( j -> new int[]{i,j})
        )
        .collect(toList());

查找和匹配

另一个常见的数据处理套路是看数据集中某些元素是否匹配一个给定的属性。Stream API通过allMatch、anyMatch、noneMatch、findFirst和findAny方法提供这样的工具。

检查谓词是否至少匹配一个元素(anyMatch)
if(menu.stream().anyMatch(Dish::isVegetarian)){
    System.out.println("The menu is vegetarian friendly");
}

anyMatch方法返回一个boolean值,因此是一个终端操作

检查谓词是否匹配所有元素(allMatch)
menu.stream().allMatch( d -> d.getCalories() < 1000);
检查谓词是否与所有谓词都不匹配(noneMatch)
menu.stream().noneMatch( d -> d.getCalories() >= 1000);
查找元素

findAny方法将返回当前流的任意元素:

Optional<Dish> dish = 
        menu.stream()
                .filter(Dish::isVegetarian)
                .findAny();

但代码里的Optional是什么?

Optional简介

Optional<T>类(java.util.Optional)是一个容器类,代表一个值存在或不存在。Java 8通过引入Optional<T> 来避免返回众所周知的容易出问题的null。

Optional里有几种 可以迫使你显式地检查值是否存在或处理值不存在情形的 方法。

例如在前面的findAny代码中你需要显式地检查Optional对象中是否存在一道菜可以访问其名称:

menu.stream()
        .filter(Dish::isVegetarian)
        .findAny()
        .ifPresent(d -> System.out.println(d.getName());
查找一个元素

有些流有一个出现顺序来指定流中项目出现的逻辑顺序。对于这种流,想要找到第一个元素。为此有一个findFirst方法,它的工作方式类似于findAny,它们的区别在于并行上的限制,如果不关心返回的元素是哪个,就用findAny。


归约

如何把一个流中的元素组合起来并表达更复杂的查询?如“计算菜单中的总卡路里”或“菜单中卡路里最高的菜是哪一个”。此类查询需要将流中所有元素结合起来,得到一个值,比如一个Integer。这样的查询可以被归类为归约操作。

元素求和

先看看如何使用for-each循环来对数字列表中的元素求和:

int sum = 0;
for(int x: numbers){
    sum += x;
}

这段代码中有两个参数:

reduce对上面这种重复应用的模式做了抽象,可以像下面这样对流中所有的元素求和:

int sum = numbers.stream().reduce(0,(a, b) -> a+b);

reduce接受两个参数:

还可以使用方法引用让这段代码更简洁。在Java 8中,Integer类现在有了 一个静态的sum方法来对两个数求和:

int sum = numbers.stream().reduce(0, Integer::sum);

无初始值

reduce还有一个重载的变体,它不接受初始值,但是会返回一个Optional对象(考虑到流中没有任何元素,无法返回其和的情况):

Optional<Integer> sum = numbers.stream().reduce((a, b) -> a+b);
最大值和最小值

利用reduce来计算最大值和最小值

Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);

数值流

Stream API还提供了原始类型流特化,专门支持处理数值流的方法

原始类型流特化

Java 8引入了三个原始类型特化流来解决这个问题:IntStream、DoubleStream和LongStream,分别将流中元素特化为int、long和double,可以避免暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。

  1. 映射到数值流

将流转换为特化版本的常用方法是mapToInt、mapToDouble和mapLong。这些方法和前面的map方法的工作方式一样,只不过返回的是一个特化流,而不是Stream<T>。例如可以像下面这一行用mapToInt对menu中的卡路里求和:

int calories = menu.stream()
                .mapToInt(Dish::getCalories)
                .sum();

mapToInt会返回一个IntStream,然后就可以调用IntStream接口中定义的sum方法对卡路里求和。如果流是空的,sum默认返回0。
IntStream还支持其他的方法,如max、min、average等。

  1. 转换回对象流

要把原始流转换成一般流,可以使用boxed方法,如下所示:

IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
  1. 默认值OptionalInt

求和很容易,因为它有一个默认值0。但是如果要计算IntStream的最大元素,默认值0是错误的结果。如何区分没有元素的流和最大值真的是0的流?Optional可以用Integer、String等参考类型来参数化。对于三种原始流特化,也分别有一个Optional原始类型特化版本:OptionalInt、OptionalDouble和OptionalLong。

例如要找到IntStream中的最大元素,可以调用max方法,它会返回一个OptionalInt,如果没有最大值的话,就可以显示处理OptionalInt去定义一个默认值了:

OptionalInt maxCalories = menu.stream()\
                .mapToInt(Dish::getCalories)
                .max();

int max = maxCalories.orElse(1);
数值范围

比如生成1到100之间的所有数字。Java 8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围:range和rangeClosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range生成的范围不包含结束值,而rangeClosed包含结束值。

IntStream evenNumbers = IntStream.rangeClosed(1, 100)
                              .filter(n -> n%2 == 0);

System.out.println(evenNumbers.count());

构建流

接下来将介绍如何从值序列、数组、文件来创建流,甚至由生成函数来创建无限流。

由值创建流

使用静态方法Stream.of通过显式值创建一个流。它可以接受任意数量的参数。

例如:

Stream<String> stream  = stream.of("Java 8","In","Action");

可以使用empty得到一个空流:

Stream<String> emptyStream  = stream.empty();
由数组创建流

可以使用静态方法Arrays.stream从数组创建一个流。它接受一个数组作为参数。

int[] numbers = {2,3,5,7,11,13};
int sum = Arrays.stream(numbers).sum();
由文件生成流

java.nio.file.Files中的很多静态方法都会返回一个流。例如,Files.lines方法会返回一个由指定文件中的各行构成的字符串流。

统计一个文件中有多少各不相同的词:

long uniqueWords = 0;
try(Stream<String> lines = Files.lines(Paths.get("data.txt"),charset.defaultCharset())){
        uniqueWords = lines.flatMap(line -> Arrays.stream(line.split("")))
                          .distinct()
                          .count();
}
catch(IOException e){
}

上面的代码使用Files.lines得到一个流,其中的每个元素都是文件中的一行。然后对line调用split方法将行拆分成单词。最后把distinct和count方法链接起来,统计出各不相同的单词的个数。

由函数生成流

Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流(不像从固定集合创建的流那样有固定大小的流)。由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去。一般来说应该使用limit(n)对无限流加以限制,避免打印无穷多个值。

  1. 迭代
Stream.iterate(0, n -> n+2)
        .limit(10)
        .forEach(System,out::println);

iterate接受一个初始值。还有一个一次应用在每个产生的新值上的Lambda(UnaryOperator<T>类型)。此操作将生成一个无限流 ----- 这个流没有结尾,因为值是按需计算的,因此这个流是无界的。

  1. 生成

与iterate方法不同的是,generate方法不是依次对每个新生成的值应用函数的。它接受一个Supplier<T>类型的Lambda提供新的值。

Stream.generate(Math:random)
            .limit(5)
            .forEach(System.out::println)
上一篇下一篇

猜你喜欢

热点阅读