Channels ▼
RSS

Parallel

Lambda Expressions in Java 8


Variable Scope

Often, you want to be able to access variables from an enclosing method or class in a lambda expression. Consider this example:

public static void repeatMessage(String text, int count) {
     Runnable r = () -> {
        for (int i = 0; i < count; i++) {
           System.out.println(text);
           Thread.yield();
        }
     };
     new Thread(r).start();
  }

Consider a call:

repeatMessage("Hello", 1000); // Prints Hello 1,000 times in a separate thread

Now look at the variables count and text inside the lambda expression. Note that these variables are not defined in the lambda expression. Instead, these are parameter variables of the repeatMessage method.

If you think about it, something not obvious is going on here. The code of the lambda expression may run long after the call to repeatMessagehas returned and the parameter variables are gone. How do the text and count variables stay around?

To understand what is happening, we need to refine our understanding of a lambda expression. A lambda expression has three ingredients:

  1. A block of code
  2. Parameters
  3. Values for the free variables; that is, the variables that are not parameters and not defined inside the code

In our example, the lambda expression has two free variables, text and count. The data structure representing the lambda expression must store the values for these variables, in our case, "Hello" and 1000. We say that these values have been captured by the lambda expression. (It's an implementation detail how that is done. For example, one can translate a lambda expression into an object with a single method, so that the values of the free variables are copied into instance variables of that object.)

The technical term for a block of code together with the values of the free variables is a closure. If someone gloats that their language has closures, rest assured that Java has them as well. In Java, lambda expressions are closures. In fact, inner classes have been closures all along. Java 8 gives us closures with an attractive syntax.

As you have seen, a lambda expression can capture the value of a variable in the enclosing scope. In Java, to ensure that the captured value is well defined, there is an important restriction. In a lambda expression, you can only reference variables whose value doesn't change. For example, the following is illegal:

public static void repeatMessage(String text, int count) {
     Runnable r = () -> {
        while (count > 0) {
           count--; // Error: Can't mutate captured variable
           System.out.println(text);
           Thread.yield();
        }
     };
     new Thread(r).start();
  }

There is a reason for this restriction. Mutating variables in a lambda expression is not thread-safe. Consider a sequence of concurrent tasks, each updating a shared counter.

int matches = 0;
  for (Path p : files)
     new Thread(() -> { if (p has some property) matches++; }).start();
        // Illegal to mutate matches

If this code were legal, it would be very, very bad. The increment matches++ is not atomic, and there is no way of knowing what would happen if multiple threads execute that increment concurrently.

Inner classes can also capture values from an enclosing scope. Before Java 8, inner classes were allowed to access only final local variables. This rule has now been relaxed to match that for lambda expressions. An inner class can access any effectively final local variable; that is, any variable whose value does not change.

Don't count on the compiler to catch all concurrent access errors. The prohibition against mutation holds only for local variables. If matchesis an instance or static variable of an enclosing class, then no error is reported, even though the result is just as undefined.

Also, it's perfectly legal to mutate a shared object, even though it is unsound. For example,

List<Path> matches = new ArrayList<>();
  for (Path p : files)
     new Thread(() -> { if (p has some property) matches.add(p); }).start();
        // Legal to mutate matches, but unsafe

Note that the variable matches is effectively final. (An effectively final variable is a variable that is never assigned a new value after it has been initialized.) In our case, matches always refers to the same ArrayList object. However, the object is mutated, and that is not thread-safe. If multiple threads call add, the result is unpredictable.

There are safe mechanisms for counting and collecting values concurrently. You may want to use streams to collect values with certain properties. In other situations, you may want to use thread-safe counters and collections.

As with inner classes, there is an escape hatch that lets a lambda expression update a counter in an enclosing local scope. Use an array of length 1, like this:

  int[] counter = new int[1];
    button.setOnAction(event -> counter[0]++);

Of course, code like this is not thread-safe. For a button callback, that doesn't matter, but in general, you should think twice before using this trick.

The body of a lambda expression has the same scope as a nested block. The same rules for name conflicts and shadowing apply. It is illegal to declare a parameter or a local variable in the lambda that has the same name as a local variable.

Path first = Paths.get("/usr/bin");
  Comparator<String> comp =
     (first, second) -> Integer.compare(first.length(), second.length());
     // Error: Variable first already defined

Inside a method, you can't have two local variables with the same name. Therefore, you can't introduce such variables in a lambda expression either. When you use the this keyword in a lambda expression, you refer to the this parameter of the method that creates the lambda. For example, consider

public class Application() {
     public void doWork() {
        Runnable runner = () -> { ...; System.out.println(this.toString()); ... };
        ...
     }
  }

The expression this.toString() calls the toString method of the Application object, not the Runnable instance. There is nothing special about the use of this in a lambda expression. The scope of the lambda expression is nested inside the doWork method, and this has the same meaning anywhere in that method.

Default Methods

Many programming languages integrate function expressions with their collections library. This often leads to code that is shorter and easier to understand than the loop equivalent. For example, consider a loop:

for (int i = 0; i < list.size(); i++)
     System.out.println(list.get(i));

There is a better way. The library designers can supply a forEach method that applies a function to each element. Then you can simply call

list.forEach(System.out::println);

That's fine if the collections library has been designed from the ground up. But the Java collections library was designed many years ago, and there is a problem. If the Collection interface gets new methods, such as forEach, then every program that defines its own class implementing Collection will break until it, too, implements that method. That is simply unacceptable in Java.

The Java designers decided to solve this problem once and for all by allowing interface methods with concrete implementations (called default methods). Those methods can be safely added to existing interfaces. In this section, we'll look at default methods in detail. In Java 8, the forEach method has been added to the Iterable interface, a superinterface of Collection, using the mechanism that I will describe here.

Consider this interface:

interface Person {
     long getId();
     default String getName() { return "John Q. Public"; }
  }

The interface has two methods: getId, which is an abstract method, and the default method getName. A concrete class that implements the Person interface must, of course, provide an implementation of getId, but it can choose to keep the implementation of getName or to override it.

Default methods put an end to the classic pattern of providing an interface and an abstract class that implements most or all of its methods, such as Collection/AbstractCollection or WindowListener/WindowAdapter. Now, you can just implement the methods in the interface.

What happens if the exact same method is defined as a default method in one interface and then again as a method of a superclass or another interface? Languages such as Scala and C++ have complex rules for resolving such ambiguities. Fortunately, the rules in Java are much simpler. They are:

  1. Superclasses win. If a superclass provides a concrete method, default methods with the same name and parameter types are simply ignored.
  2. Interfaces clash. If a super interface provides a default method, and another interface supplies a method with the same name and parameter types (default or not), then you must resolve the conflict by overriding that method.

Let's look at the second rule. Consider another interface with a getName method:

interface Named {
     default String getName() { return getClass().getName() + "_" + hashCode(); }
  }

What happens if you form a class that implements both of them?

class Student implements Person, Named {
     ...
  }

The class inherits two inconsistent getName methods provided by the Person and Named interfaces. Rather than choosing one over the other, the Java compiler reports an error and leaves it up to the programmer to resolve the ambiguity. Simply provide a getName method in the Student class. In that method, you can choose one of the two conflicting methods, like this:

class Student implements Person, Named {
     public String getName() { returnPerson.super.getName(); }
     ...
  }

Now assume that the Named interface does not provide a default implementation for getName:

interface Named {
     String getName();
  }

Can the Student class inherit the default method from the Person interface? This might be reasonable, but the Java designers decided in favor of uniformity. It doesn't matter how two interfaces conflict. If at least one interface provides an implementation, the compiler reports an error, and the programmer must resolve the ambiguity.

If neither interface provides a default for a shared method, then we are in the pre-Java 8 situation and there is no conflict. An implementing class has two choices: implement the method, or leave it unimplemented. In the latter case, the class is itself abstract.

I just discussed name clashes between two interfaces. Now consider a class that extends a superclass and implements an interface, inheriting the same method from both. For example, suppose that Person is a class and Student is defined as:

class Student extends Person implements Named { ... }

In that case, only the superclass method matters, and any default method from the interface is simply ignored. In our example, Student inherits the getName method from Person, and it doesn't make any difference whether the Named interface provides a default for getName or not. This is the "class wins" rule. The "class wins" rule ensures compatibility with Java 7. If you add default methods to an interface, it has no effect on code that worked before there were default methods. But be warned: You can never make a default method that redefines one of the methods in the Object class. For example, you can't define a default method for toString or equals, even though that might be attractive for interfaces such as List. As a consequence of the "classes win" rule, such a method could never win against Object.toString or Object.equals.

Static Methods in Interfaces

As of Java 8, you are allowed to add static methods to interfaces. There was never a technical reason why this should be outlawed: It simply seemed to be against the spirit of interfaces as abstract specifications.

Until now, it has been common to place static methods in companion classes. You find pairs of interfaces and utility classes such as Collection/Collections or Path/Paths in the standard library.

Have a look at the Paths class. It has only a couple of factory methods. You can construct a path from a sequence of strings, such as Paths.get("jdk1.8.0", "jre", "bin"). In Java 8, you can add this method to the Path interface:

public interface Path {
     public static Path get(String first, String... more) {
        return FileSystems.getDefault().getPath(first, more);
     }
     ...
  }

Then the Paths class is no longer necessary.

When you look at the Collections class, you will find two kinds of methods. A method such as:

public static void shuffle(List<?> list)

would work well as a default method of the List interface:

public default void shuffle()

You could then simply call list.shuffle() on any list.

For a factory method, that doesn't work because you don't have an object on which to invoke the method. That is where static interface methods come in. For example,

public static <T> List<T> nCopies(int n, T o)
     // Constructs a list of n instances of o

could be a static method of the List interface. Then you would call List.nCopies(10, "Fred") instead of Collections.nCopies(10, "Fred") and it would be clear to the reader that the result is a List.

It is unlikely that the Java collections library will be refactored in this way, but when you implement your own interfaces, there is no longer a reason to provide a separate companion class for utility methods.

In Java 8, static methods have been added to quite a few interfaces. For example, the Comparator interface has a very useful static comparing method that accepts a "key extraction" function and yields a comparator that compares the extracted keys. To compare Person objects by name, use Comparator.comparing(Person::name).

Conclusion

In this article, I compared strings by length with the lambda expression (first, second) -> Integer.compare(first.length(), second.length()). But with the static compare method, we can do much better and simply use Comparator.compare(String::length). This is a fitting way of closing this article because it demonstrates the power of working with functions. The compare method turns a function (the key extractor) into a more complex function (the key-based comparator). Such "higher-order functions" are discussed in more detail in my book, as well as in various online resources for Java 8.


Cay S. Horstmann is is a professor of computer science at San Jose State University, a Java Champion, and a frequent speaker at computer industry conferences. He is the author of Java SE 8 For the Really Impatient, published by Addison-Wesley, from which this article is adapted.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video