Java 8 introduced functional programming in the form of Lambda. The term Lambda comes from lambda calculus, which is used to describe computations.
Lambda
We can think of a lambda expression as an anonymous function that can be assigned to a variable and passed to a method, which accepts a functional interface as an argument. Lambda expression doesn’t have a name, but it has a list of parameters, a body, and a return type.
(parameters) -> expression
A lambda expression can be used in the context of a functional interface.
Functional Interface
A functional interface is an interface that specifies exactly one abstract method.
public interface Comparator<T> {
int compare(T o1, T o2);
}
public interface Runnable {
void run();
}
Lambda expressions let us implement the abstract method of a functional interface directly inline and treat the whole expression as an instance of a functional interface.
Function Descriptor
We call the signature of the abstract method of the functional interface a function descriptor. The function descriptor describes the signature of the lambda expression. For example, we can think of the function descriptor of Runnable
as () -> void
as it has one abstract method which accepts nothing and returns nothing (void).
Java Functional Interface
Predicate
The java.util.function.Predicate<T>
interface defines an abstract method named test
that accepts an object of generic type T and returns a boolean. This interface can be used to represent a boolean expression that uses an object of type T
.
Function Descriptor : T -> boolean
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
Consumer
The java.util.function.Consumer<T>
interface defines an abstract method named accept
that takes an object of generic type T
and returns no result (void
). We can use this interface when we need to access an object of type T
and perform some operations on it.
Function Descriptor : T -> void
Function
The java.util.function.Function<T, R>
interface defines an abstract method named apply
that takes an object of generic type T
as input and returns an object of generic type R
. We can use this interface when we need to define a lambda
that maps information from an input object to output.
Function Descriptor : T -> R
Supplier
The java.util.function.Supplier<T>
interface defines an abstract method named get
that takes nothing and returns an object of type T
.
Function Descriptor : () -> R
Primitive Specializations
Primitive interfaces are specialized interfaces to avoid autoboxing operations when the inputs or outputs are primitives.
public interface IntPredicate {
boolean test(int t);
}
Type Checking
The type of lambda is deduced from the context in which the lambda is used. The type expected for the lambda expression inside the context (for example, a method parameter that it’s passed to or a local variable that it’s assigned to) is called the target- type. Lambda expression can get their target type from an assignment context, method-invocation context (parameters and return), and a cast context.
Object o = (Runnable) () -> System.out.println("Hello");
Capturing lambdas
Lambdas can capture (to reference in their bodies) instance and static variables without restrictions. But when local variables are captured, they have to be explicitly declared final or be effectively final.
Why do we have this restriction?
Instance variables are stored on the heap, whereas local variables live on the stack. Suppose a lambda could access the local variable directly, and the lambda was used in a thread. In that case, the thread using lambda could try to access the variable after the thread that allocated the variable had deallocated it. Hence, Java implements access to a free local variable as access to a copy rather than access to the original variable. This makes no difference if the local variable is assigned to only once—hence the restriction.
Method References
There are three main kinds of method references:
- A method reference to a static method. Example, –
Integer::parseInt
- A method reference to an instance method of an arbitrary type. Example –
String::length
- A method reference to an instance method of an existing object or expression. Example –
student::getRank
where the student is the local variable of Type Student which has method getRank()
List<String> list = Arrays.asList("a","b","A","B");
list.sort((s1, s2) -> s1.compareToIgnoreCase(s2));
can be written as
List<String> list = Arrays.asList("a","b","A","B");
list.sort(String::compareToIgnoreCase);
Constructor References
Reference to the existing constructor can be done using ClassName::new
Supplier<List<String>> supplier = ArrayList::new;
is same as Supplier<List<String>> supplier = () -> new ArrayList<>();
Composing Lambda
Many functional interfaces contain default methods that can be used to compose lambda expressions. Example of composition –
- Combine two predicates into a larger predicate that performs an or operation between the two predicates
- Reverse or chain comparator
Comparators
Sorting students based on Rank in reverse order
Comparator<Student> c = Comparator.comparing(Student::getRank);
students.sort(comparing(Student::getRank).reversed());
Sorting students based on name (reverse) and then Rank in reverse order
students.sort(comparing(Student::getName).reversed()
.thenComparing(Student::getRank));
Predicates
The Predicate interface includes three methods negate
, and
, and or
, which can be use to create more complicated predicated.
Predicate<Integer> naturalNumber = i -> i > 0;
Predicate<Integer> naturalNumberLessThanHundred = naturalNumber.and( i -> i < 100);
Functions
The Function interface comes with two default methods andThen
and compose
.
Consider f(x) = x2 and g(x) = x3 + 1 then
g(f(x)) ->
Function<Integer,Integer> square = n -> n*n;
Function<Integer,Integer> squareAndCube = square.andThen(n -> n*n*n+1);
System.out.println(squareAndCube.apply(2));
65
f(g(x)) ->
Function<Integer,Integer> square = n -> n*n;
Function<Integer,Integer> squareAndCube = square.compose(n -> n*n*n + 1);
System.out.println(squareAndCube.apply(2));
Applying Lambda
Let’s see how we can write a generic method to filter a collection of books based on certain attributes (think of this as a where clause of SQL).
public static List<Book> filter(Predicate<Book> where) {
List<Book> books = Catalogue.books();
return books.stream().filter(where).collect(Collectors.toList());
}
Lambda expression to filter different books by different filter
List<Book> javaBook = filter(book -> book.getCategory().equals(JAVA));
List<Book> joshuaBlochBook = filter(book -> book.getAuthor().equals("Joshua Bloch"));
Summary
A lambda expression can be considered an anonymous function that can be passed around which can be used in the context of a functional interface. A functional interface is an interface that specifies exactly one abstract method.
Discussion about this post