Java 8 - 数值流Numberic Stream

2021-08-17 16:49:47 浏览数 (1)


Pre

Java 8 - Stream流骚操作解读2_归约操作操作了reduce, 使用 reduce 方法计算流中元素的总和.

来看个例子

代码语言:javascript复制
    // 菜单中的热量求和
    public static Integer sumCal(List<Dish> dishes){
         return dishes.stream().map(Dish::getCalories)
                 .reduce(0,Integer::sum);
    }

功能肯定是OK的。 不过这里暗含了一个装箱成本。 每个 Integer 都必须被拆箱成一个原始类型,再进行求和。

如果可以直接像下面这样调用 sum 方法,不是更好吗?

代码语言:javascript复制
dishes.stream().map(Dish::getCalories).sum()

但这是不可能的。问题在于 map 方法会生成一个 Stream 。虽然流中的元素是 Integer 类型,但 Streams 接口没有定义 sum 方法。

Stream API还提供了原始类型流特化,专门支持处理数值流的方法。


原始类型流特化

Java 8引入了三个原始类型特化流接口来解决这个问题: IntStream 、 DoubleStream 和LongStream ,分别将流中的元素特化为 int 、 long 和 double ,从而避免了暗含的装箱成本。

每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的 sum ,找到最大元素的 max 。

此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似 int 和 Integer 之间的效率差异。


映射到数值流 (mapToInt 、 mapToDouble 和 mapToLong )

将流转换为特化版本的常用方法是 mapToInt 、 mapToDouble 和 mapToLong 。

这些方法和前面说的 map 方法的工作方式一样,只是它们返回的是一个特化流,而不是 Stream

例如,可以像下面这样用 mapToInt 对 menu 中的卡路里求和:

这里, mapToInt 会从每道菜中提取热量(用一个 Integer 表示),并返回一个 IntStream(而不是一个 Stream )。然后就可以调用 IntStream 接口中定义的 sum 方法,对卡路里求和了!

请注意,如果流是空的, sum 默认返回 0 。 IntStream 还支持其他的方便方法,如max 、 min 、 average 等。


转换回数值流 boxed

一旦有了数值流,你可能会想把它转换回非特化流。例如, IntStream 上的操作只能产生原始整数: IntStream 的 map 操作接受的Lambda必须接受 int 并返回 int (一个IntUnaryOperator )。但是你可能想要生成另一类值,比如 Dish 。为此,你需要访问 Stream接口中定义的那些更广义的操作。要把原始流转换成一般流(每个 int 都会装箱成一个Integer ),可以使用 boxed 方法

需要将数值范围装箱成为一个一般流时, boxed 尤其有用。


默认值OptionalInt

求和的那个例子很容易,因为它有一个默认值: 0 。但是,如果你要计算 IntStream 中的最大元素,就得换个法子了,因为 0 是错误的结果。如何区分没有元素的流和最大值真的是 0 的流呢?

前面我们介绍了 Optional 类,这是一个可以表示值存在或不存在的容器。 Optional 可以用Integer 、 String 等参考类型来参数化。对于三种原始流特化,也分别有一个 Optional 原始类型特化版本: OptionalInt 、 OptionalDouble 和 OptionalLong 。

例如,要找到 IntStream 中的最大元素,可以调用 max 方法,它会返回一个 OptionalInt :

代码语言:javascript复制
OptionalInt max = dishes.stream()
                .mapToInt(Dish::getCalories)
                .max();

现在,如果没有最大值的话,你就可以显式处理 OptionalInt 去定义一个默认值了:


数值范围( range 和 rangeClosed)

和数字打交道时,有一个常用的东西就是数值范围。比如,假设你想要生成1和100之间的所有数字。Java 8引入了两个可以用于 IntStream 和 LongStream 的静态方法,帮助生成这种范围range 和 rangeClosed 。

这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range 是不包含结?值的,而 rangeClosed 则包含结束值。

这里我们用了 rangeClosed 方法来生成1到100之间的所有数字。它会产生一个流,然后你可以链接 filter 方法,只选出偶数。到目前为止还没有进行任何计算。最后,你对生成的流调用 count 。因为 count 是一个终端操作,所以它会处理流,并返回结果 50 ,这正是1到100(包括两端)中所有偶数的个数。请注意,比较一下,如果改用 IntStream.range(1, 100) ,则结果将会是 49 个偶数,因为 range 是不包含结?值的。


小结

事实上,流让你可以简洁地表达复杂的数据处理查询。此外,流可以透明地并行化

  • 使用 filter 、 distinct 、 skip 和 limit 对流做筛选和切片。
  • 使用 map 和 flatMap 提取或转换流中的元素
  • 使用 findFirst 和 findAny 方法查找流中的元素。你可以用 allMatch 、noneMatch 和 anyMatch 方法让流匹配给定的谓词。
  • 这些方法都利用了短路:找到结果就立即停止计算;没有必要处理整个流。
  • 以利用 reduce 方法将流中所有的元素迭代合并成一个结果,例如求和或查找最大元素。
  • filter 和 map 等操作是无状态的,它们并不存储任何状态。 reduce 等操作要存储状态才能计算出一个值。 sorted 和 distinct 等操作也要存储状态,因为它们需要把流中的所有元素缓存起来才能返回一个新的流。这种操作称为有状态操作
  • 流有三种基本的原始类型特化: IntStream 、 DoubleStream 和 LongStream 。它们的操作也有相应的特化。
  • 流不仅可以从集合创建,也可从值、数组、文件以及 iterate 与 generate 等特定方法创建。
  • 无限流是没有固定大小的流。

测试数据

代码语言:javascript复制
        List<Dish> menu = Arrays.asList(
                new Dish("pork", false, 800, Dish.Type.MEAT),
                new Dish("beef", false, 700, Dish.Type.MEAT),
                new Dish("chicken", false, 400, Dish.Type.MEAT),
                new Dish("french fries", true, 530, Dish.Type.OTHER),
                new Dish("rice", true, 350, Dish.Type.OTHER),
                new Dish("season fruit", true, 120, Dish.Type.OTHER),
                new Dish("pizza", true, 550, Dish.Type.OTHER),
                new Dish("prawns", false, 300, Dish.Type.FISH),
                new Dish("salmon", false, 450, Dish.Type.FISH));

0 人点赞