• Java 8实战(四)- Lambda类型推断与方法引用


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

    1. 类型检查

    Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型
    图3-4概述了下列代码的类型检查过程。

    在这里插入图片描述

    类型检查过程可以分解为如下所示。
     首先,你要找出filter方法的声明。
     第二,要求它是Predicate(目标类型)对象的第二个正式参数。
     第三,Predicate是一个函数式接口,定义了一个叫作test的抽象方法。
     第四,test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean。
     最后,filter的任何实际参数都必须匹配这个要求。
    这段代码是有效的,因为我们所传递的Lambda表达式也同样接受Apple为参数,并返回一个boolean。请注意,如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必须与之匹配。

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

    有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。比如,前面提到的Callable和PrivilegedAction,这两个接口都代表着什么也不接受且返回一个泛型T的函数。 因此,下面两个赋值是有效的:

    Callable<Integer> c = () -> 42;
    PrivilegedAction<Integer> p = () -> 42;
    
    • 1
    • 2

    这里, 第一个赋值的目标类型是Callable , 第二个赋值的目标类型是PrivilegedAction。

    同一个Lambda可用于多个不同的函数式接口:

    Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
    ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
    BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
    
    • 1
    • 2
    • 3

    菱形运算符
    那些熟悉Java的演变的人会记得,Java 7中已经引入了菱形运算符(<>),利用泛型推断从上下文推断类型的思想(这一思想甚至可以追溯到更早的泛型方法)。一个类实例表达式可以出现在两个或更多不同的上下文中,并会像下面这样推断出适当的类型参数:

    List<String> listOfStrings = new ArrayList<>();
    List<Integer> listOfIntegers = new ArrayList<>();
    
    • 1
    • 2

    特殊的void兼容规则
    如果一个Lambda的主体是一个语句表达式, 它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。例如,以下两行都是合法的,尽管List的add方法返回了一个boolean,而不是Consumer上下文(T -> void)所要求的void:

    // Predicate返回了一个boolean
    Predicate<String> p = s -> list.add(s);
    // Consumer返回了一个void
    Consumer<String> b = s -> list.add(s);
    
    • 1
    • 2
    • 3
    • 4

    可以从赋值的上下文、方法调用的上下文(参数和返回值),以及类型转换的上下文中获得目标类型。

    测验:类型检查——为什么下面的代码不能编译呢?
    你该如何解决这个问题呢?
    Object o = () -> {System.out.println(“Tricky example”); };
    答案:Lambda表达式的上下文是Object(目标类型)。但Object不是一个函数式接口。
    为了解决这个问题,你可以把目标类型改成Runnable,它的函数描述符是() -> void:
    Runnable r = () -> {System.out.println(“Tricky example”); };

    3. 类型推断

    你还可以进一步简化你的代码。Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。换句话说,Java编译器会像下面这样推断Lambda的参数类型:

    请注意,当Lambda仅有一个类型需要推断的参数时,参数名称两边的括号也可以省略。

    // 参数a没有显式类型
    List<Apple> greenApples = filter(inventory, a -> "green".equals(a.getColor()));
    
    • 1
    • 2

    Lambda表达式有多个参数,代码可读性的好处就更为明显。例如,你可以这样来创建一个Comparator对象:

    // 没有类型推断
    Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
    // 有类型推断
    Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
    
    • 1
    • 2
    • 3
    • 4

    请注意,有时候显式写出类型更易读,有时候去掉它们更易读。没有什么法则说哪种更好;对于如何让代码更易读,程序员必须做出自己的选择。

    4. 使用局部变量

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

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

    尽管如此,还有一点点小麻烦:关于能对这些变量做什么有一些限制。Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为final,或事实上是final。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量this。) 例如,下面的代码无法编译,因为portNumber变量被赋值两次:

    int portNumber = 1337;
    // 错误:Lambda表达式引用的局部变量必须是最终的(final)或事实上最终的
    Runnable r = () -> System.out.println(portNumber);
    portNumber = 31337;
    
    • 1
    • 2
    • 3
    • 4

    对局部变量的限制
    你可能会问自己,为什么局部变量有这些限制。
    第一,实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。
    第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(我们会在以后的各章中解释,这种模式会阻碍很容易做到的并行处理)。

    闭包
    你可能已经听说过闭包(closure,不要和Clojure编程语言混淆)这个词,你可能会想Lambda是否满足闭包的定义。用科学的说法来说,闭包就是一个函数的实例,且它可以无限制地访问那个函数的非本地变量。例如,闭包可以作为参数传递给另一个函数。它也可以访问和修改其作用域之外的变量。现在,Java 8的Lambda和匿名类可以做类似于闭包的事情:它们可以作为参数传递给方法,并且可以访问其作用域之外的变量。但有一个限制:它们不能修改定义Lambda的方法的局部变量的内容。这些变量必须是隐式最终的。可以认为Lambda是对值封闭,而不是对变量封闭。如前所述,这种限制存在的原因在于局部变量保存在栈上,并且隐式表示它们仅限于其所在线程。如果允许捕获可改变的局部变量,就会引发造成线程不安全的新的可能性,而这是我们不想看到的(实例变量可以,因为它们保存在堆中,而堆是在线程之间共享的)。

    二、方法引用

    方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。在一些情况下,比起使用Lambda表达式,它们似乎更易读,感觉也更自然。下面就是我们借助更新的Java 8 API,用方法引用写的一个排序的例子:

    先前:

    inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
    
    • 1

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

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

    1. 如何构建方法引用

    方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。但是,显式地指明方法的名称,你的代码的可读性会更好。它是如何工作的呢?当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。例如,Apple::getWeight就是引用了Apple类中定义的方法getWeight。请记住,不需要括号,因为你没有实际调用这个方法。方法引用就是Lambda表达式(Apple a) -> a.getWeight()的快捷写法。

    在这里插入图片描述

    你可以把方法引用看作针对仅仅涉及单一方法的Lambda的语法糖,因为你表达同样的事情时要写的代码更少了。

    方法引用主要有三类。
    (1) 指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt)。
    (2) 指向任意类型实例方法的方法引用( 例如String 的length 方法, 写作String::length)。
    (3) 指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensive-Transaction::getValue)。

    在这里插入图片描述

    请注意,还有针对构造函数、数组构造函数和父类调用(super-call)的一些特殊形式的方法引用。让我们举一个方法引用的具体例子吧。比方说你想要对一个字符串的List排序,忽略大小写。List的sort方法需要一个Comparator作为参数。你在前面看到了,Comparator描述了一个具有(T, T) -> int签名的函数描述符。你可以利用String类中的compareToIgnoreCase方法来定义一个Lambda表达式(注意compareToIgnoreCase是String类中预先定义的)。

    List<String> str = Arrays.asList("a","b","A","B");
    str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));
    
    • 1
    • 2

    Lambda表达式的签名与Comparator的函数描述符兼容。利用前面所述的方法,这个例子可以用方法引用改写成下面的样子:

    List<String> str = Arrays.asList("a","b","A","B");
    str.sort(String::compareToIgnoreCase);
    
    • 1
    • 2

    请注意,编译器会进行一种与Lambda表达式类似的类型检查过程,来确定对于给定的函数式接口,这个方法引用是否有效:方法引用的签名必须和上下文类型匹配。

    测验:方法引用
    下列Lambda表达式的等效方法引用是什么?

    (1) Function<String, Integer> stringToInteger = (String s) -> Integer.parseInt(s);
    (2) BiPredicate<List<String>, String> contains = (list, element) -> list.contains(element);
    
    • 1
    • 2

    答案如下。
    (1) 这个Lambda表达式将其参数传给了Integer的静态方法parseInt。这种方法接受一个需要解析的String,并返回一个Integer。因此,可以使用图3-5中的办法➊(Lambda表达式调用静态方法)来重写Lambda表达式,如下所示:

    Function<String, Integer> stringToInteger = Integer::parseInt;
    
    • 1

    (2) 这个Lambda使用其第一个参数,调用其contains方法。由于第一个参数是List类型的,你可以使用图3-5中的办法➋,如下所示:

    BiPredicate<List<String>, String> contains = List::contains;
    
    • 1

    这是因为,目标类型描述的函数描述符是 (List,String) -> boolean,而List::contains可以被解包成这个函数描述符。

    2. 构造函数引用

    对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用:ClassName::new。它的功能与指向静态方法的引用类似。例如,假设有一个构造函数没有参数。它适合Supplier的签名() -> Apple。你可以这样做:

    // 构造函数引用指向默认的Apple()构造函数
    Supplier<Apple> c1 = Apple::new;
    // 调用Supplier的get方法将产生一个新的Apple
    Apple a1 = c1.get();
    
    • 1
    • 2
    • 3
    • 4

    这就等价于:

    // 利用默认构造函数创建Apple的Lambda表达式
    Supplier<Apple> c1 = () -> new Apple();
    // 调用Supplier的get方法将产生一个新的Apple
    Apple a1 = c1.get();
    
    • 1
    • 2
    • 3
    • 4

    如果你的构造函数的签名是Apple(Integer weight),那么它就适合Function接口的签名,于是你可以这样写:

    // 指向Apple(Integer weight)的构造函数引用
    Function<Integer, Apple> c2 = Apple::new;
    // 调用该Function函数的apply方法,并给出要求的重量,将产生一个Apple
    Apple a2 = c2.apply(110);
    
    • 1
    • 2
    • 3
    • 4

    这就等价于:

    // 用要求的重量创建一个Apple的Lambda表达式
    Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
    // 调用该Function函数的apply方法,并给出要求的重量,将产生一个新的Apple对象
    Apple a2 = c2.apply(110);
    
    • 1
    • 2
    • 3
    • 4

    在下面的代码中,一个由Integer构成的List中的每个元素都通过我们前面定义的类似的map方法传递给了Apple的构造函数,得到了一个具有不同重量苹果的List:

    List<Integer> weights = Arrays.asList(7, 3, 4, 10);
    // 将构造函数引用传递给map方法
    List<Apple> apples = map(weights, Apple::new);
    
    • 1
    • 2
    • 3
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果你有一个具有两个参数的构造函数Apple(String color, Integer weight),那么它就适合BiFunction接口的签名,于是你可以这样写:

    // 指向Apple(String color,Integer weight)的构造函数引用
    BiFunction<String, Integer, Apple> c3 = Apple::new;
    // 调用该BiFunction函数的apply方法,并给出要求的颜色和重量,将产生一个新的Apple对象
    Apple c3 = c3.apply("green", 110);
    
    • 1
    • 2
    • 3
    • 4

    这就等价于:

    // 用要求的颜色和重量创建一个Apple的Lambda表达式
    BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight);
    // 调用该BiFunction函数的apply方法,并给出要求的颜色和重量,将产生一个新的Apple对象
    Apple c3 = c3.apply("green", 110);
    
    • 1
    • 2
    • 3
    • 4

    不将构造函数实例化却能够引用它,这个功能有一些有趣的应用。例如,你可以使用Map来将构造函数映射到字符串值。你可以创建一个giveMeFruit方法,给它一个String和一个Integer,它就可以创建出不同重量的各种水果:

    static Map<String, Function<Integer, Fruit>> map = new HashMap<>();
    
    static {
    	map.put("apple", Apple::new);
    	map.put("orange", Orange::new);
    	// etc...
    }
    
    public static Fruit giveMeFruit(String fruit, Integer weight){
    	// 你用map 得到了一个Function
    	return map.get(fruit.toLowerCase())
    	// 用Integer类型的weight参数调用Function的apply()方法将提供所要求的Fruit
    		      .apply(weight);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    测验:构造函数引用
    你已经看到了如何将有零个、一个、两个参数的构造函数转变为构造函数引用。那要怎么样才能对具有三个参数的构造函数,比如Color(int, int, int),使用构造函数引用呢?
    答案:你看,构造函数引用的语法是ClassName::new,那么在这个例子里面就是Color::new。但是你需要与构造函数引用的签名匹配的函数式接口。但是语言本身并没有提供这样的函数式接口,你可以自己创建一个:
    public interface TriFunction{
    R apply(T t, U u, V v);
    }
    现在你可以像下面这样使用构造函数引用了:
    TriFunction colorFactory = Color::new;

    三、Lambda和方法引用实战

    为了给这一章还有我们讨论的所有关于Lambda的内容收个尾,我们需要继续研究开始的那个问题——用不同的排序策略给一个Apple列表排序,并需要展示如何把一个原始粗暴的解决方案转变得更为简明。这会用到迄今讲到的所有概念和功能:行为参数化、匿名类、Lambda表达式和方法引用。我们想要实现的最终解决方案是这样的:

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

    1. 第 1 步:传递代码

    你很幸运,Java 8的API已经为你提供了一个List可用的sort方法,你不用自己去实现它。那么最困难的部分已经搞定了!但是,如何把排序策略传递给sort方法呢?你看,sort方法的签名是这样的:

    void sort(Comparator<? super E> c)
    
    • 1

    它需要一个Comparator对象来比较两个Apple!这就是在Java中传递策略的方式:它们必须包裹在一个对象里。我们说sort的行为被参数化了:传递给它的排序策略不同,其行为也会不同。
    你的第一个解决方案看上去是这样的:

    public class AppleComparator implements Comparator<Apple> {
    	public int compare(Apple a1, Apple a2){
    		return a1.getWeight().compareTo(a2.getWeight());
    	}
    }
    inventory.sort(new AppleComparator());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2. 第 2 步:使用匿名类

    你在前面看到了,你可以使用匿名类来改进解决方案,而不是实现一个Comparator却只实例化一次:

    inventory.sort(new Comparator<Apple>() {
    	public int compare(Apple a1, Apple a2){
    		return a1.getWeight().compareTo(a2.getWeight());
    	}
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3. 第 3 步:使用Lambda表达式

    但你的解决方案仍然挺啰嗦的。Java 8引入了Lambda表达式,它提供了一种轻量级语法来实现相同的目标:传递代码。你看到了,在需要函数式接口的地方可以使用Lambda表达式。我们回顾一下:函数式接口就是仅仅定义一个抽象方法的接口抽象方法的签名(称为函数描述符)描述了Lambda表达式的签名。在这个例子里,Comparator代表了函数描述符(T, T) -> int。因为你用的是苹果,所以它具体代表的就是(Apple, Apple) -> int。改进后的新解决方案看上去就是这样的了:

    inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
    
    • 1

    你的代码还能变得更易读一点吗?Comparator具有一个叫作comparing的静态辅助方法,它可以接受一个Function来提取Comparable键值,并生成一个Comparator对象。它可以像下面这样用(注意你现在传递的Lambda只有一个参数:Lambda说明了如何从苹果中提取需要比较的键值):

    Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());
    
    • 1

    现在你可以把代码再改得紧凑一点了:

    import static java.util.Comparator.comparing;
    inventory.sort(comparing((a) -> a.getWeight()));
    
    • 1
    • 2

    4. 第 4 步:使用方法引用

    前面解释过,方法引用就是替代那些转发参数的Lambda表达式的语法糖。你可以用方法引用让你的代码更简洁(假设你静态导入了java.util.Comparator.comparing):

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

    恭喜你,这就是你的最终解决方案!这比Java 8之前的代码好在哪儿呢?它比较短;它的意思也很明显,并且代码读起来和问题描述差不多:“对库存进行排序,比较苹果的重量。”

    四、复合Lambda表达式的有用方法

    Java 8的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递Lambda表达式的Comparator、Function和Predicate都提供了允许你进行复合的方法。这是什么意思呢?在实践中,这意味着你可以把多个简单的Lambda复合成复杂的表达式。比如,你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你还可以让一个函数的结果成为另一个函数的输入。你可能会想,函数式接口中怎么可能有更多的方法呢?(毕竟,这违背了函数式接口的定义啊!)窍门在于,我们即将介绍的方法都是默认方法,也就是说它们不是抽象方法。

    1. 比较器复合

    我们前面看到,你可以使用静态方法Comparator.comparing,根据提取用于比较的键值的Function来返回一个Comparator,如下所示:

    Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
    
    • 1

    1. 逆序
    如果你想要对苹果按重量递减排序怎么办?用不着去建立另一个Comparator的实例。接口有一个默认方法reversed可以使给定的比较器逆序。因此仍然用开始的那个比较器,只要修改一下前一个例子就可以对苹果按重量递减排序:

    // 按重量递减排序
    inventory.sort(comparing(Apple::getWeight).reversed());
    
    • 1
    • 2

    2. 比较器链
    上面说得都很好,但如果发现有两个苹果一样重怎么办?哪个苹果应该排在前面呢?你可能需要再提供一个Comparator来进一步定义这个比较。比如,在按重量比较两个苹果之后,你可能想要按原产国排序。thenComparing方法就是做这个用的。它接受一个函数作为参数(就像comparing方法一样),如果两个对象用第一个Comparator比较之后是一样的,就提供第二个Comparator。你又可以优雅地解决这个问题了:

    inventory.sort(comparing(Apple::getWeight)
    		 // 按重量递减排序
    		 .reversed()
    		 // 两个苹果一样重时,进一步按国家排序
    		 .thenComparing(Apple::getCountry));
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2. 谓词复合

    谓词接口包括三个方法:negate、and和or,让你可以重用已有的Predicate来创建更复杂的谓词。比如,你可以使用negate方法来返回一个Predicate的非,比如苹果不是红的:

    // 产生现有Predicate对象redApple的非
    Predicate<Apple> notRedApple = redApple.negate();
    
    • 1
    • 2

    你可能想要把两个Lambda用and方法组合起来,比如一个苹果既是红色又比较重:

    // 链接两个谓词来生成另一个Predicate对象
    Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);
    
    • 1
    • 2

    你可以进一步组合谓词,表达要么是重(150克以上)的红苹果,要么是绿苹果:

    // 链接Predicate的方法来构造更复杂Predicate对象
    Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150)
    							    					.or(a -> "green".equals(a.getColor()));
    
    • 1
    • 2
    • 3

    这一点为什么很好呢?从简单Lambda表达式出发,你可以构建更复杂的表达式,但读起来仍然和问题的陈述差不多!请注意,and和or方法是按照在表达式链中的位置,从左向右确定优先级的。因此,a.or(b).and(c)可以看作(a || b) && c。

    3. 函数复合

    最后,你还可以把Function接口所代表的Lambda表达式复合起来。Function接口为此配了andThen和compose两个默认方法,它们都会返回Function的一个实例。

    andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。比如,假设有一个函数f给数字加1 (x -> x + 1),另一个函数g给数字乘2,你可以将它们组合成一个函数h,先给数字加1,再给结果乘2:

    // 数学上会写作g(f(x))或(g o f)(x)
    Function<Integer, Integer> f = x -> x + 1;
    Function<Integer, Integer> g = x -> x * 2;
    Function<Integer, Integer> h = f.andThen(g);
    // 这将返回4
    int result = h.apply(1);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    你也可以类似地使用compose方法,先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果。比如在上一个例子里用compose的话,它将意味着f(g(x)),而andThen则意味着g(f(x)):

    // 数学上会写作f(g(x))或(f o g)(x)
    Function<Integer, Integer> f = x -> x + 1;
    Function<Integer, Integer> g = x -> x * 2;
    Function<Integer, Integer> h = f.compose(g);
    // 这将返回3
    int result = h.apply(1);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    比方说你有一系列工具方法,对用String表示的一封信做文本转换:

    public class Letter{
    	public static String addHeader(String text){
    		return "From Raoul, Mario and Alan: " + text;
    	}
    	public static String addFooter(String text){
    		return text + " Kind regards";
    	}
    	public static String checkSpelling(String text){
    		return text.replaceAll("labda", "lambda");
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    现在你可以通过复合这些工具方法来创建各种转型流水线了,比如创建一个流水线:先加上抬头,然后进行拼写检查,最后加上一个落款,如图3-7所示。

    Function<String, String> addHeader = Letter::addHeader;
    Function<String, String> transformationPipeline = 
    		addHeader.andThen(Letter::checkSpelling)
    				 .andThen(Letter::addFooter);
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述

    第二个流水线可能只加抬头、落款,而不做拼写检查:

    Function<String, String> addHeader = Letter::addHeader;
    Function<String, String> transformationPipeline = addHeader.andThen(Letter::addFooter);
    
    • 1
    • 2
  • 相关阅读:
    ESP8266-Arduino编程实例-MQ-6异丁烷丙烷传感器驱动
    mysql学习笔记1:mysql字符集和字符集编码
    视频转换芯片MS7200概述 HDMI转数字RGB/YUV/HDMI RXReceive/替代IT66021FN
    Vue学习:计算属性
    ArcGIS如何快速对齐两个图层
    Nlp项目实战自定义模板框架
    【STM32】IIC的初步使用
    用RocketMQ这么久,才知道消息可以这样玩
    tf.dynamic_stitch
    如何更好的选择服务器硬盘?
  • 原文地址:https://blog.csdn.net/qq_36602071/article/details/126846682