A Class Designer's Checklist for Java
Steven Zeil
1 The Checklist
-
Is the interface complete?
-
Are there redundant functions in the interface that could be removed? Are there functions that could be generalized?
-
Have you used names that are meaningful in the application area?
-
Are pre-conditions and assumptions well documented?
-
Are the data members private?
-
Does every constructor initialize every data member?
-
Does your class provide a
clone()
function? -
Does your class provide
equals()
andhashCode()
functions? -
Does your class provide a
toString()
function?
2 Discussion and Explanation
Same as in C++
- Is the interface complete?
- Are there redundant functions or functions that can be generalized?
- Have you used names that are meaningful in the application domain?
2.1 Preconditions and Assertions
A pre-condition is a condition that the person calling a function must be sure is true, before the call, if he/she expects the function to do anything reasonable.
-
Java has an
assert
statement similar to the one in C++-
Failed assertions throw an
AssertionError
-
-
However, Java disables assertions by default
- opposite of C++
- must be enabled with
-ea
option when executing program
Assert Example
public class Day
{
/**
Represents a day with a given year, month, and day
of the Gregorian calendar. The Gregorian calendar
replaced the Julian calendar beginning on
October 15, 1582
*/
public Day(int aYear, int aMonth, int aDate)
//pre: (aMonth > 0 && aMonth <= 12)
// && (aDate > 0 && aDate <= daysInMonth(aMonth,aYear))
{
assert (aMonth > 0 && aMonth <= 12);
assert (aDate > 0 && aDate <= 31);
assert (aYear > 1582 || (aYear == 1582 && aMonth > 10)
|| (aYear == 1582 && aMonth == 10 && aDate >= 15));
⋮
}
Also Same as in C++
-
Are the data members private?
-
Does every constructor initialize every data member?
2.2 Does your class contain a clone() function?
Useful in C++, even more so in Java.
- Standardized as part of the Java API
class C {
⋮
public Object clone() {...}
⋮
}
package SpreadSheetJ.Model;
import java.util.Enumeration;
import java.util.Vector;
// Expressions can be thought of as trees. Each non-leaf node of the tree
// contains an operator, and the children of that node are the subexpressions
// (operands) that the operator operates upon. Constants, cell references,
// and the like form the leaves of the tree.
//
// For example, the expression (a2 + 2) * c26 is equivalent to the tree:
//
// *
// / \
// + c26
// / \
// a2 2
public abstract class Expression implements Cloneable
{
// How many operands does this expression node have?
public abstract int arity();
// Get the k_th operand
public abstract Expression operand(int k);
//pre: k < arity()
// Evaluate this expression
public abstract Value evaluate(SpreadSheet usingSheet);
// Copy this expression (deep copy), altering any cell references
// by the indicated offsets except where the row or column is "fixed"
// by a preceding $. E.g., if e is 2*D4+C$2/$A$1, then
// e.copy(1,2) is 2*E6+D$2/$A$1, e.copy(-1,4) is 2*C8+B$2/$A$1
public abstract Expression clone (int colOffset, int rowOffset);
public Object clone ()
{
return clone(0,0);
}
public Sequence collectReferences()
{
Sequence refs = new Sequence();
for (int i = 0; i < arity(); ++i)
{
Sequence refsi = operand(i).collectReferences();
for (Enumeration p = refsi.front(); p.hasMoreElements(); ) {
CellName cn = (CellName)p.nextElement();
refs.addToBack (cn);
}
}
return refs;
}
public static Expression get (String in)
{
return new ExprParser().doParse(in);
}
public String toString ()
{
String out = "";
if (isInline() && arity() == 0) {
out = getOperator();
}
else if (isInline() && arity() == 1) {
out = getOperator();
Expression opnd = operand(0);
if (precedence() > opnd.precedence()) {
out = out + '(' + opnd + ')';
}
else
out = out + opnd;
}
else if (isInline() && arity() == 2) {
Expression left = operand(0);
Expression right = operand(1);
if (precedence() > left.precedence()) {
out = "(" + left + ')';
}
else
out = left.toString();
out = out + getOperator();
if (precedence() > right.precedence()) {
out = out + '(' + right + ')';
}
else
out = out + right;
}
else {
// write in prefix function-call form
out = getOperator() + '(';
for (int k = 0; k < arity(); ++k) {
if (k > 0)
out = out + ", ";
out = out + operand(k);
}
out = out + ')';
}
return out;
}
// The following control how the expression gets printed by
// the default implementation of put(ostream&)
public abstract boolean isInline();
// if false, print as functionName(comma-separated-list)
// if true, print in inline form
public abstract int precedence();
// Parentheses are placed around an expression whenever its precedence
// is lower than the precedence of an operator (expression) applied to it.
// E.g., * has higher precedence than +, so we print 3*(a1+1) but not
// (3*a1)+1
public abstract String getOperator();
// Returns the name of the operator for printing purposes.
// For constants, this is the string version of the constant value.
}
package SpreadSheetJ.Model;
//
// Represents a value that might be obtained for some spreadsheet cell
// when its formula was evaluated.
//
// Values may come in many forms. At the very least, we can expect that
// our spreadsheet will support numeric and string values, and will
// probably need an "error" or "invalid" value type as well. Later we may
// want to add addiitonal value kinds, such as currency or dates.
//
public abstract
class Value implements Cloneable
{
public abstract String valueKind();
// Indicates what kind of value this is. For any two values, v1 and v2,
// v1.valueKind() == v2.valueKind() if and only if they are of the
// same kind (e.g., two numeric values). The actual character string
// pointed to by valueKind() may be anything, but should be set to
// something descriptive as an aid in identification and debugging.
public abstract String render (int maxWidth);
// Produce a string denoting this value such that the
// string's length() <= maxWidth (assuming maxWidth > 0)
// If maxWidth==0, then the output string may be arbitrarily long.
// This function is intended to supply the text for display in the
// cells of a spreadsheet.
public String toString()
{
return render(0);
}
public boolean equals (Object value)
{
Value v = (Value)value;
return (valueKind() == v.valueKind()) && isEqual(v);
}
abstract boolean isEqual (Value v);
//pre: valueKind() == v.valueKind()
// Returns true iff this value is equal to v, using a comparison
// appropriate to the kind of value.
}
package SpreadSheetJ.Model;
//
// Numeric values in the spreadsheet.
//
public
class NumericValue extends Value
{
private double d;
private static final String theValueKindName = new String("Numeric");
public NumericValue()
{
d =0.0;
}
public NumericValue (double x)
{
d = x;
}
// The string used to identify numeric value kinds.
public static String valueKindName()
{
return theValueKindName;
}
// Indicates what kind of value this is. For any two values, v1 and v2,
// v1.valueKind() == v2.valueKind() if and only if they are of the
// same kind (e.g., two numeric values). The actual character string
// pointed to by valueKind() may be anything, but should be set to
// something descriptive as an aid in identification and debugging.
public String valueKind()
{
return theValueKindName;
}
// Produce a string denoting this value such that the
// string's length() <= maxWidth (assuming maxWidth > 0)
// If maxWidth==0, then the output string may be arbitrarily long.
// This function is intended to supply the text for display in the
// cells of a spreadsheet.
public String render (int maxWidth)
{
String rendering = "" + d;
if (rendering.indexOf('.') >= 0 &&
rendering.indexOf('E') < 0 &&
rendering.indexOf('e') < 0) {
while (rendering.charAt(rendering.length()-1) == '0') {
rendering = rendering.substring(0, rendering.length()-1);
}
if (rendering.charAt(rendering.length()-1) == '.') {
rendering = rendering.substring(0, rendering.length()-1);
}
}
if (maxWidth > 0 && rendering.length() > maxWidth) {
String discarded = rendering.substring(maxWidth);
rendering = rendering.substring(0, maxWidth);
if (discarded.indexOf('.') >= 0) {
// We have a problem - can't fit the decimal point into
// the allowed width
rendering = "****************************";
rendering = rendering.substring(0, maxWidth);
}
}
return rendering;
}
public Object clone()
{
return new NumericValue(d);
}
public double getNumericValue()
{
return d;
}
boolean isEqual (Value v)
//pre: valueKind() == v.valueKind()
// Returns true iff this value is equal to v, using a comparison
// appropriate to the kind of value.
{
NumericValue nv = (NumericValue)v;
return d == nv.d;
}
}
Contra-Variant Inheritance
We’ve talked before about how parameter types in C++ and Java) do not change when we inherit:
class Base {
⋮
public void compareTo (Base b) { ...
⋮
class C extends Base {
- C inherits
public void compareTo (Base b) { ...
but not
public void compareTo (C b) { ...
- This is called contra-variance.
Co-Variant Return Type
An option exception in both languages is for return types:
class Object {
⋮
protected Object clone { ...}
⋮
}
class Value implements Cloneable {
⋮
public Value clone { ...}
⋮
class NumericValue extends Value {
⋮
public NumericValue clone { ...}
⋮
Why Use Co-Variant Return Types?
- Helps to cut down on downcasting in
clone()
calls- e.g.,
void foo (NumericValue n1)
{
NumericValue n2 = n1.clone();
// instead of
NumericValue n3 = (NumericValue)n1.clone();
2.3 equals() and hashCode()
Does your class provide equals()
and hashCode()
functions?
Just as C++ libraries assume the availability of ==
and <
to support many data structures, Java libraries assume equals()
and hashCode()
-
equals()
should compare two objects to see if they have the same value -
unlike
==
which, in Java, compares two objects to see if they are in fact at the same address
equals() example
public class Book {
public Book ()
{
authorName = new String();
title = new String();
}
public String getAuthorName() {return authorName;}
public void setAuthorName(String a) {authorName = a;}
public String getTitle() {return title;}
public void setTitle(String t) {title = t;}
public boolean equals(Object right) {
Book b = (Book)right;
return authorName.equals(b.authorName) &&
title.equals(b.title);
}
private String authorName;
private String title;
}
hashCode()
-
C++ programmers provide
<
because the searchable data structures are based on binary search trees -
Java programmers provide
hashCode()
because the Java API’s searchable data structures are based on hash tables
Hashing 101
In an ideal world…
If we had a really good hashing function, we could implement table insertion and searching this way:
class set {
⋮
private Object[] table = ...;
void insert (Object key)
{
int h = key.hashCode() % table.length;
table[h] = key;
}
Object get (Object key)
{
int h = key.hashCode() % table.length;
if (table[h].equals(key))
return table[h];
else
return null;
}
Collisions
-
For that overly-simple form of hashing to work,
hashCode()
must-
be fast and easy to compute
-
return a unique value for each key.
-
-
In practice, hash functions are never perfect.
-
Two objects with unequal values may sometimes collide by returning the same hash value
-
Hash table algorithms apply various techniques to resolve collisions
-
ADT implementers try to choose hash functions that make collisions rare.
- Distributes the keys uniformly across the table.
-
hashCode() Example
public class Book {
public Book ()
{
authorName = new String();
title = new String();
}
public String getAuthorName() {return authorName;}
public void setAuthorName(String a) {authorName = a;}
public String getTitle() {return title;}
public void setTitle(String t) {title = t;}
public boolean equals(Object right) {
Book b = (Book)right;
return authorName.equals(b.authorName) &&
title.equals(b.title);
}
public int hashCode() {
return 7 * authorName.hashCode()
+ title.hashCode();
}
private String authorName;
private String title;
}
Hashing vs Trees
In the C++ checklist, I argued for the importance of equality and less-than comparisons. In Java, I instead advocate equals
and hashCode
.
-
The
std
library of C++ began with containers based upon binary search trees, which need==
and<
to work.- Hashing-based containers (the
unordered_
… containers) were added much later.
- Hashing-based containers (the
-
The standard API of Java began with constructors based upon hashing, which need
hqshCode
and==
to work.- The
Tree
… variants on those containers, which use binary search trees, were added to the API much later.
- The
The difference is based in history, but that history affects the culture of the programming language. It affects what other programmers will take for granted about your ADTs.
2.4 toString()
Does your class provide a toString()
function?
The toString()
function is used to convert an object to a String
-
Most Java I/O classes read and write
String
s -
Some debuggers use this to display object values
Example:
public class Book {
public Book ()
{
authorName = new String();
title = new String();
}
public String getAuthorName() {return authorName;}
public void setAuthorName(String a) {authorName = a;}
public String getTitle() {return title;}
public void setTitle(String t) {title = t;}
public boolean equals(Object right) {
Book b = (Book)right;
return authorName.equals(b.authorName) &&
title.equals(b.title);
}
public int hashCode() {
return 7 * authorName.hashCode()
+ title.hashCode();
}
public String toString() {
return "'" + title.toString() + "' by "
+ authorName.toString();
}
private String authorName;
private String title;
}
toString()
is the main engine for output in Java. So this recommendation replaces the item in the C++ checklist advocating for the use of an output operator.
You may recall that a major argument in favor of always providing an output operator in C++ was to aid in debugging. That’s even more true of the recommendation to provide toString()
for Java classes. In fact, if you run a Java program in the debugger and ask it to display the value of a variable, it will almost always respond with the output of that variable’s toString()
function.