Skip to content

VOL 01 / CH 08 / LESSON 04

8.4 Abstract Classes, Interfaces, and Object ​

Prerequisites: Inheritance, method overriding, and dynamic dispatch Estimated time: 35 minutes You are ready to continue when: You can choose an abstract class, an interface, or composition based on state, reuse, and the caller's contract

The general MessageSender itself does not provide a concrete transportation mechanism. If it offers a seemingly functional default send, and a subclass forgets to override it, the error is only revealed at runtime. Meanwhile, the auditor has inherited Worker but still needs to adopt a "sendable" scheduling contract.

1. Abstract Classes Provide a Controlled Structure ​

This page defines senders that return String. Compile them separately from the previous page's void-returning version. They only produce demonstration text and do not contact delivery services.

java
import java.util.Objects;

abstract class MessageSender {
    private final String message;

    MessageSender(String message) {
        this.message = Objects.requireNonNull(message, "message");
        if (message.isBlank()) {
            throw new IllegalArgumentException("blank message");
        }
    }

    protected final String message() {
        return message;
    }

    protected final String formattedMessage() {
        return "Message:" + message;
    }

    public abstract String send();
}

Abstract classes cannot be instantiated directly, but they can contain constructors, instance state, concrete methods, and abstract methods. Constructors are used to initialize the parent portion of objects in descendant classes.

java
final class SmsSender extends MessageSender {
    SmsSender(String message) {
        super(message);
    }

    @Override
    public String send() {
        return formattedMessage() + "; demo SMS channel";
    }
}

Non-abstract subclasses must implement all inherited abstract methods; otherwise, compilation fails. Abstract methods have no method body and cannot be declared private, static, or final, because these modifiers contradict the idea that the method is meant to be overridden by subclasses.

The base class keeps its state private and exposes only protected final operations, preventing subclasses from directly depending on field representations. This remains part of the inheritance contract: changing the semantics of a protected method may still break existing subclasses.

2. Interfaces Define the Capability a Caller Needs ​

The scheduling center only cares about "being able to send":

java
interface Sendable {
    String send();
}

Abstract methods in interfaces are implicitly public and abstract; their implementation must not reduce visibility:

java
final class ConsoleChannel implements Sendable {
    private final String message;

    ConsoleChannel(String message) {
        this.message = java.util.Objects.requireNonNull(message);
    }

    @Override
    public String send() {
        return "Console: " + message;
    }
}

A class can extend only one class but can implement multiple interfaces. Interfaces can also extend multiple interfaces. This enables Auditor extends Worker implements Sendable, Trackable, though multiple contracts may still conflict in method semantics or default implementations.

Interfaces have no constructors or instance fields. Their fields are implicitly public static final and are shared across implementations. Final prevents reassignment; it does not make a referenced list or array immutable. Only primitive or String fields satisfying the constant-expression rules are compile-time constants. Interface fields therefore cannot hold each object's individual state.

3. Java 17 Interfaces Can Contain Implementations ​

java
interface Sendable {
    String send();

    default String channelName() {
        return normalize("unknown");
    }

    static boolean isValid(String message) {
        return message != null && !message.isBlank();
    }

    private static String normalize(String value) {
        return value.trim().toLowerCase(java.util.Locale.ROOT);
    }
}
  • Default methods provide a default implementation for instance methods, which can be overridden.
  • Static methods belong to the interface and are invoked via Sendable.isValid(...), not inherited as instance methods in implementing classes.
  • Since Java 9, interfaces can include private methods to reuse internal implementations.

Default methods can evolve interfaces in certain scenarios, but they do not guarantee that adding methods will never break existing implementations. A new default method may cause signature conflicts with methods in implementing classes, parent classes, or other interfaces, or it may alter the resolution behavior of existing method calls.

Default Method Conflicts ​

java
interface Audited {
    default String label() { return "audit"; }
}

interface Tracked {
    default String label() { return "track"; }
}

final class Auditor implements Audited, Tracked {
    @Override
    public String label() {
        return Audited.super.label() + "+" + Tracked.super.label();
    }
}

When two unrelated interfaces provide default methods with the same signature, the implementing class must resolve the conflict. General resolution principles include "class methods take precedence" and "more specific interface preference"; in complex hierarchies, the compiler and JLS rules must be consulted to ensure correctness, relying on memorized rules or heuristics is not sufficient.

4. When to Choose Abstract Classes, Interfaces, and Composition ​

RequirementPreferred Choice
The caller needs only one capability contractInterface
Multiple unrelated types need the same capabilityInterface
Subclasses share stable state and a controlled skeletonAbstract class
You only want to reuse a collaborator's implementationComposition / delegation
You need multiple interchangeable strategiesInterface + composition

"Use an abstract class when sharing fields" is just the starting point. If types don't satisfy the same replacement contract, moving state into independent components and composing them is often more reliable. Public APIs typically make callers depend on interfaces, while whether internal implementations share an abstract base class is a separate decision.

5. Object is the root of the class hierarchy ​

Except for Object itself, every Java class directly or indirectly extends java.lang.Object. Arrays are also objects. Common methods include:

java
Object value = new SmsSender("The weather is turning sunny");
System.out.println(value.toString());
System.out.println(value.getClass().getName());
System.out.println(value.equals(value));
System.out.println(value.hashCode());

Object's default equals compares object identity. Its default hashCode is not specified to return a memory address. When defining value equality, preserve both the equals and hashCode contracts; otherwise, hash-based collections can behave incorrectly.

toString is a readable representation for developers and should not include passwords, tokens, or personal data by default, nor should it be used as a stable serialization format.

java
@Override
public String toString() {
    return "SmsSender{messageLength=" + message().length() + "}";
}

6. Interface References Still Point to Ordinary Objects ​

java
Sendable sender = new ConsoleChannel("Hold meeting tonight");
System.out.println(sender.send());

You cannot instantiate an interface directly, but an interface variable can reference an object that implements it. Instance methods are still dynamically dispatched based on the actual class. The value of interfaces lies in reducing caller dependencies, not in creating another special kind of object.

Professional Reference ​

FeatureAbstract ClassInterface
ConstructorPresentAbsent
Instance StatePresentAbsent
Abstract MethodsPresentPresent
Concrete Instance MethodsPresentdefault or private
Static MethodsPresentPresent
Number of Direct Usages by a ClassExtends one classImplements multiple interfaces

Before Proceeding ​

Let a dispatch center depend on Sendable when all it needs is sending behavior. Abstract classes should be introduced only when sharing controlled state and skeletons is truly necessary. Compile two interface implementations that conflict on default methods, and explicitly select between Audited.super and Tracked.super. When overriding value equality, you must also synchronize the maintenance of hashCode; default object strings must not be treated as addresses or serialized formats.

→ Next: Inheritance, Interfaces, and Polymorphism Experiment

Specification reference: JLS §9: Interfaces.

Built with VitePress | Software Systems Atlas