Lambdas & Single Abstract Method Interfaces
- 6 minutes read - 1067 wordsQuick Tour of Java Functional Interfaces
A functional interface is an interface with just one abstract method, such as Runnable, Comparator, Callable, etc. The main benefit of functional interfaces, with the introduction of lambdas in Java 8, is that they allow you to pass behavior (code) to a function the same way you’d pass data (objects) to a method. This means we can pass a lambda expression when a method is accepting a functional interface. Since there is only one function to override, it is possible to infer which function to implement when using a lambda expression.
Most common ones introduced in Java 8:
| Functional Interface | Override | Description |
|---|---|---|
| Predicate<T> | boolean test(T t) |
Takes an object and returns a boolean (e.g. x == 2). |
| Supplier<T> | T get() |
Takes nothing and returns an object of type T. |
| Consumer<T> | void accept(T t) |
Takes an object and returns nothing. |
| Function<T, R> | R apply(T t) |
Takes an object of type T and returns an object of type R. |
| BiFunction<T, E, R> | R apply(T t, E e) |
Takes objects of type T and E and returns an object of type R. |
Note the Comparatorcompare(T o1, T o2).
Why is this cool?
Let’s take the Supplier interface. How do we implement it?
Supplier<Integer> mySupplier = new Supplier<Integer>() {
@Override
public Integer get() {
return new Random().nextInt(0, 10);
}
};
Can we simplify this declaration?
Supplier<Integer> mySupplier = () -> new Random().nextInt(0, 10);
BAM - a one-liner! Because we can assume a default abstract function, using a lambda expression is possible.
By using the @FunctionalInterface annotation, you can define a custom functional
interface.
This indicates the compiler to validate whether there is only one declared abstract method.
Further examples
See a small example below with associated simple unit tests.
Note that the functionalities implemented below are for the sake of using the functional interfaces and observe which function needs an override. They do not represent any recommended implementation and are to be considered as they are: dummy implementations.
Skip to next paragraph for usage with the Stream API.
package org.example;
import java.util.Comparator;
import java.util.function.*;
public class FunctionalInterfacesImpl {
private StringBuilder myMessage = new StringBuilder();
public String getMessage() {
Supplier<String> messageSupplier = () -> {
return myMessage.toString();
};
return messageSupplier.get();
}
public void appendToMessage(String str) {
Consumer<String> messageAppender = x -> {
myMessage.append(str);
};
messageAppender.accept(str);
}
public String replaceMessage(String str) {
Function<String, String> messageReplacer = x -> {
myMessage.setLength(0);
myMessage.append(str);
return myMessage.toString();
};
return messageReplacer.apply(str);
}
public String replaceMessageWithLongestString(String str1, String str2) {
BiFunction<String, String, String> messageReplace = (x, y) -> {
myMessage.setLength(0);
if (x.length() > y.length()) {
myMessage.append(x);
} else {
myMessage.append(y);
}
return myMessage.toString();
};
return messageReplace.apply(str1, str2);
}
public boolean startsWithSpace() {
Predicate<String> predicate = x -> {
return x.startsWith(" ");
};
return predicate.test(myMessage.toString());
}
public int compare(String str) {
Comparator<String> comparator = (c1, c2) -> {
return c2.compareTo(c1); //reverse order
};
return comparator.compare(str, myMessage.toString());
}
}
package org.example;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.*;
public class FuncIntImplTest {
FunctionalInterfacesImpl testImpl = new FunctionalInterfacesImpl();
@Test
public void supplierTest() {
assertEquals("", testImpl.getMessage());
}
@Test
public void consumerTest() {
testImpl.appendToMessage("my test string");
assertEquals("my test string", testImpl.getMessage());
}
@Test
public void functionTest() {
assertEquals("my new test string", testImpl.replaceMessage("my new test string"));
}
@Test
public void predicateTest() {
testImpl.replaceMessage("new str");
assertFalse(testImpl.startsWithSpace());
testImpl.replaceMessage(" new str");
assertTrue(testImpl.startsWithSpace());
}
@Test
public void biFunctionTest() {
String shortS = "short";
String longS = "lonnnnnng";
assertEquals(longS, testImpl.replaceMessageWithLongestString(shortS, longS));
}
@Test
public void comparatorTest() {
String test = "this is it";
testImpl.replaceMessage("hi there");
assertTrue(testImpl.compare(test) < 0);
assertTrue(testImpl.compare(test.substring(1, 2)) > 0);
}
}
Relation to Streams
In Java 8+, the Stream API introduced methods that work with functional interfaces.
Assume you get a list of dogs from Doggy Day Care - I’ll omit the Dog class, which only contains a dog’s name and its age.
List<Dog> dogs = Arrays.asList(
new Dog("Buddy", 4),
new Dog("Pépin", 3),
new Dog("Bella", 1),
new Dog("Charlie", 2),
new Dog("Daisy", 13)
);
Extracting all adult dogs (age > 3) is a good example of how lambda expressions can be
passed in where functional interfaces are expected on the filter, map, sorted
and forEach methods. Functional interfaces allow the chaining of operations done on a stream to be done smootly.
List<String> adultDogNames = dogs.stream()
.filter(dog -> dog.age >= 3)
.sorted((dog1, dog2) -> {
return Integer.compare(dog2.getAge(), dog1.getAge());
}) // descending order
.map(Dog::getName)
.collect(Collectors.toList());
System.out.println("Adult dogs names: " + adultDogNames);
// Age all dogs by 1 year
dogs.forEach(dog -> dog.age++);
adultDogNames = dogs.stream()
.filter(dog -> dog.age >= 3)
.sorted((dog1, dog2) -> Integer.compare(dog2.getAge(), dog1.getAge()))
.map(Dog::getName)
.toList();
System.out.println("Adult dogs names after aging by 1 year: " + adultDogNames);
Adult dogs names: [Daisy, Buddy, Pépin]
Adult dogs names after aging by 1 year: [Daisy, Buddy, Pépin, Charlie]
Finding is there is at least one puppy in the stream is done using anyMatch while the oldest dog
is found using reduce.
boolean isPuppy = dogs.stream()
.anyMatch(dog -> dog.getAge() < 2);
if (isPuppy) System.out.printf("There are puppies at doggy day care today!%n");
String oldestDog = dogs.stream()
.reduce((dog1, dog2) -> dog1.getAge() > dog2.getAge() ? dog1 : dog2)
.map(Dog::getName)
.orElseThrow();
System.out.printf("The oldest dog is %s%n", oldestDog);
There are puppies at doggy day care today!
The oldest dog is Daisy
Non-exhaustive list of Stream API below - there will be a separate post dedicated to the Stream API.
| Stream API | Functional Interface | Description |
|---|---|---|
| forEach() | Consumer<T> |
Performs an action on each element of the stream. In the example, it updates the age of each dog. |
| filter() | Predicate<T> |
Filters elements based on a condition. In the example, it keeps only dogs with age ≥ 3. |
| map() | Function<T, R> |
Transforms elements from one type to another. In the example, it extracts dog names (Dog → String). |
| sorted() | Comparator<T> |
Sorts elements based on a comparator. In the example, it sorts dogs by age in descending order. |
| reduce() | BinaryOperator<T> |
Reduces the stream to a single value. In the example, it finds the oldest dog. |
| anyMatch() | Predicate<T> |
Checks if any element matches a condition. In the example, it checks for puppies (age < 2). |
Note on the Collector: collect()method collects the elements of the stream into a container, such as a list or map. It typically uses a Supplier (to create the container), an Accumulator (to add elements), and a Combiner (to combine results). For example, Collectors.toList() is commonly used to collect elements into a list.