持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第27天,点击查看活动详情
jdk1.8以后推出了很多的新特性,比如1.default关键字,2.lambda表达式,3.函数式接口,4.方法引用,5.Date Api,stream等。这里面default和Date Api的用法比较容易理解,这里不做更多阐述。今天我们要说的是 lambda表达式,函数式接口,方法引用以及stream这些比较难以理解、困惑的部分。这些的困惑包括:lambda表达式如何使用,以及为什么这么使用。函数式接口如何使用方法引用。以及stream的基本用法
一、函数式接口
1.定义
一个接口中有且只有一个抽象方法,不包括 equals这类在object中已经定义的方法,为了明确表示一个接口是函数式接口,防止别人在接口中添加其他抽象方法,我们可以给接口定义的时候添加一个添加一个@FunctionalInterface注解
2.示例如下
// 自定义函数式接口
package com.bsx.test.lambda;
import java.io.Serializable;
@FunctionalInterface
public interface IGetter<T> extends Serializable {
Object get(T source);
}
3.Java内置四大核心函数式接口
1、 消费型接口:Consumer void accept(T t) 接收一个T类型的参数,不返回值
2、 供给型接口:Supplier T get() 不接受参数,返回一个T类型的结果
3、 函数型接口:Function R apply(T t) 接收一个T类型的参数,返回一个R类型的结果
4、 断定型接口:Predicate boolean test(T t) 接收一个T类型的参数,返回一个boolean类型的结果
Function: 表示一个方法接收参数并返回结果
接收单个参数
| Interface | functional method | 说明 | | -------------------- | -------------------------------------------- | -------------------------------------------------------- | | Function | R apply(T t) | 接收参数类型为T,返回参数类型为R | | IntFunction | R apply(int value) | 以下三个接口,指定了接收参数类型,返回参数类型为泛型R | | LongFunction | R apply(long value) | | | Double | R apply(double value) | | | ToIntFunction | int applyAsInt(T value) | 以下三个接口,指定了返回参数类型,接收参数类型为泛型T | | ToLongFunction | long applyAsLong(T value) | | | ToDoubleFunction | double applyAsDouble(T value) | | | IntToLongFunction | long applyAsLong(int value) | 以下六个接口,既指定了接收参数类型,也指定了返回参数类型 | | IntToDoubleFunction | double applyAsLong(int value) | | | LongToIntFunction | int applyAsLong(long value) | | | LongToDoubleFunction | double applyAsLong(long value) | | | DoubleToIntFunction | int applyAsLong(double value) | | | DoubleToLongFunction | long applyAsLong(double value) | | | UnaryOperator | T apply(T t) | 特殊的Function,接收参数类型和返回参数类型一样 | | IntUnaryOperator | int applyAsInt(int left, int right) | 以下三个接口,指定了接收参数和返回参数类型,并且都一样 | | LongUnaryOperator | long applyAsInt(long left, long right) | | | DoubleUnaryOperator | double applyAsInt(double left, double right) | |
接收两个参数
| interface | functional method | 说明 | | ----------------------- | -------------------------------------------- | ------------------------------------------------------------ | | BiFunction | R apply(T t, U u) | 接收两个参数的Function | | ToIntBiFunction | int applyAsInt(T t, U u) | 以下三个接口,指定了返回参数类型,接收参数类型分别为泛型T, U | | ToLongBiFunction | long applyAsLong(T t, U u) | | | ToDoubleBiFunction | double appleyAsDouble(T t, U u) | | | BinaryOperator | T apply(T t, T u) | 特殊的BiFunction, 接收参数和返回参数类型一样 | | IntBinaryOperator | int applyAsInt(int left, int right) | | | LongBinaryOperator | long applyAsInt(long left, long right) | | | DoubleBinaryOperator | double applyAsInt(double left, double right) | |
Consumer: 表示一个方法接收参数但不产生返回值
接收一个参数
| interface | functional method | 说明 | | -------------- | ------------------------- | ---------------------------------- | | Consumer | void accept(T t) | 接收一个泛型参数,无返回值 | | IntConsumer | void accept(int value) | 以下三个类,接收一个指定类型的参数 | | LongConsumer | void accept(long value) | | | DoubleConsumer | void accept(double value) | |
接收两个参数
| interface | functional method | 说明 | | -------------------- | ------------------------------ | ------------------------------------------------ | | BiConsumer | void accept(T t, U u) | 接收两个泛型参数 | | ObjIntConsumer | void accept(T t, int value) | 以下三个类,接收一个泛型参数,一个指定类型的参数 | | ObjLongConsumer | void accept(T t, long value) | | | ObjDoubleConsumer | void accept(T t, double value) | |
Supplier: 返回一个结果,并不要求每次调用都返回一个新的或者独一的结果
| interface | functional method | 说明 | | --------------- | ---------------------- | -------------------------- | | Supplier | T get() | 返回类型为泛型T | | BooleanSupplier | boolean getAsBoolean() | 以下三个接口,返回指定类型 | | IntSupplier | int getAsInt() | | | LongSupplier | long getAsLong() | | | DoubleSupplier | double getAsDouble() | |
Predicate: 根据接收参数进行断言,返回boolean类型
| interface | functional method | 说明 | | ---------------- | -------------------------- | -------------------------------- | | Predicate | boolean test(T t) | 接收一个泛型参数 | | IntPredicate | boolean test(int value) | 以下三个接口,接收指定类型的参数 | | LongPredicate | boolean test(long value) | | | DoublePredicate | boolean test(double value) | | | BiPredicate | boolean test(T t, U u) | 接收两个泛型参数,分别为T,U |
二、lambda
1.概述
λ表达式有三部分组成:1.参数列表,2.箭头(->),3.一个表达式或语句块,其中表达式是指的是一句代码,语句块是用大括号"{}"包起来的一系列代码,而λ本身必须是函数接口才能使用λ表达式。lambda 语法本质上是一个匿名方法是【语法糖】,由编译器推断并帮助你转换包装为常规代码。说白了lambad表达式就是把函数定义从原来的标准定义方式给简化了,这是因为编译器可以根据表达式内容来推断入参、出参。因此使用lambda可以使用更少的代码来实现相同功能。左边 lambda形参列表的参数类型可以省略(类型推断),如果lambda形参列表只有一个参数,其参数类型和()可以省略;右边 lambda体应该使用一对{}包裹;如果只有一条执行语句(可能是return语句),其{}和return都可以省略
2.示例如下
// 以前的循环方式
String[] players = {"Rafael Nadal", "Novak Djokovic};
for (String player : players) {
System.out.print(player + ";");
}
// 使用 lambda 表达式以及函数操作(functional operation)
players.forEach((player) -> System.out.print(player + ";"));
// 排序
// 1.使用匿名内部类根据 name 排序 players
Arrays.sort(players, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return (s1.compareTo(s2));
}
});
// 2.使用 lambda expression 排序 players
Arrays.sort(players, (s1,s2) -> s1.compareTo(s2)));
// 3.也可以采用如下形式:
Arrays.sort(players, String::compareTo));
3.作用域
Lambda表达式不会从超类(supertype)中继承任何变量名,也不会引入一个新的作用域。Lambda表达式基于词法作用域,也就是说lambda表达式函数体里面的变量和它外部环境的变量具有相同的语义(也包括lambda表达式的形式参数)。此外,this关键字及其引用,在Lambda表达式内部和外部也拥有相同的语义
4. λ表达式的使用
4.1 λ表达式用在何处
λ表达式主要用于替换以前广泛使用的内部匿名类,各种回调,比如事件响应器、传入Thread类的Runnable等。看下面的例子:
Thread oldSchool = new Thread( new Runnable () {
@Override
public void run() {
System.out.println("This is from an anonymous class.");
}
} );
Thread gaoDuanDaQiShangDangCi = new Thread( () -> {
System.out.println("This is from an anonymous method (lambda exp).");
} );
注意第二个线程里的λ表达式,你并不需要显式地把它转成一个Runnable,因为Java能根据上下文自动推断出来:一个Thread的构造函数接受一个Runnable参数,而传入的λ表达式正好符合其run()函数,所以Java编译器推断它为Runnable。从形式上看,λ表达式只是为你节省了几行代码。但将λ表达式引入Java的动机并不仅仅为此。Java8有一个短期目标和一个长期目标。短期目标是:配合“集合类批处理操作”的内部迭代和并行处理(下面将要讲到);长期目标是将Java向函数式编程语言这个方向引导(并不是要完全变成一门函数式编程语言,只是让它有更多的函数式编程语言的特性),也正是由于这个原因,Oracle并没有简单地使用内部类去实现λ表达式,而是使用了一种更动态、更灵活、易于将来扩展和改变的策略(invokedynamic)
4.2 λ表达式与集合类批处理操作(或者叫块操作)
上文提到了集合类的批处理操作。这是Java8的另一个重要特性,它与λ表达式的配合使用乃是Java8的最主要特性。集合类的批处理操作API的目的是实现集合类的“内部迭代”,并期望充分利用现代多核CPU进行并行计算。Java8之前集合类的迭代(Iteration)都是外部的,即客户代码。而内部迭代意味着改由Java类库来进行迭代,而不是客户代码。例如:
for(Object o: list) { // 外部迭代
System.out.println(o);
}
// 可以写成:
list.forEach(o -> {System.out.println(o);}); //forEach函数实现内部迭代
集合类(包括List)现在都有一个forEach方法,对元素进行迭代(遍历),所以我们不需要再写for循环了。forEach方法接受一个函数接口Consumer做参数,所以可以使用λ表达式。
这种内部迭代方法广泛存在于各种语言,如C++的STL算法库、Python、ruby、Scala等。
Java8为集合类引入了另一个重要概念:流(stream)。一个流通常以一个集合类实例为其数据源,然后在其上定义各种操作。流的API设计使用了管道(pipelines)模式。对流的一次操作会返回另一个流。如同IO的API或者StringBuffer的append方法那样,从而多个不同的操作可以在一个语句里串起来。看下面的例子:
List<Shape> shapes = ...
shapes.stream()
.filter(s -> s.getColor() == BLUE)
.forEach(s -> s.setColor(RED));
首先调用stream方法,以集合类对象shapes里面的元素为数据源,生成一个流。然后在这个流上调用filter方法,挑出蓝色的,返回另一个流。最后调用forEach方法将这些蓝色的物体喷成红色。(forEach方法不再返回流,而是一个终端方法,类似于StringBuffer在调用若干append之后的那个toString)
filter方法的参数是Predicate类型,forEach方法的参数是Consumer类型,它们都是函数接口,所以可以使用λ表达式。
来看更多的例子。下面是典型的大数据处理方法,Filter-Map-Reduce:
// 给出一个String类型的数组,找出其中所有不重复的素数
public void distinctPrimary(String... numbers) {
List<String> l = Arrays.asList(numbers);
List<Integer> r = l.stream()
.map(e -> new Integer(e))
.filter(e -> Primes.isPrime(e))
.distinct()
.collect(Collectors.toList());
System.out.println("distinctPrimary result is: " + r);
}
4.3 λ表达式的更多用法
嵌套的λ表达式
Callable<Runnable> c1 = () -> () -> { System.out.println("Nested lambda"); };
c1.call().run();
用在条件表达式中
Callable<Integer> c2 = true ? (() -> 42) : (() -> 24);
System.out.println(c2.call());
定义一个递归函数,注意须用this限定
protected UnaryOperator<Integer> factorial = i -> i == 0 ? 1 : i * this.factorial.apply( i - 1 );
System.out.println(factorial.apply(3));
在Java中,随声明随调用的方式是不行的,比如下面这样,声明了一个λ表达式(x, y) -> x + y,同时企图通过传入实参(2, 3)来调用它,这在C++中是可以的,但Java中不行。Java的λ表达式只能用作赋值、传参、返回值等
int five = ( (x, y) -> x + y ) (2, 3); // ERROR! try to call a lambda in-place
三、方法引用
1.概述
在学习lambda表达式之后,我们通常使用lambda表达式来创建匿名方法。然而,有时候我们仅仅是调用了一个已存在的方法
Arrays.sort(stringsArray,(s1,s2)->s1.compareToIgnoreCase(s2));
在Java8中,我们可以直接通过方法引用来简写lambda表达式中已经存在的方法,这种特性就叫做方法引用(Method Reference)
Arrays.sort(stringsArray, String::compareToIgnoreCase);
在Java 8之前只能进行值传递,方法是不能传递的。如果你想调用一个方法你必须先获取到它所在的类的实例,然后再通过实例去调用这个方法,但是Java 8新增了方法引用这个新特性可以让你直接把方法当做值来传递。
1.1 下面这段代码代码的作用是遍历获取目录下所有的文件和目录,并且还加了一个筛选条件,只筛选出不隐藏的文件和目录,这里我们其实只是想调用FileFilter中的accept方法来进行筛选,但是我们需要先创建FileFilter的匿名对象,然后重写整个accept方法,这样我们才调用到了这个方法,其中只有第三行代码是会有变化的,其他的代码都是固定的,但是我们每次还是要把其他固定的模板代码重新写一遍。
File[] hiddenFiles = new File("F:\test").listFiles(new FileFilter() {
public boolean accept(File file) {
return !file.isHidden();
}
});
1.2 现在Java 8中的方法引用就解决了这个问题,让我们看下列的代码,我们发现匿名类和重写方法的步骤都已经没有了,上述代码的本质其实就是调用传进来的File对象的isHidden方法,现在File:: isHiden
这个写法就是和上面的代码是同样的作用,但是代码精简了很多,那些无用的冗余代码都不见了。
File[] hiddenFiles = new File("F:\test").listFiles(File::isHidden);
1.3 我们从源码来看看listFiles
方法做了什么操作,而这两种写法又有什么不同。
首先listFiles方法接受了一个FileFilter类型的对象,list
方法是获取所有的文件,files是用来存储筛选之后的元素,循环所有获得到的文件数组,然后调用FileFilter中的accept方法来进行条件筛选,放入files后返回。
public File[] listFiles(FileFilter filter) {
String ss[] = list();
if (ss == null) return null;
ArrayList<File> files = new ArrayList<>();
for (String s : ss) {
File f = new File(s, this);
if ((filter == null) || filter.accept(f))
files.add(f);
}
return files.toArray(new File[files.size()]);
}
再看看FileFilter对象是什么,发现它是一个接口,所以Java 8之前的写法都是写了个匿名对象来实现这个接口,重写它的accept方法。看到这里其实很明显了,这就是一个策略模式的应用。而方法引用就是让我们直接把需要在accept
方法里调用的方法传递进去,不需要像以前一样来个全家桶写一堆固定模板。
@FunctionalInterface
public interface FileFilter {
boolean accept(File pathname);
}
1.4 下面的图介绍了Java 8之前和之后这段代码的逻辑流程,在Java 8之前是需要先创建FileFilter匿名对象然后再调用File.listFiles
方法,而现在只需要File::isHiden
写法就可以达到同样的目的,其实它的含义就是创建了一个方法引用,所以你可以通过传递引用来传递这个方法,就好像你new了一个对象的引用,然后你把这个引用传递到别的地方,你就可以调用这个对象里的属性和方法是一样的道理。
\
\
2.定义
方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用会创建函数式接口的一个实例。当Lambda表达式中只是执行一个方法调用时,不用Lambda表达式,直接通过方法引用的形式可读性更高一些。方法引用是一种更简洁易懂的Lambda表达式。
3.示例
package com.bsx.test.lambda;
/**
* @Description: 定义一个包含了各种类型方法的类
* @author: lsh
* @date 2021/8/29 上午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: lsh
* @date 2019/8/29 上午11:37
*/
public class DoubleColonTest {
@Test
public void testColon() {
// =========静态方法==========
// 静态方法因为jvm已有对象,直接接收入参。
Consumer<String> printStrConsumer = DoubleColon::printStr;
printStrConsumer.accept("printStrConsumer");
// =========非静态方法==========
// 方法参数个数=函数式接口参数个数,通过【new 类的实例::方法名】引用
// 使用的时候,直接传入需要的参数即可
Consumer<Integer> toPrintConsumer = new DoubleColon()::printInteger;
toPrintConsumer.accept(123);
// 方法参数个数=函数式接口参数个数-1,通过【类的实例::方法名】引用
// 使用的时候,传入的第一个参数是类的实例,后面是方法的参数
Consumer<DoubleColon> toUpperConsumer = DoubleColon::toUpper;
toUpperConsumer.accept(new DoubleColon());
BiConsumer<DoubleColon, String> toLowerConsumer = DoubleColon::toLower;
DoubleColon doubleColon = new DoubleColon();
toLowerConsumer.accept(doubleColon, "toLowerConsumer");
}
}
你已经看到测试类里面针对不同类型的方法,方法引用的定义方式并不一样,使用方式也不一样,我们的困惑就在于为什么要这么定义,为什么这么使用?这样我们才能在使用jdk1.8里面的各种Function和stream的时候变得随心所欲
4.为什么这么定义这么用?
首先我们需要明确的一点是,函数式接口也是接口,只是它里面只有一个抽象方法,在使用的时候跟其他的接口并没有本质区别,区别只在于使用的方式更简洁。要实现它同样需要按照普通接口的规范去使用,比如要保证实现方法和接口的输入输出参数完全对应
4.1 静态方法引用
这个很容易理解,静态方法因为jvm已有对象,直接接收入参函数的定义跟接口完全一致
// 定义
public static void printStr(String str) {
System.out.println("printStr : " + str);
}
// 使用
Consumer<String> printStrConsumer = DoubleColon::printStr;
printStrConsumer.accept("printStrConsumer");
4.2 非静态方法
// 函数定义
public void printInteger(Integer i) {
System.out.println("printInteger: " + i);
}
public void toUpper() {
System.out.println("toUpper: " + this.toString());
}
// 函数使用
Consumer<Integer> toPrintConsumer = new DoubleColon()::printInteger;
toPrintConsumer.accept(123);
Consumer<DoubleColon> toUpperConsumer = DoubleColon::toUpper;
toUpperConsumer.accept(new DoubleColon());
非静态方法的函数引用定义和使用就有点让人很困惑了,明明函数式接口里面是2参数,可是非静态方法里面是1个参数,这个是怎么实现的呢?其实这个不一致是有要求的,就是函数式接口的参数个数-非静态方法参数个数=0或者1,我这里叫它为参数差,这个参数差取值范围不能变,如果大于1或者小于0都会报错。接下来我们分别来讨论0和1的情况:
参数差=0: 方法参数个数相等,那么直接通过一个类的实例来调用这个方法即可,因此方法引用就是【实例::方法名】,使用的时候也是直接传递所需要的参数即可。
参数差=1: 说明非静态方法少一个参数,这是就不能保证接口参数和实现方法参一一对应了,这很明显有问题。这时候我们通过【类::方法名】来定义(这种定义方式也是jdk的规定,记住就好)。因为这个方法不是静态方法,定义的时候也没有给它传递类的实例,所以我们需要在使用的时候给这个方法传递一个宿主(类的实例),这个宿主永远是接口的第一个参数,因此就会出现下面的这种定义和使用的方式
// 方法DoubleColon::toUpper没有参数,因此Consumer的参数就是DoubleColon的一个实例
// 因此定义的时候Consumer的泛型类型就是DoubleColon
// 使用的时候只需要传递一个DoubleColon的实例即可
Consumer<DoubleColon> toUpperConsumer = DoubleColon::toUpper;
toUpperConsumer.accept(new DoubleColon());
4.3 构造器引用
与函数式接口相结合,自动与函数式接口中方法兼容,构造器参数列表要与接口中抽象方法的参数列表一致! ,语法格式:类名 :: new
// 构造器引用
Supplier<Student> studentSupplier = DoubleColon::new;
System.out.println(studentSupplier.get());