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.
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