Java Interfaces, Functional Interfaces and Lambdas
Java interfaces are contracts that define what a class or lambda expression must provide. A normal interface can have multiple abstract methods. A functional interface has exactly one abstract method and can be implemented using a lambda expression. A marker interface has no methods and is used to classify or mark a type.
1. Normal Interface
A normal interface is useful when a service needs multiple related operations. For example, a payment service may need to process payments, issue refunds, and check payment status.
interface PaymentService {
void processPayment(double amount);
void refundPayment(String transactionId, double amount);
String getPaymentStatus(String transactionId);
}PaymentService has three abstract methods, so it is not a functional interface. A lambda cannot directly implement it because a lambda can represent only one abstract method.
Implementing a Normal Interface
class BankPaymentService implements PaymentService {
@Override
public void processPayment(double amount) {
System.out.println("Processing payment of ₹" + amount);
}
@Override
public void refundPayment(String transactionId, double amount) {
System.out.println(
"Refunding ₹" + amount +
" for transaction " + transactionId
);
}
@Override
public String getPaymentStatus(String transactionId) {
return "SUCCESS";
}
}Interface Reference and Actual Object
The interface itself does not create the object. The object is created from the implementing class. The variable can use the interface as its reference type.
PaymentService paymentService = new BankPaymentService();
paymentService.processPayment(5000.1);
System.out.println(
paymentService.getPaymentStatus("TXN-123")
);
paymentService.refundPayment("TXN-123", 1000);In PaymentService paymentService = new BankPaymentService(), PaymentService is the reference type and BankPaymentService is the actual object type. This is an example of polymorphism.
Polymorphism
An interface reference can point to an object of any class that implements that interface.
PaymentService paymentService = new BankPaymentService();
paymentService.processPayment(5000);The variable is declared as PaymentService, but the actual object is BankPaymentService. When processPayment is called, Java executes the implementation provided by BankPaymentService.
2. Anonymous Class Implementation
Before lambdas, a common way to provide an implementation of an interface without creating a named class was an anonymous class.
interface PaymentProcessor {
void processPayment(double amount);
}
PaymentProcessor processor2 = new PaymentProcessor() {
@Override
public void processPayment(double amount) {
System.out.println(
"Processing payment: " + amount
);
}
};
processor2.processPayment(455.51);The object is created using new PaymentProcessor() with an anonymous implementation. The method call must be placed after the closing brace and semicolon of the anonymous class.
Why This Code Is Wrong
PaymentProcessor processor2 = new PaymentProcessor() {
@Override
public void processPayment(double amount) {
System.out.println("Processing payment: " + amount);
}
processor2.processPayment(455.51); // WRONG
};processor2.processPayment(455.51) is a statement and cannot be placed directly inside the anonymous class body. It must be outside the anonymous class.
3. Functional Interface
A functional interface contains exactly one abstract method. It can therefore be implemented using a lambda expression.
@FunctionalInterface
interface PaymentHandler {
void processPayment(double amount);
}The interface has exactly one abstract method: processPayment(double amount). Therefore a lambda can provide its implementation.
Lambda Expression
PaymentHandler handler =
amount -> System.out.println(
"Processing payment: ₹" + amount
);
handler.processPayment(5000.1);The parameter amount comes from the single parameter of the functional interface method. The lambda provides the implementation of processPayment.
Lambda with Multiple Statements
A lambda without braces can contain a single expression. If the lambda needs multiple statements, curly braces are required.
PaymentHandler handler =
amount -> {
int transactionFee = 10;
System.out.println(
"Payment amount: ₹" + amount
);
System.out.println(
"Transaction fee: ₹" + transactionFee
);
};
handler.processPayment(5000.1);The variable transactionFee is a local variable inside the lambda. It is not a second lambda parameter.
Lambda Parameters
The number of lambda parameters must match the parameters of the functional interface's abstract method.
@FunctionalInterface
interface OrderHandler {
void handle(String orderId, double amount);
}
OrderHandler handler =
(orderId, amount) ->
System.out.println(
"Order " + orderId + " amount ₹" + amount
);
handler.handle("ORD-92831", 749.50);Because handle has two parameters, the lambda has two parameters: orderId and amount. Multiple lambda parameters are normally written inside parentheses.
4. Marker Interface
A marker interface contains no methods. It is used to mark or classify a class as having a particular property.
interface CriticalEvent {
// No methods
}The CriticalEvent interface does not define behavior. It simply identifies an event as a critical event.
Payment Failed Event
class PaymentFailedEvent implements CriticalEvent {
private final String transactionId;
private final String customerId;
private final double amount;
private final String reason;
public PaymentFailedEvent(
String transactionId,
String customerId,
double amount,
String reason) {
this.transactionId = transactionId;
this.customerId = customerId;
this.amount = amount;
this.reason = reason;
}
public String getTransactionId() {
return transactionId;
}
public String getCustomerId() {
return customerId;
}
public double getAmount() {
return amount;
}
public String getReason() {
return reason;
}
}Account Locked Event
class AccountLockedEvent implements CriticalEvent {
private final String customerId;
private final String reason;
public AccountLockedEvent(
String customerId,
String reason) {
this.customerId = customerId;
this.reason = reason;
}
public String getCustomerId() {
return customerId;
}
public String getReason() {
return reason;
}
}Both PaymentFailedEvent and AccountLockedEvent implement CriticalEvent. Therefore they can be treated as CriticalEvent objects.
5. Functional Interface with Generics
@FunctionalInterface
interface EventHandler<T extends CriticalEvent> {
void handle(T event);
}EventHandler is generic. T represents the event type. The bound T extends CriticalEvent means that the handler can work only with types that implement CriticalEvent.
Payment Event Handler
EventHandler<PaymentFailedEvent> paymentHandler =
event -> {
System.out.println(
"Payment failure detected"
);
System.out.println(
"Transaction: " +
event.getTransactionId()
);
System.out.println(
"Amount: ₹" +
event.getAmount()
);
System.out.println(
"Reason: " +
event.getReason()
);
};Because the handler is EventHandler<PaymentFailedEvent>, Java knows that event is a PaymentFailedEvent and therefore allows access to PaymentFailedEvent-specific methods.
6. Compile-Time Type Checking
Generics provide compile-time type safety. A handler for PaymentFailedEvent cannot accept an AccountLockedEvent.
PaymentFailedEvent paymentEvent =
new PaymentFailedEvent(
"TXN-98231",
"CUST-501",
75000,
"Bank declined transaction"
);
AccountLockedEvent accountEvent =
new AccountLockedEvent(
"CUST-501",
"Too many failed login attempts"
);
EventHandler<PaymentFailedEvent> paymentHandler =
event -> System.out.println(
event.getTransactionId()
);
paymentHandler.handle(paymentEvent); // Valid
// paymentHandler.handle(accountEvent); // Compile-time errorThe compiler catches the incorrect event-handler combination before the program runs. This is compile-time type checking.
7. Runtime Type Checking with instanceof
Sometimes an application receives an object through a common interface reference. The actual object type may then need to be checked at runtime.
class EventProcessor {
public void process(CriticalEvent event) {
if (event instanceof PaymentFailedEvent paymentEvent) {
System.out.println(
"PaymentFailedEvent detected"
);
System.out.println(
"Transaction: " +
paymentEvent.getTransactionId()
);
System.out.println(
"Amount: ₹" +
paymentEvent.getAmount()
);
} else if (event instanceof AccountLockedEvent accountEvent) {
System.out.println(
"AccountLockedEvent detected"
);
System.out.println(
"Customer: " +
accountEvent.getCustomerId()
);
} else {
System.out.println(
"Unknown critical event"
);
}
}
}The instanceof expression checks the actual object type at runtime. Pattern matching creates a correctly typed variable such as paymentEvent or accountEvent when the check succeeds.
Reference Type vs Actual Object Type
CriticalEvent event =
new PaymentFailedEvent(
"TXN-100",
"CUST-10",
25000,
"Bank declined"
);The reference type is CriticalEvent, but the actual object is PaymentFailedEvent. Therefore event instanceof PaymentFailedEvent returns true.
8. Event Handler Instead of Hard-Coded Processing
Instead of putting every action inside EventProcessor, we can pass behavior into the processor using a functional interface.
class PaymentEventProcessor {
public void process(
PaymentFailedEvent event,
EventHandler<PaymentFailedEvent> handler) {
handler.handle(event);
}
}PaymentEventProcessor does not need to know whether the handler sends an SMS, writes an audit log, creates a support ticket, or alerts an operations team. The behavior is supplied from outside.
Customer Notification Handler
EventHandler<PaymentFailedEvent> customerHandler =
event -> {
System.out.println(
"Sending SMS to customer: " +
event.getCustomerId()
);
System.out.println(
"Payment of ₹" +
event.getAmount() +
" failed."
);
};Operations Handler
EventHandler<PaymentFailedEvent> operationsHandler =
event -> {
System.out.println(
"ALERT: Operations team notified"
);
System.out.println(
"Transaction: " +
event.getTransactionId()
);
System.out.println(
"Reason: " +
event.getReason()
);
};Audit Handler
EventHandler<PaymentFailedEvent> auditHandler =
event -> {
System.out.println(
"[AUDIT] Payment failure recorded: " +
event.getTransactionId()
);
};Calling the Processor with Different Handlers
PaymentEventProcessor processor =
new PaymentEventProcessor();
processor.process(paymentEvent, customerHandler);
processor.process(paymentEvent, operationsHandler);
processor.process(paymentEvent, auditHandler);The same event can be processed using different behaviors. The processor does not contain the implementation details of those behaviors.
9. Event Bus
A more scalable design is to register multiple handlers and publish an event once. The event bus then invokes every registered handler.
import java.util.ArrayList;
import java.util.List;
class EventBus<T extends CriticalEvent> {
private final List<EventHandler<T>> handlers =
new ArrayList<>();
public void register(EventHandler<T> handler) {
handlers.add(handler);
}
public void publish(T event) {
for (EventHandler<T> handler : handlers) {
handler.handle(event);
}
}
}Registering Payment Handlers
EventBus<PaymentFailedEvent> paymentEventBus =
new EventBus<>();
paymentEventBus.register(customerHandler);
paymentEventBus.register(operationsHandler);
paymentEventBus.register(auditHandler);Publishing a Payment Event
paymentEventBus.publish(paymentEvent);One publish call causes all registered PaymentFailedEvent handlers to execute.
10. Different Event Type with Its Own Handlers
AccountLockedEvent accountEvent =
new AccountLockedEvent(
"CUST-501",
"Too many failed login attempts"
);
EventBus<AccountLockedEvent> accountEventBus =
new EventBus<>();
accountEventBus.register(
event -> {
System.out.println(
"Account locked: " +
event.getCustomerId()
);
System.out.println(
"Reason: " +
event.getReason()
);
}
);
accountEventBus.register(
event -> System.out.println(
"Sending security email to " +
event.getCustomerId()
)
);
accountEventBus.publish(accountEvent);EventBus<AccountLockedEvent> can accept only handlers designed for AccountLockedEvent. This gives compile-time type safety while allowing different behavior through lambdas.
11. Complete Real-World Example
import java.util.ArrayList;
import java.util.List;
// ============================================================
// MARKER INTERFACE
// ============================================================
interface CriticalEvent {
}
// ============================================================
// NORMAL INTERFACE
// ============================================================
interface PaymentService {
void processPayment(double amount);
void refundPayment(String transactionId, double amount);
String getPaymentStatus(String transactionId);
}
// ============================================================
// FUNCTIONAL INTERFACE
// ============================================================
@FunctionalInterface
interface EventHandler<T extends CriticalEvent> {
void handle(T event);
}
// ============================================================
// PAYMENT FAILED EVENT
// ============================================================
class PaymentFailedEvent implements CriticalEvent {
private final String transactionId;
private final String customerId;
private final double amount;
private final String reason;
public PaymentFailedEvent(
String transactionId,
String customerId,
double amount,
String reason) {
this.transactionId = transactionId;
this.customerId = customerId;
this.amount = amount;
this.reason = reason;
}
public String getTransactionId() {
return transactionId;
}
public String getCustomerId() {
return customerId;
}
public double getAmount() {
return amount;
}
public String getReason() {
return reason;
}
}
// ============================================================
// ACCOUNT LOCKED EVENT
// ============================================================
class AccountLockedEvent implements CriticalEvent {
private final String customerId;
private final String reason;
public AccountLockedEvent(
String customerId,
String reason) {
this.customerId = customerId;
this.reason = reason;
}
public String getCustomerId() {
return customerId;
}
public String getReason() {
return reason;
}
}
// ============================================================
// PAYMENT SERVICE IMPLEMENTATION
// ============================================================
class BankPaymentService implements PaymentService {
@Override
public void processPayment(double amount) {
System.out.println(
"Processing payment of ₹" + amount
);
}
@Override
public void refundPayment(
String transactionId,
double amount) {
System.out.println(
"Refunding ₹" + amount +
" for transaction " + transactionId
);
}
@Override
public String getPaymentStatus(
String transactionId) {
return "SUCCESS";
}
}
// ============================================================
// EVENT BUS
// ============================================================
class EventBus<T extends CriticalEvent> {
private final List<EventHandler<T>> handlers =
new ArrayList<>();
public void register(EventHandler<T> handler) {
handlers.add(handler);
}
public void publish(T event) {
for (EventHandler<T> handler : handlers) {
handler.handle(event);
}
}
}
// ============================================================
// RUNTIME TYPE PROCESSOR
// ============================================================
class EventProcessor {
public void process(CriticalEvent event) {
if (event instanceof PaymentFailedEvent paymentEvent) {
System.out.println(
"PaymentFailedEvent detected"
);
System.out.println(
"Transaction: " +
paymentEvent.getTransactionId()
);
System.out.println(
"Amount: ₹" +
paymentEvent.getAmount()
);
} else if (
event instanceof AccountLockedEvent accountEvent
) {
System.out.println(
"AccountLockedEvent detected"
);
System.out.println(
"Customer: " +
accountEvent.getCustomerId()
);
} else {
System.out.println(
"Unknown critical event"
);
}
}
}
// ============================================================
// MAIN
// ============================================================
public class Main {
public static void main(String[] args) {
// ----------------------------------------------------
// NORMAL INTERFACE + POLYMORPHISM
// ----------------------------------------------------
PaymentService paymentService =
new BankPaymentService();
paymentService.processPayment(5000.1);
System.out.println(
paymentService.getPaymentStatus("TXN-123")
);
paymentService.refundPayment(
"TXN-123",
1000
);
// ----------------------------------------------------
// CREATE PAYMENT FAILURE EVENT
// ----------------------------------------------------
PaymentFailedEvent paymentEvent =
new PaymentFailedEvent(
"TXN-98231",
"CUST-501",
75000,
"Bank declined transaction"
);
// ----------------------------------------------------
// CREATE ACCOUNT LOCK EVENT
// ----------------------------------------------------
AccountLockedEvent accountEvent =
new AccountLockedEvent(
"CUST-501",
"Too many failed login attempts"
);
// ----------------------------------------------------
// RUNTIME TYPE CHECKING
// ----------------------------------------------------
EventProcessor processor =
new EventProcessor();
processor.process(paymentEvent);
processor.process(accountEvent);
// ----------------------------------------------------
// FUNCTIONAL INTERFACE + LAMBDA
// ----------------------------------------------------
EventHandler<PaymentFailedEvent> customerHandler =
event -> {
System.out.println(
"Sending SMS to customer: " +
event.getCustomerId()
);
System.out.println(
"Payment of ₹" +
event.getAmount() +
" failed."
);
};
// ----------------------------------------------------
// OPERATIONS HANDLER
// ----------------------------------------------------
EventHandler<PaymentFailedEvent> operationsHandler =
event -> {
System.out.println(
"ALERT: Operations team notified"
);
System.out.println(
"Transaction: " +
event.getTransactionId()
);
System.out.println(
"Reason: " +
event.getReason()
);
};
// ----------------------------------------------------
// AUDIT HANDLER
// ----------------------------------------------------
EventHandler<PaymentFailedEvent> auditHandler =
event -> {
System.out.println(
"[AUDIT] Payment failure recorded: " +
event.getTransactionId()
);
};
// ----------------------------------------------------
// PAYMENT EVENT BUS
// ----------------------------------------------------
EventBus<PaymentFailedEvent> paymentBus =
new EventBus<>();
paymentBus.register(customerHandler);
paymentBus.register(operationsHandler);
paymentBus.register(auditHandler);
// ----------------------------------------------------
// PUBLISH PAYMENT EVENT
// ----------------------------------------------------
paymentBus.publish(paymentEvent);
// ----------------------------------------------------
// ACCOUNT EVENT BUS
// ----------------------------------------------------
EventBus<AccountLockedEvent> accountBus =
new EventBus<>();
accountBus.register(
event -> {
System.out.println(
"Account locked: " +
event.getCustomerId()
);
System.out.println(
"Reason: " +
event.getReason()
);
}
);
accountBus.register(
event -> System.out.println(
"Sending security email to " +
event.getCustomerId()
)
);
accountBus.publish(accountEvent);
}
}12. Complete Execution Flow
The payment system first uses PaymentService as a normal interface. BankPaymentService is the actual object. When a payment fails, a PaymentFailedEvent object is created. Because PaymentFailedEvent implements CriticalEvent, it can be treated as a critical event. EventProcessor can use instanceof to determine the actual event type at runtime. EventHandler is a functional interface, so lambdas can provide different behaviors. EventBus stores multiple EventHandler instances and invokes all matching handlers when an event is published.
13. Architecture
The overall design separates contracts, classification, behavior, event data, and event dispatching.
- PaymentService → normal interface defining payment operations.
- BankPaymentService → concrete implementation that creates the actual payment-service object.
- CriticalEvent → marker interface identifying critical events.
- PaymentFailedEvent → event containing failed-payment information.
- AccountLockedEvent → event containing account-lock information.
- EventHandler<T> → functional interface describing one event-handling operation.
- Lambda → supplies the actual handler behavior.
- EventBus<T> → stores and executes multiple type-safe handlers.
- EventProcessor → demonstrates runtime type checking with instanceof.
- Generics → prevent handlers from receiving the wrong event type.
14. Compile-Time vs Runtime Type Checking
- Compile-time type checking happens before the program runs and is demonstrated by EventHandler<PaymentFailedEvent> rejecting AccountLockedEvent.
- Runtime type checking happens while the program runs and is demonstrated by instanceof PaymentFailedEvent and instanceof AccountLockedEvent.
- Generics mainly provide compile-time type safety.
- instanceof is used when the program needs to determine an object's actual runtime type.
15. Marker Interface vs Functional Interface
- Marker interface → has zero abstract methods and is used for classification.
- Functional interface → has exactly one abstract method and is used to represent one behavior.
- Marker interface answers: What kind of object is this?
- Functional interface answers: What operation or behavior should be performed?
- A marker interface does not directly provide behavior.
- A functional interface can be implemented using a lambda expression.
16. Anonymous Class vs Lambda
// Anonymous class
PaymentProcessor processor1 = new PaymentProcessor() {
@Override
public void processPayment(double amount) {
System.out.println("Processing: " + amount);
}
};
// Lambda
PaymentProcessor processor2 =
amount -> System.out.println("Processing: " + amount);Both approaches can provide behavior for a functional interface. An anonymous class explicitly provides a class body, while a lambda provides the implementation of the single abstract method more concisely.
17. Important Rules to Remember
- You cannot instantiate an interface directly with new InterfaceName() when it has no implementation.
- An interface reference can point to an object created from a class that implements the interface.
- A lambda requires a functional interface as its target type.
- A functional interface must have exactly one abstract method.
- The number and compatible types of lambda parameters must match the functional interface method.
- Use braces in a lambda when the body contains multiple statements.
- A local variable declared inside a lambda is not a lambda parameter.
- Generics can connect a handler to a specific event type and provide compile-time safety.
- instanceof checks the actual object type at runtime.
- A marker interface contains no methods and is used for classification.
18. Real-World Use Case
This pattern resembles event-driven application design. A payment service produces events such as payment failures or account locks. Different parts of the application can subscribe to those events. One handler can notify the customer, another can alert operations, another can write an audit record, and another can perform fraud analysis. The event producer does not need to contain all of that behavior.
19. Event Flow
Payment fails → PaymentFailedEvent is created → Event is classified as CriticalEvent → EventBus receives the event → Registered EventHandler<PaymentFailedEvent> instances are invoked → Customer notification, operations alert, audit logging, and other actions are performed.
20. Interview Questions
- What is an interface in Java?
- Can we create an object directly from an interface?
- What is the difference between an interface reference and the actual object?
- What is polymorphism?
- What is a functional interface?
- Why can a lambda be assigned to a functional interface?
- What does @FunctionalInterface do?
- What happens if a functional interface contains two abstract methods?
- What is a marker interface?
- Why does a marker interface contain no methods?
- What is the difference between a marker interface and a functional interface?
- What is an anonymous class?
- What is the difference between an anonymous class and a lambda?
- Why are braces required when a lambda contains multiple statements?
- Does a local variable inside a lambda count as another lambda parameter?
- How does a lambda with two parameters work?
- Why must multiple lambda parameters be enclosed in parentheses?
- What are generics?
- How does EventHandler<PaymentFailedEvent> provide type safety?
- What happens if an AccountLockedEvent is passed to EventHandler<PaymentFailedEvent>?
- What is compile-time type checking?
- What is runtime type checking?
- How does instanceof perform runtime type checking?
- What is pattern matching with instanceof?
- Why use an EventHandler instead of hard-coding behavior inside EventProcessor?
- How can one event trigger multiple handlers?
- What is the purpose of EventBus<T>?
- Why does EventBus<T> use T extends CriticalEvent?
- How do normal interfaces, marker interfaces, functional interfaces, generics, and lambdas work together in this example?
21. One-Line Summary
Normal interfaces define service contracts, marker interfaces classify objects, functional interfaces define one behavior, lambdas provide that behavior, generics provide compile-time type safety, instanceof provides runtime type checking, and an event bus can connect events to multiple independent handlers.