Java SE 8新功能介绍:使用Streams API处理集合

使用Java SE 8 Streams的代码更干净,易读且功能强大.....

“ Java SE 8新功能介绍”系列的这篇文章中,我们将深入解释和探索代码,以了解如何使用流遍历集合,从集合和数组创建流,聚合流值。

在上一篇文章“ Lambda的遍历,过滤,处理集合和方法增强 ”中; 我已深入探讨和探索如何使用lambda表达式和方法引用 遍历集合,使用谓词接口过滤它们,在接口中实现默认方法,最后在接口中实现静态方法

  • 源代码托管在我的Github帐户上:从此处克隆它。

目录

  1. 使用流遍历集合。
  2. 从集合和数组创建流。
  3. 汇总流值。

1.使用流遍历集合

介绍

Java的collections框架使您可以使用ListMap类的接口以及ArraylistHashMap类的类来轻松管理应用程序中数据的有序和无序集合。 自从首次引入以来, 收藏框架一直在不断发展。 在Java SE 8中,我们现在有了一种使用流API 管理遍历聚合集合的新方法。 基于集合的流与输入或输出流不同。

怎么运行的

相反,这是处理整个数据而不是单独处理每个项目的新方法。 使用流时,无需担心循环或遍历的细节。 您直接从集合创建流对象。 然后,您可以使用它进行各种操作,包括遍历,过滤和汇总其值。 我将从eg.com.tm.java8.features.stream.traversing项目的Java8Features包中的该示例开始。 在类代码SequentialStream ,在Java SE 8中,有两种收集流,称为顺序流和并行流。

List<person> people = new ArrayList<>();
 
people.add(new Person("Mohamed", 69));
people.add(new Person("Doaa", 25));
people.add(new Person("Malik", 6));
 
Predicate<person> pred = (p) -> p.getAge() > 65;
 
displayPeople(people, pred);
 
...........
 
private static void displayPeople(List<person> people, Predicate<person> pred) {
 
     System.out.println("Selected:");
     people.forEach(p -> {
         if (pred.test(p)) {
             System.out.println(p.getName());
         }
     });
}

顺序流是两者中较简单的一种,就像迭代器一样,它使您可以一次处理一个集合中的每个项目。 但是语法比以前少了。 在这段代码中,我创建了一个人数组列表,并转换为列表。 它具有一个复杂对象的三个实例,一个名为Person的类。 然后,我使用Predicate声明条件,并显示仅满足条件的人员。 在displayPeople()方法的第48至52行中,我遍历集合,遍历数据,并一次测试一项。 运行代码,您应该得到以下结果:

Selected:
Mohamed

我将向您展示如何使用流对象重构该代码。 首先,我将注释掉这些代码行。 现在,在注释的代码下面,我将从集合对象开始。 人。 然后,我将调用一个名为stream的新方法。 与集合本身一样,流对象也具有通用声明。 如果从集合中获取流,则该流中的项目与集合本身的类型相同。 我的集合具有person类的实例,因此流使用相同的泛型类型。

System.out.println("Selected:");
 //        people.forEach(p -> {
 //            if (pred.test(p)) {
 //                System.out.println(p.getName());
 //            }
 //        });
 
  people.stream().forEach(p -> System.out.println(p.getName()));
}

您将流作为方法调用,现在有了一个可以处理的流对象。 我将从对每个方法的四个简单调用开始,这将需要一个Lamda表达式。 我将传递参数。 这就是我这次通过迭代处理的列表中的项目。 然后是Lambda运算符,然后执行该方法。 我将使用简单的系统输出,并输出此人的姓名。 我将保存并运行代码,并得到结果。 由于不再过滤,因此我将显示列表中的所有人员。

Selected:
Mohamed
Doaa
Malik

现在,一旦有了流,就可以轻松地使用谓词对象。 当我为每种方法使用并一次处理每个项目时。 我必须显式调用谓词的测试方法。 但是,使用流可以调用名为filter的方法。 那需要一个谓词对象,并且所有谓词都有一个测试方法,因此它已经知道如何调用该方法。 因此,我将对此代码进行一些分解。 我将对.forEach()方法的调用向下移动几行,然后在中间的空行上调用新的filter方法。

people.stream()
     .filter(pred)
     .forEach(p -> System.out.println(p.getName()));

filter方法需要谓词接口的实例。 然后我将传入谓词对象。filter方法返回流,但现在返回过滤后的版本,从那里我可以调用forEach()方法。 我将运行代码,现在我只显示满足谓词条件的集合中的项目。 您可以在流中做更多的事情。 查看Java SE 8 API文档中有关流的文档。

Selected:
Mohamed

您将看到,除了过滤之外,您还可以对流进行聚合并执行各种其他操作。 在结束本演示之前,我想向您展示顺序流和并行流之间的非常重要的区别。 Java SE 8中的流API的目标之一是让您分拆具有多个CPU的系统上的处理。 Java运行时会自动处理这种多CPU处理。 您需要做的就是将顺序流转换为并行流。

有两种语法上的实现方式。 我将复制我的顺序流类。 我将转到程序包浏览器,然后将其复制并粘贴。 我将命名新类ParallelStream 。 我将开设新班。 在此版本中,我将删除注释的代码。 我不再需要了 现在,这里有两种创建并行流的方法。 一种方法是从集合中调用另一种方法。 代替流,我将调用parallelStream() 。 现在,我有了一个流,该流将自动分解并分配给不同的处理器。

private static void displayPeople(List<person> people, Predicate<person> pred) {
     System.out.println("Selected:");
     people.parallelStream()
             .filter(pred)
             .forEach(p -> System.out.println(p.getName()));
 }

我将运行代码,然后看到它在做同样的事情,过滤并返回数据。

Selected:
Mohamed

这是创建并行流的另一种方法。 我将再次调用此stream()方法。 然后从流方法中,我将调用一个名为parallel()的方法,该方法完全相同。 我从顺序流开始,最后以并行流结束。 它仍然是一条小溪。 它仍然可以过滤,仍然可以按照与以前完全相同的方式进行处理。 但是现在它将在可能的情况下分解。

people.stream()
      .parallel()
      .filter(pred)
      .forEach(p -> System.out.println(p.getName()));

结论

对于何时在顺序流上使用并行流没有明确的规定。 它取决于数据的大小和复杂性以及硬件的功能。 您正在运行的多CPU系统。 我唯一可以给您的建议是尝试使用您的应用程序和数据。 设置基准,确定运行时间。 使用顺序流和并行流,看看哪种效果更好。

2.从集合和数组创建流

介绍

Java SE 8的流API旨在帮助您管理数据集合,即集合框架的成员,例如数组列表或哈希图。 但是您也可以直接从数组创建流。

怎么运行的

Java8Features这个项目中,在例如eg.com.tm.java8.features.stream.creating包中,我有一个名为ArrayToStream的类。 在其主要方法中,我创建了一个包含三个项目的数组。 它们分别是我的复杂对象Person类的实例。

public static void main(String args[]) {
 
    Person[] people = {
        new Person("Mohamed", 69),
        new Person("Doaa", 25),
        new Person("Malik", 6)};
    for (int i = 0; i < people.length; i++) {
        System.out.println(people[i].getInfo());
    }
}

此类具有专用字段的setter和getter以及新的getInfo()方法,以返回串联的字符串。

public String getInfo() {
    return name + " (" + age + ")";
}

现在,如果您想使用流来处理此数组,您可能会认为可能需要将其转换为数组列表,然后再从那里创建该流。 但是事实证明,有两种方法可以直接从数组转换为流。 这是第一种方法。 我将不需要这三行代码来处理数据。 因此,我将其注释掉。 然后在这里,我将声明一个对象,其类型为流。

Stream是接口,它是java.util.stream的成员。 当我按Ctrl + Space并从列表中选择它时,系统会询问我该流将管理的项目的通用类型。 这些将是Person类型的项目,就像数组本身中的项目一样。 我将用小写字母命名新的流对象stream。 这是创建流的第一种方法。 再次使用流接口,并调用名为of()的方法。 请注意,有几个不同的版本。

一个带有单个对象,另一个带有一系列对象。 我将使用带有一个参数的参数,然后将其传递给数组people ,这就是我需要做的所有事情。 Stream.of()表示接受此数组并将其包装在流中。 现在,我可以使用lambda表达式,过滤器,方法引用和对Stream对象起作用的其他东西。 我将为每个方法调用流对象,我将传递一个lambda表达式,我将传递当前人,然后在lambda运算符之后,我将输出该人的信息。 使用对象的getInfo()方法。

Person[] people = {
        new Person("Mohamed", 69),
        new Person("Doaa", 25),
        new Person("Malik", 6)};
 
//        for (int i = 0; i < people.length; i++) {
//            System.out.println(people[i].getInfo());
//        }
        Stream<Person> stream = Stream.of(people);
        stream.forEach(p -> System.out.println(p.getInfo()));

我将保存并运行代码,并得到结果。 我以将它们放在数组中的相同顺序输出项目。 因此,这是使用Stream.of()的一种方法。

Mohamed (69)
Doaa (25)
Malik (6)

还有另一种方法可以做完全相同的事情。 我将复制该行代码,并注释掉一个版本。 这次使用Stream.of() ,我将使用一个名为Arrays的类,该类是包java.util的成员。

从那里,我将调用一个名为stream的方法。 注意,stream方法可以包装在各种类型的数组周围。 包括基本体和复杂对象。

//      Stream<person> stream = Stream.of(people);
         
        Stream<person> stream = Arrays.stream(people);
        stream.forEach(p -> System.out.println(p.getInfo()));

我将保存并运行该版本,流将执行与以前完全相同的操作。

Mohamed (69)
Doaa (25)
Malik (6)

结论

因此, Stream.of()Arrays.stream()会做完全相同的事情。 取一个原始值或复杂对象的数组,然后将它们转换为流,然后可以将其与lambda,过滤器和方法引用一起使用。

3.汇总流值

介绍

前面已经描述了如何使用流来迭代集合。 但是,您也可以使用流来聚合集合中的项目。 也就是说,计算求和平均值计数等。 当您执行此类操作时,了解并行流的本质很重要。

怎么运行的

所以我要在项目启动这个示范Java8Features ,在包eg.com.tm.java8.features.stream.aggregating 。 我将首先使用ParallelStreams类。 在此类的main方法中,我创建了一个包含字符串项目的数组列表。

我正在使用一个简单的for循环,已将10,000个项目添加到列表中。 然后在第35和36行上,我创建一个流,并为每种方法使用,并一次输出一个流。

public static void main(String args[]) {
 
    System.out.println("Creating list");
    List<string> strings = new ArrayList<>();
    for (int i = 0; i < 10000; i++) {
        strings.add("Item " + i);
    }
    strings.stream()
           .forEach(str -> System.out.println(str));
}

当我运行此代码时,我得到了预期的结果。 这些项目按照添加到列表的顺序输出到屏幕。

.........
Item 9982
Item 9983
Item 9984
Item 9985
Item 9986
Item 9987
Item 9988
Item 9989
Item 9990
Item 9991
Item 9992
Item 9993
Item 9994
Item 9995
Item 9996
Item 9997
Item 9998
Item 9999

现在,让我们看看将其转换为并行流时会发生什么。 如前所述,我可以通过调用并行流方法或通过获取流的结果并将其传递给并行来实现。

我会做后者。 现在,我正在使用并行流,该流可以分解并且工作负载可以在多个处理器之间分配。

strings.stream()
       .parallel()
       .forEach(str -> System.out.println(str));

我将再次运行代码并观察会发生什么,请注意,打印的最后一项不是列表中的最后一项。 那将是9,999。 而且,如果我在输出中滚动,我会看到处理以某种方式跳跃。 发生的事情是运行时将数据任意拆分为多个块。

.........
Item 5292
Item 5293
Item 5294
Item 5295
Item 5296
Item 5297
Item 5298
Item 5299
Item 5300
Item 5301
Item 5302
Item 5303
Item 5304
Item 5305
Item 5306
Item 5307
Item 5308
Item 5309
Item 5310
Item 5311

然后将每个块交给可用的处理器。 只有在处理完所有块之后,才会执行我的下一部分Java代码。 但是在内部,在对forEach()方法的调用中,所有这些工作都根据需要进行了拆分。 现在,这可能会或可能不会带来性能上的好处。 这取决于数据集的大小。 以及您的硬件的性质。 但是,此示例向您展示的一件事是,如果需要顺序处理项目,即一次将它们添加到集合中的顺序相同,那么并行流可能不是这样做的方法。它。

顺序流可以确保每次都以相同的顺序工作。 但是,按照定义,并行流将以最有效的方式处理事务。 因此,并行流在聚合操作时特别有用。 您要考虑集合中的所有项目,然后从中创建某种总价值。 我将向您展示一些示例,这些示例包括对集合中的项目进行计数,取平均值并使用字符串对其求和。

在此类main方法中的CountItems中,我从相同的基本代码开始。 在列表中创建10,000个字符串。 然后,每种方法都有一个循环遍历并一次处理它们的方法。

public static void main(String args[]) {
 
    System.out.println("Creating list");
    List<string> strings = new ArrayList<>();
    for (int i = 0; i < 10000; i++) {
        strings.add("Item " + i);
    }
    strings.stream()
           .forEach(str -> System.out.println(str));
}

在此示例中,我不是要单独处理每个字符串,而是要对它们进行计数。 因此,我将注释掉该代码,这是我将使用的代码。 由于我不知道确切要收集多少物品。 我将兑现要创建的结果作为一个长整数。

我将其命名为count ,并通过调用strings获得它的值。 那是我的集合.stream() .count() ,并且返回一个长值。 然后,我将使用系统输出并报告结果。 用count:然后附加结果。

//      strings.stream()
//             .forEach(str -> System.out.println(str));
        long count = strings.stream().count();
        System.out.println("Count: " + count);

我将保存更改并运行代码,并得到结果。 集合中项目的计数几乎是瞬时的。

Creating list
Count: 10000

现在,为了使它更具戏剧性,我将在此处添加几个零,现在我要处理1,000,000,000个字符串。 我将再次运行代码,结果几乎立即又返回。

Creating list
Count: 1000000

现在看看如果我并行化字符串会发生什么。 我将在此处平行添加点:

//      strings.stream()
//             .forEach(str -> System.out.println(str));
        long count = strings.stream().parallel().count();
        System.out.println("Count: " + count);

然后我将运行代码,这将花费更长的时间。 现在,我可以通过捕获操作前后的当前时间戳来确定执行这些操作需要多长时间。 然后做一点数学。 它将显示的内容可能因一个系统而异。 但是以我的经验,当处理包含简单值的这类简单集合时,并行流并没有太大的好处。 您的里程可能会很高。 我鼓励您做自己的基准测试。 但这就是您要计数的方式。

让我们看一下求和和平均值 。 我去上课SumAndAverage 。 这次,我列出了三个人对象,每个对象的年龄不同。 我的目标是获得三个年龄的总和,以及三个年龄的平均值。 在person类的所有实例都添加到列表之后,我将添加一行新代码。 然后创建一个整数变量,命名为sum

我将从使用people.stream().获得流开始people.stream(). 从那里,我将调用一个名为mapToInt()的方法。 注意,这里有一个Map方法。 mapToDouble()mapToLong()也是如此。 这些方法的目的是获取复杂的对象并从中提取简单的原始值,并创建这些值的流,然后使用Lambda表达式执行此操作。 因此,我将选择mapToInt()因为每个人的年龄都是整数。

对于Lambda表达式,我将从代表当前人的变量开始。 然后是Lambda运算符,然后是一个返回整数的表达式。 我将使用p.getAge() 。 这将返回称为int字符串或整数字符串的内容。 还有一个双字符串类和其他一些类。 现在从这个流中开始,因为我已经知道它是一个数值,所以我可以调用一个名为sum()的方法。 就是这样。 现在,我总结了我收藏中所有交友对象的所有陈旧价值。 仅需一条语句,我将使用系统输出输出结果。 我的标签将是年龄的总和,我将在其后加上总和。

List<person> people = new ArrayList<>();
        people.add(new Person("Mohamed", 69));
        people.add(new Person("Doaa", 25));
        people.add(new Person("Malik", 6));
 
        int sum = people.stream()
                  .mapToInt(p -> p.getAge())
                  .sum();
        System.out.println("Total of ages " + sum);

我将保存我的代码并运行它。 这三个年龄段的总和为100。

Total of ages 100

平均这些值非常相似。 但是,因为每当进行平均除法运算时,都可能会遇到被零除的问题,因此,当您进行平均时,您会得到称为Optional变量的信息。

您可以使用多种类型。 对于我的平均而言,我期望一个双精度值会回来。 因此,我将创建一个名为OptionalDouble的变量。 注意,还有可选的Int和可选的日志。 我将Avg命名为变量Avg 。 我将使用刚才用来获取总和的相同类型的代码,从people.stream() 。 然后从那里,我将再次使用mapToInt() 。 然后,我将传递上次使用的相同的lambda表达式,然后从那里调用average方法。

现在有了OptionalDouble对象,在处理它之前,应始终确保它实际上具有一个double值,并使用名为isPresent()的方法来执行此操作。 因此,我将从一个if else代码模板开始。 然后将条件设置为avg.isPresent() 。 如果条件成立,我将使用系统输出。 我将其标记为“平均”。 然后我将附加平均变量。 在else子句中,我将简单地说平均数未计算出来。

OptionalDouble avg = people.stream()
                .mapToInt(p -> p.getAge())
                .average();
if (avg.isPresent()) {
    System.out.println("Average: " + avg);
} else {
    System.out.println("average wasn't calculated");
}

现在,在这个示例中,我知道它将成功,因为我已经为所有三个人提供了年龄,但情况并非总是如此。 就像我说的那样,如果最终遇到被零除的情况,则可能无法获得双倍的价值。 我将保存并运行代码,并注意使用可选的double类,它是一个复杂的对象。

Total of ages 100
Average: OptionalDouble[33.333333333333336]

因此,类型将包裹在实际值周围。 我将转到此代码,直接在其中引用该对象,并将其getAsDouble()方法。

if (avg.isPresent()) {
    System.out.println("Average: " + avg.getAsDouble());
} else {
    System.out.println("average wasn't calculated");
}

现在,我将返回原始的double值。 我将再次运行代码,现在结果就是我想要的。

Total of ages 100
Average: 33.333333333333336

结论

因此,使用流和lambda表达式,您可以使用少量代码轻松地从集合中计算聚合值。

资源资源

  1. Java教程,聚合操作
  2. Java流接口API
  3. Java教程,Lambda表达式
  4. JSR 310:日期和时间API
  5. JSR 337:Java SE 8发行内容
  6. OpenJDK网站
  7. Java平台,标准版8,API规范

我希望您喜欢阅读它,就像我喜欢编写它一样,如果您喜欢它,请分享,传播信息。

翻译自: https://www.javacodegeeks.com/2015/07/java-se-8-new-features-tour-processing-collections-with-streams-api.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值