Abstract Classes and Interfaces
Abstract Classes
An abstract class is declared with the abstract keyword. It cannot be instantiated directly — the expression new AbstractClass() is a compile error.
An abstract class may include:
- Concrete methods — methods with a body, providing implementation shared by all subclasses.
- Abstract methods — methods declared with a signature but no body (ending with a semicolon). Subclasses must provide implementations of all abstract methods in order to become concrete (instantiable) classes.
abstract class Shape {
protected String colour;
public Shape(String colour) {
this.colour = colour;
}
public String getColour() { // concrete — shared implementation
return colour;
}
public abstract double area(); // abstract — each shape calculates differently
public abstract double perimeter();
}
A subclass that does not implement all inherited abstract methods must itself be declared abstract:
abstract class Quadrilateral extends Shape {
public Quadrilateral(String colour) { super(colour); }
@Override
public double perimeter() { return 0; } // implement one...
// area() still abstract — so Quadrilateral is still abstract
}
class Rectangle extends Quadrilateral {
private double width, height;
public Rectangle(String colour, double w, double h) {
super(colour);
this.width = w;
this.height = h;
}
@Override
public double area() { return width * height; }
@Override
public double perimeter() { return 2 * (width + height); }
}
Why Abstract Classes?
Abstract classes are the right tool when:
- Subclasses share common state (fields) — the abstract class can hold those fields and provide constructor logic.
- Subclasses share common behaviour (concrete methods) — these live in the abstract class.
- But each subclass must provide its own version of some specific behaviour — these are the abstract methods.
This is the template method pattern at the class-design level: the abstract class defines the structure and the subclasses fill in the details.
Interfaces
An interface declares a contract — a set of method signatures that implementing classes must provide. A class uses the implements keyword (not extends) to adopt an interface:
interface Drawable {
void draw(Graphics g);
boolean contains(Point p);
}
class Circle extends Shape implements Drawable {
@Override
public void draw(Graphics g) { /* rendering code */ }
@Override
public boolean contains(Point p) { /* hit-test code */ }
@Override
public double area() { return Math.PI * radius * radius; }
@Override
public double perimeter() { return 2 * Math.PI * radius; }
}
A class can implement any number of interfaces — multiple interface inheritance is allowed, unlike multiple class inheritance.
Interface Members
By default, all methods in an interface are:
public(you cannot have a non-public method in an interface, exceptprivatehelper methods since Java 9)abstract(unless markeddefaultorstatic)
Fields in an interface are implicitly public static final — they are constants, not instance variables:
interface Constants {
double PI = 3.14159; // implicitly public static final
int MAX_SIZE = 100;
}
Interfaces have no constructors, no instance fields, and no private state. They are purely behavioural contracts.
Default Methods (Java 8+)
Since Java 8, interfaces can include default methods — methods with a body, providing a default implementation that implementing classes inherit. A class can override the default if it needs different behaviour:
interface Logger {
void log(String message);
default void logError(String message) {
log("ERROR: " + message); // delegates to abstract log()
}
}
Default methods were introduced primarily to allow evolution of existing interfaces without breaking existing implementations. If you add a new abstract method to a widely-used interface, every implementing class would need updating. Adding it as a default method avoids this.
Static Methods in Interfaces (Java 8+)
Interfaces can also contain static methods, which are called on the interface itself:
interface MathUtils {
static double clamp(double value, double min, double max) {
return Math.max(min, Math.min(value, max));
}
}
double c = MathUtils.clamp(3.7, 0.0, 1.0); // 1.0
These replace the common pattern of companion utility classes.
Private Methods in Interfaces (Java 9+)
Interfaces can have private methods (both instance and static) to share code between default methods without exposing it:
interface Logger {
void log(String message);
default void logWarning(String message) {
log(formatMessage("WARN", message));
}
default void logError(String message) {
log(formatMessage("ERROR", message));
}
private String formatMessage(String level, String message) {
return "[" + level + "] " + message;
}
}
Choosing: Abstract Class or Interface?
| Criterion | Abstract Class | Interface |
|---|---|---|
| Shared state (fields) | Yes | No (only constants) |
| Constructors | Yes | No |
| Access modifiers on methods | Any | Implicitly public |
| Single vs multiple inheritance | One only (extends) | Many (implements) |
| Default implementations | Yes (concrete methods) | Yes (default methods, Java 8+) |
| Semantic relationship | Is-a | Can-do / is-capable-of |
Use an abstract class when the subtypes form a tight family that shares state and implementation. Think of it as providing a partial implementation that subclasses complete.
Use an interface when you are defining a capability that can be adopted by unrelated classes — a Comparable can be any type of object that knows how to compare itself to another; a Runnable can be any object that can be executed. These concepts cut across class hierarchies.
The Key Insight
A memorable way to think about the distinction from the course:
extendsis for a hierarchy you understand fully — you control or deeply understand the superclass. You are committing to inheriting its implementation and being tightly coupled to it.implementsis for a contract that many unrelated classes might need to fulfil. You are not inheriting code (or only via default methods); you are promising to provide specific methods.
This distinction guides large-scale design: prefer interfaces for public APIs and inter-component boundaries; use abstract classes for shared implementation within a component.