Java SE 8 Documentation
Exercises

Class definitions

In the Java language, a commonly used tool of abstraction is the class. With the help of them, it is possible to introduce structure to our programs: the implementation details can be made hidden, the logically related concepts can be grouped together, and new types can be added, including their own type-specific operations. In every case, it is very important to design a so-called interface that can be used to interact with and manipulate the internal state of the object that the given class describes.

A class definition may contain fields (or attributes), method (or member functions), and further classes (this latter will be discussed in details later). For demonstration, let us now consider the Circle class and all of this operations. It is often recommended to create a separate file for each of the class definitions, so that way they become distinct units of compilation.

class Circle {
    Point2D center;
    double  radius;

    /*
     * Assumption: those are derived values, so they always change together
     * with the radius.
     */
    double  area;
    double  circumference;

    // Derivation of the actual values.
    void derive() {
        area          = radius * radius * Math.PI;
        circumference = 2 * radius * Math.PI;
    }

    void translate(double dx, double dy) {
        center.translate(dx, dy);
    }

    void scale(double k) {
        radius *= k;
        derive();
    }

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

As we have seen earlier, all the methods with the static modifier become class-level ones, so there is no active object instance needed in order to be able to work with them. Those methods are more like functions in the non-object-oriented languages.

Visibility Modifiers

But that kind of programming cannot yet be considered robust enough from the side of abstraction. Note that the internal state of the object instances can be easily changed from the outside in this implementation, so certain associated assumptions may be violated involuntarily (or even deliberately). Since the design rigorously relies on such assumptions, those attempts may cause errorneous behavior.

Circle c = new Circle();

c.radius = 2.0;
// The values of c.area and c.circumference are still 0.0

Another important thing to consider is that the actual behavior of methods should always be like a black box to the user. If it is allowed for the user to tackle with the internal state of the object, he or she may work with the class in a way that makes further implementation-specific assumptions. But if the internals of class is modified in same way, the program of the user will go wrong that may show up in either compile-time errors or quite hideous run-time errors.

Circle c = new Circle();

c.center.x += dx;
c.center.y += dy;

In order to avoid such problems, further modifiers were introduced to the language that can be used to control the visibility of names for fields, methods, and classes. Those modifiers are as follows:

  • public: The given name may be used without any restrictions, it is completely visible to the outside world. That has been already employed before since only such elements are visible from the outside of packages, and main programs must be like that in order to be able to run them.

  • protected: The given name may be used within the containing package or any of its derived classes. That is going to be discussed later once class inheritance is introduced.

  • private: The given name can be only accessed from the objects of the same class.

  • No modifier: This is the so-called "package private" visibility where the given name can be only accessed for the elements in the containing package. This is basically the default visibilty, so that was in effect when there was no public added.

Based on these definitions, the following relation could be defined. In this relation, if x is greater than y means that x allows for more visiblity for the name than y would do.

public > (none) > protected > private

With the help of those visibility modifiers, the previous class definition can be now revisited and improved to maintain assumptions regarding the implementation.

class Circle {
    private Point2D center;
    private double  radius;
    private double  area;
    private double  circumference;

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

    public void translate(double dx, double dy) {
        center.translate(dx, dy);
    }

    public void scale(double k) {
        radius *= k;
        derive();
    }

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

However, note that fields that were previously intended to be public cannot also be accessed any more. That is why we will have to create a separate "getter" method for each of the fields, so we could get their values. Obviously, the getter themselves may be invoked by anybody.

class Circle {
    private Point2D center;
    private double  radius;
    private double  area;
    private double  circumference;

    public Point2D getCenter() {
        return center;
    }

    public double getRadius() {
        return radius;
    }

    public double getArea() {
        return area;
    }

    public double getCircumference() {
        return circumference;
    }

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

    public void translate(double dx, double dy) {
        center.translate(dx, dy);
    }

    public void scale(double k) {
        radius *= k;
        derive();
    }

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

In case of references, some extra attention has to be paid, though. The getter may not hand out the reference itself but it must copy the referenced object instead. If this is not done, the reference to the internal state of the object will escape, so anybody will be able to change the referenced object from the outside.

Circle c = new Circle();

c.getCenter().x += dx;
c.getCenter().y += dy;

Let us now fix this mistake.

class Circle {
    private Point2D center;
    private double  radius;
    private double  area;
    private double  circumference;

    public Point2D getCenter() {
        Point2D result = new Point2D();

        result.x = center.x;
        result.y = center.y;
        return result;
    }

    public double getRadius() {
        return radius;
    }

    public double getArea() {
        return area;
    }

    public double getCircumference() {
        return circumference;
    }

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

    public void translate(double dx, double dy) {
        center.translate(dx, dy);
    }

    public void scale(double k) {
        radius *= k;
        derive();
    }

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

If we create a dedicated getter method to a field, it becomes read-only to the outside world. But sometimes it is preferred to allow for writing as well. In those cases, it is not still not solution to set the visibility of the field to public but create a "setter" method. The advantage of using a setter over allowing the direct access is that it also helps to maintain the implementation-specific assumptions and update values for other dependent fields. Again, if a reference is passed, do not forget to copy the object.

class Circle {
    private Point2D center;
    private double  radius;
    private double  area;
    private double  circumference;

    public Point2D getCenter() {
        Point2D result = new Point2D();

        result.x = center.x;
        result.y = center.y;
        return result;
    }

    public void setCenter(Point2D center) {
        this.center = new Point2D();

        this.center.x = center.x;
        this.center.y = center.y;
    }

    public double getRadius() {
        return radius;
    }

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

    public double getArea() {
        return area;
    }

    public double getCircumference() {
        return circumference;
    }

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

    public void translate(double dx, double dy) {
        center.translate(dx, dy);
    }

    public void scale(double k) {
        radius *= k;
        derive();
    }

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

Constructors

When objects for the classes are instantiated, a special method, a so-called constructor is called. As we have seen before, classes can be instantiated by the new operator where the class to instantiate is named with an empty pair of parentheses. That actually corresponds to the invocation of the default constructor. The default constructor is always created if there is no constructor given in the class definition.

class Circle {
    private Point2D center;
    private double  radius;
    private double  area;
    private double  circumference;

    /* This constructor does not have to be defined as it will be generated
     * automatically if no constructors were declared.
     */
    public Circle() {
        center        = null;
        radius        = 0.0;
        area          = 0.0;
        circumference = 0.0;
    }

    public Point2D getCenter() {
        Point2D result = new Point2D();

        result.x = center.x;
        result.y = center.y;
        return result;
    }

    public void setCenter(Point2D center) {
        this.center = new Point2D();

        this.center.x = center.x;
        this.center.y = center.y;
    }

    public double getRadius() {
        return radius;
    }

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

    public double getArea() {
        return area;
    }

    public double getCircumference() {
        return circumference;
    }

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

    public void translate(double dx, double dy) {
        center.translate(dx, dy);
    }

    public void scale(double k) {
        radius *= k;
        derive();
    }

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

Of course, custom constructors can be also created for a class. A constructor acts like a method, although it does not have a return value, it can receive parameters and the visibility modifiers apply to it. It can be imagined as a procedure that is invoked immediately after the object has been instantiated. Then we have the possibility to set the start state for the object depending on the parameters of the constructor.

class Circle {
    private Point2D center;
    private double  radius;
    private double  area;
    private double  circumference;

    /* Now we do not have a default constructor without parameters.  If it is
     * needed, we will have to define that ourselves (see above).
     */
    public Circle(double cx, double cy, double radius) {
        center = new Point2D();
        center.x = cx;
        center.y = cy;
        this.radius = radius;
        derive();
    }

    public Point2D getCenter() {
        Point2D result = new Point2D();

        result.x = center.x;
        result.y = center.y;
        return result;
    }

    public void setCenter(Point2D center) {
        this.center = new Point2D();

        this.center.x = center.x;
        this.center.y = center.y;
    }

    public double getRadius() {
        return radius;
    }

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

    public double getArea() {
        return area;
    }

    public double getCircumference() {
        return circumference;
    }

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

    public void translate(double dx, double dy) {
        center.translate(dx, dy);
    }

    public void scale(double k) {
        radius *= k;
        derive();
    }

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

Note that sometimes it may happen that the parameters of the constructors would violate the assumptions that we have about the objects. In such cases, we do not really have a chance to "undo" the instantiated object. That is why it is common to hide the constructor by setting its visibilty to private, and add a public class-level method for creating objects. In this method, if any of the parameters was not acceptable, no reference to an object but a null was returned.

class Circle {
    private Point2D center;
    private double  radius;
    private double  area;
    private double  circumference;

    /* The radius of the circle may be only positive.
     */
    public static Circle make(double cx, double cy, double radius) {
        return ((radius > 0.0) ? new Circle(cx, cy, radius) : null);
    }

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

    public Point2D getCenter() {
        Point2D result = new Point2D();

        result.x = center.x;
        result.y = center.y;
        return result;
    }

    public void setCenter(Point2D center) {
        this.center = new Point2D();

        this.center.x = center.x;
        this.center.y = center.y;
    }

    public double getRadius() {
        return radius;
    }

    /* The radius cannot be changed to be non-positive.
     */
    public void setRadius(double radius) {
        if (radius <= 0.0)
           return;

        this.radius = radius;
        derive();
    }

    public double getArea() {
        return area;
    }

    public double getCircumference() {
        return circumference;
    }

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

    public void translate(double dx, double dy) {
        center.translate(dx, dy);
    }

    public void scale(double k) {
        radius *= k;
        derive();
    }

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

In the Java language, there is no need to worry about releasing and deinitializing the instantiated objects, so it does not feature destructors. Sometimes there might be needed to perform certain actions on letting the objects behind (for example, release a previously allocated resource), but that is usually solved in a different way.

Overloading

With regard to constructors, note that methods and constructors may have multiple versions with different number and type of parameters. That is called overloading where multiple behaviors are bound to a single name, that is, multiple methods share the same name, then the list of parameters is checked to determine which version to invoke. Methods cannot be overloaded by their return values.

class Circle {
    private Point2D center;
    private double  radius;
    private double  area;
    private double  circumference;

    public static Circle make(double cx, double cy, double radius) {
        return ((radius > 0.0) ? new Circle(cx, cy, radius) : null);
    }

    public static Circle make(double cx, double cy) {
        return new Circle(cx, cy);
    }

    public static Circle make(double radius) {
        return make(0.0, 0.0, radius);
    }

    public static Circle make() {
        return new Circle();
    }

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

    private Circle(double radius) {
        this(0.0, 0.0, radius);
    }

    private Circle(double cx, double cy) {
        this(cx, cy, 1.0);
    }

    private Circle() {
        this(0.0, 0.0, 1.0);
    }

    public Point2D getCenter() {
        Point2D result = new Point2D();

        result.x = center.x;
        result.y = center.y;
        return result;
    }

    public void setCenter(Point2D center) {
        this.center = new Point2D();

        this.center.x = center.x;
        this.center.y = center.y;
    }

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        if (radius <= 0.0)
           return;

        this.radius = radius;
        derive();
    }

    public double getArea() {
        return area;
    }

    public double getCircumference() {
        return circumference;
    }

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

    public void translate(double dx, double dy) {
        center.translate(dx, dy);
    }

    public void scale(double k) {
        radius *= k;
        derive();
    }

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

Dynamic Arrays

Sometimes there might be a need for working with arrays that can expand over time. Althought they are not exactly like arrays, but the java.util.ArrayList can be used for that. The java.util.ArrayList class can be parametrized with the type of the elements, so it can act as an array.

Let us walk through its most important operations:

  • Create a new list. At creation, the lists must be parametrized with the type of the stored elements. That can be given between < and > symbols (that is why such lists are actually called templates). The type can only be given with a class, so, for primitive types, the corresponding wrapper class has to be named here.

     ArrayList<Integer>   listOfInts    = new ArrayList<Integer>();   // not ArrayList<int>
     ArrayList<String>    listOfStrings = new ArrayList<String>();
     ArrayList<Character> listOfChars   = new ArrayList<Character>(); // not ArrayList<char>
  • Append an element to the end of the list. Once the list has been created, a reference was returned that could be called with the add() operation to append an element to the list. Of course, the type of this element has to match with the type of the elements stored in the list.

     listOfInts.add(1);           // This will work due to autoboxing.
     listOfStrings.add("hello");
     listOfStrings.add("world");
  • Append a list to the end of the list. It is possible to append a whole list to the current one, which is of the same type.

     ArrayList<Integer> l1 = new ArrayList<Integer>();
     ArrayList<Integer> l2 = new ArrayList<Integer>();
     l1.add(1); l1.add(2); l1.add(3);
     l2.add(4); l2.add(5); l2.add(6);
     l1.addAll(l2);  // = l1 + l2
  • Insert element to a given position. Elements of the list are shifted at the specified position and the value for the new element is copied to the hole. The type of the inserted value has to match with the type of elements stored in the list. The positions are counted from zero.

     ArrayList<String> l = new ArrayList<String>();
     l.add("hello"); l.add("there");
     l.add(1, "world"); // = "hello", "world", "there"
  • Remove all elements. The list is cleared from every stored value, hence the size of the list becomes zero.

     ArrayList<Circle> l = new ArrayList<Circle>();
     l.add(Circle.make()); l.add(Circle.make(1.0, 2.0, 3.0));
     l.clear();
  • Accessing element at a given position. The position must be in between zero and the length of list minus one. So the zeroth position always refers to the first element of the list.

     ArrayList<Integer> l = new ArrayList<Integer>();
     l.add(1); l.add(2); l.add(3);
     int anElem = l.get(1);
  • Update element at a given position.

     ArrayList<Boolean> l = new ArrayList<Boolean>();
     l.add(true); l.add(false); l.add(true);
     l.set(1, true);
  • Get the size. Getting the number of elements stored in the list. Since the size may change in run time, that is a method.

     ArrayList<Byte> l = new ArrayList<Byte>();
     l.add((byte) 1);
     int lLength = l.size();
  • Find a given value. This operation searches for the first occurrence of the given element in the list and it returns the position for that. If there is no such method, the result becomes -1 (that is, an invalid position is returned).

     ArrayList<Character> l = new ArrayList<Character>();
     l.add('f'); l.add('o'); l.add('o'); l.add('b'); l.add('a'); l.add('r');
     int firstO = l.indexOf('o');
     int firstA = l.indexOf('a');
     int firstZ = l.indexOf('z');  //  = -1
  • Remove element from a given position.

     ArrayList<Boolean> l = new ArrayList<Boolean>();
     l.add(false);
     l.remove(0);  // = empty list
  • Convert to array. A list can be always be turned into an array. Note that the toArray() method here needs an array of the corresponding type, otherwise it will not able to complete the conversion. It does not matter what elements are stored. (That is a consequence of the limitations of the type system of Java.)

     ArrayList<Integer> l = new ArrayList<Integer>();
     l.add(1); l.add(2); l.add(3); l.add(4);
     Integer[] intArray = l.toArray(new Integer[0]);  // = { 1, 2, 3, 4 }

Exercises

  • Implement the utils.IntList class. This class can be used to store arrays of int values that may grow dynamically, and its interface is going to be similar to that of java.util.ArrayList. First, let there be the following constructors:

    • without any parameters (so the array contains nothing in the beginning).

    • with an int array that has the elements to store.

    and let there be the following operations:

    • add(): Append an element to the end of the array.
    • add(): Insert element to a specific position (overloaded).
    • concat(): Pppend the contents of another IntList to the current one.
    • get(): Get the first element.
    • get(): Get the element with a specific position (overloaded).
    • set(): Set the element with a specific position, if the position is valid.
    • remove(): Remove the first element.
    • remove(): Remove the element from a specific position (overloaded).
    • indexOf(): Find the position of a specific element, return -1 if the element cannot be found.
    • size(): Number of the elements.
    • clear(): Dump all elements.
    • toArray(): Create an array that is filled with the elements currently stored in the list.
    • show(): Build a textual representation for the list.

    and let there be a class-level method called read() that can be used to read the elements from a text. It can be safely assumed that the elements in the text are delimited by spaces. Remember to verify if elements are valid numbers. If any of the elements is not a valid number, return a null reference.

    Hide all the other components (fields, methods) in the class from the outside world.

  • Create a program that reads the standard input line by line and once it is finished (no further lines are fed), print all of the collected lines in a reverted order to the standard output. Use the java.util.ArrayList class in the implementation.

  • Implement the IntTree class that is to represent a binary tree that stores integers in an ordered fashion. A binary tree is a network of values where every element may have zero, one, or two successors, and zero or one predecessor. The linking between the elements would be implemented with IntTree object references, they will have to point to the left and right subtrees at the nodes of the tree. In addition to that, an int value is stored. If there is no predecessor for a node, that is the root of the tree.

    Let there be the following operations:

    • insert() that receives an int value and inserts it into the tree depending on its value. If the value to insert is lesser than the value present in the root of the tree, insert the value into its left subtree (recursion). If there is not any left subtree, create a new one with the value in its root. Do the same with the right subtree is the value to is greater than or equal to the value in the root.

    • contains() that determines if the received value is contained by the tree. This method should return with a Boolean value. (Note that that is again a recursive method.)

    • A constructor that receives an array of int values and inserts all of them into the tree.

    • A constructor that creates a tree out of a single value. That tree should not have any subtrees, it should contain only the given value in its root.

    • toArray() that returns all the stored values in an array of int values. Note that the fact that we have inserted the values to maintain an ordering can be exploited here, so the array can be built by first taking the values from the left subtree, then the value in the root, and finally taking the values from the right subtree. Hence the resulting array will have the values in ascending order. That is called inorder traversal of the tree. (That is also a recursive method.)

    • show that returns the textual representation of the tree. Now it is sufficient if only the stored elements are displayed as a list.

    • equalsTo() that determines if the actual object and object received as the parameter are the same. Two binary trees are considered equal to each other if both of them contain exactly the same elements.

Related sources
Front page
Back