You already met lambdas as compact stand-ins for anonymous inner classes, together with the rule that each one targets an interface with a single abstract method. That rule is small, but it is the hinge on which Java's modern style turns — because once a language commits to single-method interfaces, a whole vocabulary of tiny, composable types becomes possible.
Java ships that vocabulary ready-made, in the java.util.function package. Four interfaces do the lion's share of the work across the standard library: Function, Predicate, Consumer, and Supplier. Streams speak exclusively in these types, so reading any pipeline fluently means recognising each one on sight. This lesson makes that vocabulary concrete, then tackles the single behaviour that catches nearly everyone — the order in which composed functions run.
flowchart TD SRC["A functional interface has exactly one abstract method"] F["Function<br/>apply returns a transformed value"] --> FT["turns one input into one output"] P["Predicate<br/>test returns a boolean"] --> PT["answers a yes or no question"] C["Consumer<br/>accept returns nothing"] --> CT["uses a value, gives back void"] SUP["Supplier<br/>get takes nothing"] --> ST["hands back a value with no input"] style SRC fill:#1e293b,color:#fff
Each of those four is a functional interface defined by one abstract method. Function transforms a value through apply; Predicate tests one through test, which returns a boolean; Consumer acts on one through accept, returning void; Supplier produces one through get, taking no arguments. The method name is the contract — when a signature reads test, you already know a boolean is coming back.
A functional interface may still carry default and static methods freely; only the abstract method count matters. The @FunctionalInterface annotation is optional but worth adding to your own: it tells the compiler to reject any second abstract method, turning a silent slip into a build error. Nearly every interface in java.util.function carries it for exactly that reason.
import java.util.function.Function;
import java.util.function.Predicate;
public class Main {
public static void main(String[] args) {
Function<String, Integer> length = s -> s.length();
Predicate<String> isEmpty = s -> s.isEmpty();
System.out.println(length.apply("stream"));
System.out.println(isEmpty.test(""));
}
}
Consumer and Supplier sit at the edges where data enters or leaves your code. A Supplier takes no arguments and hands back a value — think of a factory method, or a lazy source that produces items on demand. A Consumer takes a value and returns nothing, so it is the natural shape for a side effect such as printing a line. When you meet forEach later in the Stream API, the action it runs on each element is a Consumer, and the thing that yields the next element can be a Supplier.
Knowing the shapes pays off the moment you read a method signature. See a parameter typed Consumer<String> and you know the method will eventually do something with each string and discard the result — no return value to track, no transformation to follow.
Target typing: a lambda takes its type from its context
A lambda has no standalone type. Its type is whatever functional interface the surrounding context expects — a mechanism called target typing — and that is why the identical expression x -> x + 1 can mean two genuinely different things.
Hand it to a Function<Integer, Integer> and the literal 1 is a number, so the lambda adds. Hand the same body to a Function<String, String> and Java now treats the incoming value as text, so the plus sign concatenates instead of adding. The lambda's text never changed; the target did, and the target chose the meaning.
That is also why Object o = x -> x + 1; will not compile. With no functional interface within reach, the compiler has nothing to measure the lambda against, and it refuses to guess. A lambda always needs a target.
flowchart LR L["the same body<br/>x becomes x plus 1"] --> FA["target is a numeric Function<br/>so the 1 is added"] L --> FB["target is a text Function<br/>so the 1 is appended"] style L fill:#3776ab,color:#fff
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
Function<Integer, Integer> addOne = x -> x + 1;
Function<String, String> tagOne = x -> x + 1;
System.out.println(addOne.apply(40));
System.out.println(tagOne.apply("score"));
}
}
Composition: wiring small functions into one
Functional interfaces are not just targets for single lambdas — several come with default methods that build new interfaces out of existing ones, with no boilerplate. Function offers andThen and compose, each returning a fresh Function that represents a two-step pipeline. Predicate offers and, or, and negate, which combine tests the way && and ! combine booleans.
Composition is what makes the functional style scale past toy examples. Instead of one large lambda that does three things, you wire three small ones together and hand the combined result to a stream. Each piece stays readable and testable on its own. The catch — and it is a real one — is that andThen and compose run their steps in opposite orders, and mixing them up changes the answer.
flowchart LR AT["f.andThen(g).apply(x)"] --> S1["runs f first<br/>then g"] CO["f.compose(g).apply(x)"] --> S2["runs g first<br/>then f"] style AT fill:#3776ab,color:#fff style CO fill:#b45309,color:#fff
What does this program print? Read it carefully and type the exact output (two lines).
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
Function<Integer, Integer> doubleIt = n -> n * 2;
System.out.println(doubleIt.apply(3));
System.out.println(doubleIt.apply(10));
}
}
doubleItis a Function whose body returnsn * 2.It is applied to
3and then to10, each on its own line.
A functional interface already has one abstract method. Which of these changes stops it being usable as a lambda target?
A functional interface must have exactly one abstract method. Default and static methods both provide a body, so they are not abstract and do not count — only a second abstract method breaks the contract.
The composition trap. timesTwo and plusThree are combined two different ways and applied to the same input. Predict both lines before you check.
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
Function<Integer, Integer> timesTwo = x -> x * 2;
Function<Integer, Integer> plusThree = x -> x + 3;
Function<Integer, Integer> a = timesTwo.andThen(plusThree);
Function<Integer, Integer> b = timesTwo.compose(plusThree);
System.out.println(a.apply(4));
System.out.println(b.apply(4));
}
}
andThenrunstimesTwofirst: 4 doubled is 8, then plus three is 11.composerunsplusThreefirst: 4 plus three is 7, then doubled is 14.
What happens if you write Object o = x -> x + 1; on its own, with no other context?
A lambda has no type of its own — it takes the type of its target. Object is not a functional interface, so the compiler has no single abstract method to match the lambda against, and it refuses the assignment rather than guessing.
Predicate composition. Two predicates are combined and tested. Predict all three lines.
import java.util.function.Predicate;
public class Main {
public static void main(String[] args) {
Predicate<Integer> even = n -> n % 2 == 0;
Predicate<Integer> positive = n -> n > 0;
Predicate<Integer> evenAndPositive = even.and(positive);
System.out.println(evenAndPositive.test(8));
System.out.println(evenAndPositive.test(-4));
System.out.println(even.negate().test(7));
}
}
8is even and positive, soeven.and(positive)is true.-4is even but not positive, so the combined test is false.even.negate()means 'not even';7is odd, so the result is true.
Recap
java.util.functionsupplies four workhorse interfaces:Functiontransforms viaapply,Predicatetests viatest,Consumerconsumes viaaccept,Supplierproduces viaget— each defined by a single abstract method.- A lambda has no type of its own; it takes the type of its target, so the same body can add numbers or join text depending on context.
@FunctionalInterfaceis optional but asks the compiler to refuse a second abstract method.Function.andThen(g)runsftheng;Function.compose(g)runsgthenf— opposite orders, easy to invert by mistake.Predicatecombines withand,or, andnegate.
Next you will put these interfaces to work inside the Stream API, where map takes a Function, filter takes a Predicate, and the whole pipeline runs lazily.