1_java|lambda表达式和方法引用


文章目录

    • 1.函数式接口
      • 1.1.定义
      • 1.2.SerializedLambda
    • 2.lambda
      • 2.1.概述
      • 2.2.示例如下
    • 3.方法引用
      • 3.1.概述
      • 3.2.定义
      • 3.3.示例
      • 3.4.为什么这么定义这么用?

jdk1.8以后推出了很多的新特性,比如1.default关键字,2.lambda表达式,3.函数式接口,4.方法引用,5.Date Api,stream等。这里面default和Date Api的用法比较容易理解,这里不做更多阐述。今天我们要说的是 lambda表达式,函数式接口,方法引用以及stream这些比较难以理解、困惑的部分。这些的困惑包括:lambda表达式如何使用,以及为什么这么使用。函数式接口如何使用方法引用。以及stream的基本用法。
1.函数式接口 1.1.定义
一个接口中有切只有一个抽象方法,不包括 equals这类在object中已经定义的方,为了明确表示一个接口是函数式接口,防止别人在接口中添加其他抽象方法,我们可以给接口定义的时候添加一个添加一个@FunctionalInterface注解。
示例如下:
package com.bsx.test.lambda; import java.io.Serializable; @FunctionalInterface public interface IGetter extends Serializable { Object get(T source); }

1.2.SerializedLambda
这一部分内容比较多,请参考另一篇文章:SerializedLambda
2.lambda 2.1.概述
λ表达式有三部分组成:1.参数列表,2.箭头(->),3.一个表达式或语句块,其中表达式是指的是一句代码,语句块是用大括号"{}"包起来的一系列代码,而λ本身必须是函数接口才能使用λ表达式。lambda 语法本质上是一个匿名方法是【语法糖】,由编译器推断并帮助你转换包装为常规代码。说白了lambad表达式就是把函数定义从原来的标准定义方式给简化了,这是因为编译器可以根据表达式内容来推断入参、出参。因此使用lambda可以使用更少的代码来实现相同功能。
2.2.示例如下
// 1. 不需要参数,返回值为 5 () -> 5// 2. 接收一个参数(数字类型),返回其2倍的值 x -> 2 * x// 3. 接受2个参数(数字),并返回他们的差值 (x, y) -> x – y// 4. 接收2个int型整数,返回他们的和 (int x, int y) -> x + y// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void) (String s) -> System.out.print(s) // 6.循环打印数组 String[] atp = {"Rafael Nadal", "Novak Djokovic"}; List> players =Arrays.asList(atp); // 以前的循环方式 for (String player : players) { System.out.print(player + "; "); }// 使用 lambda 表达式以及函数操作(functional operation) players.forEach((player) -> System.out.print(player + "; ")); String[] players = {"Rafael Nadal", "Novak Djokovic}; // 7.排序 // 1.使用匿名内部类根据 name 排序 players Arrays.sort(players, new Comparator>() { @Override public int compare(String s1, String s2) { return (s1.compareTo(s2)); } }); // 2.使用 lambda expression 排序 players Comparator> sortByName = (String s1, String s2) -> (s1.compareTo(s2)); Arrays.sort(players, sortByName); // 3.也可以采用如下形式: Arrays.sort(players, (String s1, String s2) -> (s1.compareTo(s2)));

3.方法引用 3.1.概述
在学习lambda表达式之后,我们通常使用lambda表达式来创建匿名方法。然而,有时候我们仅仅是调用了一个已存在的方法。如下:
Arrays.sort(stringsArray,(s1,s2)->s1.compareToIgnoreCase(s2));

在Java8中,我们可以直接通过方法引用来简写lambda表达式中已经存在的方法。
Arrays.sort(stringsArray, String::compareToIgnoreCase);

这种特性就叫做方法引用(Method Reference)。
3.2.定义
方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用会创建函数式接口的一个实例。
当Lambda表达式中只是执行一个方法调用时,不用Lambda表达式,直接通过方法引用的形式可读性更高一些。方法引用是一种更简洁易懂的Lambda表达式。
注意方法引用是一个Lambda表达式,其中方法引用的操作符是双冒号"::"
3.3.示例
首先定义一个包含了各种类型方法的类:
package com.bsx.test.lambda; /** * @Description: 定义一个包含了各种类型方法的类 * @author: ztd * @date 2019/8/19 上午11:30 */ public class DoubleColon { public static void printStr(String str) { System.out.println("printStr : " + str); }public void toUpper() { System.out.println("toUpper: " + this.toString()); }public void toLower(String str) { System.out.println("toLower: " + str); }public int toInt(String str) { System.out.println("toInt: " + str); return 1; }public void printInteger(Integer i) { System.out.println("printInteger: " + i); } }

写一个测试类
package com.bsx.test.lambdatest; import com.bsx.test.entity.Person; import com.bsx.test.lambda.DoubleColon; import org.junit.Test; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; /** * @Description: * @author: ztd * @date 2019/8/19 上午11:37 */ public class DoubleColonTest { @Test public void testColon() { // =========静态方法========== // 静态方法因为jvm已有对象,直接接收入参。 Consumer> printStrConsumer = DoubleColon::printStr; printStrConsumer.accept("printStrConsumer"); // =========非静态方法==========// 方法参数个数=函数式接口参数个数,通过【new 类的实例::方法名】引用 // 使用的时候,直接传入需要的参数即可 Consumer toPrintConsumer = new DoubleColon()::printInteger; toPrintConsumer.accept(123); // 方法参数个数=函数式接口参数个数-1,通过【类的实例::方法名】引用 // 使用的时候,传入的第一个参数是类的实例,后面是方法的参数 Consumer toUpperConsumer = DoubleColon::toUpper; toUpperConsumer.accept(new DoubleColon()); BiConsumer toLowerConsumer = DoubleColon::toLower; DoubleColon doubleColon = new DoubleColon(); toLowerConsumer.accept(doubleColon, "toLowerConsumer"); BiFunction toIntFunction = DoubleColon::toInt; int i = toIntFunction.apply(new DoubleColon(), "toInt"); System.out.println(i); } }

你已经看到测试类里面针对不同类型的方法,方法引用的定义方式并不一样,使用方式也不一样,我们的困惑就在于为什么要这么定义,为什么这么使用?这样我们才能在使用jdk1.8里面的各种Function和stream的时候变得随心所欲。
3.4.为什么这么定义这么用?
首先我们需要明确的一点是,函数式接口也是接口,只是它里面只有一个抽象方法,在使用的时候跟其他的接口并没有本质区别,区别只在于使用的方式更简洁。要实现它同样需要按照普通接口的规范去使用,比如要保证实现方法和接口的输入输出参数完全对应。
3.4.1.静态方法引用
这个很容易理解,静态方法因为jvm已有对象,直接接收入参函数的定义跟接口完全一致。
// 定义 public static void printStr(String str) { System.out.println("printStr : " + str); } // 使用 Consumer> printStrConsumer = DoubleColon::printStr; printStrConsumer.accept("printStrConsumer");

3.4.2.非静态方法
// 函数定义 public void printInteger(Integer i) { System.out.println("printInteger: " + i); } public void toUpper() { System.out.println("toUpper: " + this.toString()); } // 函数使用 Consumer toPrintConsumer = new DoubleColon()::printInteger; toPrintConsumer.accept(123); Consumer toUpperConsumer = DoubleColon::toUpper; toUpperConsumer.accept(new DoubleColon());

非静态方法的函数引用定义和使用就有点让人很困惑了,明明函数式接口里面是2参数,可是非静态方法里面是1个参数,这个是怎么实现的呢?
其实这个不一致是有要求的,就是函数式接口的参数个数-非静态方法参数个数=0或者1,我这里叫它为参数差,这个参数差取值范围不能变,如果大于1或者小于0都会报错。
接下来我们分别来讨论0和1的情况。
【1_java|lambda表达式和方法引用】参数差=0:方法参数个数相等,那么直接通过一个类的实例来调用这个方法即可,因此方法引用就是【实例::方法名】,使用的时候也是直接传递所需要的参数即可。
参数差=1:说明非静态方法少一个参数,这是就不能保证接口参数和实现方法参一一对应了,这很明显有问题。这时候我们通过【类::方法名】来定义(这种定义方式也是jdk的规定,记住就好)。因为这个方法不是静态方法,定义的时候也没有给它传递类的实例,所以我们需要在使用的时候给这个方法传递一个宿主(类的实例),这个宿主永远是接口的第一个参数,我成为宿主优先原则,因此就会出现下面的这种定义和使用的方式:
// 方法DoubleColon::toUpper没有参数,因此Consumer的参数就是DoubleColon的一个实例 // 因此定义的时候Consumer的泛型类型就是DoubleColon // 使用的时候只需要传递一个DoubleColon的实例即可 Consumer toUpperConsumer = DoubleColon::toUpper; toUpperConsumer.accept(new DoubleColon());

一旦我们知道了参数差=0或1、宿主优先原则之后,我们就能够理解jdk1.8以后的各种Function,stream的使用方式为什么是这样那样了。

    推荐阅读