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.
Default methods and posterity
1 / 2SynchronizedCollection 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.Entryshould 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 (
Mapviews,Iteratorimplementations). Created asouter.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.
Nested classes
1 / 2Map.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.javadefinesUtensil+Dessert→Mainprintspancake.- Duplicate definitions in
Dessert.java→ multiply-defined error if both compiled, orpotpieifjavac 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.
One class per file
1 / 2Utensil.java and Dessert.java both define package-private Utensil and Dessert
Pick an answer to see if you are right.
Items 21–25 map
| Item | Move |
|---|---|
| 21 | Design interfaces carefully; avoid risky default-method retrofits |
| 22 | Interfaces define types — not constant buckets |
| 23 | Hierarchy beats tagged class + switch |
| 24 | static nested unless you need the outer instance |
| 25 | One top-level class per source file |
Next: Generics (Chapter 5, Items 26–29).