11.1
Lambdas and Functional Interfaces
You want to tell a method how to do a job. Java only lets you pass variables, so for twenty years you wrapped the job in a class. This is the phase where you stop.
Previously on
In Section 10.8 you wrote Comparator.comparing(Unit::name) and moved on.
You have done this three times now without being told what it is. removeIf(u -> u.id() == 103) in Section 10.1. merge(w, 1, Integer::sum) in Section 10.6. That arrow has been in your code since Phase X, quietly working.
This section is the explanation you were promised. Almost nothing here is new. It is functional interfaces from Section 8.5 and anonymous classes from Section 7.4, with the noise taken out.
The problem
You have a list of students and you want it sorted.
Collections.sort(students);Java refuses. It does not know what “sorted” means for a Student, and it will not guess. For an int the order is obvious. For a class you invented, only you know.
So you tell it. In Section 10.8 you did that with Comparable:
class Student implements Comparable<Student> {
public int compareTo(Student other) {
return Integer.compare(marks, other.marks);
}
}That works, once. Now the real program arrives.
A college system has teachers, office staff and a reports page. The teacher wants students in marks order. The office wants them by name. The reports page wants roll numbers. Three orders, one class, and compareTo is a single method.
So the order has to move out of the class. That is Comparator, and Java gives you an interface with one method for the job:
public interface Comparator<T> {
int compare(T o1, T o2);
}Now write the three. Each one is a class:
class SortByName implements Comparator<Student> {
public int compare(Student a, Student b) {
return a.name.compareTo(b.name);
}
}
class SortByMarks implements Comparator<Student> {
public int compare(Student a, Student b) {
return Integer.compare(a.marks, b.marks);
}
}
class SortByRollNumber implements Comparator<Student> {
public int compare(Student a, Student b) {
return Integer.compare(a.rollNumber, b.rollNumber);
}
}Then build one and hand it over:
Comparator<Student> byName = new SortByName();
Collections.sort(students, byName);Stop and look at what you actually said. The whole instruction is one line:
a.name.compareTo(b.name)Everything else is packaging. Three files, three class names, three method signatures, three objects, so that one line could reach sort.
The reason is a rule you have never questioned. A method takes variables. It cannot take another method. So a piece of behaviour cannot travel on its own: it has to be wrapped in an object first, and the object gets passed instead.
Anonymous classes make it shorter, not different. Section 7.4 gave you a way to skip the file:
Collections.sort(students, new Comparator<Student>() {
@Override
public int compare(Student a, Student b) {
return Integer.compare(a.marks, b.marks);
}
});The three separate files are gone. Five lines of ceremony still surround one line of meaning. You are still building a class, still naming a method, still making an object. It is the same trip to the kitchen with fewer forms to fill in.
The idea
What you want is to hand over the line itself.
Collections.sort(students, (a, b) -> Integer.compare(a.marks, b.marks));This is a lambda. Read the arrow as “maps to”: these inputs map to this answer.
The shape is always the same.
(parameters) -> what to do with themCompare it against what it replaced, side by side:
| Anonymous class | Lambda | |
|---|---|---|
| What you write | new Comparator<Student>() { public int compare(Student a, Student b) { ... } } | (a, b) -> ... |
| Name the interface | yes | no |
| Name the method | yes | no |
| Write the types | yes | no |
| Say the word return | yes | not for a single expression |
Every one of those “no” answers is something Java can already work out, and the reason it can is the shape of the interface.
Comparator has one abstract method. So when a Comparator is expected and you write a lambda, there is nothing to decide. The method must be compare. The parameters must be two Student values, because the variable is a Comparator<Student>. The return must be an int.
An interface with exactly one abstract method is called a functional interface. You met the name in Section 8.5. This is what it was for.
Default methods do not break it. A default method already has a body, so it is not waiting to be written. Comparator has many of them, reversed and thenComparing among others, and it is still a functional interface because only compare is abstract.
The four shapes a lambda comes in:
// no parameters
() -> System.out.println("done")
// one parameter, brackets optional
s -> s.length()
(s) -> s.length()
// several parameters
(a, b) -> a + b
// a block, when one expression is not enough. return comes back
(a, b) -> {
int diff = a.marks - b.marks;
return diff != 0 ? diff : a.name.compareTo(b.name);
}Types are optional too, because Java can see them from the interface:
(Student a, Student b) -> ... // allowed
(a, b) -> ... // usualUnder the hood
Going deeperA lambda is not a shorter way to write an anonymous class. The two are different things once compiled, and you can see the difference without any tools beyond the JDK.
Take one file holding both:
Comparator<S> viaAnon = new Comparator<S>() {
public int compare(S x, S y) { return Integer.compare(x.marks(), y.marks()); }
};
Comparator<S> viaLambda = (x, y) -> Integer.compare(x.marks(), y.marks());Compile it and look at what is on disk:
Anon.class
Anon$1.class <- the anonymous class
Anon$S.classThe anonymous class became a file. The lambda did not. There is no Anon$2.class, and there never will be.
Ask each one what it is at run time:
anon class : Anon$1
lambda class : Anon$$Lambda/0x0000007001000400The anonymous class has the name you expected. The lambda’s class has a name with an address in it, because that class was built while the program was running. It did not exist when you compiled.
The bytecode says it plainly. Here are the two lines, from javap -c:
0: new #7 // class Anon$1
3: dup
4: invokespecial #9 // Method Anon$1."<init>":()V
7: astore_1
8: invokedynamic #10, 0 // InvokeDynamic #0:compare:()Ljava/util/Comparator;
13: astore_2The anonymous class is new, then a constructor call. Ordinary object creation, exactly as in Section 6.2, with nothing unusual about it.
The lambda is one instruction. invokedynamic means: I need something that fits here, work out what and make it. No class is named. No constructor runs. The decision is left until the line is reached for the first time.
What happens the first time a lambda runs
- The instruction is reachedThe JVM sees an
invokedynamicit has not resolved before, and stops to work it out. - A helper is calledA method in the JDK called
LambdaMetafactoryis asked to supply something matchingComparator. - A class is built in memoryOne small class is generated, implementing the interface, its one method calling the code you wrote. It never touches the disk.
- The result is rememberedThe instruction is rewritten to point straight at what was made. Every later pass costs nothing, which is why a lambda in a loop is not slow.
Where does your code go? Into a private method on the class that holds the lambda, with a name like lambda$main$0. The generated class just calls it. Your logic is compiled once, in the ordinary way, and the only thing built at run time is the small wrapper that makes it fit the interface.
The capture rule, and why it exists. A lambda can read local variables from around it, but only ones that never change after being set:
int limit = 50;
list.removeIf(s -> s.marks < limit); // fine
int limit = 50;
limit = 60;
list.removeIf(s -> s.marks < limit); // will not compileThe source rule is the same one from Section 7.4. A lambda may capture a local variable only when it is final or effectively final. The lambda can outlive the invocation described by the call-stack model, so Java gives captured locals value semantics instead of shared mutable-local semantics. The JVM remains free to implement that capture in any behaviour-preserving way.
What it costs
Lambdas remove a lot of typing. Four things come back the other way.
A stack trace stops being a set of names. An anonymous class shows up as SortByMarks.compare, which tells you where to look. A lambda shows up like this:
at Main.lambda$main$0(Main.java:14)
at java.base/java.util.TimSort.countRunAndMakeAscending(TimSort.java:355)lambda$main$0 is a generated name. The file and line are right, and the name tells you nothing. With three lambdas in one method you get $0, $1 and $2, and you count arrows to work out which is which.
Nothing has a name you can search for. SortByMarks can be found across a whole project in one search, and the name says what it does. (a, b) -> Integer.compare(a.marks, b.marks) written in eleven places is eleven copies nobody will ever find together.
Short does not always mean clear. This is a lambda:
list.sort((a, b) -> a.getDept().getName().compareTo(b.getDept().getName()) != 0
? a.getDept().getName().compareTo(b.getDept().getName())
: Integer.compare(b.getSalary(), a.getSalary()));It fits on three lines and takes a minute to read. The anonymous class version is longer and easier, because it has room for a name and a comment. Section 11.2 gives you a better answer than either.
The first call is not free. The class is built the first time the line runs. So the first pass does real work: calling the metafactory, generating a class, loading it. Everything after that is a normal method call. For a lambda that runs once at startup you paid a cost for nothing. For anything in a loop it disappears immediately.
There is a real gain hiding in the same place. Ten anonymous classes are ten class files that all have to be read from disk and loaded when your program starts. Ten lambdas are zero files, and each one costs only if it is actually reached.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
What makes an interface a functional interface, and why does the rule have to be that strict?
Show the answer
It has exactly one abstract method. Default methods do not count, however many there are, because a default method already has a body.
The rule has to be that strict because of what a lambda leaves out. When you write
(a, b) -> a.marks() - b.marks()you never say which method you are writing. Java has to work that out, and it can only do that when there is one method to choose.Comparatorhascompareand nothing else abstract, so there is no question. An interface with two abstract methods could not be a lambda, because your code would not say which of the two you meant.An anonymous class and a lambda do the same job here. What is different about the two on disk?
Show the answer
The anonymous class becomes a real file. Compile a class that holds one and you get
Outer$1.classsitting next toOuter.class. Ten anonymous classes means ten extra files.The lambda produces no file at all. Nothing is written for it, and no class with that shape exists until the line actually runs.
You can see it in the bytecode. The anonymous class compiles to
new Outer$1followed by a constructor call, which is an ordinary object being built. The lambda compiles to one instruction,invokedynamic, which says "make me something that fits here" and leaves the rest until run time.Your class needs to be sorted three different ways by three different parts of the program. Why is `Comparable` the wrong tool?
Show the answer
Because
Comparableputs the order inside the class, and a class can only hold onecompareTo.That is right when there is a single obvious order, like an id. It is wrong the moment the teacher wants marks, the office wants names, and the report wants roll numbers. You cannot write three
compareTomethods, and picking one of the three leaves the other two stuck.Comparatormoves the order out. The order becomes something you pass in at the moment you sort, so the class stays light and each caller brings its own answer.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises85 pointsabout 90 minutes
Three Orders, One Class
ex-11-1-aWrite the same job three ways and watch it shrink.
The point is not that the lambda is shortest. The point is which parts disappeared. Go back through the named class and mark every line that says something about how to compare. There is one. Everything above and below it exists so that one line could be handed to sort.
Then answer the last question honestly. If all three orders could live inside Student, Comparator would not need to exist.
What your program must do
- Write all three orders, one with a named class, one anonymous, one as a lambda
- Print the list after each sort so you can see it change
- Count the lines each version took and write the three numbers down
- Say why none of these three could have been a second compareTo
import java.util.*;
public class ThreeOrders {
record Student(String name, int rollNumber, int marks) { }
public static void main(String[] args) {
List<Student> students = new ArrayList<>(List.of(
new Student("Aditya", 101, 85),
new Student("Rohit", 102, 89),
new Student("Rohan", 103, 93),
new Student("Sonu", 104, 78)));
// TODO: sort by name, print, using a named class that implements Comparator
// TODO: sort by marks, print, using an anonymous class
// TODO: sort by roll number, print, using a lambda
// TODO: count the lines each one took
}
}
Hint 1
implements Comparator<Student> and overrides compare. The anonymous version is the same body with new Comparator<Student>() { ... } written where the object is needed.Hint 2
(a, b) -> Integer.compare(a.rollNumber(), b.rollNumber()). Everything else was packaging.Hint 3almost the answer
compareTo, so Comparable can express one order and no more. These are three orders wanted by three different parts of a program, so none of them belongs inside the class.Count the Class Files
ex-11-1-bCompile this and then look at your folder, before you run anything.
There are two comparators in the file and only one of them left a mark. Work out which, and then add one more of each and compile again to check you were right.
The class name printed at run time is the other half of the evidence. One of them is a name a compiler chose. The other has an address in it, which is what a name looks like when the class did not exist until a moment ago.
What your program must do
- Compile it and list the class files in the folder. Note which one has no file
- Print the class name of each at run time and compare the two
- Add a second anonymous class and a second lambda, compile again, and see which count changed
- Explain what the address in the lambda's class name is telling you
import java.util.*;
public class Counting {
record S(String name, int marks) { }
public static void main(String[] args) {
Comparator<S> viaAnon = new Comparator<S>() {
public int compare(S x, S y) { return Integer.compare(x.marks(), y.marks()); }
};
Comparator<S> viaLambda = (x, y) -> Integer.compare(x.marks(), y.marks());
// TODO: print getClass().getName() for both. Read the two names carefully.
// TODO: check both with instanceof Comparator
}
}
Hint 1
javac Counting.java, look at the folder. You should see Counting.class, Counting$S.class and Counting$1.class. Work out which of the two comparators the third file belongs to.Hint 2
Counting$1. The lambda calls itself something ending in a long hexadecimal number, because that class was built while the program was running.Hint 3almost the answer
Counting$2.class. Add a second lambda and the file count does not move. Lambdas are not written down at compile time at all.Make Something a Lambda Cannot Be
ex-11-1-cBreak it on purpose, then read what the compiler says.
The error you get for two abstract methods is worth reading slowly, because it names both of them. That is not the compiler being noisy. That is the compiler showing you the exact reason it cannot proceed: your lambda did not say which one it was.
Finish with @FunctionalInterface. It does nothing to your program. What it does is decide who finds out about the mistake, and that turns out to matter a lot on a large codebase.
What your program must do
- Add a second abstract method and record the exact error you get
- Undo it, add a default method instead, and confirm the lambda still compiles
- Add a static method as well and confirm that is also fine
- Put @FunctionalInterface on the interface and try breaking it again
public class OneMethod {
interface Job {
void run(String input);
}
// TODO: add a second abstract method to Job. Try to keep the lambda below. Read the error.
// TODO: undo that, and add a DEFAULT method instead. Does the lambda still work?
// TODO: add a static method too. Does that break it?
public static void main(String[] args) {
Job j = input -> System.out.println("doing " + input);
j.run("the washing");
}
}
Hint 1
Hint 2
Comparator has several and is still a functional interface.Hint 3almost the answer
@FunctionalInterface changes nothing about how the code runs. It moves the error from the person writing the lambda to the person breaking the interface, which is much closer to the mistake.The Variable That Will Not Sit Still
ex-11-1-dThe first part works and the second part does not, and the difference is worth sitting with.
makeGreeter returns a lambda that reads a local variable. By the time anyone calls get(), that method has already returned and its stack frame is gone. The lambda still answers. So the value cannot have been read from the stack when it was needed. It must have been copied in when the lambda was made.
Now the rule writes itself. A copy taken once is only safe if the original can never change afterwards, because a copy that quietly goes stale is worse than no copy at all.
The array trick at the end is not a loophole. Work out what is actually staying still, and you will see it obeys the same rule the whole way through.
What your program must do
- Return a lambda from a method that uses a local variable, and explain why that is allowed
- Show a lambda failing to compile once the variable is reassigned
- Get the changing value in anyway, using a one element array or a field
- Say why the array trick works when the plain variable does not
import java.util.*;
import java.util.function.*;
public class Capture {
static Supplier<String> makeGreeter() {
String greeting = "hello";
// TODO: return a lambda that uses greeting. This works. Why?
return null;
}
public static void main(String[] args) {
System.out.println(makeGreeter().get());
int limit = 50;
List<Integer> marks = new ArrayList<>(List.of(30, 60, 90));
// TODO: a lambda using limit. Fine.
// TODO: now assign limit = 60 somewhere and watch this stop compiling
// TODO: get the same effect anyway, using an array or a field
}
}
Hint 1
Supplier<String> has one method, get(), taking nothing and returning a value. So the lambda is () -> greeting.Hint 2
final. It only has to never be assigned twice. Java calls that effectively final, and the rule is the same one anonymous classes have followed since Section 7.4.Hint 3almost the answer
int[] limit = {50}; then use limit[0]. The captured variable never changes: it keeps the same reference value. A slot inside the referenced array can still change. The effective-final rule applies to the captured local variable, not to the state of the referenced object.After the credits
Look at this one carefully:
list.sort((a, b) -> a.name().compareTo(b.name()));The lambda takes two things and does nothing with them except pass them straight to a method that already exists. The arrow is carrying no work at all.
Section 11.2 removes it:
list.sort(Comparator.comparing(Student::name));Student::name is a method reference. It is not a new idea. It is the same lambda with the last piece of ceremony gone. That is what turns a comparator into something you build out of parts rather than write.
The same section answers the question that has been sitting under this one. Comparator is one functional interface. Java ships forty more, and four of them cover almost everything you will ever pass: something that tests, something that transforms, something that supplies, something that consumes.
Threads you opened in this section
- LambdaEvery stream operation takes one. This is what they were built for.11.3 - Streams, and When the Work Happens
- LambdaRunnable has one method, so a whole thread body fits in a lambda.14.1 - What a Thread Actually Is
Lambda will return in Phase XI. Functional Java