JDK 1.8 新特性之Lambda表达式

Lambda表达式基础

Lambda表达式【Lambda Expressions】也可称为闭包,是推动 Java 8 发布的最重要新特性。Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中),使用 Lambda 表达式可以使代码变的更加简洁紧凑。

我们在哪里可以使用Lambda呢?我们可以在函数式接口上使用Lambda表达式【这种说法有点抽象】!Lambda表达式可以为函数式接口生成一个实例,Lambda表达式可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法!但Lambda表达式的签名要和函数式接口的抽象方法保持一致!

Lambda表达式语法:

(parameters) -> expression
或
(parameters) -> { statements; }

lambda表达式的重要特征:

  • 可选类型声明:不需要声明参数类型,编译器可以从上下文环境中推断出Lambda表达式的参数类型。
  • 可选的参数圆括号:一个参数且没有参数类型时无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
  • 控制语句代码必须使用花括号进行括起来。
  • 如果主体代码只是一个表达式(如:{“IronMan”;})而不是一个语句,我们需要去掉分号以及花括号,或者显示返回语句(如:{return “Ironman”;}

注意:(String s) -> { "IronMan"; } 编译无法通过!

“IronMan” 是一个表达式,而不是一个语句。要使此Lambda有效,需要去除花括号和分号,如(String s) -> "IronMan",或者可以使用显式返回语句,如(String s) -> { return "IronMan"; }

// 定义函数式接口
interface A{
    public String test(String a);
}
A a;// 声明
// 注意:等号与最后一个分号之间是Lambda表达式
a = temp -> temp;           //正常
a = temp -> { temp; };      //异常
a = temp -> { return temp; };//正常

a = temp -> "IronMan";      //正常
a = temp -> { "IronMan"; }; //异常
a = temp -> { return "IronMan"; }; //正常

a = temp -> "test" + temp ; //正常
a = temp -> { "test" + temp; } ; //异常
a = temp -> { return "test" + temp; } ; //正常

变量作用域:

  1. Lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 Lambda 内部修改定义在域外的局部变量,否则会编译错误。
  2. Lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)
  3. Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。
// 定义函数式接口
public interface Converter<T1, T2> {
    void convert(int i);
}
int num = 1;  
Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
s.convert(2);
num = 5;  
//报错信息:Local variable num defined in an enclosing scope must be final or effectively final
String first = "";  
Comparator<String> comparator = (first, second) -> Integer.compare(first.length(), second.length());  //编译会出错 

1. 函数式接口

函数式接口(Functional Interface):有且仅有一个抽象方法,但是可以有多个非抽象方法【默认方法】的接口。函数式接口可以被隐式转换为Lambda表达式。

@FunctionalInterface注解是 Java 8 新加入的一个注解,用于接口定义上表示其为函数式接口。主要用于编译级错误检查,加上该注解,当你写的接口不符合函数式接口定义的时候,编译器会报错。

注意:加不加@FunctionalInterface对于接口是不是函数式接口没有影响,该注解只是提醒编译器去检查该接口是否仅包含一个抽象方法 。

说明: JDK 1.8之前只要是包含一个抽象方法的接口都是函数式接口,而 JDK 1.8 之后新增加的java.util.function包定义了各种类型的函数式接口,这样我们就无需重复自定义函数式接口了。

我们用函数式接口可以干什么呢?Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体来说,是函数式接口一个具体实现的实例)。

我们用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后在直接内联将它实例化。如下:

public static void process(Runnable r){
    r.run();
}
public static void main(String[] arg) {
    Runnable r1=()-> System.out.println("Hello World 1"); // 使用 Lambda
    Runnable r2=new Runnable() { // 使用匿名类
        @Override
        public void run() {
            System.out.println("Hello World 2");
        }
    };      
    process(r1);
    process(r2);
    process(()-> System.out.println("Hello World 3"));// 使用 Lambda
}

  通俗的理解是 Lambda 表达式的代码补充其调用处的接口所调用地方的抽象方法的代码,如上面的代码中r1代码添加到了r.run()的执行代码中。并且Lambda表达式的参数无论是数量还是类型都应与接口的抽象方法保持一致。

  使用 Lambda 表达式这样做的优点就是行为的分离,我们想利用process执行另一种行为,只需要在调用处将行为代码进行传递就可以了,大大降低了代码的依赖性,同时也简化了代码量,如果我们使用匿名内部类,如上r2的声明中的开头与结尾完全重复的代码还得再书写一遍,利用 Lambda 就可以省去这部分重复的代码。

  **Lambda 的这种方式就是行为参数化,使用函数式接口来传递行为。**Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。

2. 函数描述符

函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫做函数描述符(function descriptor)

java.util.function包中定义的函数式接查看表:《java.util.function包函数式接口

java.util.function包中定义的函数式接口基本上从名称上就可以判断其签名!

  • 命名时的单词:
    • 表示输入参数:
    • Bi:表示两个不同类型的参数
    • Binary:表示两个相同类型的参数
    • Supplier:表示参,有返回值的时候命名:返回值类型 + Supplier
    • 表示返回值:
    • Consumer:表示返回值为 void
    • Function:表示 返回一个值
    • Operator:表示 返回值类型与参数类型相同,当多个参数的时候通常与Binary共同使用
    • Predicate:表示返回值为 boolean

命名的基本顺序:

  • 输入参数类型 + To + 输出类型 + 输入参数【单词】 + 输出类型【单词】
  • 无参并具有返回值:返回值类型 + Supplier
  • 仅单词命名的函数式接口:
    • Supplier:无参数,返回一个结果
    • Consumer:代表了接受一个输入参数并且无返回的操作
    • Function:接受一个输入参数,返回一个结果
    • Predicate:接受一个输入参数,返回一个布尔值结果

Predicate接口使用示例:

// 定义List集合过滤器
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接口示例:

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

Function接口示例:

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;
}
// [7, 2, 6]
List<Integer> L = map(Arrays.asList("lambdas","in","action"), (String s) -> s.length());

原始类型特化

  Java 类型要么是引用类型(比如Byte、Integer、Object、List),要么是原始类型(比如int、double、byte、char)。但是泛型(比如Consumer中的T)只能绑定到引用类型,这是由泛型内部的实现方式造成的。不过 Java 提供了自动装箱技术,这个自动的转变是虚拟机自动转换的,很有意义,但是在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆中。因此装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。

  Java 8 为我们前面所说函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作

例如:使用IntPerdicate就避免了对值进行装箱操作,但要是用Predicate<Integer>就会把参数装箱成Integer对象中。

import java.util.function.Predicate;
public interface IntPredicate{//JDK中已经定义,再次只是强调说明
    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);//自动装箱

  一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如DoublePerdicateFunction接口还有针对输出参数类型的变种:ToIntFunction<T>IntToDoubleFunction等。

注意:Java API 提供了最常用的函数式接口及其函数描述符(java.util.function包中以及只有一个抽象方法的接口),如果有需要,我们完全可以自己设计一个。

函数式接口 函数描述符 原始类型特化
Predicate<T> T → boolean Int*,Long*,Double*
Consumer<T> T → void Int*,Long*,Double*
Function<T> T → R Int*<R>, Long*<R>,Double*<R>,IntToDouble*, IntToLong*, LongToDouble*,
LongToInt*, DoubleToInt*<R>, DoubleToLong*<T>
Supplier<T> () → T Boolean*,Int*,Long*, Double*
UnaryOperator<T> T → T Int*,Long*,Double*
BinaryOperator<T> (T,T) → T Int*,Long*,Double*
BiPredicate<L,R> (L,R) → boolean
BiConsumer<T,U> (T,U) → void ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T>
BiFunction<T,U,R> (T,U) → R ToIntBiFunction<T,U>,ToDoubleBiFunction<T,U>, ToLongBiFunction<T,U>

3. 类型检查、推断及限制

类型检查

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

解读Lambda表达式的类型检查过程:

解读Lambda表达式的类型检查过程

有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。例如下面两个赋值是有效的:

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

特殊的 void 兼容规则:

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

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

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

类型推断

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

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

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

局部变量

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

例如,下面的 Lambda 捕获了portNumber变量:

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

尽管如此,还有一点点小麻烦:关于能对这些变量做什么有一些限制。

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

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

注意:JDK8允许内部类使用的外部变量可以不用声明其为final,但实际上还是final类型。

对局部变量的限制

为什么局部变量有这些限制?

第一,实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda 可以直接访问局部变量,而且 Lambda 是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此, Java 在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了 – 因此就有了这个限制。

第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(这种模式会阻碍很容易做到的并行处理)。

闭包

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

4. 异常

注意:任何函数式接口都不允许抛出受检异常(checked exception)。
如果我们需要Lambda表达式来抛出异常,我们有两种办法:

  1. 定义一个自己的函数式接口,并声明受检异常。

  2. 把Lambda包在一个try/catch块中。

注意:如果 Lambda 表达式抛出一个异常,那么抽象方法所声明的 throws 语句也必须与之匹配。

5. 方法引用

方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。在一些情况下,比起使用 Lambda 表达式,它们似乎更易读,感觉也更自然。

方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个 Lambda 代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。事实上,方法引用就是让你根据已有的方法实现来创建 Lambda 表达式,并显式地指明方法的名称,你的代码的可读性会更好。

它是如何工作的呢?当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。

例如 ,Apple::getWeight就是引用了Apple类中定义的方法getWeight注意:不需要括号,因为你没有实际调用这个方法。方法引用就是 Lambda 表达式(Apple a) -> a.getWeight()的快捷写法。下表给出了Java 8中方法引用的其他一些例子。

Lambda及其等效方法引用的例子

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

方法引用主要有三类:

  1. 静态方法引用(例如IntegerparseInt方法,写作Integer::parseInt)。
    • 语法:Class::static_method
    • 形式等价参数:Lambda 表达式的参数 即 静态方法的参数
  2. 特定类的任意对象的方法引用(例如Stringlength方法,写作String::length)。
    • 语法:Class::method
    • 形式等价参数:Lambda 表达式的参数 即 类引用 + 方法的参数
  3. 特定对象的方法引用(假设你有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensiveTransaction::getValue)。
    • 语法:instance::method
    • 形式等价参数:Lambda 表达式的参数 即 方法的参数

第二种和第三种方法引用可能乍看起来有点儿晕。

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

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

依照一些简单的方子,我们就可以将 Lambda 表达式重构为等价的方法引用,如下图所示:

将Lambda表达式重构为等价的方法引用

示例(对一个字符串的List排序,并忽略大小写):

List<String> str =Arrays.asList("a","b","A","B");
str.sort((s1,s2) -> s1.compareTo(s2));
// Lambda表达式的签名与  Comparator 的函数描述符兼容
// 利用方法引用可改写为
List<String> str =Arrays.asList("a","b","A","B");
str.sort(String::compareToIgnoreCase);

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

构造函数引用

对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用:ClassName::new。它的功能与静态方法引用类似。

例如,假设有一个构造函数没有参数。它适合 Supplier 的签名() -> Apple。你可以这样做:

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

这就等价于:

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

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

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

这就等价于:

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

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

List<Integer> weights = Arrays.asList(7, 3, 4, 10);
List<Apple> apples = map(weights, Apple::new);//将构造函数引用传递给map方法

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;
}

如果你有一个具有两个参数的构造函数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);

这就等价于:

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

不将构造函数实例化却能够引用它,这个功能有一些有趣的应用。

例如,你可以使用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) {
    return map.get(fruit.toLowerCase())//你用 map 得到了一个Function<Integer,Fruit>
            //用Integer类型的weight参数调用Function的apply()方法将提供所要求的Fruit
            .apply(weight);
}

Lambda 和方法引用实战

Java 8 实战_高清中文版》第3.7章

  • 需求:苹果根据重量排序!
  • 背景:利用List集合inventory来存储苹果Apple,并利用Listsort方法进行排序。

传统做法

//创建比较器
public class AppleComparator implements Comparator<Apple> {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }
}
inventory.sort(new AppleComparator());

使用匿名类

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

使用Lambda表达式

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

Java 编译器可以根据 Lambda 出现的上下文来推断 Lambda 表达式参数的类型。因此可以重写为:

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

Comparator具有一个叫做comparing的静态辅助方法,他可以接受一个Function来提取Comparable键值,并生成一个Comparator对象。可以如下方式使用(当前传递的Lambda只有一个参数:Lambda说明了如何从苹果中提取需要比较的键值):

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

通过静态导入的方式简化代码如下:

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

使用方法引用

方法引用就是替代那些转发参数的Lambda表达式的语法糖。

import static java.util.Comparator.comparing;
inventory.sort(comparing(Apple::getWeight));

复合Lambda表达式

谓词:在计算机语言的环境下,谓词是指条件表达式的求值返回真或假的过程。

Java 8 的函数式接口基本上都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递Lambda表达式的ComparatorFunctionPredicate都提供了允许你进行复合的方法。这是什么意思呢?在实践中,这意味着你可以把多个简单的 Lambda 复合成复杂的表达式。比如,你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你还可以让一个函数的结果成为另一个函数的输入。你可能会想,函数式接口中怎么可能有更多的方法呢?这些都是默认方法,而不是抽象方法。

比较器Comparator复合

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

// 创建根据重量比较的比较器:正序
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
// 创建根据重量比较的比较器:逆序
Comparator<Apple> c = Comparator.comparing(Apple::getWeight).reversed()
// 创建根据重量比较的比较器:比较器链
Comparator<Apple> c = Comparator.comparing(Apple::getWeight)
        .reversed() // 按重量递减排序
        .thenComparing(Apple::getCountry); // 两个苹果一样重时,进一步按国家排序

谓词Predicate复合

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

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

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

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

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

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

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

函数Function复合

把 Function 接口所代表的 Lambda 表达式复合起来。 Function 接口为此配了andThencompose两个默认方法,它们都会返回 Function 的一个实例。

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

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

你也可以类似地使用compose方法,先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果。

比如在上一个例子里用compose的话,它将意味着f(g(x)),而andThen则意味着g(f(x))

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

下图说明了 andThencompose 之间的区别:
andThen和compose之间的区别

这一切听起来有点太抽象了。那么在实际中这有什么用呢?比方说你有一系列工具方法,对用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");
    }
}

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

Function<String, String> addHeader = Letter::addHeader;
Function<String, String> transformationPipeline = addHeader.andThen(Letter::checkSpelling)
        .andThen(Letter::addFooter);

使用andThen的转换流水线

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

Function<String, String> addHeader = Letter::addHeader;
Function<String, String> transformationPipeline = addHeader.andThen(Lette

赞赏

猜你喜欢

转载自blog.csdn.net/fanxiaobin577328725/article/details/82024620