This is an exercise from Deitel&Deitel's "Java. How to Program (Early Objects)", 10th edition.
9.8 (Quadrilateral Inheritance Hierarchy) Write an inheritance hierarchy for classes Quadrilateral, Trapezoid, Parallelogram, Rectangle and Square. Use Quadrilateral as the superclass of the hierarchy. Create and use a Point class to represent the points in each shape. Make the hierarchy as deep (i.e., as many levels) as possible. Specify the instance variables and methods for each class. The private instance variables of Quadrilateral should be the x-y coordinate pairs for the four endpoints of the Quadrilateral. Write a program that instantiates objects of your classes and outputs each object’s area (except Quadrilateral).
I've seen some realizations of this shape hierarchy on the Internet, but they impose additional restrictions on the orientation of quadrilaterals. For example, the bases of trapezoids/parallelograms are parallel to the X or Y axis etc. There are no indications of such restrictions in the task.
That's why I tried to implement this hierarchy using a simple custom Vector
class. The Vectors
are also used to work with points. This is a practice I've read about here.
package geometry2D;
import doubleWrapper.DoubleHandler;
public class Vector {
private double x;
private double y;
public Vector(double x, double y) {
setX(x);
setY(y);
}
public Vector(Vector vector) {
setX(vector.getX());
setY(vector.getY());
}
public Vector(Vector v1, Vector v2) {
setX(v2.getX() - v1.getX());
setY(v2.getY() - v1.getY());
}
public Vector clone() {
return new Vector(this);
}
public boolean equals(Vector vector) {
return DoubleHandler.compare(this.x, vector.x) == 0 &&
DoubleHandler.compare(this.y, vector.y) == 0;
}
public double length() {
return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
}
public Vector toUnitVector() {
double length = this.length();
if (DoubleHandler.compare(length, 0.0) == 0)
throw new IllegalArgumentException("ERROR: undefined direction! A zero vector cannot be converted to a unit vector!");
return this.scale(1 / length);
}
public Vector scale(double scale) {
return new Vector(this.x * scale, this.y * scale);
}
public Vector add(Vector addend) {
return new Vector(this.x + addend.x, this.y + addend.y);
}
public Vector subtract(Vector subtrahend) {
return new Vector(this.x - subtrahend.x, this.y - subtrahend.y);
}
public static double dotProduct(Vector v1, Vector v2) {
return v1.x * v2.x + v1.y * v2.y;
}
public static double spanArea(Vector v1, Vector v2) {
return v1.x * v2.y - v1.y * v2.x;
}
public boolean isCollinear(Vector v1, Vector v2) {
v1.subtract(this);
v2.subtract(this);
return DoubleHandler.compare(spanArea(v1, v2), 0.0) == 0;
}
public Vector rotate(double angle) {
double cos = Math.cos(angle);
double sin = Math.sin(angle);
Vector rotateUnitX = new Vector(cos, sin);
Vector rotateUnitY = new Vector(-sin, cos);
return rotateUnitX.scale(this.x).add(
rotateUnitY.scale(this.y));
}
public void setX(double x) {
this.x = x;
}
public double getX() {
return x;
}
public void setY(double y) {
this.y = y;
}
public double getY() {
return y;
}
}
To compare doubles properly, I make use of this method for comparing doubles.
package doubleWrapper;
public class DoubleHandler {
private static final long BITS = 0xFFFFFFFFFFFFFFF0L;
public static int compare(double a, double b) {
long bitsA = Double.doubleToRawLongBits(a) & BITS;
long bitsB = Double.doubleToRawLongBits(b) & BITS;
if (bitsA < bitsB)
return -1;
if (bitsA > bitsB)
return 1;
return 0;
}
}
Quadrilateral
class. There are no setters, so the objects of this class are immutable. All the subclasses are implemented in a similar fashion.
Additionally, the immutability solves the "Rectangle-Square" OOP problem (at least as I've understood).
package geometry2D;
public class Quadrilateral {
private Vector v0;
private Vector v1;
private Vector v2;
private Vector v3;
public Quadrilateral(Vector v0, Vector v1, Vector v2, Vector v3) {
if (v1.equals(v0) ||
v2.equals(v0) || v2.equals(v1) ||
v3.equals(v0) || v3.equals(v1) || v3.equals(v2))
throw new IllegalArgumentException("ERROR: two or more points coincide!");
if (v0.isCollinear(v1, v2)
|| v0.isCollinear(v1, v3)
|| v0.isCollinear(v2, v3)
|| v1.isCollinear(v2, v3))
throw new IllegalArgumentException(
"ERROR: at least three of the defined points are collinear!");
this.v0 = v0;
this.v1 = v1;
this.v2 = v2;
this.v3 = v3;
}
public Vector getV0() {
return v0;
}
public Vector getV1() {
return v1;
}
public Vector getV2() {
return v2;
}
public Vector getV3() {
return v3;
}
@Override
public String toString() {
return String.format("%s%n(%.3f, %.3f)%n(%.3f, %.3f)%n(%.3f, %.3f)%n(%.3f, %.3f)%n",
this.getClass().getSimpleName(),
v0.getX(), v0.getY(),
v1.getX(), v1.getY(),
v2.getX(), v2.getY(),
v3.getX(), v3.getY());
}
}
Trapezoid
class.
The order of points in the constructor reflects the sequence in which they are connected in the quadrilateral.
It is assumed that:
v1
- v0
is a base of the trapezoid;
v2
- v0
is a lateral side;
length
is the length of the remaining base.
The trapezoid's diagonal divides it into two triangles, and the area of the trapezoid is calculated by summing the areas of the triangles.
Using Vectors
, it's very easy to find the area of a triangle using determinants (spanArea
method).
package geometry2D;
public class Trapezoid extends Quadrilateral {
public Trapezoid(Vector v0, Vector v1, Vector v2, double length) {
super(v0, v1,
v2.add(new Vector(v0, v1).toUnitVector().scale(length)), v2);
if (length <= 0)
throw new IllegalArgumentException(
"ERROR: a triangle or a self-intersecting trapezoid!");
}
public double getArea() {
Vector base1 = new Vector(getV1(), getV0());
Vector side2 = new Vector(getV2(), getV1());
Vector base2 = new Vector(getV3(), getV2());
Vector side1 = new Vector(getV0(), getV3());
return 0.5 * (
Math.abs(Vector.spanArea(base1, side1)) +
Math.abs(Vector.spanArea(base2, side2)));
}
}
The Parallelogram
, Rectangle
and Square
classes are easy to extend out one by one.
I'd like to hear any improvement suggestions on my realization.