Java Interfaces vs Abstract Classes
Choose between interfaces and abstract classes with confidence. Default methods, sealed interfaces, functional interfaces, and concrete decision rules.
What you'll learn
- ✓Declare interfaces with abstract, default, and static methods
- ✓Implement multiple interfaces and resolve conflicts
- ✓Decide when to use an abstract class instead
- ✓Spot and use functional interfaces
- ✓Apply sealed interfaces to model closed sums
Prerequisites
Interfaces in Java used to be pure contracts: no state, no implementation. That has not been true since Java 8. Modern interfaces can carry default methods, static helpers, and now sealed permits. The line between an interface and an abstract class is thinner than ever, but the right choice is still clear once you know what to look for.
A basic interface
public interface Greeter {
String greet(String name);
}
public class English implements Greeter {
@Override
public String greet(String name) {
return "Hello, " + name;
}
}
Methods are implicitly public abstract. Fields are implicitly public static final (constants). You do not need the keywords.
Multiple inheritance of type
A class can implement many interfaces but extend only one class. That is how Java avoids the diamond problem while still letting one type satisfy many contracts.
public class Worker implements Runnable, AutoCloseable {
@Override public void run() { /* ... */ }
@Override public void close() { /* ... */ }
}
default methods
Java 8 added default methods so interfaces could grow without breaking every implementer.
public interface Greeter {
String greet(String name);
default String greetLoudly(String name) {
return greet(name).toUpperCase();
}
}
Implementers can override greetLoudly or accept the default. If a class implements two interfaces that both define the same default method, the compiler forces you to resolve it:
class Hybrid implements A, B {
@Override
public String hello() {
return A.super.hello(); // pick one explicitly
}
}
static methods on interfaces
Use static methods for factories and utilities that belong with the type.
public interface Greeter {
String greet(String name);
static Greeter english() {
return name -> "Hello, " + name;
}
}
This pattern is everywhere in the standard library: List.of(...), Path.of(...), Stream.of(...).
private methods on interfaces
Since Java 9, interfaces can have private helper methods used by their default methods, so you do not leak internal helpers into the public contract.
public interface Logger {
void log(String msg);
default void warn(String msg) { log(prefixed("WARN", msg)); }
default void info(String msg) { log(prefixed("INFO", msg)); }
private String prefixed(String level, String msg) {
return "[" + level + "] " + msg;
}
}
Abstract classes
An abstract class is a partial implementation. It can declare fields, constructors, and non public state. It is still a class, so an implementer can only extend one.
public abstract class Cache<K, V> {
private final Map<K, V> store = new HashMap<>();
public V get(K key) {
return store.computeIfAbsent(key, this::load);
}
protected abstract V load(K key);
}
Subclasses inherit store, get, and only need to fill in load. You cannot do this with an interface because interfaces cannot hold mutable state.
Interface or abstract class?
Use an interface when:
- You only want to define a contract.
- Different unrelated classes should be able to satisfy it.
- Callers should be able to use lambdas (functional interfaces).
Use an abstract class when:
- You need to share field state across subclasses.
- You want a constructor to enforce invariants.
- You are providing a template with concrete and abstract parts that share private state.
A useful rule: start with an interface. Only move to an abstract class when shared mutable state forces you to.
Functional interfaces
An interface with exactly one abstract method is a functional interface. It can be implemented with a lambda or method reference.
@FunctionalInterface
public interface Transformer<T, R> {
R apply(T input);
}
Transformer<String, Integer> length = s -> s.length();
System.out.println(length.apply("Codeloom")); // 8
The standard library already provides many: Function, Consumer, Supplier, Predicate, Runnable, Callable. Use these before inventing your own. They power Streams and Lambdas.
The @FunctionalInterface annotation is optional but signals intent and makes the compiler enforce the single abstract method rule.
sealed interfaces
Java 17 lets you restrict who can implement an interface:
public sealed interface Result<T> permits Ok, Err {}
public record Ok<T>(T value) implements Result<T> {}
public record Err<T>(String message) implements Result<T> {}
The closed set of implementations makes pattern matching exhaustive:
static String describe(Result<Integer> r) {
return switch (r) {
case Ok<Integer> ok -> "got " + ok.value();
case Err<Integer> e -> "fail: " + e.message();
};
}
This is how you model algebraic data types in modern Java. Add a new permitted type and every non exhaustive switch is a compile error, which is exactly what you want.
Marker interfaces
An interface with no methods, used purely as a type tag. Serializable and Cloneable are the famous examples. They are mostly historical; today, prefer annotations (@Deprecated, custom annotations) for metadata. Still worth recognizing in older code.
Diamond conflicts in defaults
If two interfaces declare the same default method, the implementer must override. If a class extends a superclass and implements an interface and both define the same method, the class wins. The rule is: classes beat interfaces, more specific interfaces beat less specific ones, and any remaining tie is a compile error you must break manually.
Wrap up
Interfaces describe what a thing can do; abstract classes provide a partial how. Default to interfaces, lean on sealed to model closed hierarchies, and let abstract classes earn their place by holding state. Next, put these contracts to work in Java Collections Framework.