在你应用程序中使用lambda表达式

探索java.util.function包

在Java SE 8中引入lambda表达式时,JDK API进行了重大重写。JDK 8在引入lambda之后更新的类比JDK 5在引入泛型之后更新的类更多。

由于函数式接口的定义非常简单,许多现有的接口无需修改就能实现功能。对于您现有的代码也是如此:如果您的应用程序中有在Java SE 8之前编写的接口,那么它们可能不需要修改就可以正常运行,这使得使用lambda实现它们成为可能。

JDK 8还引入了一个新的包:java.util.function,其中提供了可在应用程序中使用的函数式接口。这些函数式接口在JDK API中也大量使用,特别是在Collections框架和Stream API中。这个包在 java.base模块中。

由于有40多个接口,这个包一开始可能看起来有点可怕。它是围绕四个主要接口组织的。理解了它们,你就有了理解其他所有内容的关键点。

使用Supplier创建或提供对象
实现Supplier接口

第一个接口是Supplier接口。简而言之,supplier 不接受任何参数并返回一个对象。

我们实际上应该说:实现supplier接口的lambda不接受任何参数并返回一个对象。走捷径能让事情更容易记住,只要它们不让人困惑。

这个接口非常简单:它没有默认或静态方法,只有一个普通的get()方法。这是这个interface:

@FunctionalInterface
public interface Supplier<T> {

    T get();
}

下面的lambda是这个接口的实现:

Supplier<String> supplier = () -> "Hello Duke!";`

这个lambda表达式简单地返回Hello Duke!字符串的字符。你也可以编写一个supplier,在每次调用时返回一个新对象:

Random random = new Random(314L);
Supplier<Integer> newRandom = () -> random.nextInt(10);

for (int index = 0; index < 5; index++) {
    System.out.println(newRandom.get() + " ");
}

调用该supplier的get()方法将调用random. nextint(),并将产生随机整数。由于这个随机生成器的种子固定为值314L,您应该看到生成的随机整数如下:

1 
5 
3 
0 
2

注意,这个lambda捕获了一个来自外围作用域的变量:random,使得这个变量实际上是final。

使用supplier

注意在前面的例子中你是如何使用newRandom supplier 生成随机数的:

for (int index = 0; index < 5; index++) {
    System.out.println(newRandom.get() + " ");
} 

调用Supplier接口的get()方法将调用您的lambda。

使用特殊的Suppliers

Lambda表达式用于处理应用程序中的数据。因此,在JDK中,lambda表达式的执行速度非常关键。任何可以节省的CPU周期都必须节省,因为这可能代表实际应用程序中的重大优化。

遵循这一原则,JDK API还提供了专门的、优化的Supplier接口版本。

您可能已经注意到,我们的第二个示例提供了Integer类型,其中Random.nextInt()方法返回一个int。所以在你写的代码中,有两件事在幕后发生:

  • 由Random.nextInt()返回的int首先通过自动装箱机制装箱成一个Integer;
  • 然后,通过自动拆箱机制,在将该Integer赋给nextRandom变量时将其拆箱。

自动装箱是一种将int值直接赋给Integer对象的机制:

int i = 12;
Integer integer = i; 

在后台,将为您创建一个对象,封装该值。

自动拆箱则相反。你可以将一个Integer赋给一个int值,通过在Integer中拆包装:

Integer integer = Integer.valueOf(12); 
int i = integer;

这个装箱/拆箱不是没有代价的。大多数情况下,与应用程序正在做的其他事情(如从数据库或远程服务获取数据)相比,这种成本很小。但在某些情况下,这种成本可能是不可接受的,你需要避免支付它。

好消息是:JDK提供了一个带有IntSupplier接口的解决方案。这是这个接口:

@FunctionalInterface
public interface IntSupplier {

    int getAsInt();
}

注意,你可以使用完全相同的代码来实现这个接口:

Random random = new Random(314L);
IntSupplier newRandom = () -> random.nextInt();

对应用程序代码的唯一修改是,你需要调用getAsInt()而不是get():

for (int i = 0; i < 5; i++) {
    int nextRandom = newRandom.getAsInt();
    System.out.println("next random = " + nextRandom);
} 

运行这段代码的结果是相同的,但这次没有发生装箱/解装箱:这段代码比前一段代码性能更好。

JDK提供了四个这样的特殊suppliers,以避免在应用程序中不必要的装箱/拆箱:IntSupplier、BooleanSupplier、LongSupplier和doublessupplier。

您将看到更多特殊版本的函数式接口来处理基本类型。他们的抽象方法有一个简单的命名约定:采用主抽象方法的名称(在supplier的情况下是get()),并向其添加返回类型。因此,对于supplier接口,我们有:getAsBoolean()、getAsInt()、getAsLong()和getAsDouble()。

使用Consumer来消费对象
实现和使用Consumers

第二个接口是Consumer接口。consumer 的做法与supplier相反:它接受一个参数,但不返回任何东西。

接口稍微复杂一点:其中有默认方法,本教程后面将介绍这些方法。让我们集中讨论它的抽象方法:

@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);
    
    // default methods removed
}

你已经实现了消费者:

Consumer<String> printer = s -> System.out.println(s);

你可以用这个消费者来修改前面的例子:

for (int i = 0; i < 5; i++) {
    int nextRandom = newRandom.getAsInt();
    printer.accept("next random = " + nextRandom);
} 

使用特殊的Consumers

假设您需要打印整数。然后你可以写以下消费者:

Consumer printer = i -> System.out.println(i);

那么您可能会面临与supplier 示例相同的自动装箱问题。在您的应用程序中,这种装箱/拆箱方式是否可以接受?

如果不是这样,也不要担心,JDK已经为您提供了三种特定的消费者:IntConsumer、LongConsumer和DoubleConsumer。这三个消费者的抽象方法遵循与supplier 相同的约定,因为返回的类型总是void,所以它们都被命名为accept。

使用BiConsumer消费两个元素

然后JDK添加了Consumer接口的另一个变体,它接受两个而不是一个参数,很自然地称为BiConsumer接口。这是这个接口:

@FunctionalInterface
public interface BiConsumer<T, U> {

    void accept(T t, U u);

    // default methods removed
}

下面是一个biconsumer的例子:

BiConsumer<Random, Integer> randomNumberPrinter = 
        (random, number) -> {
            for (int i = 0; i < number; i++) {
                System.out.println("next random = " + random.nextInt());
            }
        };

你可以使用这个biconsumer 来编写前面的例子:

randomNumberPrinter.accept(new Random(314L), 5));

BiConsumer<T, U>接口有三个特殊版本来处理基本类型:ObjectIntConsumer、ObjectLongConsumer和ObjectDoubleConsumer。

将Consumer传递给Iterable

集合框架的接口中添加了几个重要的方法,本教程的另一部分将介绍这些方法。其中一个方法以Consumer作为参数,非常有用:Iterable.forEach()方法。下面是一个简单的例子,你可以在任何地方看到:

List<String> strings = ...; // really any list of any kind of objects
Consumer<String> printer = s -> System.out.println(s);
strings.forEach(printer);

最后一行代码将把consumer 应用到列表的所有对象。在这里,它将简单地在控制台上一个一个地打印它们。在后面的部分中,您将看到编写这个消费者的另一种方法。

这个forEach()方法公开了一种访问任何Iterable所有元素的内部迭代的方法,传递您需要对每个元素采取的操作。这是一种非常强大的方法,而且它还使您的代码更具可读性。

使用Predicate测试对象
实现和使用Predicates

第三个接口是Predicate接口。predicate 用于测试对象。它用于在Stream API中过滤流,稍后您将看到这个主题。

它的抽象方法接受一个对象并返回一个布尔值。这个接口也比Consumer稍微复杂一点:在它上面定义了默认方法和静态方法,稍后您将看到这些。让我们集中讨论它的抽象方法:

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);
    
    // default and static methods removed
}

在前面的部分中,您已经看到了Predicate的示例:

Predicate<String> length3 = s -> s.length() == 3;

要测试给定的字符串,你只需要调用Predicate接口的test()方法:

String word = ...; // any word
boolean isOfLength3 = length3.test(word);
System.out.prinln("Is of length 3? " + isOfLength3);

使用特殊的Predicates

假设您需要测试整数值。你可以写以下predicate:

Predicate<Integer> isGreaterThan10 = i -> i > 10;

对于consumers、consumers和这个predicate也是如此。该predicate接受的参数是对Integer类实例的引用,因此在将该值与10进行比较之前,该对象将自动拆箱。它很方便,但有一定的开销。

JDK提供的解决方案与为suppliers 和consumers提供的解决方案相同:特殊的predicate。除了Predicate外,还有三个专门的接口:IntPredicate、LongPredicate和DoublePredicate。它们的抽象方法都遵循命名约定。因为它们都返回一个布尔值,所以它们被命名为test()并接受一个与接口对应的参数。

所以你可以这样写前面的例子:

IntPredicate isGreaterThan10 = i -> i > 10;

您可以看到lambda本身的语法是相同的,唯一的区别是i现在是一个int类型而不是Integer。

用BiPredicate测试两个元素

遵循您在Consumer中看到的约定,JDK还添加了一个BiPredicate接口,它测试两个而不是一个元素。接口如下:

@FunctionalInterface
public interface BiPredicate<T, U> {

    boolean test(T t, U u);
    
    // default methods removed
}

下面是这样一个bipredicate的例子:

Predicate<String, Integer> isOfLength = (word, length) -> word.length() == length;

你可以用下面的模式来使用这个bipredicate :

String word = ...; // really any word will do!
int length = 3;
boolean isWordOfLength3 = isOfLength.apply(word, length); 

没有特殊版本的BiPredicate来处理基本类型。

将Predicate 传递给集合

添加到集合框架中的一个方法接受一个predicate:removeIf()方法。此方法使用此predicate来测试集合的每个元素。如果测试结果为true,则从集合中删除该元素。

你可以在下面的例子中看到这个模式的作用:

List<String> immutableStrings = 
        List.of("one", "two", "three", "four", "five");
List<String> strings = new ArrayList<>(immutableStrings);
Predicate<String> isOddLength = s -> s.length() % 2 == 0;
strings.removeIf(isOddLength);
System.out.println("strings = " + strings);

运行此代码将产生以下结果:

strings = [one, two, three]

在这个例子中有几点值得指出:

  • 如您所见,调用removeIf()会改变这个集合。
  • 因此,不应该对不可变集合调用removeIf(),比如List.of()工厂方法生成的集合。如果您这样做,您将会得到一个异常,因为您不能从不可变集合中删除元素。
  • aslist()产生一个行为类似数组的集合。您可以更改其现有元素,但不允许从该工厂方法返回的列表中添加或删除元素。所以在这个列表上调用removeIf()也不起作用
使用Function<T, R> 将一个对象映射为另一个对象
Function的实现和使用

第四个接口是Function接口。function 的抽象方法接受类型为T的对象,并返回该对象到任何其他类型u的转换。这个接口也有默认方法和静态方法。

@FunctionalInterface
public interface Function<T, U> {
    
    R apply(U u);
    
    // default and static methods removed
}

在Stream API中使用Functions 将对象映射到其他对象,这个主题将在后面介绍。predicate 可以被视为返回布尔值的特殊的Function类型。

使用特殊的Functions

这是一个function 的例子,它接受一个字符串并返回该字符串的长度。

Function<String, Integer> toLength = s -> s.length();
String word = ...; // any kind of word will do
int length = toLength.apply(word);

在这里,您可以再次发现正在运行的装箱和拆箱操作。首先,length()方法返回一个int类型。因为函数返回一个Integer,所以这个int被装箱了。但是,然后结果被赋给一个int类型的变量length ,因此Integer被拆箱存储在这个变量中。

如果在您的应用程序中性能不是一个问题,那么这种装箱和解装箱真的不是一个大问题。如果是,你可能会想要避免它。

JDK为您提供了解决方案,Function<T, U>接口的特殊版本。这组接口比我们在Supplier、Consumer或Predicate类别中看到的接口更复杂,因为为输入参数的类型和返回类型都定义了特殊化Function。

输入参数和输出参数都可以有四种不同的类型:

  • 参数化类型T;
  • 一个int;
  • 一个long;
  • 一个double。

事情还没有到此为止,因为API的设计很微妙。有一个特殊的接口:UnaryOperator,它继承了Function<T, T>。这个一元运算符概念用于命名接受给定类型实参并返回相同类型结果的函数。一元运算符正是您所期望的。所有经典的数学运算符都可以用一元运算符建模:平方根、所有三角运算符、对数和指数运算符。

下面是您可以在java.util.function包中找到的16种特殊类型的function。

Parameter typesTintlongdouble
TUnaryOperatorIntFunctionLongFunctionDoubleFunction
intToIntFunctionIntUnaryOperatorLongToIntFunctionDoubleToIntFunction
longToLongFunctionIntToLongFunctionLongUnaryOperatorDoubleToLongFunction
doubleToDoubleFunctionIntToDoubleFunctionLongToDoubleFunctionDoubleUnaryOperator

这些接口的所有抽象方法遵循相同的约定:它们以该函数的返回类型命名。以下是他们的名字:

  • apply()用于返回泛型类型T的函数
  • 如果返回原始类型int,则使用applyAsInt()
  • long applyAsLong ()
  • double applyAsDouble ()
将一元操作符传递给List

您可以使用UnaryOperator转换列表中的元素。有人可能想知道为什么是UnaryOperator而不是基本Function。答案实际上非常简单:一旦声明,就不能更改列表的类型。因此,您所应用的函数可以更改列表中的元素,但不能更改它们的类型。

接受此一元操作符的方法将其传递给replaceAll()方法。下面是一个例子:

List<String> strings = Arrays.asList("one", "two", "three");
UnaryOperator<String> toUpperCase = word -> word.toUpperCase();
strings.replaceAll(toUpperCase);
System.out.println(strings);

运行此代码将显示以下内容:

[ONE, TWO, THREE]

注意,这一次我们使用的是使用Arrays.asList()模式创建的列表。实际上,您不需要向该列表添加或删除任何元素:这段代码只是一个一个地修改每个元素,对于这个特定的列表,这是可能的。

用BiFunction映射两个元素

至于consumer 和predicate,functions 也有接受两个参数的版本:bifunction。接口是BiFunction<T, U, R>,其中T和U是参数,R是返回类型。接口如下:

@FunctionalInterface
public interface BiFunction<T, U, R> {
    
    R apply(T t, U u);
    
    // default methods removed
}

你可以用lambda表达式创建一个bifunction :

BiFunction<String, String, Integer> findWordInSentence = 
    (word, sentence) -> sentence.indexOf(word);

UnaryOperator接口也有一个具有两个参数的兄弟接口:BinaryOperator,它继承了BiFunction<T, T, T>。如您所料,这四个基本算术运算可以用BinaryOperator建模。

JDK中添加了所有可能的bifunction 特殊化版本的子集:

  • IntBinaryOperator, LongBinaryOperator和DoubleBinaryOperator;
  • ToIntBiFunction, ToLongBiFunction, ToDoubleBiFunction。
包装四类函数式接口

java.util.function包现在是Java的中心,因为您将在Collections Framework或Stream API中使用的所有lambda表达式都实现了来自该包的一个接口。

如您所见,这个包包含许多接口,在那里找到方法可能很棘手。

首先,你需要记住的是,有4种类型的接口:

  • suppliers:不接受任何参数,返回数据
  • consumers:接受一个参数,不返回任何数据
  • predicates:接受一个参数,返回一个布尔值
  • functions:接受一个参数,返回一些数据

其次:有些版本的接口接受两个参数而不是一个:

  • biconsumers
  • bipredicates
  • bifunctions

第三:一些接口有特殊的版本,添加这些版本是为了避免装箱和解装箱。太多了,无法一一列举。它们以它们接受的类型(例如:IntPredicate)或它们返回的类型(例如:ToLongFunction)命名。它们可以同时以两个名字命名:IntToDoubleFunction。

最后:对于所有类型都相同的情况,还有Function<T, R>和BiFunction<T, U, R>的扩展:UnaryOperator和BinaryOperator,以及基本类型的特殊版本。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值