Java's Stream stream explained in detail

1. What is Stream?

Stream is an important new feature of Java 8. It provides functional programming support and allows pipeline operations to operate collections. Stream operations traverse the data source, use pipeline operations to process the data and generate a result collection. This process usually does not affect the data source. cause impact.

At the same time, stream is not a data structure, it is just a view of a certain data source. The data source can be an array, Java container or I/O  channel , etc. Each operation in Stream will generate a new stream. Internally, the value will not be obtained immediately like ordinary collection operations. Instead, the value will be obtained lazily and will not be executed until the user actually needs the result .

Stream represents a data stream, and the number of data elements in the stream may be limited or unlimited.

The difference between streams and collections
  • No data is stored . A stream is an object based on a data source. It does not store data elements itself, but passes elements of the data source to operations through a pipeline.

  • Functional programming . Stream operations do not modify the data source, for example, filterthey do not delete data in the data source.

  • Delay operation . Many operations of the stream, such as filter, map and other intermediate operations, are delayed, and the operations will only be executed sequentially when the end operation is reached.

  • Can be untied . For an unlimited number of streams, some operations can be completed in a limited time, such as limit(n) or  findFirst(). These operations can implement "short-circuiting" and return after accessing a limited number of elements.

  • Pure consumption . The elements of the stream can only be accessed once, similar to Iterator, and there is no way back in the operation. If you want to revisit the elements of the stream from the beginning, sorry, you have to regenerate a new stream.

Collections are about data, streams are about calculations

 

 Stream in Java 8 is an enhancement to the functionality of Collection objects. It focuses on performing various very convenient and efficient aggregation operations (aggregate operations) or bulk data operations (bulk data operations) on collection objects. The Stream API greatly improves programming efficiency and program readability with the help of the newly emerged Lambda expression.

At the same time, it provides serial and parallel modes for aggregation operations. The concurrent mode can make full use of the advantages of multi-core processors and use fork/join parallel methods to split tasks and accelerate the processing process. Writing parallel code is usually difficult and error-prone, but using the Stream API makes it easy to write high-performance concurrent programs without writing a single line of multi-threaded code. Therefore, java.util.stream, which first appeared in Java 8, is a product of the combined influence of functional language + multi-core era.

2. Overview of stream operations

There are two types of stream operations: intermediate operations, termination operations, and short-circuit operations.

1. Intermediate operations

A stream can be followed by zero or more intermediate operations. Its purpose is mainly to open the stream, perform some degree of data mapping/filtering, and then return a new stream for use by the next operation. This type of operation is lazy, that is, just calling this type of method does not actually start the traversal of the stream.

map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
2. Terminate operation

​ A stream can only have one terminal operation. After this operation is executed, the stream is "lighted" and cannot be operated anymore. So this must be the last operation on the stream. The execution of the Terminal operation will actually start the traversal of the stream and generate a result or a side effect.

forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

3. Short circuit operation
  • For an intermediate operation, if it accepts an infinite stream, it can return a finite new Stream.

  • For a terminal operation, if it accepts an infinite stream, but can calculate the result in a limited time.

anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
Basic type Stream

IntStream, LongStream, DoubleStream , java provides corresponding Streams for these three basic numerical types.

Other numeric Streams are not provided in Java 8 because this would result in more amplified content. Conventional numerical aggregation operations can be performed through the above three Streams.

The construction of numerical streams
IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);
IntStream.range(1, 3).forEach(System.out::println);
IntStream.rangeClosed(1, 3).forEach(System.out::println);

range() needs to pass in two parameters: the start node and the end node, and returns an ordered LongStream. Contains all parameters between the start node and the end node, with an interval of 1.

The function of rangeClosed is similar to range. The difference is that rangeClosed includes the final end node, while range does not.

3. Characteristics of flow

Parallelism  

All stream operations can be executed serially or in parallel.

 Unless you create parallel streams explicitly, all Java libraries create serial streams. Collection.stream()Creates a serial stream for collections and Collection.parallelStream()a parallel stream for collections. IntStream.range(int, int)What is created is a serial stream. Methods can be used parallel()to convert serial streams into parallel streams, and sequential()methods can be used to convert streams into serial streams.

​ Unless the Javadoc of the method indicates that the result of the method is uncertain when executed in parallel (such as findAny, forEach), the results of serial and parallel execution should be the same.

Can't interfere 

Streams can be created from non-thread-safe collections, and non-concurrent data sources should not be changed while the stream's pipeline is executing.

The following code will throw java.util.ConcurrentModificationExceptionan exception:

List<String> l = new ArrayList(Arrays.asList("one", "two"));
Stream<String> sl = l.stream();
sl.forEach(s -> l.add("three"));

When setting up intermediate operations, you can change the data source. Concurrency problems (throwing exceptions, or unexpected results) may occur only when executing end-point operations. For example, the following code will not throw exceptions:

List l = new ArrayList(Arrays.asList("one", "two"));
Stream sl = l.stream();
l.add("three");
sl.forEach(System.out::println);

For concurrent data sources, there will be no such problem. For example, the following code is normal:

List l = new CopyOnWriteArrayList<>(Arrays.asList("one", "two"));
Stream sl = l.stream();
sl.forEach(s -> l.add("three"));

Although our above example is to modify the non-concurrent data source in the terminal operation, the non-concurrent data source may also be modified in other threads, and there will also be concurrency issues.

no status 

The parameters of most stream operations are functional interfaces, which can be implemented using Lambda expressions. They are used to describe user behavior and are called behavioral parameters.

If these behavioral parameters are stateful, the results of stream operations may be undefined, such as the following code:

List<String> l = new ArrayList(Arrays.asList("one", "two", ……));
class State {
    boolean s;
}
final State state = new State();
Stream<String> sl = l.stream().map(e -> {
    if (state.s)
        return "OK";
    else {
        state.s = true;
        return e;
	}
});
sl.forEach(System.out::println);

The results of multiple executions of the above code may be different when executed in parallel. This is because this lambda expression is stateful.

side effect

After all operations on the pipeline are executed, where are the results (if any) required by the user? The first thing to explain is that not all Stream end operations need to return results. Some operations are just to use their side effects ( Side-effects ). For example, using Stream.forEach()a method to print out the results is a common scenario of using side effects.

The use of behavioral parameters with  . In fact, the use of side effects should be avoided in other scenarios except printing . You may think that collecting elements in Stream.forEach() is a good choice, as in the code below, but unfortunately the correctness and efficiency of such use cannot be guaranteed because Stream may execute in parallel. Most uses of side effects can be done more safely and efficiently using reduction operations .

Many behavior parameters with side effects can be converted into side-effect-free implementations

ArrayList<String> list = Lists.newArrayList();
for (int i = 0;i<1000;i++) {
    list.add(i+"");
}
ArrayList<String> list2 = Lists.newArrayList();
// 副作用代码
list.parallelStream().forEach(s -> list2.add(s));
System.out.println(list2);

 The result of the above code is that an error occurred in the ArrayList operation in multi-threaded state. At the same time, if the size of list2 is not specified, an ArrayIndexOutOfBoundsException subscript out-of-bounds exception may be reported when the list is expanded.

You can change it to the following code without side effects, or use the concurrent collection class CopyOnWriteArrayList instead

1
2
3
4
5
6
COPY
ArrayList<String> list = Lists.newArrayList();
for (int i = 0;i<1000;i++) {
    list.add(i+"");
}
List<String> list2 = list.parallelStream().collect(Collectors.toList());
System.out.println(list2);
sort

The elements returned by some streams are in a certain order, which we call  encounter order . This order is the order in which the stream provides its elements. For example, the encounter order of an array is the sorting order of its elements, and the List is its iteration order. For HashSet, it has no encounter order itself.

Whether a stream is encounter order mainly depends on the data source and its intermediate operations. For example, the streams created on the data source List and Array are ordered (ordered), but the streams created on the HashSet are not ordered.

The method sorted() can convert the stream into encounter order , unorderedand can convert the stream into encounter order .

​ Note that this method does not sort or scatter the elements, but returns a stream of encounter order .

​ In addition, an operation may affect the ordering of the stream, such as mapa method, which will replace the elements in the stream with different values ​​​​or even types, so the ordering of the input elements has become meaningless, but for filtermethods In other words, it just discards some values, and the order of the input elements is still guaranteed.

For serial streams, whether the stream is ordered or not will not affect its performance, but will affect determinism. The results of an unordered stream may be different when executed multiple times.

For parallel streams, removing the ordering constraint may improve performance, for example distinct, groupingByin these aggregation operations.

associativity

An operation or function opis associative if it satisfies the following conditions:

1
COPY
(a on b) on c == a on (b on c)

For concurrent streams, if the operations satisfy associativity, we can calculate in parallel:

1
COPY
a to b to c to d == (a to b) to (c to d)

For example min, maxand string concatenation are all associative.

function object

When using Stream for functional programming, you often need to pass operations as parameters into the stream method, and the function object is the method or lambda expression as the object.

1
COPY
List<String> strArray =  Arrays.asList(stringArrays).stream().filter(x>x.contains("Tomas")).collect(Collectors.toList());

filterThe parameter in the above example x>x.contains("Tomas")is a lambda expression.

Stream creation

Streams can be created in a variety of ways:

Create a stream from a collection

The Collection interface in Java8 has been extended to provide two methods for obtaining streams:

  • Stream  stream()  : returns a sequential stream
  • Stream  parallelStream()  : Returns a parallel stream
1
2
COPY
Stream<Integer> stream1 = Arrays.asList(1,2,3,4).stream();
Stream<Integer> stream2 = Arrays.asList(1,2,3,4).parallelStream();

java.util.stream.StreamIt is one interface, and the return values ​​of various pipeline intermediate operations are its implementation classes, which allows us to conveniently pass parameters.

Create a stream from an array

The static method stream() of Arrays in Java8 can obtain an array stream: static Stream  stream( T[] array )  : returns an overloaded form of a stream that can handle arrays of corresponding basic types.

Arrays也提供了创建流的静态方法stream():

1
COPY
Arrays.stream(new int[]{1,2,3})
Create a stream from a value

You can use the static method Stream.of() to create a stream by displaying values, which can receive any number of parameters.

public static Stream of(T… values) : Returns a stream

StreamThe static methods of()can also be used to create streams:

1
COPY
Stream<String> stream3 =   Stream.of(new String[]{"1","2","3","4"});
Create a stream from a method

You can use the static methods Stream.iterate() and Stream.generate() to create infinite streams

迭代流 : public static Stream iterate(final T seed, final UnaryOperator f)

1
2
COPY
//Infinite geometric sequence with initial value 1 
Stream.iterate(1, n -> n * 2);

Generating stream: public static Stream generate(Supplier s)

1
2
COPY
//Infinite random number stream 
Stream.generate(Math::random)

Use the static methods of IntStream, LongStream, and DoubleStream to create finite streams

1
2
3
COPY
IntStream.of(new int[]{1, 2, 3});
IntStream.range(1, 3);
IntStream.rangeClosed(1, 3);

Create an infinite stream of values ​​using the ints() method of the random number class

1
2
COPY
Random random = new Random();
IntStream ints = random.ints();
Get stream from file

Obtain a stream of lines from a file using the lines method of BufferedReader

1
2
COPY
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("file.txt")));
Stream<String> lines = bufferedReader.lines();

Files类的操作路径的方法,如listfindwalk等。

其他类提供的创建流

一些类也提供了创建流的方法:

BitSet数值流

1
COPY
IntStream stream = new BitSet().stream();

Pattern 将字符串分隔成流

1
2
3
COPY
Pattern pattern = compile(",");
Stream<String> stringStream = pattern.splitAsStream("a,b,c,d");
stringStream.forEach(System.out::println);

JarFile 读取jar文件流

1
COPY
Stream<JarEntry> stream = new JarFile("").stream();

更底层的使用StreamSupport,它提供了将Spliterator转换成流的方法,目前还不了解

中间操作

​ 流操作是惰性执行的, 中间操作会返回一个新的流对象, 当执行终点操作时才会真正进行计算,下面介绍流的中间操作,除非传入的操作函数有副作用, 函数本身不会对数据源进行任何修改。

​ 这个Scala集合的转换操作不同,Scala集合转换操作会生成一个新的中间集合,显而易见Java的这种设计会减少中间对象的生成。

distinct 唯一

distinct保证数据源中的重复元素在结果中只出现一次, 它使用equals()方法判断两个元素是否相等.

1
2
COPY
Stream<String> stream3 = Stream.of(new String[]{"1", "2", "3", "4", "1", "2", "3", "4"});
System.out.println(stream3.distinct().collect(Collectors.toList()));
filter 过滤

filter根据传入的断言函数对所有元素进行检查, 只有使断言函数返回真的元素才会出现在结果中. filter不会对数据源进行修改.

1
2
3
COPY
Stream<String> stream3 = Stream.of(new String[]{"1", "2", "3", "4", "6", "7", "8", "9"});
List<String> stringList = stream3.filter(x-> Integer.parseInt(x)%2==0).collect(Collectors.toList());
System.out.println(stringList);
map 映射

map方法根据传入的mapper函数对元素进行一对一映射, 即数据源中的每一个元素都会在结果中被替换(映射)为mapper函数的返回值,也可以根据处理返回不同的数据类型。

1
2
3
COPY
Stream<String> stream3 = Stream.of(new String[]{"1", "2", "3", "4", "6", "7", "8", "9"});
List<Integer> integerList = stream3.map(x -> Integer.parseInt(x)).collect(Collectors.toList());
System.out.println(integerList);
flatmap 映射汇总

flatmap方法混合了map + flattern的功能,同时扩展flatMapToDouble、flatMapToInt、flatMapToLong提供了转换成特定流的方法。它将映射后的流的元素全部放入到一个新的流中。它的方法定义如下:

1
COPY
<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)

​ flatmap适用于多对多或者一对多的映射关系,mapper函数会将每一个元素转换成一个流对象,而flatMap方法返回的一个流包含所有mapper转换后的元素。

下面举个例子来详细说明:

给定一个列表{“aaa”,”bbb”,”ddd”,”eee”,”ccc”}。需要在控制台直接输出aaabbbdddeeeccc字样采用map来做

1
2
3
4
5
6
7
8
9
10
COPY
 List<String> list = Arrays.asList("aaa", "bbb", "ddd", "eee", "ccc");
//这里采用了两次forEach循环进行输出,显然不太优雅
list.stream().map(x -> {
    List<Character> characterList = new ArrayList<>();
    char[] chars = x.toCharArray();
    for (char c : chars) {
        characterList.add(c);
    }
    return characterList.stream();
}).forEach(xStream -> xStream.forEach(System.out::print)); //aaabbbdddeeeccc

采用flatMap来做

1
2
3
4
5
6
7
8
9
10
COPY
 List<String> list = Arrays.asList("aaa", "bbb", "ddd", "eee", "ccc");
//采用flatMap来做  体会一下flatMap的魅力吧
list.stream().flatMap(x -> {
    List<Character> characterList = new ArrayList<>();
    char[] chars = x.toCharArray();
    for (char c : chars) {
        characterList.add(c);
    }
    return characterList.stream();
}).forEach(System.out::print); //aaabbbdddeeeccc
limit 截断

limit方法指定数量的元素的流。对于串行流,这个方法是有效的,这是因为它只需返回前n个元素即可,但是对于有序的并行流,它可能花费相对较长的时间,如果你不在意有序,可以将有序并行流转换为无序的,可以提高性能。

limit(int n)当流中元素数大于n时丢弃超出的元素, 否则不进行处理, 达到限制流长度的目的.

1
2
3
COPY
Stream<Integer> stream3 = Stream.of(3,5,1,4,2,6,8,7);
List<Integer> integerList = stream3.limit(3).collect(Collectors.toList());
System.out.println(integerList);
peek 观察者

生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数;这里所说的消费函数有点类似于钩子,每个元素被消费时都会执行这个钩子

​ peek方法会对数据源中所有元素进行给定操作, 但在结果中仍然是数据源中的元素. 通常我们利用操作的副作用, 修改其它数据或进行输入输出.

​ peek接收一个没有返回值的λ表达式,可以做一些输出,外部处理等。map接收一个有返回值的λ表达式,之后Stream的泛型类型将转换为map参数λ表达式返回的类型

1
2
3
COPY
Stream<Integer>  stream3 = Stream.of(1,2,3,4,5,6,7,8,9);
List<Integer>  integerList = stream3.peek(x-> System.out.println(x)).collect(Collectors.toList());
System.out.println(integerList);
sorted 排序

sorted()将流中的元素按照自然排序方式进行排序,如果元素没有实现Comparable,则终点操作执行时会抛出java.lang.ClassCastException异常。

​ sorted(Comparator<? super T> comparator)可以指定排序的方式。

​ 对于有序流,排序是稳定的。对于非有序流,不保证排序稳定。

sorted方法用于对数据源进行排序:

1
2
3
COPY
Stream<Integer> stream3 = Stream.of(4, 5, 2, 6, 9, 0, 1, 3, 6, 8);
        List<Integer> integerList = stream3.sorted((x, y) -> x - y).collect(Collectors.toList());
System.out.println(integerList);

使用java.util.Comparator是更方便的方法, 默认进行升序排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
COPY
class Item {
            public Item(int value) {
                this.value = value;
            }
            private int value;
            public int getValue() {
                return value;
            }
            public void setValue(int value) {
                this.value = value;
            }
        }

Stream<Item> stream3 = Stream.of(new Item(4), new Item(3), new Item(6), new Item(9));
List<Item> itemList = stream3.sorted(Comparator.comparingInt(Item::getValue)).collect(Collectors.toList());
itemList.forEach(x -> System.out.print(x.getValue()+","));

使用reversed()方法进行降序排序:

1
2
3
COPY
Stream<Item> stream3 = Stream.of(new Item(4), new Item(3), new Item(6), new Item(9));
List<Item> itemList = stream3.sorted(Comparator.comparingInt(Item::getValue).reversed()).collect(Collectors.toList());
itemList.forEach(x -> System.out.print(x.getValue()+","));
skip 跳过

skip(int)返回丢弃了前n个元素的流. 如果流中的元素小于或者等于n,则返回空的流

1
2
3
COPY
Stream<Integer> stream3 = Stream.of(3,5,1,4,2,6,8,7);
List<Integer> integerList = stream3.skip(3).collect(Collectors.toList());
System.out.println(integerList);

终点操作

match 断言
1
2
3
COPY
public boolean 	allMatch(Predicate<? super T> predicate)
public boolean 	anyMatch(Predicate<? super T> predicate)
public boolean 	noneMatch(Predicate<? super T> predicate)

这一组方法用来检查流中的元素是否满足断言。

  • allMatch只有在所有的元素都满足断言时才返回true,否则flase,流为空时总是返回true

  • anyMatch只有在任意一个元素满足断言时就返回true,否则flase,

  • noneMatch只有在所有的元素都不满足断言时才返回true,否则flase,

1
2
3
4
5
6
COPY
System.out.println(Stream.of(1,2,3,4,5).allMatch( i -> i > 0)); //true
System.out.println(Stream.of(1,2,3,4,5).anyMatch( i -> i > 0)); //true
System.out.println(Stream.of(1,2,3,4,5).noneMatch( i -> i > 0)); //false
System.out.println(Stream.<Integer>empty().allMatch( i -> i > 0)); //true
System.out.println(Stream.<Integer>empty().anyMatch( i -> i > 0)); //false
System.out.println(Stream.<Integer>empty().noneMatch( i -> i > 0)); //true
count 计数

count方法返回流中的元素的数量。

1
2
COPY
String[] arr = new String[]{"a","b","c","d"};
long count = Arrays.stream(arr).count();

你也可以手动来实现它

1
2
COPY
String[] arr = new String[]{"a","b","c","d"};
long count = Arrays.stream(arr).mapToLong(x->1L).sum();
collect 收集

collect(Collector c) 将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法。辅助类Collectors提供了很多的collector收集器,可以满足我们日常的需求,你也可以创建新的collector实现特定的需求。它是一个值得关注的类,你需要熟悉这些特定的收集器,如聚合类averagingInt、最大最小值maxBy minBy、计数counting、分组groupingBy、字符串连接joining、分区partitioningBy、汇总summarizingInt、化简reducing、转换toXXX等。

收集器

Collectors里常用搜集器介绍:

方法 返回类型 作用
toList() List 把流中元素收集到List
List result = list.stream().collect(Collectors.toList());
toSet() Set 把流中元素收集到Set
Set result = list.stream().collect(Collectors.toSet());
toCollection() Collection 把流中元素收集到集合
Collection result = lsit.stream().collect(Collectors.toCollection(ArrayListL::new));
counting() Long 计算流中元素的个数
long count = lsit.stream().collect(Collectors.counting());
summingInt() Integer 对流中元素的整数属性求和
int total = lsit.stream().collect(Collectors.counting());
averagingInt Double 计算元素Integer属性的均值
double avg = lsit.stream().collect(Collectors.averagingInt(Student::getAge));
summarizingInt IntSummaryStatistics 收集元素Integer属性的统计值
IntSummaryStatistics result = list.stream().collect(Collectors.summarizingInt(Student::getAge));
**joining ** Stream 连接流中的每个字符串
String str = list.stream().map(Student::getName).collect(Collectors.joining());
**maxBy ** Optional 根据比较器选择最大值
Opetional max = list.stream().collect(Collectors.maxBy(comparingInt(Student::getAge)))
**minBy ** Optional 根据比较器选择最小值
Optional min= list.stream().collect(Collectors.minBy(comparingInt(Student::getAge)));
**reducing ** 规约产生的类型 从一个作为累加器的初始值开始,利用BinaryOperator与流中元素逐个结合,从而归约成单个值
int total = list.stream().collect(Collectors.reducing(0, Student::getAge, Integer::sum));
collectingAndThen 转换函数返回的类型 包裹另一个收集器,对其结果转换
int how = list.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
groupingBy Map<K, List> 根据某属性值对流分组,属性为K,结果为V
Map<Integer, List> map = list.stream().collect(Collectors.groupingBy(Student::getStatus));
partitioningBy Map<Boolean, List> 根据true或false进行分区
Map<Boolean, List> map = list.stream().collect(Collectors.partitioningBy(Student::getPass));
示例

collect是使用最广泛的终点操作, 也上文中多次出现:

1
2
3
COPY
List<String> list = Stream.of("a","b","c","b")
        .distinct()
        .collect(Collectors.toList())

​ toList()将流转换为List实例, 是最常见的用法, java.util.Collectors类中还有求和, 计算均值, 取最值, 字符串连接等多种收集方法。

find 返回
findAny()

返回任意一个元素,如果流为空,返回空的Optional,对于并行流来说,它只需要返回任意一个元素即可,所以性能可能要好于findFirst(),但是有可能多次执行的时候返回的结果不一样。

findFirst()

返回第一个元素,如果流为空,返回空的Optional。

forEach 遍历

forEach遍历流的每一个元素,执行指定的action。它是一个终点操作,和peek方法不同。这个方法不担保按照流的encounter order顺序执行,如果对于有序流按照它的encounter order顺序执行,你可以使用forEachOrdered方法。

forEach方法对流中所有元素执行给定操作, 没有返回值.

1
COPY
Stream.of(1,2,3,4,5).forEach(System.out::println);
嵌套遍历(不推荐)

如果要对两个集合进行遍历操作,可以将流嵌套,但是这种遍历的性能跟跟foreach嵌套一样,而且不能进行更复杂的操作,不推荐。

1
2
3
4
5
6
7
COPY
ArrayList<String> list = Lists.newArrayList("1", "2");
ArrayList<String> list2 = Lists.newArrayList("一", "二");
list.stream().forEach(str1->{
    list2.stream().forEach(str2->{
        System.out.println(str1+str2);
    });
});
max、min 最大最小值

max返回流中的最大值,

min返回流中的最小值。

1
2
3
4
5
6
7
COPY
ArrayList<Integer> list = Lists.newArrayList(3,5,2,1);
Integer max = list.stream().max(new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1 - o2;
    }
}).get();
concat 组合

concat(Stream a, Stream b)用来连接类型一样的两个流。

1
2
3
COPY
List<Integer> list1 = Arrays.asList(1,2,3);
List<Integer> list2 = Arrays.asList(4,3,2);
Stream.concat(list1.stream(),list2.stream()).forEach(System.out::print);
toXXX 转换

toArray方法将一个流转换成数组,而如果想转换成其它集合类型,西需要调用collect方法,利用Collectors.toXXX方法进行转换。

toArray()

将流中的元素放入到一个数组中,默认为Object数组

他还有一个重载方法可以返回指定类型的数组

1
2
COPY
Object[] objects = Stream.of(1, 2, 3, 4, 5).toArray();
Integer[] integers = Stream.of(1, 2, 3, 4, 5).toArray(Integer[]::new);
reduce 归约

reduce是常用的一个方法,事实上很多操作都是基于它实现的。

方法重载

它有几个重载方法:

方法 描述
reduce(BinaryOperator b) 可以将流中元素反复结合起来,得到一个值,返回 Optional
reduce(T iden, BinaryOperator b) 可以将流中元素反复结合起来,得到一个值,返回 T
reduce(U identity, BiFunction a, BinaryOperator combiner) 可以将流中元素反复结合起来,得到

PS: BinaryOperator 函数式接口,也即Lambada表达式

reduce思想

reduce是很重要的一种编程思想。这里重点介绍一下。reduce的作用是把stream中的元素给组合起来。至于怎么组合起来:

​ 它需要我们首先提供一个起始种子,然后依照某种运算规则使其与stream的第一个元素发生关系产生一个新的种子,这个新的种子再紧接着与stream的第二个元素发生关系产生又一个新的种子,就这样依次递归执行,最后产生的结果就是reduce的最终产出,这就是reduce的算法最通俗的描述;

​ 所以运用reduce我们可以做sum,min,max,average,所以这些我们称之为针对具体应用场景的reduce,这些常用的reduce,stream api已经为我们封装了对应的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
COPY
//求和 sum
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
// 没有起始值时返回为Optional类型
Optional<Integer> sumOptional = integers.stream().reduce(Integer::sum);
System.out.println(sumOptional.get()); //15

// 可以给一个起始种子值
Integer sumReduce = integers.stream().reduce(0, Integer::sum);
System.out.println(sumReduce); //15

//直接用sum方法
Integer sum = integers.stream().mapToInt(i -> i).sum();
System.out.println(sum); //15
第三个重载

前面两个方法比较简单,重点说说三个参数的reduce(U identity, BiFunction a, BinaryOperator combiner)

三个参数时是最难以理解的。 分析下它的三个参数:

  • identity: 一个初始化的值;这个初始化的值其类型是泛型U,与Reduce方法返回的类型一致;注意此时Stream中元素的类型是T,与U可以不一样也可以一样,这样的话操作空间就大了;不管Stream中存储的元素是什么类型,U都可以是任何类型,如U可以是一些基本数据类型的包装类型Integer、Long等;或者是String,又或者是一些集合类型ArrayList等;后面会说到这些用法。
  • accumulator: 其类型是BiFunction,输入是U与T两个类型的数据,而返回的是U类型;也就是说返回的类型与输入的第一个参数类型是一样的,而输入的第二个参数类型与Stream中元素类型是一样的
  • combiner: 其类型是BinaryOperator,支持的是对U类型的对象进行操作,第三个参数combiner主要是使用在并行计算的场景下;如果Stream是非并行时,它实际上是不生效的。

因此针对这个方法的分析需要分并行与非并行两个场景。

​ 就是因为U和T不一样,所以给了我们更多的发挥。比如设U的类型是ArrayList,那么可以将Stream中所有元素添加到ArrayList中再返回了,如下示例:

1
2
3
4
5
6
COPY
ArrayList<String> result = Stream.of("aa", "ab", "c", "ad").reduce(new ArrayList<>(),
                (u, s) -> {
                    u.add(s);
                    return u;
                }, (strings, strings2) -> strings);
System.out.println(result); //[aa, ab, c, ad]

​ 注意由于是非并行的,第三个参数实际上没有什么意义,可以指定r1或者r2为其返回值,甚至可以指定null为返回值。下面看看并行的情况:

​ 当Stream是并行时,第三个参数就有意义了,它会将不同线程计算的结果调用combiner做汇总后返回。注意由于采用了并行计算,前两个参数与非并行时也有了差异! 看个例子:

1
2
3
4
5
COPY
Integer reduce = Stream.of(1, 2, 3).parallel().reduce(
                4,
               (integer, integer2) -> integer + integer2,
               (integer, integer2) -> integer + integer2);
       System.out.println(reduce); //18

输出:18

​ omg,结果竟然是18。显然串行的话结果是10;这个不太好理解,但是我下面写一个等价的方式,可以帮助很好的理解这个结果:

1
2
COPY
Optional<Integer> reduce = Stream.of(1, 2, 3).map(n -> n + 4).reduce((s1, s2) -> s1 + s2);
System.out.println(reduce.get()); //18

​ 这种方式有助于理解并行三个参数时的场景,实际上就是第一步使用accumulator进行转换(它的两个输入参数一个是identity, 一个是序列中的每一个元素),由N个元素得到N个结果;第二步是使用combiner对第一步的N个结果做汇总。

reduce能干什么

好了,三个参数的reduce先介绍到这。下面继续看看reduce能为我们做什么?

1
2
3
4
5
6
7
8
COPY
//构造字符串流
List<String> strs = Arrays.asList("H", "E", "L", "L", "O");
// reduce
String concatReduce = strs.stream().reduce("", String::concat);
System.out.println(concatReduce); //HELLO
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);
Integer minReduce = integerStream.reduce(Integer.MAX_VALUE, Integer::min);
System.out.println(minReduce); //1

并发问题

除非显式地创建并行流, 否则默认创建的都是串行流.Collection.stream()为集合创建串行流,而Collection.parallelStream()创建并行流.

stream.parallel()方法可以将串行流转换成并行流,stream.sequential()方法将流转换成串行流.

1
2
COPY
Stream<Integer> stream3 = Stream.of(1,2,3,4,5,6,7,8,9);
stream3.forEach(x-> System.out.print(x+","));

输出

1,2,3,4,5,6,7,8,9,

流可以在非线程安全的集合上创建, 流操作不应该对非线程安全的数据源产生任何副作用, 否则将发生java.util.ConcurrentModificationException异常.

1
2
COPY
List<String> list = new ArrayList(Arrays.asList("x", "y"));
list.stream().forEach(x-> list.add("z"));

输出

1
2
3
4
COPY
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1388)
	at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
	at com.test.lambda.LambdaTest.main(LambdaTest.java:15)

对于线程安全的容器不会存在这个问题:

1
2
3
4
5
COPY
List<String> list = new CopyOnWriteArrayList(Arrays.asList("x", "y"));
list.stream().forEach(x->{
	list.add("z");
	System.out.println(list);
});

输出

[x, y, z]
[x, y, z, z]

当然作者建议Stream操作不要对数据源进行任何修改. 当然, 修改其它数据或者输入输出是允许的:

1
2
3
4
5
COPY
Set<String> set = new HashSet<String>();
List<String> list = new CopyOnWriteArrayList(Arrays.asList("x", "y"));
list.stream().forEach(x->{
    set.add(x);
});

理想的管道操作应该是无状态且与访问顺序无关的. 无状态是指操作的结果只与输入有关, 下面即是一个有状态的操作示例:

1
2
3
4
5
6
7
8
9
10
COPY
State state = getState();
List<String> list = new ArrayList(Arrays.asList("a", "b"));
list = list.stream().map(s -> {
  if (state.isReady()) {
    return s;
  }
  else {
    return null;
  }
});

无状态的操作保证无论系统状态如何管道的行为不变, 与顺序无关则有利于进行并行计算.

函数式接口

函数式接口会将签名匹配的函数对象(lambda表达式或方法)视作接口的实现。

1
2
3
4
5
COPY
@FunctionalInterface
interface Greeter
{
    void hello(String message);
}

函数式接口中有且只有一个非抽象方法。

1
COPY
Greeter greeter = message -> System.out.println("Hello " + message);

这在 Java 8 之前通常使用匿名内部类实现的:

1
2
3
4
5
6
COPY
Greeter greeter = new Greeter() {
            @Override
            public void hello(String message) {
                System.out.println("Hello " + message);
            }
        };

Java 8 将已有的一些接口实现为函数式接口:

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.util.Comparator
  • java.lang.reflect.InvocationHandler
  • java.io.FileFilter
  • java.nio.file.PathMatcher

java.util.function中定义了一些常用的函数式接口:

  • Consumer: 接受参数无返回
    • Consumer<T> -> void accept(T t);
    • BiConsumer<T,U> -> void accept(T t, U u);
    • DoubleConsumer -> void accept(double value);
  • Supplier: 不接受参数有返回
    • Supplier<T> -> T get();
    • DoubleSupplier -> double getAsDouble();
  • Function: 接受参数并返回
    • Function<T, R> -> R apply(T t);
    • BiFunction<T, U, R> -> R apply(T t, U u);
    • DoubleFunction<R> -> R apply(double value);
    • DoubleToIntFunction -> int applyAsInt(double value);
    • BinaryOperator<T> extends BiFunction<T,T,T>
  • Predicate: 接受参数返回boolean
    • Predicate<T> -> boolean test(T t);
    • BiPredicate<T, U> -> boolean test(T t, U u);
    • DoublePredicate -> boolean test(double value);

默认构造器可以作为supplier: Supplier<Item> supplier = Item::new;

Guess you like

Origin blog.csdn.net/qq_45443475/article/details/131437679