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;
}
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;
}