0
\$\begingroup\$

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.

\$\endgroup\$
1
\$\begingroup\$

1) Your Quadrilateral class and its subclasses aren't immutable right now... Having no setters doesn't make an objects immutable. Any fields that are mutable themselves and are not hidden nor cloned when returned make the whole object mutable.

An example with your code :

new Square(vector).getV0().setY(17); // I just made the square... not a square anymore

You have to make your Vector class immutable as well by getting rid of the setters.

If I were you, I'd also make every field final.

2) Please note that double (and float) are fairly odd fellas. They have a special value named NaN (used to represents some erroneous results such as 0/0) that can (and will) really mess up your calculations. You should check against it in the Vector constructor with this method : https://docs.oracle.com/javase/10/docs/api/java/lang/Double.html#isNaN(double)

3) You should also reimplement the hashCode method as, when you override equals you should override the hashCode as well as can be seen in the Object javadoc : https://docs.oracle.com/javase/10/docs/api/java/lang/Object.html#hashCode()

4) The isCollinear method does not work as intended right now... don't forget to unit test your code ;)

5) When you override the clone method, you should also implements the Cloneable interface. However, I'd recommend not implementing it (and thus removing the method) as, firstly, there is usually little point in cloning an immutable object and, secondly, you already have a copy constructor.

6) I don't really get why you need to use this special method for your Double comparison.... if you want high precision with real number... double are simply not meant for you...

7) You can't use the getArea method on objects of type 'Quadrilateral' which I find odd

8) Finally, please note that the package name doesn't respect the conventions (it should start with a DNS suffix such as 'fr' or 'com').

Overall, the code is fairly clear and easy to read so that's a good point.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the answer! Actually, in my first version Vectors were mutable, so I failed to make appropriate changes. \$\endgroup\$ – Alex Konrad Dec 8 '18 at 14:33
  • \$\begingroup\$ A point about that specific method for comparing doubles: as far as I know, long calculations involving floating-point values usually lead to error accumulation. These errors might cause incorrect comparisons, for example where two very close values (the difference is in the last several bits) are considered as not equal. This is not the behavior I'd like to see in my program. \$\endgroup\$ – Alex Konrad Dec 8 '18 at 14:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.