Abstraction
Abstraction is the practice of exposing what a type does while hiding how it does it. It lets callers depend on a contract rather than an implementation, so internals can change freely without breaking clients. Java expresses abstraction through two tools: abstract classes and interfaces.
Abstract Methods
An abstract method declares a signature with no body. It defines an obligation: any concrete subtype must supply an implementation.
public abstract class Notifier {
// Abstract: behavior deferred to subclasses
public abstract void send(String message);
// Concrete: shared logic lives here
public void notifyAll(java.util.List<String> messages) {
messages.forEach(this::send);
}
}
A class containing an abstract method must itself be declared abstract.
Abstract Classes
An abstract class cannot be instantiated directly—new Notifier() is a compile error. It exists to be extended, and it can mix abstract methods with concrete methods, fields, and constructors. This makes it ideal for sharing partial implementation across a family of related types.
public class EmailNotifier extends Notifier {
@Override
public void send(String message) {
System.out.println("Email: " + message);
}
}
Notifier n = new EmailNotifier();
n.notifyAll(java.util.List.of("Hi", "Welcome"));
Output:
Email: Hi
Email: Welcome
Note: An abstract class may have zero abstract methods. Declaring it
abstractsimply prevents instantiation—useful for base classes meant only to be extended.
Interfaces
An interface is a pure contract. Historically every method was implicitly public abstract; since Java 8, interfaces may also carry default and static methods with bodies. A class can implement many interfaces, making them Java’s answer to multiple inheritance of type.
public interface Auditable {
String auditTag(); // abstract
default void audit() { // default
System.out.println("AUDIT " + auditTag());
}
}
Abstract Class vs Interface
Choosing between the two is a recurring design decision. The table below summarizes the trade-offs.
| Aspect | Abstract Class | Interface |
|---|---|---|
| Instantiable | No | No |
| Multiple inheritance | One per class | Many per class |
| Fields | Any (incl. mutable instance state) | public static final constants only |
| Constructors | Yes | No |
| Method bodies | Concrete + abstract | default/static/private + abstract |
| State sharing | Yes | No |
| Best models | An is-a with shared state | A can-do capability |
When to Use Each
- Reach for an abstract class when subtypes share state and partial implementation, and the relationship is a strong is-a (e.g.,
AbstractList). - Reach for an interface when you are defining a capability that unrelated types can adopt, or when a type needs to fulfill several contracts at once (e.g.,
Comparable,Closeable).
Tip: Modern Java leans toward interfaces with default methods for flexibility, falling back to abstract classes only when shared mutable state genuinely demands it.
Best Practices
- Expose the smallest contract that satisfies your callers—abstraction is about hiding, so reveal little.
- Prefer interfaces for capabilities; use abstract classes when shared state or constructors are required.
- Keep abstract methods focused; a fat abstract type is hard to implement correctly.
- Document the contract (pre/postconditions) each abstract method must honor.
- Do not leak implementation types into your public API; return and accept abstractions.
Interview Questions
Q: Can an abstract class have a constructor?
A: Yes. It runs via super(...) when a subclass is instantiated, even though the abstract class itself cannot be instantiated directly.
Q: When would you choose an abstract class over an interface? A: When subtypes must share mutable state, fields, or a non-trivial partial implementation, and the relationship is a genuine is-a. Interfaces cannot hold instance state.
Q: Can an abstract class have no abstract methods?
A: Yes. It is still abstract and cannot be instantiated; this is a common pattern for base classes that only provide shared concrete behavior.