An example of a class hierarchy of shapes in Java

By Nicholas Duchon (Feb 27, 2014) - Back to my Musings on Java: Musing A - Java Stuff
Here's my take on creating a set of classes in Java related to geometric shapes.

We proceed by asking questions and trying to give reasonable answers to them.
1. Should the shapes be 2-dimensional or 3D?
Can we proceed without limiting ourselves to the dimension? Perhaps in some situations we want 2-D shapes (triangles, circles, etc.), but perhaps sometimes we would like to consider 3-D ones.

Are there other dimensions? Well, of course - 1-D would be a single coordinate, the position on a line or path. 4-D? Space-time for example. Mathematicians often consider higher dimensions, and then if one goes into business applications, one may very well encounter applications with lots more variables, each of which could be taken to be a dimension. Or for good measure, consider the physics of a rigid body: center of mass (3), velocity (3), acceleration (3), time (1), rotation axis (3), rotational velocity (1), rotational acceleration (1), for a grand total of 15 dimensions - and nothing like relativity or quantum mechanics yet.

Sounds like it would be nice to let the dimension be any positive integer.

How about 0? that would be a point. This looks less reasonable, since how can we give any coordinate of a shape if we don't have any coordinates at all.

After all this, we then move on to consider what kind of structure might support arbitrary dimensions of geometric objects. After some thought, you might agree that an array would be nice - with the length of the array equal to the number of dimensions. We can then have 2 or 3 or other dimensional shapes derived from this class, and the dimension array would represent the coordinates of the object in the space of that dimension.
2. How should we organize the hierarchy?
In other words, given the nature of the problem and number of attributes each class requires and how those attributes relate to each other, how should this be organized?

At first blush, from geometry class in high school (or perhaps even earlier), we think of the hierarchy something like this:

We are often told that the proper question to ask when creating a hierarchy is:

If so, then we should make B the parent class and A the child class.

This is a good place to start, particularly when confronted with classes and categories that are pretty much new to us.

But in this case, which is why I like to work on this problem in particular, the question seems to be the reverse of how we should proceed.

So here, a cube will require one dimension, a square-based box would require 2 dimensions and a general box would require 3, so the relationship among these classes should be:

NOTE: This is certainly not the only way to organize a hierarchy of geometric objects. You might want to think about others, and how one might use them in different ways.

3. What should we do, and in what order?

4. On to the code - again, what to do and in what order?

  1. Start with a top class, and NO BODY, save it with the right name
  2. Add block comments at the top: file name, date, author, purpose
  3. This may not be very interesting, but it should compile - if this doesn't compile, there is no reason to write anything else.
  4. Add main, print "hello world" or some other trivial string - again, if this doesn't work, there is no reason to continue
  5. Add one or more instance variables (just one if you are a beginner with Java).
    1. Add a toString method to print the value of the instance variable(s)
    2. Create a constructor which will give the instance variable(s) values from the parameter list
    3. Add a line to main to instantiate the class, giving the instance variable(s) interesting values (0 is generally not a good test)
  6. Create a test plan for this step and execute it
    1. If the changes are small, the test plan can be very short, but in any case, strive to make it thorough.
    2. What do you EXPECT the program to do?
    3. TEST THIS!
    4. Does the program do what you expected?
      1. If so, perhaps the program is working.
      2. If not, you need to figure out why your expectations are different from the actual results - this is the more interesting case because it will force you to learn something.
    5. Time for a break? This would be a good time - you have made progress!
  7. Now we are ready to create a child class.
    1. in this case, perhaps ND_Circle
    2. Extend the parent class
    3. Define new attribute(s)
    4. Define the toString method
    5. Define constructor(s)
      1. Use an explicit call to the parent constructor (super), if appropriate
      2. Note that without such an explicit call, the JVM does an implicit call to the no-parameter version of the parent constructor.
    6. Back to f
    7. Define new methods
    8. Back to f
  8. Back to g until done.
  9. Be happy - Go get a beer or wine or say hi to your family or something.

Main:

You really should notice just how short the main program is.

It consists of lines that instantiate the classes, and then use an implicit call to the class's toString method to print the instance attributes of that class, along with the perimeter, area and/or volume appropriate to that class.

All these instances could be referenced with a variable of the type of the parent class, ND_Shape, as long as only location, perimeter, area and volume were required.

The class hierarchy (from jGrasp):

Variables:

So here's a way of representing the new and inherited variables in each class, and their meanings:
Class
New Instance Attribute
Inherited Attributes
Notes
ND_Shape
double [] coordinates

An array of double of indeterminate size
represents the location of the object in a coordinate system
the dimension of the coordinate system is equal to the number of elements
2-D would use 2 elements
3-D would use 3
etc.
ND_Shape1D
double length
double [] coordinates In 1 dimension, only length makes much sense
but one could add open and closed sets of various kinds
and things could actually get very complicated
consider sets like the Cantor set or the set of all rational numbers.
ND_Circle
double radius
double [] coordinates center and radius are enough in 2-D
but in higher dimensions, specifying a circle would require also
specifying the 2-D surface containing it
ND_Sphere
double radius
double [] coordinates same as circle, but the area is different
has a volume and does not have a perimeter
in higher dimensions, need specifications of a containing 3-D object
ND_TriEq
double side
double [] coordinates an equilateral triangle, length of one side is enough
ND_TriIs
double side2
double side
double [] coordinates
an isosceles triangle, an additional side
side would be the pair of equal sides, side2 is unequal one
ND_Tri
double side3
double side2
double side
double [] coordinates
a general triangle - dimensions of three sides are enough
ND_Cube
double side
double [] coordinates a box with all sides the same length, only one dimension needs to be set
all angles are right angles.
ND_SquarePrism
double side2
double side
double [] coordinates
two dimensions are the same, the third (say height) varies
ND_RectangularPrism
double side3
double side
double side2
double [] coordinates
a box with all three sides varying
all angles 90 degrees

Constructors:

The constructors are not explicitly listed in this table, and all of them use coordinates as the final set of variables.

You should note that nearly all the constructors start with a call to their parent class constructors. This is a convenient way to use the parent class constructors to initialize the attribute(s) they handle, making the child constructor's job a local issue, and much shorter.

Methods:

Note that each class has its own toString method - a nice presentation of the information for that class, including perimeter, area and/or volume as is appropriate for that class.
Class
New Instance Methods
Overridden Inherited Methods
Notes
ND_Shape
double perimeter
double area
double volume
String toStringLoc
String toString First three methods are place holders
all return 0
toString adds "Center" to location string
this method puts the location in parentheses, nicely
ND_Shape1D

String toString
ND_Circle

double perimeter
double area
String toString
returns area and perimeter (circumference) of a circle
only the radius is needed for those calculations
ND_Sphere

double area
double volume
String toString
surface area and volume of a sphere with a given radius
ND_TriEq
double heron
double areaHeron

double perimeter
double area
String toString
perimeter and area of an equilateral triangle based on the length of the side
Heron's formula applies to all triangles, so this is the natural class to have it.
the code uses a dedicated formula and checks against the Heron result.
Call to Heron with 3 equal values
ND_TriIs

double perimeter
double area
double areaHeron
String toString
As the equilateral triangle case, but only two sides are the same.
The call to Heron use two equal values, and one different one
ND_Tri

double perimeter
double area
String toString
The area uses the Heron formula defined in equilateral triangle class.
ND_Cube

double area
double volume
String toString
one dimension is enough to compute area and volume.
this could done the same Heron's formula is used for triangles,
but the basic formulas for a box hardly warrant the effort.
ND_SquarePrism

double area
double volume
String toString
See the comments on a cube,
here two sides are the same, the third is allowed to be different
ND_RectangularPrism

double area
double volume
String toString
See the cube comments,
here all three sides of the box can be different.

Code:

Here's the final code - sample output after code.

// File: ND_Shape.java
// Date: Feb 28, 2014
// Author: Nicholas Duchon
// Purpose: demonstrate a shape class hierarchy

public class ND_Shape {
   double [] coordinates; // location coordinates in N dimensions.
  
   public double perimeter () {return 0;
} // default methods for all shape classes
   public double area      () {return 0;
}
   public double volume    () {return 0;
}
  
   public static void main (String args []) {
      System.out.println (new ND_Shape   (4   , 5   , 6.77));
      System.out.println (new ND_Shape1D (4.23, 1.22, 2.34, 4.54));
      System.out.println (new ND_Circle  (4   , 5.34, 4.97));
      System.out.println (new ND_Sphere  (4   , 5.34, 4.97));
      System.out.println (new ND_Cube    (4   , 5.34, 4.97));
      System.out.println (new ND_SquarePrism      (4, 7,    5.34, 4.97));
      System.out.println (new ND_RectangularPrism (4, 7, 2, 5.34, 4.97));
      System.out.println (new ND_TriEq   (4   ,       5.34, 4.97));     
      System.out.println (new ND_TriIs   (4   , 7   , 5.34, 4.97));
      System.out.println (new ND_Tri     (4   , 5, 7, 5.34, 4.97));
  
} // end main method
  
   ND_Shape (double ... d) {
      coordinates = d;
  
} // end variable argument constructor
  
   public String toStringLoc () {
      String st = "(";
      for (double d: coordinates)
         st += String.format ("%f, ", d);
      st = st.replaceFirst (", $", ")"); // replace at end of string
      return st;
  
} // end toStringLoc method
  
   public String toString () {
      return "Center: " + toStringLoc();
  
} // end default toString method
} // end class ND_Shape

class ND_Shape1D extends ND_Shape {
   double length = 0;
   public ND_Shape1D (double len, double ... d) {
      super (d);
      length = len;
  
} // end constructor
  
   public String toString () {
      return "Center: " + toStringLoc() + " Length: " + length;
  
} // end toString
} // end ND_Shape1D

class ND_Circle extends ND_Shape {
   double radius = 0;
   public ND_Circle (double r, double ... d) {
      super (d);
      radius = r;
  
} // end constructor
  
   public double perimeter () {
      return radius * 2 * Math.PI;
  
} // end method perimeter
  
   public double area () {
      return radius * radius * Math.PI;
  
} // end method area
  
   public String toString () {
      return String.format (
         "Circle:\n   Center: %s, Radius: %f, Perimeter: %f, area: %f",
         toStringLoc(), radius, perimeter(), area());
  
} // end method toString
} // end class ND_Circle

class ND_TriEq extends ND_Shape {
   double side1 = 0;
   public ND_TriEq (double s1, double ... d) {
      super (d);
      side1 = s1;
  
} // end constructor
  
   public double perimeter () {
      return side1 * 3;
  
} // end method perimeter
  
   public double area () {
      return side1 * side1 * Math.sqrt(3.0/16.0);
  
} // end method area
  
   public double areaHeron () {return heron (side1, side1, side1);}
  
   // see Heron's formula, eg, wikipedia
   public double heron (double a, double b, double c) {
      double s = (a + b + c) / 2.0;
      return Math.sqrt (s * (s-a) * (s-b) * (s-c));
  
} // end method heron area
  
   public String toString () {
      return String.format (
         "Equilateral Triangle:\n   Center: %s, side1: %f, " +
         "Perimeter: %f, area: %f\n   Heron's area: %f",
          toStringLoc(), side1, perimeter(), area(), areaHeron());
  
} // end method toString
} // end class ND_TriEq

class ND_TriIs extends ND_TriEq {
   double side2 = 0;
   public ND_TriIs (double s1, double s2, double ... d) {
      super (s1, d);
      side2 = s2;
  
} // end constructor
  
   public double perimeter () {
      return side1 * 2 + side2;
  
} // end method perimeter
  
   public double area () {
      return side2 / 4.0 * Math.sqrt(4 * side1 * side1 - side2 * side2);
  
} // end method area
  
   public double areaHeron () {return heron (side1, side1, side2);}
  
   public String toString () {
      return String.format (
         "Isosceles Triangle:\n   Center: %s, side1: %f, side2: %f, " +
         "Perimeter: %f, area: %f\n   Heron's area: %f",
          toStringLoc(), side1, side2, perimeter(), area(), areaHeron());
  
} // end method toString
} // end class ND_TriIs

class ND_Tri extends ND_TriIs {
   double side3 = 0;
   public ND_Tri (double s1, double s2, double s3, double ... d) {
      super (s1, s2, d);
      side3 = s3;
  
} // end constructor
  
   public double perimeter () {
      return side1 + side2 + side3;
  
} // end method perimeter
  
   public double area () {
      return heron (side1, side2, side3);
  
} // end method area
  
   public String toString () {
      return String.format ("Triangle:\n   Center: %s, side1: %f, "+
         "side2: %f, side3: %f, Perimeter: %f, area: %f",
          toStringLoc(), side1, side2, side3, perimeter(), area());
  
} // end method toString
} // end class ND_Tri

class ND_Sphere extends ND_Shape {
   double radius = 0;
   public ND_Sphere (double r, double ... d) {
      super (d);
      radius = r;
  
} // end constructor
  
   public double area () {
      return radius * radius * Math.PI * 4;
  
} // end method area
  
   public double volume () {
      return radius * radius * radius * Math.PI * 4 / 3;
  
} // end method perimeter
  
   public String toString () {
      return String.format (
         "Sphere:\n   Center: %s, Radius: %f, Area: %f, Volume: %f",
          toStringLoc(), radius, area(), volume());
  
} // end method toString
} // end class ND_Sphere

class ND_Cube extends ND_Shape {
   double side = 0;
   public ND_Cube (double s, double ... d) {
      super (d);
      side = s;
  
} // end constructor
  
   public double area () {
      return side * side * 6;
  
} // end method area
  
   public double volume () {
      return side * side * side;
  
} // end method perimeter
  
   public String toString () {
      return String.format (
         "Cube:\n   Center: %s, Side: %f, Area: %f, Volume: %f",
          toStringLoc(), side, area(), volume());
  
} // end method toString
} // end class ND_Cube

class ND_SquarePrism extends ND_Cube {
   double side2 = 0;
   public ND_SquarePrism (double s1, double s2, double ... d) {
      super (s1, d);
      side2 = s2;
  
} // end constructor
  
   public double area () {
      return side * side * 2 + side * side2 * 4;
  
} // end method area
  
   public double volume () {
      return side * side * side2;
  
} // end method perimeter
  
   public String toString () {
      return String.format (
         "Square Prism:\n   Center: %s, Side: %f, " +
         "Height: %f, Area: %f, Volume: %f",
          toStringLoc(), side, side2, area(), volume());
  
} // end method toString
} // end class ND_SquarePrism

class ND_RectangularPrism extends ND_SquarePrism {
   double side3 = 0;
   public ND_RectangularPrism (double s1, double s2, double s3, double ... d) {
      super (s1, s2, d);
      side3 = s3;
   } // end constructor
  
   public double area () {
      return side * side2 * 2 + side * side3 * 2 + side2 * side3 * 2;
  
} // end method area
  
   public double volume () {
      return side * side2 * side3;
  
} // end method perimeter
  
   public String toString () {
      return String.format (
         "Rectangular Prism:\n   Center: %s, Side: %f, width: %f, " +
         "Height: %f, Area: %f, Volume: %f",
          toStringLoc(), side, side2, side3, area(), volume());
  
} // end method toString
} // end class ND_RectangularPrism

Output of sample code:

Center: (4.000000, 5.000000, 6.770000)
Center: (1.220000, 2.340000, 4.540000) Length: 4.23
Circle:
   Center: (5.340000, 4.970000), Radius: 4.000000, Perimeter: 25.132741, area: 50.265482
Sphere:
   Center: (5.340000, 4.970000), Radius: 4.000000, Area: 201.061930, Volume: 268.082573
Cube:
   Center: (5.340000, 4.970000), Side: 4.000000, Area: 96.000000, Volume: 64.000000
Square Prism:
   Center: (5.340000, 4.970000), Side: 4.000000, Height: 7.000000, Area: 144.000000, Volume: 112.000000
Rectangular Prism:
   Center: (5.340000, 4.970000), Side: 4.000000, width: 7.000000, Height: 2.000000, Area: 100.000000, Volume: 56.000000
Equilateral Triangle:
   Center: (5.340000, 4.970000), side1: 4.000000, Perimeter: 12.000000, area: 6.928203
   Heron's area: 6.928203
Isoceles Triangle:
   Center: (5.340000, 4.970000), side1: 4.000000, side2: 7.000000, Perimeter: 15.000000, area: 6.777721
   Heron's area: 6.777721
Triangle:
   Center: (5.340000, 4.970000), side1: 4.000000, side2: 5.000000, side3: 7.000000, Perimeter: 16.000000, area: 9.797959