4D detailed explanation of Stream streaming programming, writing code can also be very elegant

I. Introduction

The concept and role of streaming programming

A Java stream (Stream) is a series of element sequences that can perform various operations to realize data conversion and processing. The concept of stream programming is based on the idea of ​​functional programming, which aims to simplify the code, improve readability and maintainability.

The main functions of Java Stream are as follows:

  1. Simplified collection operations: Using traditional for loops or iterators to process collection data can lead to lengthy and complex code. Using streaming programming, you can filter, map, sort, aggregate and other operations on collections in a more intuitive and concise way, making the code clearer and easier to understand.

  2. Lazy Computation: Streaming operations allow you to define a sequence of operational steps before processing data, but only actually executing them when the results are needed. This delayed computing feature means that the operational flow of data processing can be dynamically adjusted as needed to improve efficiency.

  3. Parallel processing: Java Stream provides support for parallel streams, which can divide data into multiple blocks for parallel processing, thereby making full use of the performance advantages of multi-core processors and improving code execution speed.

  4. Functional programming style: Flow programming encourages the use of functional programming ideas to achieve code simplification and flexibility by passing functions as parameters or using Lambda expressions. This functional programming pattern helps reduce side effects and makes code easier to test and debug.

Why Streaming Programming Can Improve Code Readability and Conciseness

  1. Declarative programming style: Streaming programming adopts a declarative programming style, you only need to describe the operation you want to perform on the data, without explicitly writing iteration and control flow statements. This makes the code more intuitive and easier to understand, because you can focus more on expressing your intent rather than on how to achieve it.

  2. Chained call: Flow programming uses method chained call to link multiple operations together. Each method returns a new stream object, so that you can sequentially write various operations in the code like a "pipeline", making the code logic clear. This method of chained calls makes the code look smoother, reducing the use of intermediate variables and temporary collections.

  3. Combination of operations: Stream programming provides a series of operation methods, such as filtering, mapping, sorting, aggregation, etc., which can be combined as needed. You can connect these operations in series according to specific business needs to form a complex processing flow without writing a lot of loops and conditional statements. This way of combining operations makes the code more modular and maintainable.

  4. Reduce intermediate state: The traditional iteration method usually needs to introduce intermediate variables to save intermediate results, which will increase the complexity and maintenance cost of the code. Streaming programming links multiple operations together and transfers data through the stream object itself, avoiding the introduction of intermediate states. This approach makes the code more concise and reduces the use of temporary variables.

  5. Reduce Loops and Conditions: Streaming programming can replace the traditional use of loops and conditional statements. For example, you can use the filter() method to filter elements, use the map() method to convert elements, use the reduce() method to perform aggregation operations, and so on. These methods can use one line of code to complete the corresponding operation, avoiding cumbersome loops and conditional logic, making the code more concise and clear.

2. Basic knowledge of Stream

What is Stream

Stream (stream) is a new abstraction introduced by Java 8, which represents a sequence of processing data. In simple terms, a Stream is a collection of elements, which can be collections, arrays, I/O resources, or other data sources.

The Stream API provides a wealth of operation methods, which can perform various operations such as conversion, filtering, mapping, aggregation, etc. on the elements in the Stream, so as to realize the processing and operation of the data. The design goal of the Stream API is to provide an efficient, scalable, and easy-to-use way to process large amounts of data.

Stream has the following key characteristics:

  1. Data source: Stream can be created based on different types of data sources, such as collections, arrays, I/O resources, etc. You can create a stream by calling the stream() method on a collection or an array.

  2. Data processing: Stream provides a wealth of operation methods to process elements in the stream. These operations can be combined as required to form a pipelined operation process. Common operations include filtering (filter), mapping (map), sorting (sorted), aggregation (reduce), etc.

  3. Lazy evaluation: The operation of Stream is evaluated lazily, that is to say, when the operation process is defined, the actual calculation will not be performed immediately. The actual calculation process is only triggered when a terminating operation (such as collecting results or iterating over elements) is called.

  4. Immutability: Stream is immutable, it does not modify the original data source, nor does it produce intermediate state or side effects. Each operation returns a new stream object to guarantee data immutability.

  5. Parallel processing: Stream supports parallel processing, and the stream can be converted into a parallel stream through the parallel() method, taking advantage of multi-core processors to improve processing speed. In some cases, using parallel streams can greatly improve the performance of your program.

By using Streams, we can process data in a concise, functional way. Compared with traditional loops and conditional statements, Stream provides a higher level of abstraction, making the code more readable, concise and maintainable. It is a powerful tool that helps us process and manipulate collection data more efficiently.

Features and Benefits of Stream

  1. Simplified programming model: Stream provides a cleaner, more declarative programming model, making code easier to understand and maintain. By using the Stream API, we can implement complex data operations with less code, shifting the focus from how to implement to focusing more on what we want to do.

  2. Functional programming style: Stream is designed based on functional programming ideas, which encourages the use of immutable data and pure functions for operations. This style avoids side effects and makes code more modular, testable, and maintainable. In addition, Stream also supports Lambda expressions, making the code more concise and flexible.

  3. Lazy evaluation: The operation of Stream is evaluated lazily, that is to say, the calculation is not performed immediately when the operation process is defined. The actual calculation process is only triggered when the termination operation is called. This feature avoids unnecessary calculations on the entire dataset and improves efficiency.

  4. Parallel processing capability: Stream supports parallel processing. In some cases, the stream can be converted to a parallel stream through the parallel() method, and the advantages of multi-core processors can be used to improve the processing speed. Parallel streams can automatically divide data into multiple subtasks and execute them simultaneously on multiple threads, improving the efficiency of processing large amounts of data.

  5. Optimized performance: Stream API internally uses optimization techniques, such as delayed execution, short-circuit operations, etc., to improve computing performance. Stream operations are implemented through internal iterators, which can better utilize hardware resources and adapt to changes in data size.

  6. Support rich operation methods: Stream API provides many rich operation methods, such as filtering, mapping, sorting, aggregation, etc. These methods can be combined as required to form an operational process. When combining multiple operations, Stream provides a way to chain calls, making the code more concise and more readable.

  7. Various data sources can be manipulated: Stream can not only manipulate collection data, but also other data sources, such as arrays, I/O resources and even infinite sequences. This allows us to use the same programming model for various types of data.

How to create a Stream object

  1. Create from collection: We can stream()create a Stream object by calling the method of the collection. For example:

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    Stream<Integer> stream = numbers.stream();
    
  2. Create from Array: Java 8 introduced the method Arraysof the class stream(), which we can use to create a Stream object. For example:

    String[] names = {
          
          "Alice", "Bob", "Carol"};
    Stream<String> stream = Arrays.stream(names);
    
  3. Created via Stream.of(): We can use Stream.of()the method to directly convert a set of elements into a Stream object. For example:

    Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
    
  4. Created by Stream.builder(): If we are not sure how many elements to add to the Stream, we can use to Stream.builder()create a Stream.Builder object, and use its add()method to add elements one by one, and finally call build()the method to generate a Stream object. For example:

    Stream.Builder<String> builder = Stream.builder();
    builder.add("Apple");
    builder.add("Banana");
    builder.add("Cherry");
    Stream<String> stream = builder.build();
    
  5. Create from I/O resources: Java 8 introduces some new I/O classes (eg BufferedReader, , Filesetc.), which provide many methods to read data from files, network streams, etc. These methods usually return a Stream object, which can be used directly. For example:

    Path path = Paths.get("data.txt");
    try (Stream<String> stream = Files.lines(path)) {
          
          
        // 使用 stream 处理数据
    } catch (IOException e) {
          
          
        e.printStackTrace();
    }
    
  6. Creation via Generators: In addition to creating Streams from existing data sources, we can also use generators to generate elements. Stream.generate()The method and method are provided in Java 8 Stream.iterate()to create an infinite Stream. For example:

    Stream<Integer> stream = Stream.generate(() -> 0); // 创建一个无限流,每个元素都是 0
    Stream<Integer> stream = Stream.iterate(0, n -> n + 1); // 创建一个无限流,从 0 开始递增
    

It should be noted that the Stream object is a one-time use object, it can only be consumed once. Once a terminating operation (such as collecting results, traversing elements) is performed on the Stream, the Stream will be closed and cannot be used later. Therefore, when using Stream, a new Stream object needs to be recreated as needed.

Commonly used Stream operation methods

  1. Filter (Filter): filter()The method accepts a Predicate function as a parameter for filtering the elements in the Stream. Only elements satisfying the Predicate condition will be retained. For example:

    Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
    Stream<Integer> filteredStream = stream.filter(n -> n % 2 == 0); // 过滤出偶数
    
  2. Mapping (Map): map()The method accepts a Function function as a parameter, which is used to map and transform the elements in the Stream. The result of applying the function to each element constitutes a new Stream. For example:

    Stream<String> stream = Stream.of("apple", "banana", "cherry");
    Stream<Integer> mappedStream = stream.map(s -> s.length()); // 映射为单词长度
    
  3. FlatMap: flatMap()The method is similar to map()the method, except that it can map each element as a stream and concatenate all streams into one stream. This is mainly used to solve the case of nested collections. For example:

    List<List<Integer>> nestedList = Arrays.asList(
        Arrays.asList(1, 2),
        Arrays.asList(3, 4),
        Arrays.asList(5, 6)
    );
    Stream<Integer> flattenedStream = nestedList.stream().flatMap(List::stream); // 扁平化为一个流
    
  4. Truncation (Limit): limit()The method can limit the size of the Stream and only keep the first n elements. For example:

    Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
    Stream<Integer> limitedStream = stream.limit(3); // 只保留前 3 个元素
    
  5. Skip (Skip): skip()The method can skip the first n elements in the Stream and return a new Stream composed of the remaining elements. For example:

    Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
    Stream<Integer> skippedStream = stream.skip(2); // 跳过前 2 个元素
    
  6. Sorted (Sorted): sorted()The method is used to sort the elements in the Stream, and the default is natural order. You can also provide a custom Comparator parameter to specify the collation. For example:

    Stream<Integer> stream = Stream.of(5, 2, 4, 1, 3);
    Stream<Integer> sortedStream = stream.sorted(); // 自然顺序排序
    
  7. Distinct: distinct()The method is used to remove duplicate elements in the Stream, and judge whether they are duplicates according to equals()the and methods of the elements. hashCode()For example:

    Stream<Integer> stream = Stream.of(1, 2, 2, 3, 3, 3);
    Stream<Integer> distinctStream = stream.distinct(); // 去重
    
  8. Summary (Collect): collect()The method is used to collect the elements in the Stream into the result container, such as List, Set, Map, etc. You can use the factory methods provided by the predefined Collectors class to create collectors, or you can customize collectors. For example:

    Stream<String> stream = Stream.of("apple", "banana", "cherry");
    List<String> collectedList = stream.collect(Collectors.toList()); // 收集为 List
    
  9. Reduce: reduce()The method is used to sequentially perform binary operations on the elements in the Stream to obtain a final result. It takes an initial value and a BinaryOperator function as parameters. For example:

    Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
    Optional<Integer> sum = stream.reduce((a, b) -> a + b); // 对所有元素求和
    
  10. Statistics (Summary Statistics): summaryStatistics()The method can obtain some common statistical information from the Stream, such as the number of elements, minimum value, maximum value, sum and average value. For example:

    IntStream stream = IntStream.of(1, 2, 3, 4, 5);
    IntSummaryStatistics stats = stream.summaryStatistics();
    System.out.println("Count: " + stats.getCount());
    System.out.println("Min: " + stats.getMin());
    System.out.println("Max: " + stats.getMax());
    System.out.println("Sum: " + stats.getSum());
    System.out.println("Average: " + stats.getAverage());
    

The above are just some common operation methods provided by the Stream API, and there are many other operation methods, such as Match, Find, ForEach, etc.

3. Intermediate operations of Stream

Filter operation (filter)

The filter operation (filter) is a common operation method in the Stream API, which accepts a Predicate function as a parameter to filter the elements in the Stream. Only elements that meet the Predicate condition will be retained, while elements that do not meet the condition will be filtered out.

The syntax for filter operations is as follows:

Stream<T> filter(Predicate<? super T> predicate)

Among them, Trepresents the type of Stream element, which predicateis an instance of functional interface Predicate, and its generic parameter is consistent with the type of Stream element.

Use the filter operation to filter out the elements that meet the requirements according to the user-defined conditions, so as to perform precise data filtering on the Stream.

Here is an example showing how to use the filter operation to filter out even numbers in a stream of integers:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> filteredStream = stream.filter(n -> n % 2 == 0);
filteredStream.forEach(System.out::println); // 输出结果: 2 4

In this example, we first create a Stream containing integers, and call filter()the method to pass in a Lambda expression n -> n % 2 == 0, indicating that we want to filter out even numbers. Then forEach()traverse the output results through the method.

It should be noted that the filtering operation returns a new Stream instance, and the original Stream will not be changed. This is also an important feature of Stream operation methods, they usually return a new Stream instance for chaining calls and combining multiple operation steps.

In practical applications, filtering operations can be combined with other operation methods, such as mapping (map), sorting (sorted), reduction (reduce), etc., to achieve more complex data processing and transformation. The advantage of the filtering operation itself is that it can efficiently filter large data streams, thereby improving the performance and efficiency of the program.

Mapping operation (map)

Mapping operation (map) is a common operation method in the Stream API, which accepts a Function function as a parameter, and is used to map and transform each element in the Stream to generate a new Stream.

The syntax for the mapping operation is as follows:

<R> Stream<R> map(Function<? super T, ? extends R> mapper)

Among them, Trepresents the element type of the original Stream, and Rrepresents the element type of the mapped Stream, which mapperis an instance of a functional interface Function, and its generic parameters are the original Stream element type and the mapped element type respectively.

The elements in the Stream can be processed or converted one by one using the map operation to obtain a new Stream. This process usually involves applying a passed-in function to each element, building new elements based on the function's return value.

Here is an example showing how to use the map operation to convert each string in a string stream to its length:

Stream<String> stream = Stream.of("apple", "banana", "cherry");
Stream<Integer> mappedStream = stream.map(s -> s.length());
mappedStream.forEach(System.out::println); // 输出结果: 5 6 6

In this example, we first create a Stream containing strings, and call map()the method to pass in a Lambda expression s -> s.length(), indicating that we want to convert each string to its length. Then forEach()traverse the output results through the method.

It should be noted that the mapping operation returns a new Stream instance, and the original Stream will not be changed. This is also an important feature of Stream operation methods, they usually return a new Stream instance for chaining calls and combining multiple operation steps.

In practical applications, map operations can be used in combination with other operation methods, such as filter, sort, reduce, etc., to achieve more complex data processing and transformation. The advantage of the mapping operation itself is that the conversion of the original data can be realized through simple function transformation, which reduces the cumbersome loop operation and improves the readability and maintainability of the code.

It should be noted that the mapping operation may cause a NullPointerException (NullPointerException), so when performing the mapping operation, you should ensure that the original Stream does not contain null values, and perform null value processing according to the specific situation.

Sorting operation (sorted)

The sorting operation (sorted) is a common operation method in the Stream API, which is used to sort the elements in the Stream. Sorting operations can sort by natural order or using a custom comparator.

The syntax of the sort operation is as follows:

Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)

In the first syntax form, sorted()methods sort according to the natural order of the elements. If the elements implement Comparablethe interface and have a natural order, you can directly call this method to sort.

In the second syntax form, sorted(Comparator<? super T> comparator)the method accepts a comparator (Comparator) as a parameter, which is used to specify the collation of the elements. ComparableObjects that are not of type can be sorted by custom comparators .

Here is an example showing how to use the sort operation to sort a stream of strings:

Stream<String> stream = Stream.of("banana", "apple", "cherry");
Stream<String> sortedStream = stream.sorted();
sortedStream.forEach(System.out::println); // 输出结果: apple banana cherry

In this example, we first create a Stream containing strings, and directly call sorted()the method to sort them. Then forEach()traverse the output results through the method.

It should be noted that the sorting operation returns a new Stream instance, and the original Stream will not be changed. This is also an important feature of Stream operation methods, they usually return a new Stream instance for chaining calls and combining multiple operation steps.

In practical applications, the sorting operation can be combined with other operation methods, such as filtering (filter), mapping (map), reduction (reduce), etc., to achieve more complex data processing and transformation. The advantage of the sorting operation itself is that the data can be arranged in a specific order, which is convenient for searching, comparing and analyzing.

It should be noted that the sorting operation may affect the performance of the program, especially for large data streams or complex sorting rules. Therefore, in practical applications, it is necessary to make trade-offs and optimizations according to specific situations, and select appropriate algorithms and data structures to improve the efficiency of sorting.

Truncation operations (limit and skip)

The truncation operation (limit and skip) is a commonly used operation method in the Stream API, which is used to truncate elements during the processing of the stream.

  1. limit(n): Keeps the first n elements in the stream and returns a new stream containing at most n elements. If the stream has fewer than n elements, returns the original stream.
  2. skip(n): Skips the first n elements in the stream, returning a new stream containing the remaining elements. Returns an empty stream if the stream has fewer than n elements.

The use of these two methods is described in detail below.

Example limit(n) method:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> limitedStream = stream.limit(3);
limitedStream.forEach(System.out::println); // 输出结果: 1 2 3

In this example, we create a Stream containing integers and call limit(3)the method to keep the first three elements. Then use forEach()the method to iterate over the output results.

Example of skip(n) method:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> skippedStream = stream.skip(2);
skippedStream.forEach(System.out::println); // 输出结果: 3 4 5

In this example, we create a Stream containing integers and call skip(2)the method to skip the first two elements. Then use forEach()the method to iterate over the output results.

It should be noted that the truncation operation returns a new Stream instance, and the original Stream will not be changed. This is also an important feature of Stream operation methods, they usually return a new Stream instance for chaining calls and combining multiple operation steps.

The truncation operation is very useful in processing large data streams or scenarios where data needs to be segmented and displayed in pages. By limiting or skipping a specified number of elements, you can control the size and extent of your data, improving your program's performance and reducing unnecessary calculations.

It should be noted that when using the truncation operation, you need to pay attention to the boundedness of the stream. If the stream is unbounded (for example Stream.generate()), then using limit()the method may cause the program to fall into an infinite loop, and using skip()the method does not make sense.

4. Terminal operation of Stream

forEach and peek

forEach and peek are both operation methods used in the Stream API to traverse the elements in the stream, and they provide different functions and usage scenarios in the process of processing the stream.

  1. forEach:
    forEach is a terminal operation method that takes a Consumer function as an argument and executes that function on each element in the stream. It has no return value, so the result of the operation cannot be passed to subsequent operations. forEach iterates over the entire stream, performing the same operation on each element.

Sample code:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()
     .forEach(System.out::println);

In this example, we create a List containing strings and convert it to a stream with the stream() method. Then use the forEach method to traverse and output the value of each element.

  1. peek:
    peek is an intermediate operation method that accepts a Consumer function as a parameter and executes the function on each element in the stream. Unlike forEach, the peek method returns a new stream with the same elements as the original stream.

Sample code:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> upperCaseNames = names.stream()
                                   .map(String::toUpperCase)
                                   .peek(System.out::println)
                                   .collect(Collectors.toList());

In this example, we first convert the List to a stream, and convert each element to uppercase through the map method. Then use the peek method to output the value of each element before the transformation. Finally, the elements are collected into a new List through the collect method.

It should be noted that whether it is forEach or peek, they are used to perform operations during the processing of the stream. The difference is that forEach is a terminal operation and does not return any results, while peek is an intermediate operation that can be combined and chained with other operation methods.

According to usage scenarios and requirements, choose to use forEach or peek to traverse the elements in the stream. If you just need to traverse the output elements without manipulating the results, use forEach. Use peek if you need to perform some additional operations during the traversal and pass the element to subsequent operations.

Aggregation operations (reduce and collect)

Both reduce and collect are methods used for aggregation operations in the Stream API. They can summarize, calculate and collect elements in the stream.

  1. reduce:
    reduce is a terminal operation method, which accepts a BinaryOperator function as a parameter, performs a merge operation on the elements in the stream one by one, and finally obtains a result. This method will use the first element in the stream as the initial value, then pass the initial value and the next element to the BinaryOperator function for calculation, and then calculate the result with the next element, and so on, until all elements are traversed .

Sample code:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> sum = numbers.stream()
                               .reduce((a, b) -> a + b);
sum.ifPresent(System.out::println); // 输出结果: 15

In this example, we create a List containing integers and convert it to a stream with the stream() method. Then use the reduce method to sum the elements in the stream, add each element in turn, and get the result 15.

  1. collect:
    collect is a terminal operation method that accepts an implementation of the Collector interface as a parameter to collect and summarize elements in the stream. The Collector interface defines a series of methods for aggregation operations, such as collecting elements into containers such as List, Set, and Map, or performing operations such as string connection, grouping, and counting.

Sample code:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
String joinedNames = names.stream()
                          .collect(Collectors.joining(", "));
System.out.println(joinedNames); // 输出结果: Alice, Bob, Charlie

In this example, we create a List containing strings and convert it to a stream with the stream() method. Then use the collect method to concatenate the elements in the stream into a string, separating each element with a comma and a space.

It should be noted that both reduce and collect are terminal operations, and they both trigger the traversal and processing of the stream. The difference is that the reduce method is used to accumulate and calculate the elements in the stream to obtain a final result; while the collect method is used to collect and summarize the elements in the stream to obtain a container or other custom results.

When choosing to use reduce or collect, you can decide according to your specific needs and operation types. If you need to perform some calculation and combination operation on the elements in the stream to get a result, use reduce. If you need to collect the elements in the stream into a container for summarization, grouping, counting, etc., use collect.

Match operations (allMatch, anyMatch, and noneMatch)

In the Stream API, allMatch, anyMatch and noneMatch are methods for matching operations, and they can be used to check whether elements in the stream meet specific conditions.

  1. allMatch:
    The allMatch method is used to determine whether all elements in the stream meet the given condition. Returns true when all elements in the stream satisfy the condition; false if there is an element that does not satisfy the condition.

Sample code:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean allEven = numbers.stream()
                         .allMatch(n -> n % 2 == 0);
System.out.println(allEven); // 输出结果: false

In this example, we create a List containing integers and convert it to a stream with the stream() method. Then use the allMatch method to determine whether the elements in the stream are all even. Returns false since there are odd numbers in the list.

  1. anyMatch:
    The anyMatch method is used to determine whether there is at least one element in the stream that satisfies the given condition. Returns true if at least one element in the stream satisfies the condition, or false if no element satisfies the condition.

Sample code:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean hasEven = numbers.stream()
                         .anyMatch(n -> n % 2 == 0);
System.out.println(hasEven); // 输出结果: true

In this example, we create a List containing integers and convert it to a stream with the stream() method. Then use the anyMatch method to determine whether there are even numbers in the stream. Returns true since there is an even number in the list.

  1. noneMatch:
    The noneMatch method is used to determine whether all elements in the stream do not meet the given condition. Returns true if no element in the stream satisfies the condition; false if there is an element that satisfies the condition.

Sample code:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean noneNegative = numbers.stream()
                             .noneMatch(n -> n < 0);
System.out.println(noneNegative); // 输出结果: true

In this example, we create a List containing integers and convert it to a stream with the stream() method. Then use the noneMatch method to determine whether the elements in the stream are all non-negative numbers. Since the elements in the list are all non-negative numbers, it returns true.

Note that allMatch, anyMatch, and noneMatch are terminal operations that iterate over the elements in the stream until the condition is met or all elements are processed. In terms of performance, allMatch and noneMatch can return the result immediately at the first unmatched element, while anyMatch can return the result when it finds the first matching element.

Find operations (findFirst and findAny)

In the Stream API, findFirst and findAny are methods for find operations, which can be used to obtain elements from the stream that meet certain conditions.

  1. findFirst:
    The findFirst method is used to return the first element in the stream. It returns an Optional object, or an empty Optional if the stream is empty, or an Optional for the first element in the stream if the stream is not empty.

Sample code:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Optional<String> first = names.stream()
                              .findFirst();
first.ifPresent(System.out::println); // 输出结果: Alice

In this example, we create a List containing strings and convert it to a stream with the stream() method. Then use the findFirst method to obtain the first element in the stream, and use the ifPresent method to determine whether the Optional contains a value, and perform corresponding processing.

  1. findAny:
    The findAny method is used to return any element in the stream. It returns an Optional object, or an empty Optional if the stream is empty, or an Optional for any element in the stream if the stream is not empty. In a sequential stream, the first element is usually returned; in a parallel stream, different elements may be returned due to multi-threaded processing.

Sample code:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> any = numbers.stream()
                               .filter(n -> n % 2 == 0)
                               .findAny();
any.ifPresent(System.out::println); // 输出结果: 2 或 4(取决于并行处理的结果)

In this example, we create a List containing integers and convert it to a stream with the stream() method. Then use the filter method to filter out even numbers, then use the findAny method to obtain any even number, and finally use the ifPresent method to determine whether the Optional contains a value, and perform corresponding processing.

It should be noted that findAny will have more advantages in parallel streams, because in multi-threaded processing, it can return the first found element to improve efficiency. In sequential flow, the performance of findAny is comparable to findFirst.

Statistical operations (count, max, and min)

In the Stream API, count, max, and min are methods used for statistical operations, and they can be used to obtain the number, maximum, and minimum values ​​of elements in the stream.

  1. count:
    The count method is used to return the number of elements in the stream. It returns a value of type long representing the number of elements in the stream.

Sample code:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
long count = numbers.stream()
                    .count();
System.out.println(count); // 输出结果: 5

In this example, we create a List containing integers and convert it to a stream with the stream() method. Then use the count method to get the number of elements in the stream and output the result.

  1. max:
    The max method is used to return the maximum value in the stream. It returns an Optional object, or an empty Optional if the stream is empty, or the maximum value in the stream if the stream is not empty.

Sample code:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> max = numbers.stream()
                               .max(Integer::compareTo);
max.ifPresent(System.out::println); // 输出结果: 5

In this example, we create a List containing integers and convert it to a stream with the stream() method. Then use the max method to obtain the maximum value in the stream, and use the ifPresent method to determine whether the Optional contains a value, and perform corresponding processing.

  1. min:
    The min method is used to return the minimum value in the stream. It returns an Optional object, or an empty Optional if the stream is empty, or an Optional with the smallest value in the stream if the stream is not empty.

Sample code:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> min = numbers.stream()
                               .min(Integer::compareTo);
min.ifPresent(System.out::println); // 输出结果: 1

In this example, we create a List containing integers and convert it to a stream with the stream() method. Then use the min method to obtain the minimum value in the stream, and use the ifPresent method to determine whether the Optional contains a value, and process accordingly.

These statistical manipulation methods provide a convenient way to perform count, maximum, and minimum calculations on elements in a stream. By returning an Optional object, you can avoid null pointer exceptions.

Five, parallel flow

What is parallel stream

Parallel streams are a feature in the Java 8 Stream API. It can parallelize the operation of a stream on multiple threads to improve performance when processing large amounts of data.

In traditional sequential streams, all operations are performed sequentially on a single thread. Parallel streams, on the other hand, divide the elements of the stream into small chunks, process the chunks in parallel on multiple threads, and finally combine the results. In this way, the advantages of multi-core processors can be fully utilized to speed up data processing.

To convert a sequential stream to a parallel stream, simply call the stream's parallel() method. The sample code is as follows:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
       .parallel()
       .forEach(System.out::println);

In this example, we create a List containing integers and convert it to a stream with the stream() method. Then call the parallel() method to convert the stream into a parallel stream, and then use the forEach method to traverse the elements in the stream and output them.

It is important to note that the use of parallel streams is not always suitable for all situations. The advantages of parallel streams are mainly reflected in scenarios with large data volume and long processing time. For small-scale data and simple operations, sequential streams may be more efficient. When choosing to use parallel streams, it needs to be evaluated and tested on a case-by-case basis to ensure the best performance.

In addition, it is also necessary to pay attention to the problem that parallel streams may introduce thread safety in some cases. If multiple threads access shared mutable state concurrently, data races and non-deterministic results may result. Therefore, when dealing with parallel streams, you should avoid sharing mutable state, or use appropriate synchronization measures to ensure thread safety.

How to Improve Performance Using Parallel Streams

Using parallel streams can improve program execution performance by utilizing multiple threads to process data in parallel. Here are some common ways to improve performance using parallel streams:

  1. Creating a parallel stream: To create a parallel stream, just call the method on the normal stream parallel().

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    Stream<Integer> parallelStream = numbers.parallelStream();
    
  2. Take advantage of task parallelism: Parallel streams divide data into small chunks and process those chunks in parallel on multiple threads. This can take full advantage of multi-core processors.

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    numbers.parallelStream()
           .map(n -> compute(n)) // 在多个线程上并行处理计算
           .forEach(System.out::println);
    

    In this example, mapthe calculation is performed on each element in the stream using the method. Due to the characteristics of parallel streams, computing operations will be executed in parallel on multiple threads, improving computing efficiency.

  3. Avoid shared mutable state: In parallel streams, multiple threads manipulate data concurrently. Data races and undefined results can result if mutable state (such as global variables) is shared. Therefore, avoid using shared mutable state in parallel streams, or take appropriate synchronization measures to ensure thread safety.

  4. Use appropriate operations: Some operations perform better in parallel streams, while others may cause performance degradation. In general, it is better to use aggregation-based operations (such as reduce) collectand stateless transformation operations (such as map) in parallel streams filter, while stateful transformation operations (such as sorted) may cause performance degradation.

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    
    // good performance
    int sum = numbers.parallelStream()
                     .reduce(0, Integer::sum);
    
    // good performance
    List<Integer> evenNumbers = numbers.parallelStream()
                                       .filter(n -> n % 2 == 0)
                                       .collect(Collectors.toList());
    
    // potential performance degradation
    List<Integer> sortedNumbers = numbers.parallelStream()
                                         .sorted()
                                         .collect(Collectors.toList());
    

    In this example, operations reducewith and filterhave good performance in parallel streams, while sortedoperations with may cause performance degradation.

In addition to the above methods, it should also be evaluated and tested on a case-by-case basis, whether parallel streams can improve performance. Sometimes, the overhead of parallel streams (such as thread creation and destruction, data cutting and merging, etc.) may exceed the performance improvement it brings. Therefore, when choosing to use parallel streams, comprehensive consideration should be given to factors such as data volume and operation complexity to ensure the best performance improvement.

Applicable Scenarios and Precautions for Parallel Streams

  1. Large-scale data sets: When processing large-scale data sets, using parallel streams can take full advantage of the advantages of multi-core processors and improve program execution efficiency. Parallel streams cut data into small chunks and process those chunks in parallel on multiple threads, reducing processing time.

  2. Complex computing operations: For complex computing operations, using parallel streams can speed up the computing process. Since the parallel stream can distribute computing operations to multiple threads for parallel execution, it can effectively utilize the computing power of multi-core processors and improve the computing speed.

  3. Stateless transformation operations: Parallel streams perform better when performing stateless transformation operations such as map. filterSuch operations do not depend on the state of other elements, and the processing of each element is independent of each other, which can be easily processed in parallel.

Considerations for parallel streams include:

  1. Thread safety issues: The operation of parallel streams is executed in parallel on multiple threads, so thread safety issues need to be paid attention to. If multiple threads access shared mutable state concurrently, data races and non-deterministic results may result. When dealing with parallel streams, you should avoid sharing mutable state, or employ appropriate synchronization measures to ensure thread safety.

  2. Performance evaluation and testing: The performance gains of parallel streams are not always noticeable. When choosing to use parallel streams, you should evaluate and test on a case-by-case basis to ensure the best performance gains. Sometimes, the overhead of parallel streams (such as thread creation and destruction, data cutting and merging, etc.) may outweigh the performance gains it brings.

  3. Concurrent operation limitations: Certain operations may perform poorly in parallel streams, or may produce incorrect results. For example, using stateful transformation operations (such as ) in parallel streams sortedmay result in poor performance or incorrect results. When using parallel streams, care should be taken to avoid such operations, or take appropriate handling measures when required.

  4. Memory consumption: Parallel streams require splitting the data into small chunks for parallel processing, which may result in additional memory consumption. When dealing with large-scale data sets, you should ensure that the system has enough memory to support the execution of parallel streams to avoid problems such as memory overflow.

6. Practical application examples

Use Stream to process collection data

  1. Filter out the strings whose length is greater than or equal to 5, and print out:
List<String> list = Arrays.asList("apple", "banana", "orange", "grapefruit", "kiwi");
list.stream()
    .filter(s -> s.length() >= 5)
    .forEach(System.out::println);

Output result:

banana
orange
grapefruit
  1. Convert each string in the collection to uppercase and collect into a new list:
List<String> list = Arrays.asList("apple", "banana", "orange", "grapefruit", "kiwi");
List<String> resultList = list.stream()
                              .map(String::toUpperCase)
                              .collect(Collectors.toList());
System.out.println(resultList);

Output result:

[APPLE, BANANA, ORANGE, GRAPEFRUIT, KIWI]
  1. Count the number of strings starting with the letter "a" in a collection:
List<String> list = Arrays.asList("apple", "banana", "orange", "grapefruit", "kiwi");
long count = list.stream()
                 .filter(s -> s.startsWith("a"))
                 .count();
System.out.println(count);

Output result:

1
  1. Use parallel streams to improve processing speed, filter out strings with a length less than or equal to 5, and print out:
List<String> list = Arrays.asList("apple", "banana", "orange", "grapefruit", "kiwi");
list.parallelStream()
    .filter(s -> s.length() <= 5)
    .forEach(System.out::println);

Output result:

apple
kiwi
  1. Use a Stream to sum the integers in a collection:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
                 .mapToInt(Integer::intValue)
                 .sum();
System.out.println(sum);

Output result:

15

The above example shows how to use Stream to filter, convert, count and other operations on collection data. The intermediate and terminal operations of the Stream are invoked through a chain.

File operations using Stream

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;

public class FileStreamExample {
    
    
    public static void main(String[] args) {
    
    
        String fileName = "file.txt";

        // 读取文件内容并创建 Stream
        try (Stream<String> stream = Files.lines(Paths.get(fileName))) {
    
    
            // 打印文件的每一行内容
            stream.forEach(System.out::println);

            // 统计文件的行数
            long count = stream.count();
            System.out.println("总行数:" + count);

            // 筛选包含关键词的行并打印输出
            stream.filter(line -> line.contains("keyword"))
                .forEach(System.out::println);
            
            // 将文件内容转换为大写并打印输出
            stream.map(String::toUpperCase)
                .forEach(System.out::println);

            // 将文件内容收集到 List 中
            List<String> lines = stream.collect(Collectors.toList());
            System.out.println(lines);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }
}

In the above code, the filename to be read is first specified file.txt. Then use Files.lines()the method to read each line of the file and create a Stream object. Next, we perform some operations on the Stream:

  • Use forEach()the method to print each line of the file.
  • Use count()the method to count the number of lines in a file.
  • Use filter()the method to filter out the lines containing keywords and print them out.
  • Use map()the method to convert the file content to uppercase and print it out.
  • Use collect()the method to collect the contents of the file into a List.

Please modify the file name, operation content and result processing method in the code according to actual needs. It should be noted that after using the Stream, the file resource should be closed in time, and the try-with-resources statement block can be used to automatically close the file. Also, handle possible IOException exceptions.

Use Stream to realize data conversion and filtering

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamExample {
    
    
    public static void main(String[] args) {
    
    
        List<String> names = Arrays.asList("Amy", "Bob", "Charlie", "David", "Eva");

        // 转换为大写并筛选出长度大于3的名称
        List<String> result = names.stream()
                                   .map(String::toUpperCase)
                                   .filter(name -> name.length() > 3)
                                   .collect(Collectors.toList());

        // 打印结果
        result.forEach(System.out::println);
    }
}

In the above code, we first create a list containing some names. Then use the Stream to operate on the list:

  • Use stream()the method to convert the list into a Stream.
  • map()Convert each name to uppercase using the method.
  • Use filter()the method to filter out names with a length greater than 3.
  • Use collect()the method to collect the filtered results into a new list.

Finally, we use forEach()the method to print each name in the resulting list.

Guess you like

Origin blog.csdn.net/u012581020/article/details/131698838