Javadoc conventions in the presence of default methods

Brian Goetz brian.goetz at oracle.com
Thu Jan 31 18:31:31 PST 2013


We've tried this thread a few times without success, so let's try it again.

There have been a number of cases where its not obvious how to document 
default methods.  After some analysis, this appears to be just another 
case where the complexity was present in Java from day 1, and default 
methods simply bring it to the fore because the confusing cases are 
expected to come up more often.  The following applies equally well to 
methods in abstract classes (or concrete classes) as to defaults.

There are lots of things we might want to document about a method in an 
API.  Historically we've framed them as either being "specification" 
(e.g., necessary postconditions) or "implementation notes" (e.g., hints 
that give the user an idea what's going on under the hood.)  But really, 
there are four boxes (and we've been cramming them into two):

   { API, implementation } x { specification, notes }

(We sometimes use the terms normative/informative to describe the 
difference between specification/notes.)

As background, here are some example uses for default methods which vary 
in their "expected prevalence of overriding".  I think the variety of 
use cases here have contributed to the confusion on how to document 
implementation characteristics.  (Note that all of these have analogues 
in abstract classes too, one can find examples in Abstract{List,Map,Set}.)

1.  Optional methods.  This is when the default implementation is barely 
conformant, such as the following from Iterator:

     public default void remove() {
         throw new UnsupportedOperationException("remove");
     }

It adheres to its contract, because the contract is explicitly weak, but 
any class that cares about removal will definitely want to override it.

2.  Methods with *reasonable* defaults but which might well be 
overridden by implementations that care enough.  For example, again from 
Iterator:

     default void forEach(Consumer<? super E> consumer) {
         while (hasNext())
             consumer.accept(next());
     }

This implementation is perfectly fine for most implementations, but some 
classes (e.g., ArrayList) might have the chance to do better, if their 
maintainers are sufficiently motivated to do so.  The new methods on Map 
(e.g., putIfAbsent) are also in this bucket.

3.  Methods where its pretty unlikely anyone will ever override them, 
such as this method from Predicate:

     public default Predicate<T> and(Predicate<? super T> p) {
         Objects.requireNonNull(p);
         return (T t) -> test(t) && p.test(t);
     }

These are all common enough cases.  The primary reason that the Javadoc 
needs to provide some information about the implementation, separate 
from the API specification, is so that those who would extend these 
classes or interfaces can know which methods they need to / want to 
override.  It should be clear from the doc that anyone who implements 
Iterator MUST implement remove() if they want removal to happen, CAN 
override forEach if they think it will result in better performance, and 
almost certainly don't need to override Predicate.and().


The question is made more complicated by the prevalent use of the 
ambiguous phrase "this implementation."  We often use "this 
implementation" to describe both normative and informative aspects of 
the implementation, and readers are left to guess which.  (Does "this 
implementation" mean all versions of Oracle's JDK forever?  The current 
version in Oracle's JDK?  All versions of all JDKs?  The implementation 
in a specific class?  Could IBM's JDK throw a different exception from 
UOE from the default of Iterator.remove()?  What happens when the doc is 
@inheritDoc'ed into a subclass that overrides the method?  Etc.  The 
phrase is too vague to be useful, and this vagueness has been the 
subject of many bug report.)

I think one measure of success of this effort should be "can we replace 
all uses of 'this implementation' with something that is more 
informative and fits neatly within the model."


As said earlier, there are four boxes.  Here are some descriptions of 
what belongs in each box.

1.  API specification.  This is the one we know and love; a description 
that applies equally to all valid implementations of the method, 
including preconditions, postconditions, etc.

2.  API notes.  Commentary, rationale, or examples pertaining to the API.

3.  Implementation specification.  This is where we say what it means to 
be a valid default implementation (or an overrideable implementation in 
a class), such as "throws UOE."  Similarly this is where we'd describe 
what the default for putIfAbsent does.  It is from this box that the 
would-be-implementer gets enough information to make a sensible decision 
as to whether or not to override.

4.  Implementation notes.  Informative notes about the implementation, 
such as performance characteristics that are specific to the 
implementation in this class in this JDK in this version, and might 
change.  These things are allowed to vary across platforms, vendors and 
versions.

Once we recognize that these are the four boxes, I think everything gets 
simpler.


Strawman Proposal
-----------------

As a strawman proposal, here's one way to explicitly label the four 
boxes: add three new Javadoc tags, @apinote, @implspec, and @implnote. 
(The remaining box, API Spec, needs no new tag, since that's how Javadoc 
is used already.)  @impl{spec,note} can apply equally well to a concrete 
method in a class or a default method in an interface.

(Rule of engagement: bikeshedding the names will be interpreted as a 
waiver to ever say anything about the model or the semantics.  So you 
may bikeshed, but it must be your last word on the topic.)

/**
   * ... API specifications ...
   *
   * @apinote
   * ... API notes ...
   *
   * @implspec
   * ... implementation specification ...
   *
   * @implnote
   * ... implementation notes ...
   *
   * @param ...
   * @return ...
   */

Applying this to some existing Javadoc, take AbstractMap.putAll:

     Copies all of the mappings from the specified map to this map
     (optional operation). The effect of this call is equivalent to
     that of calling put(k, v) on this map once for each mapping from
     key k to value v in the specified map. The behavior of this
     operation is undefined if the specified map is modified while
     the operation is in progress.

     This implementation iterates over the specified map's
     entrySet() collection, and calls this map's put operation
     once for each entry returned by the iteration.

The first paragraph is API specification and the second is 
implementation *specification*, as users expect the implementation in 
AbstractMap, regardless of version or vendor, to behave this way.  The 
change here would be to replace "This implementation" with @implspec, 
and the ambiguity over "this implementation" goes away.

The doc for Iterator.remove could be:

  /**
   * Removes from the underlying collection the last element returned by
   * this iterator (optional operation). This method can be called only
   * once per call to next(). The behavior of an iterator is unspecified
   * if the underlying collection is modified while the iteration is in
   * progress in any way other than by calling this method.
   *
   * @implspec
   * The default implementation must throw UnsupportedOperationException.
   *
   * @implnote
   * For purposes of efficiency, the same UnsupportedOperationException
   * instance is always thrown. [*]
   */

[*] We don't really intend to implement it this way; this is just an 
example of an @implnote.


The doc for Map.putIfAbsent could be:

   /**
    * If the specified key is not already associated with a value,
associates
    * it with the given value.
    *
    * @implspec
    * Th default behaves as if:
    * <pre> {@code
    * if (!map.containsKey(key))
    *   return map.put(key, value);
    * else
    *   return map.get(key);
    * } </pre>
    *
    * @implnote
    * This default implementation is implemented essentially as described
    * in the API note. This operation is not atomic. Atomicity, if desired,
    * must be provided by a subclass implementation.
    */


Secondary: one can build on this to eliminate some common inheritance 
anomalies by making these inheritable separately, where @inheritDoc is 
interpreted as "inherit the stuff from the corresponding section."  This 
is backward compatible because these sections do not yet exist in old 
docs.  SO to inherit API spec and implementation spec, you would do:

  /**
   * {@inheritDoc}
   * @implspec
   * {@inheritDoc}
   * ...
   */


More information about the lambda-libs-spec-experts mailing list