Stream Fluent Programming: Elegant and Efficient Code Creation

Source: juejin.cn/post/
7254960898686910520


The concept and role of flow programming

A Java stream is a sequence of elements that can perform various operations to convert and process data. The concept of flow programming is based on the idea of ​​functional programming and aims to simplify the code and improve readability and maintainability.

insert image description here

The main functions of Java Stream are as follows:

  1. Simplify collection operations: Using traditional for loops or iterators to process collection data can result in lengthy and complex code. Using flow programming, you can perform operations such as filtering, mapping, sorting, and aggregation 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 series of steps before processing the data, but only execute them when the results are needed. This delayed computing feature means that the data processing operation process 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 fully utilizing 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 using flow programming improves code readability and simplicity

  1. Declarative programming style: Flow programming adopts a declarative programming style, where you only need to describe the operations 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, as you can focus more on expressing your intent rather than on how to achieve it.

  2. Chained calls: Streaming programming uses method chained calls to link multiple operations together. Each method returns a new stream object, so that you can write various operations sequentially in the code like a "pipeline" to make 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: Streaming programming provides a series of operation methods, such as filtering, mapping, sorting, aggregation, etc. These methods can be combined and used as needed. You can connect these operations together according to specific business needs to form a complex processing flow without writing a large number of loops and conditional statements. This way of combining operations makes the code more modular and maintainable.

  4. Reduce intermediate states: Traditional iteration methods usually require the introduction of intermediate variables to save intermediate results, which increases code complexity and maintenance costs. 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 conditionals: Flow programming can replace the traditional use of loops and conditional statements. For example, you can use the filter() method to filter elements, the map() method to convert elements, and the reduce() method to perform aggregation operations, etc. These methods can complete the corresponding operations with one line of code, avoiding cumbersome loops and conditional logic, making the code more concise and clear.

Stream basics

What is Stream

Stream is a new abstract concept introduced in Java 8, which represents a sequence of processing data. Simply put, 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 conversion, filtering, mapping, aggregation and other operations on the elements in the Stream, thereby realizing data processing and operation. The Stream API is designed 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 of a collection or array.

  2. Data processing: Stream provides a wealth of operation methods to process elements in the stream. These operations can be combined as needed to form a streamlined operation process. Common operations include filter, map, sorted, reduce, etc.

  3. Lazy evaluation: Stream operations are lazily evaluated, which means that 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 traversing elements) is called.

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

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

By using Stream, we can process data in a concise and 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 simpler, 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 it 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 the code more modular, testable, and maintainable. In addition, Stream also supports Lambda expressions, making the code more concise and flexible.

  3. Lazy evaluation: Stream operations are lazily evaluated, which means that calculations are 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 data set and improves efficiency.

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

  5. Optimized performance: The Stream API uses optimization techniques internally, 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. Can operate various data sources: Stream can not only operate 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 to handle various types of data.

How to create a Stream object

  1. Create from collection: We can stream()create a Stream object by calling the collection's method. For example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = numbers.stream();
  1. Create from array: Java 8 introduced Arraysthe class stream()method, which we can use to create a Stream object. For example:
String[] names = {"Alice", "Bob", "Carol"};
Stream<String> stream = Arrays.stream(names);
  1. Creation via Stream.of(): We can Stream.of()directly convert a set of elements into a Stream object using the method. For example:
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
  1. Created through 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();
  1. Create from I/O resources: Java 8 introduces some new I/O classes (such as 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();
}
  1. 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); // 过滤出偶数
  1. 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()); // 映射为单词长度
  1. FlatMap: flatMap()The method is similar to map()the method, except that it can map each element to a stream and connect 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); // 扁平化为一个流
  1. 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 个元素
  1. 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 个元素
  1. 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(); // 自然顺序排序
  1. Distinct: distinct()The method is used to remove duplicate elements in the Stream, and determines whether they are duplicates based on 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(); // 去重
  1. Collect: collect()The method is used to collect elements in the Stream into a 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
  1. Reduce: reduce()The method is used to perform binary operations on the elements in the Stream in sequence to obtain a final result. It accepts 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); // 对所有元素求和
  1. 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. 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 of the common operation methods provided by the Stream API. There are many other operation methods, such as Match, Find, ForEach, etc.

Stream intermediate operations

Filter operation (filter)

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

The syntax for filtering operations is as follows:

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

Among them, Tthe type representing the Stream element predicateis an instance of a functional interface Predicate, and its generic parameters are consistent with the Stream element type.

Use the filter operation to filter out elements that meet the requirements based on custom conditions, thereby performing precise data filtering on the Stream.

Here is an example of how to use a 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()iterate through 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 to facilitate chain calls and combine multiple operation steps.

In practical applications, filtering operations can be used in conjunction with other operation methods, such as map, sorted, 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, thus improving the performance and efficiency of the program.

Mapping operation (map)

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

The syntax for mapping operations is as follows:

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

Among them, Trepresents the element type of the original Stream, Rrepresents the element type of the mapped Stream, and mapperis an instance of a functional interface Function. Its generic parameters are the type of the original Stream element 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 that demonstrates how to use a 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 passing in a Lambda expression s -> s.length()to convert each string to its length. Then forEach()iterate through 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 to facilitate chain calls and combine multiple operation steps.

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

It should be noted that the mapping operation may cause a NullPointerException, so when performing the mapping operation, you should ensure that the original Stream does not contain null values, and handle null values ​​according to the specific circumstances.

Sorting operation (sorted)

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 be done in natural order or using custom comparators.

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 are ordered according to the natural ordering of the elements. If the elements implement Comparablethe interface and have a natural order, you can call this method directly for sorting.

In the second syntax form, sorted(Comparator<? super T> comparator)the method accepts a comparator (Comparator) as a parameter to specify the sorting rule of the elements. Custom comparators allow you to Comparablesort objects that are not of type.

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. Then forEach()iterate through 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 to facilitate chain calls and combine multiple operation steps.

In practical applications, sorting operations can be used in conjunction with other operation methods, such as filter, map, 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 sorting operations may affect program performance, especially for large data streams or complex sorting rules. Therefore, in practical applications, it is necessary to make trade-offs and optimizations based on specific circumstances, and select appropriate algorithms and data structures to improve the efficiency of sorting.

Truncation operations (limit and skip)

Truncation operations (limit and skip) are commonly used operation methods in the Stream API, which are used to truncate elements during stream processing.

  1. limit(n): retain the first n elements in the stream and return a new stream containing at most n elements. If there are fewer than n elements in the stream, the original stream is returned.

  2. skip(n): Skip the first n elements in the stream and return a new stream containing the remaining elements. If there are fewer than n elements in the stream, an empty stream is returned.

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

limit(n) method example:

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 retain the first three elements. Then use forEach()the method to iterate over the output results.

skip(n) method example:

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 to facilitate chain calls and combine multiple operation steps.

The truncation operation is very useful when processing large data streams or in scenarios where data needs to be split and displayed in pages. By limiting or skipping a specified number of elements, you can control the size and range of data, improve program performance and reduce 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 get stuck in an infinite loop, and using skip()the method makes no sense.

Stream terminal operations

forEach and peek

forEach and peek are both operation methods in the Stream API for traversing elements in the stream. They provide different functions and usage scenarios in the process of processing the stream.

  1. forEach: forEach is a terminal operation method that accepts a Consumer function as a parameter and executes the function for 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 using the stream() method. Then use the forEach method to iterate through 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 via map method. Then use the peek method to output the value of each element before transforming it. 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 and can be combined with other operation methods and called in a chain.

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 elements to subsequent operations.

Aggregation operations (reduce and collect)

Reduce and collect are both 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 that accepts a BinaryOperator function as a parameter, merges elements in the stream one by one, and finally gets a result. This method takes the first element in the stream as the initial value, and then passes the initial value and the next element to the BinaryOperator function for calculation. The result is then calculated with the next element, and so on, until all elements have been 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 through the stream() method. Then use the reduce method to perform a sum operation on the elements in the stream, and add each element in turn to obtain 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 concatenation, 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 through the stream() method. Then use the collect method to concatenate the elements in the stream into a string, using commas and spaces to separate each element.

It should be noted that reduce and collect are both terminal operations, and they will 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 customized results.

When choosing to use reduce or collect, you can decide based on specific needs and operation types. If you need to perform some kind of calculation and merging 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 summary, grouping, counting, etc., use collect.

Matching operations (allMatch, anyMatch and noneMatch)

In the Stream API, allMatch, anyMatch and noneMatch are methods used for matching operations. 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 conditions. When all elements in the stream meet the condition, return true; if there is an element that does not meet the condition, return false.

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 through the stream() method. Then use the allMatch method to determine whether the elements in the stream are all even numbers. Since there are odd numbers in the list, false is returned.

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

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 through the stream() method. Then use the anyMatch method to determine whether there are even numbers in the stream. Returns true since there are even numbers in the list.

  1. noneMatch: The noneMatch method is used to determine whether all elements in the stream do not meet the given conditions. When there is no element in the stream that meets the condition, it returns true; if there is an element that meets the condition, it returns false.

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 through 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.

It should be noted that allMatch, anyMatch and noneMatch are terminal operations that iterate over the elements in the stream until a condition is met or all elements have been processed. In terms of performance, allMatch and noneMatch can return results immediately at the first unmatched element, while anyMatch can return results when the first matching element is found.

Find operations (findFirst and findAny)

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

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

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 get the first element in the stream, and use the ifPresent method to determine whether the Optional contains a value, and process it accordingly.

  1. findAny: The findAny method is used to return any element in the stream. It returns an Optional object. If the stream is empty, it returns an empty Optional; if the stream is not empty, it returns the Optional of any element in the stream. 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 through 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. 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 through 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. If the stream is empty, it returns an empty Optional; if the stream is not empty, it returns the Optional of the maximum value in the stream.

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 through the stream() method. Then use the max method to get the maximum value in the stream, and use the ifPresent method to determine whether the Optional contains a value, and process it accordingly.

  1. min: The min method is used to return the minimum value in the stream. It returns an Optional object. If the stream is empty, it returns an empty Optional; if the stream is not empty, it returns the Optional of the minimum value in the stream.

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 through the stream() method. Then use the min method to get the minimum value in the stream, and use the ifPresent method to determine whether the Optional contains a value, and process it accordingly.

These statistical operation methods provide a convenient way to calculate the number, maximum and minimum values ​​of elements in a stream. Null pointer exceptions can be avoided by returning an Optional object.

Parallel stream

What is parallel streaming

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 a traditional sequential flow, all operations are executed sequentially on a single thread. Parallel streams divide the elements of the stream into multiple small chunks, process these chunks in parallel on multiple threads, and finally merge the results. This can make full use of the advantages of multi-core processors and speed up data processing.

To convert a sequential stream into 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 streaming are mainly reflected in scenarios where the amount of data is large and the processing time is long. For small-scale data and simple operations, sequential flow may be more efficient. When choosing to use parallel streams, you need to evaluate and test on a case-by-case basis to ensure optimal performance.

In addition, it is also important to note that parallel streams may introduce thread safety issues in some cases. If multiple threads access shared mutable state simultaneously, this can lead to data races and indeterminate results. 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 use parallel streams to improve performance:

  1. Creating a parallel stream: To create a parallel stream, just call the method on a normal stream parallel().
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> parallelStream = numbers.parallelStream();
  1. Leveraging task parallelism: Parallel streams break data into small chunks and process those chunks in parallel on multiple threads. This takes 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 method is used to perform calculations on each element in the stream. Due to the characteristics of parallel streams, calculation operations are executed in parallel on multiple threads, improving calculation efficiency.

  1. Avoid shared mutable state: In parallel streams, multiple threads operate on data simultaneously. Sharing mutable state (such as global variables) can lead to data races and undefined results. Therefore, avoid using shared mutable state in parallel streams, or take appropriate synchronization measures to ensure thread safety.

  2. Use appropriate operations: Some operations perform better in parallel streams, while others may cause performance degradation. Generally speaking, the performance of using aggregation-based operations (such as reduce, collect) and stateless transformation operations (such as map, filter) in parallel streams is better, 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, reducethe and filteroperations have good performance in parallel streams, while sortedthe operations 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 considerations should be made based on 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 large-scale data sets need to be processed, using parallel streams can make full use 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. Because parallel streams can allocate computing operations to multiple threads for parallel execution, they can effectively utilize the computing power of multi-core processors and increase the speed of computing.

  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: Parallel stream operations are executed in parallel on multiple threads, so thread safety issues need to be paid attention to. If multiple threads access shared mutable state simultaneously, this can lead to data races and indeterminate results. When dealing with parallel streams, avoid sharing mutable state, or use appropriate synchronization measures to ensure thread safety.

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

  3. Concurrent operation limitations: Certain operations may perform poorly in parallel streams or may cause erroneous results. For example, using stateful transformation operations (such as ) in parallel streams sortedcan lead to performance degradation or erroneous results. When using parallel streams, care should be taken to avoid such operations, or take appropriate handling measures when necessary.

  4. Memory consumption: Parallel streams require splitting data into small chunks for parallel processing, which can result in additional memory consumption. When processing 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.

Practical application examples

Use Stream to process collection data

  1. Filter out strings whose length is greater than or equal to 5 and print the output:
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, transform, count and other operations on collection data. Call the intermediate operations and terminal operations of Stream through chaining.

Using Stream for file operations

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 file name 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 file contents 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 implement 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 Stream to operate on the list:

  • Use stream()the method to convert the list to a Stream.

  • Use map()the method to convert each name to uppercase.

  • Use filter()the method to filter out names longer 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/asd54090/article/details/132585287