英文:
Avoiding Javadoc duplication in Java records
问题
新的Java record
旨在减少样板代码。我可以快速创建一个不可变的 FooBar
类,具有 foo
和 bar
组件,而不必担心局部变量、构造函数值复制和getter,就像这样:
/**
* Foo bar record.
*/
public record FooBar(String foo, String bar) {
}
当然,我希望记录组件的内容出现在生成的Javadocs中!所以我添加了这个:
/**
* 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) {
}
这样做很好,记录组件会显示在文档中。
除了几乎所有记录都需要进行 foo
和 bar
的验证以确保它们不为空外,作为一个好的开发人员,我当然需要这样做,对吗?(正确。)记录允许这样做:
/**
* 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) {
/** 用于参数验证和规范化的构造函数。 */
public FooBar {
Objects.requireNonNull(foo);
Objects.requireNonNull(bar);
}
}
现在,由于我使用了 -Xdoclint:all
,我收到了警告,因为这个构造函数没有为其(隐式的)参数编写文档:
[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
如果你说“只需关闭 -Xdoclint:all
”,你可能误解了重点。这个警告是有效的,因为生成的Javadocs中确实没有为构造函数编写文档!如果有构造函数,就应该有参数的文档。所以我被迫进行旧的复制和粘贴:
/**
* 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);
}
}
等等,减少样板代码发生了什么?这个样板代码看起来几乎和以前一样糟糕,甚至更糟,因为现在它完全重复了。当然,也许语义略有不同,所以也许我可以微调措辞。也许一个应该说“永远不会为 null
”,另一个应该说“如果为 null
,则会抛出异常”,但实际上,这并不理想。
当我覆盖方法时,Javadoc 并不像我覆盖方法时那么不智能。如果我省略了带有 @Override
注解的方法的Javadoc,那么Javadoc会复制被覆盖的类或接口的文档。而且,如果我决定要微调文档,我甚至还有一个 {@inheritDoc}
Javadoc 标签,允许我复制我正在覆盖的方法的文档。
这里的解决方案是什么?我是否可以告诉Javadoc使用主记录参数文档来为构造函数生成文档,反之亦然?我是否可以使用某种Javadoc标签使其自动发生?因为现在的情况与理想差距很大。
英文:
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.
答案1
得分: 4
我刚刚为此提交了一个OpenJDK改进请求。该请求现在在JDK-8309252处公开,尽管目前尚不清楚他们是否已验证了Javadoc注释是否已复制到显式记录构造函数中,或者警告是否仅已移除。(我发送了一个跟进查询,但尚未收到任何回复或工单更新。)
以下是我请求的摘录:
> 请改进Javadoc,以便如果为记录提供了构造函数,对于每个缺失的@param
,Javadoc将使用记录描述中提供的@param
。这类似于Javadoc已经在没有给定方法的API文档的情况下复制了方法的API文档,如果没有提供文档。
>
> …
>
> Javadoc正确处理的类似情况是覆盖方法时。开发人员可能会省略带有@Override
注释的方法的文档,Javadoc将复制覆盖的类或接口的文档。在这种情况下,不需要@Override
注释,因为语义隐含在上下文中。
>
> Javadoc为开发人员想要复制覆盖方法的文档然后进行添加提供了一个{@inheritDoc}
机制。也许Javadoc可以提供一个{@defaultDoc}
或{@recordDoc}
或类似的机制来添加记录级文档。
>
> 但默认情况下,如果没有为自定义构造函数提供任何文档,Javadoc应该像在没有提供自定义构造函数的情况下一样复制记录级别的@param
文档,并且不发出doclint警告。
与此同时,作为一种解决方法,似乎(参见JDK-8275351)在Java 18中,我可以使用@SuppressWarnings("doclint:missing")
来抑制警告。我还没有验证过这一点。等我在今年晚些时候升级到Java 21后,我将在答案中进行更新。
英文:
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.
答案2
得分: 3
直接回答你的问题:没有办法告诉 javadoc 工具将 @param
的值从记录本身复制到它的构造函数,反之亦然。
然而,这个问题的性质似乎要深入一些,它打开了一个问题:“为什么事情被设计成这样?”(这在 Stack Overflow 上可能不太适合),或者“我是否应该以不同的方式做事情?我特别关心避免样板代码” - 这个问题对 Stack Overflow 来说似乎是合理的。
普通的 Javadoc 和‘无样板’本质上是不兼容的
这是一个根本性的问题。这里有一个一些代码的简单示例:
/**
* 我们大学的一个学生。
*
* 所有学生都有一个由 11 位数字组成的唯一学生 ID,该 ID 在注册时自动分配。
* 前两位数字是校验和,就像 IBAN 算法一样生成:(校验和算法的解释在这里)。
*
* 还有关于学生类其余部分的许多细节。
*/
public class Student {
让我们称中间段落中的所有内容为‘ID 解释’。
仅包括学生类的实际学生 ID 部分,我们就得到了这个完全可笑的结果:
/**
* Blabla 学生类。
*
* ID 解释。
*
* 关于学生对象性质的更多信息。
*/
public class Student {
/** ID 解释。 */
private final long studentId;
/**
* blabla。
*
* @param studentId ID 解释。
*/
public Student(long studentId) {
...
}
/**
* 返回 ID(解释)。
* @return ID。
*/
public long getStudentId() {
return studentId;
}
/**
* 设置学生 ID。(解释)
*
* @param id 新的学生 ID。
*/
public void setStudentId(long id) {
this.studentId = studentId;
}
}
这段代码的 javadoc 中包含了“学生 ID”这个词__七次__,假设你遵循了 vanilla javadoc 工具使用的所有 linter 规则,你只能消除其中约 2 个(字段,因为你可能不遵守私有成员需要 javadoc 的规则,以及类型本身的解释,在这种情况下并不要求你解释它。尽管如此,这有点奇怪;在许多方面,这似乎是解释它的最佳位置)。
我们现在进入了意见的领域。然而,我假设你,读者,会同意,如果我们有两个目标:
- 让
javadoc
不产生警告 - 写出好的文档
最好的努力可能是:在类级别解释关于 ID 的细节,然后只是称之为‘学生 ID’,要么根本不解释,要么使用 @link
或 @see
链接到主 javadoc 中的正确部分,使用我们的花哨的 HTML 生成一些命名锚点,以便我们可以链接到正确的部分。这基本上是对 java.*
类的 javadoc 的共鸣做法。
然而,这将把我们复杂的情况简化成微不足道的、相当愚蠢的情况,我们只是一遍又一遍地重复自己。这样做:
/**
* 返回名字。
* @return 名字。
*/
public String getName() { return this.name; }
是 DRY 原则的严重违反,也是令人难以置信的大量样板代码。然而,对于‘属性’(即记录明确设计的用例),这种情况经常发生。即使对于给定属性(例如我们的‘学生 ID’案例,其中包括校验和内容),还有更多值得一提的事情,你最终还是会在这里,因为你将其放在何处?当然你不会将其复制/粘贴到 2 到 5 个位置之间(构造函数、setter、getter、‘构建器方法’ 和 wither)。
这带我们到了一个简单的真理,而这个真理与记录毫无关系:
在 javadoc
中默认启用的 linter 规则本质上要求你犯下许多 DRY 原则的违反。
现在我们来看纯粹的猜测/意见:这意味着 javadoc 的默认配置根本不适合其用途,没人应该听从这些规则。去掉它们。 javadoc 的默认输出会导致人们编写可怕的代码。双倍的可怕,因为单元测试文档是不可能的。
这将我们带到了对 javadoc 的一个新的‘看法’:
- 禁用愚蠢的 linter 规则。
- 在文档本身内部对 DRY 做出决定。
这里有一个你需要回答的关键问题:
/**
* 返回名字。
*
* @return 名字
*/
public String getName() { return this.name; }
还是:
public String getName() { return this.name; }
也就是说,完全没有 javadoc。我强烈建议你接受第二种情况,并对此感到舒适。最大的问题(除了 linter 工具抱怨第二种情况!)是文档读者可能会认为关于 getName()
方法有一些有趣的事情,但类文件的作者未能声明它们,
英文:
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论