Target Typing and Functional Interfaces
Before You Start
Check each box you can do from memory. A box you cannot check yet is not a problem; it points you to a quick refresher, not a grade.
Not sure? Take the 60-second self-check.
1. Abstract vs. default methods. An interface has one abstract method and three default methods. How many abstract methods does it have?
Check
One. Default methods have concrete implementations; they do not count toward the abstract-method total. The interface has one abstract method.
2. Functional interface. What is a functional interface?
Check
An interface with exactly one abstract method. Lambdas can be used wherever the target type is a functional interface. Comparator<T> qualifies: its single abstract method is compare(T, T).
3. Lambda shape. The expression (a, b) -> a.getLastName().compareTo(b.getLastName()) takes two parameters and returns int. What interface does it match for a Player[] sort?
Check
Comparator<Player>, whose required method is int compare(Player a, Player b). The lambda’s shape matches.
Try This First
What type does the expression (a, b) -> a - b have on its own, with no surrounding context?
Write your answer before expanding.
Check
No type. A lambda expression has no inherent type. It is “shape-only”: two parameters, a body that returns a value. The type is assigned only by the context in which the lambda is used (the target type). On its own, the expression cannot be assigned to var or Object.
What You Need To Walk In With
The Insight: A lambda has no inherent type. It is a shape: a list of parameters and a body. The type is imposed from outside, by the context where the lambda appears. That context supplies a functional interface whose single abstract method must match the lambda’s shape. The same lambda expression can be a Comparator<Integer> in one context, a BiFunction<Integer,Integer,Integer> in another, and a BinaryOperator<Integer> in a third.
When the type feels confusing, come back to one rule: the declared type on the left side of the assignment (or the method’s parameter type) is always the source of truth for what interface a lambda satisfies. The lambda body only has to fit the shape of that interface’s single abstract method.
You can: identify the target type for a lambda at a given call site, count a given interface’s abstract methods (excluding defaults and Object overrides), explain why assigning a lambda to var or Object is a compile error, and predict whether a custom interface qualifies as functional.
How It Works
What target typing means
When the compiler sees Arrays.sort(players, (a, b) -> a.getTeam().compareTo(b.getTeam())), it performs three steps:
- Finds the target type. The second parameter of
Arrays.sort(T[], Comparator<? super T>)requires aComparator<Player>(becauseplayersisPlayer[]). That is the target type. - Verifies the lambda’s shape. The single abstract method of
Comparator<Player>isint compare(Player a, Player b): two parameters, returnsint. The lambda takes two parameters and returnsint. The shapes match. - Compiles the lambda. The lambda is compiled as if it were an anonymous class implementing
Comparator<Player>with the given body.
The lambda itself carries no type; the Comparator<Player> is assigned by the call site.
What a functional interface is
JLS §9.8 defines a functional interface as an interface with exactly one abstract method (the “Single Abstract Method” or SAM). Default methods and static methods do not count.
Comparator<T> is declared with @FunctionalInterface in the JDK source:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2); // the SAM
// ... many default and static methods
boolean equals(Object obj); // override of Object.equals; does NOT count
}
The equals override does not count toward the abstract-method total because it is an override of a method already declared in Object (JLS §9.8 rule). The default methods (reversed, thenComparing, etc.) are concrete and do not count. The one abstract method is compare.
One lambda, multiple types
The same lambda expression can satisfy multiple different functional interfaces:
// All three accept (a, b) -> Integer.compare(a, b)
Comparator<Integer> cmp1 = (a, b) -> Integer.compare(a, b);
BiFunction<Integer,Integer,Integer> f = (a, b) -> Integer.compare(a, b);
BinaryOperator<Integer> bo = (a, b) -> Integer.compare(a, b);
The lambda is identical in all three cases. The type is determined by the declared variable type on the left, not by anything in the lambda itself.
Quick check
Check your understanding
Which side of an assignment determines the functional-interface type assigned to a lambda?
The @FunctionalInterface annotation
Applying @FunctionalInterface to an interface instructs the compiler to verify that it has exactly one abstract method. If the count is wrong, the compiler reports an error. This is how you protect a custom interface against accidental addition of a second abstract method.
Worked Example: Predict Then Check
For each of the following, state whether it compiles and explain why:
// A
Comparator<Player> c1 = (a, b) -> a.getLastName().compareTo(b.getLastName());
// B
Object c2 = (a, b) -> a - b;
// C
var c3 = (a, b) -> a - b;
// D
Comparator<Integer> c4 = (a, b) -> a - b;
Step-by-step reasoning
A: Target type is Comparator<Player>. Lambda shape is two parameters returning int. Match. Compiles.
B: Target type is Object, which is not a functional interface. The compiler cannot assign a lambda to Object. Does not compile.
C: var infers the type from the initializer. The lambda has no type without a target. var cannot resolve the type. Does not compile. (JLS §14.4.1.)
D: Target type is Comparator<Integer>. Shape matches. Compiles. (Note: the subtraction a - b has an overflow risk, but it compiles. The overflow is covered in The Subtraction Trick and Why It Overflows.)
Show answers
| Compiles? | Reason | |
|---|---|---|
| A | Yes | Comparator<Player> is a functional interface; lambda shape matches compare(Player, Player) |
| B | No | Object is not a functional interface |
| C | No | var cannot infer the type of a free-floating lambda (JLS §14.4.1) |
| D | Yes | Comparator<Integer> is a functional interface; shape matches |
Common Misconceptions
Misconception 1: assigning a lambda to var or Object
Wrong mental model: “A lambda is a value, so I can store it in any variable, including
varorObject.”
Why it breaks:
var cmp = (a, b) -> a - b; // COMPILE ERROR
Object o = (a, b) -> a - b; // COMPILE ERROR
A lambda has no type without a functional-interface target. var infers from the initializer’s type; Object is not a functional interface. Both fail.
How to correct: Declare the variable with a concrete functional-interface type:
Comparator<Integer> cmp = (a, b) -> Integer.compare(a, b);
Source: JLS §15.27.3; JLS §14.4.1.
Quick check
Check your understanding
A student writes: var cmp = (a, b) -> a - b; Why does this fail to compile?
Misconception 2: assuming any single-method interface is functional
Wrong mental model: “One method on the interface means it’s a functional interface.”
Why it breaks: MouseListener has five abstract methods, all of which must be implemented. Writing MouseListener ml = (e) -> ...; fails because the lambda body provides only one method, not five.
Also subtle: an interface that inherits abstract methods from a parent interface counts those in the abstract-method total. Only interfaces that end up with exactly one abstract method after inheritance qualify.
How to correct: Check whether the interface is annotated with @FunctionalInterface (the compiler enforces the count) or verify the count manually. Standard functional interfaces in java.util.function are all @FunctionalInterface-annotated.
Source: JLS §9.8; Bloch, Effective Java, Item 44.
Formal Definition and Interface Contract
From JLS §9.8 (Functional Interfaces):
A functional interface is an interface that has just one abstract method (aside from the methods of
Object), and thus represents a single function contract.
From JLS §15.27.3 (Type of a Lambda Expression):
A lambda expression is compatible with its target type
TifTis a functional interface type and the lambda is congruent with the function type ofT. A lambda expression’s type is determined from the target type.
From the Java 25 Comparator API:
@FunctionalInterfacepublic interface Comparator<T>
The @FunctionalInterface annotation in the JDK source confirms Comparator<T> is intended to be used with lambdas.
Mental Model
Think of a lambda as a stencil: a shape with slots for inputs and a slot for an output. The stencil itself has no label. The label (the functional interface type) is written on the table where you place the stencil. Place the stencil on a Comparator<Player> table and the slots are Player a, Player b, int. Place the same stencil on a BiFunction<Integer,Integer,Integer> table and the slots are Integer a, Integer b, Integer. Same stencil, different table, different type.
Connections
Within CSCD 210/211: Default methods on interfaces are covered in a later topic. This is the first place you need to know that default methods do not count toward the abstract-method total.
Looking back: The anonymous-class form required writing the full class body. Target typing allows the lambda to drop the class declaration, the @Override, and the method signature, because the compiler derives all of that from the target type.
Looking ahead: Method references further compress lambdas by naming an existing method instead of writing a lambda body. The Comparator.comparing factory uses target typing pervasively; it accepts a lambda or method reference as a key extractor.
Go Deeper (optional)
None of this is needed to write correct code, so read it when you are curious, not when you are rushing.
Target typing and the Streams API. The place where target typing shows up most in professional Java code is not sorting arrays; it is the Streams API. Every call to map, filter, sorted, reduce, and collect accepts a lambda, and the type of that lambda is inferred from the stream’s element type combined with the method’s parameter type. Once you understand target typing, cryptic red underlines in stream pipelines become diagnosable mismatches rather than mysteries. This is the reason the concept is worth understanding precisely.
Choosing among the standard functional interfaces. Writing a custom functional interface is rarely necessary. The java.util.function package supplies a complete family: Function<T,R> (one input, one output), Predicate<T> (one input, boolean output), Consumer<T> (one input, no output), Supplier<T> (no input, one output), and BiFunction<T,U,R> (two inputs). Bloch, Effective Java, Item 44, explains the rule: prefer a standard interface when one fits your shape, because it gives callers a type name they already know and integrates automatically with the rest of the Streams API. Write a custom @FunctionalInterface only when none of the standard ones fits or when you need a distinct named contract with its own documentation.
The theory behind the feature. Java’s target typing is a deliberately limited cousin of the Hindley-Milner type inference used in ML and Haskell. In those languages, the type of an anonymous function is deduced from its use across the whole program, threading inference through chains of calls. Java restricts inference to a single declared target type, so the compiler never has to follow a chain: the type is always right there at the call site. That restriction keeps type-error messages short and readable, at the cost of not being able to assign a lambda to var. The tradeoff was intentional: a teaching-friendly and tooling-friendly system rather than a maximally expressive one.
Practice
Level 1
State whether each of the following is legal, and explain why:
// (a)
Runnable r = () -> System.out.println("hi");
// (b)
Object o = () -> System.out.println("hi");
// (c)
Comparator<String> c = (a, b) -> a.compareTo(b);
Show answer
(a) Legal. Runnable is a functional interface with one abstract method: void run(). The lambda body matches the shape (no parameters, returns void).
(b) Illegal. Object is not a functional interface. The compiler cannot assign a lambda to Object.
(c) Legal. Comparator<String> is a functional interface. The lambda has two parameters and returns int via String.compareTo. Shapes match.
Level 2
Which of the following interfaces can accept a lambda? Explain for each:
Runnable(one abstract method:void run())Callable<V>(one abstract method:V call() throws Exception)MouseListener(five abstract methods)Iterable<T>(one abstract method:Iterator<T> iterator())
Show answer
Runnable: Yes. Exactly one abstract method.Callable<V>: Yes. Exactly one abstract method. (Lambdas can be used for checked-exception SAMs too, but the lambda body must declare or handle the checked exception.)MouseListener: No. Five abstract methods; a lambda can only provide one body.Iterable<T>: Yes. Exactly one abstract method (iterator()).
Level 3
Explain in 3-4 sentences why Comparator<T> qualifies as a functional interface even though it declares default methods like reversed() and thenComparing(), and an equals(Object) override. Then write a lambda that provides the compare implementation for ordering String objects by length.
Show answer
Comparator<T> has exactly one abstract method: int compare(T o1, T o2). The default methods (reversed, thenComparing, etc.) are concrete; they have bodies and do not need to be implemented by any class or lambda that uses Comparator<T>. The equals(Object) override is an override of a method already declared in Object; per JLS §9.8, such overrides do not count toward the abstract-method total. Therefore Comparator<T> meets the definition of a functional interface and is annotated with @FunctionalInterface.
Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());
Check Yourself
Close the notes and answer each one from memory, then reveal it. Pulling an idea back from memory is one of the strongest ways to make it stick.
Check your understanding
A lambda expression on its own, with no surrounding context, has which of the following?
Which of the following is the correct definition of a functional interface under JLS 9.8?
Comparator<T> declares many default methods (reversed, thenComparing, etc.) and an equals(Object) override. Why does it still qualify as a functional interface?
Predict which of the following statements compile without error: (X) var c = (a, b) -> a - b; (Y) Object o = (a, b) -> a - b; (Z) Comparator<Integer> cmp = (a, b) -> a - b;
The same lambda expression (a, b) -> Integer.compare(a, b) is assigned to three different variables: one declared as Comparator<Integer>, one as BiFunction<Integer,Integer,Integer>, and one as BinaryOperator<Integer>. What does this demonstrate about target typing?