1.函数式接口是用来干嘛的
函数式接口主要是为了简化代码、提高代码的可读性以及可维护性,与Lambda表达式配合能够让代码变得更加简洁以及功能强大。他们适用于各种常见的操作、比如说条件判断、数据转换、集合操作等等。
2.四大常用内置接口
-
Predicate<T>
:用于测试条件,返回boolean
。 -
Function<T, R>
:接受一个输入并返回一个输出。 -
Consumer<T>
:接受一个输入并执行某些操作,没有返回值。 -
Supplier<T>
:不接受参数,但返回一个结果。
2.1 Predicate<T>函数式接口
Predicate
用于判断给定条件是否成立,它返回一个布尔值。常用于过滤操作或条件判断。
例子: 假设你有一个列表,想筛选出所有偶数:
import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.function.Predicate;public class PredicateExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);// 使用Predicate来判断偶数Predicate<Integer> isEven = num -> num % 2 == 0;// 使用stream API结合filter来筛选偶数List<Integer> evenNumbers = numbers.stream().filter(isEven).collect(Collectors.toList());System.out.println(evenNumbers); // 输出 [2, 4, 6, 8, 10]}
}
isEven
是一个Predicate
,用来判断数字是否为偶数。- 使用
stream().filter()
结合Predicate
来筛选出所有偶数。
看到这,读者可能会想,既然是条件判断为什么不直接使用if,还需要搞一个函数式接口?
1. 简洁性和可读性
Predicate
与Lambda表达式结合使用,代码更简洁,特别是当你有多个判断条件时。例如,当你要进行集合的过滤时,使用Predicate
会让代码显得更加清晰和易于理解。
//使用if条件
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = new ArrayList<>();
for (Integer num : numbers) {if (num % 2 == 0) {evenNumbers.add(num);}
}
System.out.println(evenNumbers); // 输出 [2, 4, 6]//使用Predicate 能够看出来下面这个函数式编程的样式更为优雅
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
Predicate<Integer> isEven = num -> num % 2 == 0;
List<Integer> evenNumbers = numbers.stream().filter(isEven).collect(Collectors.toList());
System.out.println(evenNumbers); // 输出 [2, 4, 6]
2. 可复用性
Predicate
是一个函数式接口,可以将逻辑封装成一个可复用的函数。例如,你可以将相同的条件逻辑应用到不同的集合或场景中,而不需要重复编写相同的if
判断。
3.与Stream API的结合使用
Predicate
与Java 8的Stream
API高度集成,能够使得集合的操作(如过滤、映射、排序)变得更加函数式和声明式。当需要对集合进行复杂的操作时,Predicate
和流式操作可以让代码更加简洁、灵活。
//以下通过流操作进行的过滤、聚合,比通过List遍历再去生成新的list简便、可读性高得多
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Predicate<Integer> isEven = num -> num % 2 ==0;
List<Integer> filteredList = numbers.stream().filter(isEven).collect(Collectors.toList());
System.out.println(filteredList);List<String> strings = Arrays.asList("asaas","get","efwwe ", "qwfq efw");
Predicate<String> isContainSpace = string -> string.contains(" ");
List<String> filteredSStrList = strings.stream().filter(isContainSpace).collect(Collectors.toList());
System.out.println(filteredSStrList);
4. 函数式编程风格的追求
使用Predicate
和Lambda表达式可以使你的代码遵循函数式编程的范式,这种风格适用于多线程、并发和处理大规模数据时,能提供更好的可读性和扩展性。它可以简化一些复杂操作,避免过多的控制流结构(如if
、for
等)。而且此类判断逻辑与过滤操作分离的代码风格,更符合代码“单一职责”原则,当条件发生变化时,你只需要修改Predicate
,而不必修改整个过滤过程。
2.2 Function<T, R>函数式接口
Function
是一个接收一个参数并返回一个结果的函数式接口。它有一个apply
方法,通常用于映射操作。
举个例子:
package com.company.lambda;import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.function.Function;class Student {String name;int age;public Student(String name, int age) {this.name = name;this.age = age;}public String getName() {return name;}public int getAge() {return age;}public String toString() {return name + " (" + age + " years old)";}
}public class FunctionDemo {public static void main(String[] args) {// 创建学生列表List<Student> students = Arrays.asList(new Student("Alice", 20),new Student("Bob", 22),new Student("Charlie", 23));// 定义Function,增加学生年龄5岁Function<Student, Student> increaseAge = student -> new Student(student.getName(), student.getAge() + 5);// 使用map()方法将每个学生的年龄增加5岁List<Student> updatedStudents = students.stream().map(increaseAge) // 使用Function进行年龄转换.collect(Collectors.toList());// 打印更新后的学生信息updatedStudents.forEach(System.out::println);}
}
Function<Student, Student>
:定义了一个Function
,接受一个Student
对象并返回一个新的Student
对象,新的学生年龄比原来大5岁。map()
方法:通过map()
方法,我们将Function
应用到学生列表中的每个学生,生成新的学生列表,每个学生的年龄都增加了5岁。
2.3 Consumer<T>函数式接口
Consumer
用于接收一个类型的参数并对其进行操作,通常用于消费数据,如打印、更新等操作。
例子: 假设你有一个用户列表,想要遍历并打印每个用户的姓名:
import java.util.List;
import java.util.Arrays;
import java.util.function.Consumer;public class ConsumerExample {public static void main(String[] args) {List<String> users = Arrays.asList("Alice", "Bob", "Charlie", "David");// 使用Consumer来打印每个用户的姓名Consumer<String> printName = name -> System.out.println(name);// 遍历用户列表并使用Consumer打印每个用户的姓名users.forEach(printName);}
}
这个函数式接口较为简单,也好理解。在Interator接口中foreach接收的就是一个Customer<T>函数式接口参数。
所以在List(实现了Iterator<T>接口)集合遍历时,我们常常使用下面的代码,对集合中每一个元素进行某一些操作:
List<String> list = Arrays.asList("a", "b", "c");
//为什么我们可以通过这种方式遍历集合中的元素,即是因为该forEach接收的参数类型是Customer<T>函数式接口
list.forEach(s -> System.out.println(s)); // 输出:a b c
相当于是List接口声明时,传入的类型为String,其继承自Iterator<String>的迭代器接口,在调用foreach时,即执行的上述forEach代码。
-
forEach(Consumer<? super T> action)
:方法接收一个参数,类型是Consumer<? super T>,
这是一个函数式接口,表示接受一个类型为T
或其父类类型的参数并执行某些操作(无返回值)。这里使用了通配符? super T
,表示它可以接受T
类型的元素,或者T
的父类类型元素。 -
Objects.requireNonNull(action)代码用于确保
action
(即传入的Consumer
)不为null
。如果action
为null
,则抛出NullPointerException
。这是一种保护性措施,防止传入null
引起空指针异常。 -
for (T t : this)代码使用了增强型
for
循环(也叫做 "foreach" 循环),它遍历this
对象中的每个元素。this
是当前类的实例,表示当前类实现的集合或集合的元素集合,t中的每一个元素都会执行action.accept(t)
。 -
而
accept(t)
是Consumer
接口的抽象方法,用于接受参数t
并执行某种操作。在上述示例的代码传入accept的操作就是System.out.println(s)
2.4 Supplier<T>函数式接口
Supplier
用于不接受任何参数但返回一个结果,常用于延迟计算或提供数据。例如,延迟创建一个数据库连接、一个缓存实例等。
import java.util.function.Supplier;public class SupplierExample {public static void main(String[] args) {// 使用Supplier来提供当前时间的时间戳Supplier<Long> currentTimeMillis = () -> System.currentTimeMillis();// 调用Supplier获取当前时间戳System.out.println("Current time in milliseconds: " + currentTimeMillis.get());}
}
3.总结
- 函数式接口 是只包含一个抽象方法的接口,可以被用于 Lambda 表达式或方法引用。在流处理操作时尤其有用。
- 使用
@FunctionalInterface
注解可以明确标识接口作为函数式接口,并且在编译时进行检查。 - 函数式接口可以包含多个默认方法和静态方法,它们不会影响其作为函数式接口的身份。
- Java 8 提供了多个常用的内置函数式接口,方便我们在开发中使用。
- 除了上述的四大类,Java还提供了不少其他的函数式接口。
4.参考
Java8-Lambda:内置四大函数式接口_java8 四大内置接口用于什么场景-CSDN博客
Java 8 函数式接口 | 菜鸟教程
......