11.2
Method References and the Four Shapes
Java ships forty functional interfaces and you need four of them. Learn those four, learn the two colons, and most functional code stops looking like a new language.
Previously on
Section 11.1 ended on this line:
list.sort((a, b) -> a.name().compareTo(b.name()));The lambda takes two values and passes them straight into a method that already exists. It adds nothing. This section removes it, and then answers the bigger question sitting behind it: Comparator was one functional interface, so what are the others?
The problem
You have written one lambda. Now write ten, and the questions start.
names.forEach(n -> System.out.println(n));
List<Integer> lengths = map(names, n -> n.length());
List<String> shouted = map(names, n -> n.toUpperCase());
boolean anyLong = any(names, n -> n.length() > 5);What type does each of these have? A lambda is an object implementing an interface. Comparator was the interface last time. What is the interface here? There is no comparing going on.
You could invent one for each:
interface StringToInt { int convert(String s); }
interface StringToString{ String convert(String s); }
interface StringTest { boolean test(String s); }
interface StringAction { void run(String s); }Now do it for Integer. And Student. And every pair of types you ever use. The count runs away immediately, and the interfaces would all be the same four shapes wearing different names.
The second problem is that some lambdas say nothing. Look again:
n -> System.out.println(n)Read it out. Take n, and call println with n. The lambda’s whole contribution is moving a value from the left of the arrow to the right. It is a courier for one argument.
The same waste turns up everywhere:
s -> Integer.parseInt(s)
s -> s.toUpperCase()
() -> new ArrayList<>()Every one of them names a parameter, writes an arrow, and then hands the parameter straight to a method that was already going to accept it.
The idea
Java answers both problems at once. It ships the interfaces, and it lets you drop the courier.
Four interfaces cover almost everything. They live in java.util.function, and they differ only in what goes in and what comes out.
| Takes | Returns | |
|---|---|---|
| Function<T, R> | one T | an R. Method: apply |
| Predicate<T> | one T | a boolean. Method: test |
| Supplier<T> | nothing | a T. Method: get |
| Consumer<T> | one T | nothing. Method: accept |
Function<String, Integer> length = s -> s.length();
Predicate<String> isLong = s -> s.length() > 4;
Supplier<List<String>> fresh = () -> new ArrayList<>();
Consumer<String> show = s -> System.out.println(s);
length.apply("hello"); // 5
isLong.test("hi"); // false
fresh.get(); // a new empty list
show.accept("printed"); // printsPredicate is a Function that answers yes or no. It gets its own name because asking yes or no questions is common enough to be worth one.
There are about forty in the package. All of them are variations: two arguments instead of one (BiFunction), primitives instead of objects (IntPredicate), same type in and out (UnaryOperator). Learn the four and the rest are lookups.
Now drop the courier. When a lambda does nothing but pass its argument to an existing method, name the method instead:
n -> System.out.println(n) becomes System.out::println
s -> Integer.parseInt(s) becomes Integer::parseInt
s -> s.toUpperCase() becomes String::toUpperCase
() -> new ArrayList<>() becomes ArrayList::newThat is a method reference. Two colons, and the parameter list disappears because it was only ever repeating itself.
There are four kinds, and the third is the one that confuses people.
The four kinds of method reference
- A static method
Integer::parseInt. Same ass -> Integer.parseInt(s). The argument goes in as the argument. - A method on one particular object
System.out::println. You already have the object,System.out, and the argument goes in as the argument. - A method on whichever object turns up
String::toUpperCase. Same ass -> s.toUpperCase(). The argument becomes the receiver, not the argument. It is the one that reads oddly, becausetoUpperCasetakes nothing. - A constructor
ArrayList::new. Same as() -> new ArrayList<>(), or with an argument,n -> new ArrayList<>(n). Java picks the constructor that fits.
And they join together. These interfaces have default methods, which is the Section 8.5 feature that let Java add to an interface without breaking anyone:
Function<Integer, Integer> addTwo = x -> x + 2;
Function<Integer, Integer> triple = x -> x * 3;
addTwo.andThen(triple).apply(2); // 12. add first, then multiply
addTwo.compose(triple).apply(2); // 8. multiply first, then addandThen runs left to right. compose runs right to left. Same two functions, same input, different answers, and neither one is an error.
Predicates join up too:
Predicate<String> isLong = s -> s.length() > 4;
Predicate<String> hasA = s -> s.contains("a");
isLong.and(hasA).test("banana"); // true
isLong.and(hasA).test("bond"); // false
isLong.or(hasA).test("cat"); // true
isLong.negate().test("hi"); // trueUnder the hood
Going deeperHow does andThen return a function? It builds a new lambda that closes over both of the old ones. The real source is three lines:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}Read the last line. It returns a lambda. Inside that lambda, apply(t) runs on the current function first, and its answer is fed to after.apply. Nothing runs when you call andThen. All it does is make a third function that remembers the first two.
compose is the same line with the two swapped:
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}That is the entire difference between them. Which name is on the inside.
The wildcards are doing real work. Look at the parameter type again:
Function<? super R, ? extends V> afterThat is PECS from Section 9.4, and it is why this is not annoying to use. ? super R means the next function can accept R or anything more general. So a Function<Object, String> can follow a Function<String, Integer>, because anything that handles Object handles an Integer. Without the wildcard the types would have to match exactly and most chains would be rejected.
Method references are not a separate feature. They compile to the same invokedynamic instruction a lambda does. The difference is only what the generated class calls: a lambda calls the private lambda$main$0 method the compiler made for you, and a method reference calls the method you named directly. There is no extra step, and no separate hop.
The primitive interfaces exist for one reason. Generics hold objects only, from Section 9.3, so Function<Integer, Integer> boxes:
Function<Integer, Integer> boxed = x -> x + 1; // int -> Integer -> int, every call
IntUnaryOperator prim = x -> x + 1; // int -> intFifty million calls, measured on Java 21:
| What it holds | 50 million calls | |
|---|---|---|
| Function<Integer, Integer> | boxes in, unboxes out | 204 ms |
| IntUnaryOperator | int all the way through | under 1 ms |
The arithmetic in both is one addition. Everything else is fifty million Integer objects being created and collected. That is why IntPredicate, IntFunction, ToIntFunction and the rest of the primitive family exist, and it is why Section 11.6 has IntStream in it.
What it costs
String::valueOf has nine overloads. Which one you got is decided by the interface you assigned the reference to, and your code never says. Change the interface later and a different method starts running, with no edit to the reference and no warning anywhere.
Then there is the sheer number of names. BiFunction, ToDoubleBiFunction, ObjIntConsumer, DoubleUnaryOperator. The naming turns out to be regular, and until you see the pattern it reads like an alphabet. Learn the four shapes first, and after that a name is a description rather than a word to memorise.
Debugging gets harder in a specific way. A chain of five composed functions is one line to read and one line to step over. No breakpoint sits anywhere useful, and no variable holds an intermediate value you could look at. When a pipeline gives a wrong answer, pulling it apart into named steps is usually faster than staring at it.
compose deserves its own warning. It reads left to right and runs right to left, which in review is easy to miss, and the result is a wrong number rather than an error. Using andThen every time costs nothing and removes the mistake completely.
The last cost is the one your fingers will walk into. Function<Integer, Integer> is the version that comes to mind, and it is the slow one. Put it on a hot loop and the difference is a hundredfold, for no benefit, and nothing on the screen will look wrong.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
`Function`, `Predicate`, `Supplier`, `Consumer`. What single question separates them?
Show the answer
What goes in and what comes out.
Function takes one and returns something else.
Stringin,Integerout. Its method isapply.Predicate takes one and returns a
boolean. It is a Function whose answer is always yes or no, which is common enough to deserve a name. Its method istest.Supplier takes nothing and returns something. Its method is
get.Consumer takes one and returns nothing. It exists to do something, not to answer. Its method is
accept.Every one of the forty is a variation on those four. Once you can name the shape you need, finding the interface is a lookup.
`addTwo.andThen(multiplyByThree)` and `addTwo.compose(multiplyByThree)` both build one function from two. Which does what?
Show the answer
andThenruns left to right.addTwo.andThen(multiplyByThree)applied to 2 gives 12: add first, then multiply.composeruns right to left.addTwo.compose(multiplyByThree)applied to 2 gives 8: multiply first, then add.Same two functions, same input, two different answers, and no error either way. If you cannot remember which is which, use
andTheneverywhere. It reads in the order the work happens, which is the order you were thinking in.Both take a function and return a function. That is what lets them chain.
Why does `IntUnaryOperator` exist when `Function<Integer, Integer>` already works?
Show the answer
Because
Function<Integer, Integer>cannot hold anint. Generics only work with objects, from Section 9.3, so every call boxes the input into anIntegerand unboxes the answer.Fifty million calls, measured: 204 ms for
Function<Integer, Integer>against under 1 ms forIntUnaryOperator.The gap is not the arithmetic. It is fifty million
Integerobjects being made and thrown away.IntUnaryOperatortakes anintand returns anint, so nothing is allocated at all.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises90 pointsabout 95 minutes
Name the Shape
ex-11-2-aDo these in a fixed order and the interface picks itself.
Write down what goes in. Write down what comes out. Only then choose the name. If you go the other way round, starting from the list of forty interfaces, the list wins.
The last two are there to break the pattern on purpose. One of them needs two arguments, and one of them has a type you have to name yourself. Both are still one of the four shapes underneath.
What your program must do
- For each job write down what goes in and what comes out before choosing anything
- Pick the interface, then write the lambda, then call its method
- Two of the six do not fit the four basic shapes. Find them and say what they need
- Write the method name for each of the four in a comment: apply, test, get, accept
import java.util.*;
import java.util.function.*;
public class Shapes {
public static void main(String[] args) {
// For each job below, write down IN, OUT, then pick the interface,
// then write the lambda. Do it in that order every time.
// TODO: turn a String into its length
// TODO: answer whether a String is longer than 4
// TODO: make a brand new empty ArrayList
// TODO: print a String to the screen
// TODO: add two ints together (what is different about this one?)
// TODO: turn a Student into its name (what are IN and OUT here?)
}
}
Hint 1
Function. One in and a boolean out is Predicate. Nothing in is Supplier. Nothing out is Consumer.Hint 2
Bi, or one describing two of the same type.Hint 3almost the answer
BiFunction<Integer, Integer, Integer> works and boxes three times per call. IntBinaryOperator does the same job with no boxing at all, which is the point of Section 11.6.The One That Reads Wrong
ex-11-2-bWrite each one twice, lambda first.
The lambda is the version where you can see the parameter. The method reference is the version where it has been taken away, and the whole skill is knowing where it went.
Three of the four are boring, and that is fine. Spend your time on String::toUpperCase, because that is the one behind almost every method reference error you are going to get. toUpperCase accepts nothing. Function hands over one value. Work out how those two facts fit together and this stops being confusing for good.
What your program must do
- Write all four kinds, each as a lambda first and then as a method reference
- Explain where the argument goes in each of the four
- Make one of them fail to compile on purpose, and read the error
- Say in one sentence what makes the third kind different from the other three
import java.util.*;
import java.util.function.*;
public class Colons {
static String shout(String s) { return s.toUpperCase() + "!"; }
public static void main(String[] args) {
String greeting = "hello";
// Write each one FIRST as a lambda, THEN as a method reference.
// TODO: a static method (Integer.parseInt)
// TODO: a method on one object (System.out.println, greeting.length)
// TODO: a method on any object (String.toUpperCase)
// TODO: a constructor (new ArrayList<>)
// TODO: String::toUpperCase takes no arguments, but Function supplies one.
// Work out where that argument went, and write the answer in a comment.
}
}
Hint 1
Hint 2
String::toUpperCase means: given a String, call toUpperCase on it. The value moved from inside the brackets to in front of the dot.Hint 3almost the answer
Two Names, Two Answers
ex-11-2-cPredict first. That is the whole exercise, and skipping it wastes it.
Both lines compile. Both give you an integer. One of them is the answer you had in your head and the other is not, and nothing in the output will tell you which is which.
The last part matters more than it looks. Build the pipeline on one line, print something, then apply it on the next. Notice that the additions and multiplications wait. You have written a description of some work, not the work, and that gap is the entire idea behind Section 11.3.
What your program must do
- Predict both compositions before running, then check
- Chain three functions and predict that too
- Build a pipeline on one line and apply it on another, and say when the work happened
- Do the same for and, or and negate on the two predicates
import java.util.function.*;
public class Chain {
public static void main(String[] args) {
Function<Integer, Integer> addTwo = x -> x + 2;
Function<Integer, Integer> triple = x -> x * 3;
// TODO: predict both BEFORE running, and write your predictions down
// System.out.println(addTwo.andThen(triple).apply(2));
// System.out.println(addTwo.compose(triple).apply(2));
// TODO: chain three together and predict again
// TODO: build the pipeline on one line and apply it on another.
// Print something in between. When does the arithmetic happen?
Predicate<String> isLong = s -> s.length() > 4;
Predicate<String> hasA = s -> s.contains("a");
// TODO: and, or, negate. Predict each before running
}
}
Hint 1
andThen runs left to right, in the order you read it. compose runs right to left.Hint 2
addTwo.andThen(triple).apply(2) is (2 + 2) then times 3. addTwo.compose(triple).apply(2) is (2 times 3) then plus 2. Two different answers, no error either way.Hint 3almost the answer
andThen returns a new function that remembers the other two, and no arithmetic happens until apply. Put a print between the two lines and you will see it.Measure the Boxing
ex-11-2-dOne addition, fifty million times, two ways.
The arithmetic is identical. Everything you measure is the difference between doing it on an int and doing it on an Integer, and the gap is large enough that you will not need a careful benchmark to see it.
Run it three times before drawing a conclusion. The boxed number moving in one direction across the rounds is telling you something specific, and working out what is more interesting than the first measurement.
What your program must do
- Time fifty million calls to each and record both numbers
- Run the whole comparison three times and say what happens to the boxed timing across rounds
- Work out how many Integer objects the boxed version made
- Name the primitive versions of Predicate and Function, and say when you would reach for them
import java.util.function.*;
public class Boxing {
public static void main(String[] args) {
int n = 50_000_000;
Function<Integer, Integer> boxed = x -> x + 1;
IntUnaryOperator prim = x -> x + 1;
// TODO: time n calls to each. Run the whole thing three times.
// TODO: print both timings each round and watch what happens to the boxed one
// TODO: work out how many objects the boxed version created
// TODO: find the primitive version of Predicate and of Function, and name them
}
}
Hint 1
Function<Integer, Integer> cannot hold an int, because generics only work with objects. So every call boxes the input and unboxes the answer.Hint 2
Hint 3almost the answer
IntPredicate and IntFunction, plus ToIntFunction when the int is the result. Reach for them on a hot loop over numbers, which is exactly what Section 11.6 does with IntStream.After the credits
Something in this section was quietly important, and it was not the colons.
Function<Integer, Integer> pipeline = addTwo.andThen(triple);Building the pipeline did no work. The additions and multiplications waited until apply was called. You described the work first and ran it later.
Section 11.3 does that to loops. A stream describes what should happen to a collection and does none of it until something asks for an answer:
names.stream()
.filter(n -> n.length() > 4)
.map(String::toUpperCase)
.toList();filter and map are Predicate and Function, the two you learned here. String::toUpperCase is a method reference. Nothing in that code is new except the word stream, and the surprising part is when the work happens.
Threads you opened in this section
- Method referenceRunnable and Callable both take one, which is how a task gets handed to a pool.Phase XIV. Concurrency
- Function, Predicate, Supplier, Consumerfilter takes a Predicate, map takes a Function. Streams are built out of these four.Phase XI. Functional Java
- Function, Predicate, Supplier, ConsumerOptional.orElseGet takes a Supplier, so the fallback is only built if it is needed.11.5 - Optional, and the Habit It Is Trying to Break
Method reference will return in Phase XI. Functional Java