Requirements: To understand this concept you have to be comfortable with the idea of interfaces and lambda expressions in Java. You also need to know how generic types are used in code.
Assuming that you are somewhat comfortable with lambda expressions and streams the next step is to design your code is such a way that other developers can easily use your code in a flexible way with their own lambda expressions.
This is an example of a functional interface. It defines an interface, and it only contains a single method.
@FunctionalInterface
public interface Printer {
void print(String text);
}
To indicate that this interface is intended to pass behaviour through the use
of a lambda expression
we can add the @FunctionalInterface
annotation. This allows the compiler
to complain when someone thinks it’s a good idea to add another method.
This annotation is not mandatory, but it’s good practice to show your intent
just like the use of @Override
.
The Java language libraries already define a long list of “all possible” functional interfaces. Below is a list of some of them:
“Please tell me if ‘X’ is acceptable.”
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
This interface takes an object of type T
and determines if it is “accepted”.
For instance, you may want to accept only apples that are green and weigh more than 200 grams.
public class AppleSelector implements Predicate<Apple> {
@Override
boolean test(Apple t) {
return t.getColor().equals("green") && t.getWeight()>200;
}
}
Or in the form of a lambda expression:
List<Apple> selection =
apples
.stream().
.filter(a -> a.getColor().equals("green") && a.getWeight()>200)
.collect(Collectors.toList());
Or, more useful as a way to define your own functions:
public static <T> List<T> selector(List<T> original, Predicate<T> p) {
return original.stream().filter(p).collect(Collectors.toList());
}
“Please accept ‘X’ and do something with it.”
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
A consumer takes an object and does something with it without returning anything afterward.
For instance, you may want to print the objects that are presented to you, or you may want to call a method in each of these object:
public static <T> void processList(List<T> list, Consumer<T> consumer) {
for (T t:list) {
consumer.accept(t);
}
}
public static void example() {
ArrayList<Apple> apples = readApples();
processList(apples, System.out::println);
}
As you can see println(Object o)
is a type of consumer function.
“Please convert ‘X’ into ‘Y’”
This interface functions as a map from one type to another.
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
A simple example would be converting a list containing apples to a list containing their weights.
public static <T,R> void processList(List<T> list, Function<T,R> function) {
List<R> result = new ArrayList<>();
for (T t:list) {
R converted = function.apply(t):
result.add(converted);
}
return result;
}
public static <T,R> void processStream(List<T> list, Function<T,R> function) {
return original.stream().map(function).collect(Collectors.toList());
}
Consider using this method to provide your student administration
class with a list of (student)numbers.
In order to receive a list containing the corresponding Student
s in return.
“Please provide me with ‘X’”
Every time you call the get()
method of a supplier it will provide
you with an object of the specified type.
@FunctionalInterface
public interface Supplier<T> {
T get();
}
You could use this to loop through the records of a table. Or to generate a different random value every time. etc.
The primary idea here is that you want to avoid storing the whole list of data that will be provided in memory. Instead, only when the method is called a new object is “read into memory”.
When describing these interfaces as Function<T,R>
both generic types
must be reference type.
Of course primitive alternatives exist: IntFunction<T>
will return a
primitive int
when provided with a T
.
Taken from ‘Java 8 in Action’ ISBN: 9781617291999