Chapter 7: Methods
effective-java methods api-design java best-practices
Book: Effective Java, 3rd Edition — Joshua Bloch
Status: 🟩 Complete
Difficulty: Medium
Items: 49-56 (8 items)
Time to complete: ~45 min
Overview
Chapter 7 focuses on the design and implementation of methods — the fundamental unit of a Java API. Every decision you make about parameters, return values, overloading, varargs, and documentation shapes how usable, safe, and maintainable your API will be. Bad method design creates subtle bugs that are hard to detect and painful to fix after the API is published. This chapter teaches you to write defensive, clear, and well-documented methods that are difficult to misuse.
The central theme: make it easy to do the right thing and hard to do the wrong thing. Validate inputs eagerly, protect mutable state, design signatures so callers intuitively know what to pass, and document everything that a caller needs to know.
Items
Item 49: Check Parameters for Validity
The Problem
Many methods have restrictions on their parameters — an index must be non-negative, an object must be non-null, a value must fall within a range. If you fail to check these restrictions early, the method may fail partway through with a confusing exception, or worse, silently return a wrong result that corrupts state discovered much later.
// BAD: No parameter validation — fails with cryptic ArrayIndexOutOfBoundsException
public static BigInteger mod(BigInteger m) {
return this.remainder(m); // crashes if m is null or zero
}
// BAD: Null sneaks past — NullPointerException thrown 10 stack frames later
public void process(List<String> items) {
for (String item : items) { // NPE here if items is null, not at call site
doWork(item);
}
}The Solution
Check every parameter restriction at the start of the method body and throw an appropriate exception immediately. Use Objects.requireNonNull() for nulls and Objects.checkIndex() / Objects.checkFromToIndex() (Java 9+) for index ranges.
// GOOD: Fail immediately at the earliest opportunity
public static BigInteger mod(BigInteger m) {
if (m.signum() <= 0) {
throw new ArithmeticException("Modulus <= 0: " + m);
}
return this.remainder(m);
}
// GOOD: Objects.requireNonNull — standard idiom since Java 7
public void process(List<String> items) {
Objects.requireNonNull(items, "items must not be null");
for (String item : items) {
doWork(item);
}
}
// GOOD: Index validation (Java 9+)
public char charAt(int index) {
Objects.checkIndex(index, length()); // throws IndexOutOfBoundsException
return chars[index];
}
// GOOD: Range validation for subList-style operations (Java 9+)
public List<E> subList(int fromIndex, int toIndex) {
Objects.checkFromToIndex(fromIndex, toIndex, size());
// ...
}
// GOOD: Private method — use assert (checked only with -ea flag)
private static void sort(long[] a, int offset, int length) {
assert a != null;
assert offset >= 0 && offset <= a.length;
assert length >= 0 && length <= a.length - offset;
// ...
}Why This Works
- Fail-fast principle: errors are caught at their source, not propagated. The stack trace points directly to the bad call site.
- Atomicity: the method either succeeds fully or throws before any state is modified (see Item 76).
- Exceptions thrown match caller expectations:
NullPointerException,IllegalArgumentException,IndexOutOfBoundsException. assertin private methods documents the contract without runtime cost in production.
When to Apply / When NOT to Apply
- Always validate public and protected method parameters before use.
- For constructors, validate before storing — you do not want an invalid object to exist at all.
- Exception: skip the check when validity is guaranteed by the caller’s context (e.g., private helper called with already-validated data), but document that assumption with
assert. - Skip when the check would be prohibitively expensive and the method will naturally fail quickly anyway (e.g., sorting a list — it will throw during comparison if elements are null).
Java 17 Update
Objects.requireNonNull(ref, "msg")— standard since Java 7, still the idiom.Objects.requireNonNullElse(obj, defaultValue)— Java 9+, useful for defaults.Objects.requireNonNullElseGet(obj, supplier)— Java 9+, lazy default.Objects.checkIndex(index, length)— Java 9+, replaces manual range checks.Objects.checkFromToIndex(from, to, length)— Java 9+.@NonNull/@NotNullannotations (from various frameworks/Checker Framework) are increasingly used as compile-time hints; they do not replace runtime checks in public APIs.
Item 50: Make Defensive Copies When Needed
The Problem
Java is a reference-passing language. If a caller passes a mutable object and you store the reference, the caller can mutate your object’s internals after construction. Similarly, if you return a reference to a mutable internal field, the caller can corrupt your object’s state from outside. This breaks the invariant of your class.
// BAD: Stores reference directly — caller retains control of internal state
public final class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(start + " after " + end);
this.start = start; // BUG: caller keeps reference to this Date
this.end = end;
}
public Date start() { return start; } // BUG: returns internal mutable object
public Date end() { return end; }
}
// Attack on construction:
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Period's end is now in the past!
// Attack via accessor:
p.end().setYear(78); // Period's end mutated again!The Solution
Copy mutable parameters on the way in (before validation!) and copy mutable fields on the way out.
// GOOD: Defensive copies protect invariants
public final class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
// Copy BEFORE validation to prevent TOCTOU race condition
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(this.start + " after " + this.end);
}
public Date start() { return new Date(start.getTime()); } // copy on the way out
public Date end() { return new Date(end.getTime()); }
}Why This Works
- Copying before validation prevents a Time-of-Check/Time-of-Use (TOCTOU) attack where a malicious subclass mutates the object between validation and storage.
- Do not use
clone()for defensive copies of parameters whose type is untrusted — a subclass could overrideclone()to return a hostile object. Use the copy constructor instead. - Using
clone()on your own final class’s internal fields is fine. - Immutability is the best defense: if you use
Instantinstead ofDate, orLocalDateinstead ofDate, defensive copies are unnecessary.
When to Apply / When NOT to Apply
- Apply whenever you store mutable objects passed as parameters (especially in constructors).
- Apply whenever you return a reference to a mutable internal field.
- Skip when the class is package-private and you control all callers (document the contract instead).
- Skip when the performance cost is proven significant and the class is documented as not thread-safe.
- Skip when ownership is explicitly transferred (document this clearly).
Java 17 Update
The best update for Item 50 is to not use Date at all. The java.time package (Java 8+) provides immutable types: Instant, LocalDate, LocalDateTime, ZonedDateTime. Using these eliminates the need for defensive copies entirely.
// BETTER: Use immutable java.time types — no defensive copies needed
public final class Period {
private final Instant start;
private final Instant end;
public Period(Instant start, Instant end) {
Objects.requireNonNull(start, "start");
Objects.requireNonNull(end, "end");
if (start.isAfter(end))
throw new IllegalArgumentException(start + " after " + end);
this.start = start;
this.end = end;
}
public Instant start() { return start; } // safe: Instant is immutable
public Instant end() { return end; }
}Item 51: Design Method Signatures Carefully
The Problem
Method signatures are the primary interface between a class and its callers. Poor choices — long parameter lists, ambiguous types, boolean flags, inconsistent naming — make methods hard to understand, easy to call incorrectly, and painful to maintain.
// BAD: Long parameter list — caller can forget the order
public void createUser(String firstName, String lastName,
String email, String phone,
boolean active, boolean admin,
int loginCount, String role) { ... }
// Caller: what does 'true, false' mean here?
createUser("Alice", "Smith", "alice@example.com",
"555-0100", true, false, 0, "viewer");
// BAD: Convenience method proliferation — too many
public interface Set<E> {
boolean addAll(Collection<? extends E> c);
boolean addAllList(List<E> c); // unnecessary
boolean addAllSorted(SortedSet<E> c); // unnecessary
}The Solution
Apply several design principles:
// GOOD: Use a builder or parameter object for long parameter lists
public record UserParams(
String firstName, String lastName,
String email, String phone,
boolean active, boolean admin,
int loginCount, String role
) {}
public void createUser(UserParams params) { ... }
// GOOD: Prefer enums over boolean flags — self-documenting
public enum UserRole { ADMIN, VIEWER, EDITOR }
public enum ActiveStatus { ACTIVE, INACTIVE }
public void createUser(String firstName, String lastName,
String email, String phone,
ActiveStatus status, UserRole role) { ... }
// GOOD: Prefer two-element enums to boolean parameters
public enum TemperatureScale { FAHRENHEIT, CELSIUS }
Thermometer t = new Thermometer(TemperatureScale.CELSIUS);
// vs.
Thermometer t = new Thermometer(true); // what does true mean?
// GOOD: Prefer interfaces over classes for parameter types
// Accepts HashMap, TreeMap, LinkedHashMap, or any other Map
public void process(Map<String, Integer> map) { ... }
// NOT:
public void process(HashMap<String, Integer> map) { ... } // unnecessarily restrictiveWhy This Works
Key guidelines from the book:
- Choose method names carefully: consistent with broader conventions, understandable, not too long.
- Don’t over-provide convenience methods: every method must be justified. When in doubt, leave it out.
- Avoid long parameter lists: more than 3-4 params is a design smell. Use builder, parameter object, or method decomposition.
- Prefer interfaces over classes for parameter types: maximizes flexibility and testability.
- Prefer two-element enums to boolean parameters: makes call sites self-documenting.
When to Apply / When NOT to Apply
- Always apply naming and type guidance.
- Parameter objects are worthwhile when the same group of parameters appears in multiple method signatures.
- Convenience methods are justified when a particular usage pattern is provably common (e.g.,
Collections.emptyList()).
Java 17 Update
- Records (Java 16+) make parameter objects trivial to create:
record UserParams(String name, String email) {}. They provide constructor, accessors,equals,hashCode,toStringautomatically. - Sealed interfaces (Java 17+) can model constrained parameter types.
- Text blocks (Java 15+) are not directly related but improve readability of string-heavy test code.
Item 52: Use Overloading Judiciously
The Problem
Overload resolution in Java is determined at compile time based on the static type of the argument, not the runtime type. This is counterintuitive because method overriding is resolved at runtime. Callers expect overloading to behave like overriding — and when it does not, subtle bugs appear.
// BAD: Overloading confused with overriding
public class CollectionClassifier {
public static String classify(Set<?> s) { return "Set"; }
public static String classify(List<?> l) { return "List"; }
public static String classify(Collection<?> c) { return "Collection"; }
public static void main(String[] args) {
Collection<?>[] collections = {
new HashSet<>(), new ArrayList<>(), new HashMap<>().values()
};
for (Collection<?> c : collections) {
System.out.println(classify(c)); // Always prints "Collection"!
}
}
}
// The static type of 'c' in the loop is Collection<?> — always picks the third overload.
// BAD: Autoboxing + overloading confusion
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
list.remove(1); // removes element at index 1 — remove(int)
list.remove((Integer) 1); // removes value 1 — remove(Object)
// Two remove() overloads, two very different behaviorsThe Solution
Avoid creating overloads where the same arguments could match multiple overloads ambiguously. When overloads behave differently, use different method names.
// GOOD: Different names eliminate ambiguity
public class CollectionClassifier {
public static String classifySet(Set<?> s) { return "Set"; }
public static String classifyList(List<?> l) { return "List"; }
public static String classifyCollection(Collection<?> c) { return "Collection"; }
}
// GOOD: ObjectOutputStream uses explicit names to avoid confusion
// write(boolean), write(byte), write(char), write(short), write(int)...
// NOT overloaded — each is distinct and self-documenting
// GOOD: When you must overload (e.g., to satisfy an interface), ensure all
// overloads behave identically for the same argument:
public boolean contentEquals(StringBuffer sb) {
return contentEquals((CharSequence) sb); // delegates to the canonical method
}Why This Works
- Overriding: the runtime type of the object determines which method runs. Intuitive.
- Overloading: the compile-time type of the argument determines which method runs. Surprising.
- The safe rule: never export two overloads with the same number of parameters where any argument could legally be passed to either.
- When you must create overloads, make them all forward to a single canonical implementation so they always behave the same.
When to Apply / When NOT to Apply
- For constructors: overloading is unavoidable. Consider static factory methods with distinct names instead (Item 1).
- Generics and autoboxing have made existing overloads (like
List.remove) dangerous — be careful when adding overloads to existing types. - Lambda and method references (Java 8+) make overloading even trickier: the same lambda can match multiple functional interfaces, and the overload resolution may pick the wrong one.
Java 17 Update
Lambda overloading is the primary modern hazard:
// BAD: Ambiguous with lambda
// Both submit(Runnable) and submit(Callable<T>) exist in ExecutorService
// This can be ambiguous:
executor.submit(() -> System.out.println("hello")); // Runnable or Callable<Void>?
// The compiler picks Callable here if the lambda returns void — can be surprising.
// GOOD: Cast to make intent explicit
executor.submit((Runnable) () -> System.out.println("hello"));Item 53: Use Varargs Judiciously
The Problem
Varargs (Type... args) are convenient but have costs: every varargs call allocates a new array. When the method is called millions of times in a tight loop, this allocation pressure can cause measurable GC overhead.
// BAD: Varargs method requiring at least one arg — no compile-time enforcement
static int min(int... args) {
if (args.length == 0)
throw new IllegalArgumentException("Too few arguments");
int min = args[0];
for (int i = 1; i < args.length; i++)
if (args[i] < min) min = args[i];
return min;
}
min(); // compiles fine, fails at runtime — bad UX
// BAD: Unnecessary varargs on a performance-sensitive method
// Every call allocates int[] even when called with 1, 2, or 3 argsThe Solution
If you need at least one argument, declare it explicitly before the vararg:
// GOOD: First argument is required — enforced at compile time
static int min(int firstArg, int... remainingArgs) {
int min = firstArg;
for (int arg : remainingArgs) {
if (arg < min) min = arg;
}
return min;
}
min(); // compile error — correct!
min(1); // fine
min(1, 2, 3); // fineFor performance-sensitive varargs methods, provide overloads for the common arities:
// GOOD: Performance pattern — EnumSet.of() uses this approach
public void foo() { }
public void foo(int a1) { }
public void foo(int a1, int a2) { }
public void foo(int a1, int a2, int a3) { }
public void foo(int a1, int a2, int a3, int... rest) { } // only rare calls allocate
// 95% of calls use 0-3 args and allocate no array.
// Only the 5% with 4+ args pay for array allocation.Why This Works
- Making the first arg explicit provides compile-time safety and clearer API semantics.
- The overload pattern eliminates array allocation for the common cases while retaining the flexibility of varargs for the rare, higher-arity cases.
printfandString.formatuse varargs appropriately because: (a) they are not performance hotspots, (b) the variable-arity nature is the whole point.
When to Apply / When NOT to Apply
- Use varargs when the variable-arity nature is truly the point (printf-style, accumulation).
- Avoid varargs for methods that could be called in tight loops where allocation matters.
- Never use varargs just to save callers from building an array manually — that convenience has a cost.
Java 17 Update
List.of(E... elements)andSet.of(E... elements)(Java 9+) use the overload pattern internally to avoid array allocation for 0-10 elements. Each arity 0-10 has a dedicated overload; only 11+ uses varargs.@SafeVarargsannotation suppresses “unchecked” warnings on generic varargs. Apply it only to methods that genuinely do not pollute the heap (private,final, orstaticmethods that only pass the varargs array to other@SafeVarargsmethods or read from it).
// GOOD: @SafeVarargs for generic varargs that are safe
@SafeVarargs
public static <T> List<T> flatten(List<? extends T>... lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists)
result.addAll(list);
return result;
}Item 54: Return Empty Collections or Arrays, Not Nulls
The Problem
Returning null to represent an empty collection is an API design error. Every caller must check for null before using the result. In practice, many callers forget this check, leading to NullPointerExceptions discovered at runtime, often in production.
// BAD: Returns null for empty — forces every caller to null-check
private final List<Cheese> cheesesInStock = ...;
public List<Cheese> getCheeses() {
return cheesesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock);
}
// Caller is burdened with:
List<Cheese> cheeses = shop.getCheeses();
if (cheeses != null && cheeses.contains(Cheese.STILTON)) { ... } // easy to forget the null checkThe Solution
Return empty collections (or arrays). The performance argument for null is almost always wrong — the overhead of an empty collection is negligible.
// GOOD: Return empty collection — never null
public List<Cheese> getCheeses() {
return new ArrayList<>(cheesesInStock); // naturally empty if cheesesInStock is empty
}
// EVEN BETTER: Return the same empty list singleton (avoids even one allocation)
public List<Cheese> getCheeses() {
return cheesesInStock.isEmpty()
? Collections.emptyList() // canonical immutable empty list singleton
: new ArrayList<>(cheesesInStock);
}
// GOOD: Same pattern for arrays
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheesesAsArray() {
return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}
// toArray(new Cheese[0]) is actually faster than toArray(new Cheese[size])
// because JIT-optimized path for zero-length arraysWhy This Works
- Null is not “no results” — null means “I don’t know” or “not applicable”. An empty collection correctly means “zero results”.
Collections.emptyList(),Collections.emptySet(),Collections.emptyMap()are immutable singletons — zero allocation.- Empty arrays can be stored as a
static finalconstant and reused. - Callers can use
for-eachdirectly on the result without any null check.
When to Apply / When NOT to Apply
- Always return empty, never null, for collections and arrays.
- Exception: if
nullhas a distinct semantic meaning from “empty” in your domain (e.g., “not loaded yet” vs. “loaded but zero results”), thennullorOptionalmay be appropriate — but consider using a more explicit type.
Java 17 Update
List.of()(Java 9+) returns an immutable empty list.Set.of(),Map.of()similarly. These are slightly more space-efficient than theCollections.emptyList()singleton in some JVM implementations.List.copyOf(collection)(Java 10+) — useful for returning a safe copy.- For return types, do not use
List.of()for mutable return values — it throwsUnsupportedOperationExceptionon modification.
// GOOD: Java 9+ style for immutable empty return
public List<Cheese> getCheeses() {
return cheesesInStock.isEmpty()
? List.of()
: List.copyOf(cheesesInStock);
}Item 55: Return Optionals Judiciously
The Problem
Before Optional<T> (Java 8), a method that might not return a value had two options: return null or throw an exception. Returning null transfers the null-handling burden to every caller (Item 54 problem). Throwing an exception is appropriate only for truly exceptional conditions.
Optional<T> was introduced to solve this — but it can be misused.
// BAD: Returns null — caller must null-check
public static <E extends Comparable<E>> E max(Collection<E> c) {
if (c.isEmpty()) return null; // caller must null-check
E result = null;
for (E e : c)
if (result == null || e.compareTo(result) > 0)
result = Objects.requireNonNull(e);
return result;
}
// BAD: Optional misuse — Optional in a field (wasteful, unusual)
public class UserProfile {
private Optional<String> nickname; // BAD: use String or @Nullable instead
}
// BAD: Optional<int> doesn't exist — Optional<Integer> boxes the int
public Optional<Integer> findCount() { ... } // unnecessary boxingThe Solution
Return Optional<T> when a method might legitimately have no result and null or exception is not appropriate.
// GOOD: Returns Optional — clearly signals "might be empty"
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
if (c.isEmpty()) return Optional.empty();
E result = null;
for (E e : c)
if (result == null || e.compareTo(result) > 0)
result = Objects.requireNonNull(e);
return Optional.of(result);
}
// Even better: use streams
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
return c.stream().max(Comparator.naturalOrder());
}
// GOOD: Caller has expressive options
// 1. Provide a default value
String name = getUserName().orElse("Anonymous");
// 2. Provide a default supplier (lazy)
String name = getUserName().orElseGet(() -> computeExpensiveDefault());
// 3. Throw a domain exception
User user = findUser(id).orElseThrow(() -> new UserNotFoundException(id));
// 4. Use ifPresent
getUserName().ifPresent(name -> sendGreeting(name));
// 5. Java 9+: ifPresentOrElse
getUserName().ifPresentOrElse(
name -> sendGreeting(name),
() -> log.warn("No user name found")
);
// 6. Java 9+: or() — chain optionals
Optional<String> name = getPrimaryName().or(() -> getSecondaryName());
// 7. Java 11+: isEmpty()
if (getUserName().isEmpty()) { handleMissing(); }
// GOOD: Use primitive optionals to avoid boxing
OptionalInt findIndex() { return OptionalInt.of(42); }
OptionalLong findId() { return OptionalLong.of(123L); }
OptionalDouble findScore() { return OptionalDouble.of(9.5); }Why This Works
Optionalmakes the “might be empty” contract explicit in the type system.- Callers are forced to think about the absent case.
- Stream-based code interoperates naturally:
stream.findFirst()returnsOptional.
When NOT to Use Optional
- Never use Optional as a field type — it was not designed for this and is not serializable.
- Never use Optional as a parameter type — callers should pass null or use overloads instead.
- Never use Optional in collections or maps as a value —
Map<Key, Optional<Value>>is almost always wrong. - Never return
Optional<T>when performance is critical in tight loops —Optionalwraps a value in an object, adding allocation and GC pressure. - Never use
Optional<int>— useOptionalIntinstead. - Use Optional only for return types where “no result” is a normal, expected outcome.
Java 17 Update
Optional.isEmpty()— Java 11+, cleaner than!isPresent().Optional.ifPresentOrElse(Consumer, Runnable)— Java 9+.Optional.or(Supplier<Optional<T>>)— Java 9+, chains optionals without nesting.Optional.stream()— Java 9+, convertsOptionalto a 0 or 1 element stream. Useful in flatMap chains:
// Java 9+: Optional.stream() in a flatMap
List<String> names = users.stream()
.flatMap(u -> u.getNickname().stream()) // flattens Optional into stream
.collect(Collectors.toList());Item 56: Write Doc Comments for All Exposed API Elements
The Problem
Undocumented APIs are hard to use correctly. Callers cannot know what parameters mean, what exceptions to expect, what side effects occur, or what the method is supposed to do. This leads to misuse, bugs, and frustration.
// BAD: No documentation — what does this method do? What does index mean?
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
// BAD: Incomplete documentation — missing @throws, @param semantics
/**
* Gets the element.
*/
public E get(int index) { ... }The Solution
Write a doc comment for every public class, interface, constructor, method, and field. Follow the Javadoc conventions precisely.
/**
* Returns the element at the specified position in this list.
*
* <p>This method is <i>not</i> guaranteed to run in constant time.
* In some implementations it may run in time proportional to the element
* position.
*
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException if the index is out of range
* ({@code index < 0 || index >= size()})
*/
E get(int index);
/**
* An object that maps keys to values. A map cannot contain duplicate keys;
* each key can map to at most one value.
*
* <p>(This interface takes the place of the {@code Dictionary} class,
* which was a totally abstract class rather than an interface.)
*
* @param <K> the type of keys maintained by this map
* @param <V> the type of mapped values
*/
public interface Map<K, V> { ... }
// Documenting an enum constant
public enum OrchestraSection {
/** Woodwinds, such as flute, clarinet, and oboe. */
WOODWIND,
/** Brass instruments, such as French horn and trumpet. */
BRASS,
/** Percussion instruments, such as timpani and cymbals. */
PERCUSSION,
/** Stringed instruments, such as violin and cello. */
STRING;
}
// Documenting annotation members
/**
* Indicates that the annotated method is a test method that
* must throw the designated exception to pass.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
/**
* The exception that the annotated test method must throw
* in order to pass. (The test is permitted to throw any subtype
* of the type described by this class object.)
*/
Class<? extends Throwable> value();
}Rules for Doc Comments
- The first sentence of each doc comment becomes the summary — make it count.
- No two members should have the same summary (especially for overloaded methods).
- Use
@paramfor every parameter,@return(unless void),@throwsfor every checked and unchecked exception that a caller might want to handle. - Use
{@code ...}for code in descriptions (renders as monospace and escapes HTML). - Use
{@literal ...}when you need to escape<,>,&in non-code contexts. - Use
{@link ClassName#method}for cross-references. - When documenting a generic type or method, document all type parameters with
@param <T>. - Thread safety must be documented — whether the class is thread-safe, conditional thread-safe, not thread-safe.
- Serialization form must be documented for serializable classes.
When to Apply
- All public API elements — no exceptions.
- Package-private classes: document if the class is complex enough that maintainers need guidance.
- Overriding methods: document only what differs from the superclass; use
{@inheritDoc}for the rest.
Java 17 Update
- Records: document the record class’s Javadoc; component parameters are automatically documented from the constructor’s
@paramtags. - Sealed classes (Java 17+): document which subclasses are permitted in the class-level Javadoc.
{@snippet}tag (Java 18+): inline code snippets in Javadoc that can be validated against actual source files. Not yet in Java 17 but coming soon.
Interview Questions & Exercises
Q1: What is the difference between Objects.requireNonNull() and a manual null check, and which should you use?
Context: Comes up when discussing parameter validation, defensive programming, or code review of API methods.
Answer: Both throw NullPointerException when the reference is null. Objects.requireNonNull(obj, "msg") is the standard idiom because:
- It is self-documenting: the intent is explicit.
- It accepts a descriptive message that appears in the exception.
- It is recognized as the idiomatic null-check by static analysis tools.
- It returns the reference if non-null, enabling compact assignment:
this.name = Objects.requireNonNull(name, "name").
A manual null check (if (obj == null) throw new NullPointerException(...)) is equivalent but more verbose and less idiomatic.assert obj != nullis only checked with the-eaJVM flag and should be used only for private/package-private methods.
Follow-up: When would you use Objects.requireNonNullElse() vs requireNonNull()? Answer: requireNonNullElse(obj, default) provides a fallback value instead of throwing — used when null is acceptable but should be replaced with a default.
Q2: Why must you make a defensive copy BEFORE validating the parameter, not after?
Context: This is a classic trick question about Item 50, asked in security-focused or senior-level interviews.
Answer: This is the TOCTOU (Time-of-Check/Time-of-Use) window vulnerability. If you validate first and copy second, an attacker could:
- Pass a subclass of
Datethat overridesgetTime(). - Your validation passes (the value looks valid at check time).
- Between validation and copy, the attacker changes the object’s state.
- Your copy captures the invalid state.
By copying first, you validate and store a snapshot of the original data — the attacker cannot change what you stored.
Follow-up: Why not use clone() for defensive copies in constructors? Because clone() can be overridden by subclasses to return a hostile object. Use copy constructors (new Date(other.getTime())) instead.
Q3: Explain why overloading is resolved at compile time and why that causes problems.
Context: Asked at Google, Amazon-level Java interviews when discussing Java language semantics.
Answer: Overloading (multiple methods with the same name but different parameter types) is a compile-time construct. The compiler picks the method based on the static declared type of the argument, not its runtime type. Overriding (subclass provides a different implementation of a parent method) is a runtime construct decided by the JVM via the vtable. This asymmetry is a trap: callers often expect overloading to behave like overriding. For example, if you have classify(Set<?> s) and classify(Collection<?> c) and pass a Set through a Collection<?> variable, the Collection overload is chosen — not Set. The safe rule: never create overloads where the same value can match multiple overloads ambiguously.
Follow-up: How does autoboxing complicate overloading? List.remove(int index) vs List.remove(Object o) — list.remove(1) removes by index; list.remove((Integer) 1) removes the value. These are two different behaviors hidden behind the same method name.
Q4: When should you use Optional<T> as a return type, and when should you NOT?
Context: Very common in Java 8+ interviews. Often asked as “when is Optional appropriate?”
Answer: Use Optional<T> as a return type when the method may legitimately not have a result and null would be likely to cause errors (i.e., callers might forget to null-check). Classic example: Map.get() should ideally return Optional (it doesn’t for historical reasons, but find* methods in streams do). Do NOT use Optional: (1) as a field type — it is not serializable and adds overhead; (2) as a parameter type — callers should pass null or use overloads; (3) in collections — Map<Key, Optional<Value>> makes no sense, just omit the key; (4) for primitive types — use OptionalInt, OptionalLong, OptionalDouble to avoid boxing; (5) in performance-critical loops — Optional allocates an object on every non-empty return.
Follow-up: What methods did Java 9/11 add to Optional? ifPresentOrElse, or, stream (Java 9); isEmpty (Java 11).
Q5: What is the “performance argument for returning null” and why is Bloch’s rebuttal correct?
Context: Comes up when reviewing code that returns null for empty collections “for performance reasons.”
Answer: The performance argument is that allocating a new empty ArrayList on every call wastes memory. Bloch’s rebuttal: use Collections.emptyList() (a singleton — zero allocation) or List.of() (Java 9+, also a singleton). The performance argument evaporates. Furthermore, the cost of debugging one production NullPointerException caused by a forgotten null-check vastly exceeds any memory savings from null returns. The allocation of an empty collection object is measured in nanoseconds; the debugging cost of a NPE is measured in engineering hours.
Follow-up: Why is toArray(new T[0]) faster than toArray(new T[size])? Because toArray(new T[0]) allocates the array internally with exact sizing using an optimized JVM path, whereas toArray(new T[size]) pre-allocates and may zero the array unnecessarily.
Q6: How do varargs interact with generics, and what is @SafeVarargs?
Context: Intermediate-to-senior level Java question about generic type safety.
Answer: When you create a generic varargs method like void process(T... items), the compiler generates a warning because the varargs array is a T[] internally, but Java cannot create generic arrays safely — this can cause heap pollution where a variable of one type contains an object of another type. If the method only reads from the varargs array (does not store it in an Object[] or expose it), it is safe. You annotate such a method with @SafeVarargs to suppress the warning and signal to callers that the method is safe. @SafeVarargs can only be applied to static, final, or private methods (in Java 9+, also to constructors) because those cannot be overridden. An overriding method could introduce heap pollution.
Follow-up: What is heap pollution? When a variable of a parameterized type refers to an object that is not of that type, allowing a ClassCastException to occur at a seemingly correct cast.
Q7: What must every Javadoc comment for a public method include?
Context: Code review scenarios, technical writing quality interviews, senior engineer assessments.
Answer: A complete doc comment includes:
- Summary sentence (first sentence, becomes the tooltip in IDEs and the summary table in generated docs).
@paramtag for every parameter.@returntag unless the return type is void.@throwstag for every checked and relevant unchecked exception the caller might handle.- Preconditions and postconditions described in the summary text.
- Side effects (e.g., starts a background thread, modifies a shared resource).
- Thread safety at the class level.
{@code ...}for inline code snippets.
Follow-up: What’s the difference between {@code} and {@literal}? {@code} renders in monospace font and escapes HTML special characters. {@literal} escapes HTML but does not change the font. Use {@code} for actual code, {@literal} for non-code text with special characters like < and &.
Q8: Design a method signature for “find users matching a filter, with optional sorting and pagination.” What principles from Item 51 apply?
Context: System design + API design interview question.
Answer:
// BAD: Too many boolean/enum params smashed together
List<User> findUsers(String nameFilter, String emailFilter,
boolean sortByName, boolean sortAscending,
int page, int pageSize) { ... }
// GOOD: Parameter object captures cohesive query
public record UserQuery(
String nameFilter,
String emailFilter,
UserSortField sortBy,
SortOrder order,
int page,
int pageSize
) {
public static UserQuery defaults() {
return new UserQuery(null, null, UserSortField.NAME, SortOrder.ASC, 0, 20);
}
}
public enum UserSortField { NAME, EMAIL, CREATED_AT }
public enum SortOrder { ASC, DESC }
List<User> findUsers(UserQuery query);Key principles applied: (1) Use parameter object for more than 3-4 params. (2) Use enums instead of booleans for flags. (3) Accept the most general useful type (UserQuery rather than a concrete framework class). (4) Consider a Builder if fields are frequently omitted.
Key Takeaways
- Item 49: Validate parameters immediately and throw the most specific appropriate exception. Use
Objects.requireNonNull(),Objects.checkIndex(), andassertfor private helpers. - Item 50: Copy mutable parameters on the way in (before validation) and mutable fields on the way out. Never use
clone()on untrusted types. Prefer immutable types (java.time) to eliminate the need for defensive copies. - Item 51: Limit parameters to 3-4; use builder or parameter objects for more. Prefer enums over booleans. Accept the most general useful interface type. Don’t over-provide convenience methods.
- Item 52: Overloading is resolved at compile time; overriding at runtime. Avoid ambiguous overloads. When you must overload, ensure all overloads forward to one canonical implementation.
- Item 53: Require at least one argument with an explicit first parameter, not runtime validation. Use the multi-overload performance pattern for frequently-called varargs methods.
- Item 54: Never return null for an empty collection or array. Use
Collections.emptyList(),List.of(), or astatic finalempty array constant. - Item 55: Use
Optional<T>only as a return type when “no result” is a legitimate outcome. Never in fields, parameters, or collections. UseOptionalInt/OptionalLong/OptionalDoublefor primitives. - Item 56: Document every public API element with
@param,@return,@throws, preconditions, postconditions, side effects, and thread safety. The first sentence is the summary — make it precise and unique.
Cross-references: ch01-creating-and-destroying-objects (Item 1: static factories for constructor overloading), ch04-classes-and-interfaces (immutability), ch09-exceptions (exception selection), ch11-concurrency (thread safety documentation)
Last Updated: 2026-05-10