---
title: 'Functional Interfaces'
source: 'https://academia.sh/en/courses/java-standard-library/functional-interfaces'
course: 'The Standard Library and Streams'
language: en
updated: '2026-08-17T18:09:42+00:00'
license: 'CC BY-SA 4.0'
---

# Functional Interfaces

Single abstract method is a compile-time promise: a two-method interface can never be a lambda target, and the marker only catches a definition that breaks this promise early. The interface's second promise — behavior — gets checked nowhere: the type promise gets compiled in all four members of the family, while the purity and no-side-effects promise can break silently in Predicate and Function.

The previous topic's last lesson moved the ordering rule to an
externally supplied object: a comparator was a single-method type the
caller attached to the collection. The library called it, it never made
the decision. This lesson looks at that object itself — a type with a
single abstract method, a **functional interface** — measures what it
tells the compiler, and which of its promises no one checks.

Every line of the standard library is a promise, and one of three
sources gives that promise: the **interface** itself, the chosen
**implementation**, or a rule the **caller** follows. Functional
interfaces are this trio's narrowest example — they carry only one
abstract method — and it is exactly this narrowness that lets them be
the target of lambda expressions. Does the compiler hold this
narrowness, or does it only sit in documentation? The interface carries
a second promise too: how the method is supposed to **behave**. Where
does that promise sit, who checks it?

## Single Abstract Method Is a Compile-Time Promise

- **FJ1** — Four container forms get tried: a single-method and a
  two-method interface, each both marked and unmarked with
  `@FunctionalInterface`. The source texts get written within the
  lesson.
- **FJ2** — Whether an interface's definition compiles gets measured
  separately from whether a class using that interface as a lambda
  target compiles; the two do not get mixed.
- **FJ3** — The compiler's error message text does not get printed;
  only whether it compiles gets reported, because what gets measured
  is not the message's content, it is whether the gate is open.

```java
// Compile.java - shared core: does the given source text compile
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.file.*;
import java.util.spi.ToolProvider;

class Compile {
    static boolean compiles(String source, String className) throws Exception {
        Path dir = Files.createTempDirectory("scale");
        Path file = dir.resolve(className + ".java");
        Files.writeString(file, source);
        PrintWriter silent = new PrintWriter(Writer.nullWriter());
        return ToolProvider.findFirst("javac").orElseThrow()
                .run(silent, silent, "-d", dir.toString(), file.toString()) == 0;
    }
}
```

```java
// Marker.java - what the @FunctionalInterface marker catches
public class Marker {
    static final String SINGLE = "interface Operation { int apply(int x); }";
    static final String SINGLE_MARKED = "@FunctionalInterface\ninterface Operation { int apply(int x); }";
    static final String TWO = "interface Operation { int apply(int x); int revert(int x); }";
    static final String TWO_MARKED = "@FunctionalInterface\ninterface Operation { int apply(int x); int revert(int x); }";

    static final String[][] ATTEMPTS = {
        {"single method, unmarked", SINGLE},
        {"single method, marked", SINGLE_MARKED},
        {"two methods, unmarked", TWO},
        {"two methods, marked", TWO_MARKED},
    };

    public static void main(String[] args) throws Exception {
        System.out.printf("%-24s %-20s %s%n", "form", "definition compiles", "can be a lambda target");
        for (String[] d : ATTEMPTS) {
            String label = d[0], body = d[1];
            boolean definition = Compile.compiles(body, "Operation");
            boolean lambda = definition && Compile.compiles(
                    body + "\nclass Use { Operation i = x -> x; }", "Operation");
            System.out.printf("%-24s %-20s %s%n", label,
                    definition ? "compiled" : "did not compile",
                    !definition ? "-" : (lambda ? "compiled" : "did not compile"));
        }
    }
}
```

```
form                     definition compiles  can be a lambda target
single method, unmarked  compiled             compiled
single method, marked    compiled             compiled
two methods, unmarked    compiled             did not compile
two methods, marked      did not compile      -
```

Only one of the four rows falls at the definition stage: the
two-method, marked interface never compiles at all, because the marker
tells the compiler "this interface should have exactly one abstract
method," and the compiler checks that. The other three rows'
definitions compile — an unmarked two-method interface is valid as an
ordinary interface too; Java does not forbid an interface from having
more than one abstract method.

The real distinction is in the second column. Both single-method
interfaces — marked or not — can be a lambda target. The two-method,
unmarked interface, even though its definition passes, **cannot** be a
lambda target: the expression `x -> x` does not say which method it
corresponds to, and the compiler cannot resolve it. What the marker
does and does not do splits apart right here.

## The Marker Adds No Guarantee, It Catches Early

The table confirms a single conclusion: an interface being able to be
a lambda target is a **structural** property — carrying exactly one
abstract method — and this property exists or does not, independent of
the marker. The two-method, unmarked interface compiles as a
definition too, but it was never functional to begin with; no one just
said so, until someone tries to use it with a lambda. The error at
that moment comes up far from the line where the interface got
defined, separately at every point it gets used.

The marker closes this distance. The two-method, marked interface
falls **at compile time** — without waiting for use, at the definition
itself. The promise the marker gives is not "this interface is
functional," it is "tell me here if this interface stops being
functional." The **interface** was already giving the guarantee
through its own structure; the marker only moves the moment that
guarantee breaks from the use site to the **definition site**. Types
like `Runnable`, `Comparator`, `Predicate` in the standard library
carry this marker, but even if you removed the marker, they would
still be single-abstract-method interfaces and would still be lambda
targets — the marker does not make them functional, it only catches
their breakage early.

The Object-Oriented Java course counted four things the compiler
rejects: access modifiers, `final`, checked exceptions, type
parameters. The single-abstract-method rule is not a fifth protection
added to this list, it is the **same** protection carried into the
standard library — here too, the compiler refuses a structure and
reports it at the definition line. But this protection's scope is
narrow: it looks only at the interface's **shape**, at how many methods
there are. It never looks at what the promise behind the shape is —
and this distinction shows up in the measurement below.

The shape itself carries less than it might seem to. In the
Object-Oriented Java course, a class implementing an interface got
built **by name**: the interface's name appears in the `implements`
line. A lambda expression never writes this name at all; the compiler
looks not at the target's name, only at its **shape**.

```java
// Structural.java - can the same lambda be the target of two interfaces unaware of each other
public class Structural {
    static final String TWO_INTERFACES = """
        interface Multiplier { int apply(int x); }
        interface Converter { int apply(int x); }
        class Use {
            Multiplier m = x -> x * 3;
            Converter c = x -> x * 3;
        }
        """;

    public static void main(String[] args) throws Exception {
        System.out.println("two separate interfaces unaware of each other, same-bodied lambda: "
                + (Compile.compiles(TWO_INTERFACES, "Multiplier") ? "compiled" : "did not compile"));
    }
}
```

```
two separate interfaces unaware of each other, same-bodied lambda: compiled
```

`Multiplier` and `Converter` do not know each other at all; there is no
inheritance between them, no shared supertype — both only carry one
abstract method of the same shape. The same lambda expression compiles
for both. This differs from the behavior of a class implementing an
interface: there, the name is binding; here, the shape is enough. The
promise the compiler checks is exactly this narrow: not name, not
lineage, only parameter count, parameter types, and return type.

## Default, Static, and Object Methods Do Not Count

The single-abstract-method rule does not say "single method," it says
"single **abstract** method." An interface can carry methods with a
default body (`default`) or static methods; these do not count as
abstract and do not enter the count — because the caller does not have
to implement them, the library has already written them.

- **FJ4** — Two containers get tried: one adds a default and a static
  method next to an abstract method; the other redeclares two of
  Object's own methods (`equals`, `toString`) as abstract next to an
  abstract method. Both are marked with `@FunctionalInterface`.
- **FJ5** — For both containers, whether the definition compiles and
  whether it can be a lambda target get measured separately.

```java
// DoesNotCount.java - default, static, and Object methods do not enter the abstract count
public class DoesNotCount {
    static final String MULTI_MEMBER = """
        @FunctionalInterface
        interface Operation {
            int apply(int x);
            default int doubled(int x) { return apply(x) * 2; }
            static Operation identity() { return x -> x; }
        }
        """;

    static final String OBJECT_SIGNED = """
        @FunctionalInterface
        interface Operation {
            int apply(int x);
            boolean equals(Object o);
            String toString();
        }
        """;

    public static void main(String[] args) throws Exception {
        System.out.println("one abstract + two default/static methods : "
                + (Compile.compiles(MULTI_MEMBER, "Operation") ? "compiled" : "did not compile"));
        System.out.println("one abstract + two of Object's methods    : "
                + (Compile.compiles(OBJECT_SIGNED, "Operation") ? "compiled" : "did not compile"));

        boolean lambdaMultiMember = Compile.compiles(
                MULTI_MEMBER + "\nclass K { Operation i = x -> x; }", "Operation");
        boolean lambdaObjectSigned = Compile.compiles(
                OBJECT_SIGNED + "\nclass K { Operation i = x -> x; }", "Operation");
        System.out.println("first can be a lambda target               : "
                + (lambdaMultiMember ? "compiled" : "did not compile"));
        System.out.println("second can be a lambda target              : "
                + (lambdaObjectSigned ? "compiled" : "did not compile"));
    }
}
```

```
one abstract + two default/static methods : compiled
one abstract + two of Object's methods    : compiled
first can be a lambda target               : compiled
second can be a lambda target              : compiled
```

All four lines compile. The first container gives an expected result:
`doubled` and `identity` are not abstract, they have bodies, they do
not enter the count — the interface is still single-abstract-method,
and the marker raises no objection. The second container is less
intuitive: even though `equals` and `toString` get redeclared as
abstract, the compiler does not count them, because both match a
**public** method of the `Object` class, and every implementation
already provides these methods indirectly by inheriting from `Object`.
The rule does not see three abstract declarations, it sees that two of
the three declarations **already have a counterpart in `Object`**, and
counts only `apply`.

This measurement shows the single-abstract-method rule is a slightly
narrower rule than it sounds: what gets counted is not "the count of
abstract declarations in the interface," it is "the count of methods
every implementation would have to **rewrite**." `Object`'s methods
already exist on every class, so they do not enter the rewriting
obligation. A familiar example from the previous topic gets clarified
here: `Comparator<T>` carries around a dozen `default` and `static`
methods (`reversed`, `thenComparing`, `naturalOrder`) and is still
functional, because the only place the caller has to rewrite stays
`compare`. Member count and abstract method count are not the same
thing.

## The Interface's Behavior Promise Is Checked Nowhere

A functional interface carries two separate promises. The first is
the **signature** — how many parameters, of what type, what it returns
— and this promise gets held at compile time, as measured in the
tables above. The second is **behavior**: `Predicate` expects "give
the same result for the same input," `Function` expects "create no
side effect." Which layer does this second promise sit in?

- **FJ6** — Four members of the standard family get taken as
  examples: `Predicate`, `Function`, `Supplier`, `Consumer`. The rest
  of the family follows the same pattern.
- **FJ7** — A purity violation gets made visible with a call counter,
  a side-effect violation with an accumulator; both are helper state
  written only for this measurement.

```java
// Meaning.java - the interface's type promise and behavior promise are not checked in the same place
import java.util.function.*;

public class Meaning {
    static int callCount = 0;

    public static void main(String[] args) {
        Predicate<Integer> p = x -> x > 0;
        Function<Integer, Integer> f = x -> x * 2;
        Supplier<Integer> s = () -> 42;
        Consumer<Integer> c = x -> { };
        System.out.println("all four type promises matched, all four compiled");
        System.out.println();

        Predicate<Integer> unstable = x -> { callCount++; return callCount == 1; };
        System.out.printf("same input, first call   : %s%n", unstable.test(5));
        System.out.printf("same input, second call  : %s%n", unstable.test(5));

        StringBuilder log = new StringBuilder();
        Function<Integer, Integer> impure = x -> { log.append(x).append(','); return x + 1; };
        impure.apply(1);
        impure.apply(2);
        System.out.printf("did it accumulate a side effect : %s%n", !log.isEmpty());
    }
}
```

```
all four type promises matched, all four compiled

same input, first call   : true
same input, second call  : false
did it accumulate a side effect : true
```

The first line confirms once more where the type promise sits: all
four variables matched their interface's signature, the compiler
checked this, and all four compiled. The remaining three lines test
the second promise. A `Predicate` named `unstable` gives two separate
results on two separate calls with the same input (**5**) — first
`true`, then `false` — and the compiler never got involved, and
runtime never raised an exception either. A `Function` named `impure`
writes to a `StringBuilder` outside its own scope every time it gets
called; this too got blocked neither while compiling nor while
running.

The consequence is **silent**: `Predicate.test` does not throw an
exception because it fails to "give the same result for the same
input," it runs a misbehaving implementation as it is, unchanged. The
family's type promise got confirmed in the first line above: all four
members matched their own signature, all four compiled. The behavior
promise, though, is not distributed evenly across the family. For
`Predicate` and `Function`, purity and no-side-effects are real
documented promises, and both just got broken, both stayed silent —
the compiler did not intervene, runtime raised no exception. For
`Supplier`, the documentation is even looser: there is **no
requirement at all** about producing the same or a different value on
each call, so there is not even a promise to break there. `Consumer`,
on the other hand, is built the opposite way — its purpose is a side
effect to begin with, the documentation does not count that as a
defect, does not treat it as a promise. The one general rule that fits
the table: the compiler keeps the type promise complete in all four
members of the family; the behavior promise is not uniform across the
family, and even where it holds, no layer checks it.

This repeats an observation built in the previous lesson (Comparison
and Sorting), but on a different surface: there, a non-transitive
comparator was measured producing a silently wrong order on a small
input. Here, the same silence shows up on a surface much smaller and
much more frequently used than sorting — a **one-line lambda
expression**. However narrow a functional interface is, that
narrowness does not turn into a behavior guarantee.

## Summary

- An interface being able to be a lambda target is a structural
  condition: exactly one abstract method. This condition gets held at
  compile time, independent of the marker.
- The `@FunctionalInterface` marker adds no guarantee; it only catches
  this structural condition's violation early, by moving it from the
  use site to the definition site.
- A two-method, unmarked interface compiles too, but cannot be a
  lambda target — not being functional holds without the marker too,
  it just gets noticed later.
- Functional interfaces carry a second promise: behavior (purity, no
  side effects). This promise gets checked neither by the compiler nor
  at runtime; its violation is silent, it raises no exception.
- The standard family's type promise sits with the compiler in all
  four members; the behavior promise is not uniform across the
  family — it can break in `Predicate` and `Function` and stays
  silent when it does, and for `Supplier` such a promise does not even
  exist in the documentation.

## Next Step

This lesson looked at the functional interface itself: where single
abstract method gets held, where the second promise does not. When a
lambda expression gets written, at that type's site it does one more
thing: it **captures** the names around it. The next lesson measures
what gets captured when it is written at that site — is capturing a
local variable bound to the same constraint as capturing a field, and
do the two produce the same result?
