Chapter 2. Adopting Lambda Expressions

In this chapter, you’ll learn how to adopt lambda expressions, the flagship feature of Java 8. First, you’ll learn about a pattern called behavior parameterization, which lets you write code that can cope with requirement changes. Then, you’ll see how lambda expressions let you use this pattern in a more concise way than what was possible before Java 8. Next, you’ll learn precisely where and how to use lambda expressions. You’ll also learn about method references, another Java 8 feature that lets you write code that is more succinct and descriptive. You’ll then bring all this new knowledge together into a practical refactoring example. Finally, you’ll also learn how to test using lambda expressions and method references.

Why Lambda Expressions?

The motivation for introducing lambda expressions into Java is related to a pattern called behavior parameterization. This pattern lets you cope with requirement changes by letting you write more flexible code. Prior to Java 8, this pattern was very verbose. Lambda expressions fix that by letting you utilize the behavior parameterization pattern in a concise way. Here’s an example: say you need to find invoices greater than a certain amount. You could create a method findInvoicesGreaterThanAmount:

List<Invoice> findInvoicesGreaterThanAmount(List<Invoice> invoices, double amount) {
    List<Invoice> result = new ArrayList<>();
    for(Invoice inv: invoices) {
        if(inv.getAmount() > amount) {
            result.add(inv);
        }
    }
    return result;
}

Using this method is simple enough. However, what if you need to also find invoices smaller than a certain amount? Or worse, what if you need to find invoices from a given customer and also of a certain amount? Or, what if you need to query many different properties on the invoice? You need a way to parameterize the behavior of filter with some form of condition. Let’s represent this condition by defining InvoicePredicate interface and refactoring the method to make use of it:

interface InvoicePredicate {
    boolean test(invoice inv);
}

List<Invoice> findInvoices(List<Invoice> invoices, InvoicePredicate p) {
    List<Invoice> result = new ArrayList<>();
    for(Invoice inv: invoices) {
        if(p.test(inv)) {
            result.add(inv);
        }
    }
    return result;
}

With this useful code, you can cope with any requirement changes involving any property of an Invoice object. You just need to create different InvoicePredicate objects and pass them to the findInvoices method. In other words, you have parameterized the behavior of findInvoices. Unfortunately, using this new method introduces additional verbosity, as shown here:

List<Invoice> expensiveInvoicesFromOracle
    = findInvoices(invoices, new InvoicePredicate() {
        public test(Invoice inv) {
            return inv.getAmount() > 10_000
                   && inv.getCustomer() == Customer.ORACLE;
        }
    });

In other words, you have more flexibility but less readability. Ideally, you want both flexibility and conciseness, and that’s where lambda expressions come in. Using this feature, you can refactor the preceding code as follows:

List<Invoice> expensiveInvoicesFromOracle
    = findInvoices(invoices, inv ->
                                inv.getAmount() > 10_000
                                && inv.getCustomer() ==
                                  Customer.ORACLE);

Lambda Expressions Defined

Now that you know why you need need lambda expressions, it’s time to learn more precisely what they are. In the simplest terms, a lambda expression is an anonymous function that can be passed around. Let’s take a look at this definition in greater detail:

Anonymous

A lambda expression is anonymous because it does not have an explicit name as a method normally would. It’s sort of like an anonymous class in that it does not have a declared name.

Function

A lambda is like a method in that it has a list of parameters, a body, a return type, and a possible list of exceptions that can be thrown. However, unlike a method, it’s not declared as part of a particular class.

Passed around

A lambda expression can be passed as an argument to a method, stored in a variable, and also returned as a result.

Lambda Expression Syntax

Before you can write your own lambda expressions, you need to know the syntax. You have seen a couple of lambda expressions in this guide already:

Runnable r = () -> System.out.println("Hi");
FileFilter isXml = (File f) -> f.getName().endsWith(".xml");

These two lambda expressions have three parts:

  • A list of parameters, e.g. (File f)

  • An arrow composed of the two characters - and >

  • A body, e.g. f.getName().endsWith(".xml")

There are two forms of lambda expressions. You use the first form when the body of the lambda expression is a single expression:

(parameters) -> expression

You use the second form when the body of the lambda expression contains one or multiple statements. Note that you have to use curly braces surrounding the body of the lambda expression:

(parameters) -> { statements;}

Generally, one can omit the type declarations from the lambda parameters if they can be inferred. In addition, one can omit the parentheses if there is a single parameter.

Where to Use Lambda Expressions

Now that you know how to write lambda expressions, the next question to consider is how and where to use them. In a nutshell, you can use a lambda expression in the context of a functional interface. A functional interface is one with a single abstract method. Take, for example, the two lambda expressions from the preceding code:

Runnable r = () -> System.out.println("Hi");
FileFilter isXml = (File f) -> f.getName().endsWith(".xml");

Runnable is a functional interface because it defines a single abstract method called run. It turns out FileFilter is also a functional interface because it defines a single abstract method, called accept:

@FunctionalInterface
public interface Runnable {
    void run();
}

@FunctionalInterface
public interface FileFilter {
    boolean accept(File pathname);
}

The important point here is that lambda expressions let you create an instance of a functional interface. The body of the lambda expression provides the implementation for the single abstract method of the functional interface. As a result, the following uses of Runnable via anonymous classes and lambda expressions will produce the same output:

Runnable r1 = new Runnable() {
    public void run() {
        System.out.println("Hi!");
    }
};
r1.run();

Runnable r2 = () -> System.out.println("Hi!");
r2.run();
Note

You’ll often see the annotation @FunctionalInterface on interfaces. It’s similar to using the @Override annotation to indicate that a method is overridden. Here, the @FunctionalInterface annotation is used for documentation to indicate that the interface is intended to be a functional interface. The compiler will also report an error if the interface annotated doesn’t match the definition of a functional interface.

You’ll find several new functional interfaces such as Function<T, R> and Supplier<T> in the package java.util.function, which you can use for various forms of lambda expressions.

Method References

Method references let you reuse existing method definitions and pass them around just like lambda expressions. They are useful in certain cases to write code that can feel more natural and readable compared to lambda expressions. For example, you can find hidden files using a lambda expression as follows:

File[] hiddenFiles = mainDirectory.listFiles(f -> f.isHidden());

Using a method reference, you can directly refer to the method isHidden using the double colon syntax (::).

File[] hiddenFiles = mainDirectory.listFiles(File::isHidden);

The most simple way to think of a method reference is as a shorthand notation for lambda expressions calling for a specific method. There are four main kinds of method references:

  • A method reference to a static method:

    Function<String, Integer> converter = Integer::parseInt;
    Integer number = converter.apply("10");
  • A method reference to an instance method. Specifically, you’re referring to a method of an object that will be supplied as the first parameter of the lambda:

    Function<Invoice, Integer> invoiceToId = Invoice::getId;
  • A method reference to an instance method of an existing object:

    Consumer<Object> print = System.out::println;

    Specifically, this kind of method reference is very useful when you want to refer to a private helper method and inject it into another method:

    File[] hidden = mainDirectory.listFiles(this::isXML);
    
    private boolean isXML(File f) {
        return f.getName.endsWith(".xml");
    }
  • A constructor reference:

    Supplier<List<String>> listOfString = List::new;

Putting It All Together

At the start of this chapter, you saw this verbose example of Java code for sorting invoices:

Collections.sort(invoices, new Comparator<Invoice>() {
    public int compare(Invoice inv1, Invoice inv2) {
    return Double.compare(inv2.getAmount(), inv1.getAmount());
   }
});

Now you’ll see exactly how to use the Java 8 features you’ve learned so far to refactor this code so it’s more readable and concise.

First, notice that Comparator is a functional interface because it only declares a single abstract method called compare, which takes two objects of the same type and returns an integer. This is an ideal situation for a lambda expression, like this one:

Collections.sort(invoices,
                 (Invoice inv1, Invoice inv2) -> {
                     return Double.compare(inv2.getAmount(), inv1.getAmount());
});

Since the body of the lambda expression is simply returning the value of an expression, you can use the more concise form of lambda expression:

Collections.sort(invoices,
                 (Invoice inv1, Invoice inv2)
                     -> Double.compare(inv2.getAmount(), inv1.getAmount()));

In Java 8, the List interface supports the sort method, so you can use that instead of Collections.sort:

invoices.sort((Invoice inv1, Invoice inv2)
                  -> Double.compare(inv2.getAmount(), inv1.getAmount()));

Next, Java 8 introduces a static helper, Comparator.comparing, which takes as argument a lambda to extract a comparable key. It then generates a Comparator object for you. You can use it as follows:

Comparator<Invoice> byAmount
    = Comparator.comparing((Invoice inv) -> inv.getAmount());

invoices.sort(byAmount);

You may notice that the more concise method reference Invoice::getAmount can simply replace the lambda (Invoice inv) -> inv.getAmount():

Comparator<Invoice> byAmount
  = Comparator.comparing(Invoice::getAmount);
invoices.sort(byAmount);

Since the method getAmount returns a primitive double, you can use Comparator.comparingDouble, which is a primitive specialized version of Comparator.comparing, to avoid unnecessary boxing:

Comparator<Invoice> byAmount
  = Comparator.comparingDouble(Invoice::getAmount);
invoices.sort(byAmount);

Finally, let’s tidy up the code and use an import static and also get rid of the local variable holding the Comparator object to produce a solution that reads like the problem statement:

import static java.util.Comparator.comparingDouble;
invoices.sort(comparingDouble(Invoice::getAmount));

Testing with Lambda Expressions

You may be concerned with how lambda expressions are going to affect testing. After all, lambda expressions introduce behaviors that need to be tested. When deciding how to test code that contains lambda expressions, consider the following two options:

  • If the lambda expression is small, test the behavior of the surrounding code that uses it.

  • If the lambda expression is reasonably complex, extract it into a separate method reference that you can inject and test in isolation.

Summary

Here are the key concepts from this chapter:

  • A lambda expression can be understood as a kind of anonymous function.

  • Lambda expressions and the behavior parameterization pattern let you write code that is both flexible and concise.

  • A functional interface is an interface that declares a single abstract method.

  • Lambda expressions can only be used in the context of a functional interface.

  • Method references can be a more natural alternative to lambda expressions when you need to reuse an existing method and pass it around.

  • In the context of testing, extract large lambda expressions into separate methods that you can then inject using method references.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.106.33