Java 8 函数接口详细教程

本文深入探讨Java8中各种函数接口,如Function、Predicate、Supplier等,介绍它们的使用场景及标准API,通过示例展示如何利用这些接口简化代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java 8 函数接口详细教程

本文介绍Java 8中各种函数接口,它们一般应用场景及标准API用法。

Lambdas 表达式

Java8 引入强大的新的语法升级是Lambdas表达式。Lambdas表达式是匿名函数,可视为语言的一等公民进行处理,如可以作为方法的参数或返回值。

Java 8 之前,我们通常为每种仅需封装单个功能场景要创建一个类。这导致很多不必要的模板代码来定义简单函数表达式。

所有预定义的函数接口都在 java.util.function 包中。

函数接口

所有函数式接口建议增加 @FunctionalInterface注解进行标识。这不仅清晰表明该接口目的,也让编译器在注解接口不满足一定条件是生成错误。

任何满足SAM(Single Abstract Method)接口是函数式接口。其实现可视为lambda表达式。

注意Java 8 的缺省方法不是抽象的不算数,因此一个函数接口可能有多个缺省方法。你通过Function文档可以观察到该特点。

Function<T,R> 接口

最简单且常用的lambda表达式是Function接口,接收一个值并返回另一个值得方法。单参数函数通过Function接口表示,其返回值和输入参数被参数化:

public interface Function<T, R> { … }

Function类型其中一个用法是jdk标准库中的Map.computeIfAbsent 方法,其根据map的key返回一个值,但如果key不存在,则计算一个值返回。为了计算值,其传入Function实现:

Map<String, Integer> nameMap = new HashMap<>();
Integer value = nameMap.computeIfAbsent("John", s -> s.length());

本例中,通过将函数应用于键计算值,即该键值对存入map中并从方法钓鱼总返回值。顺便说下,也可以通过匹配传入并返回值类型的方法引用代替lambda表达式。

需说明的是,调用方法的对象隐式实际上是方法的第一个参数,也可以使用方法引用作为函数接口:

Integer value = nameMap.computeIfAbsent("John", String::length);

函数接口还包括缺省方法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 是组合函数,其中quote函数使用intToString 函数的执行结果。

特殊的基本类型函数接口

因为基本类型U币能作为泛型参数,jdk提供了常用基本类型的几个Function版本,double,int,long以及它们输入参数和返回值类型的不同组合:

  • IntFunction, LongFunction, DoubleFunction: 参数是特定类型,返回值类型是泛型
  • ToIntFunction, ToLongFunction, ToDoubleFunction: 返回值是特定类型,入参是泛型
  • DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction, IntToLongFunction, LongToIntFunction, LongToDoubleFunction — 入参和返回值类型都是固定的,和其名称说明的一样

对于jdk默认没有提供的类型,如一个函数带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数组至byte数组乘以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);

二元函数接口

带两个参数的lambda表达式,我们需要使用名称包含Bi关键字的函数接口:BiFunction, ToDoubleBiFunction, ToIntBiFunction, and ToLongBiFunction。

BiFunction 两个参数和返回值都是泛型,而ToDoubleBiFunction和其他类似函数接口可以返回基本类型。

使用二元lambda表示的典型示例是jdk中Map.replaceAll 方法,其使用计算值替换map中所有值。下面使用BiFunction实现接收key和被替换的值取计算新值并返回:

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

Supplier函数接口

Supplier接口是另一个不带任何参数的特殊形式。典型用于延迟生成值。举例,定义double值得平方函数。其不接收一个值,而是Supperlier作为值:

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.genernate方法创建斐波那契数值流:

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, 它们的返回类型都是相应的基本类型。

Consumer 函数接口

与Supplier相反,Consumer接口接收一个泛型参数但没有返回值。该函数是有副作用的代表(因为修改了参数,lambda表达式不能修改参数)。

举例,对list中每个名称以输出至控制台的方式进行问候。lambda表达式传入 List.forEach方法实现Consumer函数接口:

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

也有特定版本的Consumer — DoubleConsumer, IntConsumer and LongConsumer,接收基本类型值作为参数。更有趣的是BiConsumer接口,其中一个应用场景是迭代map的entry:

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 ,分别接收两个参数,其中之一是泛型,另一个是基本类型。

Predicate 函数接口

在数学逻辑中,谓词是一个函数,它接收一个值并返回一个布尔值。Predicate函数接口是一个特殊函数接口,其接收一个泛型类型值,返回一个boolean。典型的应用场景是过滤集合的值:

List<String> names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");
 
List<String> namesWithA = names.stream()
  .filter(name -> name.startsWith("A"))
  .collect(Collectors.toList());

在上面的代码中,我们使用流API过滤list的值名称以A开头的,Predicate实现即过滤逻辑代码。和前面示例一样,IntPredicate, DoublePredicate 和 LongPredicate 几个接口接收基础类型。

Operator 函数接口

Operator 接口是Function接口的特殊情况,接收和返回类型相同。UnaryOperator接口接收单个参数,其中一个应用是Collection Api的替换list中所有值,使用相同类型的计算值:

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

List.replaceAll函数返回void类型,因为其替换一定位置的值。为了实现该目的,用于转换list值的lambda必须返回与其入参类型相同的结果。这就是为什么UnaryOperator在这里很有用。当然也可以使用方法引用代替lambda:

names.replaceAll(String::toUpperCase);

BinaryOperator接口一个最有趣的用例是reduce操作。如计算整型集合值之和。使用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函数接口。该接口参数是相同类型的一对值,函数包括逻辑实现连接两者称为一个相同类型的值。传入函数必须具有结合性,即与值得计算顺序无关,如应满足下面条件:

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

BinaryOperator 函数的结合性使得并行计算很容易。当然也针对基本类型的UnaryOperator 和 BinaryOperator,依次命名为 DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator 和 LongBinaryOperator。

传统的函数接口

不是所有的函数接口都来自Java 8 。很多之前版本的接口满足函数接口的条件则可以用作lambda表达式。典型的例子是并行API的 Runnable 和 Callable 接口。在Java 8 中这些接口使用@FunctionalInterface进行标记,这使得并发代码大大得到简化:

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

总结

本文我们描述Java Api提供可以作为lambda表达的不同函数式接口,并通过示例说明其应用场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值