Interfaces vs. Abstract Classes: Modern Java Differences
Interview Question: "Since Java 8 let interfaces have real implementations, the line between interfaces and abstract classes got blurry. What's actually still different between them?"
The short version
Interfaces can now write real code too - default, static, and private methods - so "interfaces are just empty contracts" isn't quite true anymore. But one thing never changed: state.
An abstract class can hold mutable instance fields and set them up in a constructor, which is exactly why a class can only extend one - you can't have two sets of conflicting state. An interface is still stateless: no instance fields, no constructor, and a class can implement as many as it wants.
What Java 8 and 9 actually added to interfaces
Before Java 8, an interface could only declare abstract methods - no bodies at all. That changed:
defaultmethods (Java 8): added mainly so library maintainers could add new methods to existing interfaces - likeCollection.stream()- without breaking every class that already implements them.staticmethods (Java 8): utility methods tied to the interface itself, likeComparator.comparing(). Called directly asInterfaceName.methodName(), and implementing classes can't override them.privatemethods (Java 9): internal helpers that let twodefaultmethods share logic, without exposing that helper to anyone implementing the interface.
What's still fundamentally different
| Feature | Abstract Class | Modern Interface (Java 8 / 9+) |
|---|---|---|
| Instance state | ✅ Can hold mutable instance variables | ❌ Stateless - only constants (public static final) |
| Constructors | ✅ Has one, to set up state via super() | ❌ None. Can't be instantiated |
| How many you can combine | One (extends a single class) | Many (implements as many as you want) |
When default methods collide
Since a class can implement several interfaces, two of them might hand it the same default method. Java resolves this with three fixed rules.
Rule 1: a real class always wins
If a class inherits a concrete method from its superclass, and also gets a default method with the same signature from an interface, the superclass version wins outright - the interface's default is ignored completely.
This is also why an interface can never override
Objectmethods liketoString(),equals(), orhashCode()with a default implementation - the class hierarchy always wins over an interface default.
Rule 2: the more specific interface wins
If InterfaceB extends InterfaceA and both define the same default method, InterfaceB's version wins - the more specific interface beats the general one.
Rule 3: two unrelated interfaces with the same default = compiler error
If a class implements two independent interfaces that both define the same default method, Java won't guess which one you meant. It refuses to compile:
class inherits unrelated defaults for method() from types InterfaceA and InterfaceB
The fix: override the method yourself in the class. Inside, you can write new logic or explicitly delegate to one of the interfaces:
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.")