Lab 5a
Goals:
The goal of this lab is to learn how to define equality of the values of two objects that are instances of a union of classes.
Note: This material is covered in pages 321 - 330 in the textbook. Read it carefully.
1 Getting started
For this part of the lab we will work with classes that define bank accounts: a checking account or a savings account. Download the files Banking.zip that represent this class hierarchy and start a new project. Run the project and read through the examples to make sure what is involved in defining each type of account. Make two more examples of each type of account and add the needed tests for the method amtAvailable in these classes.
2 Defining equality
Our object is to define a method that will determine whether a given Account is the same as this account. We may need such a method to find a desired account within some class that represents the bank and all accounts in it.
Of course, now that we have the abstract class it would be easy to compare just account number and the name on the account, but we want to make sure that all the customer’s data matches what we have on file exactly, including balances, interest rates, etc.
Implementing the same method
We will design the method same similar to the techonque described in the lecture, called equality by safe casting. The relevant examples can be found in the lecture notes. You may want to look at the code there as you work through this problem.
Begin by designing the method same for the Account class.
Make examples that compare all kinds of accounts: of the same kind (e.g., Checking vs. Checking) and of different kinds (e.g., Savings vs. Checking). For the accounts of the same kind you should test both true and false cases. Comparing different kinds of accounts should always produce false.
Now that you have sufficient examples, follow with the design of the same method in one of the concrete account classes (for example the Checking class). Write the template and think of what data and methods are available.
As in lecture, you need two different helper methods: one that determines whether the given account is a Checking or a Savings account, and one that converts this account into the desired type. Design the methods isChecking and isSavings, that determine whether this account is a checking or a savings account.
Design the methods toChecking and toSavings that convert this account into a checking/savings account, respectively. Header and purpose for the checking account case:
// Convert this Account into a Checking
abstract Checking toChecking();
In the Checking class the body will be just
// Produce a checking account from this account
Checking toChecking(){
return this;
}
while in the other class the method should throw a RuntimeException with the appropriate message.
Now we can define the body of the same method in the Checking class:
// Is the given Account the same as this?
boolean same(Account that){
if(that.isChecking()){
return this.sameChecking(that.toChecking());
}else{
return false;
}
}
We still need the method sameChecking but this only needs to be defined within Checking, and can be defined with private (or protected) visibility.
Complete the design of the same methods (including sameChecking and sameSavings) for the Savings account class.
3 Alternative approaches: bad and good
3.1 Bad Option - Incorrect alternative:
The method above can be incorrectly written with two features of the Java language: the instanceof operator and casting. In the Checking class this style method would look like the following:
// Is the given Account the same as this Checking? |
boolean same(Account that){ |
if(that instanceof Checking){ |
return this.sameChecking((Checking)that); |
}else{ |
return false; |
} |
} |
However, this version introduces bugs!
The issue is that any class that later extends Checking, say a PremiumChecking class, will also be considered a Checking instance by the instanceof operator.
If we implement a similar same method in PremiumChecking:
// Is the given Account the same as this PremiumChecking? |
boolean same(Account that){ |
if(that instanceof PremiumChecking){ |
return this.samePremChecking((PremiumChecking)that); |
}else{ |
return false; |
} |
} |
Now only a PremiumChecking object can be the same as the given instance of the PremiumChecking class.
But and instance of the Checking class can be the same as the given instance of the PremiumChecking class!!
These kinds of bugs can cause serious problems. This issue is also illustrated in the example file BadSame.java. Add the file to your project and run the exampleto see where the program fails to provide correct answers.
3.2 Good Option - A Correct alternative:
In the lecture we introduced another version that also works. It requires us to add a new method to the abstract class for each subclass of Account.
For our AShape hierarchy (the classes that extend the abstract class AShape from the earlier lectures, the same method implementation in each class would be similar to the one for the Circle class:
// Is this circle the same as the given shape? |
public boolean same(AShape that){ |
return that.sameCircle(this); |
} |
and we would then need to define in the abstract class one such method for every class that extends it (just returning false, and override the appropriate method in the class that extends AShape:
// in the class AShape: |
|
// Is that Circle the same as this Shape? |
public boolean sameCirc(Circ that){ return false; } |
|
// Is that Rect the same as this Shape? |
public boolean sameRect(Rect that){ return false; } |
|
// Is that Combo the same as this Shape? |
public boolean sameCombo(Combo that){ return false; } |
|
|
// in the class Circle: |
|
// Is that Circle the same as this Shape? |
public boolean sameCirc(Circ that){ |
... proper implementation that compares two Circle objects ... |
} |
This technique is popular in object-oriented languages like Java and is known as double dispatch.