Stream stream, method reference

Stream stream, method reference

Chapter 1 Stream

Speaking of Stream, it is easy to think of I/O Stream, but in fact, who stipulates that "stream" must be "IO stream"? In Java 8, thanks to Lambda
The functional programming from the past introduces a new concept of Stream to solve the existing shortcomings of the existing collection class library.

1.1 Introduction

Multi-step traversal code for traditional collections Almost all collections (such as Collection interface or Map interface, etc.) 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 method is collection traversal. E.g:

import java. util. ArrayList;
import java. util. List;
public class Demo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Zhang San");
        list.add("Li Si");
        list.add("Zhao Wu");
        list.add("Zhang Laoliu");
        for (String name : list) {
            System.out.println(name);
        }
    }
}

This is a very simple collection traversal operation: print out each string in the collection.

Disadvantages of looping through

Java 8's Lambda allows us to focus more on What to do instead of how to do it (How), which has been compared with inner classes.

Now, let's take a closer look at the above example code and find that:

The syntax of the for loop is "how to"

The body of the for loop is "what to do"

Why use a loop? Because of the traversal. But is looping the only way to traverse? Traversal means that each element is processed one by one, rather than a loop that processes sequentially 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 the collection:

  1. Filter set A into subset B according to condition 1;

  2. Then filter into subset C according to condition 2.

What then? The pre-Java 8 approach might be:

import java.util.ArrayList;
import java. util. List;
public class Demo {
    public static void main(String[ ] args) {
        List<String> list = new ArrayList<>() ;
        list. add("Zhang Wuji") ;
        list. add("Zhou Zhiruo") ;
        list. add("Zhao Min") ;
        list. add("Zhang Qiang") ;
        list. add("Zhang Sanfeng") ;
        List<String> zhangList = new ArrayList<>() ;
        for (String name : list) {
            if (name. startsWith("open") ) {
                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) ;
        }
    }
}

This code contains three loops, each of which works differently:

  1. First screen all people with the surname Zhang;

  2. Then filter the people whose names have three characters;

  3. Finally, print out the results.

Whenever we need to operate on the elements in a collection, we always need to loop, loop, recycle. Is this for granted? no. A loop is a way of doing things, not a purpose. On the other hand, using a linear loop means that it can only be traversed once. If you want to traverse again, you can only start from the beginning with another loop. So, how can Lambda's derivative Stream bring us a more elegant way of writing?

A better way to write Stream

import java. util. ArrayList;
import java. util. List;

public class Demo {
    public static void main(String[ ] args) {
        List<String> list = new ArrayList<>() ;

        list. add("Zhang Wuji") ;
        list. add("Zhou Zhiruo") ;
        list. add("Zhao Min") ;
        list. add("Zhang Qiang") ;
        list. add("Zhang Sanfeng") ;
        list. stream()
                .filter(s -> s.startsWith("open"))
                .filter(s -> s. length() == 3)
                .forEach(System. out:: println) ;
    }
}

Directly reading the literal meaning of the code can perfectly display the semantics of irrelevant logic methods: get the stream, filter the last name Zhang, filter the length to 3, and print one by one. The code does not reflect the use of linear loops or any other algorithm to traverse, and what we really want to do is better reflected in the code.

1.2 Overview of Streaming Ideas

Note: Please temporarily forget the inherent impression of traditional IO streams!

On the whole, streaming thinking is similar to the "production line" on the factory floor.

When we need to operate on multiple elements (especially multi-step operations), considering performance and convenience, we should first put together a "model" step scheme, and then execute it according to the scheme.

Note: "Stream" is actually a functional model of collection elements. It is not a collection, nor a data structure, and does not store any elements (or their address values) by itself.

Stream is a queue of elements from a data source

Elements are objects of a specific type that form a queue. Stream s in Java do not store elements, but compute on demand. The source of the data source stream. Can be a collection, an array, etc. Unlike previous Collection operations, Stream operations have two basic characteristics:

  • Pipelining: Intermediate operations return the stream object itself. This way multiple operations can be concatenated into a pipeline, as in fluentstyle. Doing so allows optimizations for operations such as laziness and short-circuiting.
  • Internal iteration: Previously, the collection traversal was performed through Iterator or enhanced for, which explicitly iterates outside the collection, which is called external iteration. Stream provides an internal iteration method, and the stream can directly call the traversal method.

When using a stream, there are usually three basic steps: getting a data source data conversion performing operations to get the desired results, each time converting the original Stream object does not change, returning a new Stream object (There can be multiple transformations), which allows operations on it to be arranged like a chain, becoming a pipeline.

1.3 Get the stream

java.util.stream.Stream<T> is the most commonly used stream interface newly added in Java 8. (This is not a functional interface.)

Obtaining a stream is very simple. There are several common ways:

  • All Collection collections can obtain streams through the stream default method;

  • The static method of of the Stream interface can get the stream corresponding to the array.

Get stream based on 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 Demo {
    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 stream based on 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 characteristic of stream elements, so obtaining the corresponding stream needs to be divided into key, value or entry:

import java.util.HashMap;
import java. util. Map;
import java. util. stream. Stream;
public class Demo{
    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 not using a collection or a map but an array, since 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 Demo {
    public static void main(String[ ] args) {
        String[ ] array = { "Zhang Wuji", "Zhang Cuishan", "Zhang Sanfeng", "Zhang Yiyuan" };
        Stream<String> stream = Stream. of(array) ;
    }
}

Note: The parameter of the of method is actually a variable parameter, so arrays are supported

1.4 Common methods

The operations of the stream model are very rich. Here are some commonly used API s. These methods can be divided into two types:

  • Delayed method: The return value type is still the method of the Stream interface itself, so chained calls are supported. (Except the finalizer method, the rest
    All methods are delayed methods. )

  • Final 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
    use. In this section, finalizers include the count and forEach methods.

One by one: forEach

Although the method name is forEach , it is not the same as the "for-each" nickname in the for loop

void forEach(Consumer<? super T> action) ;

This method receives a Consumer interface function, and will hand each stream element to this function for processing.

Review the Consumer interface

java. util. function. Consumer<T>The interface is a consumer interface.
Consumer Interface contains abstract methods void accept(T t) , It means to consume data of a specified generic type.

Basic use:

import java. util. stream. Stream;
public class Demo {
    public static void main(String[ ] args) {
        Stream<String> stream = Stream. of("Zhang Wuji", "Zhang Sanfeng", "Zhou Zhiruo") ;
        stream. forEach(name-> System. out. println(name) ) ;
    }
}

filter: filter

A stream can be transformed into another subset stream by the filter method. Method signature:

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

The interface accepts a Predicate functional interface parameter (which can be a Lambda or a method reference) as a filter condition

Review the Predicate interface

Earlier we have studied the java.util.stream.Predicate functional interface, where the only abstract method is:

boolean test(T t) ;

This method will produce a boolean value indicating whether the specified condition is met. If the result is true, the Stream's filter method will keep 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 the 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 Zhiruo") ;
        Stream<String> result = original. filter(s -> s. startsWith("open") ) ;
    }
}

map: map

If you need to map elements from a stream to another stream, you can use the map method. Method signature:

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

This interface requires a Function functional interface parameter, which can convert data of type T in the current stream to another stream of type R.

Review the Function interface

Earlier we have studied the java.util.stream.Function functional interface, where the only abstract method is:

R apply(T t) ;

This can convert a T type into an R type, and the action of this conversion is called "mapping".

basic use

The code basically used by the map method in the Stream stream is 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 parameter of the map method is converted to an int type (and automatically boxed to an Integer class object) through a method reference.

Statistics: count

Like the size method in the old Collection, streams provide the count method to count the number of elements in them:

long count() ;

This method returns a long value representing the number of elements (no longer an int value like 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 Zhiruo") ;
        Stream<String> result = original. filter(s -> s. startsWith("open") ) ;
        System. out. println(result. count() ) ; // 2
    }
}

Take the first few: limit

The limit method can intercept the stream, and only the first n are used. Method signature:

Stream<T> limit(long maxSize) ;

The parameter is a long type. If the current length of the collection 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 Zhiruo") ;
        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, skip the first n; otherwise, an empty stream of length 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 Zhiruo") ;
        Stream<String> result = original. skip(2) ;
        System. out. println(result. count() ) ; // 1
    }
}

Combination: concat

If you have two streams and want to combine them into one stream, you can use the static method concat of the Stream interface:

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 Cuishan") ;
        Stream<String> result = Stream. concat(streamA, streamB) ;
    }
}

Chapter 2 Method Reference

When using Lambda expressions, the code we actually pass in is a solution: what to do with what parameters. Then consider a situation: If the operation scheme we specify in Lambda already has the same scheme, is it necessary to write repeated logic?

2.1 Redundant Lambda scenarios

Let's look at a simple functional interface to apply Lambda expressions:

@FunctionalInterface
public interface Printable {
	void print(String str) ;
}

The only abstract method print in the Printable interface accepts a string parameter, and the purpose is to print it. Then the code to use it via Lambda is simple:

import java.awt.print.Printable;
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 print method of the Printable interface, and does not care where the specific implementation logic of the print method will print the string. The main method specifies the specific operation plan of the functional interface Printable through Lambda expression: After getting the String (type can be deduced, so it can be omitted) data, output it in the console.

2.2 Problem Analysis

The problem with this code is that there is a ready-made implementation of the operation scheme for printing the string to the console, which is the println(String) method in the System.out object. Since all Lambda wants to do is to call the println(String) method, why bother calling it manually.

2.3 Improve code with method references

What about omitting the Lambda syntax (although it's already quite concise)? Just "quote" the past:

import java.awt.print.Printable;

public class Demo02PrintRef {
    private static void printString(Printable data) {
        data. print("Hello, World! ") ;
    }
    public static void main(String[ ] args) {
        printString(System. out:: println) ;
    }
}

Note the double colon : : notation, this is called a "method reference", and the double colon is a new syntax.

2.4 Method references

The double colon : : is a reference operator, and the expression it is in is called a method reference. If the function scheme to be expressed by Lambda already exists in the implementation of a method, the method can be referenced by double colons as a substitute for Lambda.
Semantic Analysis
For example, in the above example, there is an overloaded println(String) method in the System.out object that is exactly what we need. Then, for the functional interface parameters of the printString method, the following two ways of writing are completely equivalent:

  • Lambda expression writing: s -> System.out.println(s) ;
  • Method reference writing: System.out: : println

The first semantic means: After getting the parameter, it goes through the hands of Lambda, and then passes it to the System.out.println method for processing.

The semantics of the second equivalent writing means: directly let the println method in System.out replace Lambda. The execution effect of the two writing methods is exactly the same, and the writing method cited by 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 Lambda s are used, then according to the "Derivable is Omittable" principle, there is no need to specify the parameter type, and there is no need to specify the overloaded form - they will all be deduced automatically. And if a method reference is used, it can also be deduced according to the context.

Functional interface is the foundation of Lambda, and method reference is Lambda's twin.

The following code will call a different overload of the println method, changing the functional interface to a parameter of type int:

@FunctionalInterface
public interface PrintableInteger {
	void print(int str) ;
}

After the context changes, the only matching overload can be automatically deduced, so the method reference does not change:

public class Demo03PrintOverload {
    private static void printInteger(PrintableInteger data) {
        data. print(1024) ;
    }
    public static void main(String[ ] args) {
        printInteger(System. out:: println) ;
    }
}

This time the method reference will automatically match the overloaded form of println(int).

2.5 Referencing member methods by object name

This is the most common usage and is the same as 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() ) ;
    }
}

A functional interface is still defined as:

@FunctionalInterface
public interface Printable {
	void print(String str) ;
}

Then when you need to use this printUpperCase member method to replace the Lambda of the Printable interface, you already have an object instance of the MethodRefObject class, you can refer to the member method by the object name, the code is:

import java.awt.print.Printable;
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 Referencing static methods by class name

Since the static method abs already exists in the java.lang.Math class, when we need to call the method through Lambda, there are two ways to write it. The first is the functional interface:

@FunctionalInterface
public interface Calcable {
	int calc(int num) ;
}

The first way to write it is to use a Lambda expression:

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 write using method references is:

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 ways of writing are equivalent:

  • Lambda expression: n -> Math.abs(n)
  • Method reference: Math:: : abs

2.7 Referencing member methods through super

If there is an inheritance relationship, when a super call is required in Lambda, a method reference can also be used instead. The first is the functional interface:

@FunctionalInterface
public interface Greetable {
	void greet() ;
}

Then there is the content of the parent class Human:

public class Human {
    public void sayHello() {
        System. out. println("Hello! ") ;
    }
}

Finally, there is the content of the subclass Man, which uses the writing method of Lambda:

public class Man extends Human {
    @Override
    public void sayHello() {
        System. out. println("Hello everyone, I'm Man! ") ;
    }
    //Define method method, parameter passing Greetable interface
    public void method(Greetable g) {
        g. greet() ;
    }
    public void show() {
        //Invoke the method method, using a Lambda expression
        method(() ->{
                //Create a Human object and call the sayHello method
                new Human() . sayHello() ;
                }) ;
        //Simplify Lambda
        method(() ->new Human() . sayHello() ) ;
        //Use the super keyword instead of the parent class object
        method(() ->super. sayHello() ) ;
    }
}

But it would be better to use a method reference to call the sayHello method in a parent class, such as another subclass Woman :

public class Man extends Human {
    @Override
    public void sayHello() {
        System. out. println("Hello everyone, I'm Man! ") ;
    }
    //Define method method, parameter passing Greetable interface
    public void method(Greetable g) {
        g. greet() ;
    }
    public void show() {
        method(super:: sayHello) ;
    }
}

In this example, the following two ways of writing are equivalent:

  • Lambda expression: () -> super.sayHello()
  • Method reference: super:: : sayHello

2.8 Referencing member methods through this

this represents the current object. If the method to be referenced is a member method in the current class, the method reference can be used in the format of "this::member method". 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 functional interface Richable , so a Lambda expression is required. But 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, a 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 ways of writing are equivalent:

  • Lambda expression: () -> this.buyHouse()
  • Method reference: this: : buyHouse

2.9 Class constructor references

Since the name of the constructor is exactly the same as the class name, it is not fixed. So constructor references are represented using the format classname::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 for creating Person objects:

public interface PersonBuilder {
    Person buildPerson(String name) ;
}

To use this functional interface, you can pass a 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 via constructor reference, 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 ways of writing are equivalent:

  • Lambda expression: name -> new Person(name)
  • Method reference: Person: : new

2.10 Array constructor references

Arrays are also subclasses of Object, so they also have constructors, but with a slightly different syntax. If it corresponds to the usage scenario of Lambda, a functional interface is required:

@FunctionalInterface
public interface ArrayBuilder {
	int[ ] buildArray(int length) ;
}

When applying this interface, you can pass Lambda expressions:

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 array's constructor reference:

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 ways of writing are equivalent:

  • Lambda expression: length -> new int[length]
  • Method reference: int[ ] :: new

Tags: Java

Posted by filteredhigh on Fri, 09 Sep 2022 21:06:33 +0300