Command Palette

Search for a command to run...

Level 2 · 20 min

Abstraction

Abstraction is the process of hiding complex implementation details and showing only the necessary features. It lets you focus on what an object does rather than how it does it.

Interface vs Abstract Class

Interface: pure contract, no state, multiple implementation. Use when defining a capability that unrelated classes can have (Serializable, Comparable, Runnable). Abstract Class: partial implementation, can have state and concrete methods. Use when sharing implementation across closely related classes. Java 8+ default methods blur the line — use abstract class when you need constructors or non-public state. Bloch's Item 64 (Effective Java) crystallizes the rule: "If appropriate interface types exist, then parameters, return values, variables, and fields should all be declared using interface types." Declaring LinkedHashSet<Son> sonSet = new LinkedHashSet<>() instead of Set<Son> sonSet = new LinkedHashSet<>() ties callers to the concrete type unnecessarily — swapping to HashSet later requires touching every declaration. Item 20 further refines the interface/abstract-class tradeoff: "Interfaces are generally superior" for defining types, but abstract classes can evolve more easily — adding a non-abstract method to an abstract class is safe, while adding a default method to an interface risks breaking existing implementations that already have a method with the same signature.

Design by Contract

Design by Contract (DbC) formalizes method contracts with preconditions (what the caller must guarantee), postconditions (what the implementer guarantees after the call), and invariants (what is always true). Abstraction establishes a contract — callers depend on the contract, not the implementation. Java has Javadoc @throws for documenting precondition violations.

Levels of Abstraction

Good code operates at a single level of abstraction per method/class. A method that processes an HTTP request should not also format SQL queries — mixing levels of abstraction is a design smell. When you read a method and find both high-level business logic ('place the order') and low-level implementation details ('execute prepared statement'), the method is poorly abstracted.

Key Takeaways

  • Interface for capability contracts (what it can do). Abstract class for implementation sharing (how related classes share code).
  • A method should operate at one level of abstraction — don't mix business logic with low-level I/O.
  • Leaky abstraction: when the caller needs to know implementation details to use the abstraction correctly.

Code example

// Interface: capability contract
interface Sortable<T> {
  int compareTo(T other);  // contract: 0=equal, negative=less, positive=greater
}

// Abstract class: shared implementation for related classes
abstract class AbstractRepository<T, ID> {
  protected final DataSource dataSource;

  AbstractRepository(DataSource ds) { this.dataSource = ds; }

  abstract String tableName();

  void delete(ID id) {
    // shared implementation using tableName() hook
    String sql = "DELETE FROM " + tableName() + " WHERE id = ?";
    // execute...
  }

  abstract Optional<T> findById(ID id);  // subclass provides mapping
}

class UserRepository extends AbstractRepository<User, UUID> {
  String tableName() { return "users"; }
  Optional<User> findById(UUID id) { /* User-specific mapping */ }
}