2. Lambda expressions

When people are talking about Java 8, lambda expression is always the first feature to mention. Lambda expression in Java 8 brings functional programming style into Java platform, which has been demanded by Java developers in the community for a long time. Lambda expression is also widely used as a marketing term to promote Java 8 upgrade.

Lambda expressions can increase developers’ productivity in many ways. There are a lot of books and online tutorials about Java 8 lambda expressions. This chapter is trying to explain lambda expressions from a different perspective based on the official JSR 335: Lambda Expressions for the JavaTM Programming Language. This chapter also provides how-tos for real programming tasks.

2.1 Start a thread - A simple example

Let’s start from a simple example which starts a thread and outputs some text to console, see Listing 2.1.

Listing 2.1 Start a thread and output text to console
1 public class OldThread {
2   public static void main(String[] args) {
3     new Thread(new Runnable() {
4       public void run() {
5         System.out.println("Hello World!");
6       }
7     }).start();
8   }
9 }

The small Java program in Listing 2.1 has 9 lines of code, but only one line of code (line 5) does the real work. All the rest are just boilerplate code. To increase productivity, boilerplate code should be removed as much as possible.

In Listing 2.1, line 1, 2, 8 and 9 are required for Java program to run, so these lines cannot be removed. Line 3 to 7 create a new java.lang.Thread object with an implementation of java.lang.Runnable interface, then invoke Thread object’s start() method to start the thread. Runnable interface is implemented using an anonymous inner class.

To simplify the code, new Runnable() in line 3 and 7 can be removed because the interface type Runnable can be deduced from allowed constructors of Thread class. Line 4 and 6 can also be removed, because run() is the only method in Runnable interface.

After removing boilerplate code in line 3, 4, 6 and 7, we get the new code using lambda expressions in Listing 2.2.

Listing 2.2 Lambda expressions style to start a thread
1 public class LambdaThread {
2   public static void main(String[] args) {
3     new Thread(() -> System.out.println("Hello World!")).start();
4   }
5 }

The new Java program in Listing 2.2 only has 5 lines of code and the core logic is implemented with only one line of code. Lambda expression () -> System.out.println("Hello World!") does the same thing as anonymous class in Listing 2.1, but using lambda expression is concise and elegant and easier to understand with less code. Less code means increasing productivity.

Simply put, lambda expression is the syntax sugar to create anonymous inner classes. But there are more to discuss about lambda expressions.

2.2 Functional interfaces

As shown in Listing 2.2, we made two assumptions to remove the boilerplate code. The first assumption is that the interface type, e.g. Runnable can be deduced from current context. The second assumption is that there is only one abstract method in the interface. Let’s start from the second assumption.

If an interface only has one abstract method (aside from methods of Object), it’s called functional interface. Functional interfaces are special because instances of functional interfaces can be created with lambda expressions or method references.

In Java SE 7, there are already several functional interfaces, e.g.

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.util.Comparator
  • java.io.FileFilter

Java SE 8 adds a new package java.util.function which includes new functional interfaces.

Java developers don’t need to care too much about the concept of functional interfaces. If we are trying to use lambda expressions with non-functional interfaces, compiler will throw errors, then we’ll know about that. Modern IDEs can also do checks for us. For API and library authors, if an interface is designed to be functional, it should be marked with @FunctionalInterface annotation. Compiler will generate an error if the interface doesn’t meet the requirements of being a functional interface.

2.3 Target typing

For the first assumption about lambda expressions, lambda expressions don’t have type information. The actual type of a lambda expression is deduced by compiler from its surrounding context at compile time. For example, lambda expression () -> System.out.println("Hello World!") can appear in any context where requires an instance of functional interface with the only abstract method takes no arguments and returns void. This could be java.lang.Runnable interface or any other functional interfaces created by third-party libraries or application code. So the same lambda expression can have different types in different contexts. Expressions like lambda expressions which have deduced type influenced by target type are called poly expressions.

2.4 Lambda expressions

Lambda expressions have a very simple syntax to write. The basic syntax looks like (a1, a2) -> {}. It’s similar with a method, which has a list of formal parameters and a body. Lambda expression’s syntax is also very flexible.

Listing 2.3 Example of lambda expressions
 1 list -> list.size()
 2 
 3 (x, y) -> x * y
 4 
 5 (double x, double y) -> x + y
 6 
 7 () -> "Hello World"
 8 
 9 (String operator, double v1, double v2) -> {
10   switch (operator) {
11     case "+":
12       return v1 + v2;
13     case "-":
14       return v1 - v2;
15     default:
16       return 0;
17   }
18 }

In Listing 2.3, lambda expression in line 1 only has one formal parameter, so parentheses are omitted. Expression’s body is a single expression, so braces are also omitted. Formal parameter list has no type declaration, so the type of list is implicitly inferred by compiler. Lambda expression in line 3 has two formal parameters, so parentheses around parameters list are required. Lambda expression in line 5 has explicit type for formal parameters x and y. Lambda expression in line 7 has no formal parameters. In this case, parentheses are required. Lambda expression starts from line 9 is a complicated example with three formal parameters and multiple lines of code in body. In this case, braces are required to wrap the body.

Lambda expression’s body can return a value or nothing. If a value is returned, the type of return value must be compatible with target type. If an exception is thrown by the body, the exception must be allowed by target type’s throws declaration.

2.5 Lexical scoping

In the body of lambda expressions, it’s a common requirement to access variables in the enclosing context. Lambda expression uses a very simple approach to handle resolution of names in the body. Lambda expressions don’t introduce a new level of scoping. They are lexically scoped, which means names in lambda expression’s body are interpreted as they are in the expression’s enclosing context. So lambda expression body is executed as it’s in the same context of code enclosing it, except that the body can also access expression’s formal parameters. this used in the expression body has the same meaning as in the enclosing code.

Listing 2.4 Lexical scoping of lambda expression
public void run() {
  String name = "Alex";
  new Thread(() -> System.out.println("Hello, " + name)).start();
}

In Listing 2.4, lambda expression uses variable name from enclosing context. Listing 2.5 shows the usage of this in lambda expression body. this is used to access sayHello method in the enclosing context.

Listing 2.5 this in lambda expression
public class LambdaThis {
  private String name = "Alex";

  public void sayHello() {
    System.out.println("Hello, " + name);
  }

  public void run() {
    new Thread(() -> this.sayHello()).start();
  }

  public static void main(String[] args) {
    new LambdaThis().run();
  }
}

From code examples in Listing 2.4 and Listing 2.5, we can see how lambda expressions make developers’ life much easier by simplifying the scope and name resolution.

2.6 Effectively final local variables

To capture variables from enclosing context in lambda expression body, the variables must be final or effectively final. final variables are declared with final modifiers. effectively final variables are never assigned after their initialization. Code in Listing 2.6 has compilation error, because variable name is assigned again after its initialization, so name cannot be referenced in lambda expression body.

Listing 2.6 Compilation error of using non-final variables in lambda expression body
public void run() {
  String name = "Alex";
  new Thread(() -> System.out.println("Hello, " + name)).start();
  name = "Bob";
}

2.7 Method references

Method references are references to existing methods by name. For example, Object::toString references toString method of java.lang.Object class. :: is used to separated class or instance name and method name. Method references are similar with lambda expressions, except that method references don’t have a body, just refer existing methods by name. Method references can be used to remove more boilerplate code. If the body of a lambda expression only contains one line of code to invoke a method, it can be replaced with a method reference.

StringProducer in Listing 2.7 is a functional interface with method produce.

Listing 2.7 Functional interface to produce a string
@FunctionalInterface
public interface StringProducer {
  String produce();
}

Listing 2.8 is an example of using StringProducer. displayString method takes an instance of StringProducer and outputs the string to console. In run method, method reference this::toString is used to create an instance of StringProducer. Invoking run method will output text like StringProducerMain@65ab7765 to the console. It’s the same result as invoking toString() method of current StringProducerMain object. Using this::toString is the same as using lambda expression () -> this.toString(), but in a more concise way.

Listing 2.8 Usage of StringProducer with method reference
public class StringProducerMain {
  public static void main(String[] args) {
    new StringProducerMain().run();
  }

  public void run() {
    displayString(this::toString);
  }

  public void displayString(StringProducer producer) {
    System.out.println(producer.produce());
  }
}

2.7.1 Types of method references

There are different types of method references depends on the type of methods they refer to.

Static method
Refer to a static method using ClassName::methodName, e.g. String::format, Integer::max.
Instance method of a particular object
Refer to an instance method of a particular object using instanceRef::methodName, e.g. obj::toString, str::toUpperCase.
Method from superclass
Refer to an instance method of a particular object’s superclass using super::methodName, e.g. super::toString. In Listing 2.8, if replacing this::toString with super::toString, then toString of Object class will be invoked instead of toString of StringProducerMain class.
Instance method of an arbitrary object of a particular type
Refer to an instance method of any object of a particular type using ClassName::methodName, e.g. String::toUpperCase. The syntax is the same as referring a static method. The difference is that the first parameter of functional interface’s method is the invocation’s receiver.
Class constructor
Refer to a class constructor using ClassName::new, e.g. Date::new.
Array constructor
Refer to an array constructor using TypeName[]::new, e.g. int[]::new, String[]::new.

2.8 Default interface methods

The introduction of default methods is trying to solve a longstanding problem in Java interface design: how to evolve a published interface. Java’s interface is a very tight contract between an API and its client. Before Java 8, once an interface is published and implemented by other clients, it’s very hard to add a new method to the interface without breaking existing implementations. All implementations must be updated to implement the new method. This means interface design has to be done correctly at the first time and cannot happen iteratively without breaking existing code.

Before Java 8, an interface can only have abstract methods. Abstract methods must be implemented by implementation classes. Default methods of interfaces in Java 8 can have both declarations and implementations. Implementation of a default method will be inherited by classes which don’t override it. If we want to add a new method to an existing interface, this new method can have default implementation. Then existing code won’t break because this new method will use the default implementation. New code can override this default implementation to provide a better solution. In the default method’s implementation, only existing interface methods can be used.

Listing 2.9 is a simple example about how to use default interface methods. The scenario is to insert records into a database.

Listing 2.9 First version of interface RecordInserter to insert records
public interface RecordInserter {
  void insert(Record record);
}

In the first version, interface RecordInserter only has one abstract method insert. SimpleRecordInserter class in Listing 2.10 is the first implementation.

Listing 2.10 First implementation of RecordInserter interface
public class SimpleRecordInserter implements RecordInserter {
  @Override
  public void insert(Record record) {
    System.out.println("Inserting " + record);
  }
}

Then we find out that the performance of inserting a lot of records is not very good, so we want to add batch inserting to improve performance. So a new method insertBatch is added to the RecordInserter interface with default implementation. insertBatch takes a list of Record as input, so the default implementation just iterates the list and uses existing insert method to insert Record one by one. This default implementation can make sure SimpleRecordInserter class still works.

Listing 2.11 RecordInserter interface with batch insert
public interface RecordInserter {
  void insert(Record record);

  default void insertBatch(List<Record> recordList) {
    recordList.forEach(this::insert);
  }
}

To improve the performance, a new implementation FastRecordInserter class overrides the default implementation of insertBatch method and create a better solution.

Listing 2.12 New implementation of insertBatch method
public class FastRecordInserter extends SimpleRecordInserter {
  @Override
  public void insertBatch(List<Record> recordList) {
    System.out.println("Inserting " + recordList);
    //Use SQL batch operations to insert
  }
}

2.8.1 Static interface methods

In Java 8, interfaces can also have static methods. Helper methods related to an interface can be added as static methods, see Listing 2.13.

Listing 2.13 Interface with a static method
public interface WithStaticMethod {
  static void simpleMethod() {
    System.out.println("Hello World!");
  }
}