Imagine a city planner who designed a road system 20 years ago. Thousands of buildings were constructed along those roads. Now the city needs bike lanes. The planner cannot tear down every building and start over — the city must add bike lanes without breaking the existing infrastructure. That is exactly the problem Java 8 solved with default methods.
Before Java 8, interfaces in Java were pure contracts: they declared methods but provided no implementation. Every class that implemented an interface had to provide code for every method. This worked fine — until the Java team needed to evolve existing interfaces.
Consider the java.util.Collection interface. It has existed since Java 1.2 (1998). Millions of classes implement it — in the JDK itself, in third-party libraries, in enterprise applications worldwide. When Java 8 introduced the Stream API, the team needed to add a stream() method to Collection. But adding an abstract method to an interface would break every class that implements it.
The backward compatibility problem:
Default methods solve this by allowing interfaces to provide a method body using the default keyword. Existing implementations inherit the default behavior automatically, but can override it if needed. This is interface evolution without breaking existing code.
Here is a simple before-and-after to see the problem and solution:
// BEFORE Java 8: Adding a method breaks all implementations
interface Greeting {
void sayHello(String name);
// If we add this, every class implementing Greeting must be updated!
// void sayGoodbye(String name); // <-- Would break existing code
}
class EnglishGreeting implements Greeting {
@Override
public void sayHello(String name) {
System.out.println("Hello, " + name + "!");
}
// If sayGoodbye() were abstract, this class would fail to compile
}
// AFTER Java 8: Default methods allow safe evolution
interface GreetingV2 {
void sayHello(String name);
// Default method -- existing implementations inherit this automatically
default void sayGoodbye(String name) {
System.out.println("Goodbye, " + name + "!");
}
}
class EnglishGreetingV2 implements GreetingV2 {
@Override
public void sayHello(String name) {
System.out.println("Hello, " + name + "!");
}
// sayGoodbye() is inherited from the interface -- no changes required!
}
public class DefaultMethodMotivation {
public static void main(String[] args) {
EnglishGreetingV2 greeting = new EnglishGreetingV2();
greeting.sayHello("Alice");
// Output: Hello, Alice!
greeting.sayGoodbye("Alice");
// Output: Goodbye, Alice!
}
}
EnglishGreetingV2 did not have to change at all. It inherited the default implementation of sayGoodbye() for free. This is the fundamental value of default methods.
A default method is declared in an interface using the default keyword before the return type. Unlike regular interface methods (which are implicitly abstract), a default method has a full method body.
Syntax:
default returnType methodName(parameters) {
// method body
}
Key rules for default methods:
| Rule | Description |
|---|---|
Must use default keyword |
Without it, the method is abstract and has no body |
| Must have a body | Unlike abstract methods, a default method must provide an implementation |
Implicitly public |
All interface methods are public; you cannot make a default method private or protected |
| Can be overridden | Implementing classes can override the default method with their own version |
| Can call other interface methods | A default method can invoke abstract methods declared in the same interface |
| Cannot access instance state | Interfaces have no instance fields; default methods rely on abstract method contracts |
interface Logger {
// Abstract method -- implementing classes MUST provide this
void log(String message);
// Default method -- implementing classes inherit this, can override if needed
default void logInfo(String message) {
log("INFO: " + message);
}
default void logWarning(String message) {
log("WARNING: " + message);
}
default void logError(String message) {
log("ERROR: " + message);
}
}
class ConsoleLogger implements Logger {
@Override
public void log(String message) {
System.out.println("[Console] " + message);
}
// logInfo, logWarning, logError are all inherited!
}
class FileLogger implements Logger {
@Override
public void log(String message) {
// In a real app, this would write to a file
System.out.println("[File] " + message);
}
// Override one default method for custom behavior
@Override
public void logError(String message) {
log("CRITICAL ERROR: " + message);
// In a real app, also send an alert
}
}
public class DefaultMethodBasics {
public static void main(String[] args) {
Logger console = new ConsoleLogger();
console.logInfo("Application started");
// Output: [Console] INFO: Application started
console.logWarning("Memory usage is high");
// Output: [Console] WARNING: Memory usage is high
console.logError("Database connection failed");
// Output: [Console] ERROR: Database connection failed
System.out.println();
Logger file = new FileLogger();
file.logInfo("Application started");
// Output: [File] INFO: Application started
file.logError("Database connection failed");
// Output: [File] CRITICAL ERROR: Database connection failed
}
}
Notice how ConsoleLogger inherits all three default methods, while FileLogger overrides only logError(). The default methods call the abstract log() method, which each implementation defines differently. This is the template method pattern inside an interface.
One of the most useful patterns is having default methods call abstract methods. The interface defines the structure and the implementing class fills in the details.
interface Validator{ // Abstract: each implementation defines its own validation logic boolean isValid(T item); // Default: built on top of isValid() default void validate(T item) { if (!isValid(item)) { throw new IllegalArgumentException("Validation failed for: " + item); } } // Default: validate a collection default long countInvalid(java.util.List items) { return items.stream().filter(item -> !isValid(item)).count(); } // Default: filter to only valid items default java.util.List filterValid(java.util.List items) { return items.stream().filter(this::isValid).collect(java.util.stream.Collectors.toList()); } } class EmailValidator implements Validator { @Override public boolean isValid(String email) { return email != null && email.contains("@") && email.contains("."); } } class AgeValidator implements Validator { @Override public boolean isValid(Integer age) { return age != null && age >= 0 && age <= 150; } } public class DefaultCallingAbstract { public static void main(String[] args) { EmailValidator emailValidator = new EmailValidator(); // Using inherited default methods System.out.println(emailValidator.isValid("alice@example.com")); // Output: true java.util.List emails = java.util.List.of( "alice@example.com", "invalid", "bob@test.org", "no-at-sign" ); System.out.println("Invalid count: " + emailValidator.countInvalid(emails)); // Output: Invalid count: 2 System.out.println("Valid emails: " + emailValidator.filterValid(emails)); // Output: Valid emails: [alice@example.com, bob@test.org] // validate() throws on invalid input try { emailValidator.validate("not-an-email"); } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); // Output: Validation failed for: not-an-email } } }
Each Validator implementation only needs to define isValid(). The three default methods (validate(), countInvalid(), filterValid()) are all inherited for free. If a specific validator needs different behavior for any of these, it can override just that one method.
Java 8 added dozens of default methods to existing interfaces in the JDK. Understanding these real examples shows how default methods were used to add major new capabilities without breaking any existing code.
The Iterable interface (parent of all collections) gained a forEach() default method so that every existing List, Set, and Queue could immediately use lambda-based iteration.
import java.util.List;
import java.util.ArrayList;
public class ForEachDefaultMethod {
public static void main(String[] args) {
List names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
// forEach() is a default method in Iterable -- no List change needed
names.forEach(name -> System.out.println("Hello, " + name));
// Output:
// Hello, Alice
// Hello, Bob
// Hello, Charlie
// Using method reference
names.forEach(System.out::println);
// Output:
// Alice
// Bob
// Charlie
}
}
The Stream API is arguably the biggest Java 8 feature. It was made possible by adding stream() and parallelStream() as default methods to Collection.
import java.util.List;
import java.util.stream.Collectors;
public class StreamDefaultMethod {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie", "Diana", "Eve");
// stream() is a default method in Collection
List shortNames = names.stream()
.filter(name -> name.length() <= 3)
.collect(Collectors.toList());
System.out.println("Short names: " + shortNames);
// Output: Short names: [Bob, Eve]
// parallelStream() is also a default method
long count = names.parallelStream()
.filter(name -> name.startsWith("A") || name.startsWith("E"))
.count();
System.out.println("Names starting with A or E: " + count);
// Output: Names starting with A or E: 2
}
}
Before Java 8, you had to use Collections.sort(list, comparator). Java 8 added a sort() default method directly to the List interface.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class ListSortDefaultMethod {
public static void main(String[] args) {
List names = new ArrayList<>(List.of("Charlie", "Alice", "Bob"));
// Before Java 8:
// Collections.sort(names);
// Java 8: sort() is a default method on List
names.sort(Comparator.naturalOrder());
System.out.println("Sorted: " + names);
// Output: Sorted: [Alice, Bob, Charlie]
// Sort by length, then alphabetically
names.sort(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()));
System.out.println("By length: " + names);
// Output: By length: [Bob, Alice, Charlie]
// Reverse order
names.sort(Comparator.reverseOrder());
System.out.println("Reversed: " + names);
// Output: Reversed: [Charlie, Bob, Alice]
}
}
The Map interface received many default methods in Java 8. These eliminated common boilerplate patterns.
import java.util.HashMap;
import java.util.Map;
public class MapDefaultMethods {
public static void main(String[] args) {
Map scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 87);
// getOrDefault() -- no more null checks for missing keys
int aliceScore = scores.getOrDefault("Alice", 0);
int unknownScore = scores.getOrDefault("Unknown", 0);
System.out.println("Alice: " + aliceScore + ", Unknown: " + unknownScore);
// Output: Alice: 95, Unknown: 0
// putIfAbsent() -- only puts if key is not already present
scores.putIfAbsent("Bob", 100); // Bob exists, no change
scores.putIfAbsent("Charlie", 92); // Charlie is new, added
System.out.println("Bob: " + scores.get("Bob"));
// Output: Bob: 87
// forEach() with BiConsumer
scores.forEach((name, score) ->
System.out.println(name + " scored " + score)
);
// Output:
// Alice scored 95
// Bob scored 87
// Charlie scored 92
// merge() -- combine existing and new values
scores.merge("Alice", 5, Integer::sum); // Alice: 95 + 5 = 100
System.out.println("Alice after bonus: " + scores.get("Alice"));
// Output: Alice after bonus: 100
// computeIfAbsent() -- compute value only if key is missing
scores.computeIfAbsent("Diana", name -> name.length() * 10);
System.out.println("Diana: " + scores.get("Diana"));
// Output: Diana: 50
// replaceAll() -- transform all values
scores.replaceAll((name, score) -> score + 10);
scores.forEach((name, score) -> System.out.println(name + ": " + score));
// Output:
// Alice: 110
// Bob: 97
// Charlie: 102
// Diana: 60
}
}
All of these methods (getOrDefault, putIfAbsent, forEach, merge, computeIfAbsent, replaceAll) are default methods on the Map interface. Every Map implementation -- HashMap, TreeMap, LinkedHashMap, and any custom implementation -- inherited them all without any code changes.
| Interface | Default Method | Purpose |
|---|---|---|
Iterable |
forEach(Consumer) |
Lambda-based iteration over any Iterable |
Collection |
stream() |
Create a sequential Stream |
Collection |
parallelStream() |
Create a parallel Stream |
Collection |
removeIf(Predicate) |
Remove elements matching a condition |
List |
sort(Comparator) |
Sort the list in place |
List |
replaceAll(UnaryOperator) |
Transform every element |
Map |
getOrDefault(key, default) |
Get value or return default if missing |
Map |
putIfAbsent(key, value) |
Only put if key is not present |
Map |
forEach(BiConsumer) |
Iterate over key-value pairs |
Map |
merge(key, value, BiFunction) |
Merge new value with existing |
Map |
computeIfAbsent(key, Function) |
Compute value if key is missing |
Map |
replaceAll(BiFunction) |
Transform all values |
Default methods introduce a form of multiple inheritance of behavior into Java. A class can implement multiple interfaces, and if two interfaces provide a default method with the same signature, the compiler faces a conflict: which default should the class inherit?
This is called the diamond problem, named after the diamond shape formed in the inheritance diagram. Java 8 defines clear rules for resolving these conflicts.
interface Camera {
default String takePicture() {
return "Camera: snap!";
}
}
interface Phone {
default String takePicture() {
return "Phone: click!";
}
}
// COMPILER ERROR: SmartDevice inherits unrelated defaults for takePicture()
// class SmartDevice implements Camera, Phone { }
// FIX: You MUST override the conflicting method
class SmartDevice implements Camera, Phone {
@Override
public String takePicture() {
// Option 1: Provide your own implementation
return "SmartDevice: capturing with AI!";
}
}
public class DiamondProblemBasic {
public static void main(String[] args) {
SmartDevice device = new SmartDevice();
System.out.println(device.takePicture());
// Output: SmartDevice: capturing with AI!
}
}
When you override a conflicting default method, you can delegate to one of the parent interfaces using the InterfaceName.super.methodName() syntax.
interface Flyable {
default String getSpeed() {
return "Flying at 500 km/h";
}
}
interface Swimmable {
default String getSpeed() {
return "Swimming at 30 km/h";
}
}
class Duck implements Flyable, Swimmable {
@Override
public String getSpeed() {
// Delegate to Flyable's default
return Flyable.super.getSpeed();
}
public String getSwimSpeed() {
// Can also call the other interface's default in any method
return Swimmable.super.getSpeed();
}
}
class FlyingFish implements Flyable, Swimmable {
@Override
public String getSpeed() {
// Combine both!
return Flyable.super.getSpeed() + " | " + Swimmable.super.getSpeed();
}
}
public class DiamondProblemSuper {
public static void main(String[] args) {
Duck duck = new Duck();
System.out.println(duck.getSpeed());
// Output: Flying at 500 km/h
System.out.println(duck.getSwimSpeed());
// Output: Swimming at 30 km/h
FlyingFish fish = new FlyingFish();
System.out.println(fish.getSpeed());
// Output: Flying at 500 km/h | Swimming at 30 km/h
}
}
Java uses three rules (applied in order) to resolve default method conflicts:
| Priority | Rule | Description |
|---|---|---|
| 1 (Highest) | Classes win over interfaces | If a superclass provides a concrete method, it takes priority over any interface default |
| 2 | More specific interface wins | If interface B extends interface A, and both define the same default, B's version wins |
| 3 | Class must override | If neither rule 1 nor 2 resolves it (two unrelated interfaces), the implementing class must explicitly override the method |
interface Printable {
default String getOutput() {
return "Printable default";
}
}
class BasePrinter {
public String getOutput() {
return "BasePrinter concrete method";
}
}
// BasePrinter.getOutput() wins over Printable.getOutput()
class LaserPrinter extends BasePrinter implements Printable {
// No override needed -- BasePrinter's method takes priority
}
public class ClassWinsOverInterface {
public static void main(String[] args) {
LaserPrinter printer = new LaserPrinter();
System.out.println(printer.getOutput());
// Output: BasePrinter concrete method
}
}
interface Animal {
default String sound() {
return "Some generic animal sound";
}
}
interface Dog extends Animal {
@Override
default String sound() {
return "Woof!";
}
}
// Dog is more specific than Animal, so Dog.sound() wins
class GoldenRetriever implements Animal, Dog {
// No override needed -- Dog's default wins because Dog extends Animal
}
public class SpecificInterfaceWins {
public static void main(String[] args) {
GoldenRetriever dog = new GoldenRetriever();
System.out.println(dog.sound());
// Output: Woof!
}
}
Java 8 also introduced static methods in interfaces. Unlike default methods, static methods belong to the interface itself, not to any implementing class. They cannot be overridden and are called using the interface name directly: InterfaceName.staticMethod().
Why static methods in interfaces?
Collections (for Collection) or Paths (for Path)| Rule | Description |
|---|---|
Must use static keyword |
Declared with static in the interface |
| Must have a body | Static methods must provide an implementation |
| Called via interface name | MyInterface.myStaticMethod(), never via an instance |
| Not inherited by implementing classes | Unlike defaults, static methods are not inherited |
| Cannot be overridden | Implementing classes cannot override interface static methods |
Implicitly public |
All interface methods are public (private static allowed in Java 9+) |
interface StringUtils {
// Static utility methods in an interface
static boolean isNullOrEmpty(String str) {
return str == null || str.isEmpty();
}
static boolean isNullOrBlank(String str) {
return str == null || str.trim().isEmpty();
}
static String capitalize(String str) {
if (isNullOrEmpty(str)) return str;
return Character.toUpperCase(str.charAt(0)) + str.substring(1).toLowerCase();
}
static String repeat(String str, int times) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < times; i++) {
sb.append(str);
}
return sb.toString();
}
}
public class StaticUtilityMethods {
public static void main(String[] args) {
// Called via interface name -- NOT via an instance
System.out.println(StringUtils.isNullOrEmpty(""));
// Output: true
System.out.println(StringUtils.isNullOrEmpty("Hello"));
// Output: false
System.out.println(StringUtils.isNullOrBlank(" "));
// Output: true
System.out.println(StringUtils.capitalize("jAVA"));
// Output: Java
System.out.println(StringUtils.repeat("Ha", 3));
// Output: HaHaHa
}
}
Static methods in interfaces are ideal for the factory method pattern. The interface exposes creation methods while hiding the implementation classes.
interface Shape {
double area();
String describe();
// Factory methods -- callers never need to know the implementation classes
static Shape circle(double radius) {
return new Circle(radius);
}
static Shape rectangle(double width, double height) {
return new Rectangle(width, height);
}
static Shape square(double side) {
return new Rectangle(side, side);
}
}
// Implementation classes can be package-private
class Circle implements Shape {
private final double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public String describe() {
return String.format("Circle(radius=%.1f)", radius);
}
}
class Rectangle implements Shape {
private final double width;
private final double height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public String describe() {
return String.format("Rectangle(%.1f x %.1f)", width, height);
}
}
public class StaticFactoryMethods {
public static void main(String[] args) {
// Create shapes using factory methods -- no need to know Circle or Rectangle
Shape circle = Shape.circle(5.0);
Shape rect = Shape.rectangle(4.0, 6.0);
Shape square = Shape.square(3.0);
System.out.println(circle.describe() + " -> area: " + String.format("%.2f", circle.area()));
// Output: Circle(radius=5.0) -> area: 78.54
System.out.println(rect.describe() + " -> area: " + String.format("%.2f", rect.area()));
// Output: Rectangle(4.0 x 6.0) -> area: 24.00
System.out.println(square.describe() + " -> area: " + String.format("%.2f", square.area()));
// Output: Rectangle(3.0 x 3.0) -> area: 9.00
}
}
One of the most powerful static methods added in Java 8 is Comparator.comparing(). It is a static factory method on the Comparator interface that creates comparators from method references.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class ComparatorComparing {
record Employee(String name, String department, int salary) {}
public static void main(String[] args) {
List employees = new ArrayList<>(List.of(
new Employee("Alice", "Engineering", 95000),
new Employee("Bob", "Marketing", 72000),
new Employee("Charlie", "Engineering", 88000),
new Employee("Diana", "Marketing", 85000),
new Employee("Eve", "Engineering", 95000)
));
// Comparator.comparing() is a STATIC method on the Comparator interface
employees.sort(Comparator.comparing(Employee::salary));
System.out.println("By salary (ascending):");
employees.forEach(e -> System.out.println(" " + e));
// Output:
// Employee[name=Bob, department=Marketing, salary=72000]
// Employee[name=Diana, department=Marketing, salary=85000]
// Employee[name=Charlie, department=Engineering, salary=88000]
// Employee[name=Alice, department=Engineering, salary=95000]
// Employee[name=Eve, department=Engineering, salary=95000]
// Chain with default methods: reversed(), thenComparing()
employees.sort(
Comparator.comparing(Employee::department)
.thenComparing(Employee::salary)
.reversed()
);
System.out.println("\nBy department, then salary, reversed:");
employees.forEach(e -> System.out.println(" " + e));
// Output:
// Employee[name=Bob, department=Marketing, salary=72000]
// Employee[name=Diana, department=Marketing, salary=85000]
// Employee[name=Charlie, department=Engineering, salary=88000]
// Employee[name=Alice, department=Engineering, salary=95000]
// Employee[name=Eve, department=Engineering, salary=95000]
}
}
Notice the interplay: Comparator.comparing() is a static method that creates a comparator, and reversed() and thenComparing() are default methods that modify it. Static and default methods work together to create a rich, fluent API.
Java 9 added private methods and private static methods to interfaces. The motivation is simple: when multiple default methods in the same interface share common logic, you need a way to extract that shared code without making it part of the public API.
Before Java 9, you had two bad options:
interface Reportable {
String getData();
default String htmlReport() {
return wrapInDocument("" + formatBody() + "");
}
default String textReport() {
return wrapInDocument("=== REPORT ===\n" + formatBody() + "\n==============");
}
default String csvReport() {
return wrapInDocument("data\n" + getData());
}
// Java 9+: Private method shared by multiple defaults
private String formatBody() {
String raw = getData();
if (raw == null || raw.isEmpty()) {
return "[No data available]";
}
return raw.trim();
}
// Java 9+: Private method to add metadata
private String wrapInDocument(String content) {
return "Generated: " + java.time.LocalDate.now() + "\n" + content;
}
}
class SalesReport implements Reportable {
@Override
public String getData() {
return "Q1: $1.2M, Q2: $1.5M, Q3: $1.1M, Q4: $1.8M";
}
}
public class PrivateMethodsDemo {
public static void main(String[] args) {
SalesReport report = new SalesReport();
System.out.println(report.textReport());
// Output:
// Generated: 2026-02-28
// === REPORT ===
// Q1: $1.2M, Q2: $1.5M, Q3: $1.1M, Q4: $1.8M
// ==============
System.out.println();
System.out.println(report.htmlReport());
// Output:
// Generated: 2026-02-28
// Q1: $1.2M, Q2: $1.5M, Q3: $1.1M, Q4: $1.8M
}
}
Private static methods can be called from both static methods and default methods. They are useful when the shared logic does not depend on instance-level abstract methods.
interface Identifiable {
String getName();
// Static factory using private static helper
static String generateId(String prefix) {
return prefix + "-" + randomSuffix(8);
}
static String generateShortId(String prefix) {
return prefix + "-" + randomSuffix(4);
}
// Java 9+: Private static method -- shared by static methods above
private static String randomSuffix(int length) {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
StringBuilder sb = new StringBuilder();
java.util.Random random = new java.util.Random();
for (int i = 0; i < length; i++) {
sb.append(chars.charAt(random.nextInt(chars.length())));
}
return sb.toString();
}
// Default method can also call private static methods
default String getDisplayId() {
return randomSuffix(6) + "-" + getName().toUpperCase();
}
}
class User implements Identifiable {
private final String name;
User(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
}
public class PrivateStaticMethodDemo {
public static void main(String[] args) {
// Static method usage
System.out.println(Identifiable.generateId("USER"));
// Output: USER-A7KF2M9B (random)
System.out.println(Identifiable.generateShortId("ORD"));
// Output: ORD-X3K9 (random)
// Default method usage
User user = new User("Alice");
System.out.println(user.getDisplayId());
// Output: R4TN2K-ALICE (random prefix)
}
}
| Method Type | Java Version | Has Body? | Access Modifier | Inherited? | Can Override? |
|---|---|---|---|---|---|
| Abstract | 1.0+ | No | public (implicit) | Yes | Must implement |
| Default | 8+ | Yes | public (implicit) | Yes | Yes (optional) |
| Static | 8+ | Yes | public (implicit) | No | No |
| Private | 9+ | Yes | private | No | No |
| Private static | 9+ | Yes | private | No | No |
With three kinds of methods now possible in interfaces, it is important to understand when to use each one.
| Feature | Abstract Method | Default Method | Static Method |
|---|---|---|---|
| Keyword | None (implicit) | default |
static |
| Has body? | No | Yes | Yes |
| Inherited? | Yes (must implement) | Yes (can override) | No |
| Can access other methods? | N/A | Yes, including abstract | Only other static methods |
| Called on | Instance | Instance | Interface name |
| Primary use | Define the contract | Provide optional behavior | Utility/factory methods |
| Example | void process(); |
default void log() {...} |
static Foo create() {...} |
interface Notification {
// Abstract: every notification type must define this
String getMessage();
String getRecipient();
// Default: common behavior with a sensible default
default String getSubject() {
return "Notification";
}
default String format() {
return String.format("To: %s\nSubject: %s\n\n%s",
getRecipient(), getSubject(), getMessage());
}
// Static: factory methods
static Notification email(String to, String subject, String body) {
return new Notification() {
@Override
public String getMessage() { return body; }
@Override
public String getRecipient() { return to; }
@Override
public String getSubject() { return subject; }
};
}
static Notification sms(String to, String body) {
return new Notification() {
@Override
public String getMessage() { return body; }
@Override
public String getRecipient() { return to; }
};
}
}
public class AbstractDefaultStaticDemo {
public static void main(String[] args) {
// Static factory methods create instances
Notification email = Notification.email(
"alice@example.com", "Welcome!", "Thanks for signing up."
);
Notification sms = Notification.sms(
"+1234567890", "Your code is 9876"
);
// Default methods provide formatting
System.out.println(email.format());
// Output:
// To: alice@example.com
// Subject: Welcome!
//
// Thanks for signing up.
System.out.println();
System.out.println(sms.format());
// Output:
// To: +1234567890
// Subject: Notification
//
// Your code is 9876
}
}
A functional interface has exactly one abstract method, making it usable with lambda expressions. But a functional interface can have any number of default and static methods. This is how Java 8 built rich, chainable APIs on top of functional interfaces.
The @FunctionalInterface annotation enforces the single-abstract-method rule. Default methods do not count toward this limit.
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class PredicateDefaultMethods {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie", "Anna", "Eve", "Alex");
// Predicate has ONE abstract method: test(T t)
Predicate startsWithA = name -> name.startsWith("A");
Predicate longerThan3 = name -> name.length() > 3;
// Default method: and() -- combines two predicates with logical AND
Predicate startsWithAAndLong = startsWithA.and(longerThan3);
List result1 = names.stream()
.filter(startsWithAAndLong)
.collect(Collectors.toList());
System.out.println("Starts with A AND longer than 3: " + result1);
// Output: Starts with A AND longer than 3: [Alice, Anna, Alex]
// Default method: or() -- combines with logical OR
Predicate startsWithE = name -> name.startsWith("E");
List result2 = names.stream()
.filter(startsWithA.or(startsWithE))
.collect(Collectors.toList());
System.out.println("Starts with A OR E: " + result2);
// Output: Starts with A OR E: [Alice, Anna, Eve, Alex]
// Default method: negate() -- logical NOT
List result3 = names.stream()
.filter(startsWithA.negate())
.collect(Collectors.toList());
System.out.println("Does NOT start with A: " + result3);
// Output: Does NOT start with A: [Bob, Charlie, Eve]
// Static method: Predicate.not() (Java 11+) -- even cleaner negation
// List result4 = names.stream()
// .filter(Predicate.not(startsWithA))
// .collect(Collectors.toList());
}
}
import java.util.function.Function;
public class FunctionDefaultMethods {
public static void main(String[] args) {
Function trim = String::trim;
Function toUpper = String::toUpperCase;
Function length = String::length;
// Default method: andThen() -- apply this function, then the next
Function trimThenUpper = trim.andThen(toUpper);
System.out.println(trimThenUpper.apply(" hello world "));
// Output: HELLO WORLD
// Default method: compose() -- apply the OTHER function first, then this
Function upperThenLength = length.compose(toUpper);
System.out.println(upperThenLength.apply("hello"));
// Output: 5
// Chain multiple transformations
Function pipeline = trim
.andThen(toUpper)
.andThen(s -> s.replace(" ", "_"));
System.out.println(pipeline.apply(" hello world "));
// Output: HELLO_WORLD
// Static method: Function.identity()
Function identity = Function.identity();
System.out.println(identity.apply("unchanged"));
// Output: unchanged
}
}
Comparator is the best example of how default and static methods transform a functional interface into a powerful API. It has one abstract method (compare()) plus 18 default and static methods.
| Method | Type | Purpose |
|---|---|---|
compare(T, T) |
Abstract | The core comparison (1 abstract = functional interface) |
reversed() |
Default | Reverse the sort order |
thenComparing() |
Default | Add a secondary sort key |
thenComparingInt() |
Default | Secondary sort on int-valued key (avoids boxing) |
comparing(Function) |
Static | Create a comparator from a key extractor |
comparingInt(ToIntFunction) |
Static | Create a comparator from an int key (avoids boxing) |
naturalOrder() |
Static | Comparator for natural ordering |
reverseOrder() |
Static | Comparator for reverse natural ordering |
nullsFirst(Comparator) |
Static | Wraps a comparator to handle nulls (nulls sort first) |
nullsLast(Comparator) |
Static | Wraps a comparator to handle nulls (nulls sort last) |
Without default methods, Comparator would be a bare single-method interface, and all this chaining functionality would have to live in a separate utility class.
Default methods make it possible to evolve interfaces over time without breaking existing implementations. This is one of the most important practical use cases in library development.
Consider you have published a library with an interface, and users have implemented it. Later, you want to add new capabilities.
// Version 1.0 of your library
interface DataExporter {
void export(java.util.List data, String destination);
}
// A client's implementation (you cannot change this)
class CsvExporter implements DataExporter {
@Override
public void export(java.util.List data, String destination) {
System.out.println("Exporting " + data.size() + " rows to CSV: " + destination);
}
}
// Version 2.0 -- add new features without breaking CsvExporter
interface DataExporterV2 extends DataExporter {
// New default methods -- CsvExporter inherits these without any changes
default void exportWithHeader(java.util.List data, String destination, String header) {
System.out.println("Header: " + header);
export(data, destination);
}
default boolean validate(java.util.List data) {
return data != null && !data.isEmpty();
}
default void exportIfValid(java.util.List data, String destination) {
if (validate(data)) {
export(data, destination);
} else {
System.out.println("Skipping export: invalid data");
}
}
}
public class InterfaceEvolution {
public static void main(String[] args) {
// CsvExporter works with the extended interface (if it had implemented V2)
// Demonstrating the concept:
DataExporterV2 exporter = new DataExporterV2() {
@Override
public void export(java.util.List data, String destination) {
System.out.println("Exporting " + data.size() + " rows to: " + destination);
}
};
java.util.List data = java.util.List.of("row1", "row2", "row3");
exporter.export(data, "output.csv");
// Output: Exporting 3 rows to: output.csv
exporter.exportWithHeader(data, "output.csv", "Sales Report");
// Output:
// Header: Sales Report
// Exporting 3 rows to: output.csv
exporter.exportIfValid(java.util.List.of(), "output.csv");
// Output: Skipping export: invalid data
exporter.exportIfValid(data, "output.csv");
// Output: Exporting 3 rows to: output.csv
}
}
When you write a default method, other developers need to know what the default behavior is so they can decide whether to override it. Java introduced the @implSpec Javadoc tag for this purpose.
interface Cacheable {
String getKey();
Object getValue();
/**
* Returns the time-to-live in seconds for this cached entry.
*
* @implSpec The default implementation returns 3600 (1 hour).
* Implementations that require shorter or longer cache durations
* should override this method.
*
* @return time-to-live in seconds
*/
default int getTtlSeconds() {
return 3600;
}
/**
* Determines whether this entry should be cached.
*
* @implSpec The default implementation returns true if both key and value
* are non-null. Implementations may add additional criteria.
*
* @return true if this entry is eligible for caching
*/
default boolean isCacheable() {
return getKey() != null && getValue() != null;
}
}
class UserSession implements Cacheable {
private final String userId;
private final Object sessionData;
UserSession(String userId, Object sessionData) {
this.userId = userId;
this.sessionData = sessionData;
}
@Override
public String getKey() { return "session:" + userId; }
@Override
public Object getValue() { return sessionData; }
// Override: sessions expire faster
@Override
public int getTtlSeconds() { return 900; } // 15 minutes
}
class AppConfig implements Cacheable {
private final String name;
private final String value;
AppConfig(String name, String value) {
this.name = name;
this.value = value;
}
@Override
public String getKey() { return "config:" + name; }
@Override
public Object getValue() { return value; }
// Inherits default TTL of 3600 seconds -- fine for configuration
}
public class ImplSpecDemo {
public static void main(String[] args) {
UserSession session = new UserSession("user-42", "session-data");
System.out.println(session.getKey() + " TTL: " + session.getTtlSeconds() + "s");
// Output: session:user-42 TTL: 900s
AppConfig config = new AppConfig("theme", "dark");
System.out.println(config.getKey() + " TTL: " + config.getTtlSeconds() + "s");
// Output: config:theme TTL: 3600s
System.out.println("Session cacheable: " + session.isCacheable());
// Output: Session cacheable: true
}
}
Before Java 8, if you wanted an interface with some implemented methods, you had to use an abstract class. Now that interfaces can have default methods, when should you use each? The answer depends on what you need.
| Feature | Interface (with defaults) | Abstract Class |
|---|---|---|
| Multiple inheritance | A class can implement many interfaces | A class can extend only one abstract class |
| Instance state (fields) | Only constants (static final) |
Can have mutable instance fields |
| Constructors | No constructors | Can have constructors |
| Access modifiers on methods | All public (private in Java 9+) | public, protected, package-private, private |
| Non-static fields | Not allowed | Allowed |
| Method bodies | Default, static, private (Java 9+) | Any method can have a body |
| Lambda compatibility | Yes (if functional interface) | No |
| Object methods | Cannot override equals/hashCode/toString | Can override any Object method |
| Versioning | Adding a default is backward compatible | Adding a concrete method is also safe |
| Design intent | "Can do" -- capability contract | "Is a" -- shared base implementation |
Comparable, Serializable, Closeable).AbstractList (abstract class) implementing List (interface).// Interface: defines the capability contract
interface Auditable {
String getCreatedBy();
java.time.LocalDateTime getCreatedAt();
String getLastModifiedBy();
java.time.LocalDateTime getLastModifiedAt();
default String getAuditSummary() {
return String.format("Created by %s on %s, last modified by %s on %s",
getCreatedBy(), getCreatedAt(),
getLastModifiedBy(), getLastModifiedAt());
}
}
// Abstract class: provides shared state and constructors
abstract class AuditableEntity implements Auditable {
private final String createdBy;
private final java.time.LocalDateTime createdAt;
private String lastModifiedBy;
private java.time.LocalDateTime lastModifiedAt;
protected AuditableEntity(String createdBy) {
this.createdBy = createdBy;
this.createdAt = java.time.LocalDateTime.now();
this.lastModifiedBy = createdBy;
this.lastModifiedAt = this.createdAt;
}
@Override
public String getCreatedBy() { return createdBy; }
@Override
public java.time.LocalDateTime getCreatedAt() { return createdAt; }
@Override
public String getLastModifiedBy() { return lastModifiedBy; }
@Override
public java.time.LocalDateTime getLastModifiedAt() { return lastModifiedAt; }
protected void markModified(String modifiedBy) {
this.lastModifiedBy = modifiedBy;
this.lastModifiedAt = java.time.LocalDateTime.now();
}
}
// Concrete class: inherits state management from abstract class
class Order extends AuditableEntity {
private String status;
Order(String createdBy) {
super(createdBy);
this.status = "PENDING";
}
public void updateStatus(String newStatus, String modifiedBy) {
this.status = newStatus;
markModified(modifiedBy); // From abstract class
}
public String getStatus() { return status; }
}
public class InterfaceVsAbstractClass {
public static void main(String[] args) {
Order order = new Order("Alice");
System.out.println("Status: " + order.getStatus());
// Output: Status: PENDING
// Default method from interface
System.out.println(order.getAuditSummary());
// Output: Created by Alice on 2026-02-28T..., last modified by Alice on 2026-02-28T...
order.updateStatus("SHIPPED", "Bob");
System.out.println("Updated: " + order.getAuditSummary());
// Output: Created by Alice on ..., last modified by Bob on ...
}
}
Default methods are powerful, but misuse can lead to confusing, fragile designs. Here are the most common mistakes.
Default methods should provide optional convenience behavior, not heavy implementations. If your interface has more default methods than abstract methods, you might actually need an abstract class.
// BAD: This is basically an abstract class disguised as an interface
interface BadUserService {
default void createUser(String name) {
System.out.println("Creating user: " + name);
validateUser(name);
saveToDatabase(name);
sendWelcomeEmail(name);
}
default void validateUser(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name required");
}
}
default void saveToDatabase(String name) {
System.out.println("Saving " + name + " to database");
}
default void sendWelcomeEmail(String name) {
System.out.println("Sending email to " + name);
}
// Problem: No abstract methods! What does implementing this interface mean?
// There is nothing to customize. This should be a class.
}
// GOOD: Interface defines the contract, defaults provide optional convenience
interface GoodUserService {
// Abstract: implementations must provide these
void save(String name);
void sendNotification(String name, String message);
// Default: optional convenience built on the abstract methods
default void createAndNotify(String name) {
save(name);
sendNotification(name, "Welcome, " + name + "!");
}
}
public class OverusingDefaults {
public static void main(String[] args) {
GoodUserService service = new GoodUserService() {
@Override
public void save(String name) {
System.out.println("Saved: " + name);
}
@Override
public void sendNotification(String name, String message) {
System.out.println("Notification to " + name + ": " + message);
}
};
service.createAndNotify("Alice");
// Output:
// Saved: Alice
// Notification to Alice: Welcome, Alice!
}
}
Interfaces cannot have instance fields. Attempting to simulate state with workarounds like Map-based storage creates fragile, thread-unsafe, and memory-leaking code.
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
// BAD: Simulating state in an interface -- DON'T DO THIS
interface BadStateful {
// Using a static map to fake instance state -- terrible idea!
Map
When a class implements two interfaces with the same default method, forgetting to override causes a compiler error. Always check for conflicts when implementing multiple interfaces.
Adding too many default methods to a single interface violates ISP. Just because you can keep adding defaults does not mean you should. Split large interfaces into focused ones.
// BAD: One massive interface with too many concerns
interface BadWorker {
void doWork();
default void logWork() { System.out.println("Logging..."); }
default void notifyManager() { System.out.println("Notifying..."); }
default void updateMetrics() { System.out.println("Updating metrics..."); }
default void sendReport() { System.out.println("Sending report..."); }
default void archiveResults() { System.out.println("Archiving..."); }
// A class that just needs doWork() is forced to inherit all this baggage
}
// GOOD: Segregated interfaces
interface Worker {
void doWork();
}
interface Loggable {
default void logWork() { System.out.println("Logging..."); }
}
interface Observable {
default void notifyManager() { System.out.println("Notifying..."); }
default void updateMetrics() { System.out.println("Updating metrics..."); }
}
// Classes compose only what they need
class SimpleWorker implements Worker {
@Override
public void doWork() { System.out.println("Working..."); }
// No unnecessary defaults inherited
}
class MonitoredWorker implements Worker, Loggable, Observable {
@Override
public void doWork() {
logWork();
System.out.println("Working...");
updateMetrics();
notifyManager();
}
}
public class InterfaceSegregation {
public static void main(String[] args) {
new SimpleWorker().doWork();
// Output: Working...
System.out.println();
new MonitoredWorker().doWork();
// Output:
// Logging...
// Working...
// Updating metrics...
// Notifying...
}
}
You cannot define default methods for equals(), hashCode(), or toString() in an interface. The compiler will reject it. These methods always come from Object or the implementing class.
interface Displayable {
String getDisplayName();
// COMPILER ERROR: default method toString in interface Displayable
// overrides a member of java.lang.Object
// default String toString() {
// return getDisplayName();
// }
// CORRECT: Use a differently named method
default String toDisplayString() {
return "[" + getDisplayName() + "]";
}
}
class Product implements Displayable {
private final String name;
Product(String name) { this.name = name; }
@Override
public String getDisplayName() { return name; }
// Override toString() in the CLASS, not the interface
@Override
public String toString() { return toDisplayString(); }
}
public class ObjectMethodsDemo {
public static void main(String[] args) {
Product p = new Product("Laptop");
System.out.println(p.toDisplayString());
// Output: [Laptop]
System.out.println(p);
// Output: [Laptop]
}
}
| Practice | Do | Don't |
|---|---|---|
| Keep defaults simple | Build defaults on top of abstract methods | Put complex business logic in defaults |
| Document behavior | Use @implSpec to describe default behavior |
Leave default behavior undocumented |
| Prefer composition | Use small, focused interfaces that classes compose | Create one giant interface with many defaults |
| Use for evolution | Add defaults when extending published interfaces | Use defaults as an excuse for lazy interface design |
| Static for utilities | Put factory and helper methods as static | Create separate companion utility classes |
| Respect ISP | Split interfaces by responsibility | Force implementors to inherit irrelevant defaults |
| Override intentionally | Override defaults when you have better behavior | Override defaults just to log or add trivial wrappers |
| Handle diamond conflicts | Explicitly override and delegate with super |
Assume the compiler will "figure it out" |
| Use private methods | Extract shared logic into private methods (Java 9+) | Duplicate code across multiple defaults |
| No state | Rely on abstract method contracts for data | Simulate instance state with static maps |
This example demonstrates a realistic plugin system that uses default methods, static methods, private methods (Java 9+), the diamond problem, and interface evolution -- all in one cohesive design.
The scenario: A text processing framework where plugins can transform, validate, and format text. New plugins can be added without modifying the framework.
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
// ============================================================
// Core Plugin Interface
// ============================================================
interface TextPlugin {
// Abstract: every plugin must define its transformation
String process(String input);
// Abstract: every plugin must identify itself
String getName();
// Default: chain multiple processing steps
default String processAll(List inputs) {
return inputs.stream()
.map(this::process)
.collect(Collectors.joining("\n"));
}
// Default: safe processing with error handling
default String safeProcess(String input) {
if (input == null || input.isEmpty()) {
return "[empty input]";
}
try {
return process(input);
} catch (Exception e) {
return "[error: " + e.getMessage() + "]";
}
}
// Default: logging wrapper
default String processWithLogging(String input) {
System.out.println("[" + getName() + "] Processing: " + input);
String result = process(input);
System.out.println("[" + getName() + "] Result: " + result);
return result;
}
// Static: factory methods
static TextPlugin uppercase() {
return new TextPlugin() {
@Override
public String process(String input) { return input.toUpperCase(); }
@Override
public String getName() { return "UppercasePlugin"; }
};
}
static TextPlugin replace(String target, String replacement) {
return new TextPlugin() {
@Override
public String process(String input) {
return input.replace(target, replacement);
}
@Override
public String getName() {
return "ReplacePlugin(" + target + "->" + replacement + ")";
}
};
}
static TextPlugin chain(TextPlugin... plugins) {
return new TextPlugin() {
@Override
public String process(String input) {
String result = input;
for (TextPlugin plugin : plugins) {
result = plugin.process(result);
}
return result;
}
@Override
public String getName() { return "ChainedPlugin"; }
};
}
}
// ============================================================
// Validation Capability (separate interface -- ISP)
// ============================================================
interface Validatable {
default boolean isValidInput(String input) {
return input != null && !input.isEmpty() && input.length() <= 10000;
}
default String validateAndProcess(String input, java.util.function.Function processor) {
if (!isValidInput(input)) {
throw new IllegalArgumentException("Invalid input");
}
return processor.apply(input);
}
}
// ============================================================
// Metrics Capability (separate interface -- ISP)
// ============================================================
interface Measurable {
default long measureProcessingTime(Runnable task) {
long start = System.nanoTime();
task.run();
long elapsed = System.nanoTime() - start;
return elapsed / 1_000_000; // milliseconds
}
}
// ============================================================
// Concrete Plugins
// ============================================================
class TrimPlugin implements TextPlugin {
@Override
public String process(String input) {
return input.trim();
}
@Override
public String getName() { return "TrimPlugin"; }
}
class CensorPlugin implements TextPlugin, Validatable {
private final List bannedWords;
CensorPlugin(List bannedWords) {
this.bannedWords = bannedWords;
}
@Override
public String process(String input) {
String result = input;
for (String word : bannedWords) {
result = result.replaceAll("(?i)" + word, "***");
}
return result;
}
@Override
public String getName() { return "CensorPlugin"; }
// Override Validatable default with stricter validation
@Override
public boolean isValidInput(String input) {
return Validatable.super.isValidInput(input) && !input.isBlank();
}
}
class MarkdownPlugin implements TextPlugin, Validatable, Measurable {
@Override
public String process(String input) {
return input
.replaceAll("\\*\\*(.+?)\\*\\*", "$1")
.replaceAll("\\*(.+?)\\*", "$1")
.replaceAll("`(.+?)`", "$1");
}
@Override
public String getName() { return "MarkdownPlugin"; }
}
// ============================================================
// Plugin Pipeline
// ============================================================
class PluginPipeline {
private final List plugins = new ArrayList<>();
void addPlugin(TextPlugin plugin) {
plugins.add(plugin);
}
String execute(String input) {
String result = input;
for (TextPlugin plugin : plugins) {
result = plugin.safeProcess(result);
}
return result;
}
void executeWithLogging(String input) {
String result = input;
for (TextPlugin plugin : plugins) {
result = plugin.processWithLogging(result);
}
System.out.println("Final result: " + result);
}
}
// ============================================================
// Main Application
// ============================================================
public class PluginSystemDemo {
public static void main(String[] args) {
System.out.println("=== Individual Plugins ===");
// Using static factory methods
TextPlugin upper = TextPlugin.uppercase();
System.out.println(upper.process("hello world"));
// Output: HELLO WORLD
TextPlugin replacer = TextPlugin.replace("Java", "Java 8");
System.out.println(replacer.process("Learning Java is fun!"));
// Output: Learning Java 8 is fun!
// Chained plugin via static factory
TextPlugin pipeline = TextPlugin.chain(
new TrimPlugin(),
TextPlugin.uppercase(),
TextPlugin.replace(" ", "_")
);
System.out.println(pipeline.process(" hello world "));
// Output: HELLO_WORLD
System.out.println("\n=== Censor Plugin with Validation ===");
CensorPlugin censor = new CensorPlugin(List.of("spam", "scam"));
System.out.println(censor.process("This is spam and scam content"));
// Output: This is *** and *** content
System.out.println("Valid input: " + censor.isValidInput("test"));
// Output: Valid input: true
System.out.println("Blank valid: " + censor.isValidInput(" "));
// Output: Blank valid: false (stricter override)
System.out.println("\n=== Markdown Plugin with Timing ===");
MarkdownPlugin markdown = new MarkdownPlugin();
String html = markdown.process("This is **bold** and *italic* and `code`");
System.out.println(html);
// Output: This is bold and italic and code
long ms = markdown.measureProcessingTime(() ->
markdown.process("Processing **this** text")
);
System.out.println("Processing took: " + ms + "ms");
// Output: Processing took: 0ms (very fast)
System.out.println("\n=== Plugin Pipeline ===");
PluginPipeline fullPipeline = new PluginPipeline();
fullPipeline.addPlugin(new TrimPlugin());
fullPipeline.addPlugin(new CensorPlugin(List.of("bad")));
fullPipeline.addPlugin(TextPlugin.uppercase());
String result = fullPipeline.execute(" this has bad words ");
System.out.println("Pipeline result: " + result);
// Output: Pipeline result: THIS HAS *** WORDS
System.out.println("\n=== Safe Processing (null/error handling) ===");
TextPlugin upper2 = TextPlugin.uppercase();
System.out.println(upper2.safeProcess(null));
// Output: [empty input]
System.out.println(upper2.safeProcess(""));
// Output: [empty input]
System.out.println(upper2.safeProcess("works fine"));
// Output: WORKS FINE
System.out.println("\n=== Batch Processing ===");
List batch = List.of("hello", "world", "java");
System.out.println(upper2.processAll(batch));
// Output:
// HELLO
// WORLD
// JAVA
}
}
Concepts demonstrated in this example:
| # | Concept | Where Used |
|---|---|---|
| 1 | Abstract methods in interfaces | TextPlugin.process(), TextPlugin.getName() |
| 2 | Default methods | processAll(), safeProcess(), processWithLogging() |
| 3 | Static factory methods | TextPlugin.uppercase(), TextPlugin.replace(), TextPlugin.chain() |
| 4 | Default methods calling abstract methods | safeProcess() calls process() |
| 5 | Interface Segregation Principle | Separate Validatable and Measurable interfaces |
| 6 | Overriding default methods | CensorPlugin.isValidInput() overrides Validatable's default |
| 7 | Calling super default with Interface.super |
CensorPlugin.isValidInput() delegates to Validatable.super |
| 8 | Multiple interface implementation | MarkdownPlugin implements TextPlugin, Validatable, Measurable |
| 9 | Anonymous class with interface | Static factory methods return anonymous implementations |
| 10 | Template method pattern via defaults | safeProcess() wraps process() with null/error handling |
| 11 | Strategy pattern | Different TextPlugin implementations plugged into PluginPipeline |
| 12 | Method references with defaults | this::process in processAll() |
| Topic | Key Point |
|---|---|
| Default method syntax | default returnType method() { body } |
| Static method syntax | static returnType method() { body } |
| Private method syntax (9+) | private returnType method() { body } |
| Why default methods exist | Backward-compatible interface evolution (adding methods without breaking implementors) |
| Why static methods exist | Utility and factory methods that belong to the interface, not instances |
| Diamond problem rule 1 | Classes always win over interface defaults |
| Diamond problem rule 2 | More specific (sub) interface wins over parent interface |
| Diamond problem rule 3 | If unresolved, the class must explicitly override |
| Calling a specific default | InterfaceName.super.method() |
| Functional interface compatibility | Default and static methods do NOT count toward the single abstract method limit |
| Cannot override Object methods | Defaults cannot define equals(), hashCode(), or toString() |
| Static methods not inherited | Must call via InterfaceName.staticMethod(), never via an instance |
| Use interface when | Defining a capability contract; need multiple inheritance |
| Use abstract class when | Need instance state, constructors, or protected methods |
| Best pattern | Interface for contract + abstract class for skeletal implementation |