Abstraction, Abstract Classes and Interfaces in Java

Abstraction exposes what a type does and hides how. Abstract classes share partial implementation, interfaces declare a capability.

Abstraction as an idea

Abstraction means describing what a type offers without committing to how it works. A caller depending on the abstraction is insulated from every implementation detail behind it. Java provides two tools for it: abstract classes and interfaces.

Abstract classes

abstract class Employee {

    private final String name;

    protected Employee(String name) {
        this.name = name;
    }

    public String name() {              // shared implementation
        return name;
    }

    public abstract double monthlyPay(); // each subtype must supply this

    public String payslip() {            // built on the abstract method
        return name + " earns " + monthlyPay();
    }
}
class SalariedEmployee extends Employee {

    private final double annualSalary;

    SalariedEmployee(String name, double annualSalary) {
        super(name);
        this.annualSalary = annualSalary;
    }

    @Override
    public double monthlyPay() {
        return annualSalary / 12;
    }
}
  • An abstract class cannot be instantiated, but it can have constructors, called by subclasses.
  • It may hold state, and it may mix abstract and concrete methods.
  • A subclass must implement every abstract method or be declared abstract itself.

Interfaces

interface Searchable {

    List<String> search(String term);        // implicitly public abstract

    default boolean matches(String term) {   // a default implementation
        return !search(term).isEmpty();
    }

    static Searchable empty() {              // a static factory
        return term -> List.of();
    }
}
class NoteIndex implements Searchable {
    @Override
    public List<String> search(String term) {
        return List.of("Introduction to Java");
    }
}

What an interface may contain

MemberSinceNote
Abstract methods1.0Implicitly public abstract
Constants1.0Implicitly public static final
default methods8Allow interfaces to evolve without breaking implementers
static methods8Helpers that belong with the type
private methods9Shared code between default methods
Default methods exist mainly for backward compatibility. When forEach was added to Iterable in Java 8, every existing implementation would have broken without them. They are not a licence to put substantial logic into interfaces.

Choosing between them

QuestionAbstract classInterface
Can hold instance stateYesNo, only constants
ConstructorsYesNo
How many per classOneMany
Access modifiers on membersAnyPublic, or private helpers
ModelsAn is-a relationship with shared codeA capability or a contract
Typical nameA noun: EmployeeA capability: Comparable, Runnable

The practical rule: if subtypes share state and real implementation, use an abstract class. If you are declaring what something can do, use an interface. When in doubt, start with an interface, because a class may implement many.

Using both together

interface Payment {
    double amount();
    String reference();
}

abstract class OnlinePayment implements Payment {

    private final String reference;

    protected OnlinePayment(String reference) {
        this.reference = reference;
    }

    @Override
    public String reference() {
        return reference;
    }

    protected abstract String gateway();
}

class CardPayment extends OnlinePayment {

    private final double amount;

    CardPayment(String reference, double amount) {
        super(reference);
        this.amount = amount;
    }

    @Override public double amount()  { return amount; }
    @Override protected String gateway() { return "card-gateway"; }
}

The interface defines the contract, the abstract class supplies what every online payment shares, and the concrete class fills in the rest. This layering is common in real systems.

Functional interfaces

@FunctionalInterface
interface Validator {
    boolean isValid(String value);
}

Validator notBlank = value -> value != null && !value.isBlank();

An interface with exactly one abstract method can be implemented by a lambda. The annotation is optional but makes the intent explicit and prevents a second abstract method being added by accident.

Common mistakes

  • Creating an interface for a class that will only ever have one implementation.
  • Declaring fields in an interface and being surprised they are constants shared by all.
  • Putting real business logic into default methods.
  • Using an abstract class where no state or shared code exists.
  • Forgetting that an implementing method must be public, since interface methods are public by definition.

Best practices

  • Program to interfaces: List<String> names = new ArrayList<>();
  • Keep interfaces small and focused on one capability.
  • Give an abstract class a protected constructor, since only subclasses should call it.
  • Use default methods to add convenience or to evolve an API, not to hold the core logic.
  • Consider a sealed interface when the set of implementations is meant to be closed.

Practice

  1. Design a Notification abstraction with email and SMS implementations, and justify interface or abstract class.
  2. Why can an interface not declare a constructor?
  3. Add a default method to an interface with two existing implementations and explain what did not break.
  4. What happens when a class implements two interfaces that declare the same default method?
  5. Convert an abstract class with no state into an interface and list what changed.

Conclusion

Abstraction is choosing what to expose. Use an interface to declare a capability, an abstract class to share partial implementation, and prefer depending on the abstraction rather than on any concrete class.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.