Skip to main content

Interfaces and Nested Classes

Source: Effective Java, 3rd Edition — Chapter 4 by Joshua Bloch (Items 21–25)

This lesson finishes Chapter 4: designing interfaces for the long term, using interfaces only as types, replacing tagged classes with hierarchies, choosing the right nested class, and keeping one top-level class per source file.

Item 21: Design interfaces for posterity​

Before Java 8, adding a method to an interface broke every existing implementation at compile time. Default methods (Java 8) inject a shared implementation into classes that do not override the new method — but there is no guarantee that default works for every preexisting type.

Example: Collection.removeIf (Java 8) walks with an iterator and calls it.remove(). A solid general-purpose default — yet SynchronizedCollection wrappers that synchronize every call but do not override removeIf inherit unsynchronized traversal. Concurrent mutation can yield ConcurrentModificationException or worse. JDK maintainers overrode defaults in platform synchronized wrappers; third-party wrappers may still be broken.

Lessons:

  • Avoid adding default methods to released interfaces unless critical; think hard about wrapper / invariant breakage.
  • Default methods are great on new interfaces to ease implementation (Item 20).
  • Default methods do not let you remove or change existing method signatures safely.
  • Design interfaces carefully up front — flaws irritate users forever. Test with ≥3 diverse implementations and multiple client programs before release.
Quiz

Default methods and posterity

1 / 2

SynchronizedCollection does not override removeIf (Java 8+)

Pick an answer to see if you are right.

Item 22: Use interfaces only to define types​

When a class implements an interface, that interface is a type describing what clients can do with instances. Do not use interfaces for other purposes.

Constant interface antipattern: an interface with only static final fields, implemented so callers need not qualify names:

// Do not do this
public interface PhysicalConstants {
static final double AVOGADROS_NUMBER = 6.022_140_857e23;
}

Problems: constants leak into the class’s exported API as implements; binary compatibility traps you; subclasses inherit constant namespace pollution. Platform examples (ObjectStreamConstants) are anomalies.

Better exports:

  • Constants tied to a class → add to that class (Integer.MAX_VALUE).
  • Constants as a set → enum (Item 34).
  • Otherwise → noninstantiable utility class (Item 4) + optional static import.

Underscores in numeric literals (Java 7+): no value change; use for readability (groups of three in base 10 when ≥5 digits).

Item 23: Prefer class hierarchies to tagged classes​

A tagged class carries a tag field (enum Shape) and switches on it — circle vs rectangle in one class:

// Inferior — tag + switch + irrelevant fields
class Figure {
enum Shape { RECTANGLE, CIRCLE }
final Shape shape;
double length, width; // rectangle only
double radius; // circle only
double area() { switch (shape) { ... } }
}

Shortcomings: boilerplate, jumbled logic, wasted fields, non-final irrelevant fields, runtime switch misses, closed to extension without editing the switch, type gives no flavor hint.

Fix: subtyping — abstract root with abstract flavor-dependent methods; concrete subclass per flavor:

abstract class Figure { abstract double area(); }
class Circle extends Figure { ... }
class Rectangle extends Figure { ... }

Each flavor gets its own type, final fields, compile-time enforcement, independent extension. Hierarchies can model natural relationships (Square extends Rectangle).

Item 24: Favor static member classes over nonstatic​

Four nested kinds: static member, nonstatic member (inner), anonymous, local. Only static member is not an “inner class.”

  • Static member class: like a normal class nested for packaging; no implicit outer this. Use when the nested type does not need an enclosing instance (Map.Entry should be static — otherwise each entry holds a wasteful back-reference and can leak the map).
  • Nonstatic member class: each instance tied to one outer instance (Map views, Iterator implementations). Created as outer.new Inner() or from outer instance methods.
  • Anonymous / local: short (≈10 lines); lambdas replaced most anonymous function objects (Item 42).

If a member class does not need the outer instance → static. Omitting static adds hidden outer reference (space, construction cost, GC leak risk — Item 7).

Public/protected nested classes in exported types: static vs nonstatic is a binary compatibility commitment.

Quiz

Nested classes

1 / 2

Map.Entry implemented as a nonstatic member class (missing static)

Pick an answer to see if you are right.

Item 25: Limit source files to a single top-level class​

The compiler allows multiple top-level classes per .java file, but never do it. Two files defining the same package-private classes compile order-dependent behavior:

  • Utensil.java defines Utensil + Dessert → Main prints pancake.
  • Duplicate definitions in Dessert.java → multiply-defined error if both compiled, or potpie if javac Dessert.java Main.java.

Fix: one public top-level class per file (name matches file). Related helpers → private static member classes (Item 24) instead of extra top-level types in one file.

Quiz

One class per file

1 / 2

Utensil.java and Dessert.java both define package-private Utensil and Dessert

Pick an answer to see if you are right.

Items 21–25 map​

ItemMove
21Design interfaces carefully; avoid risky default-method retrofits
22Interfaces define types — not constant buckets
23Hierarchy beats tagged class + switch
24static nested unless you need the outer instance
25One top-level class per source file

Next: Generics (Chapter 5, Items 26–29).