Java 8中的功能接口
1. 简介
本教程是 Java 8 中存在的不同功能接口的指南,以及它们的一般用例和标准 JDK 库中的用法。
2. Java 8 中的 Lambda
Java 8 以 lambda 表达式的形式带来了强大的新语法改进。lambda 是一个匿名函数,我们可以将其作为一等语言公民来处理。例如,我们可以将它传递给或从方法返回。 在 Java 8 之前,我们通常会为需要封装单个功能的每种情况创建一个类。这意味着需要大量不必要的样板代码来定义用作原始函数表示的东西。 “Lambda 表达式和函数式接口:提示和最佳实践” 一文更详细地描述了使用 lambda 的函数式接口和最佳实践。本指南重点介绍java.util.function包中存在的一些特定功能接口。
3. 功能接口
建议所有功能接口都具有信息丰富的*@FunctionalInterface*注释。这清楚地传达了接口的目的,并且还允许编译器在带注释的接口不满足条件时生成错误。
任何带有 SAM(Single Abstract Method) 的接口都是函数式接口,其实现可以被视为 lambda 表达式。 请注意,Java 8 的默认方法不是抽象的,也不算数;一个功能接口可能仍然有多个默认方法。我们可以通过查看函数的 文档 来观察这一点。
4. 功能
lambda 最简单和最普遍的情况是一个函数式接口,其方法接收一个值并返回另一个值。单个参数的函数由Function接口表示,该接口由其参数的类型和返回值进行参数化:
public interface Function<T, R> { … }
标准库中Function类型的用法之一是Map.computeIfAbsent方法。此方法通过键从映射中返回一个值,但如果键不存在于映射中,则计算一个值。要计算一个值,它使用传递的 Function 实现:
Map<String, Integer> nameMap = new HashMap<>();
Integer value = nameMap.computeIfAbsent("John", s -> s.length());
在这种情况下,我们将通过将函数应用于键来计算值,将其放入映射中,并从方法调用中返回。我们可以将lambda替换为匹配传递和返回值类型的方法引用。
请记住,我们调用方法的对象实际上是方法的隐式第一个参数。这允许我们将实例方法length引用转换为Function接口:
Integer value = nameMap.computeIfAbsent("John", String::length);
Function接口还有一个默认的compose方法,它允许我们将多个函数组合为一个并按顺序执行它们:
Function<Integer, String> intToString = Object::toString;
Function<String, String> quote = s -> "'" + s + "'";
Function<Integer, String> quoteIntToString = quote.compose(intToString);
assertEquals("'5'", quoteIntToString.apply(5));
quoteIntToString函数是应用于intToString函数结果的quote函数的组合。
5. 原始函数特化
由于基本类型不能是泛型类型参数,因此对于最常用的基本类型double、int、long以及它们在参数和返回类型中的组合,有多个版本的Function接口:
- IntFunction , LongFunction , DoubleFunction:参数是指定类型,返回类型是参数化的
- ToIntFunction , ToLongFunction , ToDoubleFunction:返回类型是指定类型,参数是参数化的
- DoubleToIntFunction、DoubleToLongFunction、IntToDoubleFunction、IntToLongFunction、LongToIntFunction、LongToDoubleFunction:将参数和返回类型定义为原始类型,由它们的名称指定 举个例子,对于一个接受一个short并返回一个byte的函数来说,没有开箱即用的函数接口,但没有什么能阻止我们自己编写:
@FunctionalInterface
public interface ShortToByteFunction {
byte applyAsByte(short s);
}
现在我们可以编写一个方法,使用ShortToByteFunction定义的规则将short数组转换为byte数组:
public byte[] transformArray(short[] array, ShortToByteFunction function) {
byte[] transformedArray = new byte[array.length];
for (int i = 0; i < array.length; i++) {
transformedArray[i] = function.applyAsByte(array[i]);
}
return transformedArray;
}
下面是我们如何使用它来将一个 short 数组转换为一个乘以 2 的字节数组:
short[] array = {(short) 1, (short) 2, (short) 3};
byte[] transformedArray = transformArray(array, s -> (byte) (s * 2));
byte[] expectedArray = {(byte) 2, (byte) 4, (byte) 6};
assertArrayEquals(expectedArray, transformedArray);
6. 二元函数特化
要使用两个参数定义 lambda,我们必须使用名称中包含“ Bi”关键字的附加接口:BiFunction、ToDoubleBiFunction、ToIntBiFunction和ToLongBiFunction。 BiFunction具有参数和返回类型泛型化,而ToDoubleBiFunction和其他允许我们返回原始值。 在标准 API 中使用此接口的典型示例之一是Map.replaceAll方法,该方法允许将地图中的所有值替换为某个计算值。 让我们使用一个接收键和旧值的BiFunction实现来计算薪水的新值并返回它。
Map<String, Integer> salaries = new HashMap<>();
salaries.put("John", 40000);
salaries.put("Freddy", 30000);
salaries.put("Samuel", 50000);
salaries.replaceAll((name, oldValue) ->
name.equals("Freddy") ? oldValue : oldValue + 10000);
7. Supplier
Supplier功能接口是另一个不带任何参数的Function特化。我们通常将它用于延迟生成值。例如,让我们定义一个对double值求平方的函数。它本身不会收到一个值,而是这个值的Supplier:
public double squareLazy(Supplier<Double> lazyValue) {
return Math.pow(lazyValue.get(), 2);
}
这允许我们使用Supplier实现来延迟生成调用此函数的参数。如果参数的生成需要相当长的时间,这可能很有用。我们将使用 Guava 的sleepUninterruptibly方法进行模拟:
Supplier<Double> lazyValue = () -> {
Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS);
return 9d;
};
Double valueSquared = squareLazy(lazyValue);
Supplier的另一个用例是定义序列生成的逻辑。为了演示它,让我们使用静态Stream.generate方法来创建斐波那契数Stream:
int[] fibs = {0, 1};
Stream<Integer> fibonacci = Stream.generate(() -> {
int result = fibs[1];
int fib3 = fibs[0] + fibs[1];
fibs[0] = fibs[1];
fibs[1] = fib3;
return result;
});
我们传递给Stream.generate方法的函数实现了Supplier功能接口。请注意,要作为生成器有用,Supplier通常需要某种外部状态。在这种情况下,它的状态包括最后两个斐波那契数列。 为了实现这个状态,我们使用一个数组而不是几个变量,因为在 lambda 中使用的所有外部变量都必须是有效的 final。 Supplier功能接口的其他特化包括BooleanSupplier、DoubleSupplier、LongSupplier和IntSupplier,它们的返回类型是对应的原语。
8. Consumer
与Supplier不同,Consumer接受一个泛型参数并且不返回任何内容。它是一个代表副作用的函数。 例如,让我们通过在控制台中打印问候语来问候姓名列表中的每个人。传递给List.forEach方法的 lambda 实现了Consumer功能接口:
List<String> names = Arrays.asList("John", "Freddy", "Samuel");
names.forEach(name -> System.out.println("Hello, " + name));
还有专门的Consumer版本——DoubleConsumer、IntConsumer和LongConsumer——接收原始值作为参数。更有趣的是BiConsumer界面。它的一个用例是遍历映射的条目:
Map<String, Integer> ages = new HashMap<>();
ages.put("John", 25);
ages.put("Freddy", 24);
ages.put("Samuel", 30);
ages.forEach((name, age) -> System.out.println(name + " is " + age + " years old"));
另一组专门的BiConsumer版本由ObjDoubleConsumer、ObjIntConsumer和ObjLongConsumer 组成,它们接收两个参数;其中一个参数是泛型的,另一个是原始类型。
9. Predicate
在数学逻辑中,谓词是一个接收值并返回布尔值的函数。 Predicate函数接口是Function的一种特殊化,它接收一个泛型值并返回一个布尔值。Predicate lambda的一个典型用例是过滤一组值:
List<String> names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");
List<String> namesWithA = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
在上面的代码中,我们使用Stream API过滤列表,只保留以字母“A”开头的名称。Predicate实现封装了过滤逻辑。 与前面的所有示例一样,此函数有IntPredicate、DoublePredicate和LongPredicate版本,它们接收原始值。
10. Operator
Operator接口是接收和返回相同值类型的函数的特殊情况。UnaryOperator接口接收单个参数。Collections API 中的一个用例是用一些相同类型的计算值替换列表中的所有值:
List<String> names = Arrays.asList("bob", "josh", "megan");
names.replaceAll(name -> name.toUpperCase());
List.replaceAll函数 在替换现有值时返回void。为了达到这个目的,用于转换列表值的 lambda 必须返回与它接收到的相同的结果类型。这就是UnaryOperator在这里有用的原因。 当然,我们可以简单地使用方法引用来代替name -> name.toUpperCase():
names.replaceAll(String::toUpperCase);
BinaryOperator最有趣的用例之一是归约操作。假设我们想要将一个整数集合聚合为所有值的总和。使用Stream API,我们可以使用收集器来做到这一点,但更通用的方法是使用reduce方法:
List<Integer> values = Arrays.asList(3, 5, 8, 9, 12);
int sum = values.stream()
.reduce(0, (i1, i2) -> i1 + i2);
reduce方法接收一个初始累加器值和一个BinaryOperator函数。这个函数的参数是一对相同类型的值;该函数本身还包含一个逻辑,用于将它们连接到相同类型的单个值中。传递的函数必须是 associative,这意味着值聚合的顺序无关紧要,即应满足以下条件:
op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)
BinaryOperator运算符函数的关联属性使我们能够轻松地并行化约简过程。 当然,也有UnaryOperator和BinaryOperator的特化可以与原始值一起使用,即DoubleUnaryOperator、IntUnaryOperator、LongUnaryOperator、DoubleBinaryOperator、IntBinaryOperator和LongBinaryOperator。
11. 遗留功能接口
并非所有函数式接口都出现在 Java 8 中。Java 早期版本中的许多接口都符合FunctionalInterface的约束,我们可以将它们用作 lambda。突出的示例包括并发 API 中使用的Runnable和Callable接口。在 Java 8 中,这些接口也标有*@FunctionalInterface*注解。这使我们能够大大简化并发代码:
Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));
thread.start();