为了账号安全,请及时绑定邮箱和手机立即绑定

Java Stream API: 探索映射操作功能

标签:
Java
流API允许我们以各种方式操作数据。最常见的情况之一是将数据从一种形式或类型转换为另一种形式——这个过程被称为映射操作。Java流API提供多种映射操作。在这篇教程中,我们将通过示例来探讨这些操作的具体应用。

Stream API中的所有映射操作都是中间操作。通常,映射操作具有无状态性,因为它们独立处理流中的每个元素,互不影响。然而,在罕见和自定义的场景中,某些映射操作可能会引入状态性,尽管这种情况在标准实现中很少见,并且通常不被鼓励,因为它可能导致意外结果。

最近我发布了一篇关于Stream API中间操作的教程,这里就不再赘述这些细节了。如果您有兴趣,可以通过下面的链接查看。

Java Stream API:探索中间操作(Intermediate Operations)中间操作在流处理中起着至关重要的作用,Stream API 提供了各种强大的中间操作……更多内容请访问 levelup.gitconnected.com

现在,不再多说,让我们直接来探索映射功能。

准备工作

本教程将以实例为导向,因此,我将创建一个记录类(Record 类),用于后续示例。

    public record CustomerPurchase(String ID,  
                                   String 客户ID,  
                                   List<String> 产品ID列表,  
                                   int 数量,  
                                   double 价格,  
                                   String 支付方式) {  

    }

若想了解更多关于记录资料的信息,请参阅下方链接的更多详情:

Java 记录:Java 编程语言常常因为过于冗长而受到批评。一个常见的例子是创建一个简单的……levelup.gitconnected.com

让我们基于这条记录创建对象列表:

    private static List<Transaction> transactions = List.of(  
            new Transaction("1",  
                    "customer1",  
                    List.of("笔记本电脑", "耳机"),  
                    1150.00元,  
                    LocalDateTime.of(2024, 8, 20, 10, 19)),  
            new Transaction("2",  
                    "customer2",  
                    List.of("智能手机"),  
                    600.99元,  
                    LocalDateTime.of(2024, 8, 21, 1, 45)),  
            new Transaction("3",  
                    "customer1",  
                    List.of("显示器", "键盘", "鼠标"),  
                    380.00元,  
                    LocalDateTime.of(2024, 8, 22, 3, 30))  
    );
映射操作

我们将首先探索的操作当然是 map() 方法。

<R> 流<R> 映射到(Function<? super T, ? extends R> 函数器);

map() 方法接受一个 Function,该函数将流中的每个元素从一种类型转换为另一种类型,从而生成一个新的流,其中元素已被映射。例如,可以将字符串流映射为整数流。

    Stream<String> exampleStream1 = Stream.of("1", "2", "3", "4", "5");  
    Stream<Integer> mappedStream1 = exampleStream1  
            //.map(s -> Integer.parseInt(s)) -- 等效的 Lambda 表达式版本  
            .map(Integer::parseInt);  
    List<Integer> example1 = mappedStream1.toList();  
    // [1, 2, 3, 4, 5] - 结果列表

它可以用于将对象的集合转换为这些对象特定属性值的集合,如下例所示:将这些对象的特定属性值转换为集合。

Set<String> example2 = transactions.stream()  
            .map(Transaction::customerId)  
            .collect(Collectors.toSet());  
    // [customer2, customer1]

在这个例子中,我们将交易映射到与每个交易相关的客户ID,并将它们收集到一个集合中以消除重复。这导致了一个包含唯一客户ID的集合。

数字流

除了 Stream 接口外,Java Stream API 还提供了针对特定数值类型的专用流接口,例如 IntStreamLongStreamDoubleStream。这些数值流提供了针对各自类型的特定方法,确保高效处理。例如,IntStream 提供了 mapToInt 方法,LongStream 则提供了 mapToLong 方法等。您可以在这里了解更多关于 Stream API 的结构:

Java Stream API:探索创建流的方法Java 8 引入了 Stream API,这彻底改变了语言的处理方式,使得既可以通过命令式也可以通过函数式……levelup.gitconnected.com 映射到数值型

Stream 接口还提供了将对象流转换为数值流的方法。

    // 将流中的元素映射为整数流
    IntStream mapToInt(ToIntFunction<? super T> mapper);  
    // 将流中的元素映射为长整数流
    LongStream mapToLong(ToLongFunction<? super T> mapper);  
    // 将流中的元素映射为双精度浮点数流
    DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);

一个常见的用例是求所有交易的总值。为此,我们创建一个交易流,并将每个交易映射为其总值。由于总值是一个基本的 double 类型,我们需要将对象流转换为原始值流。这可以通过使用 mapToDouble 方法来实现,这使得我们可以高效地对这些值进行累加。

    double sumExample3 = transactions.stream()  
            .mapToDouble(Transaction::totalAmount)  
            .sum();  
    // 计算所有交易的总金额(结果为 2130.99)

同样,我们可以使用 mapToIntmapToLong 分别用于处理整数类型的 int 流和长整数类型的 long 流。

扁平化过程

flatMap() 方法是 Java Stream API 中的一个强大工具,它可以将流中的每个元素转换为另一个流中的元素,然后将这些流扁平化为一个单一的流。当您有一个由集合或数组组成的流,并希望单独处理这些元素而不是处理这些嵌套结构中的元素时,通常会使用它。

    <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper); // `flatMap` 平铺映射,`Stream` 流

例如,如果你有一个列表流(Stream of lists),flatMap() 可以将它转换成一个包含这些列表中所有元素的流,从而可以直接对每个元素执行进一步操作。此操作特别适用于处理复杂的结构化数据,使其更容易以更流畅和函数式的方式处理。

在下面的例子中,我们有一个列表的流。flatMap 操作将方法引用应用于每个列表,将其转换为该列表元素的流。这些单独的流随后被扁平化为一个单一的流,从而可以无缝地一起处理所有元素。

    Stream<List<Integer>> 示例4流 = Stream.of(List.of(1, 2), List.of(3, 4));  
    Stream<Integer> 扁平化示例4 = 示例4流  
            .flatMap(List::stream);  
    List<Integer> 整数列表示例4 = 扁平化示例4.toList();  
    // 结果为: [1, 2, 3, 4]

也可以使用数组来完成这个任务。

    List<String> example5 = Stream.of(new String[]{"a", "b"},  
                                      new String[]{"c", "d"})  
            .flatMap(Arrays::stream)  
            .toList();  
    // 生成的列表为:[a, b, c, d]

我们甚至可以尝试这个大胆的例子:

    List<Integer> example5 = Stream.of(Stream.of(1, 2),   
                                       Stream.of(3, 4))  
            .flatMap(s -> s)  
            .toList();  
    // [1, 2, 3, 4]

在这个例子中,我创建了一个流的集合,并在处理流程中应用了flatMap。由于我们已经处理了一个流的集合,因此不需要任何额外的转换——每个流都被直接通过。这个例子纯粹是为了演示目的而设计的,我认为它有效地展示了flatMap()的工作方式。

我们可以用一个实际的例子来说明,例如我们将所有交易中的所有产品汇总到一个列表中。

    List<String> example7 = transactions.stream()  
            .map(Transaction::productIds)  
            .flatMap(List::stream)  
            .toList();  // [笔记本电脑, 耳机, 智能手机, 显示器, 键盘, 鼠标]
多映射操作

mapMulti() 方法是 Java 流式API 中的一种灵活的 flatMap() 替代方法,它在 Java 16 中被引入,。它允许将流中的每个元素映射为多个元素,但提供了更大的控制能力来生成这些元素。

    Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper) {...}

与需要为每个输入元素返回一个流的 flatMap() 不同,mapMulti() 更加灵活,它使用一个 BiConsumer,允许你直接将多个元素添加到结果流中,而无需为每个输入元素创建新的流。这种方法在需要从单个输入生成多个输出的场景中更为高效,特别是在输出数量变化时。

mapMulti() 尤其在您希望根据条件转换元素或生成复杂的输出结果时特别有用,使其成为高级流处理中的强大工具。

    List<String> example8 = Stream.of("a", "b", "c", "d")  
            .<String>mapMulti((str, consumer) -> {  
                consumer.accept(str);  
                consumer.accept(str.toUpperCase());  
            })  
            .toList();  
    // 将每个字符串及其大写形式添加到列表中,结果为 [a, A, b, B, c, C, d, D]

在上述例子中,每个字母都被替换成它本身和它的大写形式。

Stream 接口提供了将流展平为特定类型的数值流的方法,提供了以下方法:

平铺映射为整数流 flatMapToInt(函数<? super T, ? extends IntStream> mapper);  
平铺映射为长整数流 flatMapToLong(函数<? super T, ? extends LongStream> mapper);  
平铺映射为双精度浮点数流 flatMapToDouble(函数<? super T, ? extends DoubleStream> mapper);
将数值流转换为其他类型的流

IntStream接口提供了如下方法:

    映射到长整(mapToLong整到长整函数映射器)  
    映射到双精度(mapToDouble整到双精度函数映射器)

通过这些方法,我们可以轻松地做到改变一种流的性质。

    IntStream 示例9 = IntStream.of(1, 2, 3);  
    示例9  
            .mapToLong(Long::valueOf);  

    IntStream 示例10 = IntStream.of(1, 2, 3);  
    示例10  
            .mapToDouble(Double::valueOf);

以下方法由LongStream接口提供:

    IntStream mapToInt(LongToIntFunction 转换器函数);  
    DoubleStream mapToDouble(LongToDoubleFunction 转换器函数);

我们可以这样使用它:

longStream example11 = LongStream.of(1L, 2L, 3L);  
IntStream longStreamExample11 = example11  
        .mapToInt(n -> (int) n);  
longStream example12 = LongStream.of(1L, 2L, 3L);  
DoubleStream doubleStreamExample12 = example12  
        .mapToDouble(Double::valueOf);

DoubleStream接口提供了以下方法:

    IntStream mapToInt(DoubleToIntFunction 双到整型函数);  
    LongStream mapToLong(DoubleToLongFunction 双到长整型函数);

我们可以像这样使用它:

(在此处插入具体示例)
    DoubleStream 示例13 = DoubleStream.of(1.1, 2.2, 3.3);  
    IntStream longStreamExample13 = 示例13  
            .mapToInt(n -> (int) Math.round(n));  
    DoubleStream 示例14 = DoubleStream.of(1.1, 2.2, 3.3);  
    LongStream doubleStreamExample14 = 示例14  
            .mapToLong(Math::round);
箱形操作

在 Java 中,将基本类型(如 intchardouble)自动转换为其对应的包装类对象(如 IntegerCharacterDouble)称为 自动装箱。编译器自动执行此过程。相反的过程,即将包装类对象转换回基本类型,则称为 自动拆箱

每种数字类型流都提供了一个 boxed 操作,该操作将每个元素转换成其对应的包装类对象。

IntStream

    Stream<Integer> 装箱();

LongStream:

    Long 装箱 Stream;

双流数据流:

    Stream<Double> 装箱();

此操作的示例如下:

    IntStream example15 = IntStream.of(1, 2, 3);  
    Stream<Integer> 包装example15 = example15.boxed();  

    LongStream example16 = LongStream.of(1L, 2L, 3L);  
    Stream<Long> 包装example16 = example16.boxed();  

    DoubleStream example17 = DoubleStream.of(1.1, 2.2, 3.3);  
    Stream<Double> 包装example17 = example17.boxed();

实际上,你可以使用boxed方法将int转换为Integer,并使用int方法将Integer转换回int。同样的方法也适用于longdouble,分别使用mapToLongmapToDouble方法。这使得在流中可以在基本类型和它们的包装类之间实现无缝转换。

对象操作映射

我们也可以使用 mapToObj 方法将任何数值流转换为对象流。每个数值流接口(IntStreamLongStreamDoubleStream)都提供了各自版本的此方法。

IntStream:

返回一个流,该流中的每个元素都是通过整数函数映射得到的新对象。

LongStream:

<U> Stream<U> 映射到对象(Long函数<? extends U> mapper);

双流处理:

<U> Stream<U> mapToObj(DoubleFunction<? extends U> mapper); // mapToObj 方法将 DoubleFunction 映射到 Stream 中的每个元素

我们可以像这样使用它:

    IntStream example18 = IntStream.of(1, 2, 3);  
    Stream<Integer> integerStreamExample18 = example18  
            .mapToObj(Integer::valueOf);  
    LongStream example19 = LongStream.of(1L, 2L, 3L);  
    Stream<Long> longStreamExample19 = example19  
            .mapToObj(Long::valueOf);  
    DoubleStream example20 = DoubleStream.of(1.1, 2.2, 3.3);  
    Stream<Double> doubleStreamExample20 = example20  
            .mapToObj(Double::valueOf);

你可能想知道boxed()方法能否达到这个目的,你的想法是正确的。实际上,boxed()方法内部使用mapToObj来执行转换。

以下完整代码如下所示:
如下链接可找到完整代码:

GitHub - polovyivan/java-streams-mapping-operations通过在 GitHub 上创建一个账户,您可以为 polovyivan/java-streams-mapping-operations 项目做出贡献。 结论部分

如果你是一名Java程序员或者计划成为一名Java程序员,你一定会遇到映射运算。掌握并理解其工作原理是必不可少的。本教程旨在帮助你掌握这一点。

感谢您的阅读!如果您喜欢这篇文章,请点赞并关注这篇文章。如果您有任何问题或建议,欢迎在评论区留言或通过我的LinkedIn个人资料与我联系。账户

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消