My view on lambda expressions in java is quite tangled:
One I think this way: lambda expressions reduce the reading experience of java programs. Java programs have never been outstanding in expressiveness. On the contrary, one of the factors that makes Java popular is its security and conservatism - even beginners can write robust and easy-to-maintain code as long as they pay attention to it. The lambda expressions have relatively higher requirements for developers, so they also increase some maintenance difficulties.
Another thing I think is: As a code code, it is necessary to learn and accept new features of the language. If you give up its expressive strengths just because of its poor reading experience, then some people find it difficult to understand even trinocular expressions. Language is also developing, and those who can't keep up will be left behind voluntarily.
I don't want to be left behind. However, if I had to make a choice, my decision was still relatively conservative: there is no need to use lambda in the Java language - it makes many people in the current Java circle unaccustomed to it, and will cause an increase in labor costs. If you like it very much, you can consider using scala.
Anyway, I still started to try to master Lambda, after all, some of the code maintained at work uses Lambda (trust me, I will gradually remove it). The tutorials to learn are related tutorials on the official Oracle Java website.
――――――――――
Suppose that a social network application is currently being created. One feature is that administrators can perform certain actions on members who meet the specified criteria, such as sending messages. The following table describes this use case in detail:
| Field | describe |
| name | Actions to perform |
| Key participants | administrator |
| Prerequisites | Administrator login to the system |
| Post-conditions | Only perform actions for members who meet the specified criteria |
| Main success scenario | 1. The administrator sets filtering standards for the target members to perform the operation; 2. The administrator selects the action to perform; 3. The administrator clicks the Submit button; 4. The system finds members who meet the specified criteria; 5. The system performs pre-selected operations on members who meet the specified criteria. |
| Extended | Before selecting the execution operation or before clicking the Submit button, the administrator can choose whether to preview member information that meets the filtering criteria. |
| Frequency of occurrence | It happens many times in a day. |
Use the following Person class to represent member information in social networks:
public class Person { public enum Sex { MALE, FEMALE } String name; LocalDate birthday; Sex gender; String emailAddress; public int getAge() { // ... } public void printPerson() { // ... }}Assume that all members are saved in a List<Person> instance.
In this section, we start with a very simple method, then try to implement it using local classes and anonymous classes, and at the end we will gradually experience the power and efficiency of Lambda expressions. The complete code can be found here.
Solution 1: Create methods to find members that meet the specified criteria one by one
This is the simplest and roughest solution to implement the aforementioned cases: it is to create several methods and each method verifies a criterion (such as age or gender). The following code verifies that age is older than a specified value:
public static void printPersonsOlderThan(List<person> roster, int age) { for (Person p : roster) { if (p.getAge() >= age) { p.printPerson(); } } }This is a very fragile solution, and it is very likely that the application will not run due to a little update. If we add new member variables to the Person class or change the algorithm for measuring age in the standard, we need to rewrite a lot of code to adapt to this change. Furthermore, the restrictions here are too rigid. For example, what should we do if we want to print members who are younger than a specified value? Add another new method printPersonsYoungerThan? This is obviously a stupid method.
Solution 2: Create a more general method
The following method is more adaptable than printPersonsOlderThan; this method prints member information within the specified age group:
public static void printPersonsWithinAgeRange( List<person> roster, int low, int high) { for (Person p : roster) { if (low <= p.getAge() && p.getAge() < high) { p.printPerson(); } } }Now there is a new idea: What should we do if we want to print member information of the specified gender, or that meets the specified gender and is within the specified age group? What if we adjust the Person class and add properties such as friendship and geographic location. Although writing methods like this is more universal than printPersonsYoungerThan, writing a method for every possible query can also lead to fragility in the code. It is better to put the standard check code into a new class.
Solution 3: Implement standard inspection in a local class
The following method prints member information that meets the search criteria:
public static void printPersons(List<person> roster, CheckPerson tester) { for (Person p : roster) { if (tester.test(p)) { p.printPerson(); } } }A CheckPerso object tester is used in the program to verify each instance in the List parameter roster. If tester.test() returns true, the printPerson() method will be executed. In order to set the search criteria, the CheckPerson interface needs to be implemented.
The following class implements CheckPerson and provides a specific implementation of the test method. The test method in this class filters information on membership that meets the requirements for military service in the United States: that is, male gender and age between 18 and 25 years old.
class CheckPersonEligibleForSelectiveService implements CheckPerson { public boolean test(Person p) { return p.gender == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25; } }To use this class, you need to create an instance and trigger the printPersons method:
printPersons( roster, new CheckPersonEligibleForSelectiveService());
The code now looks less fragile - we don't need to rewrite the code because of changes in the Person class structure. However, there is still additional code here: a newly defined interface that defines an internal class for each search standard in the application.
Because CheckPersonEligibleForSelectiveService implements an interface, an anonymous class can be used without defining an inner class for each standard.
Solution 4: Use anonymous classes to implement standard inspection
One parameter in the printPersons method called below is anonymous class. The function of this anonymous class is the same as that of the CheckPersonEligibleForSelectiveService class in Scheme 3: they are all filtered members with male gender and ages between 18 and 25 years old.
printPersons( roster, new CheckPerson() { public boolean test(Person p) { return p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25; } } );This scheme reduces the amount of encoding, because there is no longer a need to create new classes for each search scheme to be executed. However, it is still a bit uncomfortable to do this: although the CheckPerson interface has only one method, the anonymous class implemented is still a bit verbose and bulky. At this time, you can use Lambda expression to replace anonymous classes. The following will explain how to use Lambda expression to replace anonymous classes.
Solution 5: Use Lambda expressions to implement standard checking
The CheckPerson interface is a functional interface. The so-called functional interface refers to any interface that only contains one abstract method. (A functional interface can also have multiple default methods or static methods). Since there is only one abstract method in the functional interface, the method name of the method can be omitted when implementing the method of this functional interface. To implement this idea, you can replace anonymous class expressions with Lambda expressions. In the printPersons method rewritten below, the relevant code is highlighted:
printPersons( roster, (Person p) -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25 );
Here you can also use a standard function interface to replace the CheckPerson interface, thereby further simplifying the code.
Solution 6: Use standard functional interfaces in Lambda expressions
Let's take a look at the CheckPerson interface:
interface CheckPerson { boolean test(Person p); }This is a very simple interface. Because there is only one abstract method, it is also a functional interface. This abstract method only accepts one parameter and returns a boolean value. This abstract interface is so simple that we will consider whether it is necessary to define such an interface in the application. At this time, you can consider using standard functional interfaces defined by JDK, and you can find these interfaces under the java.util.function package.
In this example, we can use the Predicate<T> interface to replace CheckPerson. There is a boolean test(T t) method in this interface:
interface Predicate<t> { boolean test(T t); }The Predicate<T> interface is a generic interface. A generic class (or a generic interface) specifies one or more type parameters using a pair of angle brackets (<>). There is only one type parameter in this interface. When you declare or instantiate a generic class using a concrete class, you get a parameterized class. For example, the parameterized class Predicate<Person> is like this:
interface Predicate<person> { boolean test(Person t); }In this parameterized class, there is a method that is consistent with the parameters and return values of the CheckPerson.boolean test(Person p) method. Therefore, you can use the Predicate<T> interface to replace the CheckPerson interface as demonstrated in the following method:
public static void printPersonsWithPredicate( List<person> roster, Predicate<person> tester) { for (Person p : roster) { if (tester.test(p)) { p.printPerson(); } } }Then use the following code to filter members of the military service like in Plan 3:
printPersonsWithPredicate( roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25 );
Have you noticed that when using Predicate<Person> as the parameter type, no explicit parameter type is specified. This is not the only place where lambda expressions are applied. The following scheme will introduce more usage of lambda expressions.
Solution 7: Use lambda expressions throughout the application
Let’s take a look at the printPersonsWithPredicate method and consider whether you can use lambda expressions here:
public static void printPersonsWithPredicate( List<person> roster, Predicate<person> tester) { for (Person p : roster) { if (tester.test(p)) { p.printPerson(); } } }In this method, each Person instance in the roster is checked using the Predicate instance tester. If the Person instance complies with the check criteria defined in the tester, the printPerson method of the Person instance will be triggered.
In addition to triggering the printPerson method, Person instances that meet the tester standard can also execute other methods. You can consider using a lambda expression to specify the method to be executed (I think this feature is good, which solves the problem that methods in Java cannot be passed as objects). Now you need a lambda expression similar to the printPerson method - a lambda expression that only requires one parameter and returns void. Remember one thing: to use lambda expressions, you need to implement a functional interface first. In this example, a functional interface is needed, which contains only one abstract method. This abstract method has a parameter of type Person and returns to void. You can take a look at the standard functional interface Consumer<T> provided by JDK, which has an abstract method void accept(T t) just meets this requirement. In the following code, use an instance of Consumer<T> to call the accept method instead of p.printPerson():
public static void processPersons( List<person> roster, Predicate<person> tester, Consumer<person> block) { for (Person p : roster) { if (tester.test(p)) { block.accept(p); } } }Correspondingly, you can use the following code to filter members of the military service age:
processPersons( roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25, p -> p.printPerson() );
If we want to do things that are not just printing member information, but more things, such as verifying membership, getting member contact information, etc. At this point, we need a functional interface with a return value method. JDK's standard functional interface Function<T,R> has a method like this R apply(T t). The following method gets data from the parameter mapper and performs the behavior specified by the parameter block on these data:
public static void processPersonsWithFunction( List<person> roster, Predicate<person> tester, Function<person , string> mapper, Consumer<string> block) { for (Person p : roster) { if (tester.test(p)) { String data = mapper.apply(p); block.accept(data); } } }The following code obtains the email information of all members of the military service age in Roster and prints it out:
processPersonsWithFunction( roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25, p -> p.getEmailAddress(), email -> System.out.println(email) );
Solution 8: Use generics more frequently
Let’s review the processPersonsWithFunction method. The following is a generic version of this method. The new method requires more tolerance in parameter types:
public static <x , Y> void processElements( Iterable<x> source, Predicate<x> tester, Function<x , Y> mapper, Consumer<y> block) { for (X p : source) { if (tester.test(p)) { Y data = mapper.apply(p); block.accept(data); } } }To print member information for military service at the right age, you can call the processElements method like the following:
processElements( roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25, p -> p.getEmailAddress(), email -> System.out.println(email) );
During the method call process, the following behavior is performed:
Obtain object information from a collection, in this example, obtain Person object information from the collection instance roster.
Filter objects that can match the Predicate instance tester. In this example, the Predicate object is a lambda expression that specifies the conditions for filtering the military service at the right age.
The filtered object is handed over to a Function object mapper for processing, and the mapper will match a value to this object. In this example, the Function object mapper is a lambda expression that returns the email address of each member.
Specifies a behavior by the Consumer object block for the value matched by the mapper. In this example, the Consumer object is a lambda expression, which is the function of printing a string, which is the member email address returned by the Function instance mapper.
Solution 9: Use aggregation operation using lambda expression as a parameter
The following code uses aggregation operation to print the email addresses of military-age members in the roster collection:
roster.stream() .filter( p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25) .map(p -> p.getEmailAddress()) .forEach(email -> System.out.println(email));
Analyze the execution process of the above code and organize the following table:
Behavior | Aggregation operation |
Get the object | Stream<E> stream() |
Filter objects that match the specified criteria of the Predicate instance | Stream<T> filter(Predicate<? super T> predict) |
Get the matching value of the object through a Function instance | <R> Stream<R> map(Function<? super T,? extends R> mapper) |
Execute behavior specified by the Consumer instance | void forEach(Consumer<? super T> action) |
The filter, map, and forEach operations in the table are all aggregate operations. The elements processed by the aggregation operation come from the Stream, not directly from the collection (that is, because the first method called in this example program is stream()). A Stream is a data sequence. Unlike collections, Stream does not store data with a specific structure. Instead, Stream gets data from a specific source, such as from a collection, through a pipeline. pipeline is a Stream operation sequence, in this example filter-map-forEach. In addition, aggregation operations usually use lambda expressions as parameters, which also gives us a lot of custom space.