Basic usage of lambda expressions

Introduction

Lambda expressions are an important feature introduced in Java 8 that allow us to write anonymous functions in a more concise way. Before discussing Lambda expressions, let us first review the problems with anonymous inner classes.

Problems with anonymous inner classes

Before Java 8, we usually used anonymous inner classes to pass behavior. For example, in scenarios such as event processing and thread creation, we often need to implement an interface or inherit a class and override methods in it. This causes the code to appear verbose, and the introduction of Lambda expressions solves this problem, making the code more concise and readable.

public class Example {<!-- -->
    public static void main(String[] args) {<!-- -->
        //Start a new thread
        new Thread(new Runnable() {<!-- -->
            @Override
            public void run() {<!-- -->
                // run method in anonymous inner class
                System.out.println("Thread is running!");
            }
        }).start();

        //Code in main thread
        System.out.println("Main thread is running!");
    }
}

In this example, we use an anonymous inner class that implements the Runnable interface by creating a new thread. Although this way of writing was common before Java 8, it has some problems:

  1. Longitude: The syntax for using anonymous inner classes is relatively verbose, especially for simple functions, making the code cumbersome.

  2. Poor readability: The syntax of anonymous inner classes may make the code structure unclear and reduce readability.

We can improve this problem by using Lambda expressions to make the code more concise and clear. Here is the equivalent code using Lambda expressions:

public class Example {<!-- -->
    public static void main(String[] args) {<!-- -->
        // Use Lambda expression to start a new thread
        new Thread(() -> System.out.println("Thread is running!")).start();

        //Code in main thread
        System.out.println("Main thread is running!");
    }
}

By using Lambda expressions, we eliminate the lengthy anonymous inner class syntax and the code becomes more concise and easier to understand. This is an example of how lambda expressions can simplify code.

Syntax of Lambda expressions

The basic syntax of a lambda expression is as follows:

(parameters) -> expression

or

(parameters) -> {<!-- --> statements; }

Among them, the parameter is a parameter list enclosed in parentheses, and the arrow (->) separates the parameter list from the body of the Lambda expression. The body can be an expression or a set of statements.

Example

Let us illustrate the basic use of Lambda expressions through a simple example. Suppose we have an interface MyInterface:

interface MyInterface {<!-- -->
    void myMethod();
}

Before Java 8, we might need to use an anonymous inner class to implement this interface:

MyInterface myInterface = new MyInterface() {<!-- -->
    @Override
    public void myMethod() {<!-- -->
        System.out.println("Hello, world!");
    }
};

Using Lambda expressions, the above code can become more concise:

MyInterface myInterface = () -> System.out.println("Hello, world!");

The introduction of Lambda expressions makes the code more compact, especially when there is only one abstract method in the interface, the use of Lambda expressions will become particularly obvious.

This is just the basic usage of Lambda expressions. Next we will discuss the FunctionalInterface annotation in depth.

FunctionalInterface annotation description

Behind Lambda expressions, there is an important concept called Functional Interface. A functional interface is an interface with only one abstract method. It can contain multiple default methods or static methods, but can only have one abstract method.

In order to clearly identify an interface as a functional interface, Java 8 introduced the @FunctionalInterface annotation. This annotation is used to ensure that the interface contains only one abstract method so that it can be captured by a Lambda expression.

Definition of Functional Interface

Let’s define a functional interface with a simple example:

@FunctionalInterface
interface MyFunctionalInterface {<!-- -->
    void myMethod();
}

Here, the @FunctionalInterface annotation ensures that there is only one abstract method myMethod in the interface. If you try to add a second abstract method to this interface, the compiler will throw an error.

The relationship between Lambda expressions and Functional Interface

Lambda expressions are often used with functional interfaces to more concisely represent the implementation of a single abstract method. Let’s use the interface defined above as an example:

MyFunctionalInterface myFunctionalInterface = () -> System.out.println("Hello from Functional Interface!");

In this example, the Lambda expression implements the abstract method myMethod of the MyFunctionalInterface interface. Since MyFunctionalInterface is a functional interface, Lambda expressions can be correctly mapped to this interface.

Advantages of Functional Interface

Using the @FunctionalInterface annotation and combining it with Lambda expressions, we can write code in a more concise and clear way. This coding style helps make Java code more modern and easier to understand and maintain.

In the next part, we’ll dive into the principles of lambda expressions and understand how they work.

Analysis of Lambda expression principle

Understanding how lambda expressions work helps to gain a deeper understanding of how it works in Java. Behind the lambda expression is the functional interface and the operation mechanism of invokedynamic.

invokedynamic

Before Java 8, method calls on the Java Virtual Machine (JVM) were mainly completed through invokestatic, invokespecial, invokevirtual and other instructions. To support Lambda expressions, Java 8 introduced the invokedynamic directive, which allows the runtime to dynamically determine the call point of a method.

The introduction of invokedynamic provides Java with a more flexible method calling mechanism, making the implementation of Lambda expressions more efficient. It allows dynamic generation of bytecode at runtime, mapping lambda expressions to methods of functional interfaces.

Type inference of Lambda expressions

The type of a lambda expression is inferred by the compiler. The compiler infers the type of a lambda expression through contextual information and the target type. This means that lambda expressions can appear as instances of functional interfaces without explicitly specifying the type.

For example, where a Runnable is expected, you can use a Lambda expression without explicitly specifying the type:

Runnable myRunnable = () -> System.out.println("Hello, Runnable!");

In this example, the compiler can infer from the context that the type of myRunnable is Runnable because Runnable is a functional interface.

Internal implementation of Lambda expression

The internal implementation of lambda expressions is completed by the compiler and the Java virtual machine. The compiler translates the Lambda expression into bytecode and uses the invokedynamic directive to generate an instance of the functional interface. The method of this example is the actual implementation of the Lambda expression.

During actual runtime, the Java virtual machine uses the invokedynamic instruction to dynamically generate and link a call site qualifier so that Lambda expressions can be bound to functional interface methods.

Performance of Lambda expressions

Lambda expressions have been widely used in Java, and their performance has been optimized. The Java virtual machine optimizes the invokedynamic instruction to improve the execution efficiency of Lambda expressions. Although the performance of Lambda expressions may not be as good as traditional anonymous inner classes, in most cases, this performance difference is acceptable, and the simplicity and readability advantages of Lambda expressions are more prominent.

In the next section, we will discuss the elliptical way of writing lambda expressions, which is a further simplified way of lambda expressions.

Lambda expression omitted writing method

The omitted writing of lambda expressions is syntactic sugar introduced by Java to further simplify the code. This writing method mainly includes omitting parameter types, omitting parentheses, omitting curly braces, etc.

Omission of parameter types

In a lambda expression, the parameter type can be omitted if the parameter type can be inferred. For example:

// Parameterless type omitted
(MyInterface1 myInterface) -> System.out.println("Hello, world!");

// Parameter types can be inferred and omitted
(MyInterface1 myInterface) -> System.out.println("Hello, world!");

// Omit completely
myInterface -> System.out.println("Hello, world!");

In this example, the compiler can automatically infer the type of myInterface based on the context, so we can omit the parameter type.

Omission of brackets

If the lambda expression’s parameter list has only one parameter, you can omit the parentheses in the parameter list. For example:

//with parentheses
(MyInterface2 myInterface) -> System.out.println("Hello, world!");

// no brackets
myInterface -> System.out.println("Hello, world!");

In this example, since there is only one parameter, we can omit the parentheses.

Omission of curly braces

If the body of the lambda expression is only one line of code, the curly braces can be omitted. For example:

//with curly braces
(MyInterface3 myInterface) -> {<!-- --> System.out.println("Hello, world!"); }

// no curly braces
(MyInterface3 myInterface) -> System.out.println("Hello, world!");

In this example, since the body of the lambda expression is only one line of code, we can omit the curly braces.

Comprehensive omission

When the parameter types, parentheses, and curly braces of the Lambda expression can be omitted, the most concise form can be obtained:

//Omit completely
myInterface -> System.out.println("Hello, world!");

This most concise form of Lambda expression is often used in simple scenarios to improve the simplicity and readability of the code.

In the next section, we will summarize Lambda expressions, emphasizing their advantages and applicable scenarios.

When using lambda expressions, exception handling is an important consideration. In Lambda expressions, the way exceptions are handled may be somewhat different from traditional anonymous inner classes.

Exception handling in Lambda

There are two main situations of exception handling in Lambda expressions: checked exceptions and unchecked exceptions.

1. Checked exception

For checked exceptions, the interface method in the Lambda expression must declare the corresponding exception, otherwise it will not pass compilation. For example:

interface MyFunctionalInterface {<!-- -->
    void myMethod() throws SomeCheckedException;
}

// Checked exception handling in Lambda expressions
MyFunctionalInterface myInterface = () -> {<!-- -->
    // SomeCheckedException may be thrown
    // ...
};

2. Unchecked exception

For unchecked exceptions, exception handling in Lambda expressions is similar to ordinary Java methods. You can use try-catch blocks to catch exceptions or let exceptions continue to propagate. For example:

interface MyFunctionalInterface {<!-- -->
    void myMethod();
}

// Unchecked exception handling in Lambda expressions
MyFunctionalInterface myInterface = () -> {<!-- -->
    try {<!-- -->
        // May throw RuntimeException
        // ...
    } catch (RuntimeException e) {<!-- -->
        // Handle the exception or let the exception continue to propagate
        // ...
    }
};

Comparison of exception handling in Lambda and anonymous inner classes

Exception handling in Lambda expressions is more concise compared to anonymous inner classes. In anonymous inner classes, the code may appear more verbose because a complete try-catch block must be used.

new Thread(new Runnable() {<!-- -->
    @Override
    public void run() {<!-- -->
        try {<!-- -->
            //Exception handling in anonymous inner classes
            // ...
        } catch (SomeCheckedException e) {<!-- -->
            // Handle exceptions
            // ...
        }
    }
}).start();

In Lambda expressions, exceptions can be handled through concise syntax:

new Thread(() -> {<!-- -->
    //Exception handling in Lambda expressions
    // ...
}).start();

Overall, Lambda expressions provide a more concise and elegant way to handle exceptions, especially when there is only one abstract method in a functional interface. However, it should be noted that when using Lambda expressions, you must choose whether to handle exceptions or let exceptions propagate according to the specific situation.

Method reference

Method Reference is an important concept when introducing the advanced topic of lambda expressions. Method references provide a more concise syntax for representing specific types of lambda expressions. It is not a new feature, but a shorthand form of Lambda expression, making the code clearer and easier to read.

The syntax of method reference is expressed by using ::, which is mainly used to simplify Lambda expressions and make the code more compact. There are four main forms of method citation:

  1. Static method reference: ClassName::staticMethodName
  2. Instance method reference: instance::instanceMethodName
  3. Object method reference: ClassName::instanceMethodName
  4. Constructor reference: ClassName::new

Let us elaborate on these forms with examples:

1. Static method reference

Suppose there is a static method:

class MyClass {<!-- -->
    static void staticMethod() {<!-- -->
        System.out.println("Static method");
    }
}

Use a Lambda expression to call this static method:

Runnable runnable = () -> MyClass.staticMethod();

Use static method references:

Runnable runnable = MyClass::staticMethod;

2. Instance method reference

Suppose there is an instance method:

class MyClass {<!-- -->
    void instanceMethod() {<!-- -->
        System.out.println("Instance method");
    }
}

Use a Lambda expression to call this instance method:

MyClass myInstance = new MyClass();
Runnable runnable = () -> myInstance.instanceMethod();

Use instance method references:

MyClass myInstance = new MyClass();
Runnable runnable = myInstance::instanceMethod;

3. Object method reference

Suppose there is a class:

class MyClass {<!-- -->
    void instanceMethod() {<!-- -->
        System.out.println("Instance method");
    }
}

Use a Lambda expression to call this instance method:

Function<MyClass, Void> function = myInstance -> myInstance.instanceMethod();

Use object method references:

Function<MyClass, Void> function = MyClass::instanceMethod;

4. Constructor reference

Suppose there is a class:

class MyClass {<!-- -->
    MyClass() {<!-- -->
        System.out.println("Constructor");
    }
}

Use a Lambda expression to call this constructor:

Supplier<MyClass> supplier = () -> new MyClass();

Use constructor references:

Supplier<MyClass> supplier = MyClass::new;

Method references make code more concise, especially when the body of the lambda expression is just a call to an existing method. Choosing an appropriate method reference form can improve the readability of your code.

Lambda expression summary

Lambda expression is an important feature introduced in Java 8. It introduces the concept of functional programming to the Java language, making code writing more concise and flexible. When summarizing Lambda expressions, let us review its advantages and applicable scenarios.

Advantages of Lambda expressions

  1. Simplicity: Lambda expressions can achieve the same function with less code, making the code more concise and clear.

  2. Readability: The simplicity of lambda expressions helps improve the readability of code, especially when dealing with functional programming scenarios.

  3. Flexibility: Lambda expressions allow functions to be treated as first-class citizens and can be passed and assigned to variables, thereby increasing the flexibility of the code.

  4. Functional programming support: The introduction of Lambda expressions makes Java better support functional programming, making the code more expressive.

Applicable scenarios of Lambda expressions

  1. Set operations: Lambda expressions are very convenient when operating on collections, such as using forEach, filter, map and other methods.

  2. Event handling: In event listeners, Lambda expressions can implement callback functions in a more concise way.

  3. Multi-threading: Lambda expressions provide a more convenient way when creating threads or using concurrency tools.

  4. Simple functional interface implementation: When a simple functional interface needs to be implemented, Lambda expressions can be completed in a more compact form.

Summary

The introduction of lambda expressions makes the Java language more modern, makes the code more concise and readable, and provides better support for functional programming. However, when using Lambda expressions, you still need to pay attention to following the specifications of functional interfaces to ensure that the Lambda expression can be correctly mapped to the corresponding interface method.

By in-depth understanding of the basic use of Lambda expressions, annotations of Functional Interface, analysis of the principles of Lambda expressions, omissions of Lambda expressions, and summary, you can more fully grasp the application and advantages of Lambda expressions in Java. In actual projects, reasonable use of Lambda expressions can make the code more concise and flexible, and improve development efficiency.