Data Structures and Collections

Một phần của tài liệu Java 8 recipes, 2nd edition (Trang 138 - 158)

Lambda Expressions

There are very few means by which a new feature in an existing language can have a significant impact on the ecosystem. Lambda expressions for the Java language are one such significant new feature that will have an effect on many facets of the ecosystem. Simply defined, lambda expressions are a convenient way to create anonymous functions. They provide an easy way to create a single method interface using an expression or series of statements.

Lambda expressions are built upon functional interfaces, which are interfaces that contain a single abstract method.

They can be applied in many different contexts, ranging from simple anonymous functions to sorting and filtering Collections. Moreover, lambda expressions can be assigned to variables and then passed into other objects.

In this chapter, you will learn how to create lambda expressions, and you’ll see many examples of how they can be applied in common scenarios. You’ll also learn how to generate the building blocks for lambda expressions, so that you can construct applications to facilitate them. The chapter will delve into the java.util.function package, which contains a bevy of useful functional interfaces that lambdas can implement. Lastly, you will see how to simplify certain types of lambda expressions into method references for a more concise approach.

After reading this chapter, you to will be able to see the impact that lambda expressions have on the Java language. They modernize the language by making developers more productive, and opening new possibilities in many areas. Lambda expressions turn the page on Java, bringing the language into a new light, with the likes of other languages that have had similar constructs for some time. Those languages helped to pave the way for lambda expressions in the Java language, and there is no doubt that lambda expressions will pave the way for many elegant solutions.

6-1. Writing a Simple Lambda Expression

Problem

You want to encapsulate a piece of functionality that prints out a simple message.

Solution

Write a lambda expression that accepts a single parameter that contains the message you want to print, and

implement the printing functionality within the lambda. In the following example, a functional interface, HelloType, is implemented via a lambda expression and assigned to the variable helloLambda. Lastly, the lambda is invoked, printing the message.

CHAPTER 6 N LAMBDA EXPRESSIONS

public class HelloLambda {

/**

* Functional Interface */

public interface HelloType { /**

* Function that will be implemented within the lambda * @param text

*/

void hello(String text);

}

public static void main(String[] args){

// Create the lambda, passing a parameter named "text" to the // hello() method, returning the string. The lambda is assigned // to the helloLambda variable.

HelloType helloLambda =

(String text) -> {System.out.println("Hello " + text);};

// Invoke the method call helloLambda.hello("Lambda");

} }

Results:

Hello Lambda

How It Works

A lambda expression is an anonymous block of code that encapsulates an expression or a series of statements and returns a result. Lambda expressions are also known as closures in some other languages. They can accept zero or more parameters, any of which can be passed with or without type specification since the type can be automatically derived from the context. While it is possible for code written in the Java language to move forward without the use of lambda expressions, they are an important addition that greatly improves overall maintainability, readability, and developer productivity. Lambda expressions are an evolutionary change to the Java language, as they are another step toward modernization of the language, and help keep it in sync with other languages.

The syntax of a lambda expression includes an argument list, a new character to the language known as the

“arrow token” (->), and a body. The following model represents the structure of a lambda expression:

(argument list) -> { body }

The argument list for a lambda expression can include zero or more arguments. If there are no arguments, then an empty set of parentheses can be used. If there is only one argument, then no parentheses are required. Each argument on the list can include an optional type specification. If the type of the argument is left off, then the type is derived from the current context.

In the solution for this recipe, curly braces surround the body of a block, which contains more than a single expression. The curly braces are not necessary if the body consists of a single expression. The curly braces in

CHAPTER 6 N LAMBDA EXPRESSIONS and then returned. If the body of the lambda is an expression and not a statement, a return is implicit. On the contrary, if the body includes more than one statement, a return must be specified, and it marks return of control back to the caller.

The following code demonstrates a lambda expression that does not contain any arguments:

StringReturn msg = () -> "This is a test";

Let’s take a look at how this lambda expression works. In the previous listing, an object of type StringReturn is returned from the lambda expression. The empty set of parentheses denotes that there are no arguments being passed to the expression. The return is implicit, and the string "This is a test" is returned from the lambda expression to the invoker. The expression in the example is assigned to a variable identified by msg. Assume that the functional interface, StringReturn, contains an abstract method identified as returnMessage(). In this case, the msg.returnMessage() method can be invoked to return the string.

The body of a lambda expression can contain any Java construct that an ordinary method may contain. For instance, suppose a string were passed as an argument to a lambda expression, and you wanted to return some value that is dependent upon the string argument. The following lambda expression body contains a block of code, which returns an int, based upon the string value of the argument passed into the expression.

ActionCode code = (codestr) -> { switch(codestr){

case "ACTIVE": return 0;

case "INACTIVE": return 1;

default:

return -1;

} };

In this example, the ActionCode functional interface is used to infer the return type of the lambda expression.

For clarification, let’s see what the interface looks like.

interface ActionCode{

int returnCode(String codestr);

}

The code implies that the lambda expression implements the returnCode method, which is defined within the ActionCode interface. This method accepts a string argument (codestr), which is passed to the lambda expression, returning an int. Therefore, from this example you can see that a lambda can encapsulate the functionality of a method body.

Note

N A lambda expression can contain any statement that an ordinary Java method contains. However, the continue and break keywords are illegal at the top level.

6-2. Enabling the Use of Lambda Expressions

Problem

You are interested in authoring code that enables the use of lambda expressions.

CHAPTER 6 N LAMBDA EXPRESSIONS

Solution 1

Write custom functional interfaces that can be implemented via lambda expressions. All lambda expressions implement a functional interface, a.k.a. an interface with one abstract method definition. The following lines of code demonstrate a functional interface that contains one method definition.

interface ReverseType {

String reverse(String text);

}

The functional interface contains a single abstract method definition, identified as String reverse(String text).

The following code, which contains a lambda expression, demonstrates how to implement ReverseType.

ReverseType newText = (testText) -> { String tempStr = "";

for (String part : testText.split(" ")) {

tempStr = new StringBuilder(part).reverse().toString();

}

return tempStr;

};

The following code could be used to invoke the lambda expression:

System.out.println(newText.reverse("HELLO"));

Result:

OLLEH

Solution 2

Use a functional interface that is contained within the java.util.function package to implement a lambda expression to suit the needs of the application. The following example uses the Function<T,R> interface to perform the same task as the one contained in solution 1. This example accepts a string argument and returns a string result.

Function<String,String> newText2 = (testText) -> { String tempStr = "";

for (String part : testText.split(" ")) {

tempStr = new StringBuilder(part).reverse().toString();

}

return tempStr;

};

This lambda expression is assigned to the variable newText2, which is of type Function<String,String>.

Therefore, a string is passed as an argument, and a string is to be returned from the lambda expression. The functional interface of Function<T,R> contains an abstract method declaration of apply(). To invoke this lambda expression, use the following syntax:

System.out.println(newText2.apply("WORLD"));

Result:

CHAPTER 6 N LAMBDA EXPRESSIONS

How It Works

A basic building block of a lambda expression is the functional interface. A functional interface is a standard Java interface that contains only one abstract method declaration and provides a target type for lambda expressions and method references. A functional interface may contain default method implementations as well, but only one abstract declaration. The abstract method is then implicitly implemented by the lambda expression. As a result, the lambda expression can be assigned to a variable of the same type as the functional interface. The method can be called upon from the assigned variable at a later time, thus invoking the lambda expression. Following this pattern, lambda expressions are method implementations that can be invoked by name. They can also be passed as arguments to other methods (see Recipe 6-8).

Note

N The functional interface in solution 1 contains the @FunctionalInterface annotation. This can be placed on a functional interface to catch compiler-level errors, but it has no effect on the interface itself.

At this point you may be wondering if you will be required to develop a functional interface for each situation that may be suitable for use with a lambda expression. This is not the case, as there are many functional interfaces already in existence. Some examples include java.lang.Runnable, javafx.event.EventHandler, and java.util.

Comparator. See some of the other recipes in this chapter for examples using lambda expressions that implement these interfaces. However, there are also many more functional interfaces that are less specific, enabling them to be tailored to suit the needs of a particular requirement. The java.util.function package contains a number of functional interfaces that can be useful when implementing lambda expressions. The functional interfaces contained within the package are utilized throughout the JDK, and they can also be utilized in developer applications. Table 6-1 lists the functional interfaces that are contained within the java.util.function package, along with a description of each. Note that a Predicate test that returns a Boolean value.

Table 6-1. Functional Interfaces Contained in java.util.function Interface Implementation Description

BiConsumer<T,U> Function operation that accepts two input arguments and returns no result.

BiFunction<T,U,R> Function that accepts two arguments and produces a result.

BinaryOperator<T> Function operation upon two operands of the same type, producing a result of the same type as the operands.

BiPredicate<T,U> Predicate of two arguments. Returns a Boolean value.

BooleanSupplier Supplier of Boolean-valued results.

Consumer<T> Function operation that accepts a single input argument and returns no result.

DoubleBinaryOperator Function operation upon two double-valued operands and producing a double-valued result.

DoubleConsumer Function operation that accepts a single double-valued argument and returns no result.

DoubleFunction<R> Function that accepts a double-valued argument and produces a result.

DoublePredicate Predicate of one double-valued argument.

DoubleSupplier Supplier of double-valued results.

DoubleToIntFunction Function that accepts a double-valued argument and produces an int-valued result.

CHAPTER 6 N LAMBDA EXPRESSIONS

Table 6-1. (continued)

Interface Implementation Description

DoubleToLongFunction Function that accepts a double-valued argument and produces a long-valued result.

DoubleUnaryOperator Function operation on a single double-valued operand that produces a double-valued result.

Function<T,R> Function that accepts one argument and produces a result.

IntBinaryOperator Function operation upon two int-valued operands and producing an int-valued result.

IntConsumer Function operation that accepts a single int-valued argument and returns no result.

IntFunction<R> Function that accepts an int-valued argument and produces a result.

IntPredicate Predicate of one int-valued argument.

IntSupplier Supplier of int-valued results.

IntToDoubleFunction Function that accepts an int-valued argument and produces a double-valued result.

IntToLongFunction Function that accepts an int-valued argument and produces a long-valued result.

IntUnaryOperator Function operation on a single int-valued operand that produces an int-valued result.

LongBinaryOperator Function operation upon two long-valued operands and producing a long-valued result.

LongConsumer Function operation that accepts a single long-valued argument and returns no result.

LongFunction<R> Function that accepts a long-valued argument and produces a result.

LongPredicate Predicate of one long-valued argument.

LongSupplier Supplier of long-valued results.

LongToDoubleFunction Function that accepts a long-valued argument and produces a double-valued result.

LongToIntFunction Function that accepts a long-valued argument and produces an int-valued result.

LongUnaryOperator Function operation on a single long-valued operand that produces a long-valued result.

ObjDoubleConsumer<T> Function operation that accepts an object-valued and a double-valued argument and returns no result.

ObjIntConsumer<T> Function operation that accepts an object-valued and an int-valued argument and returns no result.

ObjLongConsumer<T> Function operation that accepts an object-valued and a long-valued argument and returns no result.

Predicate<T> Predicate of one argument.

Supplier<T> Supplier of results.

ToDoubleBiFunction<T,U> Function that accepts two arguments and produces a double-valued result.

ToDoubleFunction<T> Function that produces a double-valued result.

ToIntBiFunction<T,U> Function that accepts two arguments and produces an int-valued result.

ToIntFunction<T> Function that produces an int-valued result.

ToLongBiFunction<T,U> Function that accepts two arguments and produces a long-valued result.

ToLongFunction<T> Function that produces a long-valued result.

CHAPTER 6 N LAMBDA EXPRESSIONS Utilizing functional interfaces contained within the java.util.function package can greatly reduce the amount of code you need to write. Not only are the functional interfaces geared toward tasks that are performed a high percentage of the time, but they are also written using generics, allowing them to be applied in many different contexts. Solution 2 demonstrates such an example, whereby the Function<T,R> interface is used to implement a lambda expression that accepts a string argument and returns a string result.

6-3. Sorting with Fewer Lines of Code

Problem

Your application contains a list of Player objects for a hockey team. You would like to sort that list of Players by those who scored the most goals, and you would like to do so using terse, yet easy-to-follow code.

Note

N The solutions in this recipe utilize Collections and sorting. To learn more about Collections, refer to Chapter 7.

Solution 1

Create a Comparator using an accessor method contained within the Player object for the field by which you want to sort. In this case, you want to sort by number of goals, so the Comparator should be based upon the value returned from getGoals(). The following line of code shows how to create such a Comparator using the Comparator interface and a method reference.

Comparator<Player> byGoals = Comparator.comparing(Player::getGoals);

Next, utilize a mixture of lambda expressions and streams (See Chapter 7 for full details on streams), along with the forEach() method, to apply the specified sort on the list of Player objects. In the following line of code, a stream is obtained from the list, which allows you to apply functional-style operations on the elements.

team.stream().sorted(byGoals)

.map(p -> p.getFirstName() + " " + p.getLastName() + " - "

+ p.getGoals())

.forEach(element -> System.out.println(element));

Assuming that the List referenced by team is loaded with Player objects, the previous line of code will first sort that list by the Player goals, and then print out information on each object.

Results from the sort:

== Sort by Number of Goals ==

Jonathan Gennick - 1 Josh Juneau - 5 Steve Adams - 7 Duke Java - 15 Bob Smith - 18

Solution 2

Utilize the Collections.sort() method, passing the list to sort along with a lambda expression that performs

CHAPTER 6 N LAMBDA EXPRESSIONS

Collections.sort(team, (p1, p2)

-> p1.getLastName().compareTo(p2.getLastName()));

team.stream().forEach((p) -> {

System.out.println(p.getLastName());

});

Result:

== Sort by Last Name ==

Adams Gennick Java Juneau Smith

Note

N This solution could be further simplified if the Player class included a comparison method. If this were the case, a method reference could be used, rather than implementing a lambda expression. For more information regarding method references, see Recipe 6-9.

How It Works

Java 8 introduces some new features that greatly increase developer productivity for sorting collections. Three new features are demonstrated in the solution to this recipe: lambda expressions, method references, and streams. We will look into streams and method references in more detail within other recipes in this book, but we also briefly describe them here to enable the understanding of this recipe. Streams can be applied to collections of data, and they allow enhanced functional-style operations to be applied to the elements within the collections. Streams do not store any data; rather, they enable more functionality on the collections from which they are obtained.

In solution 1, a Comparator is generated, by which the Player objects will be evaluated for the number of goals scored (getGoals). A stream is then generated from a List<Player> that is referenced as team. The stream provides the sorted() function, which accepts a Comparator by which to perform a sort on a stream of data. The Comparator that was initially generated is passed to the sorted() function, and then the map() function is called upon the result.

The map() function provides the ability to map expressions to each element within the stream. Therefore, within the map, this solution utilizes a lambda expression to create a string that contains each Player object’s firstName, lastName, and goals fields. Lastly, since the List<Player> is an iterable, it contains the forEach() method. The forEach() method enables an expression or group of statements to be applied to each element within the list. In this case, each element in the list is printed to the command line. As such, since the map() function was applied to the stream, each element in the list is subsequently printed per the algorithm applied within the map(). Therefore, the result is that the players first and last names along with the number of goals each has scored will be printed at the command line.

Solution 2 uses a different technique to accomplish a similar task. In the second solution, the Collections.sort() method is invoked on the list. The first argument to Collections.sort() is the list itself, and the second

argument is the comparison implementation in the form of a lambda expression. The lambda expression in this case has two parameters passed to it, both Player objects, and it compares the lastName of the first player to the lastName of the second player. Therefore, the sort will be performed on the lastName field of the Player object, in ascending order. To finish off solution 2, the sorted list is printed out. To do this a stream is generated from the sorted list, and the

Một phần của tài liệu Java 8 recipes, 2nd edition (Trang 138 - 158)

Tải bản đầy đủ (PDF)

(627 trang)