skip to content

methods

items 49–56: validation, defensive copies, overloading, optionals.

chapter
chapter 16.07
reading time
10 min read

items 49 to 56 are about a method's edges: what comes in, what goes out, and what the signature promises. check parameters at the door, copy mutable things you don't own, keep signatures short, remember overloads are picked at compile time, and never return null where an empty collection or an Optional would do.

items 49 to 56. records, pattern matching and new javadoc features each touched one.

what comes in

item 49 · validate parameters at the door

a bad argument should fail at the call that passed it, with a clear message, not three layers later as a confusing NullPointerException (or worse, silently wrong data). check at the start of every public method and constructor, and document what you throw.

java
public Account(String owner, long cents, int branch) {
    this.owner = Objects.requireNonNull(owner, "owner");
    if (cents < 0) throw new IllegalArgumentException("cents < 0: " + cents);
    this.branch = Objects.checkIndex(branch, BRANCHES); // IndexOutOfBoundsException
    this.cents = cents;
}

for private helpers you control every caller, so an assert (enabled with -ea in tests) documents the assumption without a runtime cost in production. skip the check only when it's expensive and the computation would fail anyway with a fine exception (sorting checks comparability as it goes).

item 50 · copy what you don't own

if your class stores a mutable object the caller handed in (an array, a Date, a list), the caller still holds a reference to it. they can change your "immutable" object from outside, by accident or on purpose. same for getters that hand out internal state.

stack
main
mine
t
heap
Ticket
seats
int[]
120
131

new Ticket(mine) stores the caller's array. main's mine and the ticket's seats are two arrows to one array.

step 1/3
step through it. two arrows to one array means the caller can reach inside the ticket. the copy breaks the link.
don'tshares the caller's array
Main.java
final class Ticket {
    private final int[] seats;
    Ticket(int[] seats) {
        this.seats = seats;
    }
    int[] seats() {
        return seats;
    }
}
 
void main() {
    int[] mine = {12, 13};
    var t = new Ticket(mine);
    mine[0] = 99;      // via the input
    t.seats()[1] = 0;  // via the getter
    var s = t.seats();
    IO.println(s[0] + " " + s[1]);
}
output
99 0
docopy in, copy out
Main.java
final class Ticket {
    private final int[] seats;
    Ticket(int[] seats) {
        this.seats = seats.clone();
    }
    int[] seats() {
        return seats.clone();
    }
}
 
void main() {
    int[] mine = {12, 13};
    var t = new Ticket(mine);
    mine[0] = 99;
    t.seats()[1] = 0;
    var s = t.seats();
    IO.println(s[0] + " " + s[1]);
}
output
12 13

two details: copy before validating (otherwise another thread can change the argument between your check and your copy), and don't use clone to copy an argument whose class might be an untrusted subclass. better still, take immutable types (Instant, List.copyOf) and there's nothing to copy.

item 51 · design signatures carefully

a short checklist: names that read like the rest of the API; four parameters or fewer (more is unreadable, and identical types get swapped); parameter types that are interfaces (Map, not HashMap); and two-value enums instead of boolean flags.

don'twhat does true mean here?
java
heater.set(21, true, false);
doenums and a record say it
java
heater.set(new Target(21, Mode.HEAT,
    Fan.AUTO));

long lists shrink three ways: split the method (List.subList(...).indexOf(...) instead of an indexOf(from, to, x)), group parameters into a small record, or use a builder for the arguments (item 2).

choosing between methods

item 52 · overloads are picked at compile time

overloading looks like overriding, but the choice happens at a different moment, with different information:

predict the output
java
static String kind(Object o)  { return "object"; }
static String kind(String s)  { return "string"; }
static String kind(Integer i) { return "integer"; }
 
void main() {
    Object[] things = { "hi", 42, 3.0 };
    for (Object t : things) IO.print(kind(t) + " ");
}
given Object t = "hi";
overloading
kind(t)
the static type of t is Object, so javac binds kind(Object). decided, forever.
calls whatever javac picked. the String inside never gets a vote. → "object"
overriding
t.toString()
only checks that Object has a toString(). doesn't pick a body.
looks at the object: it's a String, so String's toString() runs. → "hi"
the same t, two kinds of choice. amber is when the decision is actually made.

the safe policy: never export two overloads with the same number of parameters when a caller could plausibly pass something that fits both. give them different names (writeInt, writeLong) instead. the JDK's own scar is List.remove:

Main.java
void main() {
    List<Integer> xs = new ArrayList<>(List.of(10, 20, 30));
    xs.remove(1);                    // remove(int index): removes 20
    IO.println(xs);
    xs.remove(Integer.valueOf(10));  // remove(Object o): removes 10
    IO.println(xs);
}
output
[10, 30]
[30]

item 53 · varargs, carefully

varargs are great for "any number of these" (String.format, List.of). two things to get right. if the method needs at least one argument, say so in the signature instead of checking args.length at run time:

Main.java
static int min(int first, int... rest) {   // min() with no arguments won't compile
    int m = first;
    for (int x : rest) m = Math.min(m, x);
    return m;
}
 
void main() {
    IO.println(min(4) + " " + min(4, 2, 9));
}
output
4 2

and every call allocates an array. in hot code, provide fixed-arity overloads for the common counts and varargs for the rest. that's why List.of has overloads for 0 to 10 elements before the E... one.

what goes out

item 54 · return empty, not null

a method that returns null for "nothing" forces every caller to remember a null check, and the one that forgets crashes later. an empty collection or array is the honest answer, and it's cheap: List.of(), Collections.emptyList() and a shared zero-length array allocate nothing.

don'tcallers must remember
java
List<Order> open() {
    return orders.isEmpty() ? null
        : new ArrayList<>(orders);
}
// for (var o : shop.open()) → NPE
doloops just work
java
List<Order> open() {
    return List.copyOf(orders);
}
// empty in, empty out

item 55 · Optional for return values, sparingly

Optional<T> says "there might not be a result" in the type, so callers can't forget. use it for return values of methods that legitimately may have no answer (findFirst, max, a lookup). don't use it for fields, parameters, collection elements, or map values, and never wrap a collection (return an empty one).

Main.java
record User(String name, String email) {}
 
static Optional<User> find(List<User> users, String name) {
    return users.stream().filter(u -> u.name().equals(name)).findFirst();
}
 
void main() {
    var users = List.of(new User("ada", "ada@x.io"), new User("bo", null));
    IO.println(find(users, "ada").map(User::email).orElse("no email"));
    IO.println(find(users, "cy").map(User::email).orElse("no such user"));
    IO.println(find(users, "bo").map(User::email).orElse("no email")); // map turns null into empty
}
output
ada@x.io
no such user
no email

reach for map, filter, orElse, orElseThrow and ifPresentOrElse; calling get() after isPresent() is a null check wearing a costume. for primitives use OptionalInt and friends. performance-critical code can still return null or a sentinel, documented. the full tour is in optional.

item 56 · document every exported element

every public class, interface, constructor, method and field needs a doc comment: what it does (not how), its preconditions (@param), postconditions, side effects, thread-safety (item 82), and every exception it throws (@throws, item 74). the first sentence becomes the summary, so make it stand alone. generics, enums and annotations need their type parameters, constants and members documented too.

keylines
  • check arguments first, fail with a clear exception, and document it.
  • copy mutable arguments in and mutable fields out, or only accept immutable types.
  • short parameter lists, interface types, enums instead of boolean flags.
  • overloads are chosen at compile time from static types. don't export confusing ones.
  • empty collections instead of null; Optional only for return values that may be absent.
  • if it's public, it's documented: params, returns, throws, thread-safety.