JAVA / METHODS
Javadoc comments and the contract of a method
Write Javadoc that states a method's preconditions, return guarantee and failure modes so callers can use it without reading the body.
What you will learn
- Attach /** */ directly above a declaration so javadoc and IDE hovers pick it up.
- Use @param for preconditions, @return for meaning, @throws for failure conditions.
- Write a first sentence that stands alone; javadoc cuts the summary at its first period.
- Document behaviour, not the body, so you can rewrite the implementation safely.
Understanding Javadoc comments and the contract of a method
A Javadoc comment starts with /** and sits immediately above the declaration it describes; the compiler discards it, but the javadoc tool, your editor's hover popup and anyone reading the signature treat it as that method's specification. The placement is the whole mechanism: a comment written with a single star, or one that sits below the signature or inside the body, is an ordinary comment and the method renders as undocumented. The first sentence is special, because javadoc copies it into the summary table and stops at the first period, so it has to read as a complete claim on its own.
The reason to write one is to publish a contract. A contract has three parts: what the caller must guarantee before the call, what the method guarantees when it returns, and how the call fails when the caller breaks their side. Everything the contract does not mention is deliberately unspecified, and that is what buys you freedom: percentOf below can be rewritten with integer arithmetic or BigDecimal and no caller may complain, as long as it still rejects a non-positive whole and rounds ties up. The flip side is that whatever you do write down becomes binding, so describing the algorithm instead of the observable result locks you into the current body.
The block tags are the slots of that contract, not decoration. @param carries the constraint on each parameter, a range or non-null or non-empty, since the type already says it is an int; @return says what the value means at the edges, so the caller learns that an absent match yields -1 rather than an exception. @throws pairs an exception type with the condition that triggers it, and it is the only place unchecked exceptions such as IllegalArgumentException get documented, because they never appear in the signature. Vagueness costs you later: "rounded to the nearest integer" leaves 37.5 undecided, while adding "ties are rounded up" turns today's behaviour into a promise a caller can rely on and a test can pin down.
Treat the doc as the thing you maintain first: when the required range of a parameter changes, the @param line changes in the same edit, otherwise the comment becomes a confident lie that outlives the code.
public class Ratios {
/**
* Returns {@code part} as a whole-number percentage of {@code whole},
* rounded to the nearest integer with ties rounded up.
*
* @param part the measured amount, must not be negative
* @param whole the total amount, must be greater than zero
* @return the percentage, which is between 0 and 100 whenever
* {@code part <= whole}
* @throws IllegalArgumentException if {@code part} is negative or if
* {@code whole} is not positive
*/
public static int percentOf(int part, int whole) {
if (part < 0) {
throw new IllegalArgumentException("part must not be negative: " + part);
}
if (whole <= 0) {
throw new IllegalArgumentException("whole must be positive: " + whole);
}
return Math.round(part * 100.0f / whole);
}
public static void main(String[] args) {
System.out.println(percentOf(3, 8));
System.out.println(percentOf(1, 3));
try {
percentOf(4, 0);
} catch (IllegalArgumentException e) {
System.out.println("rejected: " + e.getMessage());
}
}
}A Javadoc comment is a method's published contract, and only what it states, preconditions, return guarantee and failure modes, is actually promised.
Worked examples
A postcondition the caller can observe
The doc promises a live read-only view, and the program shows both halves of that promise holding.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class Roster {
private final List<String> names = new ArrayList<>();
/**
* Adds one name to the end of this roster.
*
* @param name the name to add, must not be {@code null}
* @throws NullPointerException if {@code name} is {@code null}
*/
public void add(String name) {
Objects.requireNonNull(name, "name");
names.add(name);
}
/**
* Returns the names of this roster in the order they were added.
*
* <p>The result is a live read-only view: it shows names added after this
* call, and every mutating method on it throws
* {@link UnsupportedOperationException}.
*
* @return an unmodifiable view of the names, never {@code null}
*/
public List<String> names() {
return Collections.unmodifiableList(names);
}
public static void main(String[] args) {
Roster roster = new Roster();
roster.add("ada");
List<String> view = roster.names();
roster.add("linus");
System.out.println(view);
try {
view.add("grace");
} catch (UnsupportedOperationException e) {
System.out.println("view refused the write, as documented");
}
}
}Example explained
Line 1The @param line on add states the precondition and the @throws line names the failure, so a caller never has to open the body to learn that null is rejected.
Line 2The prose in names() promises a live view, which is why roster.add("linus") after view was obtained makes the printed list two entries long.
Line 3view.add("grace") throws UnsupportedOperationException, exactly the documented failure; Collections.unmodifiableList is one implementation that honours the promise, not part of the promise itself.
Line 4<p> starts a paragraph because Javadoc bodies are HTML fragments; a blank comment line alone does not separate paragraphs in the generated page.
Inheriting a contract with {@inheritDoc}
An override reuses the interface's contract text and adds a stronger guarantee on top of it.
public class Clocks {
interface Clock {
/**
* Returns the current reading in whole seconds.
*
* @return a reading that never decreases between calls on the same clock
*/
long readSeconds();
}
static final class FixedClock implements Clock {
private final long reading;
FixedClock(long reading) {
this.reading = reading;
}
/**
* {@inheritDoc}
*
* <p>This implementation always returns the value it was constructed
* with, which is a stronger promise than {@code Clock} makes.
*/
@Override
public long readSeconds() {
return reading;
}
}
public static void main(String[] args) {
Clock clock = new FixedClock(42L);
System.out.println(clock.readSeconds());
System.out.println(clock.readSeconds() == clock.readSeconds());
}
}Example explained
Line 1The contract lives on Clock.readSeconds: its @return promises a reading that never decreases, and every implementation is bound by that sentence.
Line 2{@inheritDoc} pulls the interface text into the override's page instead of copying it, so the two descriptions cannot drift apart when one is edited.
Line 3The added sentence narrows the guarantee rather than loosening it; an override may promise more, but it must never demand more from callers or guarantee less.
Line 4The second line prints true only because of FixedClock's extra promise, so code typed against Clock alone must not assume two calls agree.
Important notes
Nothing verifies the doc at runtime, since the class file does not carry it; run javadoc with doclint enabled to catch a @param naming a parameter that no longer exists or a missing @return.
The summary ends at the first period followed by whitespace, so an abbreviation such as "e.g. " inside the opening sentence truncates it mid-thought; move it out of the first sentence.
Common mistakes
Writing /* with one star, or placing the comment under the signature or inside the body: javadoc and IDE hovers ignore it, so the method ships undocumented while the author believes it is documented.
Echoing the signature back into the tags (@param whole the whole, @return an int): the caller still has no way to know that whole must be positive and discovers it through an IllegalArgumentException at runtime.
Saying nothing about null or empty input: callers pass them anyway, whatever the current body happens to do becomes the de facto contract, and fixing it later breaks working code.
Try it yourself
Change, predict, then run
Write static String initials(String fullName) with a Javadoc comment containing a one-sentence summary, an @param stating what fullName must be, an @return describing the result for a single-word name, and an @throws line. Then implement it so every line you wrote is literally true, and print initials("ada lovelace").
Open the Java workspaceCheck your understanding
A method format(String pattern) has Javadoc that never mentions null. Callers start passing null and depend on the NullPointerException the current body happens to throw. You later change the body to return "" for null and their code breaks. What is the accurate description of the situation?
- Nothing was wrong; returning an empty string is more forgiving, so the change can only help callers.
- The signature should have listed throws NullPointerException, which would have forced callers to handle it.
- The Javadoc never specified null, so no null behaviour was ever promised and the callers built on an accident of the body.
- The @param pattern tag was unnecessary anyway, because the parameter name is already visible in the signature.
Show answer
A contract covers only what it states, so with null unspecified the exception was an implementation detail that callers had no right to rely on, and the real fix is to decide and document the null behaviour before anyone depends on either version. Option 1 is tempting because a throws clause looks like documentation, but NullPointerException is unchecked: listing it neither forces a caller to handle anything nor says under which condition it happens, which is precisely the job of an @throws line.