Definition
- **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?
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
}
}
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.
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);
}
}
Supplier: RELIANCE
Callable: RELIANCE
Supplier: RELIANCE
How is Supplier different from Callable?
| Aspect | Supplier
|--------|-------------|-------------|
| 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?
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
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.
---