一.函数式接口
(一)概念
函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda
,所以函数式接口就是可以适用于Lambda
使用的接口。只有确保接口中有且仅有一个(默认方法、静态方法、私有方法不影响),Java中的Lambda
才能顺利地进行推导。
备注:“语法糖”是指使用更加方便,但是原理不变的代码语法。
如在遍历集合时使用的for-each
语法,其实底层的实现原理仍然是迭代器,这便是“语法糖”。从应用层面来讲,Java中的Lambda
可以被当做是匿名内部类的“语法糖”,但是二者在原理上是不同的。
(二)格式
只要确保接口中有且仅有一个抽象方法即可:
修饰符 interface 接口名称 {
public abstract 返回值类型 方法名称(可选参数信息);
// 其他非抽象方法内容
}
(三)FunctionalInterface注解
Java 8
中专门为函数式接口引入了一个新的注解:@FunctionalInterface
。该注解可用于一个接口的定义上:
@FunctionalInterface
public interface MyFunctionalInterface {
void myMethod();
}
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实抽象方法,否则将会报错。需要注意的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
二.Lambda表达式
(一)函数式编程思想概述
在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。
1.面向对象的思想:
做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情.
2.函数式编程思想:
只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程
(二)冗余的Runnable代码
AnonymousRunnable.java
public class AnonymousRunnable {
public static void main(String[] args) {
// 匿名内部类
new Thread(new Runnable() {
@Override
// 覆盖重写抽象方法
public void run() {
System.out.println(Thread.currentThread().getName());
}
}).start(); // 启动线程
}
}
运行结果:
本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个
Runnable
接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。
对于
Runnable
的匿名内部类用法,可以分析出几点内容:
Thread
类需要Runnable
接口作为参数,其中的抽象run
方法是用来指定线程任务内容的核心。- 为了指定
run
的方法体,不得不需要Runnable
接口的实现类。- 为了省去定义一个
RunnableImpl
实现类的麻烦,不得不使用匿名内部类。- 必须覆盖重写抽象
run
方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错。- 而实际上,似乎只有方法体才是关键所在。
(三)体验Lambda的更优写法
借助Java 8
的全新语法,上述Runnable
接口的匿名内部类写法可以通过更简单的Lambda
表达式达到等效:
Lambda.java
public class Lambda {
public static void main(String[] args) {
// Lambda表达式
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
}).start();
}
}
运行结果:
(四)Lambda标准格式
Lambda
省去面向对象的条条框框,格式由3个部分组成:
- 一些参数
- 一个箭头
- 一段代码
Lambda表达式的标准格式为:
(参数类型 参数名称) -> {
代码语句
}
格式说明:
- 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
-
->
是新引入的语法格式,代表指向动作。 - 大括号内的语法与传统方法体要求基本一致。
练习1:
需求:
在下面的代码中,请使用Lambda
的标准格式调用invokeCook
方法,打印输出“吃饭了!!!”字样:
无参无返回实现Lambda表达式代码:
Cook.java:
public interface Cook {
abstract void makeFood();
}
LambdaInvokeCooks.java
public class LambdaInvokeCooks {
public static void main(String[] args) {
// TODO 请在此使用Lambda【标准格式】调用invokeCook方法
invokeCook(() -> {
System.out.println("吃饭了!!!");
});
}
private static void invokeCook(Cook cook) {
cook.makeFood();
}
}
运行结果:
备注:小括号代表
Cook
接口makeFood
抽象方法的参数为空,大括号代表makeFood
的方法体。
练习2:
需求:
- 使用数组存储多个
Person
对象 - 对数组中的
Person
对象使用Arrays
的sort
方法通过年龄进行升序排序
有参有返回实现Lambda表达式代码:
Person.java
public class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
LambdaIArraysSort.java
public class LambdaIArraysSort {
public static void main(String[] args) {
Person[] people = {
new Person("古力娜扎", 20),
new Person("玛尔扎哈", 30),
new Person("迪丽热巴", 18)};
// Lambda表达式
Arrays.sort(people, (Person o1, Person o2) -> {
return o1.getAge() - o2.getAge();
});
for (Person person : people) {
System.out.println(person.getName() + person.getAge());
}
}
}
(五)Lambda表达式省略规则
在Lambda
标准格式的基础上,使用省略写法的规则为:
- 小括号内参数的类型可以省略;
- 如果小括号内有且仅有一个参数,则小括号可以省略;
- 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、
return
关键字及语句分号。
Lambda表达式省略代码:
Calculator.java
public interface Calculator {
abstract int calc(int a, int b);
}
PrintNum.java
public interface PrintNum {
abstract void print(int a);
}
LambdaInvokeCalculator.java
public class LambdaInvokeCalculator {
public static void main(String[] args) {
// Lambda表达式
invokeCalculator(300, 200, (int a, int b) -> {
return a + b;
});
invokePrintNum(10, (int a) -> {
System.out.println(a);
});
// Lambda表达式的省略格式
invokeCalculator(150, 250, (a, b) -> a + b);
invokePrintNum(20, a -> System.out.println(a));
}
private static void invokeCalculator(int a, int b, Calculator c) {
System.out.println(c.calc(a, b));
}
private static void invokePrintNum(int a, PrintNum p) {
p.print(a);
}
}
运行结果:
(六)Lambda的使用前提
Lambda
表达式的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
- 使用
Lambda
表达式必须具有接口,且要求接口中有且仅有一个抽象方法。
无论是JDK内置的Runnable
、Comparator
接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda
表达式。 - 使用
Lambda
表达式必须具有上下文推断。
也就是方法的参数或局部变量类型必须为Lambda
对应的接口类型,才能使用Lambda
作为该接口的实例。
四.函数式编程
(一)Lambda的延迟执行
一种典型的场景就是对参数进行有条件使用,例如对日志消息进行拼接后,在满足条件的情况下进行打印输出:
Logger.java:
public class Logger {
public static void showLog(int level, String msg) {
System.out.println(msg);
if (level == 1) {
System.out.println("日志等级为1,日志内容为:" + msg);
}
}
public static void main(String[] args) {
String msg1 = "Hello";
String msg2 = "World";
String msg3 = "!";
showLog(2, msg1 + msg2);
showLog(1, msg1 + msg2 + msg3);
}
}
运行结果:
这段代码存在问题:无论级别是否满足要求,作为showLog
方法的第二个参数,两个字符串一定会首先被拼接并传入方法内,然后才会进行级别判断。如果级别不符合要求,那么字符串的拼接操作就白做了,存在性能浪费。
备注:
SLF4J
是应用非常广泛的日志框架,它在记录日志时为了解决这种性能浪费的问题,并不推荐首先进行字符串的拼接,而是将字符串的若干部分作为可变参数传入方法中,仅在日志级别满足要求的情况下才会进行字符串拼接。例如:LOGGER.debug("变量{}的取值为{}", "os", "macOS")
,其中的大括号{}
为占位符。如果满足日志级别要求,则会将“os”
和“macOS”
两个字符串依次拼接到大括号的位置;否则不会进行字符串拼接。
体验Lambda的更优写法
LambdaLogger.java
public class LambdaLogger {
public static void showLog(int level, MessageBuilder mb) {
if (level == 1) {
System.out.println("日志等级为1,日志内容为:" + mb.concatMsg());
}
}
public static void main(String[] args) {
String msg1 = "Hello";
String msg2 = "World";
showLog(2, () -> {
System.out.println("Lambda2 执行!");
return msg1 + msg2;
});
showLog(1, () -> {
System.out.println("Lambda1 执行!");
return msg1 + msg2;
});
}
}
运行结果:
(二)使用Lambda作为方法的参数
如果方法的参数是一个函数式接口类型,那么就可以使用Lambda
表达式进行替代。
LambdaArgs.java
public class LambdaArgs {
// 定义一个方法startThread,方法的参数使用方法式接口Runnable
public static void startThread(Runnable runnable) {
// 开启多线程
new Thread(runnable).start();
}
public static void main(String[] args) {
startThread(new Runnable() {
// 使用匿名内部类作为方法的参数
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
// 使用Lambda作为方法的参数
startThread(() -> System.out.println(Thread.currentThread().getName()));
}
}
运行结果:
(三)使用Lambda作为方法的返回值
如果方法的返回值类型是一个函数式接口,那么就可以直接返回一个Lambda
表达式替代。
LambdaReturn.java
public class LambdaReturn {
// 定义一个方法newComparator1,方法的返回值使用匿名内部类
public static Comparator<String> newComparator1() {
return new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.charAt(0) - o2.charAt(0);
}
};
}
// 定义一个方法newComparator1,方法的返回值使用匿名内部类
public static Comparator<String> newComparator2() {
return (o1, o2) -> o1.charAt(1) - o2.charAt(1);
}
public static void main(String[] args) {
String[] array = {"ccc", "bab", "aaa", "aba"};
System.out.println("原数组:" + Arrays.toString(array));
Arrays.sort(array, newComparator1());
System.out.println("按字符串第一个字符升序排序:" + Arrays.toString(array));
Arrays.sort(array, newComparator2());
System.out.println("按字符串第二个字符升序排序:" + Arrays.toString(array));
}
}
运行结果:
五.常用函数式接口
(一)Supplier接口
java.util.function.Supplier<T>
接口仅包含一个无参的方法:T get()
用来获取一个泛型参数指定类型的对象数据。
Supplier<T>
接口被称之为生产型接口,指定接口的泛型是什么类型,那么接口中的get
方法就会生产什么类型的数据。
SupplierTest.java
public class SupplierTest {
public static void main(String[] args) {
Supplier<String> sup1 = () -> {
return "Hello";
};
Supplier<String> sup2 = () -> "Java";
System.out.println(sup1.get());
System.out.println(sup2.get());
}
}
运行结果:
练习:使用 Supplier 接口作为方法参数类型,通过Lambda表达式求出int数组中的最大值。
SupplierMax.java
public class SupplierMax {
public static int getMax(Supplier<Integer> sup) {
return sup.get();
}
public static void main(String[] args) {
int[] arrays = {2, 56, 23, 87, 82, 54};
int maxNum = getMax(() -> {
int max = 0;
for (int num : arrays) {
if (num > max) {
max = num;
}
}
return max;
});
System.out.println(maxNum);
}
}
运行结果:
(二)Consumer接口
java.util.function.Consumer<T>
接口被称之为消费型接口,指定接口的泛型是什么类型,那么接口中的accept
方法就会消费什么类型的数据。
ConsumerTest.java
public class ConsumerTest {
public static void main(String[] args) {
Consumer<String> consumer = a -> {
StringBuilder sb = new StringBuilder(a);
System.out.println(sb.reverse());
};
String str = "Hello";
consumer.accept(str);
}
}
运行结果:
1.默认方法:andThen
如果一个方法的参数和返回值全都是Consumer
类型,那么就可以实现效果:消费数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是Consumer
接口中的默认方法andThen
。
JDK源代码:
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
练习1:通过链式写法打印完全大写的HELLO,然后打印完全小写的hello
AndThenTest.java
public class AndThenTest {
public static void consumerAndThen(Consumer<String> consumer1, Consumer<String> consumer2) {
consumer1.andThen(consumer2).accept("Hello");
}
public static void main(String[] args) {
consumerAndThen(
(str1) -> {
System.out.println(str1.toUpperCase());
}, (str2) -> {
System.out.println(str2.toLowerCase());
}
);
}
}
运行结果:
练习2:
下面的字符串数组当中存有多条信息,请按照格式“ 姓名:XX。性别:XX。 ”的格式将信息打印出来。要求将打印姓名的动作作为第一个 Consumer 接口的Lambda实例,将打印性别的动作作为第二个 Consumer 接口的Lambda实例,将两个 Consumer 接口按照顺序“拼接”到一起。
ConsumerPrintTest.java
public class ConsumerPrintTest {
public static void printMessage(String[] array, Consumer<String[]> consumer1, Consumer<String[]> consumer2) {
for (String info : array) {
String[] infoArray = info.split(",");
consumer1.andThen(consumer2).accept(infoArray);
}
}
public static void main(String[] args) {
String[] array = {"迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男"};
printMessage(array, (infoArray) -> {
System.out.print("姓名:" + infoArray[0] + ",");
}, (infoArray) -> {
System.out.println("性别:" + infoArray[1]);
});
}
}
运行结果:
(三)Predicate接口
有时候我们需要对某种类型的数据进行判断,从而得到一个boolean
值结果。可以使用java.util.function.Predicate<T>
接口。
PredicateTest.java
public class PredicateTest {
public static void main(String[] args) {
Predicate<String> predicate = (string) -> string.length() == 6;
System.out.println(predicate.test("abcdef"));
}
}
运行结果:
1.默认方法:and
既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个Predicate
条件使用“与”逻辑连接起来实现“并且”的效果时,可以使用default
方法and
JDK源代码:
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) ‐> test(t) && other.test(t);
}
PredicateAndTest.java
public class PredicateAndTest {
public static boolean testAnd(Predicate<String> pre1, Predicate<String> pre2, String str) {
// 相当于return pre1.test(str) && pre2.test(str);
return pre1.and(pre2).test(str);
}
public static void main(String[] args) {
System.out.println(testAnd((message) -> message.length() == 5, (message) -> message.contains("e"), "Hello"));
}
}
运行结果:
2.默认方法:or
JDK源代码:
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) ‐> test(t) || other.test(t);
}
PredicateOrTest.java
public class PredicateOrTest {
public static boolean testOr(Predicate<String> pre1, Predicate<String> pre2, String str) {
// 相当于return pre1.test(str) || pre2.test(str);
return pre1.or(pre2).test(str);
}
public static void main(String[] args) {
System.out.println(testOr((message) -> {
return message.length() == 5;
}, (message) -> {
return message.contains("e");
}, "Hello123"));
}
}
运行结果:
3.默认方法:negate
JDK源代码:
default Predicate<T> negate() {
return (t) ‐> !test(t);
}
PredicateNegateTest.java
public class PredicateNegateTest {
public static boolean testNegate(Predicate<String> pre1,String str) {
// 相当于return !pre1.test(str);
return pre1.negate().test(str);
}
public static void main(String[] args) {
System.out.println(testNegate((message) -> {
return message.length() == 5;
}, "Hello123"));
}
}
运行结果:
练习1:
数组当中有多条“姓名+性别”的信息如下,请通过 Predicate 接口的拼装将符合要求的字符串筛选到集合
ArrayList 中,需要同时满足两个条件:
- 必须为女生
- 姓名为4个字
PredicateFilter.java
public class PredicateFilter {
public static List filterMessage(String[] array, Predicate<String[]> pre1, Predicate<String[]> pre2) {
List<String> filterList = new ArrayList<>();
for (String info : array) {
String[] infoArray = info.split(",");
if (pre1.and(pre2).test(infoArray)) {
filterList.add(Arrays.toString(infoArray));
}
}
return filterList;
}
public static void main(String[] args) {
String[] array = {"迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男", "赵丽颖,女"};
System.out.println(filterMessage(array, (infoArray) -> infoArray[0].length() == 4, (infoArray) -> "女".equals(infoArray[1])));
}
}
运行结果:
(四)Function接口
java.util.function.Function<T,R>
接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。
FunctionTest.java
public class FunctionTest {
public static void main(String[] args) {
Function<String, Integer> fun = (a)->{
return Integer.parseInt(a);
};
System.out.println(fun.apply("2"));
}
}
运行结果: