skip to content

Some languages model a callback as a one-method interface, while others have first-class function types. Compare the two encodings and explain what each one makes possible that the other does not.

level: seniorimportance: should knowfreq 45%

answer

  1. nominal = named single-method type; structural = shaped function type
  2. Java: no function type, lambda takes the target SAM type
  3. same shape, different names -> incompatible in Java, fine in Kotlin
  4. C# delegates are nominal but multicast
  5. name carries semantics: ordering, retry policy, comparison

basics

~20 s

A single-abstract-method interface is a nominal function type: it has a name, documentation, extra default helpers and overload identity, but two identically shaped ones are incompatible. Real function types — Kotlin's (A)->B, Go's func types, Swift closures — are interchangeable by shape but anonymous.

solid answer

~50 s

Both encode "a piece of behaviour passed as a value"; they differ in whether the type is **named or shaped**. - **Java** has no first-class function type. A lambda gets its type from the single-abstract-method interface expected at that position, so `Runnable` and a hand-written `interface Task { void run(); }` are incompatible despite identical shape, and you re-wrap with `r::run`. It also forces specialised variants such as `IntPredicate` to avoid boxing. - **Kotlin** has structural function types `(A) -> B`; `fun interface` exists mainly so a lambda can convert to a Java-facing SAM, and the conversion is explicit. - **C#** delegates are nominal too — `Action` and `Func` are named delegate types — but they are **multicast**, which no SAM interface is. - **Go**'s `func(T) error` is a structural named type, and a one-method interface is a different tool you pick when the receiver's identity or state matters. Use the nominal form when the **name** carries semantics — an ordering contract, not just a shape.

code

java · 5 lines
java
interface Task { void run(); }

Runnable r = () -> System.out.println("go");
// Task t = r;          // does not compile: unrelated nominal types
Task t = r::run;        // must re-wrap

go deeper

for a junior

Know that a lambda in Java becomes an instance of the expected single-method interface, and that other languages have real function types instead.

for a middle

Explain why identically shaped SAM types are incompatible, and name a language where the equivalent code needs no conversion.

for a senior

Choose the encoding per API: named type when the contract's meaning exceeds its signature, function type when the shape is the contract, and know the boxing and combinator consequences.

for a principal

Decide the vocabulary an API publishes across language boundaries, given that a nominal consumer cannot accept a structural producer without a wrapper at the edge.

## Two encodings of the same idea "Pass behaviour as a value" can be typed in two ways. **Nominal (single-abstract-method type).** You declare a named type with exactly one abstract operation, and a value of that type is a piece of behaviour. Java's functional interfaces work this way, as do C# delegates. The type has an identity beyond its shape: a name, documentation, possibly extra default helper methods, and the ability to participate in overload resolution. **Structural (function type).** The language has a type constructor for functions — Kotlin's `(A) -> B`, Go's `func(A) B`, Swift's `(A) -> B`, TypeScript's `(a: A) => B` — and any function with the right parameter and return types is a value of it. Nothing is declared. ## What the nominal encoding buys **Meaning.** A Java `Comparator<T>` is not just "a function from two Ts to an int". Its documentation carries a contract: total ordering, consistency, antisymmetry. Sorted collections rely on it. A bare function type has nowhere to hang that requirement, which is why even languages with real function types keep named single-method types for contracts whose semantics exceed their signature. **Extra members.** Because a functional interface is still an interface, it can carry default methods: `Comparator.thenComparing`, `Predicate.and`, `Function.andThen`. The combinators live on the type. In a structural function-type language they must be free functions or extension functions instead — Kotlin puts them in the standard library as extensions on function types. **Overload identity.** Two methods can accept differently named single-method types with the same shape and be distinguishable. With structural function types they collide. ## What the structural encoding buys **Interchangeability.** In Kotlin, a `(String) -> Int` produced anywhere is accepted anywhere that type is expected, with no adapter. In Java, a value of `interface Task { void run(); }` is not a `Runnable` even though the shapes are identical; you must re-wrap it as `r::run`. That friction is the whole reason `java.util.function` exists as a large fixed vocabulary that everyone is expected to reuse. **No boxing gymnastics from erasure.** Because Java's function-like types are ordinary generic interfaces and generics erase to references, `Predicate<Integer>` boxes; the JDK therefore ships hand-written specialisations `IntPredicate`, `LongFunction`, `DoubleUnaryOperator`, and so on. A language with a real function type still has to decide about specialisation, but the API surface does not fan out into dozens of near-duplicate named types. **Composition without ceremony.** Higher-order code — a function returning a function returning a function — types naturally, whereas nominal encodings need a named type per arity and shape. ## The middle ground and its seams Kotlin has both, and the seam is instructive. Function types are the native encoding; `fun interface` was added so a lambda can be converted to a Java-facing SAM type when calling into Java libraries. The conversion is explicit at the call site precisely because the two are not the same thing. C# sits in the nominal camp with a twist: `Action` and `Func<...>` are named delegate types, so nominally they are as rigid as Java's interfaces — but the language special-cases conversion between compatible delegate types, and delegates are **multicast**, meaning several handlers can be combined into one delegate value and invoked together. That capability has no equivalent in a SAM interface, which is why C# events are built on it. Go has both and keeps them separate on purpose: a `func(T) error` is used when the behaviour is all that matters, and a one-method interface is used when the receiver carries state or identity the caller may want to inspect or type-assert on. `http.HandlerFunc` is the bridge — a named function type with a method, so a plain function satisfies the `http.Handler` interface. ## Choosing - Use a **named single-method type** when the contract's meaning exceeds its signature (ordering, retry policy, comparison semantics), when you want combinators to hang off it, or when two same-shaped parameters must be distinguishable in overloads. - Use a **function type** when the shape is the whole contract, when values will be composed and passed through many layers, or when you are writing generic higher-order code. - In an API crossing language boundaries, remember that the nominal side cannot accept the structural side without a wrapper, so publish the named type and let callers convert once at the edge.

  • Why does Java's standard library contain IntPredicate as well as Predicate?
    Because functional interfaces are ordinary generic interfaces and Java generics erase to references, so `Predicate<Integer>` boxes every argument. The primitive specialisations exist to avoid that in hot code. It is a direct consequence of encoding functions as nominal generic types rather than having a first-class function type with primitive-aware instantiation.
  • What can a C# delegate do that a single-method interface cannot?
    Multicast. Several delegate values can be combined with += into one value that invokes all of them in order, which is the foundation of C# events. A one-method interface has exactly one implementation per value; combining handlers requires a composite object you write yourself, and the return value and exception semantics of the combination become your problem.
  • Go has function types, so why does it still define interfaces with a single method?
    Because an interface value carries a receiver, which may hold state, be inspected with a type assertion, or implement further interfaces. `http.Handler` is an interface for exactly that reason, and `http.HandlerFunc` is a named function type with a ServeHTTP method that adapts a plain function into it — the two encodings coexisting deliberately in one API.

saying these in an interview costs you the question

  • Claiming Java lambdas have their own type; they take the type of the single-method interface expected at that position.
  • Assuming two identically shaped single-method interfaces are assignable to each other.
  • Treating C# Func and Action as structural function types; they are named delegate types with special conversion rules.
  • Saying function types are strictly better, ignoring that contracts like ordering have no place to live on a bare shape.
  • Forgetting that combinators such as and/or/compose have to live somewhere — on the interface, or as free/extension functions.

context