聊聊 Java8 函数接口

开发 前端
本文主要演示了Java 8 API中的不同功能的函数接口,这些接口可以用作Lambda表达式。函数式流式编程方法在Java 8 之后的项目中应用非常普遍。

概述

在Java 8之前,我们通常会为每种需要封装单个功能的情况创建一个类,这意味着需要大量不必要的样板代码。

Java 8以Lambda表达式的形式带来了一个强大的新语法改进,Lambda是一个匿名函数。

Function接口

建议所有函数接口都使用@FunctionalInterface注解,用于清楚传达函数接口的目的,并且还允许编译器在带有该注解的接口,在不满足条件的情况下生成编译错误。

任何带有SAM(单一抽象方法)的接口都是函数接口,被视为Lambda表达式。

Java8 的默认方法不是抽象的,也不算在内,函数接口允许存在多个默认方法。

Lambda最简单、最通用的情况是一个函数接口,该接口具有一个接收一个值并返回另一个值的方法。单个参数的函数可以由Function接口表示,该接口通过其参数的类型和返回值进行参数化:

public interface Function<T, R> { … }

Function类型在标准JDK库中的用法之一是Map.computeIfAbsent方法。此方法按键返回映射中的值,但如果映射中尚未存在键,则会计算值。要计算一个值,它使用传递的Function实现:

Map<String, Integer> nameMap = new HashMap<>();
Integer value = nameMap.computeIfAbsent("John", s -> s.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));

基元类型函数

由于基元类型不能是泛型类型参数,因此对于最常用的基元类型double、int、long及其在参数和返回类型中的组合,函数接口有以下版本:

  • IntFunction、LongFunction、DoubleFunction:参数是指定类型的,返回类型是参数化的
  • ToIntFunction、ToLongFunction、ToDoubleFunction:返回类型为指定类型,参数是参数化的
  • DoubleToIntFunction、DoubleToLongFunction、IntToDoubleFunction、IntToLongFunction、LongToIntFunction和LongToDoubleFunction:参数和返回类型都定义为基元类型,由它们的名称指定

例如,对于一个使用short并返回字节的函数:

@FunctionalInterface
public interface ShortToByteFunction {

    byte applyAsByte(short s);

}

现在,我们可以编写一个方法,使用ShortToByteFunction定义的规则将short数组转换为字节数组:

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);

Bi函数接口

要用两个参数定义lambda,我们必须使用名称中包含“Bi”关键字的附加接口:BiFunction、ToDoubleBiFunction、ToIntBiFunction和ToLongBiFunction。

BiFunction同时生成了参数和返回类型,而ToDoubleBiFunction和其他函数允许我们返回基元值。

在标准API中使用此接口的典型示例之一是Map.replaceAll方法,它允许用一些计算值替换Map中的所有值。

让我们使用一个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);

Suppliers函数接口

通常用它来提供数据产出,例如,让我们定义一个函数,它将一个值平方:

public double squareLazy(Supplier<Double> lazyValue) {
    return Math.pow(lazyValue.get(), 2);
}

Supplier<Double> lazyValue = () -> {
    Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS);
    return 9d;
};

Double valueSquared = squareLazy(lazyValue);

让我们使用一个静态Stream.generate方法来创建一个Fibonacci数字流:

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;
});

我们使用一个数组而不是两个变量,因为lambda内部使用的所有外部变量都必须是有效的final。

Consumers函数接口

例如,让我们通过在控制台中打印问候语来问候姓名列表中的每个人。传递给List.forEach方法的lambda实现了Consumer函数接口:

List<String> names = Arrays.asList("John", "Freddy", "Samuel");
names.forEach(name -> System.out.println("Hello, " + name));

还有专门版本的Consumer(DoubleConsumer、IntConsumer和LongConsumer),它们接收基元值作为参数:

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组成,它们接收两个参数;其中一个参数是泛型的,另一个是基元类型。

Predicates函数接口

Predicates是一个接收值并返回布尔值的函数。

List<String> names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");

List<String> namesWithA = names.stream()
  .filter(name -> name.startsWith("A"))
  .collect(Collectors.toList());

在上面的代码中,我们使用流API过滤列表,并只保留以字母“a”开头的名称。Predicates实现封装了筛选逻辑。

与前面的所有示例一样,此函数的IntPredicate、DoublePredicate和LongPredicate版本都接收基元值。

Operators函数接口

Operator接口是接收和返回相同值类型的函数的特殊情况。UnaryOperator接口接收一个参数。它在集合API中的一个用例是用相同类型的一些计算值替换列表中的所有值:

List<String> names = Arrays.asList("bob", "josh", "megan");
names.replaceAll(name -> name.toUpperCase());

当然,我们可以简单地使用方法引用来代替name->name.toUpperCase():

names.replaceAll(String::toUpperCase);

BinaryOperator最有趣的用例之一是归约运算。假设我们想将一组整数聚合为所有值的总和。使用流API,我们可以使用收集器来实现这一点,但更通用的方法是使用reduce方法:

List<Integer> values = Arrays.asList(3, 5, 8, 9, 12);

int sum = values.stream()
  .reduce(0, (i1, i2) -> i1 + i2);

reduce方法接收一个初始累加器值和一个BinaryOperator函数。此函数的参数是一对相同类型的值;函数本身还包含一个逻辑,用于将它们连接到同一类型的单个值中。传递的函数必须是关联的,这意味着值聚合的顺序无关紧要,即应满足以下条件:

op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)

其他

并不是所有的功能接口都出现在Java 8中。以前版本的Java中的许多接口都符合FunctionalInterface的约束,我们可以将它们用作lambda。

突出的例子包括并发API中使用的Runnable和Callable接口。在Java 8中,这些接口也用@FunctionalInterface注释进行标记。这使我们能够极大地简化并发代码:

Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));
thread.start();

结论

本文主要演示了Java 8 API中的不同功能的函数接口,这些接口可以用作Lambda表达式。函数式流式编程方法在Java 8 之后的项目中应用非常普遍。

责任编辑:武晓燕 来源: 今日头条
相关推荐

2020-05-25 16:25:17

Java8Stream函数式接口

2021-08-03 07:51:43

Java 8 函数接口

2020-10-16 10:07:03

Lambda表达式Java8

2022-12-26 07:47:37

JDK8函数式接口

2015-09-30 09:34:09

java8字母序列

2019-08-05 08:05:27

Java开发代码

2022-01-06 07:39:17

Java Default关键字 Java 基础

2020-07-24 08:11:04

Java8ava5语言

2021-08-13 12:53:42

StringBuildStringJoineJava

2023-05-12 07:40:01

Java8API工具

2021-01-14 10:00:57

Restful接口

2017-10-31 20:45:07

JavaJava8Optional

2020-05-29 07:20:00

Java8异步编程源码解读

2014-12-22 10:14:31

Java8

2023-03-15 17:37:26

Java8ListMap

2023-12-21 08:02:21

CPUJava8列表

2023-05-17 08:20:34

Java 17编程语言

2021-03-04 08:14:37

Java8开发接口

2020-05-27 08:05:33

MybatisMapper接口

2021-09-18 09:45:33

前端接口架构
点赞
收藏

51CTO技术栈公众号