MODULE 3q - An Extended Java Object
THE BOX PROGRAM RENAMED
Log in to a Thor host and copy the file Box.java to Block.java and
then make all the amendments indicated by comments in the program
below. The name of the public class is changed from Box to Block
in the first line and, secondly, third System.out.println statement
has been removed. Finally, there is no static in class Square.
Set up this program now.
public class Block // new name
{ public static void main(String[] args)
{ Square jack = new Square(6);
System.out.println("Details of jack...\n" + jack.toString());
Square jill = new Square(5);
System.out.println("Details of jill...\n" + jill);
} // println() removed
}
class Square
{ private int side; // static removed
public Square(int s)
{ this.side = s;
}
public int area()
{ return this.side*this.side;
}
public String toString()
{ return "Square: Side = " + this.side + "\n" +
" Area = " + this.area() + "\n";
}
}
Try the program out. It should behave exactly as an earlier version of
Box.java did. The output should be:
Details of jack...
Square: Side = 6
Area = 36
Details of jill...
Square: Side = 5
Area = 25
SQUARES AND CUBES
The goal of this worksheet is to experiment with a second do-it-yourself
type Cube which, of course, is a three-dimensional version of a Square.
It would not be difficult to declare class Cube thus:
class Cube
{ private int side;
public Cube(int s)
{ this.side = s;
}
public int surface()
{ return 6*this.side*this.side;
}
public String toString()
{ return "Cube: Side = " + this.side + "\n" +
" Surface = " + this.surface() + "\n";
}
}
Apart from the changes of name from Square to Cube and area to
surface this is just about identical to class Square except that the
new surface() method has a factor of 6 in it to reflect that the fact
that a Cube is made from six Squares and its total surface area is
therefore six times that of one of the component Squares.
This relationship between a Cube and a Square leads to the idea of
extending a class...
A FIRST VARIATION
The first variation of the Block program, shown below, incorporates a
declaration of a new class Cube. Note that jack continues to be of
type Square but jill is of type Cube as indicated. All changes to the
previous version are indicated by comments. Set up this version now.
public class Block
{ public static void main(String[] args)
{ Square jack = new Square(6);
System.out.println("Details of jack...\n" + jack.toString());
Cube jill = new Cube(5); // type Cube now
System.out.println("Details of jill...\n" + jill);
}
}
class Square
{ private int side;
public Square(int s)
{ this.side = s;
}
public int area()
{ return this.side*this.side;
}
public String toString()
{ return "Square: Side = " + this.side + "\n" +
" Area = " + this.area() + "\n";
}
}
class Cube extends Square // note extends Square
{ public Cube(int s)
{ super(s); // super(s) in constructor
}
public int surface() // multiplies inherited
{ return 6*this.area(); // area() by a factor of 6
}
public String toString() // overrides inherited toString()
{ return "Cube: Surface = " + this.surface() + "\n";
}
}
Try the program out. The output should be:
Details of jack...
Square: Side = 6
Area = 36
Details of jill...
Cube: Surface = 150
INHERITANCE, OVERRIDING AND super
By writing class Cube extends Square in the heading line, class
Cube is said to `inherit' from class Square. In effect, class Cube
contains all the data fields and methods of class Square as well as
any data fields and methods declared in itself.
There are two exceptions: the first is that class Cube doesn't inherit
the constructor of class Square and the second is that any method in
class Cube which has the same name as one in class Square will take
precedence. Thus the toString() method in class Cube is said to
`override' the toString() method inherited from class Square.
A principal consequence is that class Cube inherits data field side
and this can be used to specify the side of the Cube just as well as
it can be used to specify the side of a Square provided a slightly
different constructor is used. The obvious constructor for Cube is:
public Cube(int s)
{ this.side = s;
}
Unfortunately this doesn't work. Instead of assigning a value to
this.side in the body of the constructor Cube the approved approach
is to invoke the constructor of the class being inherited from. This
suggests that Square(s) might be appropriate but the rules require
using super(s) as a general-purpose way of invoking the constructor
of the `parent' class.
AN INHERITANCE DIAGRAM
The terms `inheritance', `parent class' and `child' class are often
used when discussing object oriented programming. Sometimes an
inheritance diagram is drawn to describe the relationships:
Square
/ \
/ \
/ \
Cube ????
Rather as in a family tree, this reflects the fact that class Cube is
a child class whose parent class is Square. The ???? represents a
potential sister class for Cube. For example one might have a class
Domino inheriting from Square on the grounds that a Domino is formed
from two Squares.
It is important to note that a parent class may have any number of
children but a child class has exactly one parent.
A SECOND VARIATION
The method surface() returns the surface area of a Cube and achieves
this by simply invoking the area() method inherited from Square. The
area() method supplies the area of one Square face of the Cube and,
when this is multiplied by 6, you get the total surface area of the
Cube.
It might be thought that the surface() method could equally return
6*this.side*this.side and this is the only modification in the second
variation of the Block program shown below. Set up this version now.
public class Block
{ public static void main(String[] args)
{ Square jack = new Square(6);
System.out.println("Details of jack...\n" + jack.toString());
Cube jill = new Cube(5);
System.out.println("Details of jill...\n" + jill);
}
}
class Square
{ private int side;
public Square(int s)
{ this.side = s;
}
public int area()
{ return this.side*this.side;
}
public String toString()
{ return "Square: Side = " + this.side + "\n" +
" Area = " + this.area() + "\n";
}
}
class Cube extends Square
{ public Cube(int s)
{ super(s);
}
public int surface()
{ return 6*this.side*this.side; // THE ONLY CHANGED LINE
}
public String toString()
{ return "Cube: Surface = " + this.surface() + "\n";
}
}
TRY IT OUT
Try compiling this program. You will get an error message complaining:
Variable side in class Cube not accessible from class Cube.
Although side is indeed `in class Cube' it is there by inheritance
and Java still recognises that its origin is in a different class AND
that it has the visibility modifier private in that different class.
A THIRD VARIATION
Change the declaration of the data field side from
private int side;
to
public int side;
and try compiling again. There should be no error messages and, when the
program is run, the output should be as before.
BAD PRACTICE
Although the program now works again, it is generally bad practice to
make a data field public in these circumstances. The principle of
encapsulation is compromised. The fourth variation will show the
approved way of attending to the problem.
Before looking at the next variation, note that the consequences of side
being private explain why the toString() method of Cube didn't include:
return "Cube: Side = " + this.side + "\n" +
When private, the inherited date field side would not be accessible.
A FOURTH VARIATION
The approved way of determining the value of a private data field is
to get at it via a public method. In the fourth variation shown
below, the data field side is private once more but there is a new and
public method getSide() in class Cube which returns the value of side.
This method is invoked in both the surface() method and the toString()
method of Cube.
All changes to the previous version are indicated by comments. Set
up this version now.
public class Block
{ public static void main(String[] args)
{ Square jack = new Square(6);
System.out.println("Details of jack...\n" + jack.toString());
Cube jill = new Cube(5);
System.out.println("Details of jill...\n" + jill);
}
}
class Square
{ private int side; // back to private
public Square(int s)
{ this.side = s;
}
public int getSide()
{ return this.side; // new method
}
public int area()
{ return this.side*this.side;
}
public String toString()
{ return "Square: Side = " + this.side + "\n" +
" Area = " + this.area() + "\n";
}
}
class Cube extends Square
{ public Cube(int s)
{ super(s);
}
public int surface()
{ return 6*this.getSide()*this.getSide(); // changed again
}
public String toString()
{ return "Cube: Side = " + this.getSide() + "\n" + // modified toString()
" Surface = " + this.surface() + "\n"; // method
}
}
TRY IT OUT
Compile and run this program. The output should be:
Details of jack...
Square: Side = 6
Area = 36
Details of jill...
Cube: Side = 5
Surface = 150
Note that despite side being declared private there is no difficulty
about referring to this.side in class Square because the data field
side is declared in this class and is being referred to within it.
Note also that the approved way of changing the value of side from
outside class Square would also be to go via a public method, perhaps
called setSide() as in:
public void setSide(int s)
{ this.side = s:
}
This, of course, is effectively duplicating the work of the constructor
but one cannot use a constructor except at the time of instantiation.
JAVA NAMING CONVENTIONS - YET MORE
Note that getSide and setSide follow the Java naming convention.
As method names they begin with lower-case letters but the new
word Side in the middle merits an upper-case S. The data field
side continues of course to merit a lower-case s.
A FIFTH VARIATION - OVERLOADING
The constructors in class Square and class Cube enable the user to
specify any int value for the side of a Square or a Cube. Suppose it
turns out that the most commonly used value for side is 1 (giving rise
to a so-called `unit square' or `unit cube'). It would be useful to
be able to set up such Squares and Cubes by writing
new Square() and new Cube()
where there are no actual arguments. For these operations to work
the constructors would have to be
public Square() and public Cube()
{ this.side = 1; { super(1);
} }
where there are no formal arguments. Notice that super(1) calls
the earlier version of the constructor in class Square and it would
probably be better now to use plain super() to invoke the new
argumentless constructor in class Square.
A potential worry is whether the new versions of the constructors can
coexist with the earlier, more general purpose, constructors. It
turns out that Java allows such coexistence, which is generally known
as `overloading'.
Moreover, Java allows overloading of methods as well as constructors.
Thus, any class may contain several constructors or several methods of
the same name provided only that their argument lists are different.
The difference has to be more than a simple change of identifier, thus
the following:
public Square(int s) and public Square(int edge)
are not deemed to be different, In each case there is a single int
argument and the fact that one is called s and the other edge doesn't
count.
The fifth variation of the Block program is shown overleaf. All
changes to the previous version are indicated by comments. This
variation incorporates the earlier constructors as well as new
versions which, when called, result in the instantiation of unit
Squares and unit Cubes respectively. These might be regarded as
defaults.
In method main(), jack and jill are set to a unit Square and a unit
Cube respectively. The earlier constructors, though present, are
not used directly. Note that super() has been used in the new
constructor in class Cube though super(1) would achieve the same
effect.
Set up this version now.
public class Block
{ public static void main(String[] args)
{ Square jack = new Square(); // no argument
System.out.println("Details of jack...\n" + jack.toString());
Cube jill = new Cube(); // no argument
System.out.println("Details of jill...\n" + jill);
}
}
class Square
{ private int side;
public Square() // new constructor
{ this.side = 1;
}
public Square(int s) // old constructor
{ this.side = s;
}
public int getSide()
{ return this.side;
}
public int area()
{ return this.side*this.side;
}
public String toString()
{ return "Square: Side = " + this.side + "\n" +
" Area = " + this.area() + "\n";
}
}
class Cube extends Square
{ public Cube() // new constructor
{ super(); // no argument in super()
}
public Cube(int s) // old constructor
{ super(s);
}
public int surface()
{ return 6*this.getSide()*this.getSide();
}
public String toString()
{ return "Cube: Side = " + this.getSide() + "\n" +
" Surface = " + this.surface() + "\n";
}
}
TRY IT OUT
Compile and run this program. The output should be:
Details of jack...
Square: Side = 1
Area = 1
Details of jill...
Cube: Side = 1
Surface = 6
EXERCISES
Verify that the earlier constructors still function properly by setting
up four local variables in method main() thus:
{ Square jack = new Square(); // unit Square
Cube jill = new Cube(); // unit Cube
Square jacky = new Square(6); // Square with side 6
Cube jilly = new Cube(5); // Cube with side 5
Here jack and jill exploit the new constructors and jacky and jilly
exploit the earlier versions. Add appropriate println() statements to
write out the four objects.
Next, add an extra method volume() to class Cube. The volume can
be calculated by multiplying the area of one face by the side but
the getSide() method must be used. The new method will be:
public int volume()
{ return this.getSide()*this.area();
}
Additionally modify the toString() method so that the volume is
written out too. Try the program out:
public String toString()
{ return "Cube: Side = " + this.getSide() + "\n" +
" Area = " + this.surface() + "\n" +
" Vol = " + this.volume() + "\n";
}
Add a new method perimeter() to class Square and arrange that this
method returns the perimeter of the Square. Adjust the toString()
method in class Square so that when jack's details are written out
they include the Perimeter as well as the Side and Area.
Next add a new method seam() to class Cube and arrange that this method
returns the total length of all the sides of the Cube. Adjust the
toString() method in class Cube so that when jill's details are written
out they include the Seam as well as the Side, Area and Volume.
OTHER TASKS
By this stage of the course you should be able to attempt the following
problems in the Problems sheet:
9. Determining a Square Root by Iteration
10. The Recurring Fraction Problem
3. [REVISITED] All Prime Numbers less than 600
Solve problem 3 using a boolean array instead of an int array.
It makes much more sense to use type boolean.