A Class Designer's Checklist for Java

Steven Zeil

Last modified: Apr 23, 2018
Contents:

1 The Checklist

  1. Is the interface complete?

  2. Are there redundant functions in the interface that could be removed? Are there functions that could be generalized?

  3. Have you used names that are meaningful in the application area?

  4. Are pre-conditions and assumptions well documented?

  5. Are the data members private?

  6. Does every constructor initialize every data member?

  7. Does your class provide a clone() function?

  8. Does your class provide equals() and hashCode() functions?

  9. Does your class provide a toString() function?

2 Discussion and Explanation

Same as in C++

  1. Is the interface complete?
  2. Are there redundant functions or functions that can be generalized?
  3. 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.


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++

2.2 Does your class contain a clone() function?

Useful in C++, even more so in Java.


class C { ⋮ public Object clone() {...} ⋮ }
Expression.java
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.



}

Value.java
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.

}

NumericValue.java
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 {

   public void compareTo (Base b) { ...

but not

   public void compareTo (C b) { ...


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?

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() example

bookEquals.java
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()


Hashing 101

 
Hash tables use a hashing function to compute an element’s position within the array that holds the table.


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

 


hashCode() Example

bookhash.java
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

Example:

bookToString.java
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;

}