Java SE 8 Documentation
Exercises

Deriving and Extending Classes

When declaring classes, it could easily happen that the same methods and fields are written all over again but in another class. For example, consider the case when an object type is introduced for circles.

// That is the shortened version of the previously implemented class.
class Circle {
    private Point2D center;
    private double radius;
    private double area;
    private double circumference;

    public static Circle make(double x, double y, double radius) {
        return ((radius > 0) ? new Circle(x, cy, radius) : null);
    }

    private Circle(double x, double y, double radius) {
        center = new Point2D();
        center.x = x;
        center.y = y;
        this.radius = radius;
        derive();
    }

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        if (radius > 0) {
            this.radius = radius;
            derive();
        }
    }

    private void derive() {
        area          = radius * radius * Math.PI;
        circumference = 2 * radius * Math.PI;
    }

    public double getArea() {
        return area;
    }

    public double getCircumference() {
        return circumference;
    }

    public String toString() {
        return String.format("Circle { center = %s, radius = %.3f }",
          center.show(), radius);
    }
}

Then we would like to implement an object type for implementing squares:

class Square {
    private Point2D center;
    private double side;
    private double area;
    private double circumference;

    public static Square make(double x, double y, double side) {
        return ((side > 0) ? new Square(x, y, side) : null);
    }

    private Square(double x, double y, double side) {
        center = new Point2D();
        center.x = x;
        center.y = y;
        this.side = side;
        derive();
    }

    public double getSide() {
        return side;
    }

    public void setSide(double radius) {
        if (side > 0) {
            this.side = side;
            derive();
        }
    }

    private void derive() {
        area          = side * side;
        circumference = 4 * side;
    }

    public double getArea() {
        return area;
    }

    public double getCircumference() {
        return circumference;
    }

    public String toString() {
        return String.format("Square { center = %s, side = %.3f }",
          center.show(), side);
    }
}

We may notice certain similarities between the structure of those classes and the behavior of their methods that should be exploited in some way. With the help of that, we can get a program that is more maintainable, more manageable, and it becomes easier to modularize its structure.

In order to improve the program, lift the common parts into a common base, a so-called base class. This class is going to be used as a foundation for deriving the two other classes: through derivation, the base is extended in a way where the previous implementation is regained but with a shorter program.

In that case, the base class is going to be Shape that describes shapes in general. According to this, it only contains methods and fields that are applicable to all the shapes. (Here we assume that every shape has a centre point.)

class Shape {
    Point2D center;
    protected double area;
    protected double circumference;

    public Shape(double x, double y) {
        center = new Point2D();
        center.x = x;
        center.y = y;
    }

    public double getArea() {
        return area;
    }

    public double getCircumference() {
        return circumference;
    }

    public String toString() {
        return String.format("center = %s", center.show());
    }
}

After that, all the other classes can be given as a specialization of that class. When a class is derived from an other, all of its methods and fields are visible so they do not have to be declared again.

class Circle extends /* "is a" */ Shape {
    private double radius;

    public static Circle make(double x, double y, double radius) {
        return ((radius > 0) ? new Circle(x, y, radius) : null);
    }

    private Circle(double x, double y, double radius) {
        super(x, y);
        this.radius = radius;
        derive();
    }

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        if (radius > 0) {
            this.radius = radius;
            derive();
        }
    }

    private void derive() {
        super.area          = radius * radius * Math.PI;
        super.circumference = 2 * radius * Math.PI;
    }

    public String toString() {
        return String.format("Circle { %s, radius = %.3f }", super.toString(),
          radius);
    }
}

class Square extends /* "is a" */ Shape {
    private double side;

    public static Square make(double x, double y, double side) {
        return ((side > 0) ? new Square(x, y, side) : null);
    }

    private Square(double x, double y, double side) {
        super(x, y);
        this.side = side;
        derive();
    }

    public double getSide() {
        return side;
    }

    public void setSide(double side) {
        if (side > 0) {
            this.side = side;
            derive();
        }
    }

    private void derive() {
        super.area          = side * side;
        super.circumference = 4 * side;
    }

    public String toString() {
        return String.format("Square { %s, side = %.3f }", super.toString(),
          side);
    }
}

Note that in the resulting program, two new keywords were used during the "compression", which are super and protected.

super is a reference that is similar to this, but it does not refer to the actual object or one of its constructor but that of the base class. That is because when a class is derived, an object of the base's type is created that could be reached through the super reference. In the previous example, super was used to invoke the constructor for the base class and the toString() method from there as well. In the constructors of the Circle and Square classes, calling super() is actually mandatory, because the run-time system will not be able to initialize the fields that are part of the base (which is center in the example). Note that super() has to be the first statement in the constructor body. If the constructor of the base class does not have any parameters, this might be even omitted because the compiler is able to insert that automatically. But if that is not the case, we will have to mention that explicitly.

Another case when super was used is the reference. In the code snipper above, the super.toString() call was used that means that not the toString() method for the actual type should be invoked, but the original method from the base class, which has been overridden. With the help of this, we can have the corresponding implementation from Shape to generate the textual representation of the centre point that could be extended in the derived classes (so it does not have to be written again).

It could be also noticed that in place of the private modifier, protected is used for the fields. The purpose of that is hide the fields for the outside world but make them available for the derived classes. If private was used, the fields would not be accessible even from the derived classes. This was needed for being able to change the values of the area and circumference fields for the Circle and Shape classes. But, for example, there have been separate methods in the base that could be used for modifying those values, they could have stayed private.

A java.lang.Object as a Common Ground

In the Java language, every object type is derived from a common base, which is java.lang.Object. This relationship is implicit, so it is not needed to use the extends keyword, because any class that does not have that will be automatically derived from java.lang.Object, and all of its operations are inherited, for example equals() or toString(). But because java.lang.Object is a generalization, similarly to the Shape class, the default implementation of those methods are very generic. For example, equals() compares only the references of the objects, since there is not much other information is available in the generic case, and toString() generates a textual representation from the memory address and the type of the given object.

class Dummy /* extends Object */ {}

The Dummy class extends the java.lang.Object class, so it will have all of its methods. That is, it will have equals() and toString().

Dummy d1 = new Dummy();
Dummy d2 = new Dummy();
System.out.println(String.format("d1 = %s, d2 = %s", d1.toString(), d2.toString()));
System.out.println(d1.equals(d2));

As we could have seen above, the toString() method could always be overidden in the derived classes. (We will get back to overriding later on.)

Using Composition Instead of Inheritance

It is good to know that inheritence comes with a side effect, because in that case the base class becomes the generalization of the derived ones. That is, it is possible to use any of the derived class' objects anywhere where the objects of the base class could be used (since they support the same operations). That is called the Liskov substitution principle.

So, we can write programs like that one:

Shape[] shapes = new Shape[] { Circle.make(0,0,1), Square.make(0,0,1) };

for (Shape s : shapes) {
    System.out.println(s.toString());
}

Thus, when we would like to relate objects of different types in our programs, inheritance may not be the solution to this problem. Actually, most of the time, there is no need for inheritance at all.

Composition is a kind of relationship (association) between classes of objects. In that case, none of the classes in question are derived from the other, instead, one of them uses the other as part of its definition, hence it becomes an integral part of that, so that cannot exist without it.

As an example, consider the following definition for the Person type:

class Person {
    private String  name;  /* String  "is part of" Person */
    private Integer age;   /* Integer "is part of" Person */

    public static Person make(String name, Integer age) {
        return ((age > 0 && !name.isEmpty()) ? new Person(name, age) : null);
    }

    private Person(String name, Integer age) {
        this.name = new String(name);
        this.age  = new Integer(age);
    }

    public String getName() {
        return new String(name);
    }

    public Integer getAge() {
        return new Integer(age);
    }

    public String toString() {
        return String.format("Person { name = %s, age = %d }", name, age);
    }
}

Here, the Person object uses the String and Integer objects for its own definition. Those components are always related to a Person value, where they are used individually.

Person jesus = Person.make("Jesus Christ", 32);

In result, it turns out that inheritance may be replaced with composition. For example, the Circle class could be implemented in a way where Shape is featured as a simple component there.

class Circle {
    private Shape shape;
    private double radius;

    public static Circle make(double x, double y, double radius) {
        return ((radius > 0) ? new Circle(x, y, radius) : null);
    }

    private Circle(double x, double y, double radius) {
        shape = new Shape(x, y);
        this.radius = radius;
        derive();
    }

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        if (radius > 0) {
            this.radius = radius;
            derive();
        }
    }

    private void derive() {
        shape.area          = radius * radius * Math.PI;
        shape.circumference = 2 * radius * Math.PI;
    }

    public String toString() {
        return String.format("Circle { %s, radius = %.3f }",
          shape.toString(), radius);
    }
}

But a Circle object cannot be used as a Shape object.

Using Aggregation Instead of Inheritance

Of course, it is not necessary to demand that the existence of every object used in the composition depend on the existence of the containing object. That is why aggregation may be used, which is another potential relationship between classes of objects.

As an illustration of that, let us stay at the definition of Person, and add another field to it that stores friends of a person.

class Person {
    private String            name;    /* String  "is part of" Person */
    private Integer           age;     /* Integer "is part of" Person */
    private ArrayList<Person> friends; /* Person  "has" Persons */

    public static Person make(String name, Integer age) {
        return ((age > 0 && !name.isEmpty()) ? new Person(name, age) : null);
    }

    private Person(String name, Integer age) {
        this.name    = new String(name);
        this.age     = new Integer(age);
        this.friends = new ArrayList<Person>();
    }

    public String getName() {
        return new String(name);
    }

    public Integer getAge() {
        return new Integer(age);
    }

    public void addFriend(Person friend) {
        friends.add(friend);
    }

    public String toString() {
        return String.format("Person { name = %s, age = %d, friends = %s }",
          name, age, friends.toString());
    }
}

As it can be seen, friends may exist without any specific Person object. But if that is needed, two Person objects may be connected this way.

Person stan = Person.make("Stanley Marsh", 11);
jesus.addFriend(stan);
System.out.println(stan.toString());
System.out.println(jesus.toString());

Exercises

  • Implement the utils.List type that it capable of storing objects of type java.lang.Object in a linked list (aggregation), which may grow dynamically. Linking here means that new elements are not placed in an array that is resized when needed but as a series of lists referring to each other, where the first element of the list and a list of all the other elements are stored at each node. If there are no more succeeding elements in a node, set the reference of the list to null.

    With the help of this representation, implement the following operations on this type:

    • A constructor that takes an element as parameter and builds a singleton list that contains the element.

    • add(): Insert an element to the beginning of the list.

    • concat(): Append all the elements of the list to the end of the actual one.

    • getFirst(): Get the first element from the list.

    • getRest(): Get the remaining part of the list, that is a list without the first element (as a List).

    • length(): Measure the size of the list.

    • remove(): Remove the first element from the list.

    • toArray(): Return all the elements in the list as a single array.

    • toString(): Return the textual representation of the list.

  • Write a program that is able to read data for various types of shapes from the standard input and store everything. Since the number of shapes is not known beforehand, it would make sense to place them in a java.util.ArrayList.

    Data should be read from the standard input until there is data (like the "EOF", that is, "End of File" is reached) or the "quit" command is received.

    Another command would be "add" that is followed by the details of a shape enclosed in parentheses. The supported shapes should be as follows.

    • "rectangle": A Rectangle object, that is a rectangle that could be created from the coordinates of the top-left corner, and the sizes of two sides.

    • "square": A Square object, that is a square that could be created from the coordinates of the top-left corner, and the size of the sides.

    • "ellipse": An Ellipse object, that is an ellipse that could be created from the coordinates of the center, and the size of the corresponding axes.

    • "circle": A Circle object, that is a circle that could be created from the coordinates of the center, and the length of the radius.

    Once the program is out of input, display all the data read to the standard output (through the custom toString() methods for each of classes).

  • Create an arrays.generic.MArray class that is capable of representing arrays of arbitrary dimensions with the help of a one-dimensional array. The array itself should hold java.lang.Object values (as an aggregation) so they could be any type. The supported operations should be as follows.

    • A constructor where the number of dimensions and the respective sizes could be passed as parameters.

    • set() that could be used for setting a value for an element at the given position (by each dimensions), but only if the number of dimensions and the corresponding indices are valid.

    • get() that could be used for getting a value for an element at the given position (by each dimensions), but only if the number of dimensions and the corresponding indices are valid. Otherwise, a null reference should be returned.

    • getDimensions() that returns the respective sizes of the dimensions stored.

  • With the help of the arrays.generic.MArray (as composition) implement the arrays.DoubleMatrix class, where the Double type is used for storing real numbers in a matrix. Let there be the following operations:

    • A constructor that specifies the dimensions of the matrix.

    • set() that could be used to set values in the given positions.

    • get() that could be used to get values in the given positions.

    In case of invalid indices (that is, when they would point to element out of bounds of the matrix) do not change the matrix. For the get() method, work with the Double (wrapper) class, so null references could be returned for invalid positions.

    It is important to hide the fact from the user that arrays.generic.MArray was used for the implementation in the background.

Related sources
Front page
Back