OOP Advanced
Default Methods
Interface Evolution
You want to add a new method to an interface used by 100 classes. Without default methods, all 100 must be updated. Default methods let you add new methods with implementations - existing code keeps working.
Basic default method
Provide implementation in an interface.
// Basic Default Method Syntax
interface Greeting {
// Abstract method - MUST be implemented
String getName();
// Default method - has implementation
default void sayHello() {
System.out.println("Hello, " + getName() + "!");
}
default void sayGoodbye() {
System.out.println("Goodbye, " + getName() + "!");
}
}
// Implementing class only needs to implement abstract methods
class Person implements Greeting {
private String name;
Person(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
// sayHello() and sayGoodbye() inherited from interface!
}
public class BasicDefault {
public static void main(String[] args) {
System.out.println("=== Basic Default Methods ===\n");
String personName = ;
Person alice = new Person(personName);
// Calling abstract method (implemented)
System.out.println("Name: " + alice.getName());
// Calling default methods (inherited)
alice.sayHello();
alice.sayGoodbye();
System.out.println("\n=== Another Implementation ===");
// Anonymous class also gets defaults
String robotName = ;
Greeting robot = new Greeting() {
@Override
public String getName() {
return robotName;
}
};
robot.sayHello(); // Uses default
System.out.println("\n=== Why Default Methods? ===");
System.out.println("""
Before Java 8:
- Adding method to interface = breaking change
- All implementations had to be updated
With Default Methods:
- Can add methods with implementation
- Existing code continues to work
- Interface evolution without breaking changes
""");
}
}
// Basic Default Method Syntax
interface Greeting {
// Abstract method - MUST be implemented
String getName();
// Default method - has implementation
default void sayHello() {
System.out.println("Hello, " + getName() + "!");
}
default void sayGoodbye() {
System.out.println("Goodbye, " + getName() + "!");
}
}
// Implementing class only needs to implement abstract methods
class Person implements Greeting {
private String name;
Person(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
// sayHello() and sayGoodbye() inherited from interface!
}
public class BasicDefault {
public static void main(String[] args) {
System.out.println("=== Basic Default Methods ===\n");
String personName = ;
Person alice = new Person(personName);
// Calling abstract method (implemented)
System.out.println("Name: " + alice.getName());
// Calling default methods (inherited)
alice.sayHello();
alice.sayGoodbye();
System.out.println("\n=== Another Implementation ===");
// Anonymous class also gets defaults
String robotName = ;
Greeting robot = new Greeting() {
@Override
public String getName() {
return robotName;
}
};
robot.sayHello(); // Uses default
System.out.println("\n=== Why Default Methods? ===");
System.out.println("""
Before Java 8:
- Adding method to interface = breaking change
- All implementations had to be updated
With Default Methods:
- Can add methods with implementation
- Existing code continues to work
- Interface evolution without breaking changes
""");
}
}
// Basic Default Method Syntax
interface Greeting {
// Abstract method - MUST be implemented
String getName();
// Default method - has implementation
default void sayHello() {
System.out.println("Hello, " + getName() + "!");
}
default void sayGoodbye() {
System.out.println("Goodbye, " + getName() + "!");
}
}
// Implementing class only needs to implement abstract methods
class Person implements Greeting {
private String name;
Person(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
// sayHello() and sayGoodbye() inherited from interface!
}
public class BasicDefault {
public static void main(String[] args) {
System.out.println("=== Basic Default Methods ===\n");
String personName = ;
Person alice = new Person(personName);
// Calling abstract method (implemented)
System.out.println("Name: " + alice.getName());
// Calling default methods (inherited)
alice.sayHello();
alice.sayGoodbye();
System.out.println("\n=== Another Implementation ===");
// Anonymous class also gets defaults
String robotName = ;
Greeting robot = new Greeting() {
@Override
public String getName() {
return robotName;
}
};
robot.sayHello(); // Uses default
System.out.println("\n=== Why Default Methods? ===");
System.out.println("""
Before Java 8:
- Adding method to interface = breaking change
- All implementations had to be updated
With Default Methods:
- Can add methods with implementation
- Existing code continues to work
- Interface evolution without breaking changes
""");
}
}
// Basic Default Method Syntax
interface Greeting {
// Abstract method - MUST be implemented
String getName();
// Default method - has implementation
default void sayHello() {
System.out.println("Hello, " + getName() + "!");
}
default void sayGoodbye() {
System.out.println("Goodbye, " + getName() + "!");
}
}
// Implementing class only needs to implement abstract methods
class Person implements Greeting {
private String name;
Person(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
// sayHello() and sayGoodbye() inherited from interface!
}
public class BasicDefault {
public static void main(String[] args) {
System.out.println("=== Basic Default Methods ===\n");
String personName = ;
Person alice = new Person(personName);
// Calling abstract method (implemented)
System.out.println("Name: " + alice.getName());
// Calling default methods (inherited)
alice.sayHello();
alice.sayGoodbye();
System.out.println("\n=== Another Implementation ===");
// Anonymous class also gets defaults
String robotName = ;
Greeting robot = new Greeting() {
@Override
public String getName() {
return robotName;
}
};
robot.sayHello(); // Uses default
System.out.println("\n=== Why Default Methods? ===");
System.out.println("""
Before Java 8:
- Adding method to interface = breaking change
- All implementations had to be updated
With Default Methods:
- Can add methods with implementation
- Existing code continues to work
- Interface evolution without breaking changes
""");
}
}
// Basic Default Method Syntax
interface Greeting {
// Abstract method - MUST be implemented
String getName();
// Default method - has implementation
default void sayHello() {
System.out.println("Hello, " + getName() + "!");
}
default void sayGoodbye() {
System.out.println("Goodbye, " + getName() + "!");
}
}
// Implementing class only needs to implement abstract methods
class Person implements Greeting {
private String name;
Person(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
// sayHello() and sayGoodbye() inherited from interface!
}
public class BasicDefault {
public static void main(String[] args) {
System.out.println("=== Basic Default Methods ===\n");
String personName = ;
Person alice = new Person(personName);
// Calling abstract method (implemented)
System.out.println("Name: " + alice.getName());
// Calling default methods (inherited)
alice.sayHello();
alice.sayGoodbye();
System.out.println("\n=== Another Implementation ===");
// Anonymous class also gets defaults
String robotName = ;
Greeting robot = new Greeting() {
@Override
public String getName() {
return robotName;
}
};
robot.sayHello(); // Uses default
System.out.println("\n=== Why Default Methods? ===");
System.out.println("""
Before Java 8:
- Adding method to interface = breaking change
- All implementations had to be updated
With Default Methods:
- Can add methods with implementation
- Existing code continues to work
- Interface evolution without breaking changes
""");
}
}
// Basic Default Method Syntax
interface Greeting {
// Abstract method - MUST be implemented
String getName();
// Default method - has implementation
default void sayHello() {
System.out.println("Hello, " + getName() + "!");
}
default void sayGoodbye() {
System.out.println("Goodbye, " + getName() + "!");
}
}
// Implementing class only needs to implement abstract methods
class Person implements Greeting {
private String name;
Person(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
// sayHello() and sayGoodbye() inherited from interface!
}
public class BasicDefault {
public static void main(String[] args) {
System.out.println("=== Basic Default Methods ===\n");
String personName = ;
Person alice = new Person(personName);
// Calling abstract method (implemented)
System.out.println("Name: " + alice.getName());
// Calling default methods (inherited)
alice.sayHello();
alice.sayGoodbye();
System.out.println("\n=== Another Implementation ===");
// Anonymous class also gets defaults
String robotName = ;
Greeting robot = new Greeting() {
@Override
public String getName() {
return robotName;
}
};
robot.sayHello(); // Uses default
System.out.println("\n=== Why Default Methods? ===");
System.out.println("""
Before Java 8:
- Adding method to interface = breaking change
- All implementations had to be updated
With Default Methods:
- Can add methods with implementation
- Existing code continues to work
- Interface evolution without breaking changes
""");
}
}
// Basic Default Method Syntax
interface Greeting {
// Abstract method - MUST be implemented
String getName();
// Default method - has implementation
default void sayHello() {
System.out.println("Hello, " + getName() + "!");
}
default void sayGoodbye() {
System.out.println("Goodbye, " + getName() + "!");
}
}
// Implementing class only needs to implement abstract methods
class Person implements Greeting {
private String name;
Person(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
// sayHello() and sayGoodbye() inherited from interface!
}
public class BasicDefault {
public static void main(String[] args) {
System.out.println("=== Basic Default Methods ===\n");
String personName = ;
Person alice = new Person(personName);
// Calling abstract method (implemented)
System.out.println("Name: " + alice.getName());
// Calling default methods (inherited)
alice.sayHello();
alice.sayGoodbye();
System.out.println("\n=== Another Implementation ===");
// Anonymous class also gets defaults
String robotName = ;
Greeting robot = new Greeting() {
@Override
public String getName() {
return robotName;
}
};
robot.sayHello(); // Uses default
System.out.println("\n=== Why Default Methods? ===");
System.out.println("""
Before Java 8:
- Adding method to interface = breaking change
- All implementations had to be updated
With Default Methods:
- Can add methods with implementation
- Existing code continues to work
- Interface evolution without breaking changes
""");
}
}
// Basic Default Method Syntax
interface Greeting {
// Abstract method - MUST be implemented
String getName();
// Default method - has implementation
default void sayHello() {
System.out.println("Hello, " + getName() + "!");
}
default void sayGoodbye() {
System.out.println("Goodbye, " + getName() + "!");
}
}
// Implementing class only needs to implement abstract methods
class Person implements Greeting {
private String name;
Person(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
// sayHello() and sayGoodbye() inherited from interface!
}
public class BasicDefault {
public static void main(String[] args) {
System.out.println("=== Basic Default Methods ===\n");
String personName = ;
Person alice = new Person(personName);
// Calling abstract method (implemented)
System.out.println("Name: " + alice.getName());
// Calling default methods (inherited)
alice.sayHello();
alice.sayGoodbye();
System.out.println("\n=== Another Implementation ===");
// Anonymous class also gets defaults
String robotName = ;
Greeting robot = new Greeting() {
@Override
public String getName() {
return robotName;
}
};
robot.sayHello(); // Uses default
System.out.println("\n=== Why Default Methods? ===");
System.out.println("""
Before Java 8:
- Adding method to interface = breaking change
- All implementations had to be updated
With Default Methods:
- Can add methods with implementation
- Existing code continues to work
- Interface evolution without breaking changes
""");
}
}
// Basic Default Method Syntax
interface Greeting {
// Abstract method - MUST be implemented
String getName();
// Default method - has implementation
default void sayHello() {
System.out.println("Hello, " + getName() + "!");
}
default void sayGoodbye() {
System.out.println("Goodbye, " + getName() + "!");
}
}
// Implementing class only needs to implement abstract methods
class Person implements Greeting {
private String name;
Person(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
// sayHello() and sayGoodbye() inherited from interface!
}
public class BasicDefault {
public static void main(String[] args) {
System.out.println("=== Basic Default Methods ===\n");
String personName = ;
Person alice = new Person(personName);
// Calling abstract method (implemented)
System.out.println("Name: " + alice.getName());
// Calling default methods (inherited)
alice.sayHello();
alice.sayGoodbye();
System.out.println("\n=== Another Implementation ===");
// Anonymous class also gets defaults
String robotName = ;
Greeting robot = new Greeting() {
@Override
public String getName() {
return robotName;
}
};
robot.sayHello(); // Uses default
System.out.println("\n=== Why Default Methods? ===");
System.out.println("""
Before Java 8:
- Adding method to interface = breaking change
- All implementations had to be updated
With Default Methods:
- Can add methods with implementation
- Existing code continues to work
- Interface evolution without breaking changes
""");
}
}
default methods have bodies. Implementing classes inherit the implementation.
Override default methods
Implementing classes can override defaults.
// Overriding Default Methods
interface Logger {
default void log(String message) {
System.out.println("[LOG] " + message);
}
default void warn(String message) {
System.out.println("[WARN] " + message);
}
default void error(String message) {
System.out.println("[ERROR] " + message);
}
}
// Uses all defaults
class BasicLogger implements Logger {
// No overrides - uses all default implementations
}
// Overrides some defaults
class TimestampLogger implements Logger {
@Override
public void log(String message) {
System.out.println("[" + timestamp() + "] LOG: " + message);
}
@Override
public void error(String message) {
System.out.println("!!! [" + timestamp() + "] ERROR: " + message + " !!!");
}
// warn() still uses default
private String timestamp() {
return "09:30:00";
}
}
// Overrides ALL defaults
class JsonLogger implements Logger {
@Override
public void log(String message) {
System.out.println(format("LOG", message));
}
@Override
public void warn(String message) {
System.out.println(format("WARN", message));
}
@Override
public void error(String message) {
System.out.println(format("ERROR", message));
}
private String format(String level, String msg) {
return "{\"level\":\"" + level + "\",\"message\":\"" + msg + "\"}";
}
}
public class OverrideDefault {
public static void main(String[] args) {
System.out.println("=== BasicLogger (all defaults) ===");
Logger basic = new BasicLogger();
basic.log("Application started");
basic.warn("Low memory");
basic.error("Connection failed");
System.out.println("\n=== TimestampLogger (partial override) ===");
Logger timestamp = new TimestampLogger();
timestamp.log("Application started");
timestamp.warn("Low memory"); // Uses default!
timestamp.error("Connection failed");
System.out.println("\n=== JsonLogger (full override) ===");
Logger json = new JsonLogger();
json.log("Application started");
json.warn("Low memory");
json.error("Connection failed");
System.out.println("\n=== Polymorphism with Defaults ===");
Logger[] loggers = {new BasicLogger(), new TimestampLogger(), new JsonLogger()};
for (Logger logger : loggers) {
System.out.println("\n" + logger.getClass().getSimpleName() + ":");
logger.log("Test message");
}
}
}
Classes can accept the default or provide their own implementation.
Multiple interfaces with defaults
Handle conflicts when interfaces have same default method.
// Multiple Interfaces with Default Methods
interface Flyable {
default void move() {
System.out.println("Flying through the air");
}
default void takeOff() {
System.out.println("Taking off...");
}
}
interface Swimmable {
default void move() {
System.out.println("Swimming through the water");
}
default void dive() {
System.out.println("Diving deep...");
}
}
// Implements only Flyable - no conflict
class Bird implements Flyable {
// Gets Flyable.move() and takeOff()
}
// Implements only Swimmable - no conflict
class Fish implements Swimmable {
// Gets Swimmable.move() and dive()
}
// Implements BOTH - MUST resolve conflict!
class Duck implements Flyable, Swimmable {
@Override
public void move() {
// MUST override - compiler error otherwise!
System.out.println("Duck can fly and swim!");
}
// Can call specific interface's default
public void flyMove() {
Flyable.super.move(); // Calls Flyable's default
}
public void swimMove() {
Swimmable.super.move(); // Calls Swimmable's default
}
// takeOff() from Flyable - no conflict
// dive() from Swimmable - no conflict
}
// Another approach: delegate to one interface
class Seaplane implements Flyable, Swimmable {
@Override
public void move() {
// Delegate to Flyable's version
Flyable.super.move();
}
}
public class MultipleDefaults {
public static void main(String[] args) {
System.out.println("=== Single Interface ===");
Bird bird = new Bird();
bird.move();
bird.takeOff();
Fish fish = new Fish();
fish.move();
fish.dive();
System.out.println("\n=== Diamond Problem Resolution ===");
Duck duck = new Duck();
duck.move(); // Duck's override
duck.takeOff(); // From Flyable
duck.dive(); // From Swimmable
System.out.println("\n=== Calling Specific Interface's Default ===");
duck.flyMove();
duck.swimMove();
System.out.println("\n=== Delegation Approach ===");
Seaplane plane = new Seaplane();
plane.move(); // Delegates to Flyable
System.out.println("\n=== Diamond Problem Rules ===");
System.out.println("""
When two interfaces have same default method:
1. Implementing class MUST override
2. Compiler error if not resolved
3. Can call specific default: Interface.super.method()
No conflict if:
- Methods have different names
- One interface extends the other
""");
}
}
Class must override if two interfaces have same default method signature.
Default calls abstract
Default methods can use abstract methods.
// Combining Default and Abstract Methods
interface DataProcessor {
// Abstract methods - MUST implement
String getSource();
void processItem(String item);
// Default methods - use abstract methods
default void processAll(String[] items) {
System.out.println("Processing from: " + getSource());
for (String item : items) {
processItem(item); // Calls abstract method
}
System.out.println("Done processing " + items.length + " items");
}
default void validate(String item) {
if (item == null || item.isEmpty()) {
throw new IllegalArgumentException("Invalid item");
}
}
}
// Implementation must provide abstract methods
class FileProcessor implements DataProcessor {
private String filename;
FileProcessor(String filename) {
this.filename = filename;
}
@Override
public String getSource() {
return "File: " + filename;
}
@Override
public void processItem(String item) {
validate(item); // Uses default!
System.out.println(" Processing: " + item.toUpperCase());
}
}
class DatabaseProcessor implements DataProcessor {
private String tableName;
DatabaseProcessor(String tableName) {
this.tableName = tableName;
}
@Override
public String getSource() {
return "Database: " + tableName;
}
@Override
public void processItem(String item) {
validate(item);
System.out.println(" INSERT INTO " + tableName + " VALUES ('" + item + "')");
}
}
// Interface with hook methods
interface Workflow {
// Template with hooks
default void execute() {
beforeStart(); // Hook
doWork(); // Abstract - must implement
afterComplete(); // Hook
}
// Hooks with empty defaults
default void beforeStart() {
// Empty default - override to add behavior
}
default void afterComplete() {
// Empty default - override to add behavior
}
// Abstract - must implement
void doWork();
}
class SimpleTask implements Workflow {
@Override
public void doWork() {
System.out.println("Doing simple work...");
}
// Uses empty hooks
}
class LoggedTask implements Workflow {
@Override
public void doWork() {
System.out.println("Doing logged work...");
}
@Override
public void beforeStart() {
System.out.println("[LOG] Task starting...");
}
@Override
public void afterComplete() {
System.out.println("[LOG] Task completed!");
}
}
public class DefaultWithAbstract {
public static void main(String[] args) {
System.out.println("=== DataProcessor ===\n");
String[] data = {"apple", "banana", "cherry"};
DataProcessor fileProc = new FileProcessor("data.txt");
fileProc.processAll(data);
System.out.println();
DataProcessor dbProc = new DatabaseProcessor("fruits");
dbProc.processAll(data);
System.out.println("\n=== Workflow with Hooks ===\n");
System.out.println("SimpleTask:");
Workflow simple = new SimpleTask();
simple.execute();
System.out.println("\nLoggedTask:");
Workflow logged = new LoggedTask();
logged.execute();
System.out.println("\n=== Pattern Summary ===");
System.out.println("""
Template Method in Interfaces:
- Default method defines algorithm structure
- Abstract methods are customization points
- Implementations provide specific behavior
Hook Pattern:
- Default methods with empty bodies
- Override to add optional behavior
- Clean extension points
""");
}
}
Default implementation calls abstract methods that subclasses implement.
API evolution
Add new methods to existing interfaces safely.
// Using Default Methods for API Evolution
// Version 1 of the API (before default methods)
interface PaymentProcessorV1 {
boolean processPayment(double amount);
boolean refund(String transactionId);
}
// Old implementation works with V1
class StripePaymentV1 implements PaymentProcessorV1 {
@Override
public boolean processPayment(double amount) {
System.out.println("Stripe: Processing $" + amount);
return true;
}
@Override
public boolean refund(String transactionId) {
System.out.println("Stripe: Refunding " + transactionId);
return true;
}
}
// Version 2 - Adding new features with defaults
interface PaymentProcessorV2 {
boolean processPayment(double amount);
boolean refund(String transactionId);
// NEW in V2 - won't break existing implementations!
default boolean processRecurring(double amount, String schedule) {
System.out.println("Default recurring: $" + amount + " " + schedule);
return processPayment(amount); // Fallback to regular payment
}
// NEW in V2
default void validatePaymentMethod() {
System.out.println("Default validation passed");
}
// NEW in V2 - utility method
default String formatAmount(double amount) {
return String.format("$%.2f", amount);
}
}
// Old implementation upgraded to V2 - still works!
class StripePaymentV2 implements PaymentProcessorV2 {
@Override
public boolean processPayment(double amount) {
System.out.println("Stripe: Processing " + formatAmount(amount));
return true;
}
@Override
public boolean refund(String transactionId) {
System.out.println("Stripe: Refunding " + transactionId);
return true;
}
// processRecurring() - uses default
// validatePaymentMethod() - uses default
}
// New implementation can override new methods
class PayPalPaymentV2 implements PaymentProcessorV2 {
@Override
public boolean processPayment(double amount) {
System.out.println("PayPal: Processing " + formatAmount(amount));
return true;
}
@Override
public boolean refund(String transactionId) {
System.out.println("PayPal: Refunding " + transactionId);
return true;
}
@Override
public boolean processRecurring(double amount, String schedule) {
System.out.println("PayPal Subscriptions: " + formatAmount(amount) +
" billed " + schedule);
return true;
}
@Override
public void validatePaymentMethod() {
System.out.println("PayPal: Validating linked account...");
}
}
public class RealWorldEvolution {
public static void main(String[] args) {
System.out.println("=== API Evolution Example ===\n");
System.out.println("--- Stripe (uses defaults) ---");
PaymentProcessorV2 stripe = new StripePaymentV2();
stripe.processPayment(99.99);
stripe.validatePaymentMethod(); // Uses default
stripe.processRecurring(9.99, "monthly"); // Uses default
System.out.println("\n--- PayPal (custom implementations) ---");
PaymentProcessorV2 paypal = new PayPalPaymentV2();
paypal.processPayment(99.99);
paypal.validatePaymentMethod(); // Custom
paypal.processRecurring(9.99, "monthly"); // Custom
System.out.println("\n=== Benefits of Default Methods ===");
System.out.println("""
1. Backward Compatibility
- Add methods without breaking existing code
- Old implementations continue to work
2. Optional Features
- Provide sensible defaults
- Override only when needed
3. Interface Evolution
- Grow APIs over time
- No need for adapter classes
4. Utility Methods
- Add helper methods to interfaces
- All implementations can use them
""");
System.out.println("=== Common Use Cases ===");
System.out.println("""
- Collection.forEach() - added in Java 8
- Comparator.reversed() - added in Java 8
- List.sort() - added in Java 8
- Stream API methods on collections
""");
}
}
Collections API added forEach(), stream() via default methods. No breakage.
Exercise: Practical.java
Evolve an interface with new default methods