Chapter 1 Stream stream
When it comes to streams, it's easy to think of I / O streams. In fact, who stipulates that "streams" must be "IO streams"? In Java 8, thanks to the functional programming brought by Lambda, a new Stream concept is introduced to solve the existing drawbacks of the existing set class library.
1.1 INTRODUCTION
Multistep traversal code of traditional set
Almost all collections (such as Collection interface or Map interface) support direct or indirect traversal operations. When we need to operate on the elements in the Collection, in addition to the necessary addition, deletion and acquisition, the most typical is set traversal. For example:
import java.util.ArrayList; import java.util.List; public class Demo01ForEach { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("Zhang Wuji"); list.add("Zhou Zhi Luo"); list.add("Zhao Min"); list.add("Zhang Qiang"); list.add("Zhang Sanfeng"); for (String name : list) { System.out.println(name); } } }
This is a very simple set traversal operation: print out every string in the set.
Disadvantages of loop traversal
Java 8's Lambda allows us to focus more on What rather than How, which has been compared with internal classes before. Now, let's take a closer look at the above example code and find out:
- The syntax of the for loop is "how to do it"
- The loop body of the for loop is "what to do"
Why cycle? Because it's going to be traversal. But is loop the only way to traverse? Traversal refers to the processing of each element one by one, rather than the cycle of sequential processing from the first to the last. The former is the purpose, the latter is the way.
Imagine if you want to filter the elements in a collection:
- Set A is filtered into subset B according to condition one;
- Then according to condition two, it is filtered to subset C.
What can we do? Prior to Java 8, this might have been:
import java.util.ArrayList; import java.util.List; public class Demo02NormalFilter { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("Zhang Wuji"); list.add("Zhou Zhi Luo"); list.add("Zhao Min"); list.add("Zhang Qiang"); list.add("Zhang Sanfeng"); List<String> zhangList = new ArrayList<>(); for (String name : list) { if (name.startsWith("Zhang")) { zhangList.add(name); } } List<String> shortList = new ArrayList<>(); for (String name : zhangList) { if (name.length() == 3) { shortList.add(name); } } for (String name : shortList) { System.out.println(name); } } }
There are three loops in this code, each with different functions:
- First, all the people surnamed Zhang were screened;
- Then we screen people whose names have three words;
- Finally, the results are printed out.
Whenever we need to operate on the elements in the collection, we always need to loop, loop and recycle. Is that right? No Circulation is the way to do things, not the purpose. On the other hand, using linear loops means that you can only traverse once. If you want to traverse again, you can only start from scratch with another loop.
So, how can Stream, a derivative of Lambda, bring us more elegant writing?
A better way to write Stream
Let's take a look at what is elegant with the Stream API of Java 8:
import java.util.ArrayList; import java.util.List; public class Demo03StreamFilter { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("Zhang Wuji"); list.add("Zhou Zhi Luo"); list.add("Zhao Min"); list.add("Zhang Qiang"); list.add("Zhang Sanfeng"); list.stream() .filter(s -> s.startsWith("Zhang")) .filter(s -> s.length() == 3) .forEach(System.out::println); } }
Reading the literal meaning of the code directly can perfectly show the semantics of the irrelevant logic mode: get the flow, filter the surname Zhang, filter the length of 3, print one by one. The code does not reflect the use of linear loops or any other algorithm for traversal. What we really want to do is better reflected in the code.
1.2 overview of flow thinking
Note: Please temporarily forget the inherent impression of traditional IO stream!
On the whole, flow thinking is similar to the "production line" in the factory.
When it is necessary to operate on multiple elements (especially multi-step operation), considering the performance and convenience, we should first spell out a "model" step scheme, and then execute it according to the scheme.
This figure shows the filtering, mapping, skipping, counting and other multi-step operations. This is a collection element processing scheme, and the scheme is a "function model". Each box in the diagram is a "flow", which can be converted from one flow model to another by calling the specified method. The rightmost number, 3, is the final result.
Here, filter, map and skip are all operating on the function model, and the collection elements are not really processed. Only when the end method count is executed, the whole model will execute according to the specified policy. This benefits from Lambda's delayed execution feature.
Note: "Stream stream" is actually a function model of collection elements. It is not a collection or a data structure. It does not store any elements (or their address values) in itself.
Stream is a queue of elements from a data source
- The element is a specific type of object, forming a queue. Stream s in Java do not store elements, they are computed on demand.
- The source of the data source stream. It can be a collection, an array, etc.
Unlike previous Collection operations, Stream operations have two basic features:
- Pipelining: intermediate operations return the flow object itself. In this way, multiple operations can be connected in series into a pipe, just like the fluent style. Doing so optimizes operations such as lazy execution and short circuiting.
- Internal iteration: in the past, collection traversal was performed explicitly outside the collection through Iterator or enhanced for, which is called external iteration. Stream provides the way of internal iteration, and the stream can directly call the traversal method.
When a Stream is used, it usually includes three basic steps: obtaining a data source → data conversion → performing operations to obtain the desired results. Each time the original Stream object is converted, it does not change, and a new Stream object is returned (there can be multiple conversions), which allows its operations to be arranged like a chain and become a pipeline.
1.3 access flow
Java. Util. Stream. Stream < T > is the most common stream interface newly added by Java 8. (this is not a functional interface.)
Getting a flow is very simple. There are several common ways:
- All Collection collections can obtain streams by stream default method;
- The static method of the Stream interface can get the Stream corresponding to the array.
Get stream from Collection
First, the java.util.Collection interface adds the default method stream to get the stream, so all its implementation classes can get the stream.
import java.util.*; import java.util.stream.Stream; public class Demo04GetStream { public static void main(String[] args) { List<String> list = new ArrayList<>(); // ... Stream<String> stream1 = list.stream(); Set<String> set = new HashSet<>(); // ... Stream<String> stream2 = set.stream(); Vector<String> vector = new Vector<>(); // ... Stream<String> stream3 = vector.stream(); } }
Get flow from Map
The java.util.Map interface is not a sub interface of Collection, and its K-V data structure does not conform to the single characteristics of flow elements. Therefore, to obtain the corresponding flow, it needs to be divided into key, value or entry
import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; public class Demo05GetStream { public static void main(String[] args) { Map<String, String> map = new HashMap<>(); // ... Stream<String> keyStream = map.keySet().stream(); Stream<String> valueStream = map.values().stream(); Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream(); } }
Get stream from array
If you are using an array instead of a collection or mapping, because it is impossible to add a default method to an array object, the static method of is provided in the Stream interface, which is very simple to use:
import java.util.stream.Stream; public class Demo06GetStream { public static void main(String[] args) { String[] array = { "Zhang Wuji", "Zhang Cu Shan", "Zhang Sanfeng", "Zhang Yiyuan" }; Stream<String> stream = Stream.of(array); } }
1.4 common methods
The operation of the flow model is very rich. Here are some common API s. These methods can be divided into two types:
-
Delay method: the return value type is still a method of the Stream interface's own type, so chain call is supported. (except for the termination method, all other methods are delay methods.)
-
End method: the return value type is no longer a method of the Stream interface's own type, so chain calls like StringBuilder are no longer supported. In this section, the termination methods include count and forEach methods.
Note: for more methods beyond this section, please refer to the API documentation.
One by one: forEach
Although the method name is for each, it is different from the "for each" nickname in the for loop.
void forEach(Consumer<? super T> action);
Review Consumer interface
The java.util.function.Consumer interface is a consumer interface.
The Consumer interface contains the abstract method void accept (t t t), which means to consume data of a specified generic type.
Basic use:
import java.util.stream.Stream; public class Demo12StreamForEach { public static void main(String[] args) { Stream<String> stream = Stream.of("Zhang Wuji", "Zhang Sanfeng", "Zhou Zhi Luo"); stream.forEach(name-> System.out.println(name)); } }
Filtering: filter
You can convert one stream to another through the filter method. Method signature:
Stream<T> filter(Predicate<? super T> predicate);
The interface receives a Predicate function interface parameter (which can be a Lambda or method reference) as a filter condition.
Review the Predicate interface
We have learned about the java.util.stream.Predicate functional interface before. The only abstract method is:
boolean test(T t);
This method will produce a boolean value result, which represents whether the specified condition is met. If the result is true, the filter method of the Stream will retain the element; if the result is false, the filter method will discard the element.
Basic use
The basic code used by the filter method in Stream stream is as follows:
import java.util.stream.Stream; public class Demo07StreamFilter { public static void main(String[] args) { Stream<String> original = Stream.of("Zhang Wuji", "Zhang Sanfeng", "Zhou Zhi Luo"); Stream<String> result = original.filter(s -> s.startsWith("Zhang")); } }
Here, the filter condition is specified by Lambda expression: the surname must be Zhang.
Mapping: map
If you need to map elements in a flow to another flow, you can use the map method. Method signature:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
This interface requires a Function type interface parameter, which can convert the T type data in the current flow to another R type flow.
Review Function interface
We have learned the java.util.stream.Function functional interface before. The only abstract method is:
R apply(T t);
This can convert a T type to an R type, and the action of this conversion is called mapping.
Basic use
The basic codes used for map method in Stream stream are as follows:
import java.util.stream.Stream; public class Demo08StreamMap { public static void main(String[] args) { Stream<String> original = Stream.of("10", "12", "18"); Stream<Integer> result = original.map(str->Integer.parseInt(str)); } }
In this code, the parameters of the map method are referenced by the method, and the string type is converted to int type (and automatically boxed as Integer class object).
Number of Statistics: count
Just like the size method in the old Collection, the flow provides the count method to count the number of elements:
long count();
This method returns a long value representing the number of elements (no longer int as in the old collection). Basic use:
import java.util.stream.Stream; public class Demo09StreamCount { public static void main(String[] args) { Stream<String> original = Stream.of("Zhang Wuji", "Zhang Sanfeng", "Zhou Zhi Luo"); Stream<String> result = original.filter(s -> s.startsWith("Zhang")); System.out.println(result.count()); // 2 } }
First few: limit
limit method can intercept convection, only the first n can be used. Method signature:
Stream<T> limit(long maxSize);
The parameter is a long type. If the current length of the set is greater than the parameter, it will be intercepted; otherwise, no operation will be performed. Basic use:
import java.util.stream.Stream; public class Demo10StreamLimit { public static void main(String[] args) { Stream<String> original = Stream.of("Zhang Wuji", "Zhang Sanfeng", "Zhou Zhi Luo"); Stream<String> result = original.limit(2); System.out.println(result.count()); // 2 } }
Skip the first few: skip
If you want to skip the first few elements, you can use the skip method to get a new stream after interception:
Stream<T> skip(long n);
If the current length of the stream is greater than N, the first n will be skipped; otherwise, an empty stream with a length of 0 will be obtained. Basic use:
import java.util.stream.Stream; public class Demo11StreamSkip { public static void main(String[] args) { Stream<String> original = Stream.of("Zhang Wuji", "Zhang Sanfeng", "Zhou Zhi Luo"); Stream<String> result = original.skip(2); System.out.println(result.count()); // 1 } }
Combination: concat
If there are two streams and you want to merge them into one Stream, you can use the static method concat of the Stream interface:
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
Note: This is a static method, which is different from the concat method in java.lang.String.
The basic use code of this method is as follows:
import java.util.stream.Stream; public class Demo12StreamConcat { public static void main(String[] args) { Stream<String> streamA = Stream.of("Zhang Wuji"); Stream<String> streamB = Stream.of("Zhang Cu Shan"); Stream<String> result = Stream.concat(streamA, streamB); } }
1.5 exercise: set element processing (traditional way)
subject
Now there are two ArrayList collection storage teams with multiple member names, which require the following steps in turn using the traditional for loop (or enhanced for loop):
- The first team only needs the names of members whose names are three words; they are stored in a new set.
- After the first team is filtered, only the first three people are needed; they are stored in a new collection.
- The second team only needs the name of the member whose surname is Zhang; it is stored in a new collection.
- Do not use the first two people after the second team screening; store in a new collection.
- Merge two teams into one team; store in a new collection.
- Creates a Person object based on a name; stores it in a new collection.
- Print the Person object information for the entire team.
The codes of the two teams (sets) are as follows:
import java.util.ArrayList; import java.util.List; public class DemoArrayListNames { public static void main(String[] args) { //The first team ArrayList<String> one = new ArrayList<>(); one.add("Di Ali Gerba"); one.add("Song Yuan Qiao"); one.add("Su Xing He"); one.add("Shi Tian Tian"); one.add("Stone jade"); one.add("Lao Tzu"); one.add("Chuang-tzu"); one.add("Master Hongqi"); //Second team ArrayList<String> two = new ArrayList<>(); two.add("Guli Nazha"); two.add("Zhang Wuji"); two.add("Zhao Liying"); two.add("Zhang Sanfeng"); two.add("Nicholas Zhao Si"); two.add("Zhang Tian AI"); two.add("Zhang Er dog"); // .... } }
The code of the Person class is:
public class Person { private String name; public Person() {} public Person(String name) { this.name = name; } @Override public String toString() { return "Person{name='" + name + "'}"; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
Answer
Since the traditional for loop writing method is used, then:
public class DemoArrayListNames { public static void main(String[] args) { List<String> one = new ArrayList<>(); // ... List<String> two = new ArrayList<>(); // ... // The first team only needs the name of the member whose name is 3 words; List<String> oneA = new ArrayList<>(); for (String name : one) { if (name.length() == 3) { oneA.add(name); } } // After the first team selection, only the first three people are needed; List<String> oneB = new ArrayList<>(); for (int i = 0; i < 3; i++) { oneB.add(oneA.get(i)); } // The second team only needs the name of Zhang; List<String> twoA = new ArrayList<>(); for (String name : two) { if (name.startsWith("Zhang")) { twoA.add(name); } } // The second team should not be the first two after screening; List<String> twoB = new ArrayList<>(); for (int i = 2; i < twoA.size(); i++) { twoB.add(twoA.get(i)); } // Merge two teams into one team; List<String> totalNames = new ArrayList<>(); totalNames.addAll(oneB); totalNames.addAll(twoB); // Create Person object based on name; List<Person> totalPersonList = new ArrayList<>(); for (String name : totalNames) { totalPersonList.add(new Person(name)); } // Print the Person object information for the entire team. for (Person person : totalPersonList) { System.out.println(person); } } }
The operation result is:
Person{name='Song Yuan Qiao'} Person{name='Su Xing He'} Person{name='Shi Tian Tian'} Person{name='Zhang Tian AI'} Person{name='Zhang Er dog'}
1.6 exercise: set element processing (Stream mode)
subject
Replace the traditional for loop writing method in the previous question with Stream stream processing method. The initial contents of the two sets are the same, and the definition of the Person class is the same.
Answer
The equivalent Stream stream processing code is:
import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; public class DemoStreamNames { public static void main(String[] args) { List<String> one = new ArrayList<>(); // ... List<String> two = new ArrayList<>(); // ... // The first team only needs the name of the member whose name is 3 words; // After the first team selection, only the first three people are needed; Stream<String> streamOne = one.stream().filter(s -> s.length() == 3).limit(3); // The second team only needs the name of Zhang; // The second team should not be the first two after screening; Stream<String> streamTwo = two.stream().filter(s -> s.startsWith("Zhang")).skip(2); // Merge two teams into one team; // Create Person object based on name; // Print the Person object information for the entire team. Stream.concat(streamOne, streamTwo).map(Person::new).forEach(System.out::println); } }
The operation effect is exactly the same:
Person{name='Song Yuan Qiao'} Person{name='Su Xing He'} Person{name='Shi Tian Tian'} Person{name='Zhang Tian AI'} Person{name='Zhang Er dog'}
Chapter II method reference
When using Lambda expressions, the code we actually pass in is a solution: what to do with what parameters. So consider a situation: if there is already the same scheme for the operation scheme we specified in Lambda, is it necessary to write repetitive logic?
2.1 redundant Lambda scenarios
Take a look at a simple functional interface to apply Lambda expressions:
@FunctionalInterface public interface Printable { void print(String str); }
The only abstract method in the Printable interface, print, takes a string parameter to print and display it. The code to use Lambda is simple:
public class Demo01PrintSimple { private static void printString(Printable data) { data.print("Hello, World!"); } public static void main(String[] args) { printString(s -> System.out.println(s)); } }
The printString method only calls the Printable interface's print method, regardless of where the specific implementation logic of the print method will print the String. The main method specifies the specific operation scheme of the functional interface Printable through the Lambda expression as follows: after getting the String (the type can be deduced, so it can be omitted), output it in the console.
2.2 problem analysis
The problem with this code is that there is a ready-made implementation for the operation scheme of console printout of strings, that is, the println(String) method in the System.out object. Since Lambda wants to call the println(String) method, why call it manually?
2.3 reference improvement code with method
Can you omit Lambda's syntax (although it's already quite concise)? Just "reference" the past:
public class Demo02PrintRef { private static void printString(Printable data) { data.print("Hello, World!"); } public static void main(String[] args) { printString(System.out::println); } }
Notice the double colon:: notation, which is called method reference, and the double colon is a new syntax.
2.4 method references
Double colon:: is a reference operator, and its expression is called a method reference. If the function scheme to be expressed by Lambda already exists in the implementation of a method, then the method can be referenced as the substitute of Lambda through double colons.
semantic analysis
For example, in the above example, an overloaded println(String) method in the System.out object is exactly what we need. Then, for the functional interface parameter of printString method, compare the following two writing methods, which are completely equivalent:
- Lambda expression: S - > system.out.println (s);
- Method reference: System.out::println
The first semantics refers to that after getting the parameters, they are passed to the System.out.println method for processing by Lambda.
The semantics of the second equivalent writing method is to directly replace Lambda with println method in System.out. The execution effect of the two writing methods is exactly the same, and the writing method referred to in the second method reuses the existing scheme, which is more concise.
Note: the parameter passed in Lambda must be the type that the method in the method reference can receive, otherwise an exception will be thrown
Derivation and omission
If you use Lambda, you don't need to specify parameter types or overload forms according to the principle of "derivable is eligible" - they are all automatically derived. If method reference is used, it can also be deduced according to the context.
Functional interface is the foundation of Lambda, and method reference is the twin brother of Lambda.
The following code will call different overloaded forms of the println method, and change the functional interface to an int type parameter:
@FunctionalInterface public interface PrintableInteger { void print(int str); }
Since the unique matching overload can be automatically deduced after the context changes, there is no change in the method reference:
public class Demo03PrintOverload { private static void printInteger(PrintableInteger data) { data.print(1024); } public static void main(String[] args) { printInteger(System.out::println); } }
This method reference will automatically match to the overloaded form of println(int).
2.5 reference member methods by object name
This is the most common use, as in the example above. If a member method already exists in a class:
public class MethodRefObject { public void printUpperCase(String str) { System.out.println(str.toUpperCase()); } }
Functional interfaces are still defined as:
@FunctionalInterface public interface Printable { void print(String str); }
If you need to use the printUpperCase member method to replace the Lambda of the Printable interface, and you already have an object instance of the MethodRefObject class, you can reference the member method by the object name. The code is:
public class Demo04MethodRef { private static void printString(Printable lambda) { lambda.print("Hello"); } public static void main(String[] args) { MethodRefObject obj = new MethodRefObject(); printString(obj::printUpperCase); } }
2.6 reference static methods by class name
Because the static method abs already exists in the java.lang.Math class, there are two ways to write when we need to call the method through Lambda. The first is the functional interface:
@FunctionalInterface public interface Calcable { int calc(int num); }
The first is to use Lambda expressions:
public class Demo05Lambda { private static void method(int num, Calcable lambda) { System.out.println(lambda.calc(num)); } public static void main(String[] args) { method(-10, n -> Math.abs(n)); } }
But a better way to use method references is to:
public class Demo06MethodRef { private static void method(int num, Calcable lambda) { System.out.println(lambda.calc(num)); } public static void main(String[] args) { method(-10, Math::abs); } }
In this example, the following two expressions are equivalent:
- Lambda expression: n - > math.abs (n)
- Method reference: Math::abs
Reference member methods through super
If there is an inheritance relationship, the method reference can also be used to replace the super call in Lambda. The first is the functional interface:
@FunctionalInterface public interface Greetable { void greet(); }
Then there is the content of the parent Human class:
public class Human { public void sayHello() { System.out.println("Hello!"); } }
The last part is the content of the subclass Man, in which Lambda is used:
public class Man extends Human { @Override public void sayHello() { System.out.println("Hello everyone,I am Man!"); } //Define the method, pass the parameter to the Greetable interface public void method(Greetable g){ g.greet(); } public void show(){ //Call method method, use Lambda expression method(()->{ //Create Human object and call sayHello method new Human().sayHello(); }); //Simplified Lambda method(()->new Human().sayHello()); //Use super keyword instead of parent class object method(()->super.sayHello()); } }
But it is better to use method reference to call sayHello method in the parent class, for example, another subclass, man:
public class Man extends Human { @Override public void sayHello() { System.out.println("Hello everyone,I am Man!"); } //Define the method, pass the parameter to the Greetable interface public void method(Greetable g){ g.greet(); } public void show(){ method(super::sayHello); } }
In this example, the following two expressions are equivalent:
- Lambda expression: () - > super. Sayhello()
- Method reference: super::sayHello
2.8 reference member methods through this
This represents the current object. If the method to be referenced is the member method in the current class, you can use the format of "this:: member method" to use the method reference. The first is a simple functional interface:
@FunctionalInterface public interface Richable { void buy(); }
Here is a Husband husband class:
public class Husband { private void marry(Richable lambda) { lambda.buy(); } public void beHappy() { marry(() -> System.out.println("Buy a house")); } }
The happy method beHappy calls the marriage method marry, whose parameter is the function interface Richable, so a Lambda expression is needed. However, if the content of this Lambda expression already exists in this class, you can modify the Husband husband class:
public class Husband { private void buyHouse() { System.out.println("Buy a house"); } private void marry(Richable lambda) { lambda.buy(); } public void beHappy() { marry(() -> this.buyHouse()); } }
If you want to cancel the Lambda expression and replace it with a method reference, the better way to write it is:
public class Husband { private void buyHouse() { System.out.println("Buy a house"); } private void marry(Richable lambda) { lambda.buy(); } public void beHappy() { marry(this::buyHouse); } }
In this example, the following two expressions are equivalent:
- Lambda expression: () - > this. Buyhouse()
- Method reference: this::buyHouse
Constructor reference of class 2.9
Because the name of the constructor is exactly the same as the class name, it is not fixed. So the constructor reference is represented in the form of the class name:: new. First, a simple Person class:
public class Person { private String name; public Person(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
Then there is the functional interface used to create the Person object:
public interface PersonBuilder { Person buildPerson(String name); }
To use this functional interface, you can use the Lambda expression:
public class Demo09Lambda { public static void printName(String name, PersonBuilder builder) { System.out.println(builder.buildPerson(name).getName()); } public static void main(String[] args) { printName("Zhao Liying", name -> new Person(name)); } }
But with constructor references, there is a better way to write:
public class Demo10ConstructorRef { public static void printName(String name, PersonBuilder builder) { System.out.println(builder.buildPerson(name).getName()); } public static void main(String[] args) { printName("Zhao Liying", Person::new); } }
In this example, the following two expressions are equivalent:
- Lambda expression: name - > new person (name)
- Method reference: Person::new
2.10 constructor reference of array
Arrays are also subclass objects of objects, so they also have constructors, but the syntax is slightly different. If it corresponds to the usage scenario of Lambda, a functional interface is required:
@FunctionalInterface public interface ArrayBuilder { int[] buildArray(int length); }
When applying the interface, you can use the Lambda expression:
public class Demo11ArrayInitRef { private static int[] initArray(int length, ArrayBuilder builder) { return builder.buildArray(length); } public static void main(String[] args) { int[] array = initArray(10, length -> new int[length]); } }
But a better way to write it is to use the constructor reference of the array:
public class Demo12ArrayInitRef { private static int[] initArray(int length, ArrayBuilder builder) { return builder.buildArray(length); } public static void main(String[] args) { int[] array = initArray(10, int[]::new); } }
In this example, the following two expressions are equivalent:
- Lambda expression: length - > New Int [length]
- Method reference: int []: New