Interfaces vs. Abstract Classes: Modern Java Differences
Interview Question: "Since Java 8 introduced default methods, the line between interfaces and abstract classes blurred. How do they actually differ now in terms of state, multiple inheritance conflicts, and method resolution rules?"
The Quick Answer
"Even though modern Java allows interfaces to have concrete implementations via default, static, and private methods, the core difference remains state. Abstract classes can maintain mutable instance state through instance fields and initialize it with constructors; hence, a class can only extend one abstract class. Interfaces remain strictly stateless contracts; they have no instance fields, no constructors, and a class can implement multiple interfaces."
Modern Java Features: Interfaces (Java 8 & Java 9+)
Before Java 8, interfaces could only declare abstract methods. Modern Java expanded this dramatically:
defaultMethods (Java 8): Added primarily for backward compatibility. It allowed library maintainers (like the Java Collections Framework) to add new methods (e.g.,Collection.stream(),Iterable.forEach()) to existing interfaces without breaking millions of legacy implementing classes.staticMethods (Java 8): Utility methods relevant to the contract (e.g.,Comparator.comparing()). They cannot be overridden by implementing classes and are invoked directly viaInterfaceName.methodName().privateMethods (Java 9): Helper methods used internally within the interface to share common logic between multipledefaultmethods, without exposing that helper logic to external implementers.
The 3 Core Differences That Still Remain
| Feature | Abstract Class | Modern Interface (Java 8 / 9+) |
|---|---|---|
| Instance State | ✅ Can have mutable instance variables. | ❌ Stateless. Only constants (public static final). |
| Constructors | ✅ Has constructors to initialize state via super(). | ❌ No constructors. Cannot be instantiated. |
| Inheritance Limit | ❌ Single class inheritance (extends only one). | ✅ Multiple inheritance of type (implements many). |
Conflict Resolution & Multiple Inheritance Rules
Because a class can implement multiple interfaces, default methods introduce the possibility of method collisions. Java resolves these with three strict compiler rules:
Rule 1: The "Class Always Wins" Rule
If a class inherits a concrete method from a superclass and a default method from an interface with the identical signature, the superclass implementation always wins. The interface default method is completely ignored.
Note: This is also why an interface can never declare default implementations for
java.lang.Objectmethods (toString(),equals(),hashCode())—the class hierarchy always overrides interface defaults.
Rule 2: Sub-interface Wins Over Super-interface
If InterfaceB extends InterfaceA, and both define the same default method, the more specific sub-interface (InterfaceB) wins.
Rule 3: The Conflict Rule (Compiler Error)
If a class implements two completely independent interfaces (InterfaceA and InterfaceB) that both provide the same default method, the compiler will refuse to compile:
class inherits unrelated defaults for method() from types InterfaceA and InterfaceB
The Fix: The child class MUST explicitly override the colliding method. Inside the override, you can provide completely custom logic or delegate to a specific interface using the syntax:
InterfaceA.super.methodName();
Clean Code Example
Here is how multiple inheritance method collisions are resolved across different languages:
- Java
- C++
- Python
interface CloudService {
default void connect() {
logAttempt();
System.out.println("Connecting to Cloud Service...");
}
// Java 9: Private helper method shared within the interface
private void logAttempt() {
System.out.println("Initiating network handshake...");
}
}
interface OnPremService {
default void connect() {
System.out.println("Connecting to On-Premises Server...");
}
}
// Conflict Resolution: HybridClient implements two interfaces with identical connect() methods
class HybridClient implements CloudService, OnPremService {
// MUST explicitly override to resolve ambiguity
@Override
public void connect() {
// Explicitly choose which default implementation to execute via InterfaceName.super
CloudService.super.connect();
System.out.println("Hybrid fallback established.");
}
}
#include <iostream>
class CloudService {
public:
virtual ~CloudService() = default;
virtual void connect() {
logAttempt();
std::cout << "Connecting to Cloud Service..." << std::endl;
}
private:
void logAttempt() {
std::cout << "Initiating network handshake..." << std::endl;
}
};
class OnPremService {
public:
virtual ~OnPremService() = default;
virtual void connect() {
std::cout << "Connecting to On-Premises Server..." << std::endl;
}
};
// Conflict Resolution: In C++, disambiguate using the Scope Resolution Operator (::)
class HybridClient : public CloudService, public OnPremService {
public:
void connect() override {
// Explicitly scope which base method to execute
CloudService::connect();
std::cout << "Hybrid fallback established." << std::endl;
}
};
class CloudService:
def connect(self) -> None:
self._log_attempt()
print("Connecting to Cloud Service...")
def _log_attempt(self) -> None:
print("Initiating network handshake...")
class OnPremService:
def connect(self) -> None:
print("Connecting to On-Premises Server...")
# Conflict Resolution: In Python, MRO (Method Resolution Order) picks the first parent,
# or you can explicitly call the specific class method passing self:
class HybridClient(CloudService, OnPremService):
def connect(self) -> None:
# Explicitly invoke the desired parent implementation
CloudService.connect(self)
print("Hybrid fallback established.")