Lambda Expressions in Java and its implementation by Kush Patel on July 28, 2020 1,401 views

Java is one of the most widely used programming languages. Despite origins dating back in 1995, it has consistently advanced over the years. One of the significant upgrades in Java after generics, was the introduction of lambda expression. Lambda expression is a function that can be passed around as if it was an object and invoked on demand. Lambda expressions combined with the Streams API allows programmers to write complex collection processing algorithms, focusing on what to compute rather than how to compute.

Predate Java 8

Till Java 7, the behaviour of a button in Swing platform was configured as follows:

button.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent event) {
          System.out.println("perform action");
      }
});

Here an event listener is registered to the button click action. But what’s the intent of the programmer? To do something when the button is clicked. To achieve this, one needs to pass an object that implements the ActionListener interface, override actionPerformed method and then finally add the behaviour (what to do when the button is clicked).

Issues with the above snippet:

  • Passing code (behaviour, intent of programmer) as data (object)
  • Four lines of boilerplate code used in order to call a single line of desired logic
  • The intent of programmer is buried deep inside the boilerplate code

With Java 8, using lambda expression, the above snippet translates to:

button.addActionListener(event -> System.out.println("perform action"));

Improvements:

  • Instead of passing an object that implements an interface, a block of code is passed. Here, the underlying anonymous object creation code is not handled by the programmer.
  • No boilerplate code.
  • Fairly easy to read the programmer’s intent.

Flavours of Lambda expression

Supplier<String> noArgs = () -> "Hello World!";

Lambda expression with no arguments. Empty pair of parentheses () is used to signify that there are no arguments.

UnaryOperator<Integer> incrementBy2 = i -> i + 2;

Lambda expression with single argument. The parentheses can be left out as there’s just one argument.

Function<String, Byte[]> downloadResultForStudentId = id -> {
	String url = formUrlFromStudentId(id);
	return downloadAttachmentFromUrl(url);
};

Multiple statements within the body of lambda expression. The block of code is surrounded by curly braces. This code block behaves the same way as that of a method. Braces can also be used with a single statement lambda expression.

UnaryOperator<Integer> incrementBy2ExplicitTypes = (Integer i) -> i + 2;

Lambda expression that explicitly states the type of arguments. If the type is stated, it should be placed within parentheses.

Effectively Final Variables

Till Java 7, when using an anonymous inner class, if it takes an argument from the surrounding scope, it has to be declared with the final modifier. With Java 8 this restriction has been relaxed. Although the referred variables are not declared with the final modifier, they need to be effectively final, i.e their value should not be updated once initialized. 

String name = "foo";
Supplier<String> greetings = () -> {
      return "hi" + name;
};

Attempting to update the referred variable would result in a compilation error:

String name = "foo";
Supplier<String> greetings = () -> {
	name = "bar";
	return "hi" + name;
};

The compilation error would look something like:

error: local variables referenced from a lambda expression must be final or effectively final

Functional Interface

The addActionListener() method takes an argument of ActionListener type. But in the below code snippet we pass a Lambda expression. So what’s the type of a Lambda expression?

button.addActionListener(event -> System.out.println("perform action"));

The type of the lambda expression is nothing but the same ActionListener type. But this interface type is now spiced up with the FunctionalInterface annotation. 

A functional interface is an interface with a single abstract method. This interface type is used as the type of lambda expression. So when a method takes an argument of Functional Interface type, you can pass a lambda expression to it. java.util.function package was added in Java 8 and it contains all the general purpose functional interfaces. Note that it is not the complete set. Other specific purpose functional interfaces are defined in the respective packages where they are used.

Behind the Scene

You might be wondering how the compiler implements lambda expressions and how the JVM deals with them. Since every lambda expression can be translated to an anonymous inner class by copying the body of the lambda expression into the body of the appropriate method of an anonymous class. Are lambda expressions simply syntactic sugar for anonymous inner classes? We will find it out shortly. Consider the following example. 

int evenSum = IntStream.of(2, 5, 4, 7, 3, 6)
		.filter(number -> number % 2 == 0)
		.sum();

From a list of values, It sums up integers with even parity. The filter() method takes in code (the actual logic that filters out odd numbers). sum() method adds the element in this stream. 

Rewriting the code using the anonymous inner class way:

int anonymousEvenSum = IntStream.of(2, 5, 4, 7, 3, 6)
		.filter(new IntPredicate() {
			@Override
			public boolean test(int value) {
				return value % 2 == 0;
			}
		})
		.sum();

Here the test() method is an abstract method of IntPredicate functional interface. But this approach is strongly discouraged! The following reasons summarize why it would be a bad implementation if lambda expression were to be substituted inside an anonymous inner class:

  • Adverse effect on application startup performance – The compiler generates a new class file for every anonymous inner class with filename ClassName$1 where ClassName is the name of the class in which anonymous class is defined. The result would be a generation of a class file for every lambda expression you would write — eeek! Each class file needs to be loaded and verified before use. The loading operation (that involves disk I/O, if the class file is inside a JAR then decompressing the JAR file) might be expensive in itself. Generation of many class files (that could’ve been avoided) is certainly not desirable, because it affects application boot time.
  • Increased application memory consumption – Each loaded class file would occupy space in JVM’s Metaspace. Also, these anonymous inner classes would be instantiated into separate objects. In addition to the above code snippet, if there were to be a code snippet that sums odd integers (implemented using anonymous inner class), there would be two separate class files after compilation, two instantiated objects for each class, despite the only change being the actual logic to filter out elements based on the parity.

Consider the following example:

public class AnonymousClassTest {
	BinaryOperator<Integer> adder = new BinaryOperator<Integer>(){
		@Override
		public Integer apply(Integer x, Integer y) {
			return x + y;
		}
	};
}

BinaryOperator<T> is a functional interface that takes two operands of the same type, operates on them and returns the result of the same type as the operands. Here we’ve implemented a simple adder that adds two Integer objects. 

The bytecode for the class file produced by the compiler can be examined using the command

javap -c ClassName 

Bytecode generated for the above code snippet looks something like this:

0: aload_0
1: invokespecial #1            // Method java/lang/Object."<init>":()V
4: aload_0
5: new           #2                    // class AnonymousClassTest$1
8: dup
9: aload_0
10: invokespecial #3         // Method AnonymousClassTest$1."<init>":(LAnonymousClassTest;)V
13: putfield #4                      // Field adder:Ljava/util/function/BinaryOperator;
16: return

Understanding bytecode is fairly simple. AnonymousClassTest$1 is the name of the anonymous inner class generated by the compiler. Its instance is created (#5), no-arg constructor is invoked (#10), its reference is assigned to the adder field (#13).

Following is the code snippet for the same adder but now using a lambda expression:

public class LambdaTest {
	BinaryOperator<Integer> adder = (x, y) -> x + y;
}

Inspecting the compiled bytecode:

0: aload_0
1: invokespecial #1         // Method java/lang/Object."<init>":()V
4: aload_0
5: invokedynamic #2,  0     // InvokeDynamic #0:apply:()Ljava/util/function/BinaryOperator;
10: putfield      #3        // Field adder:Ljava/util/function/BinaryOperator;
13: return

Notice something different? It’s the invokedynamic bytecode. JSR-292 was the proposal that requested to introduce this new bytecode. The title reads ‘Supporting Dynamically Typed Languages on the JavaTMPlatform’. Including dynamic typing in the warm snuggly world of a hard-core statically-typed language, so exciting!

The lambda expression is translated into bytecode as follows:

  1. An invokedynamic call site is generated. When invoked, an instance of FunctionalInterface is returned. The instance would be of the target type of the lambda expression.
  2. The body of lambda expression would be converted into a method. This method would be invoked by the invokedynamic instruction.

The actual benefit of the new bytecode instruction is that it defers the translation strategy until runtime. Even the above strategy is not fixed. If need be there might be new strategies in the future.

Although lambda expressions were introduced in Java 8 back in 2014, developers are still struggling to embrace this change. Both, object-oriented programming and functional programming have their own advantages to offer. Java 8 essentially incorporated some features from the functional paradigm. A quick fact check: FunctionalInterface annotation was added to the Runnable interface in Java 8. Thus not only new functional interfaces were added, but the capabilities of existing types were also upgraded. Java is evolving and so should we.

References and Useful links

  • An excellent blog about birth of invokedynamic bytecode instruction – http://blog.headius.com/2008/09/first-taste-of-invokedynamic.html
  • Metaspace: Java 8 replacement for Permanent Generation – https://stackoverflow.com/questions/18339707/permgen-elimination-in-jdk-8
  • What’s JSL, JSR and JEP? – https://stackoverflow.com/questions/51282326/what-is-the-difference-or-relation-between-jls-jsr-and-jep
  • Java Bytecode instruction list – https://en.wikipedia.org/wiki/Java_bytecode_instruction_listings

Improve Machine Learning Binary Classification using Boosting Crank up the UI development

About Author

Kush Patel

Senior Developer

Kush, a seasoned full stack developer with a curious mind, has worked on several complex enterprise applications. He thrives on challenging the status quo and aims to deliver what was expected on time. He's constantly looking for an opportunity to upgrade his skill set and implement it in everyday life. On life front he enjoys reading books, running marathons, long swim sessions, organizing kitchen pantry and creating memes. He holds a Bachelor's degree in Computer Engineering from Dharmsinh Desai University.