Java8新特性:函数式接口
1. 函数式接口
函数式接口:有且仅有一个抽象方法的接口。
函数式接口用于函数式编程的场景,对Java来说就是lambda表达式,只有确保接口中有且仅有一个抽象方法,lambda表达式才能正确推导。
函数式接口的lambda表达式,就像是匿名内部类的另一种表现形式。
1.1 函数式接口格式
interface FunctionalInterface1 {
void test();
}
1.2 @FunctionalInterface
该注解可以作用在一个接口上,用于检测接口是否符合函数式接口的规范,即是否有且仅有一个抽象方法。如果不符合要求,就会报错。
该注解仅用于检查编写规范,但是只要是符合“有且仅有一个抽象方法”要求的接口,即使不使用该注解,它也是一个函数式接口。
1.3 自定义函数式接口
public class Demo01 {
public static void main(String[] args) {
// 使用函数式接口,后面的lambda表达式,就是对接口唯一抽象方法的实现
Demo01Interface lambda = () -> System.out.println("functional interface test");
lambda.test();
}
}
// 函数式接口
@FunctionalInterface
interface Demo01Interface {
void test();
}
2. 函数式编程
上面的例子可以看到,我们将lambda表达式看做一个对象,并且定义一个变量引用它。在面向对象的基础上,Java通过lambda表达式和方法引用,进行函数式编程。
2.1 lambda表达式的延迟执行
有些语句被执行后,结果不一定会被使用,造成了性能浪费。
lambda表达式是延迟执行的,从这方面,可以提高利用率,提升性能。
性能浪费日志案例
打印日志信息可以快速定位问题的位置,记录程序运行的状况。
如果我们在日志的打印的方法中,进行信息的拼接,在满足条件的情况下再进行输出
public class Demo02 {
public static void main(String[] args) {
String pre = "Debug: ";
String content = "this is logger demo. ";
String suf = "currentTime: " + LocalDateTime.now();
// 调用日志打印方法
log(1, pre + content + suf);
}
// 日志打印方法
public static void log(int level, String msg) {
if (level == 1) {
System.out.println(msg);
}
}
}
这段代码的问题:无论level是否符合要求,log方法的第二个参数都会进行字符串的拼接。如果级别不符合要求,那么字符串拼接操作就白做了,存在性能浪费。
解决方式1 – 可变参数
/**
* 日志打印方法-可变参数
* 将字符串作为可变参数传入,在符合条件的情况下再进行遍历拼接
* /
public static void log(int level, String... msgs) {
if (level == 1) {
StringBuilder sb = new StringBuilder();
for (String msg : msgs) {
sb.append(msg);
}
System.out.println(sb);
}
}
解决方式2 – labmda表达式写法
使用lambda表达式,需要一个函数式接口
public class Demo03LambdaLogger {
public static void main(String[] args) {
String pre = "Debug: ";
String content = "this is logger demo. currentTime: ";
String suf = LocalDateTime.now().toString();
// 使用函数式接口,构造消息
MessageBuilder builder = () -> pre + content + suf;
// 日志打印
log(1, builder);
}
public static void log(int level, MessageBuilder builder) {
if (level == 1) {
String msg = builder.build();
System.out.println(msg);
}
}
}
/**
* 用于构建日志信息的函数式接口
*/
interface MessageBuilder {
// 构建日志消息方法
String build();
}
验证lambda表达式的延迟执行: 在编写lambda表达式时,添加打印语句,并将level调整为非1的值,看是否进行打印
MessageBuilder builder = () -> {
System.out.println("进行消息构建...");
return pre + content + suf;
};
log(0, builder);
2.2 使用lambda表达式作为参数和返回值
抛开原理,Java中的lambda表达式可以被当做匿名内部类的替代。如果方法的参数是一个函数式接口类型,那么就可以使用lambda表达式替代;
lambda表达式作为参数
例如Runnable接口只有一个run方法,就是一个函数式接口。如果有一个方法的参数为Runnable类型,那么就可以传入一个lambda表达式作为参数。
static void startWork(Runnable runnable) {
new Thread(runnable).start();
}
public static void main(String[] args) {
startWork(()-> System.out.println("task is running..."));
}
lambda表达式作为返回值
如果一个方法的返回值是一个函数式接口,那么就可以直接返回一个lambda表达式。当需要通过一个方法获取java.util.Comparator接口类型的对象作为排序器时,就可以调该方法获取。
public static void main(String[] args) {
// 创建list集合并使用比较器进行排序
List<String> list = new ArrayList<>();
list.add("aaaa");
list.add("aaa");
list.add("aa");
list.add("a");
Collections.sort(list, getComparator());
System.out.println(list);
}
// 获取比较器,这里直接返回一个lambda表达式
static Comparator<String> getComparator() {
// 比较字符串的长度进行排序
return (a, b) -> a.length() - b.length();
}
3. Java提供的常用函数式接口
JDK提供了很多的函数式接口以及丰富的使用场景,它们主要在 java.util.function 包中。下面是最简单的几个接口及使用示例。
3.1 Supplier
java.util.function.Supplier<T> 接口仅包含一个无参方法 T get()。用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,也就意味着这个方法需要使用lambda表达式向外提供一个T类型的对象。
public class Demo06Supplier {
public static void main(String[] args) {
String s = getStringFromSupplier(() -> "string from supplier");
System.out.println(s);
}
static String getStringFromSupplier(Supplier<String> supplier) {
return supplier.get();
}
}
练习:求数组元素的最大值
public class Demo07SupplierPractice {
public static void main(String[] args) {
int[] nums = {1, 4, 2, 7, 5};
// 调用getMaxValue方法,传递一个lambda表达式作为参数,lambda表达式定义如何产生这个最大值
int max = getMaxValue(() -> {
int maxTemp = nums[0];
for (int num : nums) {
if (num > maxTemp) {
maxTemp = num;
}
}
return maxTemp;
});
System.out.println(max);
}
// 定义一个方法,参数为 Supplier
static Integer getMaxValue(Supplier<Integer> supplier) {
return supplier.get();
}
}
3.3 Comsumer
java.util.function.Comsumer<T> 接口和Supplier接口相反,它不是生产一个数据,而是消费一个数据,数据类型由泛型决定。
抽象方法: accept(T t)
Consumer接口中包含一个抽象方法 void accept(T t),意为消费一个指定泛型类型的数据。如:
public class Demo08Consumer {
public static void main(String[] args) {
// 调用方法,传递lambda表达式,定义如何消费这一字符串
consumeString( s -> {
System.out.println("消费方式--将其分割为数组,并打印数组中的元素...");
String[] strings = s.split(" ");
for (String s1 : strings) {
System.out.println(s1);
}
});
}
// 定义一个消费字符串的方法,传入Consumer作为参数
static void consumeString(Consumer<String> consumer) {
consumer.accept("Hello consumer");
}
}
默认方法 andThen
如果一个方法的参数和返回值类型全是Consumer<T>类型,那么就可以实现这种效果:消费数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是Consumer接口的andThen,JDK源代码:
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
实现该组合,需要两个或两个以上的lambda表达式,andThen的含义就是一步接着一步,两个步骤组合的情况:
public class Demo09ConsumerAndThen {
public static void main(String[] args) {
// 编写两个lambda表达式,定义对数据不同的消费方式
Consumer<String> consumer1 = (s) -> System.out.println(s.toUpperCase() + "FIRST UPPERCASE");
Consumer<String> consumer2 = (s) -> System.out.println(s.toLowerCase() + "second lowercase");
consumerString(consumer1.andThen(consumer2));
}
// 定义一个消费字符串的方法, 参数传入Consumer对象
static void consumerString(Consumer<String> consumer) {
consumer.accept("two consumer:");
}
}
练习
下面的字符串数组当中存有多条信息,请按照格式“ 姓名:XX,性别:XX。 ”的格式将信息打印出来。要求将打印姓名的动作作为第一个 Consumer 接口的Lambda实例,将打印性别的动作作为第二个 Consumer 接口的Lambda实例,将两个 Consumer 接口按照顺序“拼接”到一起。
数组:
String[] array = { “迪丽热巴,女”, “古力娜扎,女”, “马尔扎哈,男” };
public class Demo09ConsumerPractice {
public static void main(String[] args) {
String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男"};
// 传入两个lambda表达式,分别定义accept方法不同的消费方式:
// 1.打印姓名 2.打印性别
print(array, (s) -> System.out.print("姓名:" + s.split(",")[0]),
(s) -> System.out.println(",性别:" + s.split(",")[1] + "。"));
}
// 定义打印方法,传入两个Consumer,在方法的内部使用andThen进行组合
static void print(String[] arr, Consumer<String> consumer1, Consumer<String> consumer2) {
for (String str : arr) {
consumer1.andThen(consumer2).accept(str);
}
}
}
3.4 Predicate
有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值的结果。这时候可以使用 java.util.function.Predicate<T> 接口。
抽象方法 test(T t)
test(T t) 用于条件判断的场景:
public class Demo10Predicate {
public static void main(String[] args) {
// 编写一个lambda表达式,定义test方法判断逻辑
testStringLength(s -> s.length() > 8);
}
// 定义一个判断字符串长度的方法,传入一个Predicate作为参数
static void testStringLength(Predicate<String> predicate) {
// 使用test方法判断字符串
boolean bool = predicate.test("helloWorld");
System.out.println("字符串太长了吗? " + (bool?"是的":"不是"));
}
}
默认方法 and(Predicate<? super T> predicate)
条件判断存在常见的与、或、非三种常见的逻辑关系。其中将两个Predicate 条件使用 与 连接起来,实现并且效果时,可以使用default方法 and,JDK源码如下:
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
// 可以看到,and就是将两个test方法的结果使用 && 进行了运算,并返回结果
return (t) -> test(t) && other.test(t);
}
练习:判断一个字符串既要包含 “W” 又要包含 “H” :
public static void main(String[] args) {
boolean result = testContains(s -> s.contains("H"), s -> s.contains("W"));
System.out.println(result?"包含W和H":"不包含或仅包含一个");
}
static boolean testContains(Predicate<String> p1, Predicate<String> p2) {
return p1.and(p2).test("Hello World");
}
默认方法 or(Predicate<? super T> predicate)
使用此方法可进行 或 运算,JDK源码如下:
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
练习:如果要实现 “字符串包含H或者W”,只需将上面的 “与” 的练习中and方法改为or即可
public static void main(String[] args) {
boolean result = testContains(s -> s.contains("H"), s -> s.contains("W"));
System.out.println(result?"包含W和H":"不包含或仅包含一个");
}
static boolean testContains(Predicate<String> p1, Predicate<String> p2) {
return p1.and(p2).test("Hello World");
}
默认方法 negate()
negate表示"非"的关系,即对自身的取反。默认方法negate()源码也非常简单:
default Predicate<T> negate() {
return (t) -> !test(t);
}
从它的实现中可以看出,它是对执行过的test方法的结果进行取反,需要在test方法调用前调用negate方法。
public static void main(String[] args) {
negateResult(s -> s.length() < 5);
}
static void negateResult(Predicate<String> predicate) {
boolean result = predicate.negate().test("HelloWorld");
System.out.println("字符串很长吗? " + (result?"很长":"不长"));
}
练习:集合信息的筛选
数组当中有多条 姓名+性别 的信息如下,请通过Predicate接口的拼装将符合要求的字符串删选到一个ArrayList中,需要同时满足两个条件:
1)必须为女生;2)姓名为四个字;
数组如下:
String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男", "赵丽颖,女" };
解答:
public static void main(String[] args) {
String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男", "赵丽颖,女" };
// 定义两个Predicate的表达式,第一个筛选姓名,第二个筛选性别
Predicate<String> p1 = s -> s.split(",")[0].length() == 4;
Predicate<String> p2 = s -> s.split(",")[1].equals("女");
List<String> persons = filterPerson(array, p1, p2);
System.out.println(persons);
}
static List<String> filterPerson(String[] array, Predicate<String> p1, Predicate<String> p2) {
List<String> personList = new ArrayList<>();
for (String person : array) {
if (p1.and(p2).test(person)) {
personList.add(person);
}
}
return personList;
}
3.5 Function
java.util.function.Function<T, R> 接口用来根据一个类型的数据,得到另一个类型的数据,前者称为前置条件,后者称为后置条件。
抽象方法 R apply(T t)
Function中的抽象方法为 R apply(T t),根据T类型的参数获取R类型的结果。
使用场景:如将 String 类型转换为 Integer类型。当然仅仅是将String转换为Integer不必使用此接口直接使用 Integer.valueOf(String s)方法即可,这里只是作为一个演示:
public class Demo14Function {
public static void main(String[] args) {
// 定义lambda表示,如何操作字符串
Function<String, Integer> function = s -> Integer.valueOf(s);
// 调用apply方法
Integer i = function.apply("100");
System.out.println(i);
}
}
默认方法 andThen(Function<T, R> function)
Function接口也有一个默认的andThen方法,用来进行组合操作。JDK源码如下:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
使用这个方法可以定义 "先做什么,再做什么"的顺序
public class Demo15FunctionAndThen {
public static void main(String[] args) {
// 定义两个lambda表达式,第一个将字符串转换为整数,第二个将整数进行加100的运算
Function<String, Integer> f1 = s -> Integer.valueOf(s);
Function<Integer, Integer> f2 = i -> i += 100;
Integer result = f1.andThen(f2).apply("900");
System.out.println(result);
}
}
练习:自定义函数模型的拼接
题目:
请使用 Function 进行函数模型的拼接,按照顺序需要执行的多个函数操作为:
- 将字符串截取数字年龄部分,得到字符串;
- 将上一步的字符串转换成为int类型的数字;
- 将上一步的int数字累加100,得到结果int数字。
解答:
public class Demo16FunctionPractice {
public static void main(String[] args) {
// 根据题意,定义3个lambda表达式
// 1.字符串截取表达式
Function<String, String> f1 = s -> s.split(",")[1];
// 2.字符串转换表达式
Function<String, Integer> f2 = s -> Integer.valueOf(s);
// 3.字符串累加表达式
Function<Integer, Integer> f3 = i -> i += 100;
// 执行
String str = "bulabula,30";
Integer result = f1.andThen(f2).andThen(f3).apply(str);
System.out.println(result);
}
}
默认方法 compose(Function<T, R> function)
compose(Function<T, R> function) 和 andThen方法刚好相反,它是先执行参数的apply后,将结果作为调用compose方法的Function对象执行apply方法的参数,JDK源码如下:
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
使用这个方法可以定义在做什么事情之前,先做什么:
public class Demo17FunctionCompose {
public static void main(String[] args) {
Function<String, Integer> f1 = s -> Integer.valueOf(s);
Function<Integer, Integer> f2 = i -> i *= 100;
// 在执行乘法运算之前,先将字符串转换为整数
Integer result = f2.compose(f1).apply("10");
System.out.println(result);
}
}