1.1  Exercise: The Circle Class

A class called circle is designed as shown in the following class diagram. It contains:

The source codes for Circle is as follows:

public class Circle {           // save as "Circle.java"
   // private instance variable, not accessible from outside this class
   private double radius;
   private String color;
   
   // 1st constructor, which sets both radius and color to default
   public Circle() {
      radius = 1.0;
      color = "red";
   }
   
   // 2nd constructor with given radius, but color default
   public Circle(double r) {
      radius = r;
      color = "red";
   }
   
   // A public method for retrieving the radius
   public double getRadius() {
     return radius; 
   }
   
   // A public method for computing the area of circle
   public double getArea() {
      return radius*radius*Math.PI;
   }
}

Compile "Circle.java". Can you run the Circle class? Why? This Circle class does not have a main() method. Hence, it cannot be run directly. This Circle class is a “building block” and is meant to be used in another program.

Let us write a test program called TestCircle which uses the Circle class, as follows:

public class TestCircle {        // save as "TestCircle.java"
   public static void main(String[] args) {
      // Declare and allocate an instance of class Circle called c1
      //  with default radius and color
      Circle c1 = new Circle();
      // Use the dot operator to invoke methods of instance c1.
      System.out.println("The circle has radius of " 
         + c1.getRadius() + " and area of " + c1.getArea());
   
      // Declare and allocate an instance of class circle called c2
      //  with the given radius and default color
      Circle c2 = new Circle(2.0);
      // Use the dot operator to invoke methods of instance c2.
      System.out.println("The circle has radius of " 
         + c2.getRadius() + " and area of " + c2.getArea());
   }
}

Now, run the TestCircle and study the results.

TRY:

  1. Constructor: Modify the class Circle to include a third constructor for constructing a Circle instance with the given radius and color.
    // Construtor to construct a new instance of Circle with the given radius and color
    public Circle (double r, String c) {......}
    Modify the test program TestCircle to construct an instance of Circle using this constructor.
  2. Getter: Add a getter for variable color for retrieving the color of a Circle instance.
    // Getter for instance variable color
    public String getColor() {......}
    Modify the test program to test this method.
  3. public vs. private: In TestCircle, can you access the instance variable radius directly (e.g., System.out.println(c1.radius)); or assign a new value to radius (e.g., c1.radius=5.0)? Try it out and explain the error messages.
  4. Setter: Is there a need to change the values of radius and color of a Circle instance after it is constructed? If so, add two public methods called setters for changing the radius and color of a Circle instance as follows:
    // Setter for instance variable radius
    public void setRadius(double r) {
       radius = r;
    }
     
    // Setter for instance variable color
    public void setColor(String c) { ...... }
    Modify the TestCircle to test these methods, e.g.,
    Circle c3 = new Circle();   // construct an instance of Circle
    c3.setRadius(5.0);          // change radius
    c3.setColor(...);           // change color
  5. Keyword "this": Instead of using variable names such as r (for radius) and c (for color) in the methods' arguments, it is better to use variable names radius (for radius) and color (for color) and use the special keyword "this" to resolve the conflict between instance variables and methods' arguments. For example,
    // Instance variable
    private double radius;
       
    // Setter of radius
    public void setRadius(double radius) {
       this.radius = radius;   // "this.radius" refers to the instance variable
                               // "radius" refers to the method's argument
    }
    Modify ALL the constructors and setters in the Circle class to use the keyword "this".
  6. Method toString(): Every well-designed Java class should contain a public method called toString() that returns a short description of the instance (in a return type of String). The toString() method can be called explicitly (via instanceName.toString()) just like any other method; or implicitly through println(). If an instance is passed to the println(anInstance) method, the toString() method of that instance will be invoked implicitly. For example, include the following toString() methods to the Circle class:
    public String toString() {
       return "Circle: radius=" + radius + " color=" + color;
    }
    Try calling toString() method explicitly, just like any other method:
    Circle c1 = new Circle(5.0);
    System.out.println(c1.toString());   // explicit call
    toString() is called implicitly when an instance is passed to println() method, for example,
    Circle c2 = new Circle(1.2);
    System.out.println(c2.toString());  // explicit call
    System.out.println(c2);             // println() calls toString() implicitly, same as above
    System.out.println("Operator '+' invokes toString() too: " + c2);  // '+' invokes toString() too

1.2  Exercise: The Author and Book Classes

A class called Author is designed as shown in the class diagram. It contains:

Write the Author class. Also write a test program called TestAuthor to test the constructor and public methods. Try changing the email of an author, e.g.,

Author anAuthor = new Author("Tan Ah Teck", "ahteck@somewhere.com", 'm');
System.out.println(anAuthor);   // call toString()
anAuthor.setEmail("paul@nowhere.com")
System.out.println(anAuthor);

A class called Book is designed as shown in the class diagram. It contains:

Write the class Book (which uses the Author class written earlier). Also write a test program called TestBook to test the constructor and public methods in the class Book. Take Note that you have to construct an instance of Author before you can construct an instance of Book. E.g.,

Author anAuthor = new Author(......);
Book aBook = new Book("Java for dummy", anAuthor, 19.95, 1000);
// Use an anonymous instance of Author
Book anotherBook = new Book("more Java for dummy", new Author(......), 29.95, 888);

Take note that both Book and Author classes have a variable called name. However, it can be differentiated via the referencing instance. For a Book instance says aBook, aBook.name refers to the name of the book; whereas for an Author's instance say auAuthor, anAuthor.name refers to the name of the author. There is no need (and not recommended) to call the variables bookName and authorName.

TRY:

  1. Printing the name and email of the author from a Book instance. (Hint: aBook.getAuthor().getName(), aBook.getAuthor().getEmail()).
  2. Introduce new methods called getAuthorName(), getAuthorEmail(), getAuthorGender() in the Book class to return the name, email and gender of the author of the book. For example,
    public String getAuthorName() { ...... }

2.  Exercises on Inheritance

2.1  Exercise: The Circle and Cylinder Classes

In this exercise, a subclass called Cylinder is derived from the superclass Circle as shown in the class diagram (where an an arrow pointing up from the subclass to its superclass). Study how the subclass Cylinder invokes the superclass' constructors (via super() and super(radius)) and inherits the variables and methods from the superclass Circle.

You can reuse the Circle class that you have created in the previous exercise. Make sure that you keep "Circle.class" in the same directory.

public class Cylinder extends Circle {  //save as "Cylinder.java"
   private double height;  // private variable
   
   // Constructor with default color, radius and height
   public Cylinder() {
      super();        // call superclass no-arg constructor Circle()
      height = 1.0; 
   }
   // Constructor with default radius, color but given height
   public Cylinder(double height) {
      super();        // call superclass no-arg constructor Circle()
      this.height = height;
   }
   // Constructor with default color, but given radius, height
   public Cylinder(double radius, double height) {
      super(radius);  // call superclass constructor Circle(r)
      this.height = height;
   }
   
   // A public method for retrieving the height
   public double getHeight() {
      return height; 
   }
  
   // A public method for computing the volume of cylinder
   //  use superclass method getArea() to get the base area
   public double getVolume() {
      return getArea()*height; 
   }
}

Write a test program (says TestCylinder) to test the Cylinder class created, as follow:

public class TestCylinder {  // save as "TestCylinder.java"
   public static void main (String[] args) {
      // Declare and allocate a new instance of cylinder
      //   with default color, radius, and height
      Cylinder c1 = new Cylinder();
      System.out.println("Cylinder:"
            + " radius=" + c1.getRadius()
            + " height=" + c1.getHeight()
            + " base area=" + c1.getArea()
            + " volume=" + c1.getVolume());
   
      // Declare and allocate a new instance of cylinder
      //   specifying height, with default color and radius
      Cylinder c2 = new Cylinder(10.0);
      System.out.println("Cylinder:"
            + " radius=" + c2.getRadius()
            + " height=" + c2.getHeight()
            + " base area=" + c2.getArea()
            + " volume=" + c2.getVolume());
   
      // Declare and allocate a new instance of cylinder
      //   specifying radius and height, with default color
      Cylinder c3 = new Cylinder(2.0, 10.0);
      System.out.println("Cylinder:"
            + " radius=" + c3.getRadius()
            + " height=" + c3.getHeight()
            + " base area=" + c3.getArea()
            + " volume=" + c3.getVolume());
   }
}

Method Overriding and "Super": The subclass Cylinder inherits getArea() method from its superclass Circle. Try overriding the getArea() method in the subclass Cylinder to compute the surface area (=2π×radius×height + 2×base-area) of the cylinder instead of base area. That is, if getArea() is called by a Circle instance, it returns the area. If getArea() is called by a Cylinder instance, it returns the surface area of the cylinder.

If you override the getArea() in the subclass Cylinder, the getVolume() no longer works. This is because the getVolume() uses the overridden getArea() method found in the same class. (Java runtime will search the superclass only if it cannot locate the method in this class). Fix the getVolume().

Hints: After overridding the getArea() in subclass Cylinder, you can choose to invoke the getArea() of the superclass Circle by calling super.getArea().

TRY:

Provide a toString() method to the Cylinder class, which overrides the toString() inherited from the superclass Circle, e.g.,

@Override
public String toString() {      // in Cylinder class
   return "Cylinder: subclass of " + super.toString()  // use Circle's toString()
          + " height=" + height;
}

Try out the toString() method in TestCylinder.

Note: @Override is known as annotation (introduced in JDK 1.5), which asks compiler to check whether there is such a method in the superclass to be overridden. This helps greatly if you misspell the name of the toString(). If @Override is not used and toString() is misspelled as ToString(), it will be treated as a new method in the subclass, instead of overriding the superclass. If @Override is used, the compiler will signal an error. @Override annotation is optional, but certainly nice to have.

3.  Exercises on Composition vs Inheritance

They are two ways to reuse a class in your applications: composition and inheritance.

3.1  Exercise: The Point and Line Classes

Let us begin with composition with the statement "a line composes of two points".

Complete the definition of the following two classes: Point and Line. The class Line composes 2 instances of class Point, representing the beginning and ending points of the line. Also write test classes for Point and Line (says TestPoint and TestLine).

public class Point {
   // Private variables
   private int x;    // x co-ordinate
   private int y;    // y co-ordinate
   
   // Constructor
   public Point (int x, int y) {......}
   
   // Public methods
   public String toString() {
      return "Point: (" + x + "," + y + ")";
   }
   
   public int getX() {......}
   public int getY() {......}
   public void setX(int x) {......}
   public void setY(int y) {......}
   public void setXY(int x, int y) {......}
}
public class TestPoint {
   public static void main(String[] args) {
      Point p1 = new Point(10, 20);   // Construct a Point
      System.out.println(p1);
      // Try setting p1 to (100, 10).
      ......
   }
}
public class Line {
   // A line composes of two points (as instance variables)
   private Point begin;    // beginning point
   private Point end;      // ending point
   
   // Constructors
   public Line (Point begin, Point end) {  // caller to construct the Points
      this.begin = begin;
      ......
   }
   public Line (int beginX, int beginY, int endX, int endY) {
      begin = new Point(beginX, beginY);   // construct the Points here
      ......
   }
   
   // Public methods
   public String toString() { ...... }
   
   public Point getBegin() { ...... }
   public Point getEnd() { ...... }
   public void setBegin(......) { ...... }
   public void setEnd(......) { ...... }
   
   public int getBeginX() { ...... }
   public int getBeginY() { ...... }
   public int getEndX() { ...... }
   public int getEndY() { ...... }
   
   public void setBeginX(......) { ...... }
   public void setBeginY(......) { ...... }
   public void setBeginXY(......) { ...... }
   public void setEndX(......) { ...... }
   public void setEndY(......) { ...... }
   public void setEndXY(......) { ...... }
   
   public int getLength() { ...... } // Length of the line
                                     // Math.sqrt(xDiff*xDiff + yDiff*yDiff)
   public double getGradient() { ...... } // Gradient in radians
                                          // Math.atan2(yDiff, xDiff)
}
public class TestLine {
   public static void main(String[] args) {
      Line l1 = new Line(0, 0, 3, 4);
      System.out.println(l1);
   
      Point p1 = new Point(...);
      Point p2 = new Point(...);
      Line l2 = new Line(p1, p2);
      System.out.println(l2);
      ...
   }
}

The class diagram for composition is as follows (where a diamond-hollow-head arrow pointing to its constituents):

Instead of composition, we can design a Line class using inheritance. Instead of "a line composes of two points", we can say that "a line is a point extended by another point", as shown in the following class diagram:

Let's re-design the Line class (called LineSub) as a subclass of class Point. LineSub inherits the starting point from its superclass Point, and adds an ending point. Complete the class definition. Write a testing class called TestLineSub to test LineSub.

public class LineSub extends Point {
   // A line needs two points: begin and end.
   // The begin point is inherited from its superclass Point.
   // Private variables
   Point end;               // Ending point
   
   // Constructors
   public LineSub (int beginX, int beginY, int endX, int endY) {
      super(beginX, beginY);             // construct the begin Point
      this.end = new Point(endX, endY);  // construct the end Point
   }
   public LineSub (Point begin, Point end) {  // caller to construct the Points
      super(begin.getX(), begin.getY());      // need to reconstruct the begin Point
      this.end = end;
   }
   
   // Public methods
   // Inherits methods getX() and getY() from superclass Point
   public String toString() { ... }
   
   public Point getBegin() { ... }
   public Point getEnd() { ... }
   public void setBegin(...) { ... }
   public void setEnd(...) { ... }
   
   public int getBeginX() { ... }
   public int getBeginY() { ... }
   public int getEndX() { ... }
   public int getEndY() { ... }
   
   public void setBeginX(...) { ... }
   public void setBeginY(...) { ... }
   public void setBeginXY(...) { ... }
   public void setEndX(...) { ... }
   public void setEndY(...) { ... }
   public void setEndXY(...) { ... }
   
   public int getLength() { ... }       // Length of the line
   public double getGradient() { ... }  // Gradient in radians
}

Summary: There are two approaches that you can design a line, composition or inheritance. "A line composes two points" or "A line is a point extended with another point"”. Compare the Line and LineSub designs: Line uses composition and LineSub uses inheritance. Which design is better?

3.2  Exercise: Circle and Cylinder using Composition

Try rewriting the Circle-Cylinder of the previous exercise using composition (as shown in the class diagram) instead of inheritance. That is, "a cylinder is composed of a base circle and a height".

public class Cylinder {
   private Circle base;   // Base circle, an instance of Circle class
   private double height;
   
   // Constructor with default color, radius and height
   public Cylinder() {
      base = new Circle(); // Call the constructor to construct the Circle
      height = 1.0; 
   }
   ......
}

Which design (inheritance or composition) is better?

4.  Exercises on Polymorphism, Abstract Classes and Interfaces

4.1  Exercise: Polymorphism

Examine the following codes and draw the class diagram.

abstract public class Animal {
   abstract public void greeting();
}
public class Cat extends Animal {
   @Override
   public void greeting() {
      System.out.println("Meow!");
   }
}
public class Dog extends Animal {
   @Override
   public void greeting() {
      System.out.println("Woof!");
   }
   
   public void greeting(Dog another) {
      System.out.println("Woooooooooof!");
   }
}
public class BigDog extends Dog {
   @Override
   public void greeting() {
      System.out.println("Woow!");
   }
   
   @Override
   public void greeting(Dog another) {
      System.out.println("Woooooowwwww!");
   }
}

Explain the outputs (or error) for the following test program.

public class TestAnimal {
   public static void main(String[] args) {
      // Using the subclasses
      Cat cat1 = new Cat();
      cat1.greeting();
      Dog dog1 = new Dog();
      dog1.greeting();
      BigDog bigDog1 = new BigDog();
      bigDog1.greeting();
       
      // Using Polymorphism
      Animal animal1 = new Cat();
      animal1.greeting();
      Animal animal2 = new Dog();
      animal2.greeting();
      Animal animal3 = new BigDog();
      animal3.greeting();
      Animal animal4 = new Animal();
      
      // Downcast
      Dog dog2 = (Dog)animal2;
      BigDog bigDog2 = (BigDog)animal3;
      Dog dog3 = (Dog)animal3;
      Cat cat2 = (Cat)animal2;
      dog2.greeting(dog3);
      dog3.greeting(dog2);
      dog2.greeting(bigDog2);
      bigDog2.greeting(dog2);
      bigDog2.greeting(bigDog1);
   }
}

5.  More Exercises on OOP

5.1  Exercise: The Discount System

You are asked to write a discount system for a beauty saloon, which provides services and sells beauty products. It offers 3 types of memberships: Premium, Gold and Silver. Premium, gold and silver members receive a discount of 20%, 15%, and 10%, respectively, for all services provided. Customers without membership receive no discount. All members receives a flat 10% discount on products purchased (this might change in future). Your system shall consist of three classes: Customer, Discount and Visit, as shown in the class diagram. It shall compute the total bill if a customer purchases $x of products and $y of services, for a visit. Also write a test program to exercise all the classes.