Relevant Reading Material
Chapters From How to Design Classes (HtDC)
-
Chapter 4
-
Chapter 5
-
Chapter 6
-
Intermezzo 1
-
Chapter 8
-
Chapter 9
-
Chapter 10
-
Chapter 11
Evaluation order DrRacket and Java
In DrRacket we had a set of rules that dictate how an expression gets evaluated. We used the Stepper in order to visualize these steps for an expression under evaluation.
Java has its own evaluation rules. We will start with a core set of evaluation rules and add to this set as the class progresses. All popular IDEs also provide a debugger that allows us to step through the execution of a Java program. For now you can think of a debugger to be similar to the DrRacket stepper. As we progress through the course we’ll see more on debugging and some of the difference between a debugger and the DrRacket Stepper.
Itemizations
Let’s consider the design of a chess game. We would like to capture the pieces of a chess board and the chess board as data. Our chess game will also have a graphical user interfaces so we need to capture any information that we might need in order to draw the pieces on our chessboard.
We will focus on the information for chess pieces first excluding movement.
Chess has the following pieces
-
Pawn
-
Rook
-
Knight
-
Bishop
-
Queen and
-
King
Each player is given a collection of these pieces. The collection consists of
-
8 Pawns
-
2 Rooks
-
2 Knights
-
2 Bishops
-
1 Queen and
-
1 King
The two collections have different colours, one player has white and the other black.
One possible way to capture these pieces of information in Racket could be the following
;; A Colour is one of
;; - 'white
;; - 'black
;; A Piece is one of
;; - Pawn
;; - Rook
;; - Knight
;; - Bishop
;; - Queen
;; - King
(define-struct pawn (loc colour))
;; A Pawn is (make-pawn Posn Colour)
;; INTERP: represents a pawn with its colour and location
(define-struct rook (loc colour))
;; A Rook is (make-rook Posn Colour)
;; INTERP: represents a rook with its colour and location
(define-struct knight (loc colour))
;; A Knight is (make-knight Posn Colour)
;; INTERP: represents a knight with its colour and location
(define-struct bishop (loc colour))
;; A Bishop is (make-bishop Posn Colour)
;; INTERP: represents a bishop with its colour and location
(define-struct queen (loc colour))
;; A Queen is (make-queen Posn Colour)
;; INTERP: represents a queen with its colour and location
(define-struct king (loc colour))
;; A King is (make-king Posn Colour)
;; INTERP: represents a king with its colour and location
In Racket an itemization Data Definition groups together data that served a specific role in our design. The name of the itemization Data Definition was used in our signatures and Data Definitions but did not really have a presence in our code/evaluation.
Interfaces in Java
Java provides Interfaces. A Java Interface is much like a class but
-
There is no constructor! We cannot directly create an instance of an interface.
-
Everything in the interface is
public
by default. -
There are no fields in interfaces.
-
You can only have method signatures in interfaces. [1]
-
A class implements an interface by explicitly declaring in its class header the name of the interface using the special keyword
implements
. A class can implement as many interfaces as it wants. -
Any class that claims to implement an interface must provide code for each signature in the interface.
Think of an interface as a way to provide a view on your objects that shows only the relevant methods that can be used on your objects from this view.
Back to our chess game now, we will capture itemizations in Java as interfaces and thus Piece
is going to be an interface. Each of the itemization elements of the data definition, i.e., Pawn
, Rook
etc. will implement our interface, i.e., Piece
Notice the new style of arrow that we have between Piece
and all of the kinds of pieces. The empty-head arrow in UML denotes a new relantionship between classes. We tend to call this relationship is-a.
Using the class King
we read this arrow as follows
A King
is a Piece
.
We will see this in action shortly. The phrase conveys the fact that we can use Piece
as a compile-time type for a variable and we can assign any object that implements the interface Piece
.
Here is some of the code
/**
* Represents the colour of a pieces in a Chess game.
*/
public interface Colour {
}
/**
* Created by therapon on 5/16/16.
*/
public class White implements Colour {
}
/**
* Created by therapon on 5/16/16.
*/
public class Black implements Colour {
}
/**
* Represents a piece in our chess game.
*
*/
public interface Piece {
}
/**
* Created by therapon on 5/16/16.
*/
public class Pawn implements Piece {
private Posn loc;
private Colour colour;
public Pawn(Posn loc, Colour colour) {
this.colour = colour;
this.loc = loc;
}
/**
* Getter for property 'loc'.
*
* @return Value for property 'loc'.
*/
public Posn getLoc() {
return this.loc;
}
/**
* Getter for property 'colour'.
*
* @return Value for property 'colour'.
*/
public Colour getColour() {
return this.colour;
}
}
/**
* Created by therapon on 5/16/16.
*/
public class Rook implements Piece {
private Posn loc;
private Colour colour;
public Rook(Posn loc, Colour colour) {
this.loc = loc;
this.colour = colour;
}
/**
* Getter for property 'loc'.
*
* @return Value for property 'loc'.
*/
public Posn getLoc() {
return this.loc;
}
/**
* Getter for property 'colour'.
*
* @return Value for property 'colour'.
*/
public Colour getColour() {
return this.colour;
}
}
And here is a test for Pawn
. We will add a test to verify Colour
soon.
import org.junit.Assert;
public class PawnTest {
private Pawn p1;
@org.junit.Before
public void setUp() throws Exception {
this.p1 = new Pawn(new Posn(1,1), new White());
}
@org.junit.Test
public void getLoc() throws Exception {
Assert.assertTrue(samePosn(this.p1.getLoc(), new Posn(1,1)));
}
private Boolean samePosn(Posn loc, Posn posn) {
return loc.getX().equals(posn.getX()) && loc.getY().equals(posn.getY());
}
}
A Java interface is a valid type for a field/variable. The possible values that we can set to a field/variable whose type is the name of an Interface are all the instances of classes that implement that interface.
import org.junit.Before;
import org.junit.Test;
public class PieceTest {
private Piece pawn;
private Piece rook;
private Piece bishop;
private Piece knight;
private Piece queen;
private Piece king;
private Posn p1;
private Colour black;
private Colour white;
@Before
public void setUp() throws Exception{
this.p1 = new Posn(1, 1);
this.black = new Black();
this.white = new White();
this.pawn = new Pawn(this.p1, this.black); (1)
this.rook = new Rook(this.p1, this.white);
this.bishop = new Bishop(this.p1, this.black);
this.knight = new Knight(this.p1, this.white);
this.queen = new Queen(this.p1, this.black);
this.king = new King(this.p1, this.white);
}
@Test
public void testPiece(){
this.pawn.getLoc(); (2)
}
}
1 | Observe that the instance we are creating on the right hand side of the = is a Pawn while the field we are
assigning to this.pawn was given the type Piece . Given that Pawn implements Piece this is a valid assignment. |
2 | This line gives a compile time error |
PieceTest.java:30: error: cannot find symbol this.pawn.getLoc(); ^ symbol: method getLoc() location: variable pawn of type Piece
The compiler complains that there is no getLoc()
method defined for the field pawn
which has the compile-time type Piece
. The reason is that
Piece
as an interface provides no method signatures. Even though we know that we assigned the value new Pawn(this.p1, this.black)
to
the field pawn
and that Pawn
does have a method called getLoc()
the compiler only checks against the compile-time type which in this case is the interface Piece
in order to find a method getLoc()
and not
against the runtime-type Pawn
.
The compiler only considers information that is available at compile time. |
Adding methods on our Piece
Itemization
Recall in Racket our functions took as input a value of our itemization and then using a cond
statement detected which one of the valid values for this itemization we have and
delegated to the appropriate function.
For example if we wanted to have get the location of a Piece
;;;; Signature
;; piece-posn: Piece -> Posn
;;;; Purpose
;; Given a piece returns the piece's location
(define (piece-posn piece)
(cond
[(pawn? piece) (pawn-loc piece)]
[(rook? piece) (rook-loc piece)]
[(knight? piece) (knight-loc piece)]
[(bishop? piece) (bishop-loc piece)]
[(queen? piece) (queen-loc piece)]
[(king? piece) (king-loc piece)]))
With Java since we captured our itemization as an Interface we need to
-
add a signature to the interface that captures our itemization and
-
to each class that maps to an element of the itemization provide an implementation of this signature. The code we add to each class that implements our Interface is the same as the code in our helper functions in Racket.
In order to be use the methods getLoc()
and getColour()
we need to
-
Add the signatures of these methods in
Piece
-
Ensure that all classes that implement
Piece
provide an implementation for each signature inPiece
.-
The signatures have to match exactly!
-
Let’s update Piece
to add the signatures that we want to use for all Piece
instances.
/**
* Represents a piece in our chess game.
*
*/
public interface Piece {
/**
* Get the location of this piece
* @return the current location of this piece
*/
Posn getLoc();
/**
* Get the colour of this piece
* @return this piece's colour
*/
Colour getColour();
}
Another way to think about how the Racket implementation maps to the Java implementation is that
the cond
is done by Java during evaluation, and we need to add the corresponding Java code that maps to the
right hand side of the cond
-clause to the appropriate class.
And here is our updated UML Class Diagram
Now our code compiles!
Java Evaluation
Whiteboard!
Abstracting commonalities using inheritance
Observe that each of the classes that implement Piece
have two fields
-
loc
-
colour
Java provides a mechanism called inheritance that allows classes to inherit properties from their parents.
Inheritance in Java is a relationship between classes. The meaning of inheritance in Java is close to the meaning of the word inheritance in the English in that one class derives characteristics from another.
In Java the characteristics that are derived are
-
public
fields -
public
methods.
private
characteristics (methods/fields) of a class are not inherited.
So we could create a new class that will contains the common fields and methods across all classes that implement Piece
.
The purpose of this class is to contain all the common fields and methods across pieces. It is an abstraction (think of it as an incomplete segment)
that we would like all concrete Piece
implementation to share. As such we do not want to create instances of this incomplete segment.
Java allows us to define classes that are abstract in nature and cannot be instantiated.
/**
* Created by therapon on 5/16/16.
*/
public abstract class AbstractPiece { (1)
protected Posn loc; (2)
protected Colour colour;
public AbstractPiece(Posn loc, Colour colour) { (3)
this.colour = colour;
this.loc = loc;
}
/**
* Getter for property 'loc'.
*
* @return Value for property 'loc'.
*/
public Posn getLoc() {
return this.loc;
}
/**
* Getter for property 'colour'.
*
* @return Value for property 'colour'.
*/
public Colour getColour() {
return this.colour;
}
}
Protected modifier
We can use protected
for fields and/or methods. A field/method
that is marked as protected
designates that
|
1 | An abstract class is declared by using the keyword abstract in the class definition header |
2 | protected is another modifier like public and private . |
3 | Abstract classes can have constructors inside their definition. These constructors cannot be used to create an instance of the abstract class. They are used by subclasses to create the segments of the abstract class that they inherit and then fill in the remaining fields (if there are any). |
By declaring a class abstract
Java will not allow us to create
instance of the class directly, e.g., you cannot use new
on an abstract
class. It is essentially "placeholder" with some common behaviour and/or
some method signatures that we want all subclasses to inherit and/or
implement.
Here is the updated implementation of Queen
/**
* Created by therapon on 5/16/16.
*/
public class Queen extends AbstractPiece implements Piece { (1)
public Queen(Posn loc, Colour colour) {
super(loc,colour); (2)
}
}
1 | To define an inheritance relationship to another class we need to specify our parent class in the class definition header using the keyword extends. |
2 | In the constructor we can use the keyword super to refer to our parent class' constructor. We can also use super to refer to one of our parents methods, e.g., super.getLoc() calls
the method in our parent class which is AbstractPiece . |
In Java a class can only extend one other class and no more! |
And here is our update UML Class Diagram
Given that all the code that implements the signatures found in Piece
are in AbstractPieces
we could move the implements Piece
declaration to AbstractPiece
and all of our
classes that extend AbstractPiece
will still implement Piece
.
/**
* Created by therapon on 5/16/16.
*/
public abstract class AbstractPiece implements Piece { (1)
protected Posn loc;
protected Colour colour;
public AbstractPiece(Posn loc, Colour colour) {
this.colour = colour;
this.loc = loc;
}
/**
* Getter for property 'loc'.
*
* @return Value for property 'loc'.
* */
public Posn getLoc() {
return loc;
}
/**
* Getter for property 'colour'.
*
* @return Value for property 'colour'.
* */
public Colour getColour() {
return colour;
}
}
1 | AbstractPiece now implements Piece and through inheritance so will all the classes that extends AbstractPiece .
Notice that AbstractPiece does not provide implementations for the signatures defined in Piece . We will talk about
abstract classes extensively in our next lecture. |
/**
* Created by therapon on 5/16/16.
*/
public class King extends AbstractPiece {
public King(Posn loc, Colour colour) {
super(loc, colour);
}
}
And our updated UML Class Diagram again
Java Evaluation again
Whiteboard!
Shapes Example
Let’s try another example. We would like to design a program that allows us to manipulate shapes on a canvas.
A Shape, for now, can be one of
-
a circle with a radius and a pin (its center)
-
a square with a pin (the top left corner) and a side
-
a rectangle with a pin (the top left corner) a width and a height
This is an example of an itemization.
We can make some observations about the data for each shape.
-
There are elements of each shape that are common, the pin.
-
There are element of each shape that are uncommon (or special) to each individual shape, e.g., radius for circle
Lets look at the operations that we want for each shape.
-
We would like to move a shape on the canvas in the x-direction
-
We would like to move a shape on the canvas in the y-direction
-
We would like to calculate the area of a shape
-
We would like to calculate the circumference of a shape
In Racket
Recall what we had to do for itemizations in Racket?
;; A Shape is one of
;; - (make-circle Posn PosInt)
;; - (make-square Posn PosInt)
;; - (make-rectangle Posn PosInt PosInt)
;; shape-area: Shape -> Number
(define (shape-area shape)
(cond
[(circle? shape) (circle-area shape)]
[(square? shape) (square-area shape)]
[(rectangle? shape) (rectangle-area shape)]))
;; circle-area: Circle -> Number
(define (circle-area circle)
(* pi (sqr (circle-radius))))
;; square-area: Square -> Number
(define (square-area square)
(sqr (square-side square)))
;; rectangle-area: Rectangle -> Number
(define (rectangle-area rectangle)
(* (rectangle-width rectangle)
(rectangle-height rectangle)))
In Java
We can use inheritance for the common parts of our data so that we define a pin in one location in our code and have all the specific shapes that use a pin inherit that field.
Fields and constructors in the presence of inheritance
The class Circle
has access to radius
but it also has access to pin
. The same argument goes for each subclass.
The Java language provides syntax to refer to you parent. In Java the special keyword super
refers to your parent.
In order to create a new Circle
instance we need to provide a constructor method like before. Now however we need to
create our constructor to accept a value for radius
and a value for pin
.
public abstract class AbstractShape implements Shape {
protected Posn pin;
public AbstractShape(Posn pin) {
this.point = point;
}
}
public class Circle extends AbstractShape { (1)
private Integer radius;
public Circle(Posn pin, Integer radius) {
super(pin);
this.radius = radius;
}
}
1 | The Java keyword extends specifies that Circle inherits from Shape . We say that Circle is a subclass of Shape and Shape is the superclass of Circle . |
In Java you can only have 1 superclass. This property is called single inheritance. A superclass however can have more than one subclasses.
The statement super(pin)
tells Java to call the parent (or superclass)
constructor in Shape
passing pin
as the argument to Shape
's
constructor. This will setup pin
correctly and then we proceed to set
the fields found in Circle
.
In Java calling the superclass constructor must be the first line in the subclass constructor.
Methods in the presence of inheritance
Java applies similar rules to methods when it comes to inheritance and which methods are derived or not.
A subclass inherits all public
and protected
methods that are present in the superclass.
A subclass can
-
Use the inherit method as is and call it on itself.
-
It can override the method in order to provide it’s own implementation. An override however must satisfy the following conditions
-
The method signature must be the exactly the same.
-
Use the
@Override
annotation.
-
If you would like to call the overriden method, Java allows you to do so with super.<method-name>(<args>)
where
-
<method-name>
is the name of the method you want to call in the superclass. -
<args>
are any number of arguments (0 or more) that you want to pass to the superclass' method you are calling.
Inheritance, types and subtype polymorphism
Inheritance defines a relationship between a superclass and a subclass. This relationship affects how the Java compiler and the JVM treat instances of these classes.
Given that a subclass must have all the publicly available methods of its superclass (overridden or not) we can view a subclass as its superclass.
Circle c1 = new Circle(new Point(10,20), 5);
The above line is valid Java code. We create a variable called c1
that can hold objects of type Circle
and
we assigned to c1
the object new Circle(new Point(10,20), 5)
.
Shape s1 = new Circle(new Point(10,20), 5);
AbstractShape s2 = new Circle(new Point(10,20), 5);
The preceding line is also valid Java. We create a new variable called s1
that can hold objects of type Shape
and
we assigned to s1
the object new Circle(new Point(10,20), 5)
. Some terminology here we say
-
the compile time type (or static type) of
s1
isShape
-
the runtime type (or dynamic type) of
s1
isCircle
. -
the compile time type (or static type) of
s2
isAbstractShape
-
the runtime type (or dynamic type) of
s2
isCircle
.
Conceptually you can think of this as, given that a Circle
has the same methods and fields as a Shape
(well maybe a bit more) then it is perfectly valid to use it as a Shape
if I want to.
The Java compiler will treat s1
as a Shape
in your code and will not allow you to call a method on s1
that is not found in the Shape
interface or one of its superclasses/superinterfaces. Superclasses here refers to the parent, grandparent, grand-grandparent etc.
The Java compiler will treat c1
as a Circle
in your code and will not allow you to call a method on c1
that is not found in the Circle
class or one of its superclasses. Superclasses here refers to the parent, grandparent, grand-grandparent etc.
The JVM however has the following evaluation rule :
-
when looking up a method on an instance
o
that has runtime typeT
-
look at the class
T
and see if the method is defined in classT
, override or not. If the method is defined inT
run it ono
-
else if the method is not defined in
T
, grabT
's immediate superclass (lets called thatS1
), if the method is defined inS1
, override or not, run it ono
-
else if the method is not defined is
S1
, grabS1
's immediate superclass (lets called thatS2
), if … -
we terminate once we reach the class
Object
. IfObject
does not have the method then we throw an error. This error should never happen in a Java program that was build using the Java compiler.
-
The ability of one instance to be viewed/used as different types is called polymorphism — the ability to takes many "shapes"/"forms"/"views".
There are 3 kinds of polymorphism that we will explore in this class,
|
In Java the cond
is taken care of by the language due to the way we designed our classes.
What we have to write is the helper functions of for shape-area
and we have to write them
inside the appropriate class.
public class Circle extends AbstractShape{
// elided code
public Double area() {
return Math.PI * this.radius * this.radius;
}
}
public class Square extends AbstractShape{
// elided code
public Double area() {
return this.side * this.side;
}
}
public class Rectangle extends AbstractShape{
// elided code
public Double area() {
return this.width * this.height;
}
}
public class ShapeTest {
private Shape c1;
private Shape s1;
private Shape r1;
@Before
public void setUp() throws Exception {
this.c1 = new Circle(new Point(10,20), 5);
this.s1 = new Square(new Point(100,100), 20);
this.r1 = new Rectangle(new Point(300,300), 30, 10);
}
@Test
public void testArea() {
Assert.assertEqual(this.c1.area(), new Double(78.53981633974483));
Assert.assertEqual(this.s1.area(), new Double(400));
Assert.assertEqual(this.r1.area(), new Double(300));
}
}
Equality in Java
Java provides two mechanisms for checking equality between values.
-
==
, the double equals is used to check-
equality between primitive types
-
"memory equality", check whether the two references point to the same object in memory
-
-
equals()
is a method defined in the classObject
that is inherited to all classes. The JVM expects developers to override theequals()
method in order to define the notion of equality between objects of classes that they define.
Overriding the equals()
method however imposes extra restrictions. These restrictions are
spelled out in the equals()
method documentation.
-
reflexive - for a non-null reference value
x
,x.equals(x)
returnstrue
-
symmetric - for non-null reference values
x
andy
,x.equals(y)
returnstrue
if and only ify.equals(x)
returnstrue
-
transitive - for non-null reference values
x
,y
, andz
,-
if
x.equals(y)
returnstrue
and -
y.equals(z)
returnstrue
, then -
x.equals(z)
must returntrue
-
-
consistent - for non-null references
x
,y
, multiple invocations ofx.equals(y)
should return the same result provided the data insidex
andy
has not been altered. [2] -
for any non-null reference value
x
x.equals(null)
returnsfalse
-
the code needs to override
hashCode
as well in order to uphold thehashCode()
method’s contract. The contract forhashCode()
is spelled out inhashCode()
's documentation.
Here are the conditions for the hashCode()
method’s contract
-
for a non-null reference value
x
multiple invocations ofx.hashCode()
must return the same value provided the data insidex
has not been altered. [2] -
for any two non-null reference values
x
,y
-
if
x.equals(y)
returnstrue
then -
x.hashCode()
andy.hashCode()
must return the same result
-
-
for any two non-null reference values
x
,y
-
if
x.equals(y)
returnsfalse
then -
it is prefered but not required that
x.hashCode()
andy.hashCode()
return different/distinct results.
-
Let’s look at an example using our Posn
class.
/**
* Represents a Cartesian coordinate.
*
*/
public class Posn {
private Integer x;
private Integer y;
public Posn(Integer x, Integer y) {
this.x = x;
this.y = y;
}
/**
* Getter for property 'x'.
*
* @return Value for property 'x'.
*/
public Integer getX() {
return this.x;
}
/**
* Getter for property 'y'.
*
* @return Value for property 'y'.
*/
public Integer getY() {
return this.y;
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object o) { (1)
if (this == o) return true; (2)
if (o == null || getClass() != o.getClass()) return false; (3)
Posn posn = (Posn) o; (4)
if (this.x != null ? !this.x.equals(posn.x) : posn.x != null) return false; (5)
return this.y != null ? this.y.equals(posn.y) : posn.y == null; (6)
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
int result = this.x != null ? this.x.hashCode() : 0; (7)
result = 31 * result + (this.y != null ? this.y.hashCode() : 0); (8)
return result;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return "Posn{" +
"x=" + x +
", y=" + y +
'}';
}
}
The strategy used to check for equality is recursive; we check each field in turn and since a field can be a reference type, we need to call its equals()
method. There are many different implementations of equals()
, we will go over this specific one here so that we have one example of how we could write an equals()
method.
The strategy used for the implementation of hashCode()
is also recursive; we use each field and get its hash code, we then compose all field hash codes together along with a prime number to get this
object’s hash code.
1 | Observe that the compile-time type for the argument o is Object not Posn . |
2 | We first check whether the argument passed is in fact the same exact object in memory as this object using == . |
3 | We then check that if the argument o is null or
the runtime-type of o [3] is not the same as the runtime-type of this then the two values cannot be equal. |
4 | If the runtime-types are the same then we cast--force the compile-time type of a variable to change to a new compile-time type-- o to be a Posn and give it a new name posn . |
5 | Now we check each field for equality, first x . |
6 | and then y . The ?: is the Java if-expression; an if statement that returns a value. The test is the code before ? , if that expression returns true then we evaluate the expression found between ? and : . If the test expression returns false then we evaluate the expression found after the : . |
7 | If a field, x in this case, is null then return 0 else grab its hash code by calling hashCode() on that object. |
8 | repeat to get the hash code for y , add the field hash code’s together and multiply by 31 . [4] |