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:
• A square is a kind of rectangle
• A cube is a kind of box with a square base
• A box with a square base is a kind of box
• An equilateral triangle is a kind of isosceles triangle
• An isosceles triangle is a kind of triangle
• Everything has some kind of location in a coordinate space

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

• A is a B?
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:

• RectagularPrism (or Box) extends SquarePrism extends Cube

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?

• Select the shapes, or at least select one or two and see where they lead - say:
•  circle to sphere
• box to square box to cube
• Select a top class (say ND_Shape, for Nicholas Duchon) - a place for main!
• Think of main in that class (our class ND_Shape) as test code for that class and its children
• code that really uses these classes would have its own main, not using this main
• Any attributes specific to the parent class? location! dimensions? arbitrary (1 or more). Type: double []
• Create a toString method in EVERY class.
• Consider using toString methods of parent classes
• Consider providing other String methods at various levels of the hierarchy, to be used by children String methods for example
• Consider constructors -
• what information do they need?
• are there reasonable defaults? If not, don't provide default constructors - hide them.
• should they use constructors of the parent class? this can be effective
• Test plans should consider the following issues:
• since we are going to be creating a number of classes, we don't need to solve all problems at once!
• create detailed test cases for each new class as they are created

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

} // end constructor

public double perimeter () {
return radius * 2 * Math.PI;

} // end method perimeter

public double area () {

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

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