Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Java abstract classes: shared invariants and extension points

Last updated: 28 Sept 20263 min read
tutorial
IntermediateBy AITrove Editorial

An abstract class can define state and implemented methods while requiring subclasses to supply selected operations; it cannot be instantiated directly.

Java 8+. Use a JDK that supports this release.

Keep the policy boundary small

A dispatch request has an invariant: its destination must be usable. Validation belongs in the base constructor because every transport shares that rule. The transport-specific receipt is the extension point.

The final dispatch method keeps the operation sequence fixed. A subclass supplies a receipt format through an abstract method, but it cannot bypass the base validation by replacing dispatch. This is useful only when those restrictions actually belong to the abstraction.

Java permits one direct class superclass. If a type only needs to promise behavior, an interface avoids consuming that inheritance choice. If several implementations share storage and a controlled method sequence, an abstract class can carry those requirements.

Do not call extension points during construction

The superclass constructor runs before subclass initialization completes. Calling an overridable method there can read uninitialized subclass fields. Store validated base state first and call extension points only after the object is constructed.

This sample returns a receipt string. It does not send a message or charge a carrier account. Real external dispatch requires failure handling and an idempotency contract; an inheritance hierarchy supplies neither.

Working program

Java
public class DispatchPolicies {
    static abstract class DispatchRequest {
        private final String destination;
        DispatchRequest(String destination) {
            if (destination == null || destination.trim().isEmpty()) {
                throw new IllegalArgumentException("Destination is required");
            }
            this.destination = destination;
        }
        public final String dispatch() { return receipt(destination); }
        protected abstract String receipt(String destination);
    }
    static final class WarehouseRequest extends DispatchRequest {
        WarehouseRequest(String destination) { super(destination); }
        protected String receipt(String destination) {
            return "queued:" + destination;
        }
    }
    public static void main(String[] args) {
        DispatchRequest request = new WarehouseRequest("delhi-warehouse");
        System.out.println(request.dispatch());
    }
}

Output

Output
queued:delhi-warehouse

Cost and design choices

Dispatch here performs work proportional to the destination text copied into the result. The hierarchy does not change that cost. Avoid claiming method dispatch is the bottleneck before measuring the complete operation.

Protected mutable fields couple every subclass to storage details. Private state plus a narrow extension contract keeps validation under one owner. Composition is often simpler when policies vary independently.

Common Mistakes

  • Do not call abstract or overridable methods from a constructor.
  • Do not inherit solely to reuse an unrelated helper.
  • Do not make validated state publicly mutable.

Connect the contracts

Compare the boundary explained in Inheritance boundaries with the assumptions made by this program.

Compare the boundary explained in Runtime dispatch with the assumptions made by this program.

java
abstract-classes
Storage details