Avoiding Javadoc duplication in Java records

367 Views Asked by At

The new Java record is supposed to cut down on boilerplate. I can quickly create an immutable FooBar class with foo and bar components without worrying about local variables, constructor value copying, and getters, like this:

/**
 * Foo bar record.
 */
public record FooBar(String foo, String bar) {
}

Of course I want to document what the record components are, so they will show up in the generated Javadocs! So I add this:

/**
 * Foo bar record.
 * @param foo That foo thing; cannot be <code>null</code>.
 * @param bar That bar thing; cannot be <code>null</code>.
 */
public record FooBar(String foo, String bar) {
}

That works nicely—the record components show up in the documentation.

Except that for almost 100% of records, as a good developer of course I need to validate the foo and bar to ensure they are non-null, right? (Right.) Records allow this easily:

/**
 * Foo bar record.
 * @param foo That foo thing; cannot be <code>null</code>.
 * @param bar That bar thing; cannot be <code>null</code>.
 */
public record FooBar(String foo, String bar) {

  /** Constructor for argument validation and normalization. */
  public FooBar {
    Objects.requireNonNull(foo);
    Objects.requireNonNull(bar);
  }

}

And now, since I am using -Xdoclint:all, I get a warning because the construct doesn't document its (implicit) parameters:

[WARNING] Javadoc Warnings
[WARNING] …/FooBar.java:xx: warning: no @param for foo
[WARNING] public FooBar {
[WARNING] ^
[WARNING] …/FooBar.java:xx: warning: no @param for bar
[WARNING] public FooBar {
[WARNING] ^
[WARNING] 2 warnings

If you say "just turn off -Xdoclint:all", you're missing the point. The warning is valid, because indeed no documentation shows up for the constructor in the generated Javadocs! If there is a constructor, there should be documentation for the parameters. So I'm forced to do the old copy and paste:

/**
 * Foo bar record.
 * @param foo That foo thing; cannot be <code>null</code>.
 * @param bar That bar thing; cannot be <code>null</code>.
 */
public record FooBar(String foo, String bar) {

  /**
   * Constructor for argument validation and normalization.
   * @param foo That foo thing; cannot be <code>null</code>.
   * @param bar That bar thing; cannot be <code>null</code>.
   */
  public FooBar {
    Objects.requireNonNull(foo);
    Objects.requireNonNull(bar);
  }

}

Wait, what happened to cutting down on boilerplate? The boilerplate seems almost as bad as before, if not worse, because now it's complete duplication. Sure, maybe the semantics are slightly different, so maybe I could tweak the wording. Maybe one should say "can never be null" and the other should say "throws an exception if null", but really, this is not ideal.

Javadoc isn't this unintelligent when I'm overriding methods. If I leave off the Javadoc for a method with an @Override annotation, then Javadoc copies over the documentation from the overridden class or interface. And if I decide I want to tweak the documentation, I even have a {@inheritDoc} Javadoc tag that allows me to copy the documentation from the method I'm overriding.

What's the solution here? Is there a way I can tell Javadoc to use the main record parameter documentation for the constructor or vice versa? Is there some Javadoc tag I can use to make it happen automatically? Because the situation as-is is very far from ideal.

2

There are 2 best solutions below

1
Garret Wilson On BEST ANSWER

I just filed an OpenJDK improvement request for this. The request is now public at JDK-8309252, although it's not clear if they have verified that the Javadoc comments are copied over to an explicit record constructor, or if the warnings have merely been removed. (I sent a follow-up query but have not yet received any reply or ticket update.)

Here is an excerpt from my request:

Please improve Javadoc so that if a constructor is provided for a record, for each missing @param Javadoc will use the @param provided in the record description. This is analogous to how Javadoc already copies method API documentation for a method @Override if no documentation is given.

An analogous situation which Javadoc (now) handles correctly is when overriding methods. A developer may leave off documentation for a method annotated with @Override, and Javadoc will copy over the documentation from the overridden class or interface. In this case there is no need for an @Override annotation, as the semantics are implicit by the context.

Javadoc provides an {@inheritDoc} mechanism for when the developer wants to duplicate the documentation for an overridden method and then add to it. Perhaps Javadoc might provide a {@defaultDoc} or {@recordDoc} or some similar mechanism to add to the record-level documentation.

Still by default, if no documentation at all is provided for a custom constructor, Javadoc should copy over the record-level @param documentation as it does already if no custom constructor is provided, and emit no doclint warning.

In the meantime, for a workaround, it appears (see JDK-8275351) that in Java 18 I may be able to suppress the warnings using @SuppressWarnings("doclint:missing"). I have not verified this. I'll update this answer later this year when I move to Java 21 after it is released.

Update 2023-11-23: I have confirmed that Java 21 brings an improvement. A warning is emitted if the record fields themselves are not documented, but no warning is generated if a validating constructor is present, whether or not it has a Javadoc comment. Unfortunately as Holger mentioned, the record-level field documentation is not copied over (reflecting the unfinished state of JDK-8309252). Instead, boilerplate phrases to the effect of "the value for the foo record component" is inserted. If main documentation is provided for the validating, I still gate "warning: no comment". And if I do provide a main description for the validating constructor, no field documentation is copied at all. At least the suppression of the warning is an improvement.

11
rzwitserloot On

To directly answer your question: No, there is no way to tell the javadoc tool to copy the @param values from the record itself to its construcor or vice versa.

However, the nature of the question seems to go a little further than that and is opening the door to asking 'why were things designed this way' (that's bordering on unsuitable for SO) or 'should I be doing things in a different way? I care in particular about avoiding boilerplate' - and that one seems fair enough for Stack Overflow.

vanilla Javadoc and 'no boilerplate' are fundamentally incompatible

That's the fundamental problem. Here is a trivial example of some code:

/**
 * A student at our university.
 *
 * All students have a unique student ID which consists of
 * 11 digits and which is assigned upon enrollment automatically.
 * The first 2 digits are a checksum generated just like the IBAN
 * algorithm does: (Explanation of that checksum algorithm here).
 *
 * And many more details about the rest of the student class here.
 */
public class Student {

Let's call all that stuff in the middle paragraph the 'ID explanation'.

Just including the actual student ID parts of the student class, we get this complete farce:

/**
 * Blabla student class.
 *
 * ID explanation.
 *
 * More about nature of student objects.
 */
public class Student {

  /** ID explanation. */
  private final long studentId;

  /**
   * blabla.
   *
   * @param studentId ID Explanation.
   */
  public Student(long studentId) {
    ...
  }

  /**
   * returns the ID (Explanation).
   * @return the ID.
   */
  public long getStudentId() {
    return studentId;
  }

  /**
   * Sets the student ID. (Explanation)
   *
   * @param id The new student ID.
   */
  public void setStudentId(long id) {
    this.studentId = studentId;
  }
}

This code's javadoc contains the word 'student ID' seven times, and assuming you hold to the rule that you take all the linter rules used by the vanilla javadoc tool at face value, you can eliminate only about 2 of those (the field, because you might not adhere to the rule that private members need javadoc, and the explanation in the type itself, where it is not required you explain it. Though, this is a bit bizarre; in many ways, that's the best place to explain it).

We now get into opinion territory. However, I'm going to assume you, the reader, would agree that if we have 2 goals:

  • Have javadoc emit no warnings
  • Write good documentation

the best effort is probably: Explain the details about the ID at the class level, and then just call it 'student ID' with either no explanation at all, or by using @link or @see to link back to the right section in the main javadoc, getting our fancy HTML out to make some named anchors so we can link to the right section. This is by and large echoed by the javadoc on java.* classes which tend to do it that way.

However, this has then reduced our complex case into the trivial, and rather stupid, scenario where we just keep repeating ourselves over and over. This:

/**
 * Returns the name.
 * @return The name.
 */
public String getName() { return this.name; }

Is a severe violation of DRY, and ridiculous levels of boilerplate. And yet, for 'properties' (i.e. the use case records are designed for explicitly), this happens all the time. even when there is a lot more to say about a given property (such as our 'student ID' case with the checksum stuff), you still end up here, because where else do you put all that? Surely you don't copy/paste it to 2 to 5 locations (between constructor, setter, getter,'builder method', and wither).

This gets us to a simple truth, and this truth has nothing whatsoever to do with records:

The linter rules enabled by default in javadoc INHERENTLY demand you commit a ton of DRY violations.

Now we go to pure conjecture/opinion: That means the javadoc default configuration is simply unfit for purpose, and nobody should ever listen to those rules. Take em out. javadoc's default output leads one to write horrible code. Double horrible, because unit testing documentation is not possible.

This gets us to a new 'view' on javadoc:

  • Disable the silly linter rules.
  • Make a decision about DRY within the documentation itself.

Here's the key question you need to answer. What is better:

/**
 * Returns the name.
 *
 * @return The name
 */
public String getName() { return this.name; }

or:

public String getName() { return this.name; }

As in, no javadoc at all. I'm strongly suggesting you cozy up to the second case and get comfy with it. The biggest issue (other than the linter tool complaining about the second case!) is that a documentation reader might assume there are interesting things to say about the getName() method but that the author of the class file has failed to state them, whereas with the explicit javadoc the reader may surmise that, truly, this method returns 'the name', and from the context of what this class is about, there really isn't anything more to say.

However, that's a false conclusion. Because of course there could be quite interesting things to say about name (just like there were interesting things to say about student ID, such as about its checksum coding system), but they are said elsewhere (such as in the class-level javadoc), or have been left unsaid. Javadoc is not unit-testable, the fact that there is javadoc does not mean that it is correct, nor that it is complete.

Hence, there is no point to the javadoc here, at all - if the name of the method 100% covers what the docs ought to say about it, don't javadoc it.

This gets a little bit trickier when you have nothing to say because it was said elsewhere, such as at the class level. You may want to add the useless javadoc (/** Returns the name. @return The name */), just so you can {@link} them to the right section where you expand on the details.

To the point: records and javadoc

I therefore very strongly suggest you do the following:

  • Document everything inherent about the properties of your record on the record's own javadoc.
  • Assume the reader is smart enough to look there and simply leave anything completely unjavadocced if the record-level javadoc + the name of the thing combine to say all there is to say.

With these rules in places, The constructor in the snippet included in this question should remain completely unjavadocced. The record-level javadoc + the notion of 'this is a constructor for it' completely cover what you want to say.

The one thing that you should put in the javadoc of a record constructor are notes about the construction process itself that the record-level javadoc doesn't mention. I can imagine, if you want to be ridiculously thorough, that you write this:

/**
 * @param foo Some foo thing; cannot be {@code null}.
 * @param bar Some bar thing; cannot be {@code null}.
 */
public record FooBar(String foo, String bar) {
  /**
   * @throws NullPointerException If either {@code foo} or {@code bar} is {@code null}.
   */
  public FooBar {
    Objects.requireNonNull(foo);
    Objects.requireNonNull(bar);
  }
}

Yes, javadoc will throw a small fit: Will complain about lack of a body, and lack of @param values for the 2 constructor parameters. If you remove the @throws note (and with that, the entire javadoc), javadoc indeed complains that there is no javadoc.

But, it will mention this constructor in the generated javadoc. It simply will not have any text beyond its signature. Which is fine. Or at least, either you accept that this is fine, or you accept that massive boilerplate/DRY violations are unavoidable when writing javadoc, especially for 'properties' - whether you use records to represent them, or not.