[TOC]
第一章:函数式接口
1.1 函数式接口介绍
函数式接口在Java中是指:有且仅有一个抽象方法的接口。
函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。
从应用层面来讲,Java中的Lambda可以被当做是匿名内部类的“语法糖”,但是二者在原理上是不同的。
1.2 格式
只要确保接口中有且仅有一个抽象方法即可:
修饰符 interface 接口名称 {
public abstract 返回值类型 方法名称(可选参数信息);
// 其他非抽象方法内容
}
1.3 @FunctionalInterface注解
与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注 解可用于一个接口的定义上:
@FunctionalInterface
public interface MyFunctionalInterface {
void myMethod();
}
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要注 意的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
1.4 自定义函数式接口
对于刚刚定义好的 MyFunctionalInterface 函数式接口,典型使用场景就是作为方法的参数:
public static void show(MyFunctionalInterface func){
func.mythond();
}
public static void main(String[] args) {
// 传入匿名内部类
show(new MyFunctionalInterface() {
@Override
public void mythond() {
System.out.println("执行了");
}
});
// 简写Lambda表达式
show(()-> System.out.println("执行了"));
}
第二章:函数式编程
在兼顾面向对象特性的基础上,Java语言通过Lambda表达式与方法引用等,为开发者打开了函数式编程的大门。 下面我们做一个初探。
2.1 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);
}
}
这段代码存在问题:无论级别是否满足要求,作为 log 方法的第二个参数,三个字符串一定会首先被拼接并传入方 法内,然后才会进行级别判断。如果级别不符合要求,那么字符串的拼接操作就白做了,存在性能浪费。
备注:SLF4J是应用非常广泛的日志框架,它在记录日志时为了解决这种性能浪费的问题,并不推荐首先进行 字符串的拼接,而是将字符串的若干部分作为可变参数传入方法中,仅在日志级别满足要求的情况下才会进 行字符串拼接。例如: LOGGER.debug("变量{}的取值为{}。", "os", "macOS") ,其中的大括号 {} 为占位 符。如果满足日志级别要求,则会将“os”和“macOS”两个字符串依次拼接到大括号的位置;否则不会进行字 符串拼接。这也是一种可行解决方案,但Lambda可以做到更好。
体验Lambda的更优写法
使用Lambda必然需要一个函数式接口:
@FunctionalInterface
public interface MessageBuilder {
String message();
}
然后对log方法进行改造
public class Test01 {
private static void log(int level,MessageBuilder builder){
if(level==1){
String mes = builder.message();
System.out.println(mes);
}
}
public static void main(String[] args) {
String msgA = "Hello";
String msgB = "World";
String msgC = "Java";
log(1,()-> msgA+msgB +msgC);
}
}
这样一来,只有当级别满足要求的时候,才会进行三个字符串的拼接;否则三个字符串将不会进行拼接
2.2 使用Lambda作为参数和返回值
如果抛开实现原理不说,Java中的Lambda表达式可以被当作是匿名内部类的替代品。
如果方法的参数是一个函数式接口类型,那么就可以使用Lambda表达式进行替代。
使用Lambda表达式作为方法参数,其实就是使用函数式 接口作为方法参数。 例如 java.lang.Runnable 接口就是一个函数式接口,假设有一个 startThread 方法使用该接口作为参数,那么就 可以使用Lambda进行传参。这种情况其实和 Thread 类的构造方法参数为 Runnable 没有本质区别。
public static void startThead(Runnable run){
new Thread(run).start();
}
public static void main(String[] args) {
startThead(()-> System.out.println(Thread.currentThread().getName()));
}
类似地,如果一个方法的返回值类型是一个函数式接口,那么就可以直接返回一个Lambda表达式。当需要通过一 个方法来获取一个 java.util.Comparator 接口类型的对象作为排序器时,就可以调该方法获取。
private static Comparator newComparator() {
return (o1, o2) -> o1.length()-o2.length();
}
public static void main(String[] args) {
// 对字符串数组排序,按照字符长度排序
String[]arr = {"ab","b","abc","abcde","abcd","aaaaaa"};
//
Arrays.sort(arr);
Arrays.sort(arr,newComparator());
System.out.println(Arrays.toString(arr)); // [b, ab, abc, abcd, abcde, aaaaaa]
}
其中直接return一个Lambda表达式即可。
第三章:常用的函数式接口
JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在 java.util.function 包中被提供。 下面是最简单的几个接口及使用示例。
3.1 Supplier接口
java.util.function.Supplier 接口仅包含一个无参的方法: T get() 。用来获取一个泛型参数指定类型的对 象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象 数据
public static String show(Supplier sp){
return sp.get();
};
public static void main(String[] args) {
System.out.println(show(()->"你好Java"));
}
练习:求数组最大值
public static int getMax(Supplier sp){
return sp.get();
}
public static void main(String[] args) {
int[]arr={11,2,3,6,4,66,22,19};
int max = getMax(()->{
int maxValue = arr[0];
for (int i = 1; i < arr.length; i++) {
if(maxValue
3.2 Consumer接口
java.util.function.Consumer 接口则正好与Supplier接口相反,它不是生产一个数据,而是消费一个数据, 其数据类型由泛型决定。
-
抽象方法 :accept,Consumer 接口中包含抽象方法 void accept(T t) ,意为消费一个指定泛型的数据。基本使用如:
public static void printList(ArrayList
list, Consumer con){ con.accept(list); } public static void main(String[] args) { ArrayList list = new ArrayList<>(); list.add("张三"); list.add("李四"); list.add("王五"); printList(list,arrayList->{ for (int i = 0; i < arrayList.size();i++){ System.out.println("姓名:" + arrayList.get(i)); } }); } -
默认方法:如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费数据的时候,首先做一个操作, 然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的default方法 andThen 。下面是JDK的源代码:
-
源码
default Consumer
andThen(Consumer super T> after) { Objects.requireNonNull(after); return (T t) ‐> { accept(t); after.accept(t); }; } /* 备注: java.util.Objects 的 requireNonNull 静态方法将会在参数为null时主动抛出 NullPointerException 异常。这省去了重复编写if语句和抛出空指针异常的麻烦。 */ -
代码
//要想实现组合,需要两个或多个Lambda表达式即可,而 andThen 的语义正是“一步接一步”操作。例如两个步骤组合的情况: public static void printList(ArrayList
list, Consumer one,Consumer two){ one.andThen(two).accept(list); } public static void main(String[] args) { ArrayList list = new ArrayList<>(); list.add("张三"); list.add("李四"); list.add("王五"); printList(list,arrayList->{ System.out.println("======顺序打印======="); for (int i = 0; i < arrayList.size();i++){ System.out.println("姓名:" + arrayList.get(i)); } },arrayList -> { System.out.println("======倒序打印======="); for (int i = arrayList.size()-1; i >=0;i--){ System.out.println("姓名:" + arrayList.get(i)); } }); }
-
3.3 Predicate接口
有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用 java.util.function.Predicate 接口。
抽象方法test
Predicate 接口中包含一个抽象方法: boolean test(T t) 。用于条件判断的场景:
public static void main(String[] args) {
// 判断一个字符串中是否包含大写的J和大写的H
String str = "Hello Java";
boolean isHas = checkStr(str,data->data.contains("J")&&data.contains("H"));
System.out.println(str + "是否包含大写J和H:"+isHas);
}
public static boolean checkStr(String str , Predicate predicate){
return predicate.test(str);
}
默认方法and、or、nagate
- 方法
// and 且
default Predicate and(Predicate super T> other) {
Objects.requireNonNull(other);
return (t) ‐> test(t) && other.test(t);
}
// or 或
default Predicate or(Predicate super T> other) {
Objects.requireNonNull(other);
return (t) ‐> test(t) || other.test(t);
}
// nagate非
default Predicate negate() {
return (t) ‐> !test(t);
}
-
代码
// and public static void main(String[] args) { // 判断一个字符串中是否包含大写的J和大写的H String str = "Hello Java"; boolean isHas = checkStr(str,data->data.contains("J"),data->data.contains("H")); System.out.println(str + "包含大写J和H:"+isHas); } public static boolean checkStr(String str , Predicate
one,Predicate two){ return one.and(two).test(str); } // or public static void main(String[] args) { // 判断一个字符串中是否包含大写的J和大写的H String str = "Hello Java"; boolean isHas = checkStr(str,data->data.contains("J"),data->data.contains("H")); System.out.println(str + "包含大写J或H:"+isHas); } public static boolean checkStr(String str , Predicate one,Predicate two){ return one.or(two).test(str); } } // nagate public static void main(String[] args) { // 判断一个字符串中是否包含大写的J和大写的H String str = "Hello Java"; boolean isHas = checkStr(str,data->data.contains("J")&&data.contains("H")); System.out.println(str + "没有包含大写J和H:"+isHas); } public static boolean checkStr(String str , Predicate predicate){ return predicate.negate().test(str); } }
3.4 Function接口
java.util.function.Function
接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件, 后者称为后置条件。
抽象方法:apply
Function 接口中最主要的抽象方法为: R apply(T t) ,根据类型T的参数获取类型R的结果。 使用的场景例如:将 String 类型转换为 Integer 类型。
public static void main(String[] args) {
methond(str->{
return Integer.parseInt(str);
});
// 打印结果30
}
public static void methond(Function func){
Integer num = func.apply("20");
System.out.println(num + 10);
}
默认方法:andThen
Function 接口中有一个默认的 andThen 方法,用来进行组合操作。JDK源代码如:
default Function andThen(Function super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) ‐> after.apply(apply(t));
}
该方法同样用于“先做什么,再做什么”的场景,和 Consumer 中的 andThen 差不多:
public static void main(String[] args) {
// 将一个字符串变成数字后,再乘以10的结果
test(str->Integer.parseInt(str),num->num * 10);
}
public static void test(Function one,Function two){
int result = one.andThen(two).apply("20");
System.out.println("结果是:" + result);
}