12Oct
Java Lambda Expressions
Java Lambda Expressions

Lambda Expressions are the most remarkable feature added to the Java platform with Java 8. It’s specified in JSR 335 and JEP 126. The very need for this feature is to gain some of the capabilities supplied by functional programming. The main idea behind this concept is to be able to parametrize the functions for subsequent executions of them.

Till Java 8, this could already be simulated via the use of anonymous inner classes with some design patterns like command pattern and functors. However, the adoption of lambda expressions gave way to the direct use of this concept.

1. What is a Lambda Expression?

A lambda expression is a piece of code that is giving an alternative way to the anonymous class to pass the function as a parameter to other subsequent flows of code such as methods, constructors, etc.. In this approach, a function can be referenced with a variable and passed as a reference to be executed in a subsequent flow of code execution.

The structure of a lambda expression is as follows; it is formed of some arguments, followed by an arrow and that’s followed by a code block.

Lambda Expression
Lambda Expression

2. Type of Lambda Expressions; Functional Interfaces

A lambda expression is identified by a special single-method interface called Functional Interface. A functional interface is the target type that’s determined by the compiler and used as the type of the reference to the lambda expression.

This binding of a lambda expression to a functional interface is determined from the context of the lambda expression. That means, the binding of a lambda expression to a target type can take place in different contexts such as variable declarations, method arguments, constructors, etc.; and from this binding, the compiler finds the target type, which is a functional interface, and infers the types of the parameters used in the lambda expression according to that functional interface.

A functional interface can be marked with an informative annotation @FunctionalInterface that can be used to inform other developers.

Let’s do a simple example to understand it well.
Think that we want to lowercase or uppercase a text based on a condition.
It will be a dynamic evaluation so we can abstract the operation.
By leveraging the lambda expressions we can do it as following:

Here is the lambda expressions for case operations:

t -> t.toUpperCase();
t -> t.toLowerCase();

By looking at the code above, we see that there is a parameter t; we do not know its type, and in the code block of the expression, the methods of t, which are toUpperCase and toLowerCase, are called.

To be able to pass these expressions to somewhere, we have to declare a functional interface; with that single-method interface, the compiler will be able to infer the type of t:

public interface CaseOperation {
    String operate(String text);
}

Then we can write our main code as such:

public void printWithCaseOperation(String text, CaseOperation operation){

    System.out.println(operation.operate(text));    
}

public void mainCode(){

    if(upperCaseEnabled){
        printWithCaseOperation("Hello Lambda!", t -> t.toUpperCase());
    } else {
        printWithCaseOperation("Hello Lambda!", t -> t.toLowerCase());
    }
}

Here, when we call the method printWithCaseOperation with a lambda expression as its second parameter, the compiler infers that the method’s second parameter is of type CaseOperation and, so is also of the lambda expression’s type, too.

3. Some Internals

At this point, let’s watch this video to listen to the internals of lambda expressions. For whom needs some speed, I will summarize it, you’re welcome:

  • We need lambdas basically since of; parallel friendly APIs and less code with the usage of closure-like functional programming capabilities.
  • Lambdas are not a new function type in the VM-level; it’s mostly about compiler level.
  • The compiler does a great job for us by transforming our lambdas into the form of a related defined functional interface. In other words; the compiler infers our lambdas as functional interfaces.
  • When the compiler sees the lambda expression, it simply creates a static method from the resolved related functional interface and in the invocation time, the VM executes that method by calling invokedynamic which is an invocation mode introduced in Java SE 7.

4. Lambda Syntax

Java Lambda Expressions have a basic structure as drawn in the diagram above. Besides this, some inferences can be made automatically by the compiler for us. When writing lambda expressions these compiler inferences directs us to write less code.

4.1 Full Syntax

The full syntax of a lambda expression is as follows.

(int a, int b) -> { return a + b; }

The left side of the arrow is just like the input type declaration part of a method signature.
Then put the arrow.
And lastly, write the body in curly braces just as in a method body.

4.2 Single Statements in Body

Curly brackets and the return keyword can be omitted in case of single statements in the body.

(int a, int b) -> a + b

4.3 Implicit Target Types

Types of the input variables can be omitted and these can be inferred by the compiler.

(a, b) -> a + b

4.4 Single Implicit Target Type

Parentheses are optional in case of a single implicit target type.

a -> a.size()

4.5 Explicit Target Types

When using explicit target types, then parentheses are required.

(String str) -> str.length()

4.6 Lambda Expressions Without Parameters

() -> "javalopment"

4.7 Multiple Statements In Body

The body can have multiple statements and in this case, the use of curly braces is mandatory.

(a, b) -> {
    int result = a + b;
    return result;
}

5. Built-in Functional Interfaces

Now that we know; ultimately, a functional interface is a method reference and also defines the target type of a lambda expression for the sake of compiler.

So we can structure our API around functional interfaces and use lambdas for more effective and clean code. However, as you see, a functional interface just defines the target types of lambda expressions. Hence, the same functional interfaces could be used in most cases. For that aim, in Java 8; several common built-in functional interfaces have already been created for us.

So instead of declaring our custom functional interfaces, we can use the built-in ones that will mostly meet our needs. Let’s look over that built-in functional interfaces.

5.1 Functions

The Function interface can be used in case of the need for;

      one input, one output

/**
 * @param <T> the type of the input to the function
 * @param <R> the type of the result of the function
 */
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

So if you need a functional interface that gets one input and returns an output then you should use the Function interface instead of creating a custom one of yours.

Let’s examine the following code:

Function<Person, String> f = t -> t.getGivenName();
String name = f.apply(Person.createShortList().get(0));
System.out.println(name);

In the code above, our lambda gets an instance t of type Person as an input and returns the name as String. When we execute the lambda via the Function interface then we get the result.

5.2 Suppliers

The Supplier interface can be used in case of the need for;

no input, one output

/**
 * @param <T> the type of results supplied by this supplier
 */
@FunctionalInterface
public interface Supplier<T> {
    T get();
}

So if you need a functional interface that gets no input and returns an output then you should use the Supplier interface instead of creating a custom one of yours.

Let’s examine the following code:

public int sum(int a, int b) {		
    int result = a + b;
    debug("supplierTest", () -> "returns " + result + " - for a: " + a + ", b: " + b);
    return result;
}

public void debug(String method, Supplier log){
    if(logger.isDebugEnabled()){
        logger.debug(method + " " + log.get());
    }
}

5.3 Consumers

The Consumer interface can be used in case of the need for;

one input, no output

/**
 * @param <T> the type of the input to the operation
 */
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

So if you need a functional interface that gets one input and returns no output then you should use the Consumer interface instead of creating a custom one of yours.

Let’s examine the following code:

Consumer<String> upperCaseWriter = (s) -> System.out.println(s.toUpperCase());
Consumer<String> lowerCaseWriter = (s) -> System.out.println(s.toLowerCase());

public void consumerTest(){
    write("upper-cased", upperCaseWriter);
    write("LOWER-CASED", lowerCaseWriter);
    write("Just as how it's written!", (s) -> System.out.println(s));
}

public void write(String log, Consumer<String> writer){
    writer.accept(log);
}

5.4 Predicates

The Predicate interface can be used in case of the need for;

one input, one boolean output

/**
 * @param <T> the type of the input to the predicate
 */
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

So if you need a functional interface that gets one input and returns a boolean output then you should use the Predicate interface instead of creating a custom one of yours.

Let’s examine the following code:

public static final Predicate<Person> YOUNG = p -> p.getAge() >= 18 && p.getAge() <= 25;
	
public void predicateTest() {
		
    Person person = Person.createShortList().get(0);
    System.out.println(YOUNG.test(person));
}

5.5 BiPredicate

The BiPredicate interface can be used in case of the need for;

two inputs, one boolean output

public boolean filter(String a, String b, BiPredicate<String, String> filterPredicate){
    return filterPredicate.test(a, b);
}
	
public void testBiPredicate(){
    boolean equals = filter("javalopment", "Javalopment", (a, b) -> a.equals(b));
    System.out.println(equals);
}

5.6 Primitives version of Predicate

IntPredicate, LongPredicate, and DoublePredicate are primitive versions of Predicate interface.

In these versions; you do not need to declare the input type. For example; for IntPredicate, the input type is an integer value.

public static final IntPredicate IS_YOUNG = age -> age >= 18 && age <= 25;
	
public void isYoung() {
    System.out.println(IS_YOUNG.test(14));
}

5.7 UnaryOperator

The UnaryOperator interface can be used in case of the need for;

one input, one output and both are the same type

/**
 * @param <T> the type of the operand and result of the operator
 */
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {

    static <T> UnaryOperator<T> identity() {
        return t -> t;
    }
}

Let’s look over the following code:

public void testUnaryOperator(){		
    UnaryOperator<String> upperCase = t -> t.toUpperCase();
    System.out.println(upperCase.apply("test"));
}

5.8 BinaryOperator

The BinaryOperator interface can be used in case of the need for;

two inputs, one output and all are the same type

Let’s look over the following code:

public static final BinaryOperator<Integer> SUM = (a, b) -> a + b; 
	
public void sum() {		
    System.out.println(SUM.apply(10, 20));
}

6. Composition Support In Built-in Functional Interfaces

Some of the built-in functional interfaces provide some utility methods enabling the composition of multiple functions.

6.1 Predicate Composition

The Predicate interface provides two utility methods for combining the predicates: and, or.
With these methods, we can easily combine existing predicates to generate new ones.

6.1.1 Predicate and

The Predicate interface provides the default method and. With the use of and, the new combined predicate returns true if all of the predicates return true.

Let’s look over the following code:

String text = "abc.def_ghi";
				
Predicate<String> containsDot = str -> str.contains(".");
Predicate<String> containsUnderscore = str -> str.contains("_");

Predicate<String> combinedPredicate = containsDot.and(containsUnderscore);

if(combinedPredicate.test(text)) {
    System.out.println("passed");
}

6.1.2 Predicate or

The Predicate interface also provides the default method or. With the use of or, the new combined predicate returns true if any one of the predicates returns true.

Let’s look over the following code:

String text = "abc.def";
				
Predicate<String> containsDot = str -> str.contains(".");
Predicate<String> containsUnderscore = str -> str.contains("_");

Predicate<String> seperated = containsDot.or(containsUnderscore);

if(seperated.test(text)) {
    System.out.println("passed");
}

6.2 Function Composition

The Function interface provides two utility methods for combining the functions: compose, andThen.
With these methods, we can easily combine existing functions to generate new ones.

6.2.1 Function compose

The Function interface provides the default method compose. With the use of compose, a new function is generated from a chain of functions. The new combined function will operate in the reverse order of the functions chained.

Let’s look over the following code:

Function<String, String> greet = str -> "Hi " + str;
Function<String, String> upperCase = str -> str.toUpperCase();

Function<String, String> upperCasedGreet = upperCase.compose(greet);

String greetingText = upperCasedGreet.apply("Erol");
System.out.println(greetingText); // HI EROL

6.2.2 Function andThen

The Function interface provides the default method andThen. With the use of andThen, a new function is generated from a chain of functions. The new combined function will operate in the same order of the functions chained.

Let’s look over the following code:

Function<String, String> greet = str -> "Hi " + str;
Function<String, String> upperCase = str -> str.toUpperCase();

Function<String, String> upperCasedGreet = greet.andThen(upperCase);

String greetingText = upperCasedGreet.apply("Erol");
System.out.println(greetingText); // HI EROL

7. Summary

In this article, we have looked over Java lambda expressions and its special reference type, Functional Interfaces. We have also gone over the built-in functional interfaces provided and examined the basic use cases.

In the next step, it will be worthwhile to going further to the details of the Java 8 Stream API.

You can see the sample code for this article on my Github page:
https://github.com/erolhira/java

Leave a Reply