一文带你深刻了解 Lambda 表达式和方法引用

前言

尽管目前不少公司已经使用 Java8 做为项目开发语言,可是仍然有一部分开发者只是将其设置到 pom 文件中,并未真正开始使用。而项目中若是有8新特性的写法,例如λ表达式。也只是 Idea Alt+Enter 生成的。最近天气很是热,出门晒太阳不如和我一块儿系统的学习一下 Java8 的新特性。提升开发效率也可、享受同事羡慕的眼神也可,让咱们开始吧java

声明:本文首发于博客园,做者:后青春期的Keats;地址:https://www.cnblogs.com/keatsCoder/ 转载请注明,谢谢!程序员

新特性

函数式编程:Lambda表达式、流式编程数据库

其余特性:默认方法、新的Optional类、CompletableFutrue、LocalDate/LocalTimeexpress

这篇文章重点讨论 Lambda 及某些状况下更易读、更天然的方法引用。编程

Lambda表达式

行为参数化

行为参数化就是一个方法接受多个不一样的行为做为参数,并在内部使用他们,完成不一样行为的能力。其实说白了就是将一段代码做为另外一个方法的形参,使该方法更加的灵活、能够应对多变的需求。设计模式

举个关于苹果的例子

例如老师安排张三这么一个任务("法外狂徒"张三改行作程序员了):篮子有不少苹果 List ,须要筛选出这些苹果中的绿色苹果 app

根据具象筛选苹果

这个需求很简单,张三两下就搞定了:ide

public static List<Apple> filterGreenApples(List<Apple> appleList){
    List<Apple> result = new ArrayList<>();
    for (Apple apple : appleList) {
        if("green".equals(apple.getColor())){
            result.add(apple);
        }
    }
    return result;
}

但是这个时候老师改主意了。说绿色的很差吃想吃红色的苹果,张三只好复制这个方法进行修改,将green改为red并修改方法名为 filterRedApples。然而若是老师又让他筛选多种其余颜色的苹果,例如:浅绿色、暗红色、黄色等。这种复制、修改的方法就显得有些难应付。一个良好的原则是尝试抽象其共性函数式编程

对于筛选苹果的需求,能够尝试给方法添加一个参数 color。很是简单的就能够应对老师对不一样颜色苹果的需求。函数

public static List<Apple> filterApplesByColor(List<Apple> appleList, String color){
    List<Apple> result = new ArrayList<>();
    for (Apple apple : appleList) {
        if(color.equals(apple.getColor())){
            result.add(apple);
        }
    }
    return result;
}

张三满意的提交了代码。可是这时老师又对张三说:我想要一些重一点的苹果,通常大于150g的苹果就是比较重的。做为程序员,张三早就想好老师可能会改重量。所以提早定义一个参数做为苹果的重量:

public static List<Apple> filterApplesByWeight(List<Apple> appleList, int weight){
    List<Apple> result = new ArrayList<>();
    for (Apple apple : appleList) {
        if(apple.getWeight() > weight){
            result.add(apple);
        }
    }
    return result;
}

解决方案不错。但是张三复制了大量的方法用于遍历库存。并对每一个苹果应用筛选条件。他打破了DRY(Dont repeat youselt 不要重复本身)的软件设计原则。试想一下,若是张三想换一种遍历的方式,那么每一个方法都须要再改一次,工做量很大。那有没有一种方法能将颜色和质量组合成一个方法呢?能够尝试加一个 flag,而后根据 flag 的值来肯定使用哪一个判断条件。但这种方法十分差劲!试想若是之后有了更多的条件:苹果的大小、产地、品种等等。这个代码应该怎么维护?所以张三须要一种更加灵活的方式来实现筛选苹果的方法。

根据抽象条件筛选

无论使用什么条件筛选,他们都有共性:

  • 须要一个苹果
  • 执行一段代码
  • 返回一个 boolean 的值

其中执行一段代码这一步是不肯定的,而参数和返回值是肯定的,所以咱们能够定义一个接口:

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

及不一样条件筛选的实现:

public class AppleHeavyWeightPredicate implements ApplePredicate{
    @Override
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}

筛选的方法也能够改为这样:

public static List<Apple> filterApples(List<Apple> appleList, ApplePredicate applePredicate){
    List<Apple> result = new ArrayList<>();

    for (Apple apple : appleList) {
        if(applePredicate.test(apple)){
            result.add(apple);
        }
    }
    return result;
}

这时不管应对怎样的需求,张三只须要从新实现 test 方法,而后经过 filterApples 方法传递 test 方法的行为。这表示 filterApples 方法的行为参数化了!

可是张三又以为这样的实现太麻烦了,每次新来一个需求他都须要建立一个类实现 ApplePredicate 接口。有没有更好的办法呢?答案是确定的。在 Java8 以前能够经过匿名类来实现:

public static void main(String[] args) {
    List<Apple> appleList = new ArrayList<>();
    appleList.add(new Apple("red", 150));

    List<Apple> result = filterApples(appleList, new ApplePredicate() {
        @Override
        public boolean test(Apple apple) {
            return "green".equals(apple.getColor()) && apple.getWeight() > 150;
        }
    });
}

匿名类虽然能够解决建立新类的问题,可是他太长了。那要如何简化呢? Java8 提供的 Lambda 就是专门用来简化它的。且看代码:

public static void main(String[] args) {
    List<Apple> appleList = new ArrayList<>();
    appleList.add(new Apple("red", 150));

    List<Apple> result = filterApples(appleList, apple -> "green".equals(apple.getColor()) && apple.getWeight() > 150);
}

从苹果的例子能够看到,行为参数化是一种颇有用的模式,它可以轻松应对多变的需求,它经过把一个行为(一段代码)封装起来,并经过传递和使用建立的行为将其参数化。这种作法相似于策略设计模式。而JavaAPI中已经在多出实践过这个模式了,例如 Comparator 排序、Runnable执行代码块等等

Lambda管中窥豹

Lambda是一种简洁的传递一个行为的匿名函数,它没有名称,却有参数列表、函数主体、返回值、甚至还能够抛出异常。基本语法像这样:

(parameters) -> {statements;}

(parameters) -> expression

在哪里及如何使用Lambda

函数式接口

函数式接口就是只定义一个抽象方法的接口(若是接口中定义了默认方法实现,不管有多少个。只要它只有一个抽象方法,它仍然是函数式接口)

前面咱们在 ApplePredicate 接口中只定义了一个抽象方法 test,因此 ApplePredicate 接口就是函数式接口。相似的还有 Comparator 和 Runnable 等。 Lambda 能够代替匿名类来做为函数式接口的实例。

public static void main(String[] args) {
    Runnable r1 = () -> System.out.println("Hello World 1");
    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"));
}

public static void process(Runnable r){
    r.run();
}

@FunctionalInterface

该注解能够用来声明一个接口是函数式接口,若是接口上有声明,但程序员又为接口写了其余抽象方法,编译器会报错

环绕执行模式

资源处理(处理文件、数据库)常见的操做方法就是:打开一个资源、作一些处理、关闭/释放资源。这个打开和关闭阶段老是很类似,而且会围绕执行处理的哪些重要代码。这就是所谓的环绕执行模式。例如:

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

这个写法是有局限性的,由于你没法灵活的修改处理逻辑的代码。那就跟着我来将他改形成 lambda 可用的形式吧

  1. 行为参数化

    首先咱们要作的行为定义为 processFile。如下是从文件中读取两行的参数化写法

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

  2. 使用函数式接口来传递行为

    processFile 这个方法须要匹配的函数描述符长这样: BufferedReader -> String 。那咱们能够照着它定义接口

    @FunctionalInterface
    public interface BufferedReaderProcesser {
        String profess(BufferedReader br);
    }
  3. 执行一个行为

    改造 processFile 方法,让 BufferedReaderProcesser 接口做为它所执行行为的载体

    public static String processFile(BufferedReaderProcesser brf) throws IOException {
        try(BufferedReader br = new BufferedReader(new FileReader("a.txt"))){
            return brf.professFile(br);
        }
    }
  4. 传递Lambda

    接下来就可使用 Lambda 来传递不一样的行为来以不一样的方式处理文件了:

    public static void main(String[] args) throws IOException {
        // 读一行
        String str1 = processFile(br -> br.readLine());
        // 读两行
        String str2 = processFile(br -> br.readLine() + " " + br.readLine());
        // 找到第一个包含 lambda 的行
        String str3 = processFile(br ->
                                  {
                                      String s;
                                      while ((s = br.readLine()).length() > 0) {
                                          if (s.contains("lambda")) {
                                              return s;
                                          }
                                      }
                                      return null;
                                  }
                                 );
        System.out.println(str1);
        System.out.println(str2);
        System.out.println(str3);
    }

    且看控制台的输出:

    1588507834876

Java提供的函数式接口

Java8 的设计师们在 java.util.function 包中引入了不少新的函数式接口,如下是几个经常使用的

Predicate

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

布尔类型接口:在须要将一个任意类型的对象处理成布尔表达式时,可能须要它。例如咱们以前处理的苹果,固然 T 也能够是学生对象(筛选出身高大于多少的)、用户对象(筛选具备某特征的用户)等等

Consumer

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

消费类型接口:Consumer 是一个消费型方法,他接收一个泛型 而后处理掉。不返回任何东西。

Function

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

若是你须要定义一个 Lambda 将输入的对象信息映射到输出,那 Function 是再合适不过的了。

function 包下还有须要相似的函数式接口,读者能够自行去关注一下接口中方法的参数和返回值。来决定使用哪一个。

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

经过上面的介绍,读者已经对 Lambda 表达式的写法有了必定的了解,那么 Java 编译器是如何识别 Lambda 的参数和返回值的呢?

类型检查

Java 经过上下文(好比,接受他传递的方法的参数或是接受他值的局部变量)来推断 Lambda 表达式须要的目标类型而这个目标类型通常是一个函数式接口,以后判断表达式的参数和返回值是否与接口中惟一抽象方法的声明相对应

类型推断

Java 编译器从上下文中推断出表达式的目标类型后,表达式的参数类型也就被编译器所知道。因此书写表达式时能够省略参数类型,例如:

String str1 = processFile(br -> br.readLine());

processFile 方法的参数(Lambda的目标类型)是:BufferedReaderProcesser brf。BufferedReaderProcesser 接口惟一的抽象方法:String profess(BufferedReader br);方法声明的参数类型是 BufferedReader 。Java 编译器能够推断到这里。所以直接写 br 是没问题的。对于两个参数的方法也能够省略参数类型。而一个参数的方法能够省略参数类型和参数两边的括号

方法引用

方法引用让你能够重复使用现有的方法定义,并像 Lambda 同样传递它们。即提早写好的,可复用的 Lambda 表达式。若是一个 Lambda 表明的只是“直接调用这个方法”,那最好仍是用名称调用它。方法引用的写法以下:

目标引用::方法名 // 由于这里没有实际调用方法,故方法的 () 不用写

三类方法引用

  • 指向静态方法的方法引用

    (args) -> ClassName.staticMethod(args) 写成 ClassName::staticMethod

  • 指向任意类型实例方法的方法引用,例如 T 类的实例 arg0

    (arg0, rest) -> arg0.instanceMethod(rest) 写成 T::instanceMethod

  • 指向现有对象的实例方法的方法引用。

    (args) -> expr.instanceMethod(args) 写成 expr::instanceMethod

第二类和第三类乍看有些迷糊,仔细分辨能够发现:若是方法的调用者是 Lambda 的参数,则目标引用是调用者的类。若是调用者是已经存在的实例对象,则目标引用是该对象

构造函数方法引用

方法引用还能够被用在构造函数上,写法是这样:ClassName::new

好比获取对于获取类型Supplier的接口,我分别用三种写法写出建立一个苹果对象的方法:

// 方法引用写法
Supplier<Apple> s1 = Apple::new;
// Lambda 写法
Supplier<Apple> s2 = () -> new Apple();
// 普通写法
Supplier<Apple> s3 = new Supplier<Apple>() {
    @Override
    public Apple get() {
        return new Apple();
    }
};

复合 Lambda 表达式

上面咱们所讨论的 Lambda 表达式都是单独使用的,而 function 包中不少接口中还定义了额外的默认方法,用来复合 Lambda 表达式。

比较器复合

倒序

假如咱们有一个给苹果按指定重量排序的方法

List<Apple> appleList = new ArrayList<>();
// 构造一个按质量升序排序的比较器
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
appleList.sort(c);
// 按质量倒叙
appleList.sort(c.reversed());

其中,Comparator.comparing 方法是一个简化版的 compare 方法的实现形式,源码以下:

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
    Function<? super T, ? extends U> keyExtractor)
{
    Objects.requireNonNull(keyExtractor);
    return (Comparator<T> & Serializable)
        (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

该方法接收一个 Function 接口的实现类做为参数,而咱们的 Apple::getWeight 方法解析过来就是实现了 Function 接口,重写 apply 方法,apply 方法的声明解析为 int apply(Apple a) ,方法内经过调用 a.getWeight() 方法返回 int 类型的值。后来 return 语句中的 (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2)); 其实就是 Comparator 的 Lambda 表达式实现的匿名类中的方法体。重写的是 int compare(T o1, T o2); 方法

比较器链

咱们常常遇到这样的问题,比较苹果质量时,质量相同。那么接下来就须要第二选择条件了。Comparable 接口也提供了便于 Lambda 使用的比较器链方法 thenComparing。好比首先比较质量,当质量相同时按照价格降序

Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
Comparator<Apple> compareByWeightThenPrice = c.thenComparing(Apple::getPrice).reversed();

appleList.sort(compareByWeightThenPrice);

谓词复合

Predicate 谓词接口中有三个可用的复合方法: and、or、negate 分别表示与或非。使用方法和比较器复合大同小异,读者能够自行体验

函数复合

Function 函数接口中有 andThen() 和 compose() 方法,参数都是 Function 的实现,区别以下

a.andThen(b) 是先执行 a 再执行 b

a.compose(b) 是先执行 b 再执行 a

总结

  1. Lambda 和方法引用自己并不难,理解行为参数化是使用 Lambda 和方法引用的前提
  2. 函数式接口是仅仅声明了一个抽象方法的接口,只有在接受函数式接口的地方才能使用 Lambda 表达式
  3. 方法引用可让你复用现有的方法实现
  4. Comparator、Predicate、Function等函数式接口都提供了几个用来结合 Lambda 表达式的默认方法