JAVA / METHODS
Designing method signatures that read clearly
Shape Java method names, parameter types, order and return types so a call site reads clearly without opening the declaration.
What you will learn
- Name methods as verb phrases so the call reads as a statement
- Replace boolean flag parameters with a two-constant enum
- Give same-typed parameters distinct types so a swapped call fails to compile
- Let the return type express absence instead of returning -1 or null
Understanding Designing method signatures that read clearly
A signature is read far more often than it is written, and almost always from the call site rather than the declaration. When someone writes reader.read(buffer, 0, 8), the compiler is matching types and positions only; the parameter names you chose so carefully do not appear there, because Java has no named arguments. That leaves exactly three things to carry meaning at the point of use: the method name, how many parameters there are, and what their types are.
The mental model that helps is to read the call as a sentence and ask whether it makes a claim you can check. Verb phrases turn into statements: invoice.applyDiscount(10), path.resolve(name), list.removeIf(rule). Order matters for the same reason: put the thing being acted on first and the tuning knobs last, and keep that order identical across sibling methods, which is why System.arraycopy and Files.copy both take the source before the destination.
Where reading is not enough, spend types. A boolean parameter compresses a decision down to the word true, which explains nothing at the call site, while a two-constant enum costs one extra word and names the decision. A run of same-typed parameters is the other danger zone, because every permutation compiles; moving one argument to the receiver, splitting the method, or introducing a small type converts a class of runtime bugs into compile errors. The return type is part of the signature too, so a specific type or an Optional states what can come back, whereas -1 and null hide it.
import java.util.Locale;
public class Signatures {
enum Rounding { TO_CENTS, TO_WHOLE }
// The argument names itself, so nobody has to open this method to read a call.
static String formatAmount(double amount, Rounding rounding) {
String pattern = (rounding == Rounding.TO_CENTS) ? "%.2f" : "%.0f";
return String.format(Locale.ROOT, pattern, amount) + " USD";
}
// Same behaviour, but the call site is a puzzle.
static String formatAmountFlagged(double amount, boolean round) {
return String.format(Locale.ROOT, round ? "%.0f" : "%.2f", amount) + " USD";
}
public static void main(String[] args) {
System.out.println(formatAmount(1234.56, Rounding.TO_CENTS));
System.out.println(formatAmount(1234.56, Rounding.TO_WHOLE));
System.out.println(formatAmountFlagged(1234.56, true));
System.out.println(formatAmountFlagged(1234.56, false));
}
}A method signature is the only interface most callers ever read, so its name, parameter types and parameter order must make each call understandable on its own.
Worked examples
Let the receiver carry the first role
Turning a two-argument static call into an instance call makes the order visible in the code that uses it.
public class Day {
private final int index;
Day(int index) {
this.index = index;
}
int daysUntil(Day other) {
return other.index - this.index;
}
public static void main(String[] args) {
Day launch = new Day(10);
Day release = new Day(25);
System.out.println(launch.daysUntil(release));
System.out.println(release.daysUntil(launch));
}
}Example explained
Line 1launch.daysUntil(release) names the starting point before the method name, so the call reads in the same order as the subtraction it performs.
Line 2A static daysBetween(10, 25) would hide that role in the position of an int, and daysBetween(25, 10) compiles just as well.
Line 3The second line proves the compiler cannot catch a swap here, which is why the ordering has to look wrong when a human reads it.
Line 4other.index - this.index pins the direction down once inside the method instead of at every call site.
Return type instead of sentinel value
Two methods answer the same question, but only one admits in its signature that the answer may be missing.
import java.util.Optional;
public class Lookup {
private static final String[] CODES = {"A1", "B2", "C3"};
static Optional<String> findCode(String prefix) {
for (String code : CODES) {
if (code.startsWith(prefix)) {
return Optional.of(code);
}
}
return Optional.empty();
}
static int indexOfCode(String prefix) {
for (int i = 0; i < CODES.length; i++) {
if (CODES[i].startsWith(prefix)) {
return i;
}
}
return -1;
}
public static void main(String[] args) {
System.out.println(findCode("B").orElse("none"));
System.out.println(findCode("Z").orElse("none"));
System.out.println(indexOfCode("Z"));
}
}Example explained
Line 1Optional<String> is part of the signature, so a caller sees the possibility of no result before reading anything else.
Line 2orElse("none") puts the fallback at the call site, which is the only place that knows what a sensible fallback is.
Line 3indexOfCode says only int, so the -1 convention lives in prose; a caller who forgets to check it gets an ArrayIndexOutOfBoundsException far from this method.
Line 4Both methods perform the identical search, which shows the difference is purely in what the signature promises.
Important notes
Parameter names are still worth choosing well, since they drive IDE hints, generated documentation and debuggers; they simply are not visible in the compiled call, so they cannot be the only thing keeping callers honest.
Do not wrap every value in a new type. Reach for a distinct type where a mistake is plausible: a boolean switch, neighbouring parameters of the same type, or a number whose unit matters, such as milliseconds versus seconds.
Common mistakes
Trusting parameter names to carry the meaning: the call site shows positions only, so copyRange(buffer, 8, 0) compiles as happily as copyRange(buffer, 0, 8) and the mistake shows up as wrong data at runtime.
Encoding options as strings such as csv or json instead of enum constants: a typo like jsn compiles cleanly, fails only when the method runs, and the caller has no way to discover which spellings are legal.
Bolting one more trailing boolean onto an existing method instead of adding a named alternative: call sites drift into save(doc, true, false), and every reader has to open the declaration to decode them.
Try it yourself
Change, predict, then run
Start from static String format(String text, boolean upper, boolean trim) and redesign it, keeping the behaviour identical, so that no call site needs a comment to be understood. Then call your version three ways and print the results.
Open the Java workspaceCheck your understanding
Callers of static void copy(String source, String target) keep passing the two names in the wrong order. Which change stops the mistake at compile time?
- Rename the parameters to from and to
- Add a comment above the method documenting the required order
- Introduce distinct types so the signature becomes copy(Source source, Target target)
- Reorder the declaration so the target comes first, matching assignment order
Show answer
Java resolves a call by argument types and positions, and parameter names never appear at the call site, so renaming them leaves the swapped call compiling exactly as before. Two different types make the wrong order a type error the compiler reports on the spot. Renaming is the tempting answer because it does help whoever reads the declaration, but the mistake is made in code that never looks at the declaration, and reordering the parameters just moves the ambiguity.