Back to Lambda Expressions
Q2Question 2 of 5

What is target typing in lambdas?

Key Concepts
  • A lambda has no intrinsic type: the compiler infers its functional interface type from the target context.
  • Four target contexts determine the type: variable assignment, method parameter, return statement, and explicit cast.
  • The same lambda () -> "RELIANCE" can be Supplier<String>, Callable<String>, or any compatible functional interface.
  • Ambiguity occurs when an overloaded method accepts two functional interfaces with the same lambda-compatible signature.
  • The compiler reports 'reference to method is ambiguous' and the fix is an explicit cast or a typed variable.
  • Best practice is to avoid overloading methods with different functional interface parameters that share the same lambda shape.
  • Supplier.get() throws no checked exception while Callable.call() declares throws Exception, but a non-throwing lambda matches both.

Definition

A lambda has no type of its own: the compiler determines its functional interface type from the target context (variable, parameter, return, or cast), and ambiguity arises when two overloaded parameters share the same lambda shape.
DIAGRAM
- **No intrinsic type**: a lambda expression like `() -> "RELIANCE"` is not inherently a Supplier or a Callable. The compiler decides based on where the lambda appears.
- **Variable context**: `Supplier<String> s = () -> "RELIANCE"` makes the lambda a Supplier.
- **Parameter context**: `execute(() -> "RELIANCE")` uses the method parameter type to infer.
- **Return context**: `return () -> "RELIANCE"` uses the method return type.
- **Cast context**: `(Callable<String>) () -> "RELIANCE"` forces a specific type.
- **Ambiguity**: if `process(Supplier<String>)` and `process(Callable<String>)` are both defined, passing `() -> "RELIANCE"` fails because both match.

A lambda is like a blank cheque at National Bank of India with the amount filled in but the payee line empty. The payee (type) is determined by where Rajesh presents the cheque: at the savings counter it becomes a Supplier, at the loan counter it becomes a Callable. The same cheque works at any counter. But if two counters are side by side with identical forms (overloaded methods), the teller cannot decide which counter Rajesh wants. He must point explicitly (cast).

Understanding the Concepts

What is target typing in lambda expressions?

Target typing means the compiler determines the lambda's functional interface type from the context in which it appears. The lambda itself has no type.

How do the four target contexts work?

JAVA
package com.bank.core;

import java.util.function.Supplier;
import java.util.concurrent.Callable;

public class TargetTypingDemo {
    public static void main(String[] args) {
        // Context 1: variable assignment
        Supplier<String> supplier = () -> "RELIANCE";
        Callable<String> callable = () -> "RELIANCE";
        // Same lambda, different types!

        System.out.println("Supplier: " + supplier.get());
        System.out.println("Callable: " + callable.call());
    }

    // Context 2: method parameter
    static void processSupplier(Supplier<String> s) {
        System.out.println("Supplier: " + s.get());
    }
    static void processCallable(Callable<String> c) throws Exception {
        System.out.println("Callable: " + c.call());
    }

    // Context 3: return type
    static Supplier<String> createSupplier() {
        return () -> "RELIANCE"; // inferred as Supplier
    }
}
TEXT
Supplier: RELIANCE
Callable: RELIANCE

Why does overloading cause ambiguity?

When two overloaded methods accept different functional interfaces with the same lambda-compatible signature, the compiler cannot choose.

JAVA
package com.bank.core;

import java.util.function.Supplier;
import java.util.concurrent.Callable;

public class AmbiguityDemo {
    // Two overloads with compatible lambda signatures
    static void process(Supplier<String> s) {
        System.out.println("Supplier: " + s.get());
    }
    static void process(Callable<String> c) throws Exception {
        System.out.println("Callable: " + c.call());
    }

    public static void main(String[] args) throws Exception {
        // WRONG: compile error! "reference to process is ambiguous"
        // process(() -> "RELIANCE");

        // CORRECT: explicit cast resolves ambiguity
        process((Supplier<String>) () -> "RELIANCE");
        process((Callable<String>) () -> "RELIANCE");

        // CORRECT: typed variable
        Supplier<String> sup = () -> "RELIANCE";
        process(sup);
    }
}
TEXT
Supplier: RELIANCE
Callable: RELIANCE
Supplier: RELIANCE

How is Supplier different from Callable?

| Aspect | Supplier | Callable |
|--------|-------------|-------------|
| Method | T get() | T call() throws Exception |
| Checked exception | No | Yes (throws Exception) |
| Package | java.util.function | java.util.concurrent |
| Use case | Lazy value, factory | Task that may throw |

A lambda () -> "x" matches both because it throws no checked exception. A lambda () -> { throw new IOException(); } matches only Callable because Supplier does not allow checked exceptions.

What are the rules and constraints?

The compiler uses the target type to validate the lambda body. If the target is Predicate, the lambda must take one String and return boolean. If the target is Function, the lambda must take one String and return Integer. Mismatched shapes produce compile errors.

What are common mistakes?

JAVA
package com.bank.core;

public class TargetTypingMistakes {
    // WRONG: avoid overloading with same-shape FIs
    // static void execute(Supplier<String> s) { }
    // static void execute(Callable<String> c) { }

    // CORRECT: use different method names
    static void executeSync(java.util.function.Supplier<String> s) {
        System.out.println(s.get());
    }
    static void executeAsync(java.util.concurrent.Callable<String> c) throws Exception {
        System.out.println(c.call());
    }

    public static void main(String[] args) throws Exception {
        executeSync(() -> "RELIANCE");  // no ambiguity
        executeAsync(() -> "RELIANCE"); // no ambiguity
    }
}

When should you use explicit casts?

Use explicit casts only when you must work with existing overloaded APIs that you cannot change. For new APIs, use different method names to eliminate ambiguity entirely.

What should a senior engineer know?

Target typing extends beyond lambdas to method references and conditional expressions. The ternary operator condition ? supplier : callable also requires target typing resolution. Understanding target typing helps debug cryptic compiler errors in complex generic method chains.

Visual Overview

DIAGRAM
  Lambda: () -> "RELIANCE"
       │
       │  no intrinsic type
       ▼
  ┌──────────────────────────────────────────┐
  │  Target Context determines type:          │
  │                                           │
  │  Variable:  Supplier<String> s = ...      │
  │  Parameter: process(Supplier<String>)     │
  │  Return:    Supplier<String> method()     │
  │  Cast:      (Supplier<String>) ...        │
  └──────────────────────────────────────────┘
       │
       │  Ambiguity when 2 overloads match:
       │  process(Supplier) vs process(Callable)
       ▼
  Fix: explicit cast or typed variable

Key Takeaway

A lambda expression has no intrinsic type. The compiler determines its functional interface type from the target context: the variable it is assigned to, the method parameter it is passed to, the return type of the enclosing method, or an explicit cast. The same lambda () -> "RELIANCE" can be a Supplier, a Callable, or any functional interface with a compatible signature, depending purely on where it appears.

The ambiguity problem occurs when a method is overloaded with two different functional interfaces that have the same lambda-compatible signature. Both Supplier.get and Callable.call accept no arguments and return a value, so a lambda () -> result matches both. The compiler cannot choose and reports the error. The fix is either an explicit cast or assigning the lambda to a typed variable first.

The best practice for new APIs is to avoid overloading methods with different functional interface parameters that share the same lambda shape. Use different method names instead: processSync for Supplier and processAsync for Callable. This eliminates ambiguity entirely and makes the code self-documenting.

Remember one sentence: a lambda has no type of its own; the compiler infers it from context, and overloaded methods with same-shape functional interfaces cause ambiguity.

---

Share
Use ← → arrow keys to navigate