函数式接口
概念
函数式接口 –> 有且仅有一个抽象方法的接口。
函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。
格式
只要确保接口中有且仅有一个抽象方法即可:
修饰符 interface 接口名称 {
public abstract 返回值类型 方法名称(可选参数信息);
// 其他非抽象方法内容
}
由于接口当中抽象方法的public abstract是可以省略的,所以定义一个函数式接口很简单:
public interface MyFunctionalInterface {
void myMethod();
}
@FunctionalInterface
与@Override注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解:@FunctionalInterface。该注解可用于一个接口的定义上:
@FunctionalInterface
public interface MyFunctionalInterface {
void myMethod();
}
使用注解,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。
注意: 即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
自定义函数式接口
对于刚刚定义好的MyFunctionalInterface函数式接口,典型使用场景就是作为方法的参数:
public class Demo09FunctionalInterface {
// 使用自定义的函数式接口作为方法参数
private static void doSomething(MyFunctionalInterface inter) {
inter.myMethod(); // 调用自定义的函数式接口方法
}
public static void main(String[] args) {
// 调用使用函数式接口的方法
doSomething(() -> System.out.println(“Lambda执行啦!”));
}
}
练习:自定义函数式接口(无参无返回)
题目
请定义一个函数式接口Eatable,内含抽象eat方法,没有参数或返回值。使用该接口作为方法的参数,并进而通过Lambda来使用它。
解答
函数式接口的定义:
@FunctionalInterface
public interface Eatable {
void eat();
}
应用场景代码:
public class DemoLambdaEatable {
private static void keepAlive(Eatable human) {
human.eat();
}
public static void main(String[] args) {
keepAlive(() -> System.out.println(“吃饭饭!”));
}
}
练习:自定义函数式接口(有参有返回)
题目
请定义一个函数式接口Sumable,内含抽象sum方法,可以将两个int数字相加返回int结果。使用该接口作为方法的参数,并进而通过Lambda来使用它。
解答
函数式接口的定义:
@FunctionalInterface
public interface Sumable {
int sum(int a, int b);
}
应用场景代码:
public class DemoLambdaSumable {
private static void showSum(int x, int y, Sumable sumCalculator) {
System.out.println(sumCalculator.sum(x, y));
}
public static void main(String[] args) {
showSum(10, 20, (m, n) -> m + n);
}
}
函数式编程
Lambda的延迟执行
有些场景的代码执行后,结果不一定会被使用,从而造成性能浪费。而Lambda表达式是延迟执行的,这正好可以作为解决方案,提升性能。
性能浪费的日志案例
一种典型的场景就是对参数进行有条件使用,例如对日志消息进行拼接后,在满足条件的情况下进行打印输出:
public class Demo01Logger {
private static void log(int level, String msg) {
if (level == 1) {
System.out.println(msg);
}
}
public static void main(String[] args) {
String msgA = “Hello”;
String msgB = “World”;
String msgC = “Java”;
log(1, msgA + msgB + msgC);
}
}
步骤
1.将三个字符串拼接
2,传递参数
3.method方法中进行判断
这段代码存在问题:无论级别是否满足要求,作为log方法的第二个参数,三个字符串一定会首先被拼接并传入方法内,然后才会进行级别判断。如果级别不符合要求,那么字符串的拼接操作就白做了,存在性能浪费。
备注:SLF4J是应用非常广泛的日志框架,它在记录日志是为了解决这种性能浪费的问题,并不推荐首先进行字符串的拼接,而是将字符串的若干部分作为可变参数传入方法中,仅在日志级别满足要求的情况下才会进行字符串拼接。例如:LOGGER.debug(“变量{}的取值为{}。”, “os”, “macOS”),其中的大括号{}为占位符。如果满足日志级别要求,则会将“os”和“macOS”两个字符串依次拼接到大括号的位置;否则不会进行字符串拼接。这也是一种可行解决方案,但Lambda可以做到更好。
体验Lambda的更优写法
使用Lambda必然需要一个函数式接口:
@FunctionalInterface
public interface MessageBuilder {
String buildMessage();
}
然后对log方法进行改造:
public class Demo02LoggerLambda {
private static void log(int level, MessageBuilder builder) {
if (level == 1) {
System.out.println(builder.buildMessage());
}
}
public static void main(String[] args) {
String msgA = “Hello”;
String msgB = “World”;
String msgC = “Java”;
log(1, () -> msgA + msgB + msgC );
}
}
这样一来,只有当级别满足要求的时候,才会进行三个字符串的拼接;否则三个字符串将不会进行拼接。
lambda表达式会在接口调用它的抽象方法的时候执行
证明Lambda的延迟
下面的代码可以通过结果进行验证:
public class Demo03LoggerDelay {
private static void log(int level, MessageBuilder builder) {
if (level == 1) {
System.out.println(builder.buildMessage());
}
}
public static void main(String[] args) {
String msgA = “Hello”;
String msgB = “World”;
String msgC = “Java”;
log(2, () -> {
System.out.println(“Lambda执行!”);
return msgA + msgB + msgC;
});
}
}
推导与省略
从结果中可以看出,在不符合级别要求的情况下,Lambda将不会执行。从而达到节省性能的效果。
扩展:实际上使用内部类也可以达到同样的效果,只是将代码操作延迟到了另外一个对象当中通过调用方法来完成。而是否调用其所在方法是在条件判断之后才执行的。
使用Lambda作为参数和返回值
如果抛开实现原理不说,Java中的Lambda表达式可以被当作是匿名内部类的替代品。如果方法的参数是一个函数式接口类型,那么就可以使用Lambda表达式进行替代。使用Lambda表达式作为方法参数,其实就是使用函数式接口作为方法参数。
例如java.lang.Runnable接口就是一个函数式接口,假设有一个startThread方法使用该接口作为参数,那么就可以使用Lambda进行传参。这种情况其实和Thread类的构造方法参数为Runnable没有本质区别。
public class Demo04Runnable {
private static void startThread(Runnable task) {
new Thread(task).start();
}
public static void main(String[] args) {
startThread(() -> System.out.println(“线程任务执行!”));
}
}
类似地,如果一个方法的返回值类型是一个函数式接口,那么就可以直接返回一个Lambda表达式。当需要通过一个方法来获取一个java.util.Comparator接口类型的对象作为排序器时:
import java.util.Arrays;
import java.util.Comparator;
public class Demo06Comparator {
private static Comparator newComparator() {
return (a, b) -> b.length() - a.length();
}
public static void main(String[] args) {
String[] array = { “abc”, “ab”, “abcd” };
System.out.println(Arrays.toString(array));
Arrays.sort(array, newComparator());
System.out.println(Arrays.toString(array));
}
}
其中直接return一个Lambda表达式即可。
练习:自定义Lambda参数和返回值
题目
请自定义一个函数式接口MySupplier,含有无参数的抽象方法get得到Object类型的返回值。并使用该函数式接口分别作为方法的参数和返回值。
解答
函数式接口MySupplier如:
@FunctionalInterface
public interface MySupplier {
Object get();
}
使用该接口作为方法的参数,并且在传递参数时将实际参数写成Lambda:
public class Demo05MySupplier {
private static void printParam(MySupplier supplier) {
System.out.println(supplier.get());
}
public static void main(String[] args) {
printParam(() -> “Hello”);
}
}
使用该接口作为方法的参数,也很简单:
public class Demo07MySupplier {
private static MySupplier getData() {
return () -> “Hello”;
}
private static void printData(MySupplier supplier) {
System.out.println(supplier.get());
}
public static void main(String[] args) {
printData(getData());
}
}
其中main方法不再自己指定Lambda表达式,而是通过调用一个getData方法来获取Lambda的内容。
JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在java.util.function包中被提供。前文的MySupplier接口就是在模拟一个函数式接口:java.util.function.Supplier。其实还有很多,下面是最简单的几个接口及使用示例。
Supplier接口
java.util.function.Supplier接口仅包含一个无参的方法:T get()。用来获取一个泛型参
数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。
import java.util.function.Supplier;
public class Demo08Supplier {
private static String getString(Supplier function) {
return function.get();
}
public static void main(String[] args) {
String msgA = “Hello”;
String msgB = “World”;
System.out.println(getString(() -> msgA + msgB));
}
}
备注:其实这个接口在前面的练习中已经模拟过了。
练习:求数组元素最大值
题目
使用Supplier接口作为方法参数类型,通过Lambda表达式求出int数组中的最大值。提示:接口的泛型请使用java.lang.Integer类。
解答
import java.util.function.Supplier;
public class DemoIntArray {
public static void main(String[] args) {
int[] array = { 10, 20, 100, 30, 40, 50 };
printMax(() -> {
int max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
return max;
});
}
private static void printMax(Supplier supplier) {
int max = supplier.get();
System.out.println(max);
}
}
Consumer接口
java.util.function.Consumer接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。
抽象方法:accept
Consumer接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据。基本使用如:
import java.util.function.Consumer;
public class Demo09Consumer {
private static void consumeString(Consumer function) {
function.accept(“Hello”);
}
public static void main(String[] args) {
consumeString(s -> System.out.println(s));
consumeString(System.out::println);
}
}
当然,更好的写法是使用方法引用。
默认方法:andThen
如果一个方法的参数和返回值全都是Consumer类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是Consumer接口中的default方法andThen。下面是JDK的源代码:
default Consumer andThen(Consumer
练习:格式化打印信息
题目
下面的字符串数组当中存有多条信息,请按照格式“姓名:XX。性别:XX。”的格式将信息打印出来。要求将打印姓名的动作作为第一个Consumer接口的Lambda实例,将打印性别的动作作为第二个Consumer接口的Lambda实例,将两个Consumer接口按照顺序“拼接”到一起。
public static void main(String[] args) {
String[] array = { “迪丽热巴,女”, “古力娜扎,女”, “马尔扎哈,男” };
}
解答
import java.util.function.Consumer;
public class DemoConsumer {
public static void main(String[] args) {
String[] array = { “迪丽热巴,女”, “古力娜扎,女”, “马尔扎哈,男” };
printInfo(s -> System.out.print(“姓名:” + s.split(“,”)[0]),
s -> System.out.println(“。性别:” + s.split(“,”)[1] + “。”),
array);
}
private static void printInfo(Consumer one, Consumer two, String[] array) {
for (String info : array) {
one.andThen(two).accept(info); // 姓名:迪丽热巴。性别:女。
}
}
}
Predicate接口
有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate接口。
抽象方法:test
Predicate接口中包含一个抽象方法:boolean test(T t)。
用来验证一个数据是否合法,如果合法,返回true.
用于条件判断的场景:
import java.util.function.Predicate;
public class Demo15PredicateTest {
private static void method(Predicate predicate) {
boolean veryLong = predicate.test(“HelloWorld”);
System.out.println(“字符串很长吗:” + veryLong);
}
public static void main(String[] args) {
method(s -> s.length() > 5);
}
}
条件判断的标准是传入的Lambda表达式逻辑,只要字符串长度大于5则认为很长。
默认方法:and
既然是条件判断,就会存在与、或、非三种常见的逻辑关系。
其中将两个Predicate条件使用“与”逻辑连接起来实现“并且”的效果时,可以使用default方法and。
and –> 具有&&特性,有false即false
其JDK源码为:
default Predicate and(Predicate
默认方法:or
与and的“与”类似,默认方法or实现逻辑关系中的“或”。
其中之一成立,结果必成立
JDK源码为:
default Predicate or(Predicate
默认方法:negate
“与”、“或”已经了解了,剩下的“非”(取反)也会简单。
默认方法negate的JDK源代码为:
default Predicate negate() {
return (t) -> !test(t);
}
从实现中很容易看出,它是执行了test方法之后,对结果boolean值进行“!”取反而已。一定要在test方法调用之前调用negate方法,正如and和or方法一样:
import java.util.function.Predicate;
public class Demo17PredicateNegate {
private static void method(Predicate predicate) {
boolean veryLong = predicate.negate().test(“HelloWorld”);
System.out.println(“字符串很长吗:” + veryLong);
}
public static void main(String[] args) {
method(s -> s.length() < 5);
}
}
练习:集合信息筛选
题目
数组当中有多条“姓名+性别”的信息如下,请通过Predicate接口的拼装将符合要求的字符串筛选到集合ArrayList中,需要同时满足两个条件:
必须为女生;
姓名为4个字。
public class DemoPredicate {
public static void main(String[] args) {
String[] array = { “迪丽热巴,女”, “古力娜扎,女”, “马尔扎哈,男”, “赵丽颖,女” };
}
}
解答
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public class DemoPredicate {
public static void main(String[] args) {
String[] array = { “迪丽热巴,女”, “古力娜扎,女”, “马尔扎哈,男”, “赵丽颖,女” };
List list = filter(array,
s -> “女”.equals(s.split(“,”)[1]),
s -> s.split(“,”)[0].length() == 3);
System.out.println(list);
}
private static List filter(String[] array, Predicate one,
Predicate two) {
List list = new ArrayList<>();
for (String info : array) {
if (one.and(two).test(info)) {
list.add(info);
}
}
return list;
}
}
Function接口
java.util.function.Function
抽象方法:apply
Function接口中最主要的抽象方法为:R apply(T t)
–> 根据类型T的参数获取类型R的结果。
使用的场景例如:将String类型转换为Integer类型。
import java.util.function.Function;
public class Demo11FunctionApply {
private static void method(Function
默认方法:andThen
Function接口中有一个默认的andThen方法,用来进行组合操作。
–> 将两个Function合并成一个,并且有先后顺序
JDK源代码如:
default Function
练习:自定义函数模型拼接
题目
请使用Function进行函数模型的拼接,按照顺序需要执行的多个函数操作为:
将字符串截取数字年龄部分,得到字符串;
将上一步的字符串转换成为int类型的数字;
将上一步的int数字累加100,得到结果int数字。
解答
import java.util.function.Function;
public class DemoFunction {
public static void main(String[] args) {
String str = “赵丽颖,20”;
int age = getAgeNum(str, s -> s.split(“,”)[1],
Integer::parseInt,
n -> n += 100);
System.out.println(age);
}
private static int getAgeNum(String str, Function
总结:延迟方法与终结方法
在上述学习到的多个常用函数式接口当中,方法可以分成两种:
延迟方法:只是在拼接Lambda函数模型的方法,并不立即执行得到结果。
终结方法:根据拼好的Lambda函数模型,立即执行得到结果值的方法。
通常情况下,这些常用的函数式接口中唯一的抽象方法为终结方法,而默认方法为延迟方法。
但这并不是绝对的。下面的表格中进行了方法分类的整理:
接口名称 方法名称 抽象/默认 延迟/终结
Supplier get 抽象 终结
Consumer accept 抽象 终结
andThen 默认 延迟
Predicate test 抽象 终结
and 默认 延迟
or 默认 延迟
negate 默认 延迟
Function apply 抽象 终结
andThen 默认 延迟
备注:JDK中更多内置的常用函数式接口,请参考java.util.function包的API文档。