Java 8 Learn Notes - Streams

Main reference

[1] http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples

1. How Streams Work

A stream represents a sequence of elements and supports different kind of operations to perform computations upon those elements:

List<String> myList =
    Arrays.asList("a1", "a2", "b1", "c2", "c1");

myList
    .stream()
    .filter(s -> s.startsWith("c"))
    .map(String::toUpperCase)
    .sorted()
    .forEach(System.out::println);

// C1
// C2

2. Different Kinds of Streams

Streams can be created from various data sources, especially collections. Lists and Sets support new methods stream() and parallelStream() to either create a sequential or a parallel stream.

For Sequencial Stream

Arrays.asList("a1", "a2", "a3")
    .stream()
    .findFirst()
    .ifPresent(System.out::println);  // a1

Calling the method stream() on a list of objects returns a regular object stream. But we don‘t have to create collections in order to work with streams as we see in the next code sample:

Stream.of("a1", "a2", "a3")
    .findFirst()
    .ifPresent(System.out::println);  // a1

Just use Stream.of() to create a stream from a bunch of object references.

Besides regular object streams Java 8 ships with special kinds of streams for working with the primitive data types intlong and double. As you might have guessed it‘s IntStream,LongStream and DoubleStream.

IntStreams can replace the regular for-loop utilizing IntStream.range()

IntStream.range(1, 4)
    .forEach(System.out::println);

// 1
// 2
// 3

All those primitive streams work just like regular object streams with the following differences: Primitive streams use specialized lambda expressions, e.g. IntFunction instead ofFunction or IntPredicate instead of Predicate. And primitive streams support the additional terminal aggregate operations sum() and average()

Arrays.stream(new int[] {1, 2, 3})
    .map(n -> 2 * n + 1)
    .average()
    .ifPresent(System.out::println);  // 5.0

Sometimes it‘s useful to transform a regular object stream to a primitive stream or vice versa. For that purpose object streams support the special mapping operations mapToInt(),mapToLong() and mapToDouble

Stream.of("a1", "a2", "a3")
    .map(s -> s.substring(1))
    .mapToInt(Integer::parseInt)
    .max()
    .ifPresent(System.out::println);  // 3

Primitive streams can be transformed to object streams via mapToObj()

IntStream.range(1, 4)
    .mapToObj(i -> "a" + i)
    .forEach(System.out::println);

// a1
// a2
// a3

Here‘s a combined example: the stream of doubles is first mapped to an int stream and than mapped to an object stream of strings:

Stream.of(1.0, 2.0, 3.0)
    .mapToInt(Double::intValue)
    .mapToObj(i -> "a" + i)
    .forEach(System.out::println);

// a1
// a2
// a3

3. Processing Order

An important characteristic of intermediate operations is laziness.

4. Reusing Streams

Java 8 streams cannot be reused. As soon as you call any terminal operation the stream is closed

Stream<String> stream =
    Stream.of("d2", "a2", "b1", "b3", "c")
        .filter(s -> s.startsWith("a"));

stream.anyMatch(s -> true);    // ok
stream.noneMatch(s -> true);   // exception

To overcome this limitation we have to to create a new stream chain for every terminal operation we want to execute, e.g. we could create a stream supplier to construct a new stream with all intermediate operations already set up:

Supplier<Stream<String>> streamSupplier =
    () -> Stream.of("d2", "a2", "b1", "b3", "c")
            .filter(s -> s.startsWith("a"));

streamSupplier.get().anyMatch(s -> true);   // ok
streamSupplier.get().noneMatch(s -> true);  // ok

Each call to get() constructs a new stream on which we are save to call the desired terminal operation.

5. Advanced Operations

Most code samples from this section use the following list of persons for demonstration purposes:

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return name;
    }
}

List<Person> persons =
    Arrays.asList(
        new Person("Max", 18),
        new Person("Peter", 23),
        new Person("Pamela", 23),
        new Person("David", 12));

Collect

Collect is an extremely useful terminal operation to transform the elements of the stream into a different kind of result, e.g. a ListSet or Map. Collect accepts a Collector which consists of four different operations: a supplier, an accumulator, a combiner and a finisher. This sounds super complicated at first, but the good part is Java 8 supports various built-in collectors via the Collectors class. So for the most common operations you don‘t have to implement a collector yourself.

List<Person> filtered =
    persons
        .stream()
        .filter(p -> p.name.startsWith("P"))
        .collect(Collectors.toList());

System.out.println(filtered);    // [Peter, Pamela]

Then

Map<Integer, List<Person>> personsByAge = persons
    .stream()
    .collect(Collectors.groupingBy(p -> p.age));

personsByAge
    .forEach((age, p) -> System.out.format("age %s: %s\n", age, p));

// age 18: [Max]
// age 23: [Peter, Pamela]
// age 12: [David]

Collectors are extremely versatile. You can also create aggregations on the elements of the stream, e.g. determining the average age of all persons:

Double averageAge = persons
    .stream()
    .collect(Collectors.averagingInt(p -> p.age));

System.out.println(averageAge);     // 19.0

In order to transform the stream elements into a map, we have to specify how both the keys and the values should be mapped. Keep in mind that the mapped keys must be unique, otherwise an IllegalStateException is thrown. You can optionally pass a merge function as an additional parameter to bypass the exception:

Map<Integer, String> map = persons
    .stream()
    .collect(Collectors.toMap(
        p -> p.age,
        p -> p.name,
        (name1, name2) -> name1 + ";" + name2));

System.out.println(map);
// {18=Max, 23=Peter;Pamela, 12=David}

Now that we know some of the most powerful built-in collectors, let‘s try to build our own special collector. We want to transform all persons of the stream into a single string consisting of all names in upper letters separated by the | pipe character. In order to achieve this we create a new collector via Collector.of(). We have to pass the four ingredients of a collector: a supplier, an accumulator, a combiner and a finisher.

Collector<Person, StringJoiner, String> personNameCollector =
    Collector.of(
        () -> new StringJoiner(" | "),          // supplier
        (j, p) -> j.add(p.name.toUpperCase()),  // accumulator
        (j1, j2) -> j1.merge(j2),               // combiner
        StringJoiner::toString);                // finisher

String names = persons
    .stream()
    .collect(personNameCollector);

System.out.println(names);  // MAX | PETER | PAMELA | DAVID

Since strings in Java are immutable, we need a helper class like StringJoiner to let the collector construct our string. The supplier initially constructs such a StringJoiner with the appropriate delimiter. The accumulator is used to add each persons upper-cased name to the StringJoiner. The combiner knows how to merge two StringJoiners into one. In the last step the finisher constructs the desired String from the StringJoiner.

FlatMap

---恢复内容结束---

Main reference

[1] http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples

1. How Streams Work

A stream represents a sequence of elements and supports different kind of operations to perform computations upon those elements:

List<String> myList =
    Arrays.asList("a1", "a2", "b1", "c2", "c1");

myList
    .stream()
    .filter(s -> s.startsWith("c"))
    .map(String::toUpperCase)
    .sorted()
    .forEach(System.out::println);

// C1
// C2

2. Different Kinds of Streams

Streams can be created from various data sources, especially collections. Lists and Sets support new methods stream() and parallelStream() to either create a sequential or a parallel stream.

For Sequencial Stream

Arrays.asList("a1", "a2", "a3")
    .stream()
    .findFirst()
    .ifPresent(System.out::println);  // a1

Calling the method stream() on a list of objects returns a regular object stream. But we don‘t have to create collections in order to work with streams as we see in the next code sample:

Stream.of("a1", "a2", "a3")
    .findFirst()
    .ifPresent(System.out::println);  // a1

Just use Stream.of() to create a stream from a bunch of object references.

Besides regular object streams Java 8 ships with special kinds of streams for working with the primitive data types intlong and double. As you might have guessed it‘s IntStream,LongStream and DoubleStream.

IntStreams can replace the regular for-loop utilizing IntStream.range()

IntStream.range(1, 4)
    .forEach(System.out::println);

// 1
// 2
// 3

All those primitive streams work just like regular object streams with the following differences: Primitive streams use specialized lambda expressions, e.g. IntFunction instead ofFunction or IntPredicate instead of Predicate. And primitive streams support the additional terminal aggregate operations sum() and average()

Arrays.stream(new int[] {1, 2, 3})
    .map(n -> 2 * n + 1)
    .average()
    .ifPresent(System.out::println);  // 5.0

Sometimes it‘s useful to transform a regular object stream to a primitive stream or vice versa. For that purpose object streams support the special mapping operations mapToInt(),mapToLong() and mapToDouble

Stream.of("a1", "a2", "a3")
    .map(s -> s.substring(1))
    .mapToInt(Integer::parseInt)
    .max()
    .ifPresent(System.out::println);  // 3

Primitive streams can be transformed to object streams via mapToObj()

IntStream.range(1, 4)
    .mapToObj(i -> "a" + i)
    .forEach(System.out::println);

// a1
// a2
// a3

Here‘s a combined example: the stream of doubles is first mapped to an int stream and than mapped to an object stream of strings:

Stream.of(1.0, 2.0, 3.0)
    .mapToInt(Double::intValue)
    .mapToObj(i -> "a" + i)
    .forEach(System.out::println);

// a1
// a2
// a3

3. Processing Order

An important characteristic of intermediate operations is laziness.

4. Reusing Streams

Java 8 streams cannot be reused. As soon as you call any terminal operation the stream is closed

Stream<String> stream =
    Stream.of("d2", "a2", "b1", "b3", "c")
        .filter(s -> s.startsWith("a"));

stream.anyMatch(s -> true);    // ok
stream.noneMatch(s -> true);   // exception

To overcome this limitation we have to to create a new stream chain for every terminal operation we want to execute, e.g. we could create a stream supplier to construct a new stream with all intermediate operations already set up:

Supplier<Stream<String>> streamSupplier =
    () -> Stream.of("d2", "a2", "b1", "b3", "c")
            .filter(s -> s.startsWith("a"));

streamSupplier.get().anyMatch(s -> true);   // ok
streamSupplier.get().noneMatch(s -> true);  // ok

Each call to get() constructs a new stream on which we are save to call the desired terminal operation.

5. Advanced Operations

Most code samples from this section use the following list of persons for demonstration purposes:

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return name;
    }
}

List<Person> persons =
    Arrays.asList(
        new Person("Max", 18),
        new Person("Peter", 23),
        new Person("Pamela", 23),
        new Person("David", 12));

Collect

Collect is an extremely useful terminal operation to transform the elements of the stream into a different kind of result, e.g. a ListSet or Map. Collect accepts a Collector which consists of four different operations: a supplier, an accumulator, a combiner and a finisher. This sounds super complicated at first, but the good part is Java 8 supports various built-in collectors via the Collectors class. So for the most common operations you don‘t have to implement a collector yourself.

List<Person> filtered =
    persons
        .stream()
        .filter(p -> p.name.startsWith("P"))
        .collect(Collectors.toList());

System.out.println(filtered);    // [Peter, Pamela]

Then

Map<Integer, List<Person>> personsByAge = persons
    .stream()
    .collect(Collectors.groupingBy(p -> p.age));

personsByAge
    .forEach((age, p) -> System.out.format("age %s: %s\n", age, p));

// age 18: [Max]
// age 23: [Peter, Pamela]
// age 12: [David]

Collectors are extremely versatile. You can also create aggregations on the elements of the stream, e.g. determining the average age of all persons:

Double averageAge = persons
    .stream()
    .collect(Collectors.averagingInt(p -> p.age));

System.out.println(averageAge);     // 19.0

In order to transform the stream elements into a map, we have to specify how both the keys and the values should be mapped. Keep in mind that the mapped keys must be unique, otherwise an IllegalStateException is thrown. You can optionally pass a merge function as an additional parameter to bypass the exception:

Map<Integer, String> map = persons
    .stream()
    .collect(Collectors.toMap(
        p -> p.age,
        p -> p.name,
        (name1, name2) -> name1 + ";" + name2));

System.out.println(map);
// {18=Max, 23=Peter;Pamela, 12=David}

Now that we know some of the most powerful built-in collectors, let‘s try to build our own special collector. We want to transform all persons of the stream into a single string consisting of all names in upper letters separated by the | pipe character. In order to achieve this we create a new collector via Collector.of(). We have to pass the four ingredients of a collector: a supplier, an accumulator, a combiner and a finisher.

Collector<Person, StringJoiner, String> personNameCollector =
    Collector.of(
        () -> new StringJoiner(" | "),          // supplier
        (j, p) -> j.add(p.name.toUpperCase()),  // accumulator
        (j1, j2) -> j1.merge(j2),               // combiner
        StringJoiner::toString);                // finisher

String names = persons
    .stream()
    .collect(personNameCollector);

System.out.println(names);  // MAX | PETER | PAMELA | DAVID

Since strings in Java are immutable, we need a helper class like StringJoiner to let the collector construct our string. The supplier initially constructs such a StringJoiner with the appropriate delimiter. The accumulator is used to add each persons upper-cased name to the StringJoiner. The combiner knows how to merge two StringJoiners into one. In the last step the finisher constructs the desired String from the StringJoiner.

FlatMap

We‘ve already learned how to transform the objects of a stream into another type of objects by utilizing the map operation. Map is kinda limited because every object can only be mapped to exactly one other object. But what if we want to transform one object into multiple others or none at all? This is where flatMap comes to the rescue.

FlatMap transforms each element of the stream into a stream of other objects. So each object will be transformed into zero, one or multiple other objects backed by streams. The contents of those streams will then be placed into the returned stream of the flatMap operation.

Before we see flatMap in action we need an appropriate type hierarchy:

class Foo {
    String name;
    List<Bar> bars = new ArrayList<>();

    Foo(String name) {
        this.name = name;
    }
}

class Bar {
    String name;

    Bar(String name) {
        this.name = name;
    }
}

Next, we utilize our knowledge about streams to instantiate a couple of objects:

List<Foo> foos = new ArrayList<>();

// create foos
IntStream
    .range(1, 4)
    .forEach(i -> foos.add(new Foo("Foo" + i)));

// create bars
foos.forEach(f ->
    IntStream
        .range(1, 4)
        .forEach(i -> f.bars.add(new Bar("Bar" + i + " <- " + f.name))));

Now we have a list of three foos each consisting of three bars.

FlatMap accepts a function which has to return a stream of objects. So in order to resolve the bar objects of each foo, we just pass the appropriate function:

foos.stream()
    .flatMap(f -> f.bars.stream())
    .forEach(b -> System.out.println(b.name));

// Bar1 <- Foo1
// Bar2 <- Foo1
// Bar3 <- Foo1
// Bar1 <- Foo2
// Bar2 <- Foo2
// Bar3 <- Foo2
// Bar1 <- Foo3
// Bar2 <- Foo3
// Bar3 <- Foo3

Finally, the above code example can be simplified into a single pipeline of stream operations:

IntStream.range(1, 4)
    .mapToObj(i -> new Foo("Foo" + i))
    .peek(f -> IntStream.range(1, 4)
        .mapToObj(i -> new Bar("Bar" + i + " <- " f.name))
        .forEach(f.bars::add))
    .flatMap(f -> f.bars.stream())
    .forEach(b -> System.out.println(b.name));

FlatMap is also available for the Optional class introduced in Java 8. Optionals flatMapoperation returns an optional object of another type. So it can be utilized to prevent nastynull checks.

Think of a highly hierarchical structure like this:

class Outer {
    Nested nested;
}

class Nested {
    Inner inner;
}

class Inner {
    String foo;
}

In order to resolve the inner string foo of an outer instance you have to add multiple null checks to prevent possible NullPointerExceptions:

Outer outer = new Outer();
if (outer != null && outer.nested != null && outer.nested.inner != null) {
    System.out.println(outer.nested.inner.foo);
}

The same behavior can be obtained by utilizing optionals flatMap operation:

Optional.of(new Outer())
    .flatMap(o -> Optional.ofNullable(o.nested))
    .flatMap(n -> Optional.ofNullable(n.inner))
    .flatMap(i -> Optional.ofNullable(i.foo))
    .ifPresent(System.out::println);

Reduce

The reduction operation combines all elements of the stream into a single result. Java 8 supports three different kind of reduce methods. The first one reduces a stream of elements to exactly one element of the stream.

persons
    .stream()
    .reduce((p1, p2) -> p1.age > p2.age ? p1 : p2)
    .ifPresent(System.out::println);    // Pamela

The second reduce method accepts both an identity value and a BinaryOperatoraccumulator. This method can be utilized to construct a new Person with the aggregated names and ages from all other persons in the stream:

Person result =
    persons
        .stream()
        .reduce(new Person("", 0), (p1, p2) -> {
            p1.age += p2.age;
            p1.name += p2.name;
            return p1;
        });

System.out.format("name=%s; age=%s", result.name, result.age);
// name=MaxPeterPamelaDavid; age=76

The third reduce method accepts three parameters: an identity value, a BiFunctionaccumulator and a combiner function of type BinaryOperator. Since the identity values type is not restricted to the Person type, we can utilize this reduction to determine the sum of ages from all persons:

Integer ageSum = persons
    .stream()
    .reduce(0, (sum, p) -> sum += p.age, (sum1, sum2) -> sum1 + sum2);

System.out.println(ageSum);  // 76

6. Parallel Streams

It can be stated that parallel streams can bring be a nice performance boost to streams with a large amount of input elements. But keep in mind that some parallel stream operations like reduce and collect need additional computations (combine operations) which isn‘t needed when executed sequentially.

Furthermore we‘ve learned that all parallel stream operations share the same JVM-wide common ForkJoinPool. So you probably want to avoid implementing slow blocking stream operations since that could potentially slow down other parts of your application which rely heavily on parallel streams.

时间: 2025-01-07 03:57:34

Java 8 Learn Notes - Streams的相关文章

Stanford iOS Learn Notes - 3

题目是iOS Learn Notes,不过这篇文章更多的是梳理了一下过去半个月的弯路:没有跟iOS视频,去看国内的教材去了…… 掐指一算,已经半个月没有更新这个博客了,恍恍惚惚了半个月呀. 其实这半个月也没有闲着,把<Swift语言实战入门>的前8章翻了一下.第8章没有看完,实在看不下去了.怎么评价呢?这本书可以看出来写的时候应该还是用心写了的,最起码不像有些书,直接粘贴复制Swfit的官方文档翻译. 看了也有收获,比如 首先又熟悉了一遍Swift语法.这本书对语法进行了精简提炼,所以通读一遍

Java进阶教程:Streams API

Java进阶教程:Streams API Stream是啥 首先明确一点,Stream流和IO包里的InputStream.OutputStream是完全不同的概念!它是Java 8 中引入的新特性,Stream可以对集合元素进行各种高效.便利的聚合操作! 聚合是个什么东东呢? 聚合在信息科学中是指对有关的数据进行内容挑选.分析.归类,最后分析得到人们想要的结果,主要是指任何能够从数组产生标量值的数据转换过程 [1]  .近年来随着大数据的发展,聚合技术已经广泛地应用于文本分析,信息安全,网络传

Java 8 中的 Streams API 详解

为什么需要 Stream Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念.它也不同于 StAX 对 XML 解析的 Stream,也不是 Amazon Kinesis 对大数据实时处理的 Stream.Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利.高效的聚合操作(aggregate operation),或者大批量数据操作 (

Java 8 中的 Streams API

Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利.高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation) 同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程.通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并

[Java coding] leetcode notes

Start from Difficulty 1: 1, Remove specified elem from unsorted array: 2 pointers, one is to iterate all the elements in the array, the other is to increase if current value is not equal to specified element. 2, Remove duplicates from sorted array: s

Stanford iOS Learn Notes - 5

今天总结一下 View Controller Lifecycle和Autolayout,即课程的第8节. 1. View Controller Lifecycle 其实听了两遍课程之后,我还是不能准确的总结出每一个controller的生命周期是多长.controller即MVC中的C,在MVC被创建后,controller的生命周期开始,这个很容易理解.但是我困惑的是MVC生命周期的结束. 首先怎么定义MVC生命周期的结果?controller的deinit函数被调用(内存释放)?还是View

Stanford iOS Learn Notes - 8

好久没有写笔记了.这二十天好忙啊,项目要发布,没有太多的时间看iOS了. 现在已经把Stanford的教程学完了,但是感觉很尴尬.其实看代码的时候,感觉没一个Demo都没有什么东西,但是看Demo看时似是而非的看不懂.视频里面,教授用每个API都是信手拈来,但是自己去写,课后看代码的时候,就不是那么回事了.不同的地方改用什么对象,UILable还是UIButton?这个对象都有什么API?这些其实都不知道.而要成为熟练的iOS开发者,这些其实是非常非常重要的. 翻了之前的几遍笔记主要是写了API

Bash Scripting Learn Notes (1)

References: [1] http://www.tldp.org/LDP/Bash-Beginners-Guide/html/index.html 1. Executing programs from a script When the program being executed is a shell script, bash will create a new bash process using a fork. This subshell reads the lines from t

Stanford iOS Learn Notes - 1

这段时间在学习Stanford的iOS 8 的教学视频,学而不思则怠,所以准备总结一下看视频学习的一些笔记,便于自己加深理解. 现在已经学了6节课,从这六节课看,前三节课主要是讲了一个Calculator的Demo,并穿插了很少一些iOS的介绍,以及MVC的介绍.第四节主要是介绍了一些Swift的语法,第五节和第六节主要讲述了iOS的触控操作,并且穿插了一些Swift的语法.因此,这篇我把篇笔记分为三部分: 总结一下教授对iOS的简介 总结一下教授在这六节课里面讲的语法 总结一下Happines