Because of a course I prepared, I recently had a closer look at the java.util.function
package in JDK 8, and discovered a couple of interesting design choices.
Callable is not a Supplier
Their respective definitions are:
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Thus, a Callable and a Supplier are defined in the same way:
Callable<String> callable = () -> "Hello";
Supplier<String> supplier = () -> "Hello";
But they are not equivalent:
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(callable); (1)
executorService.submit(() -> "Hello");(2)
executorService.submit(supplier); (3)
executorService.submit((Callable) supplier); (4)
1 | OK |
2 | OK |
3 | Doesn’t compile |
4 | Throws at runtime |
Comparator is not a BiFunction
Their respective definitions are:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
@FunctionalInterface
public BiFunction<T, U, R> {
R apply(T t, U u);
}
The same statements as above apply, but there’s one thing of note.
No method throws a checked exception.
Obviously, methods names are different, but Comparator
could have been written in the following way:
public interface Comparator<T> extends BiFunction<T, T, Integer> {
int compare(T o1, T o2);
default Integer apply(T o1, T o2) {
return compare(o1, o2);
}
}
Yet, Comparator
is a legacy class, so better not touch it.
Predicate is not a Function
Definitions:
@FunctionalInterface
public Predicate<T> {
boolean test(T t);
}
@FunctionalInterface
public interface Function<T,R> {
R apply(T t);
}
As above, this one could have been written as:
public interface Predicate<T> extends Function<T, Boolean> {
boolean test(T t);
default Boolean apply(T t) {
return test(t);
}
}
Or even better, keeping just one single method:
public interface Predicate<T> extends Function<T, Boolean> {
}
There might be two reasons for this lack of relationship:
- The specialized method name (
test
instead ofapply
), but to be honest, that’s not a real issue since mostPredicate
instances will be written as lambdas anyway. - The
boolean
primitive return type - instead ofBoolean
, to prevent returningnull
. Guess what? Thanks to auto-boxing, it prevents nothing:
Predicate<Object> predicate = o -> (Boolean) null;
Conclusion
Java is a language that values backward-compatibility (e.g. generics and type erasure) - a lot. That means that whatever design choices are made, they will stay forever, or at least long enough to have consequences.
According to my understanding of Functional Programming, two functions are equivalent if, from the same input they return the same output. Naming is not relevant, and adds noise. While the 2 first lack of relationships described above can be justified by backward-compatibility, I find the last one contradicting that. Any thoughts?