Java is a statically typed language, but, as we've discovered, the language alone does not determine how much of an advantage that static checking provides. The programmer must actively seek to utilize the type system to get as much static checking as possible.
The following points outline some ways to fully utilize Java's type system. The bug patterns we've discussed provide the motivation behind these suggestions; in fact, some of these points have been discussed already in the context of specific bug patterns, while others are more generally applicable.
- Make fields final whenever possible.
- Make methods final when they should never be overridden.
- Include classes for default values.
- Use checked exceptions to ensure that all clients handle exceptional conditions.
- Define new exception types to nail down the various exceptional conditions precisely.
- Break classes whose instances take on one of a fixed number of states into distinct subclasses in a Composite hierarchy.
- Stay clear of platform-dependent behavior.
- Test on as many platforms as possible.
- Minimize casts and instanceof tests.
- Use the Singleton design pattern to help minimize use of instanceof.
- Use extra methods and dynamic dispatch to help minimize use of instanceof.
Let's consider each of these practices in turn, along with the motivations behind them.
The Java language alone does not determine how much of an advantage that static checking provides—programmers must actively seek to utilize the type system.
Make Fields final Whenever Possible
A final field can never be modified. The type checker itself will prevent its mutation. And, as we've seen from several bug patterns, whenever things can be modified, they can break.
A great rule of thumb: if you can get away with never modifying a field, then make that field impossible to modify.
Make Methods final When They Shouldn't Be Overridden
Dynamic dispatch is a powerful tool and a key component of object-oriented programming. But if a method can be overridden, it can be overridden with a method that does not act as expected in a particular context.
An easy preventative measure is to simply declare methods as final when there will never be a good reason to override them. Of course, this measure should be applied with care; every time a method is declared as final, the extensibility of the program is diminished. As we have seen, extensibility is often in conflict with our goal of keeping bugs out of the program.
One place to apply this maxim in particular is in the context of the Template Method design pattern (for references on design patterns, see Resources). In this pattern, you define an abstract class that contains only part of the logic needed to accomplish its purpose. This abstract class includes both concrete methods and abstract methods. The concrete methods invoke the abstract methods, which are defined differently in each concrete subclass. Of course, the abstract methods are supposed to be overridden, but the defined concrete methods in the base class never should be. So why not make them final and avoid any problems?
Include Classes for Default Values
Don't use a null value as a placeholder for default values; it'll be dereferenced and you'll get the horribly nondescriptive NullPointerException.
Instead, define one class per distinct default value. In general, this will involve constructing an application of the Composite pattern in which the original classes and the new default classes will both become subclasses of a new abstract class. If the default classes contain no fields, apply the Singleton pattern.
By using these classes for default values, you won't just get a better error message when one of those values is used inappropriately; you'll also allow the type checker to rule out many ways in which the value can be used inappropriately before you execute a single line of code.
Use Checked Exceptions to Ensure That All Clients Handle Exceptional Conditions
Checked exceptions are an extremely powerful mechanism. Methods that throw checked exceptions must declare that they throw the exceptions; clients must either handle the exception in a try-catch block or explicitly throw the exception themselves.
In both cases, the client's attention is brought to the fact that the exception can occur; that way, a conscious decision must be made as to how to handle it. And that's all done by the type checker. The code won't compile until it's done. For these reasons, when used properly, checked exceptions can help to improve code robustness.
However, like any sharp knife, they can also cut you. Checked exceptions should not be used to signal run-time errors in a program. Forcing clients of a library containing such exceptions to handle these exceptions is a tedious waste of programmer time and clutters the code with misleading annotations.
The best approach is to use checked exceptions only when throwing such an exception is part of expected (albeit unusual) program behavior. But, in such cases, they can be a big win.
Define New Exception Types to Nail Down the Various Exceptional Conditions Precisely
Because the static type system checks that clients handle checked exceptions, and because exceptions in Java are instances of classes, we can design a class hierarchy of exceptions to increase the precision with which distinct exceptional conditions are handled.
The design tradeoffs when putting together this hierarchy are just like those for other class hierarchies: break groups of exceptions into separate cases when they should be handled separately and group exceptions with common properties together via Composite hierarchies.
Break Classes Whose Instances Take On One of a Fixed Number of States into Distinct Subclasses in a Composite Hierarchy
One way to think about types is as collections of values. Another way to think about them is as static assertions about expressions. How can we break up the collections of values that our types represent so we can make the corresponding assertions they represent as useful as possible?
As we've seen with the Impostor Type bug pattern. Single classes often represent collections of values that are naturally separated into distinct collections. One telltale sign of an Impostor Type is a class that contains a boolean (such as isEmpty) or other field with a fixed set of possible values whose value for a given instance has strong implications as to how that instance can be used.
In these cases, if we restructure the class as a Composite class hierarchy with subclasses—one for each possible value of the field—we can use static typing to more precisely specify which of these subclasses are valid in given contexts. The type checker will do the rest.
One potentially useful alternative approach is to use what is known as the State design pattern. Here, a class of objects dynamically takes on various states during a computation. The current state of the object is represented as a field. The set of states can then be defined in a Composite class hierarchy.
When applying this pattern, try to keep state-specific functionality in the state classes themselves. Then, rather than explicitly checking the state of an object, you can simply dispatch method calls on the state. In this way, we can continue to use static checking to maximum advantage.
Minimize Casts and instanceof Tests
In the current context, it is really casts we're worried about, as they allow us to circumvent the static type system, providing less static checking.
I mention casts and instanceof tests together, however, because you will often see casts inside an if clause guarded by an instanceof test. When that happens, it strongly suggests that you should use separate methods in the various classes to dispatch on the code appropriate for a given type.
Use the Singleton Pattern to Help Minimize Use of instanceof
When there is only one instance of a class, then checking that a value is identical to that instance is equivalent to checking instanceof on the class (except that it's less expensive).
Also, checking that a value of unknown type is equal to some instance of the class is equivalent to checking that the two instances are identical (==). Thus, Singletons give us stronger invariants over the set of possible values in a program.
Some Mentioned Design Patterns
In this article, we mentioned the following design patterns: Command, Composite, Singleton, Template, State, and Visitor.
Command. This pattern encapsulates commands in objects so that you can control their selection by sequencing, queuing, undoing, and otherwise manipulating them.
Composite. This pattern lets you build complex objects by recursively composing similar objects in a tree-like manner. It also allows the objects in the tree to be manipulated in a consistent manner, since they all have a common super-class or interface.
Singleton. This pattern ensures that only one instance of a class is created. All objects that use an instance of that class will use the same instance. This pattern is a potential optimization pattern.
Template Method. This pattern allows you to write an abstract class that contains only part of the logic needed to accomplish its purpose. You then organize the class so that its concrete methods call an abstract method where the missing logic would have appeared, and provide the missing logic in subclass methods that override the abstract methods.
State. This pattern encapsulates the states of an object as discrete objects, each belonging to a separate subclass of an abstract state class.
Visitor. This pattern provides an alternative way to implement an operation that involves objects in a complex structure by providing logic in each of their classes to support the operation. It lets you avoid complicating the classes of the objects in the structure by putting all of the necessary logic in a separate visitor class. It also allows the logic to be varied by using different visitor classes.
Related Online Articles:
- Bug Patterns in Java - The Split Cleaner
- Bug Pattern in Java - The Broken Dispatch
- Bug Pattern in Java - The Impostor Type
- Bug Pattern in Java - Saboteur Data
- Bug Pattern in Java - About the Bug Patterns
- Bug Pattern in Java - Debugging & the Development Process
- Bug Pattern in Java - Bug Specifications & Implementations
- Bug Patterns in Java - The Run-On Initialization
- Bug Pattern in Java - The Double Descent
- Bug Patterns in Java - Platform-Dependent Patterns
No comment yet. Be the first to post a comment.