preface
There have been many articles about the basic concepts of Stream and functional interfaces, and I believe many friends have also used them in practical work experience. However, some of you may have little contact with the use of functional interfaces. In fact, we often use map, filter and other methods to use the knowledge of functional interfaces:
// The following two interfaces are derived from Java8 source code definitions: Function
/ / and Predicate
is a functional interface
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
Stream<T> filter(Predicate<? super T> predicate);
Copy the code
By using functional interfaces, you can write simpler, cleaner, better, and more elegant code.
In this article, you will learn:
-
Basic concepts of Lambda and method references.
-
Basic use of Stream.
-
How to write cleaner code through functional interfaces and generic interfaces.
You can then further your knowledge by browsing through the following articles:
-
Stream basic concepts and creation methods
-
A few tips for making code more elegant via Stream
-
Take Zoe as an example to explain the application of functional interfaces
-
Reduction, grouping, and partitioning: an in-depth tutorial on Java Stream termination
-
The ultimate trick for Java 8 Stream – the Collectors operation
Basic concept
In this part, the basic concepts and connections of functional interfaces, Lambda expressions and method references will be briefly introduced to lay a good foundation for subsequent practical use and review the basic knowledge.
Functional interface
Let’s start with a practical example (excerpted from the Function interface in the Java8 source code) :
@FunctionalInterface
public interface Function<T.R> {
R apply(T t);
}
Copy the code
In addition to all the features of interfaces, functional interfaces also have the following characteristics:
- An interface with one and only abstract methods (must).
- contains
@FunctionalInterface
Annotations (optional, it is recommended to add a requirement that allows the compiler to verify 1).
In Java, some common interfaces have been defined at the bottom:
Function interface and method name | Characteristics of the | The sample |
---|---|---|
java.lang.Runnable#run | No parameter, no return value | |
java.util.function.Supplier#get | No arguments, a return value | |
java.util.function.Consumer#accept | One argument, with no return value | |
java.util.function.Function#apply | One argument, one return value |
There are also extensions such as Predicate, BiFunction, etc. These interfaces are designed to describe the characteristics of different methods, and are used to describe m parameters and 0-1 return values. The full functional interfaces defined in Java can be searched for themselves. When our method characteristics do not exist in defined interfaces in Java or we need more explicit interface names, we need to write our own functional interfaces.
Lambda expressions
In Java, Lambda expressions can be simply understood as a form of transitive anonymous functions. Lambada expressions mainly have the following two forms:
(params) -> { statements; }
(params) -> expression
The second form can also be simplified to param -> expression if it has only one argument. The following code corresponds to each of the three forms:
(username) -> {
String msg = "Hello, " + username + ".";
System.out.println(msg);
}
(a, b) -> a + b
msg -> System.out.println(msg)
Copy the code
Method references
Method references are in the form of class/object names :: method names, such as Integer::sum, STR ::length, and String::length. Method references can be thought of as a concise definition of a Lambda expression. Here are some examples of equivalent Lambda expressions and method references:
// The first acts as a method reference, and the second acts as a method for Lambda expressions
To use method references, however, you need to ensure that the corresponding method is included, such as the sum method here
Integer:: sum;
(int a, int b) -> a + b;
// Java8 source code Integer class sum definition
public static int sum(int a, int b) {
return a + b;
}
// The second and third lines below are also examples of method references equivalent to Lambda expressions
String str = "Hello, world!";
str::length;
() -> str.length();
Java8 source code in the String class length method definition
public int length(a) {
return value.length;
}
Copy the code
In practice, there are three main forms of method references, two of which are included in the above code example. Here are three forms:
-
Class name :: Static method name
This form corresponds to the first example in the code example above, in which the format of static method arguments and return values corresponds to the format of the Lambda representation.
-
Object :: member method name
This form corresponds to the second example in the code example above, in which method references are equivalent to calling all member methods of the class using the passed object, whereas corresponding to the Lambda expression, since STR is already an object, The format of the arguments and return values of all member methods corresponds to the format of the Lambda expression.
-
The name of the class: : members method name This form is not shown in the above code sample, but the form and 2 is very similar, but due to the members of the class name and method name used here, we all know that want to call a member of the class method requires a particular instance of a class, also can saying is through class new out an object to call the method name of the class members, For the length function, if String::length is used, the equivalent Lambda expression is STR -> str.length().
Use of Lambda expressions and method references
Neither Lambda expressions nor method references can be used directly, but must be mapped to the specified functional interface type. (For example, when using Stream’s map interface, we pass Lambda expressions or method references implicitly mapped to Function
mapper extends R> mapper extends R> mapper extends R> mapper extends R> mapper extends R> mapper extends R> mapper extends R> mapper extends R> mapper extends R> mapper extends R> mapper extends R> mapper extends R> mapper extends R> mapper extends R> mapper extends R> mapper For example, Integer::sum (Lambda expression (int a, int b) -> a + b) contains two arguments and a return value, then BiFunction
can be used to receive, Specific use is as follows:
public static void main(String[] args) {
BiFunction<Integer, Integer, Integer> sumFunction = Integer::sum;
System.out.println(sumFunction.apply(1.1));
}
Copy the code
The use of the Stream
So-called Stream (flow), in fact, as a convenience to our on a set of data generated by the operation, is similar to the piping Linux operation, such as ps – ef | grep Java | cut 1-4 – c | sort – n | uniq, similarly, If we were to de-order a list of strings and concatenate them with commas, we would simply write the following code:
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("a"."c"."a"."b"));
String str = list.stream()
.distinct()
.sorted()
.collect(Collectors.joining(","));
System.out.println(str);
}
Copy the code
We can see that by using Stream, we don’t need to implement specific algorithm details, just need to declaratively tell Stream what we need to do, just like using SQL statement to query data with conditions.
This is the end of the introduction to Stream. More ways to use Stream can be found by browsing the documentation mentioned in the introduction or other summary blogs. They are covered in great detail.
Use of functional interfaces
As a matter of fact, this document has introduced in detail how to use functional interfaces to simplify our code, but the actual application scenarios of functional interfaces are not introduced. Here is an example to explain:
First suppose we have a user class:
import lombok.Data;
/** * User class **@authorZhuangzhou de Butterfly *@dateThe 2022-02-19 * /
@Data
public class User {
/** * user id */
private Long id;
/** * User name */
private String username;
/** * age */
private Integer age;
/** * Gender: 0 male 1 female */
private Integer gender;
}
Copy the code
Suppose we now need to write two methods, one to get all the adult users in the list and one to get all the male users. Normally we would write code like this (ignoring the Stream approach and assuming both age and gender have default values) :
import java.util.ArrayList;
import java.util.List;
/** * User services **@authorZhuangzhou de Butterfly *@dateThe 2022-02-19 * /
public class UserService {
/** * Filters the user list to get all adult users **@paramUserList userList *@returnList of adult users */
public List<User> getAdult(List<User> userList) {
List<User> adultList = new ArrayList<>();
for (User user : userList) {
if (user.getAge() >= 18) { adultList.add(user); }}return adultList;
}
/** * Filter the user list to get all male users **@paramUserList userList *@returnList of male users */
public List<User> getMale(List<User> userList) {
List<User> maleList = new ArrayList<>();
for (User user : userList) {
if (user.getGender() == 0) { maleList.add(user); }}returnmaleList; }}Copy the code
You can see that there are three steps in both methods:
- Initialize the list of filter results.
- Iterate through the list of users and add the qualified users to the result list.
- Returns a list of filter results.
If we needed additional dimensions to filter the user list, we would have to write a lot of repetitive code, just changing the filter criteria. If we pass the filter condition (a field of the class satisfies a condition, takes an object as an argument, and returns a Boolean value) to the method as a normal argument, we can write code like this:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
/** * User services **@authorZhuangzhou de Butterfly *@dateThe 2022-02-19 * /
public class UserService {
/** * Filters the user list to get all adult users **@paramUserList userList *@returnList of adult users */
public List<User> getAdult(List<User> userList) {
return getUserByCondition(userList, user -> user.getAge() >= 18);
}
/** * Filter the user list to get all male users **@paramUserList userList *@returnList of male users */
public List<User> getMale(List<User> userList) {
return getUserByCondition(userList, user -> user.getGender() == 0);
}
/** * Filters the list of users based on criteria **@paramUserList userList *@paramCondition conditions *@returnFilter result list */
private List<User> getUserByCondition(List<User> userList, Predicate<User> condition) {
List<User> resultList = new ArrayList<>();
for (User user : userList) {
if(condition.test(user)) { resultList.add(user); }}returnresultList; }}Copy the code
You can see that by using the Predicate
functional interface, when we write the other two methods, all we need to do is pass the query conditions to the getUserByCondition method interface, which is another convenience of the functional interface. If we find that there is a lot of repetitive logic in the code, and only part of the execution statement is different, we can consider whether that part of the execution statement can be separated out and passed through Lambada expressions or method references to avoid a lot of repetitive code.
Using the idea above, we can write a simple demo method that can filter all lists based on criteria:
/** * Filters the list based on criteria **@paramThe list list *@paramCondition conditions *@returnResult list */
public static <E> List<E> filter(List<E> list, Predicate<E> condition) {
List<E> resultList = new ArrayList<>();
for (E e : list) {
if(condition.test(e)) { resultList.add(e); }}return resultList;
}
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(Arrays.asList(1.2.3.4.5.6));
filter(list, item -> item > 3).forEach(System.out::println);
}
Copy the code
In actual development, through the combination of a generic and functional interface, we can simplify a lot of code, it can be reference to zoe, as an example explain the application of functional interface, hope to make you the simple example above the application of functional interface have a probably understanding, behind only need more practice, good at discovering optimization point in the code, You can appreciate the convenience of functional interfaces.
conclusion
In fact, before writing this article, I want to write a document that can contain all the basic knowledge of Stream, functional interface and actual use, but there are many excellent articles related to these knowledge points, and then write relatively simple, resulting in the actual content of the original preparation of a large length of introduction did not write much. Still, I hope this article gives you some new ideas for coding, and if there are any mistakes, please share them with me.
The resources
Java8 In Action