skip to content
everything.effectiveJava.classesAndInterfaces

classes and interfaces

items 15–25: accessibility, immutability, composition, interfaces.

chapter
chapter 16.03
reading time
14 min read

items 15 to 25 are about the shape of your types. hide everything you can (encapsulation), make objects immutable unless there's a reason not to, compose instead of extending classes you don't control, and model "one of these kinds" as a sealed hierarchy instead of a class with a type tag.

eleven items. amber marks the ones records, sealed types, modules and default methods changed.

hiding things

item 15 · make everything as private as it can be

encapsulation is what lets you change a class without breaking its users. start every member at private, and loosen only when a real caller needs it. protected feels cozy but it's public API: any subclass anywhere can depend on it forever.

member =
other modules
your module
package com.shop
class Cart
protected int total;
subclass, other pkg
subclass, other module

the package, plus subclasses anywhere. it's part of your exported API forever, just like public.

pick an access level and see who can reach a member. green can see it. since java 17 the module boundary is real: a public class in an unexported package is invisible outside its module.

one sneaky leak: a public static final array. final stops you from replacing the array, not from changing what's in it.

don'tanyone can rewrite it
Main.java
class Config {
    public static final String[] MODES =
        {"read", "write"};
}
 
void main() {
    Config.MODES[0] = "delete";
    IO.println(Config.MODES[0]);
}
output
delete
dohand out an unmodifiable list
java
class Config {
    public static final
        List<String> MODES =
            List.of("read", "write");
}
// Config.MODES.set(0, "x") throws
// UnsupportedOperationException

item 16 · public classes expose methods, not fields

a public field can't validate, can't change representation, can't be made thread-safe, and can't be removed. an accessor can do all four. (package-private and private nested classes can expose fields, they're not API.)

don'tthe field is the API now
java
public class Point {
    public int x;
    public int y;
}
dothe method is the API
java
public record Point(int x, int y) {}
// p.x() is an accessor. the field
// stays private and final.

item 17 · make things immutable

an immutable object has one state, forever. it's thread-safe without locks, it can be shared and cached freely, and it makes a perfect map key or set element. the rules: no setters, a final class (or private constructors), private final fields, and no way for anyone to reach a mutable field inside.

that last rule is the one records don't do for you. a record is shallowly immutable:

don'tthe record changed behind its back
Main.java
record Team(String name,
            List<String> members) {}
 
void main() {
    var list = new ArrayList<String>();
    list.add("ada");
    var team = new Team("core", list);
    list.add("bo");
    IO.println(team.members());
}
output
[ada, bo]
docopy in the compact constructor
Main.java
record Team(String name,
            List<String> members) {
    Team {
        members = List.copyOf(members);
    }
}
 
void main() {
    var list = new ArrayList<String>();
    list.add("ada");
    var team = new Team("core", list);
    list.add("bo");
    IO.println(team.members());
}
output
[ada]

"change" an immutable object by returning a new one (BigDecimal.add, LocalDate.plusDays, String.strip). the cost is an allocation per step, which is usually nothing (see item 6). when it isn't, a mutable companion (like StringBuilder for String) does the heavy lifting. the full story, including why String is immutable, is in immutability.

inheritance

item 18 · compose, don't extend

extending a class you don't control ties your correctness to its implementation details. the classic failure: you override one method and assume the others route through it.

your codenames.add("ada")names.addAll(…, null)names.add(0, null)NoNullList (yours)add(e)checks for null ✓ArrayList (JDK)addAll(c)add(index, e)Object[] elementDatanulladabonullcopies into the array, no add()what you assumedthe subclass's promise depends on how the superclass happens to be written.
the subclass guards add, but addAll and add(index, e) copy straight into ArrayList's array. what you assumed isn't what happens, so nulls get in.
Main.java
import java.util.*;
 
class NoNullList<E> extends ArrayList<E> {
    @Override public boolean add(E e) {
        return super.add(Objects.requireNonNull(e, "no nulls here"));
    }
}
 
public class Main {
    public static void main(String[] args) {
        var names = new NoNullList<String>();
        names.add("ada");
        names.addAll(Arrays.asList("bo", null)); // doesn't go through add()
        names.add(0, null);                      // neither does this
        System.out.println(names);
    }
}
output
[null, ada, bo, null]

and it's fragile in both directions: if a future JDK made addAll call add, a subclass that counted adds would suddenly double count. composition doesn't care. wrap the list, expose only what you check, forward the rest:

java
final class NoNullList<E> {
    private final List<E> items = new ArrayList<>();
 
    void add(E e) { items.add(Objects.requireNonNull(e, "no nulls here")); }
    void addAll(Collection<? extends E> es) { es.forEach(this::add); }
    E get(int i) { return items.get(i); }
    int size() { return items.size(); }
}

inheritance is fine inside one package you own, or for classes designed to be extended (next item). it's also only right for a true "is-a". more in composition over inheritance.

item 19 · design for subclassing, or forbid it

a class meant to be extended must document which overridable methods it calls on itself, and must never call them from a constructor: the subclass's override runs before the subclass's own constructor has set anything.

predict the output
java
class Widget {
    Widget() { render(); }         // calls an overridable method
    void render() { }
}
 
class Label extends Widget {
    private final String text;
    Label(String text) { this.text = text; }
    @Override void render() { IO.println("rendering " + text); }
}
 
void main() {
    new Label("hi").render();
}

if you didn't design for it, forbid it: make the class final, or give it only private constructors and static factories.

interfaces

item 20 · prefer interfaces to abstract classes

a class can implement many interfaces but extend only one class, so an interface can be retrofitted onto existing classes and used as a mixin (Comparable, AutoCloseable). pair the interface with a skeletal implementation (AbstractList, AbstractMap) and implementers write only the essentials:

Main.java
static List<Integer> squares(int n) {
    return new AbstractList<>() {
        @Override public Integer get(int i) { return Objects.checkIndex(i, n) * i; }
        @Override public int size() { return n; }
    };
}
 
void main() {
    var sq = squares(6);
    IO.println(sq);   // toString, iteration, equals: all inherited
    IO.println(sq.contains(16) + " " + sq.indexOf(25) + " " + sq.subList(1, 3));
}
output
[0, 1, 4, 9, 16, 25]
true 5 [1, 4]

two methods written, a full read-only List back: contains, indexOf, subList, iterator, equals, hashCode, all derived from get and size.

item 21 · default methods can't save a bad interface

adding a default method to a published interface injects code into every existing implementation, without the authors knowing. java 8's own Collection.removeIf default was correct in general, but wrong for synchronized wrapper classes written before it existed: they didn't override it, so it ran without taking their lock. design interfaces carefully up front, and test them with several implementations before release. after that, they're hard to change.

item 22 · interfaces are types, not bags of constants

an interface should say "an object of this type can do X". an interface that only holds constants (so classes can implement it to skip the prefix) leaks those constants into the implementing class's API forever.

don'tthe constant interface
java
interface Physics {
    double G = 6.674e-11;
}
class Orbit implements Physics { }
doa utility class + static import
java
final class Physics {
    private Physics() {}
    static final double G = 6.674e-11;
}
// import static ...Physics.G;

item 23 · no tagged classes, use a hierarchy

a "tagged class" has a kind field and a switch on it in every method, plus fields that only make sense for some kinds. it's verbose, error-prone (add a kind, miss a switch) and wastes memory. the fix is a type per kind.

don'tone class, a tag, and switches
java
class Shape {
    enum Kind { CIRCLE, RECT }
    final Kind kind;
    double r;      // circles only
    double w, h;   // rects only
 
    double area() {
        switch (kind) {
            case CIRCLE: return Math.PI * r * r;
            case RECT: return w * h;
            default: throw new AssertionError();
        }
    }
}
dosealed interface + records
Main.java
sealed interface Shape
        permits Circle, Rect, Tri {}
record Circle(double r)
        implements Shape {}
record Rect(double w, double h)
        implements Shape {}
record Tri(double b, double h)
        implements Shape {}
 
static double area(Shape s) {
    return switch (s) {
        case Circle c ->
            Math.PI * c.r() * c.r();
        case Rect(var w, var h) -> w * h;
        case Tri(var b, var h) ->
            b * h / 2;
    };
}
 
void main() {
    IO.println(area(new Circle(1)));
    IO.println(area(new Rect(2, 3)));
    IO.println(area(new Tri(4, 5)));
}
output
3.141592653589793
6.0
10.0

no default branch in the good version, on purpose: the switch is exhaustive over a sealed type, so adding a fourth record turns every unhandled switch into a compile error instead of a runtime AssertionError.

odds and ends

item 24 · nested classes: static unless you need the outer this

there are four kinds, and only one quietly holds a reference to its enclosing object:

  • ├── static member classa regular class that lives in another's namespace. the default choice
  • ├── inner classnon-static. each instance points at an outer instance
  • ├── anonymous classdeclared and instantiated in one expression. mostly lambdas now
  • └── local classdeclared inside a method. rare
the four kinds of nested class. the inner one carries a hidden reference to the outer instance, if it uses it.

that hidden reference costs memory and can keep a big outer object alive (an inner-class listener registered somewhere global is a textbook leak). if the nested class doesn't need the outer instance, declare it static.

Outer.java
public class Outer {
    int clicks;
    class Handler { void run() { clicks++; } } // uses the outer instance
    class Quiet { }                            // doesn't
    static class Nested { }
 
    public static void main(String[] args) {
        for (var c : new Class<?>[] { Handler.class, Quiet.class, Nested.class }) {
            var names = java.util.Arrays.stream(c.getDeclaredFields()).map(f -> f.getName()).toList();
            System.out.println(c.getSimpleName() + " " + names);
        }
    }
}
output, from jdk 25
Handler [this$0]
Quiet []
Nested []

this one needs a real JVM (reflection), so it doesn't run in the browser. copy it into java Main.java to try it.

item 25 · one top-level class per source file

java lets you put several top-level classes in one file, and if two files define the same top-level class, which one you get depends on the order you pass files to javac. one class per file, named after it, makes that impossible. if you want helpers next to their user, make them static nested classes.

keylines
  • start private. protected and public are promises you keep forever.
  • immutable objects are simple, shareable and thread-safe. records are only shallowly so: copy mutable components.
  • extending a class you don't own ties you to its internals. compose and forward.
  • design a class for inheritance (no overridable calls in constructors) or make it final or sealed.
  • interfaces for types, skeletal classes for convenience, never interfaces for constants.
  • a type tag plus switches wants to be a sealed hierarchy of records.