Lambda Expressions in Java

Introduction

Lambda functions have been an addition that came with Java 8, and was the language's first step towards functional programming, following a general trend toward implementing useful features of various compatible paradigms.

The motivation for introducing lambda functions was mainly to reduce the cumbersome repetitive code that went into passing along class instances to simulate anonymous functions of other languages.

Here's an example:

String[] arr = { "family", "illegibly", "acquired", "know", "perplexing", "do", "not", "doctors", "where", "handwriting", "I" };

Arrays.sort(arr, new Comparator<String>() {
    @Override public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
});

System.out.println(Arrays.toString(arr));

As you can see, the whole bit of instancing a new Comparator class and overriding its contents is a snippet of repetitive code that we may as well do without, since it's always the same.

The entire Arrays.sort() line can be replaced by something much shorter and sweeter, but functionally equivalent:

Arrays.sort(arr, (s1,s2) -> s1.length() - s2.length());

These short and sweet bits of code that do the same thing as their verbose counterparts are called syntactic sugar. This is because they don't add functionality to a language, but instead make it more compact and readable. Lambda functions are an example of syntactic sugar for Java.

Although I highly suggest reading this article in order, if you're unfamiliar with the topic then here's a quick list of what we'll cover for easier reference:

Lambdas as Objects

Before we get into the nitty-gritty of lambda syntax itself, we should take a look at what lambda functions are in the first place and how are they used.

As mentioned, they're simply syntactic sugar, but they're syntactic sugar specifically for objects implementing a single method interface.

In those objects, the lambda implementation is considered the said method's implementation. If the lambda and the interface match, the lambda function can be assigned to a variable of that interface's type.

Single-Method Interface Matching

In order to match a lambda to a single method interface, also called a "functional interface", several conditions need to be met:

  • The functional interface has to have exactly one unimplemented method, and that method (naturally) has to be abstract. The interface can contain static and default methods implemented within it, but what's important is that there's exactly one abstract method.
  • The abstract method has to accept arguments, in the same order, that correspond to the parameters lambda accepts.
  • The return type of both the method and the lambda function have to match.

If all of that is satisfied, all the conditions for matching have been made and you can assign your lambda to the variable.

Let's define our interface:

public interface HelloWorld {
    abstract void world();
}

As you can see, we have a pretty useless functional interface.

It contains exactly one function, and that function can do anything at all, as long as it accepts no arguments and returns no values.

We're going to make a simple Hello World program using this, though imagination is the limit if you want to play with it:

public class Main {
    public static void main(String[] args) {
        HelloWorld hello = () -> System.out.println("Hello World!");
        hello.world();
    }
}

As we can see if we run this, our lambda function has successfully matched to the HelloWorld interface, and the object hello can now be used to access its method.

The idea behind this is that you can use lambdas wherever you'd otherwise use functional interfaces to pass along functions. If you remember our Comparator example, Comparator<T> is actually a functional interface, implementing a single method - compare().

That's why we could replace it with a lambda that behaves similar to that method.

Implementation

The basic idea behind lambda functions is the same as the basic idea behind methods - they take parameters in and use them within the body consisting of expressions.

The implementation is just a bit different. Let's take the example of our String sorting lambda:

(s1,s2) -> s1.length() - s2.length()

Its syntax can be understood as:

parameters -> body

Parameters

Parameters are the same as function parameters, those are values passed to a lambda function for it to do something with.

Parameters are usually enclosed in brackets and separated by commas, although in the case of a lambda, which receives only one parameter, the brackets can be omitted.

A lambda function can take any number of parameters, including zero, so you could have something like this:

() -> System.out.println("Hello World!")

This lambda function, when matched to a corresponding interface, will work the same as the following function:

static void printing(){
    System.out.println("Hello World!");
}

Similarly, we can have lambda functions with one, two, or more parameters.

A classic example of a function with one parameter is working on each element of a collection in a forEach loop:

public class Main {
    public static void main(String[] args) {
        LinkedList<Integer> childrenAges = new LinkedList<Integer>(Arrays.asList(2, 4, 5, 7));
        childrenAges.forEach( age -> System.out.println("One of the children is " + age + " years old."));
    }
}

Here, the sole parameter is age. Note that we removed parentheses around it here, because that's allowed when we have only one parameter.

Using more parameters works similarly, they're just separated by a comma and enclosed in parentheses. We've already seen two-parameter lambda when we matched it to Comparator to sort Strings.

Body

A body of a lambda expression consists of a single expression or a statement block.

If you specify only a single expression as the body of a lambda function (whether in a statement block or by itself), the lambda will automatically return the evaluation of that expression.

If you have multiple lines in your statement block, or if you just want to (it's a free country), you can explicitly use a return statement from within a statement block:

// just the expression
(s1,s2) -> s1.length() - s2.length()

// statement block
(s1,s2) -> { s1.length() - s2.length(); }

// using return
(s1,s2) -> {
    s1.length() - s2.length();
    return; // because forEach expects void return
}

You could try substituting any of these into our sorting example at the beginning of the article, and you'll find that they all work exactly the same.

Variable Capture

Variable capture enables lambdas to use variables declared outside of the lambda itself.

There are three very similar types of variable capture:

  • local variable capture
  • instance variable capture
  • static variable capture

The syntax is almost identical to how you would access these variables from any other function, but conditions under which you can do so are different.

You can access a local variable only if it's effectively final, which means that it does not change its value after assignment. It doesn't have to be explicitly declared as final, but it's advisable to do so to avoid confusion. If you use it in a lambda function and then change its value, the compiler will start whining.

The reason you can't do this is because the lambda can't reliably reference a local variable, because it may be destroyed before you execute the lambda. Because of this, it makes a deep copy. Changing the local variable may lead to some confusing behavior, as the programmer might expect the value within the lambda to change, so to avoid confusion, it's explicitly forbidden.

When it comes to instance variables, if your lambda is within the same class as the variable you're accessing, you can simply use this.field to access a field in that class. Moreover, the field does not have to be final, and can be changed later during the course of the program.

This is because if a lambda is defined within a class, it's instanced along with that class and tied to that class instance, and can thus easily refer to the value of the field it needs.

Static variables are captured much like instance variables, except for the fact that you wouldn't use this to refer to them. They can be changed and don't need to be final for the same reasons.

Method Referencing

Sometimes, lambdas are just stand-ins for a specific method. In the spirit of making the syntax short and sweet, you don't actually have to type out the entire syntax when that is the case. For example:

s -> System.out.println(s)

is equivalent to:

System.out::println

The :: syntax will let the compiler know that you just want a lambda that passes along the given argument to println. You always just preface the method name with :: where you would write a lambda function, otherwise accessing the method as you would normally, meaning you still have to specify the owner class before the double colon.

There are various types of method references, depending on the type of method you're calling:

  • static method reference
  • parameter method reference
  • instance method reference
  • constructor method reference
Static Method Reference

We need an interface:

public interface Average {
    abstract double average(double a, double b);
}

A static function:

public class LambdaFunctions {
    static double averageOfTwo(double a, double b){
        return (a+b)/2;
    }
}

And our lambda function and call in main:

Average avg = LambdaFunctions::averageOfTwo;
System.out.println(avg.average(20.3, 4.5));
Parameter Method Reference

Again, we're typing in main.

Comparator<Double> cmp = Double::compareTo;
Double a = 20.3;
System.out.println(cmp.compare(a, 4.5));

The Double::compareTo lambda is equivalent to:

Comparator<Double> cmp = (a, b) -> a.compareTo(b)
Instance Method Reference

If we take our LambdaFunctions class and our function averageOfTwo (from Static Method Reference) and make it non-static, we'll get the following:

public class LambdaFunctions {
    double averageOfTwo(double a, double b){
        return (a+b)/2;
    }
}

To access this we now need an instance of the class, so we'd have to do this in main:

LambdaFunctions lambda = new LambdaFunctions();
Average avg = lambda::averageOfTwo;
System.out.println(avg.average(20.3, 4.5));
Constructor Method Reference

If we have a class called MyClass and want to call it's constructor through a lambda function, our lambda will look like this:

MyClass::new

It will accept as many arguments as it can match to one of the constructors.

Conclusion

In conclusion, lambdas are a useful feature for making our code simpler, shorter, and more readable.

Some people avoid using them when there's a lot of Juniors on the team, so I'd advise consulting with your team before you refactor all of your code, but when everyone's on the same page they're a great tool.

See Also

Here's some further reading into how and where to apply lambda functions:

Author image
Belgrade, Serbia