One problem with anonymous inner classes is that when the implementation of an anonymous inner class is very simple, such as when the interface has only one abstract function, the syntax of anonymous inner classes is a little clunky and unclear. We often have a practical need to pass one function as an argument to another, such as when a button is clicked, we need to set the button response function to the button object. Lambda expressions can treat functions as arguments to functions and code (functions) as data (parameters), which satisfies the above requirements. Lambda expressions provide more flexibility when implementing interfaces that have only one abstract function.
A use case using Lambda expressions
Suppose you’re building a social networking application. You now want to develop a feature that allows administrators to do various things to users, such as search, print, retrieve mail, and so on. Suppose the users of a social networking application are represented by the Person class:
public class Person {
public enum Sex {
MALE, FEMALE
}
private String name;
private LocalDate birthday;
private Sex gender;
private String emailAddress;
public int getAge(a) {
// ...
}
public void printPerson(a) {
// ...}}Copy the code
Assume that all users of a social networking application are saved in a List
We implemented the use case in a simple way, followed by an efficient and concise implementation using local classes, anonymous inner classes, and finally lambda expressions.
Method 1: Create a method that matches users based on a feature query
The simplest way to do this is to create several functions, each of which searches for specified user characteristics, such as the searchByAge() method, which prints all users older than a certain value:
public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if(p.getAge() >= age) { p.printPerson(); }}}Copy the code
This approach is potentially problematic, and the program will fail if some changes (such as new data types) are introduced. Suppose the application is updated and the Person class is changed, such as date of birth instead of age; It’s also possible that the algorithms used to search for age are different. Then you won’t have to write as many apis to accommodate these changes.
Approach 2: Create a more generic search method
This method is more generic than printPersonsOlderThan; It provides the ability to print users of a certain age range:
public static void printPersonsWithinAgeRange(
List<Person> roster, int low, int high) {
for (Person p : roster) {
if(low <= p.getAge() && p.getAge() < high) { p.printPerson(); }}}Copy the code
What if you want to print for a particular gender or for users of both genders and ages? What if you changed the Person class to add other attributes, such as relationship status or geographic location? Although this method is more general than the printPersonsOlderThan method, creating specific functions for each query can lead to less robust applications. You can use interfaces to hand off specific searches to specific classes that need to be searched (the idea of interface oriented programming – the simple factory pattern).
Method 3: Set specific search criteria in the local class
The following method can print out all the user information that matches the search criteria
public static void printPersons( List
roster, CheckPerson tester)
{
for (Person p : roster) {
if(tester.test(p)) { p.printPerson(); }}}Copy the code
This method checks whether elements in each roster list meet the search criteria by calling the Tester. test method. If Tester.test returns true, the qualified Person instance is printed.
Search is implemented by implementing the CheckPerson interface.
interface CheckPerson {
boolean test(Person p);
}
Copy the code
The following class implements the Test method of the CheckPerson interface. This will return true if the Person attribute is male and between the ages of 18 and 25
class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25; }}Copy the code
To use this class, you simply instantiate an instance and pass it to printPersons as an argument.
printPersons(roster, new CheckPersonEligibleForSelectiveService());
Copy the code
While this approach is less fragile — you don’t need to redo more methods when Person changes — you still need to add some code: create a native class for each search criterion to implement the interface. CheckPersonEligibleForSelectiveService class implements an interface, you can use an alternative local unseen inner class class, by declaring a new inner class to meet the different search.
Method 4: Specify the search criteria in the anonymous inner class
The second argument to the printPersons call is an anonymous inner class that filters users who are male and between the ages of 18 and 25:
printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25; }});Copy the code
This approach reduces a lot of code because you don’t have to create a new class for each search criterion. However, given that the CheckPerson interface has only one function, the syntax of the anonymous inner class is a bit clunky. In this case, consider using lambda expressions to replace anonymous inner classes, as described below.
Method 5: Actually search the interface through Lambda expressions
The CheckPerson interface is a functional interface. An interface with only one abstract method is a functional interface (a functional interface may also replace multiple default or static methods). Since functional interfaces contain only one abstract method, you can implement the method without its name. So you can use lambda expressions instead of anonymous inner class expressions like this:
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
Copy the code
The syntax of lambda expressions is covered in more detail later. You can also use a standard functional interface instead of the CheckPerson interface to further reduce the amount of code.
Approach 6: Use standard functional interfaces and Lambda expressions
The CheckPerson interface is a very simple interface:
interface CheckPerson {
boolean test(Person p);
}
Copy the code
It has only one abstract method, so it is a functional interface. This function takes one argument and one return value. It’s too simple to define in your application. So there are standard functional interfaces defined in the JDK, which can be found in the java.util.function package. For example, you can use Predicate
instead of CheckPerson. This interface contains only the Boolean test(T T) method.
interface Predicate<T> {
boolean test(T t);
}
Copy the code
Predicate
is a generic interface that requires one or more parameters in Angle brackets (<>). This interface replaces only one parameter, T. When you declare or instantiate a generic with a real type parameter, you get a parameterized type. For example, the parameterized type Predicate
interface Predicate<Person> {
boolean test(Person t);
}
Copy the code
The parameterized interface contains an interface, just like checkPerson.boolean test(Person P). Therefore, you can use Predicate
instead of CheckPerson as in the following code:
public static void printPersonsWithPredicate( List
roster, Predicate
tester)
{
for (Person p : roster) {
if(tester.test(p)) { p.printPerson(); }}}Copy the code
So, we can call this function like this:
printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
Copy the code
This is not the only way to use lamdba expressions. It is recommended to use lambda expressions in the following other ways.
Method 7: Use Lambda expressions in all applications
Take a look at methods printPersonsWithPredicate where you can also use the lambda expressions:
public static void printPersonsWithPredicate( List
roster, Predicate
tester)
{
for (Person p : roster) {
if(tester.test(p)) { p.printPerson(); }}}Copy the code
This method checks whether each Person instance in the roster meets Tester’s criteria. If the Person instance meets the criteria set in Tester, the information for the Person instance will be printed out.
You can specify a different action to print a Person instance that meets the search criteria defined in Tester. You can specify that the action is a lambda expression. Suppose you want a lambda representation that functions like printPerson (a single argument that returns void), you need to implement a functional interface. In this case, you need a functional interface that contains a single Person parameter and returns void. The Consumer
interface replaces a void Accept (T T) function that meets the above requirements. The following function replaces the p.printperson () call with the Consumer
public static void processPersons( List
roster, Predicate
tester, Consumer
block)
{
for (Person p : roster) {
if(tester.test(p)) { block.accept(p); }}}Copy the code
Call the processPersons function like this:
processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson()
);
Copy the code
What if you want to do more with your users’ information than just print it out? What if you wanted to verify members’ personal information or access their contacts? In this case, you need a functional interface with an abstract function that returns a value. The Function
interface contains the R apply(T T) method with one parameter and one return value. The following method takes the data matched by the argument and does the corresponding processing according to the lambda expression block:
public static void processPersonsWithFunction( List
roster, Predicate
tester, Function
mapper, Consumer
block)
,>
{
for (Person p : roster) {
if(tester.test(p)) { String data = mapper.apply(p); block.accept(data); }}}Copy the code
The following function fetches from the roster the mailbox addresses of the users that match the search criteria and prints the addresses.
processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
Copy the code
Method 8: Use generics to make it more generic
Reprocessing processPersonsWithFunction function, the function can accept below contain any set of data types:
public static <X, Y> void processElements( Iterable
source, Predicate
tester, Function
mapper, Consumer
block)
,>
{
for (X p : source) {
if(tester.test(p)) { Y data = mapper.apply(p); block.accept(data); }}}Copy the code
The above function can be called to print the email addresses of users that match the search criteria:
processElements(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
Copy the code
This method is called as long as the following action is performed:
- Gets the object from the collection, which in this case is the substitution
Person
The instanceroster
Collection.roster
Is a List and an Iterable. - Filter in accordance with
Predicate
Data typedtester
The object. In this example, the Predicate object is a lambda expression that specifies the criteria for the search. - use
Function
Type mapper maps each object that matches the filter criteria. In this case, the Function object returns the user’s email address. - Performs an in for each mapped object
Consumer
An action defined in an object block. In this case, the Consumer object is a LAMdba expression that prints the E-mail box returned by the Function object.
You can replace this with an aggregate operation.
Method 9: Merge operations using lambda expressions as arguments
The following example uses the aggregation operation to print out the email addresses of users that match the search criteria:
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));
Copy the code
The following table maps the operations performed by the processElements function and the corresponding aggregation operations
ProcessElements action | Aggregation operations |
---|---|
Obtaining object source | Stream stream() |
Filter in accordance withPredicate objectInstance of lambda expression | Stream filter(Predicate<? super T> predicate) |
useThe Function objectMaps objects that meet filtering criteria to a value | Stream map(Function<? super T,? extends R> mapper) |
performConsumer object(lambda expression) specifies the action | void forEach(Consumer<? super T> action) |
Filter,map, and forEach are aggregate operations. Aggregate operations process individual elements from the stream, not directly from the collection (which is why the first function called is stream()). Steam serializes elements. Unlike a collection, it is not a data structure that stores data. Instead, the stream loads values from the source, such as collections that pipeline data into the stream. A pipeline is a stream serialization operation, in this case filter-map-foreach. Also, aggregate operations can usually take a lambda expression as an argument, so you can customize the desired action.
Use lambda expressions in GUI programs
To handle events in a graphical user interface (GUI) application, such as keyboard input events, mouse movement events, and scrolling events, you typically implement a specific interface to create an event handler. In general, the time processing interface is a functional interface, and they usually have only one function.
The previous implementation with anonymous inner classes was timed accordingly:
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Hello World!"); }});Copy the code
You can use the following code instead:
btn.setOnAction(
event -> System.out.println("Hello World!"));Copy the code
Lambda expression syntax
A lambda expression consists of the following structure:
-
() enclose arguments. If there are multiple arguments, separate them with commas. The checkPerson. test function takes one argument, p, representing an instance of Person.
Note: You can omit the argument types in lambda expressions. Alternatively, you can omit the parentheses if you have only one argument. For example, the following lambda expression is also valid:
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
Copy the code
- Arrow symbol: ->
- Body: Consists of an expression or a declaration block. The example uses an expression like this:
p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
Copy the code
If you set an expression, the Java runtime evaluates the expression and returns the result. Also, you can use a return declaration:
p -> {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
Copy the code
Instead of returning an expression in a lambda expression, you must enclose the code block with {}. However, when a void type is returned, no parentheses are required. For example, the following is also a valid lambda expression:
email -> System.out.println(email)
Copy the code
Lambda expressions look a bit like declarative functions, and you can think of lambda expressions as anonymous functions (functions without names).
Here is an example of a lambda expression with multiple parameters:
public class Calculator {
interface IntegerMath {
int operation(int a, int b);
}
public int operateBinary(int a, int b, IntegerMath op) {
return op.operation(a, b);
}
public static void main(String... args) {
Calculator myApp = new Calculator();
IntegerMath addition = (a, b) -> a + b;
IntegerMath subtraction = (a, b) -> a - b;
System.out.println("40 + 2 =" +
myApp.operateBinary(40.2, addition));
System.out.println("20-10 =" +
myApp.operateBinary(20.10, subtraction)); }}Copy the code
The operateBinary method performs a mathematical operation on two numbers. The operation itself is an instantiation of the IntegerMath class. In the example, lambda expressions define two operations, addition and subtraction. The example output is as follows:
40 + 2 = 42
20 - 10 = 10
Copy the code
Gets a local variable in a closure
Like local and anonymous classes, lambda expressions have access to local variables; They have access to local variables. Lambda expressions are also current scoped, meaning that they do not inherit any named names from the parent scope or introduce a new level of scope. The scope of a lambda expression is the scope in which it is declared. The following example illustrates this point:
import java.util.function.Consumer;
public class LambdaScopeTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
Consumer<Integer> myConsumer = (y) ->
{
System.out.println("x = " + x);
System.out.println("y = " + y);
System.out.println("this.x = " + this.x);
System.out.println("LambdaScopeTest.this.x = " +
LambdaScopeTest.this.x); }; myConsumer.accept(x); }}public static void main(String... args) {
LambdaScopeTest st = new LambdaScopeTest();
LambdaScopeTest.FirstLevel fl = st.new FirstLevel();
fl.methodInFirstLevel(23); }}Copy the code
The following information will be printed:
x = 23
y = 23
this.x = 1
LambdaScopeTest.this.x = 0
Copy the code
If you use x instead of y in the lambda expression myConsumer as follows, the compilation will fail.
Consumer<Integer> myConsumer = (x) -> {
}
Copy the code
“Variable x is already defined in method methodInFirstLevel(int)” because lambda expressions do not introduce new scopes. As a result, you can directly access member variables, functions, and local variables in the closure in which the lambda expression resides. For example, lambda expressions can directly access the argument X to the method methodInFirstLevel. Class-level scopes can be accessed using the this keyword. In this example, this.x is the value of the member variable firstlevel. x.
However, like local and anonymous classes, lambda expression values can access local variables and parameters that are qualified as final or precisely final. For example, suppose we add the following definition declaration to methodInFirstLevel:
Effectively Final: A variable or parameter whose value does not change after initialization is of the same type.
void methodInFirstLevel(int x) {
x = 99;
}
Copy the code
MethodInFirstLevel parameter x is no longer effectively final due to the declaration of x =99. As a result, the Java compiler will report an error like “Local variables referenced from a lambda expression must be final or effectively final”.
The target type
How does Java determine the data type of a lambda expression at run time? Take another look at the lambda expression that selects males between the ages of 18 and 25:
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
Copy the code
This lambda expression is passed as an argument to the following two functions:
- public static void printPersons(List roster, CheckPerson tester)
- public void printPersonsWithPredicate(List roster, Predicate tester)
When the Java runtime calls the method printPersons, it expects data of type CheckPerson, so lambda expressions are of that type. Method is called when the Java runtime printPersonsWithPredicate, it expects a Predicate < Person > type of data, so the lambda expressions is one such types. The data types these methods expect are called target types. To determine the type of a lambda expression, the Java compiler determines its target type in the context of the lambda expression. Lambda expressions can only be executed if the Java compiler can predict the target type.
Target type and function parameters
For function parameters, the Java compiler can determine the target type through two other language features: overload resolution and type parameter inference. Look at the following two functional interface (Java. Lang. Runnable and Java. Util. Concurrent. Callable) :
public interface Runnable {
void run(a);
}
public interface Callable<V> {
V call(a);
}
Copy the code
The runnable. run method does not return any value, but Callable
. Call does. Suppose you override the invoke method as follows:
void invoke(Runnable r) {
r.run();
}
<T> T invoke(Callable<T> c) {
return c.call();
}
Copy the code
Which method will be called to execute the following program?
String s = invoke(() -> "done");
Copy the code
The invoke(Callable
) method is called because it returns a value; The invoke(Runnable) method returns no value. In this case, the lambda expression () -> “done” is of type Callable
.
The last
Thank you for reading. If you are interested, you can follow the wechat public account to get the latest pushed articles.