Integrated documentation a la Javadoc is both a benefit and a burden developerWorks

Brian Goetz (brian@quiotix.com), Principal Consultant, Quiotix Corp

 

    The Java language takes an integrated approach to API documentation through the Javadoc comment convention. The Javadoc tool can help generate pretty API documentation, but the vast majority of Java API documentation out there is simply awful. Because it is part of the source code, the responsibility for the documentation of APIs falls squarely on the engineer. In this article, Brian rants about the current state of Java documentation practices and offers some guidelines on how to write more useful Javadoc.

 

For most Java class libraries, the Javadoc is the only documentation. And, except for commercial software components, many Java classes have no Javadoc to speak of. While Javadoc is great as an API reference tool, it's a pretty poor way to learn how a class library is organized and how it should be used. And even when present, the Javadoc often contains only the most basic information about what a method does, ignoring important features such as error handling, domain and range for parameters and return values, thread safety, locking behavior, preconditions, postconditions, invariants, or side effects.

Learning from Javadoc

For many Java facilities, including most open-source packages and most internally developed components, the reality is that very few class libraries or components come with any significant documentation besides the Javadoc. This means that developers will be learning to use facilities from the Javadoc, and we should consider organizing our Javadoc around this reality. I've often joked that one of the most important skills for a Java programmer today is the skillful use of Google and Javadoc to reverse-engineer poorly documented APIs. It may be true, but it's not really very funny.

 

Most Java packages have some sort of "root" object, the first object you need to create before you can get at any other objects in that facility. In JNDI, this root object is the Context, and in JMS and JDBC it is the Connection. If someone were to tell you that the fundamental object in JDBC is a Connection, and how to obtain one, you could likely figure out from the Javadoc how then to create and execute a Statement and iterate the resulting ResultSet by perusing the list of available methods in the Javadoc. But how did you figure out that getting a Connection was your first step? Javadoc organizes classes alphabetically within a package, and methods alphabetically within the class. Unfortunately, there's no magic "Start Here" sign in Javadoc to draw readers to a logical starting place from which to explore an API.

The package description

The closest approximation to a "Start Here" sign is the package description, but it is rarely used effectively. If you place the file package.html with the source code for a package, the standard doclet will put the contents in the generated package-summary.html file along with the list of classes in that package. Unfortunately, the standard doclet, which produces the HTML documentation with which we're all familiar, doesn't make the package description easy to find. When you click on a package in the upper left pane, it brings up the method list in the lower left pane, but doesn't bring up the package summary in the main pane -- you have to click on the package name in the lower left pane to see it. But no matter, most packages don't have a package description anyway.

 

The package documentation is a great place to put "Start Here" documentation as an overview of what the package does, what the key abstractions are, and where to start exploring the package's Javadoc.

The class documentation

Beyond the package documentation, the class-specific documentation can also play a significant role in helping a user to navigate through a new facility. The class documentation should of course include a description of what that particular class does, but should also describe how that class relates to other classes in the package, and in particular identify any relevant factory classes for that class. For example, the Statement class documentation in JDBC should explain that a Statement is obtained through the createStatement() method of the Connection class. That way, if a new user stumbles onto the Statement page, he can find out that first he needs to obtain a Connection. A package that used this convention for each class would quickly point a user to the root object, and the user would be well on his way.

 

Because Javadoc is designed around documenting specific classes, there is often no obvious place in the Javadoc to put example code that illustrates the use of several related classes together. But by narrowly focusing on the documentation for a specific class or method, we lose the chance to talk about how the package fits together. It would be very useful to many users if there were a simple code example that demonstrates some basic usage, in either the package documentation or the class documentation for the root object. For example, the Connection class documentation could have a simple example of acquiring a connection, creating a prepared statement, executing the statement, and iterating the result set. Technically, this might not belong on the Connection page because it describes other classes in the package as well. However, especially if combined with the above technique of referencing classes on which the current class depends, the user would very quickly find his way toward a simple working example, regardless of the organization of the class.

Bad documentation == bad code

For most Java class libraries, with the exception of commercial products sold as packaged components, the Javadoc ranges from nonexistent to poor. Given the reality that the Javadoc is the only documentation we have for most packages, this essentially means that we are resigning ourselves to most of our code not being usable by people other than the authors -- at least not without significant archaeological effort.

 

Because the documentation is now part of the code, I think it's time the software engineering community agreed that good code with bad documentation should be considered bad code, because it is effectively not reusable. Just like unit testing, which until recently had a bad name and which has only recently come into favor among many engineers, API documentation must also become an integrated part of the development process in order for us to improve the reliability and reusability of the software we produce.

Writing Javadoc is a form of code review

A side effect of producing reasonable Javadoc is that it forces us to do a code review of sorts, exploring the architecture of our classes and how they relate to each other. If a package, class, or method is hard to document, it's probably trying to do more than one thing at once, and this should be a clue that perhaps it needs to be re-engineered.

 

The self-review aspects of documentation make it all the more important that writing Javadoc be done early in the development process and then reviewed periodically as the code evolves, rather than simply waiting until the code is complete and then writing the documentation (if there's any time left). The latter strategy, which is all too common, pushes off writing documentation until the end of the project, when schedules are strained and staff is stressed. The result is, all too often, the kind of worthless documentation shown in Listing 1, which provides only the "illusion of documentation." It doesn't communicate any of the things a user really needs to know about how this class works.

 

Listing 1. Typical worthless Javadoc

 

    /**

      * Represents a command history

      */

    public class CommandHistory {

 

        /**

         * Get the command history for a given user

         */

        public static CommandHistory getCommandHistory(String user) {

 

So what constitutes good documentation?

The organizational techniques described above -- referencing related or factory classes in the class description, and including a package overview and code examples -- are a great start toward good documentation. This helps new users use the Javadoc to learn a new facility.

 

But architectural overview is only half of the picture. The other half is the nitty-gritty explanation of what the methods do and don't do, under what conditions they operate, and how they deal with error conditions. Most Javadoc, even that which adequately describes how a method behaves in the desired case, falls short of providing all the information that is needed, including:

 

    * How the method deals with error conditions or bad inputs

    * How error conditions are communicated back to the caller

    * Which specific subclasses of exceptions might be thrown

    * Which values are valid for inputs

    * Class invariants, method preconditions, or method postconditions

    * Side effects

    * Whether there are important linkages between methods

    * How the class deals with an instance being accessed simultaneously from multiple threads

 

The Javadoc conventions provide the @param tag, which lets us document what the parameter means, in addition to its name and type. However, not all methods accept any value for a parameter gracefully. For example, while it is legitimate to pass null to any method that takes an object parameter without running afoul of type-checking rules, not all methods deal gracefully with being passed a null. The Javadoc should explicitly describe the valid range of parameters; if it is expecting a parameter to be non-null, it should say so, and if it is expecting values in a certain range, such as strings of a certain length or integers greater than zero, it should say that, too. Not all methods carefully check their arguments for validity; the combination of no validity checks and no documentation on the range of acceptable inputs is a recipe for disaster.

Return codes

Javadoc makes it easy to describe the meaning of the return value, but just as with method parameters, the @return tag should include a detailed description of the range of values that might be returned. For object-valued return types, will it ever return null? For integer-valued return types, is the result restricted to a set of known values or to non-negative values? Do any return codes have special meaning, such as returning -1 from java.io.InputStream.read() to indicate end-of-file? Is the return code used to indicate an error condition, such as returning a null if a table entry can't be found?

Exceptions

The standard doclet reproduces the throws clause of the method, but the Javadoc @throws tags should be much more specific. For example, NoSuchFileException is a subclass of IOException, but most methods in java.io are only declared to throw IOException. However, the fact that a method may throw NoSuchFileException separately from other IOExceptions is useful for the caller to know -- and it should be included in the Javadoc. Also, you should specify the actual error condition under which various exception classes are thrown so that the caller knows what corrective action to take when a given exception is thrown. You should document every checked and unchecked exception a method may throw with an @throws tag, and document the conditions under which that exception will be thrown.

Preconditions, postconditions, and invariants

Of course, you should document the effect of a method on the object's state. But you may want to go further, and describe method preconditions, postconditions and class invariants. A precondition is a constraint on the object state before a method is called; for example, a precondition to calling Iterator.next() is that hasMore() be true. A postcondition is a constraint on the object state after a method call has completed, such as a List is not empty after calling add(). An invariant is a constraint on the object state that is guaranteed to be true at all times, such as Collection.size() == Collection.toArray().length().

 

Design-by-contract tools, such as jContract, allow you to specify preconditions, postconditions, and class invariants using special comments, and the tools then generate additional code to enforce these constraints. Whether or not you are using a tool to enforce these expectations, documenting these constraints give users an idea of what they can safely do with a class.

Side effects

Sometimes a method has side effects other than changes to the object state, such as changes to the state of related objects, the JVM, or the underlying computing platform. For example, all methods that perform I/O have side effects. Some side effects are harmless, such as keeping a count of the number of requests processed by a class. Others can have significant effects on program performance and correctness, such as modifying the state of an object passed to a method, or storing a copy of a reference to that object. Side effects such as modifying the state of related objects or storing references to objects passed as method parameters should be documented.

Method linkage

Method linkage means that two methods in a class rely on each other and make assumptions about each other's behavior. A common situation where this occurs is when a method uses the toString method of the same class internally, and assumes that toString will format the object state in a particular way. This situation can cause problems if the class is subclassed and the toString method is overridden; the other method will suddenly stop functioning correctly, unless it is also overridden. If your methods rely on implementation behavior of other methods, then those dependencies need to be documented. Then, if the class is subclassed, both methods can be overridden in a consistent way so that the subclass will still function properly.

Thread safety

One of the most important behaviors you should document -- and which almost never is -- is thread safety. Is this class thread-safe? If not, can it be made thread-safe by wrapping calls with synchronization? Must those synchronizations be relative to a specific monitor, or will any monitor used consistently be good enough? Do methods acquire locks on objects that are visible from outside the class?

 

Thread safety is actually not a binary attribute; there are several identifiable degrees of thread safety. Documenting thread safety, or even determining the degree of thread safety, is not always easy. But failing to do so can lead to serious problems; using non-thread-safe classes in concurrent applications can cause sporadic failures which often don't appear until deployment (when the application is exposed to load). And wrapping additional locking around already-thread-safe classes can hurt performance and could even cause deadlocks.

 

In his book, Effective Java Programming Language Guide (see Resources), Josh Bloch offers a useful taxonomy for documenting the degrees of thread safety of a class. Classes can be categorized into one of the following groups, in order of decreasing thread safety: immutable, thread-safe, conditionally thread-safe, thread-compatible, and thread-hostile.

 

This classification is an excellent framework for communicating vital information about a class' behavior under concurrent access. It doesn't matter if you use this exact taxonomy or not, but you should certainly identify the degree of thread safety that your class was designed to exhibit. I would also suggest that if a method acquires a lock on an object that is visible outside of the class' own code, you should document that it is doing so, even though this is only an "implementation detail," so as to assist in making global-lock-order decisions and to prevent deadlock.

Conclusion

Documenting a class's behavior is much more than simply giving one-line descriptions of what each method does. Effective Javadoc should include descriptions of:

 

    * How classes relate to each other

    * How methods affect the state of the object

    * How methods communicate error conditions to their callers and what errors they might signal

    * How the class deals with being used in a multithreaded application

    * The domain of methods' arguments and the range of their return values