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.
- item 49validate parameters at the doorcheckIndex, flexible constructors
- item 50copy what you don't ownrecords + copyOf
- item 51design signatures carefully
- item 52overloads are picked at compile timepattern matching switch
- item 53varargs, carefully
- item 54return empty, not null
- item 55Optional for return values, sparingly
- item 56document every exported elementsnippets, markdown comments
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.
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.
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]);
}99 0
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]);
}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.
heater.set(21, true, false);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:
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) + " ");
}kind(t)
t.toString()
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:
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);
}[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:
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));
}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.
List<Order> open() {
return orders.isEmpty() ? null
: new ArrayList<>(orders);
}
// for (var o : shop.open()) → NPEList<Order> open() {
return List.copyOf(orders);
}
// empty in, empty outitem 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).
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
}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.
- 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.