Learn how to use lambda expressions.

The introduction

Let’s start with an example. Why are streams introduced in Java8?

For example, implement a requirement to find the number of boys in a student set.

The traditional way to write this is:

public long getCountsOfMaleStudent(List<Student> students) {
    long count = 0;
    for (Student student : students) {
        if(student.isMale()) { count++; }}return count;
}
Copy the code

This doesn’t seem like a problem, as we’ve written too much of this ** “boilerplate” code **, and while a smart IDE simplifies this tedious process with the code Template feature, it doesn’t change the nature of redundant code.

Let’s look at using a stream:

public long getCountsOfMaleStudent(List<Student> students) {
    return students.stream().filter(Student::isMale).count();
}
Copy the code

One line of code solves the problem!

While readers may not be familiar with the syntactic features of flow, this is the idea of functional programming:

  • Get back to the essence of the problem and think in terms of mental models.
  • Lazy loading.
  • Simplify code.

Let’s start with the introduction of streams.

Create a flow

There are many ways to create streams, the most common of which is through the Stream() method of Collection or the Stream() method of Arrays. Such as:

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);
Copy the code

Of course, the Stream interface itself provides many flow-related operations.

/ / create the stream
Stream<Integer> numbers = Stream.of(1.2.3);
// Create an empty stream
Stream<String> emptyStream = Stream.empty();
// Create an infinite stream with the element "hi"
Stream<String> infiniteString = Stream.generate(() -> "hi");
// Create an incrementing infinite stream starting at 0
Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE));
Copy the code

Stream. Generate () and Stream. Iterate () are infinite streams. Use the limit() method, as in:

Stream<Double> top10 = Stream.generate(Math::random).limit(10);
Copy the code

Alternatively, elements can be skipped using the skip() method, and the concat() method joins the two streams.

/ / 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")); 
Copy the code

Common flow operations

filter

The filter() method simply filters elements based on the input conditional expression.

The interface is defined as follows:

Stream<T> filter(Predicate<? super T> predicate);
Copy the code

You can see that the input parameter is a Predicate, which is a conditional expression.

An example:

Stream.of("a"."1b"."c"."0x").filter(value -> isDigit(value.charAt(0)));
Copy the code

Filter out elements whose first character is a number.

The output is:

1b, 0x

map

The main purpose of map() is to convert new data through mapping functions. The interface is defined as follows:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);
Copy the code

The input parameter is a Function. An example:

Stream.of("a"."b"."c").map(String::toUpperCase);
Copy the code

Convert the string to uppercase. Output result:

A, B, C

flatMap

FlatMap () functions like map(), but it returns a Stream by converting multiple streams to a flat Stream. The interface is defined as follows:

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
Copy the code

An example:

Stream<List<Integer>> listStream = Stream.of(asList(1.2), asList(3.4));
Stream<Integer> integerStream = listStream.flatMap(numbers -> numbers.stream());
Copy the code

It converts a Stream consisting of two lists into a Stream containing all elements. Output:

[1, 2, 3, 4]

Stateful transitions

In the previous functions, neither map nor filter changes the state of the flow, that is, the result does not depend on the previous element. In addition, Java8 also provides stateful transformations, with distinct and sorted being the common operations.

distinct

The primary purpose of distinct() is to remove duplicate elements from a stream. Distinct and Oracle. Examples are as follows:

Stream<String> distinctStream = Stream.of("one"."one"."two"."three").distinct();
Copy the code

Removing duplicate elements from the string returns:

one, two, three

sorted

Sorted () is basically used to sort streams according to specified criteria. The interface is defined as follows:

Stream<T> sorted(Comparator<? super T> comparator);
Copy the code

As you can see, the incoming parameter is a Comparator, which is a functional interface. An example:

Stream<String> sortedStream = Stream.of("one"."two"."three").sorted(Comparator.comparing(String::length).reversed());
Copy the code

Sort strings in descending order by length.

Notice that the Comparator.comparing method is used to simplify the call.

The output is:

[three, one, two]

Optional type

Before moving on to the next topic, let’s introduce a new data structure for Java8: Optional. Optional encapsulates the result, which may or may not have a value, and can be used to add default values, map other values, throw exceptions, etc.

Examples of common operations are as follows:

// An Optional data is generated
Optional<String> maxStrOpt = Stream.of("one"."two"."three").max(String::compareToIgnoreCase);
// Add the data to the List if the value exists
ArrayList<String> result = newArrayList<String>(); maxStrOpt.ifPresent(result::add);// Map the result to uppercase and fetch.Optional<String> upperResult = maxStrOpt.map(String::toUpperCase); System.out.println(upperResult.get());// What happens if the value is null
maxStrOpt.orElse(""); 
// Add default value ""
maxStrOpt.orElseGet(() -> System.getProperty("user.dir")); 
// Returns the result through an expression
maxStrOpt.orElseThrow(RuntimeException::new); // Throw an exception
Copy the code

Aggregation operations

The previous functions were all returned streams, and due to the lazy loading nature of the Stream, they are not actually executed until the aggregation operations described in this section and the collection operations described in the following sections are performed.

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();
Copy the code

Count the number of words in a character stream that contain the character O. Results:

2

max/min

The main function of Max /min() is to get the maximum/minimum value of an element. The interface is defined as follows:

Optional<T> max(Comparator<? super T> comparator);
Optional<T> min(Comparator<? super T> comparator);
Copy the code

As you can see, the incoming parameter is the result of a Comparator function and returns an Optional parameter. An example:

Optional<String> maxStrOpt = Stream.of("one"."two"."three").max(String::compareToIgnoreCase); System.out.println(maxStrOpt.get());Copy the code

According to the alphabet comparison, the maximum value is calculated, and the result is:

two

findFirst/findAny

The primary purpose of findFirst() is to find the result of the first match. The main purpose of findAny() is to find a result that finds any match. It is particularly effective in parallel flows, because as soon as a matching element is found on any shard, the whole calculation ends.

All returns are Optional.

The interface is defined as follows:

Optional<T> findFirst(a);
Optional<T> findAny(a);
Copy the code

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());
Copy the code

The result is:

one two

anyMatch/allMatch/noneMatch

If only care about whether the match is successful, the return Boolean as a result, you can use 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);
Copy the code

AnyMatch represents anyMatch (or); AllMatch indicates all matches (and); NoneMatch represents 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);
Copy the code

The result is:

true, false, false

reduce

Reduce (), which is primarily a reduction operation, offers three different uses.

Usage 1: Interface definition:

Optional<T> reduce(BinaryOperator<T> accumulator);
Copy the code

It primarily accepts a BinaryOperator accumulator and returns Optional.

An example:

Optional<Integer> sum1 = Stream.of(1.2.3).reduce((x, y) -> x + y); System.out.println(sum1.get());Copy the code

The sum over the digital stream yields:

6

Usage 2: Interface definition:

T reduce(T identity, BinaryOperator<T> accumulator);
Copy the code

Unlike the previous method, it provides an initial value of identity, which ensures that the entire result cannot be empty, so instead of returning Optional, it returns the corresponding type T.

An example:

Integer sum2 = Stream.of(1.2.3).reduce(10, (x, y) -> x + y); System.out.println(sum2);Copy the code

The result is:

16

Usage:

Interface definition:

<U > U reduce(U identity, BiFunction < U, ? super T, U > accumulator, BinaryOperator < U > combiner);
Copy the code

This is one of the most complex uses, and is primarily used to convert elements to different data types. Accumulator is an accumulator that mainly accumulates data. Combiner combines data of different sections (in 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);
Copy the code

Count the word length of the elements and add them together to get:

5

Collect Operation

The collect() method is mainly used to convert streams to other data types.

Convert to set

You can convert it to a List, Set, and a specified collection type using the Collectors. ToList (), toSet(), and toCollection() methods. 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 via toCollection
TreeSet<Integer> numberTreeSet = numbers.stream().collect(Collectors.toCollection(TreeSet::new));
Copy the code

Note:

  • Static import is implemented for methods such as Collectors. ToList.
  • ToList () defaults to ArrayList and toSet() to HashSet. If neither of these data types satisfy the requirements, the toCollectio() method can be used to convert to the desired collection type.

Converted into value

In addition to turning into sets, you can also turn results into values. Common conversion functions include:

  • / summarizingLong Collectors. SummarizingInt () ()/summarizingDouble () / / statistical information, sum, average, quantity, the maximum and minimum values.
  • MaxBy ()/minBy() // Find the maximum/minimum value
  • Collectors. Counting () // Count the Collectors
  • / summingLong Collectors. SummingInt () ()/summingDouble () / / sum
  • Collectors. AveragingInt ()/averagingDouble()/averagingDouble(
  • Collectors. Joining () // Join the string

An example:

List<String> wordList = Arrays.asList("one"."two"."three");
// Get statistics, print average and maximum
IntSummaryStatistics summary = wordList.stream().collect(summarizingInt(String::length));
System.out.println(summary.getAverage() + "," + summary.getMax());
// Get the average length of the word
Double averageLength = wordList.stream().collect(averagingInt(String::length));
// Get the maximum word length
Optional<String> maxLength = wordList.stream().collect(maxBy(Comparator.comparing(String::length)));
Copy the code

The common feature of these methods is that the data type returned is Collector. Although it can be used in the Collect() method alone, it is rarely used in practice (after all, Stream itself provides a similar method) and is more commonly used in conjunction with the groupingBy() method to perform secondary processing of grouped data.

partitioningBy

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. The interface is defined as follows:

public static<T> Collector<T, ? , Map<Boolean, List<T>>> partitioningBy(Predicate<?super T> predicate)
Copy the code

An example:

public Map<Boolean, List<Student>> maleAndFemaieStudents(Stream<Student> students) {   		return students.collect(Collectors.partitioningBy(student -> student.isMale()));
}
Copy the code

The student stream is partitioned by gender and the results are saved in a Map.

groupingBy

The groupingBy operation is also based on the COLLECT operation, and its Function is to group operations according to conditions. Different from the partitioningBy operation, its input is a Function, so that the Key in the Map that returns the result is no longer a Boolean type. It’s a grouping of values that fit the criteria, and it’s 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)
Copy the code

A case in point

public Map<String, List<Student>> studentByName(Stream<Student> students) {
    return students.collect(Collectors.groupingBy(student -> student.getName()));
}
Copy the code

Group the students by name. As mentioned earlier, the groupingBy function can work with the aggregate function to do more complex operations. The following describes several common usage scenarios:

Group cities by state and count them.

public Map<String, Long> stateToCount(Stream<City> cities) {
    return cities.collect(groupingBy(City::getState, counting()));
}
Copy the code

Cities are grouped by state, and the total population is counted.

public Map<String, Integer> stateToCityPopulation(Stream<City> cities) {
    return cities.collect(groupingBy(City::getState, summingInt(City::getPopulation)));
}
Copy the code

Group cities by state and find the most populous cities in each state.

public Map<String, City> stateToLargestCity(Stream<City> cities) {
    return cities.collect(groupingBy(City::getState, maxBy(Comparator.comparing(City::getPopulation))));
}
Copy the code

Group cities by state and find 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)))));
}
Copy the code

Cities are grouped by state, and statistical information is obtained by population. Using statistics, you can perform summation, average, quantity, maximum/minimum

public Map<String, IntSummaryStatistics> stateToCityPopulationSummary(Stream<City> cities) {
    return cities.collect(groupingBy(City::getState, summarizingInt(City::getPopulation)));
}
Copy the code

Group the cities by state and then connect the names of the cities in 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)));
}
Copy the code

Group cities by state and join the city names in each state using the Joining function.

public Map<String, String> stateToCityNames2(Stream<City> cities) {
    return cities.collect(groupingBy(City::getState, mapping(City::getName, joining(","))));
}
Copy the code

As can be seen from the above examples, groupingBy functions and aggregate functions can form complex application scenarios.

Basic types of flow (IntStream LongStream, DoubleStream)

In the previous flows, the Stream and generic types were used to indicate element types. Java8 also provides a more direct way to stream basic data types to simplify usage.

  • IntStream can be used for byte, short, int, char, booelan types;
  • You can use LongStream for long;
  • You can use DoubleStream for float and Double types.

Example of creating a primitive type stream:

IntStream intStream = IntStream.of(1.2.3);
// Does not contain the upper limit of 10
IntStream rangeStream = IntStream.range(1.10);
// Contains an upper limit of 10
IntStream rangeClosedStream = IntStream.rangeClosed(1.10);  
Copy the code

Primitive streams also directly provide methods such as sum, average, Max, and min that are not available in a Stream. There is also a mapToInt mapToLong/mapToDouble method to transfer into basic types of flow. These two features make it easy to perform certain operations. Here’s another example.

Stream<String> twoWords = Stream.of("one"."two");
int twoWordsLength = twoWords.mapToInt(String::length).sum();
Copy the code

Count the total character length of the raw string stream.

Use streams in file operations

File operation is also one of the operations we usually use more, the use of flow can also help us simplify the operation.

Access directories and filtering

Files.list(Paths.get(".")).forEach(System.out::println); Files.list(Paths.get(".")).filter(Files::isDirectory);
Copy the code

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"));
Copy the code

Accessing subdirectories

List<File> allFiles = Stream.of(new File(".").listFiles()).flatMap(file -> file.listFiles() == null ? Stream.of(file) : Stream.of(file.listFiles())).collect(toList());
Copy the code

summary

Stream is the key abstraction for dealing with collections in Java8. It can specify operations on collections and perform very complex operations such as finding, filtering, and mapping data. Manipulating collection data using the Stream API is similar to database queries 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. For details of the API, see the following brain map: