Abstract Classes, Interfaces, and Code Organization

In the previous lesson, Vehicle had a quiet problem: nothing stops you from writing new Vehicle("Ford"). And a “generic vehicle” does not exist in the real world. It is a concept, not a thing.

Worse still: Vehicle.start() had to invent a default implementation ("The vehicle starts.") that no subclass actually uses. We wrote code purely so it would compile.

Java has two tools to fix this, and choosing badly between them is one of the design decisions you pay for the longest:

  • Abstract class: an incomplete mold. It brings state and already-solved behavior, and leaves gaps the subclass is forced to fill.
  • Interface: a pure contract. It says nothing about how anything is done, only what whoever signs it must be able to do.

1. Abstract classes: molds that cannot be instantiated

Comparison between the anatomy of an abstract class and that of an interface Abstract class — an incomplete mold public abstract class Shape { protected String name; ← can hold STATE public abstract double area(); ← no body: forces the subclass to write it public void describe() { ... } ← has a body: inherited as is Cannot be instantiated: new Shape() is an error. And a class extends ONLY ONE abstract class. Interface — a pure contract public interface Drawable { int MAX_LAYERS = 10; ← constant: public static final void draw(); ← abstract and public by default default void highlight() { ... } ← default implementation (Java 8+) static Drawable empty() { ... } ← utility belonging to the interface itself Holds no instance state. And a class may implement AS MANY interfaces as it wants.
The abstract class contributes state and inheritable code; the interface contributes a contract any class can sign, no matter what it inherits from.

A class marked abstract cannot be instantiated. It exists only to be extended:

public abstract class Shape {
    protected final String name;

    protected Shape(String name) {              // yes, abstract classes have constructors
        this.name = name;
    }

    // Abstract method: no body. Every subclass MUST implement it.
    public abstract double area();
    public abstract double perimeter();

    // Concrete method: already solved, inherited as is.
    public void describe() {
        System.out.printf("%s → area %.2f, perimeter %.2f%n",
            name, area(), perimeter());
    }
}

Look carefully at describe(), because that is where the whole point lives: it calls two methods that do not exist yet. The abstract class writes the general algorithm once and delegates the concrete steps to whoever extends it.

public class Circle extends Shape {
    private final double radius;

    public Circle(double radius) {
        super("Circle");
        if (radius <= 0) {
            System.out.println("Invalid radius, used 1 by default.");
            radius = 1;
        }
        this.radius = radius;
    }

    @Override
    public double area() { return Math.PI * radius * radius; }

    @Override
    public double perimeter() { return 2 * Math.PI * radius; }
}

public class Rectangle extends Shape {
    private final double width, height;

    public Rectangle(double width, double height) {
        super("Rectangle");
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() { return width * height; }

    @Override
    public double perimeter() { return 2 * (width + height); }
}

And now new Shape(...) does not even compile. The compiler stopped allowing the object that made no sense. That is exactly what we were after.

// Shape s = new Shape("something");   // ERROR: Shape is abstract; cannot be instantiated

Shape[] shapes = { new Circle(3), new Rectangle(4, 5) };
for (Shape s : shapes) {
    s.describe();   // polymorphism, same as the previous lesson
}

If a subclass does not implement every abstract method it inherits, it must be declared abstract as well. Java will not let you have a concrete class with holes in it.


2. Interfaces: contracts anyone can sign

An interface describes what can be done, never how:

public interface Payable {
    // Every method is public abstract by default: no need to write it
    boolean pay(double amount);
    boolean isAvailable();
}

Any class can sign it with implements, and the compiler forces it to honor the whole thing:

public class CreditCard implements Payable {
    private final String number;
    private double availableCredit;

    public CreditCard(String number, double availableCredit) {
        this.number = number;
        this.availableCredit = availableCredit;
    }

    // true if the charge went through, false if it was rejected for insufficient credit
    @Override
    public boolean pay(double amount) {
        if (amount > availableCredit) {
            System.out.println("Insufficient credit.");
            return false;
        }
        availableCredit -= amount;
        System.out.println("Paid with card " + number);
        return true;
    }

    @Override
    public boolean isAvailable() { return availableCredit > 0; }
}

The key point: CreditCard inherits from nobody. The interface does not consume the single extends you have. That is its decisive advantage.

default and static methods

Since Java 8, an interface can carry implementations:

public interface Payable {
    boolean pay(double amount);
    boolean isAvailable();

    // default: inheritable implementation that classes may override or not
    default void payIfPossible(double amount) {
        if (isAvailable()) {
            pay(amount);
        } else {
            System.out.println("Payment method unavailable.");
        }
    }

    // static: a utility belonging to the interface, not to the classes
    static boolean isValidAmount(double amount) {
        return amount > 0 && amount < 1_000_000;
    }
}

default methods exist for a very concrete reason: they let you add a new method to an interface without breaking the thousand classes already implementing it. Before Java 8, adding a method to a public interface broke the whole ecosystem depending on it.

Use them sparingly. An interface full of default methods stops being a contract and starts being a badly disguised abstract class.


3. Implementing several interfaces at once

This is where you see why interfaces are not simply “abstract classes without code”:

A class extends a single abstract class but implements several interfaces «abstract class» Bird eat(), plumage «interface» Swimmer swim() «interface» Flyer fly(), maxAltitude() extends (solid line, only one) implements (dashed, as many as you like) Duck extends Bird implements Swimmer, Flyer inherits eat(); implements swim() and fly() Java has no multiple class inheritance, but an object can honor as many contracts as it needs.
One line of inheritance, many contracts. That is why interfaces are the tool for combining capabilities that share no common ancestor.
public class Duck extends Bird implements Swimmer, Flyer {
    @Override public void swim() { System.out.println("The duck swims."); }
    @Override public void fly()  { System.out.println("The duck flies."); }
}

And now the same object can be seen from different angles depending on what each method needs:

Duck duck = new Duck();

Bird b = duck;       // as a bird
Swimmer s = duck;    // as something that swims
Flyer f = duck;      // as something that flies

// A method that only needs something to swim does not need to know it is a duck:
public void swimmingContest(Swimmer[] participants) {
    for (Swimmer participant : participants) {
        participant.swim();
    }
}

That array can hold a Duck, a Fish, and a Submarine — three classes that share absolutely no ancestor. The interface is the only thing they have in common, and it is enough.


4. Which one to pick

CriterionAbstract classInterface
Relationship it expresses”is a” — shared identity”is capable of” — shared capability
How many you can useOnly one (extends)As many as you want (implements)
Instance stateYes, ordinary fieldsNo, only static final constants
ConstructorsYesNo
Method visibilityAnything, including protectedAlways public
Adding a method laterBreaks subclasses if it is abstractBreaks nothing if it is default

The practical rule that holds in 90% of cases:

Interface by default. Abstract class only when there is genuinely shared state or code you do not want to repeat.

And the two combine perfectly well — that is the most common pattern in serious libraries:

public interface CourseRepository {
    void save(Course course);
    Course findById(long id);
}

// Abstract base that solves the repetitive part: searching the internal store
public abstract class InMemoryCourseRepository implements CourseRepository {
    protected final Course[] store = new Course[100];
    protected int count = 0;

    @Override
    public Course findById(long id) {
        for (int i = 0; i < count; i++) {
            if (store[i].getId() == id) {
                return store[i];
            }
        }
        return null;   // not found
    }
    // save() stays abstract: each concrete subclass decides what to validate before saving
}

5. Code Organization: Packages and the Namespace Concept in Java

What is a namespace?

In computer science and software engineering, a namespace is an abstract container designed to group identifiers (classes, interfaces, types) and provide them with a unique context to prevent name collisions.

As a project grows and pulls in third-party libraries (for instance database drivers, HTTP clients, or domain models), name overlap becomes inevitable: two developers might declare an Order class, or your internal domain might define Date while your SQL driver also supplies its own Date. Without namespaces, all types would compete in a single flat global scope, leaving the compiler unable to determine which one you meant.

Java has no namespace keyword: it uses package

Unlike languages like C++, C#, or PHP, Java does not have a dedicated namespace keyword. Instead, the language natively implements the namespace concept through three complementary pillars:

  1. The package declaration: establishes the namespace for every type declared in the source file.
  2. The Fully Qualified Class Name (FQCN): the canonical, unambiguous identity of a class to the JVM.
  3. The import statement: a lexical shortcut so you do not have to type the full FQCN on every single reference.

Furthermore, while namespaces in C# or C++ are purely logical constructs decoupled from physical storage, Java enforces a strict physical rule: the namespace declared in package must match the directory hierarchy on the classpath exactly.

A project's package structure and how it maps to the package declaration The folder path and the package declaration must match. Always. com.facundouferer.shop domain Product.java · Customer.java · Order.java service CartService.java · PaymentService.java Main.java the application entry point package com.facundouferer.shop.domain; First line of every file. If it does not match the real path, it will not compile. import ...shop.domain.Product; Needed to use a class from another package. Within the same package it is unnecessary. Convention: your domain, reversed. facundouferer.ar → com.facundouferer So two different libraries never collide. Group by responsibility (domain, service, repository), not by artifact type (all the interfaces in one bucket).
Packages are a project's first layer of architecture: the folder name already tells you what lives inside it.
package com.facundouferer.shop.domain;   // first line, mandatory

public class Product { ... }

Fully Qualified Class Name (FQCN) and type identity

To the JVM, a class’s true identity is never just its short name (Product). Its absolute identity to the ClassLoader is its Fully Qualified Class Name (FQCN):

com.facundouferer.shop.domain.Product

Thanks to this hierarchical namespace system, two homonymous classes can coexist without any ambiguity within the same application:

  • com.facundouferer.shop.domain.Product (internal core domain entity)
  • com.supplier.catalog.Product (DTO imported from an external supplier API)

Resolving name collisions with import

The import statement does not load bytecode into memory; it merely registers an alias so you do not have to write out the full FQCN every time you reference a class.

What happens if you need to use two classes with the same short name from different namespaces/packages within the same source file?

// ERROR: Java does not allow importing two classes with the same short name in the same compilation unit
import java.util.Date;
import java.sql.Date; // Compile error: 'Date is already defined in a single-type import'

Java rejects this ambiguity. To resolve the namespace collision:

  1. Import the class you use most frequently (it will use its short name).
  2. Disambiguate the second class by writing its full FQCN directly in your declarations or instantiations:
package com.facundouferer.shop.service;

import java.util.Date; // Unqualified Date resolves to java.util.Date

public class AuditService {
    // Uses the short name from the imported namespace:
    private Date operationDate = new Date();

    // Explicit disambiguation using the full FQCN to prevent namespace collision:
    private java.sql.Date databaseDate = new java.sql.Date(System.currentTimeMillis());

    public void record() {
        System.out.println("In-memory date (java.util): " + operationDate);
        System.out.println("Database date (java.sql): " + databaseDate);
    }
}

Namespace-level encapsulation: package-private

Namespaces in Java are more than organizational folders; they are architectural trust boundaries.

As introduced in the Constructors, Access Modifiers, and Getters/Setters lesson, Java’s default access level (no modifier, or package-private) restricts visibility strictly to types living in the exact same package/namespace. This allows you to expose a clean public API (public) while keeping implementation helpers and internal domain mechanics completely shielded from external code.

Core rules for package and namespace design

  • Reverse domain convention: Always begin package names with your inverted internet domain (com.facundouferer), guaranteeing that your namespaces remain globally unique across public repositories like Maven Central.
  • All lowercase: No uppercase letters, underscores, or hyphens (shop.domain, not shop_domain).
  • One .java file per public class: And the filename must match the class name exactly.
  • Group by responsibility, not artifact type: shop.domain and shop.service express clean architecture; shop.interfaces and shop.classes merely duplicate technical mechanics without architectural meaning.
  • Judicious static imports: import static java.lang.Math.PI; pulls static members directly into the file’s lexical namespace, but use it sparingly to avoid obscuring where methods come from.

6. Modeling relationships: association, aggregation, composition

Inheritance is not the only way to connect classes, nor the most common. In practice most relationships are containment, and they are told apart by a single question: what happens to the part when the whole disappears?

Association, aggregation, and composition ordered from the weakest relationship to the strongest The three ways to relate objects, from weakest to strongest Teacher Course Association — they know each other and collaborate, but each lives on its own. Team Player Aggregation — the whole gathers parts that already existed. Disband it and they remain. House Room Composition — the part cannot exist without the whole. Demolish the house, it goes too. Hollow diamond: the part survives. Filled diamond: the part dies with the whole, which creates it in its constructor.
The question that decides which is which: if I destroy the container, does the part still make sense on its own?
import java.util.Arrays;

// ASSOCIATION: they know each other, neither owns the other
public class Teacher {
    private Course[] courses = new Course[10];
    private int courseCount = 0;

    public void assign(Course c) {
        if (courseCount == courses.length) {
            courses = Arrays.copyOf(courses, courses.length * 2);
        }
        courses[courseCount] = c;
        courseCount++;
    }
}

// AGGREGATION: the team receives players that already existed and outlives them
public class Team {
    private final Player[] players;

    public Team(Player[] players) {
        this.players = Arrays.copyOf(players, players.length);   // defensive copy, see Arrays of Objects
    }
}

// COMPOSITION: the house CREATES its rooms and never lets them go
public class House {
    private final Room[] rooms;

    public House(int roomCount) {
        rooms = new Room[roomCount];
        for (int i = 0; i < roomCount; i++) {
            rooms[i] = new Room(i + 1);   // creates them right here
        }
    }

    public int roomCount() { return rooms.length; }
    // There is no getRooms(): nobody outside touches the parts
}

Notice the pattern in the code: in composition, the container creates the parts in its own constructor and does not expose them. In aggregation, it receives them from outside. That difference in the code is exactly the conceptual difference.


7. Common mistakes

MistakeWhat happensHow to fix it
Using an abstract class where an interface belongsBurns the single available extends and the class can no longer inherit from what it actually needs.Always start with the interface; add an abstract class only if there is shared state.
An interface with a default for every methodIt stops being a contract and becomes an abstract class with no constructor and no state.default is for evolving an interface without breaking implementations, not for writing logic.
Declaring mutable fields in an interfaceEvery field in an interface is public static final: a shared global constant, not object state.If you need state, you need a class (abstract or otherwise).
Package that does not match the folderConfusing compile errors about classes that “do not exist”.The package declaration must mirror the exact path.
Namespace collision when importing homonymous classes (e.g. two Date or Order classes)Compile error: is already defined in a single-type import.Disambiguate the namespace by importing one and using the Fully Qualified Class Name (FQCN) for the other.
Modeling as aggregation something that is compositionThe part gets exposed and someone outside mutates it, or shares it between two containers.If the part cannot live without the whole: create it inside and do not expose it.
One giant package holding every classpackage-private protects nothing and the architecture is unreadable.Split by responsibility from day one.

8. Guided hands-on exercise

Challenge: payment methods

  1. Define the Payable interface with boolean pay(double amount) (true if the charge succeeds, false if it is rejected), boolean isAvailable(), and a default method payIfPossible(double amount) that only charges when the method is available.
  2. Implement it in CreditCard (with available credit) and in DigitalWallet (with an account balance).
  3. Create an abstract class DigitalPaymentMethod implements Payable that stores the holder’s email and resolves isAvailable() using an active flag, leaving pay() abstract.
  4. Make DigitalWallet extend that abstract class.
  5. In main, build a Payable[] and charge the same amount to every one of them in a single loop, with no instanceof and no casting.
See suggested solution
public interface Payable {
    boolean pay(double amount);
    boolean isAvailable();

    default boolean payIfPossible(double amount) {
        if (amount <= 0) {
            System.out.println("  ✗ Invalid amount, charge skipped.");
            return false;
        }
        if (isAvailable()) {
            return pay(amount);
        }
        System.out.println("  ✗ Method unavailable, charge skipped.");
        return false;
    }
}

// Abstract class: contributes the STATE and the shared behavior.
public abstract class DigitalPaymentMethod implements Payable {
    protected final String email;
    protected boolean active;

    protected DigitalPaymentMethod(String email) {
        if (email == null || !email.contains("@")) {
            System.out.println("Invalid email, used \"no-email@example.com\" by default.");
            email = "no-email@example.com";
        }
        this.email = email;
        this.active = true;
    }

    @Override
    public boolean isAvailable() {
        return active;
    }

    public void deactivate() { this.active = false; }

    // pay() stays abstract: each digital method charges its own way.
}

public class DigitalWallet extends DigitalPaymentMethod {
    private double balance;

    public DigitalWallet(String email, double balance) {
        super(email);
        this.balance = balance;
    }

    @Override
    public boolean isAvailable() {
        return super.isAvailable() && balance > 0;   // reuses and refines
    }

    @Override
    public boolean pay(double amount) {
        if (amount > balance) {
            System.out.printf("  ✗ Wallet (%s) — insufficient balance.%n", email);
            return false;
        }
        balance -= amount;
        System.out.printf("  ✓ Wallet (%s) — remaining balance $%.2f%n", email, balance);
        return true;
    }
}

// Inherits from nobody: it only signs the contract.
public class CreditCard implements Payable {
    private final String lastFour;
    private double availableCredit;

    public CreditCard(String lastFour, double availableCredit) {
        this.lastFour = lastFour;
        this.availableCredit = availableCredit;
    }

    @Override
    public boolean isAvailable() { return availableCredit > 0; }

    @Override
    public boolean pay(double amount) {
        if (amount > availableCredit) {
            System.out.printf("  ✗ Card ****%s — insufficient credit.%n", lastFour);
            return false;
        }
        availableCredit -= amount;
        System.out.printf("  ✓ Card ****%s — remaining credit $%.2f%n",
            lastFour, availableCredit);
        return true;
    }
}

public class MainCharges {
    public static void main(String[] args) {
        DigitalWallet emptyWallet = new DigitalWallet("empty@mail.com", 0);

        Payable[] methods = {
            new CreditCard("4417", 50000),
            new DigitalWallet("facu@mail.com", 30000),
            emptyWallet                                // no balance: will be skipped
        };

        System.out.println("Charging $12,500 to every method:");
        for (Payable method : methods) {
            method.payIfPossible(12500);   // the default decides; nobody asks for the type
        }
    }
}

Two things to look at here.

First: CreditCard and DigitalWallet share no ancestor whatsoever, and still live in the same Payable[]. The interface was enough.

Second: DigitalWallet.isAvailable() calls super.isAvailable() and adds its own condition on top. It reuses the abstract class’s rule instead of repeating it — exactly the pattern you used with super.computeSalary() in the previous lesson.


Key takeaways

  • An abstract class is an incomplete mold: it contributes state and code, forces the gaps to be filled, and cannot be instantiated.
  • An interface is a contract: it says what can be done, not how, and it does not consume your single extends.
  • Practical rule: start with the interface; add an abstract class only when there is genuinely shared state or logic.
  • default methods exist so an interface can evolve without breaking the classes already implementing it.
  • A class extends one class but implements every interface it needs. That is Java’s answer to multiple inheritance.
  • The package is Java’s native implementation of the namespace concept: it prevents collisions via FQCNs, must strictly match the folder hierarchy, and should be grouped by responsibility, not by artifact type.
  • Association, aggregation, and composition are told apart by one question: if I destroy the whole, does the part still exist?