1 Introduction
Let's start with an example to see why streams are introduced in Java 8?
For example, we need to find the number of boys in the student set.
The traditional way of writing is:
public long getCountsOfMaleStudent(List<Student> students) { long count = 0; for (Student student : students) { if (student.isMale()) { count++; } } return count; }
It seems that there is no problem, because we have written too many similar * * "template" codes * *. Although the intelligent IDE simplifies this boring process through the code template function, it can not change the essence of redundant code after all.
Let's look at the way to use stream:
public long getCountsOfMaleStudent(List<Student> students) { return students.stream().filter(Student::isMale).count(); }
One line of code solves the problem!
Although the reader may not be familiar with the syntax characteristics of flow, this is the embodiment of the idea of functional programming:
- Return to the essence of the problem and think about the problem according to the mental model.
- Delay loading.
- Simplify the code.
The following is an introduction to the formal entry flow.
2 create flow
There are many ways to create a stream. The most common way is to generate a stream through the Stream() method of Collection or the Stream() method of Arrays. For example:
List<Integer> numbers = Arrays.asList(1, 2, 3); Stream<Integer> numberStream = numbers.stream(); String[] words = new String[]{"one", "two"}; Stream<String> wordsStream = Arrays.stream(words);
Of course, the Stream interface itself provides many Stream related operations.
// Create stream Stream<Integer> numbers = Stream.of(1, 2, 3); // Create an empty stream Stream<String> emptyStream = Stream.empty(); // Create an infinite stream with element "hi" Stream<String> infiniteString = Stream.generate(() -> "hi"); // Create an incremental infinite stream starting at 0 Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE));
Both Stream.generate() and Stream.iterate() generate infinite streams. If you want to intercept them as finite streams, you can use the limit() method, such as:
Stream<Double> top10 = Stream.generate(Math::random).limit(10);
In addition, you can skip elements through the skip() method and connect the two streams through the concat() method.
// 3, 4 Stream<Integer> skipedStream = Stream.of(1, 2, 3, 4).skip(2); // hello,world Stream<String> concatedStream = Stream.concat(Stream.of("hello"), Stream.of(",world"));
3 common stream operations
filter
The function of the filter() method is to filter elements according to the entered conditional expression.
The interface is defined as follows:
Stream<T> filter(Predicate<? super T> predicate);
It can be seen that the input parameter is a Predicate, that is, a conditional expression.
An example:
Stream.of("a", "1b", "c", "0x").filter(value -> isDigit(value.charAt(0)));
Filter out elements whose first character is a number.
The output result is:
1b, 0x
map
The main function of map() is to convert it into new data through mapping function. The interface is defined as follows:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
As you can see, the input parameter is a Function. An example:
Stream.of("a", "b", "c").map(String::toUpperCase);
Converts a string to uppercase. Output results:
A, B, C
flatMap
The Function of flatMap() is similar to map(), but it still returns a Stream through Function, that is, it converts multiple streams into a flat Stream. The interface is defined as follows:
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
An example:
Stream<List<Integer>> listStream = Stream.of(asList(1, 2), asList(3, 4)); Stream<Integer> integerStream = listStream.flatMap(numbers -> numbers.stream());
It converts a Stream composed of two list s into a Stream containing all elements. Output:
[1, 2, 3, 4]
4 stateful transition
In the functions described earlier, neither map nor filter will change the state of the flow, that is, the result does not depend on the previous elements. In addition, Java 8 also provides stateful transformation. The common operations are distinct and sorted.
distinct
The main function of distinct() is to remove duplicate elements from the stream. And Oracle's distinct. Examples are as follows:
Stream<String> distinctStream = Stream.of("one", "one", "two", "three").distinct();
Remove the duplicate elements in the string, and the return result is:
one, two, three
sorted
The main function of sorted() is to sort the convection according to the specified conditions. The interface is defined as follows:
Stream<T> sorted(Comparator<? super T> comparator);
It can be seen that the input parameter is a Comparator, that is, a functional interface. An example:
Stream<String> sortedStream = Stream.of("one", "two","three").sorted(Comparator.comparing(String::length).reversed());
Arrange strings in descending order of length.
Note that the Comparator.comparing method is used here to simplify the call.
The output result is:
[three, one, two]
5Optional type
Before introducing the next topic, let's introduce a new data structure in Java 8: Optional. The main function of Optional is to encapsulate the results. The results may or may not have values, and the results can be processed later, such as adding default values, mapping other values, throwing exceptions, etc.
The following are common operation examples:
// An Optional data is generated Optional<String> maxStrOpt = Stream.of("one", "two", "three").max(String::compareToIgnoreCase); // If the value exists, add the data to the List ArrayList<String> result = new ArrayList<String>();maxStrOpt.ifPresent(result::add); // Map the result to uppercase and take it out. Optional<String> upperResult = maxStrOpt.map(String::toUpperCase);System.out.println(upperResult.get()); // Subsequent processing when the value is empty maxStrOpt.orElse(""); // Add default value '' maxStrOpt.orElseGet(() -> System.getProperty("user.dir")); // Returns a result through an expression maxStrOpt.orElseThrow(RuntimeException::new); // Throw exception
6 aggregation operation
The functions described before are all returned streams. According to the delayed loading characteristics of streams, they will not be executed. They will only be executed after the aggregation operations in this section and the collection operations described in subsequent chapters.
The so-called aggregation operation is the process of aggregating a group of data into a result through operation.
Common aggregation operations are described below:
count
count() is used to count the total number of elements. It is often used together with filter. An example:
long count = Stream.of("one", "two", "three").filter(word->word.contains("o")).count();
Count the number of words containing the character o in the character stream. result:
2
max/min
The main function of max/min() is to obtain the maximum / minimum value of the element. The interface is defined as follows:
Optional<T> max(Comparator<? super T> comparator); Optional<T> min(Comparator<? super T> comparator);
It can be seen that the input parameter is a Comparator functional result, and the return is an Optional. An example:
Optional<String> maxStrOpt = Stream.of("one", "two", "three").max(String::compareToIgnoreCase);System.out.println(maxStrOpt.get());
Compare according to the alphabet and count the maximum value. The result is:
two
findFirst/findAny
The main function of findFirst() is to find the first matching result. The main function of findAny() is to find a result of any match. It is particularly effective in parallel streams, because as long as a matching element is found on any partition, the whole calculation will end.
The returned results are all Optional.
The interface is defined as follows:
Optional<T> findFirst(); Optional<T> findAny();
An example:
Optional<String> findFirstResult = Stream.of("one", "two", "three").filter(word -> word.contains("o").findFirst(); System.out.println(findFirstResult.get()); Optional<String> findAnyResult = Stream.of("one", "two", "three").filter(word -> word.contains("t").findAny(); System.out.println(findAnyResult.get());
The result is:
one two
anyMatch/allMatch/noneMatch
If you only care about whether the match is successful, that is, the boolean result is returned, you can use the anyMatch/allMatch/noneMatch function. The interface is defined as follows:
boolean anyMatch(Predicate<? super T> predicate); boolean allMatch(Predicate<? super T> predicate); boolean noneMatch(Predicate<? super T> predicate);
Where anyMatch represents any match (or); allMatch means all matches (and); noneMatch indicates a mismatch (not).
An example:
boolean anyMatch = Stream.of("one", "two", "three").anyMatch(word -> word.contains("o")); boolean allMatch = Stream.of("one", "two", "three").allMatch(word -> word.contains("o")); boolean noneMatch = Stream.of("one", "two", "three").noneMatch(word -> word.contains("o"); System.out.println(anyMatch + ", " + allMatch + ", " + noneMatch);
The result is:
true, false, false
reduce
reduce() is mainly used for reduction. It provides three different usages.
Usage 1: interface definition:
Optional<T> reduce(BinaryOperator<T> accumulator);
It mainly receives the accumulator of a BinaryOperator and returns the Optional type.
An example:
Optional<Integer> sum1 = Stream.of(1, 2, 3).reduce((x, y) -> x + y);System.out.println(sum1.get());
Sum the digital stream and the result is:
6
Usage 2: interface definition:
T reduce(T identity, BinaryOperator<T> accumulator);
The difference from the previous method is that it provides an initial value identity, which ensures that the entire calculation result cannot be empty, so it will no longer return Optional and directly return the corresponding type T.
An example:
Integer sum2 = Stream.of(1, 2, 3).reduce(10, (x, y) -> x + y);System.out.println(sum2);
The result is:
16
Usage 3:
Interface definition:
<U > U reduce(U identity, BiFunction < U, ? super T, U > accumulator, BinaryOperator < U > combiner);
This is the most complex usage, which is mainly used to convert elements into different data types. Accumulator is an accumulator, which mainly performs accumulation operations. combiner combines data of different segments (parallel flow scenario).
An example:
Integer sum3 = Stream.of("on", "off").reduce(0, (total, word) -> total + word.length(), (x, y) -> x + y); System.out.println(sum3);
Count the word length of elements and add them together. The result is:
5
7 collection operation (collect)
The collect() method is mainly used to convert the stream to other data types.
Convert to collection
You can convert to List, Set, and the specified collection type through the Collectors.toList()/toSet()/toCollection() method. An example:
List<Integer> numbers = asList(1, 2, 3, 4, 5); // Convert to List List<Integer> numberList = numbers.stream().collect(toList()); // Convert to Set Set<Integer> numberSet = numbers.stream().collect(toSet()); // Convert to TreeSet through toCollection TreeSet<Integer> numberTreeSet = numbers.stream().collect(Collectors.toCollection(TreeSet::new));
Note:
- Here, static import is implemented for methods like Collectors.toList.
- toList() is converted to ArrayList by default, and toSet() is converted to HashSet by default. If these two data types do not meet the requirements, they can be converted to the required collection type through toCollectio() method.
Convert to value
In addition to converting to a set, you can also convert the result to a value. Common conversion functions include:
- Collectors.summarizingInt()/summarizingLong()/summarizingDouble() / / obtain statistical information and perform summation, average, quantity, maximum value and minimum value.
- Collectors.maxBy()/minBy() / / find the maximum / minimum value
- Collectors.counting() / / calculate quantity
- Collectors. Summerint() / summerlong() / summerdouble() / / sum
- Collectors.averagingInt()/averagingDouble()/averagingDouble() / / average
- Collectors.joining() / / connect strings
An example:
List<String> wordList = Arrays.asList("one", "two", "three"); // Get statistics and print average and maximum values IntSummaryStatistics summary = wordList.stream().collect(summarizingInt(String::length)); System.out.println(summary.getAverage() + ", " + summary.getMax()); // Gets the average length of the word Double averageLength = wordList.stream().collect(averagingInt(String::length)); // Gets the maximum word length Optional<String> maxLength = wordList.stream().collect(maxBy(Comparator.comparing(String::length)));
The common feature of these methods is that the data type returned is Collector. Although it can be used alone in the Collect() method, it is rarely used in practice (after all, Stream itself provides a similar method). It is more commonly used together with the groupingBy() method to secondary process the grouped data.
8 partitioning by
The partitioningBy operation is completed based on the collect operation. It will partition according to the conditional flow and return a Map. The Key is boolean and the Value is the List of the corresponding partition, that is, the results are only qualified and unqualified. The interface is defined as follows:
public static <T> Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate)
An example:
public Map<Boolean, List<Student>> maleAndFemaieStudents(Stream<Student> students) { return students.collect(Collectors.partitioningBy(student -> student.isMale())); }
The student flow is partitioned by gender, and the results are saved in the Map.
9 grouping by
The groupingBy operation is also completed based on the collect operation. The Function is to perform grouping operations according to conditions. The difference between it and partitioningBy is that its input is a Function, so the Key in the Map that returns the result is no longer a boolean type, but a qualified grouping value, which will be used in a wider range of scenarios.
The interface is defined as follows:
public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier)
An example
public Map<String, List<Student>> studentByName(Stream<Student> students) { return students.collect(Collectors.groupingBy(student -> student.getName())); }
Group students by their names. As mentioned earlier, the groupingBy function can cooperate with the aggregate function to perform more complex operations. Here are several common usage scenarios:
Group according to the state where the city is located, and then count the quantity.
public Map<String, Long> stateToCount(Stream<City> cities) { return cities.collect(groupingBy(City::getState, counting())); }
The total population is counted by grouping according to the state where the city is located.
public Map<String, Integer> stateToCityPopulation(Stream<City> cities) { return cities.collect(groupingBy(City::getState, summingInt(City::getPopulation))); }
Group cities according to the states where they are located, and then find the cities with the largest population in each state.
public Map<String, City> stateToLargestCity(Stream<City> cities) { return cities.collect(groupingBy(City::getState, maxBy(Comparator.comparing(City::getPopulation)))); }
Group according to the state where the city is located, and then find the name with the longest city name in each state.
public Map<String, Optional<String>> stateToLongestCityName(Stream<City> cities) { return cities.collect(groupingBy(City::getState, mapping(City::getName, maxBy(Comparator.comparing(String::length))))); }
They are grouped according to the state where the city is located, and then the statistical information is obtained according to the population. Statistics can be used to perform summation, average, quantity, maximum / minimum value
public Map<String, IntSummaryStatistics> stateToCityPopulationSummary(Stream<City> cities) { return cities.collect(groupingBy(City::getState, summarizingInt(City::getPopulation))); }
Group cities according to their states, and then connect the city names of each state
public Map<String, String> stateToCityNames(Stream<City> cities) { return cities.collect(groupingBy(City::getState, reducing("", City::getName, (s, t) -> s.length() == 0 ? t : s + ", " + t))); }
Group according to the state where the city is located, connect the city names of each state, and use the joining function.
public Map<String, String> stateToCityNames2(Stream<City> cities) { return cities.collect(groupingBy(City::getState, mapping(City::getName, joining(", ")))); }
As can be seen from the above examples, the groupingBy function can be combined with the aggregation function to represent a very complex application scenario.
10 basic type stream (IntStream,LongStream,DoubleStream)
In the streams described above, the Stream is used to identify the element type in conjunction with the generic type. Java 8 also provides a more direct flow mode for basic data types to simplify use.
- For byte, short, int, char and booelan types, IntStream can be used;
- For long type, you can use LongStream;
- For float and Double types, you can use DoubleStream.
Example of creating a basic type flow:
IntStream intStream = IntStream.of(1, 2, 3); // Excluding upper limit 10 IntStream rangeStream = IntStream.range(1, 10); // Including upper limit 10 IntStream rangeClosedStream = IntStream.rangeClosed(1, 10);
Basic type streams also directly provide methods such as sum, average, Max and Min that are not available in Stream. There is also a mapToInt/mapToLong/mapToDouble method to convert the flow into a basic type flow. Using these two features, you can easily perform some operations. Let's take another example.
Stream<String> twoWords = Stream.of("one", "two"); int twoWordsLength = twoWords.mapToInt(String::length).sum();
Count the total length of characters for the original character stream.
11 using streams in file operations
File operation is also a kind of operation that we usually use more. Using stream can also help us simplify the operation.
Accessing directories and filtering
Files.list(Paths.get(".")).forEach(System.out::println);Files.list(Paths.get(".")).filter(Files::isDirectory);
Filter files by extension
Files.newDirectoryStream(Paths.get("."), path -> path.toString().endsWith("md")).forEach(System.out::println);File[] textFiles = new File(".").listFiles(path -> path.toString().endsWith("txt"));
Access subdirectories
List<File> allFiles = Stream.of(new File(".").listFiles()).flatMap(file -> file.listFiles() == null ? Stream.of(file) : Stream.of(file.listFiles())).collect(toList());
12 summary
Stream is a key abstract concept for processing collections in Java 8. It can specify the operations on collections and perform very complex operations such as finding, filtering and mapping data. Using the Stream API to operate on the collection data is similar to a database query executed using SQL. You can also use the Stream API to perform operations in parallel. In short, the Stream API provides an efficient and easy-to-use way to process data. See the following brain diagram for detailed API:
