java 1.8.091_Java 8 Stream API 入门者教程

Java 8 给我们带来了一个新功能,也就是本文要介绍的 Stream API,它可以让我们以一种声明的方式处理数据。Stream 使用一种类似用 SQL 的语法来提供一种对 Java 集合运算和表达的高阶抽象。极大提高 Java 程序员的生产力,让程序员写出高效率、干净、简洁的代码。本文是

java8_stream_api-iteblog.jpg

如果想及时了解Spark、Hadoop或者HBase相关的文章,欢迎关注微信公众帐号:iteblog_hadoop

Stream 创建

有很多方法可以创建不同源的 stream 实例,stream 实例一旦创建,将不会修改其源,因此我们从单个源创建多个 stream 实例。

Empty Stream

如果我们想创建一个空的 Stream,可以使用 empty() 方法,具体如下: Stream iteblogEmptyStream = Stream.empty();

通常在 streams 没有元素然后不想返回 null 的情况下使用: public Stream streamOf(List list) {

return list == null || list.isEmpty() ? Stream.empty() : list.stream();

}

通过集合(Collection)创建 Stream

Java 中的任何继承 Collection 接口的类都可以创建 Stream List list = Lists.newArrayList("iteblog", "iteblog_hadoop");

Stream listStream = list.stream();

Set set = Sets.newHashSet();

Stream setStream = set.stream();

通过数组(Array)创建 Stream

数组也可以创建 Stream Stream streamOfArray = Stream.of("a", "b", "c");

当然,我们也可以通过已有的数组来创建 Stream String[] iteblogArr = new String[]{"iteblog", "iteblog_hadoop", "java 8"};

Stream streamOfArrayFull = Arrays.stream(iteblogArr);

Stream streamOfArrayPart = Arrays.stream(iteblogArr, 1, 3);

通过 Stream.builder() 创建 Stream

Stream 提供了 builder 方法来创建 Stream: Stream streamBuilder = Stream.builder().add("iteblog").add("iteblog_hadoop").add("java").build();

Stream streamBuilder = Stream.builder().add("iteblog").add("iteblog_hadoop").add("java").build();

上面创建的 Stream 类型是 Stream,如果我们想创建指定类型的 Stream,需要显示地指定类型 Stream streamBuilder = Stream.builder().add("iteblog").add("iteblog_hadoop").add("java").build();

通过 Stream.generate() 创建 Stream

Stream.generate() 方法接收一个 Supplier 类型的参数来生成元素,生成的 stream 大小是无限的,所以我们需要指定 stream 生成的大小,以免出现内存不够的问题: Stream streamGenerated = Stream.generate(() -> "iteblog").limit(88);

通过 Stream.iterate() 创建 Stream

我们也可以通过 Stream.iterate() 来创建 Stream Stream streamIterated = Stream.iterate(2, n -> n * 2).limit(88);

Stream.iterate 方法的第一个参数将是这个 Stream 的第一个值,第二个元素将是前一个元素乘以 2。和 Stream.generate() 方法一样,我们也需要指定 stream 生成的大小,以免出现内存不够的问题。

通过原子类型创建 Stream

Java 8 中的 int, long 和 double 三个原子类型可以用来创建 streams,对应的接口分别是 IntStream, LongStream, DoubleStream IntStream intStream = IntStream.range(0, 10);

LongStream longStream = LongStream.rangeClosed(0, 10);

DoubleStream doubleStream = DoubleStream.of(1.0, 2.0);

range(int startInclusive, int endExclusive) 相当于下面的代码: for (long i = startInclusive; i < endExclusive ; i++) { ... }

rangeClosed(int startInclusive, int endInclusive) 相当于下面的代码: for (long i = startInclusive; i <= endInclusive ; i++) { ... }

区别大家应该看出来了:rangeClosed 生成的 Stream 包含最后一个元素,而 range 缺不是。

当然,Java 8 中的 Random 类也为我们添加了生成上面三个原子类型对应的 Stream: Random random = new Random();

IntStream intStream = random.ints(10);

LongStream longs = random.longs(10);

DoubleStream doubleStream = random.doubles(10);

通过字符串创建 Stream

Java 8 中的 String 类提供了 chars() 方法来创建 Stream: IntStream streamOfChars = "abc".chars();

我们也可以通过下面方法来创建 Stream: Stream streamOfString = Pattern.compile(", ").splitAsStream("a, b, c");

通过文件创建 Stream

Java 8 的 Java NIO 类中 Files 允许我们通过 lines() 方法创建 Stream,文件中的每一行数据将变成 stream 中的一个元素: Path path = Paths.get("/user/iteblog/test.txt");

Stream streamOfStrings = Files.lines(path);

Stream streamWithCharset = Files.lines(path, Charset.forName("UTF-8"));

Stream 的引用

下面的代码是允许的: Stream stream = Stream.of("iteblog", "iteblog_hadoop", "spark")

.filter(element -> element.contains("iteblog"));

Optional anyElement = stream.findAny();

我们使用 stream 变量来引用一个定义好的 Stream,这个是允许的,接下来我们使用 findAny() 来操作这个 Stream,也是可以运行的。但是如果我们复用 stream 变量,这样执行的时候就会出现 IllegalStateException 异常: Optional firstElement = stream.findFirst();

Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed

at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)

at java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:464)

at com.java.iteblog.Java8Test.main(Java8Test.java:19)

上面的实例说明 Java 8 streams 的引用是不可复用的。之所以这样是因为 Java 的 streams 设计目的是提供一种能力,以函数的方式将有限的操作序列应用于元素的源,而不是存储元素。如果我们这样写是可以的 List iteblogList = Stream.of("iteblog", "iteblog_hadoop", "spark")

.filter(element -> element.contains("iteblog")).collect(Collectors.toList());

Optional anyElement = iteblogList.stream().findAny();

Optional firstElement = iteblogList.stream().findFirst();

Stream 管道(stream pipeline)

要对数据源的元素执行一系列操作并聚合它们的结果,需要三个部分:数据源(source)、中间操作(intermediate operations)和终端操作(terminal operation)。

java_streams-iteblog.png

如果想及时了解Spark、Hadoop或者HBase相关的文章,欢迎关注微信公众帐号:iteblog_hadoop

中间操作返回一个新的修改过的 Stream。比如下面的例子我们使用 skip() 方法跳过了旧 Stream 的第一个元素,并返回一个名为 iteblogSkip 的新 Stream: Stream iteblogSkip = Stream.of("iteblog", "iteblog_hadoop", "spark").skip(1);

如果需要多个修改操作,可以链接多个中间操作: Stream iteblogSkip = Stream.of("iteblog", "iteblog_hadoop", "spark")

.skip(1).map(element -> element.substring(0, 3));

上面例子我们同时使用了 skip() 和 map() 方法,并得到了有新的 Stream 引用。

stream 本身是没有价值的,用户真正感兴趣的是终端操作的结果,它可以是某种类型的值,也可以是应用于流的每个元素的操作。每个流只能使用一个终端操作。使用 stream 的正确和最方便的方式是通过 stream 管道,它是一个数据源、中间操作和终端操作的链(chain),例如: long count = Stream.of("iteblog", "iteblog_hadoop", "spark")

.skip(1).map(element -> element.substring(0, 3)).count();

延迟调用(Lazy Invocation)

java8_stream_lazy_invocation-iteblog.png

如果想及时了解Spark、Hadoop或者HBase相关的文章,欢迎关注微信公众帐号:iteblog_hadoop

中间操作是惰性(Lazy)的,这意味着仅在终端操作执行需要时才调用它们。为了说明这个,假设我们有一个名为 wasCalled() 的方法,每次被调用其内部的计数器都会递增: private static long counter;

private static void wasCalled() {

counter++;

}

现在我们在 filter() 中间操作中调用 wasCalled() 方法: List list = Arrays.asList("iteblog", "iteblog_hadoop", "spark");

counter = 0;

Stream stream = list.stream().filter(element -> {

wasCalled();

return element.contains("iteblog");

});

System.out.println(counter);

我们的数据源 list 里面有三个元素,然后我们在这个数据源上调用了 filter() 方法,按道理应该会对每个元素调用一次 filter() 方法,这样 counter 比变量的值应该是 3。但是如果我们运行上面的代码你会发现 counter 的值还是 0!也就是说 filter() 方法根本就没调用,原因就是中间操作是惰性(Lazy)的,只有加上终端操作才会执行这个 filter() 方法。

我们把上面的代码修改成下面的代码: List list = Arrays.asList("iteblog", "iteblog_hadoop", "spark");

list.stream().filter(element -> {

System.out.println("filter() was called");

return element.contains("hadoop");

}).map(element -> {

System.out.println("map() was called");

return element.toUpperCase();

}).findFirst();

输出:

filter() was called

filter() was called

map() was called

可以看出,在终端输出了两次 filter() was called,一次 map() was called。也就是说 filter() 函数被调用了两次;map() 函数被调用了一次。Stream 管道是垂直执行的,在我们的例子里面,先运行 filter() 再运行 map(),只有 filter() 返回为 true 才会调用 map(),然后 findFirst() 只需要找到第一个满足的元素就可以终止程序的运行。

Stream 的运行顺序

从性能的角度来看,Stream 管道中不同操作的链接顺序是很重要的。下面两段代码片段的运行结果是一样的,但是下面的代码是推荐使用的。 long size = list.stream().map(element -> {

wasCalled();

return element.substring(0, 3);

}).skip(2).count();

long size = list.stream().skip(2).map(element -> {

wasCalled();

return element.substring(0, 3);

}).count();

因为第一个代码片段运行了三次 map(),而第二个代码片段只运行了一次 map()。所以在写 Java Stream 程序的时候,推荐 Stream 管道的顺序是:skip() -> filter() -> distinct()。

Stream 聚合

Stream API 有许多终端操作,它们将 Stream 聚合为一个原子类型的数据,例如 count()、max()、min()、sum() 等,但是这些操作根据预定义的实现进行工作的。如果开发人员需要定制 Stream 的聚合逻辑,该怎么办?这就是本小结要介绍的 reduce() 和 collect() 方法。

reduce() 方法介绍

reduce()有三个重载的方法,但是都是接收以下几种类型的参数:identity:累加器的初始值,如果 stream 为空且没有要累加的内容,则为默认值;

accumulator:指定元素聚合逻辑的函数。accumulator 在对数据源里面的每个元素进行聚合时,都会生成一个新的临时对象,生成的对象数等于数据源里面的元素个数,但是只有最后一个值是有用的,这个对性能不是很好的。

combiner:聚合累加器结果的函数。只有在并行模式下才会调用 Combiner,以减少来自不同线程的累加器的结果。

现在让我们来看看如何来使用 reduce()三个方法:

实例一 OptionalInt sum = IntStream.range(1, 4).reduce((a, b) -> a + b);

System.out.println(sum);

输出:

OptionalInt[6](也就是 1 + 2 + 3)

实例二 int reducedTwoParams = IntStream.range(1, 4).reduce(10, (a, b) -> a + b);

System.out.println(reducedTwoParams);

输出:

16(也就是 10 + 1 + 2 + 3)

实例三 int reducedParams = Stream.of(1, 2, 3)

.reduce(10, (a, b) -> a + b, (a, b) -> {

System.out.println("combiner was called");

return a + b;

});

System.out.println(reducedParams);

输出:

16(也就是 10 + 1 + 2 + 3)

可以看出,实例三虽然我们指定了 combiner 但是控制台并没有输出 combiner was called,也就是说上面的 combiner 其实并没有调用。如果我们想调用 combiner,可以修改如下: int reducedParallel = Arrays.asList(1, 2, 3).parallelStream()

.reduce(10, (a, b) -> a + b, (a, b) -> {

System.out.println("combiner was called");

return a + b;

});

System.out.println(reducedParallel);

输出:

combiner was called

combiner was called

36

可以看出这次的输出结果是 36,并且输出两次 combiner was called。之所以是 36,是因为上面程序先对每个元素调用 accumulator,也就是调用了三次 accumulator,然后其和累加器的初始值进行相加,因为这个 actions 是并行执行的,所以调用三次 accumulator 得到的结果为 (10 + 1 = 11; 10 + 2 = 12; 10 + 3 = 13)。现在我们调用 combiner 把上面三次结果相加 (12 + 13 = 25; 25 + 11 = 36) 所以得到了 36。

collect() 方法介绍

collect() 方法也提供了聚合相关的逻辑实现,其函数签名为 R collect(Collector< ? super T, A, R> collector),Java 8 中提供了大多数常用的 collectors 逻辑实现,我们可以直接使用。为了说明如何使用,我们还是提供一些例子: static class Product {

private int price;

private String name;

Product(int price, String name) {

this.price = price;

this.name = name;

}

public int getPrice() {

return price;

}

public void setPrice(int price) {

this.price = price;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

}

List productList = Arrays.asList(new Product(23, "potatoes"),

new Product(14, "orange"), new Product(13, "lemon"),

new Product(23, "bread"), new Product(13, "sugar"));

将 productList 里面的商品名称全部拿出来,并转换成 list: List collectorCollection = productList.stream().map(Product::getName).collect(Collectors.toList());

将 productList 里面的商品名称全部拿出来,并组合成 string: String listToString = productList.stream().map(Product::getName)

.collect(Collectors.joining(", ", "[", "]"));

计算 productList 里面所有商品的平均价格: double averagePrice = productList.stream().collect(Collectors.averagingInt(Product::getPrice));

计算 productList 里面所有商品的总价: int summingPrice = productList.stream().collect(Collectors.summingInt(Product::getPrice));

计算 productList 里面所有商品统计信息: IntSummaryStatistics statistics = productList.stream().collect(Collectors.summarizingInt(Product::getPrice));

System.out.println(statistics);

输出:

IntSummaryStatistics{count=5, sum=86, min=13, average=17.200000, max=23}

按照商品价格对商品进行归类 Map> collectorMapOfLists = productList.stream()

.collect(Collectors.groupingBy(Product::getPrice));

上面的结果是价格相等的商品都放到同一个 List 中。

按照相关逻辑对商品进行分组 Map> mapPartioned = productList.stream()

.collect(Collectors.partitioningBy(element -> element.getPrice() > 15));

上面程序的运行结果是价格大于15的放到一个 List 中。

将 list 转换成 set Set unmodifiableSet = productList.stream()

.collect(Collectors.collectingAndThen(Collectors.toSet(),

Collections::unmodifiableSet));

自定义 collector

总有些原因系统自带的 API 无法满足我们的需求,这时候我们就可以自定义 collector。比如下面我们自定义了一个 collector,把所有的商品放到 LinkedList 里面: Collector> toLinkedList =

Collector.of(LinkedList::new, LinkedList::add,

(first, second) -> {

first.addAll(second);

return first;

});

LinkedList linkedListOfPersons = productList.stream().collect(toLinkedList);

总结

Stream API 是一套功能强大但易于理解的用于处理元素序列的工具。它允许我们减少大量的代码,创建更多可读的程序,并可以提高应用程序的生产率。本博客将在后面几篇文章介绍如何更好地使用 Java 8 Stream API。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值