Lab: Experimenting with Abstract Classes
Instructions:
Answer the following questions one at a time. After answering each question,
check your answer (by clicking on the check-mark icon if it is available)
before proceeding to the next question.
Getting Ready:
Before going any further, you should:
-
Depending on your development environment, create either a
directory or a project for this lab.
-
Setup your development environment.
-
Download the following files:
to an appropriate directory/folder. (In most browsers/OSs, the
easiest way to do this is by right-clicking/control-clicking on
each of the links above and then selecting Save as...
or Save link as....)
-
Add the appropriate files you downloaded to the directory/project
you created for this lab.
.java
Files: In most IDEs, .java
files
(i.e., source files) can just be copied into the project.
.class
and .jar
Files:
In some IDEs it is easier to use .class
files and in others it is easier to use a .jar
file
that contains the .class
files. Hence, you have been
provided with both.
See the course "Help" page on your IDE for more information.
Resources: In some IDEs, resources (e.g., images, data
files) need to be in a special directory whereas in others they
need to be in the working directory.
See the course "Help" page on your IDE for more information.
1. Basics:
This part of the lab will help you understand the basics of
abstract classes.
-
Open
TwoPartNumber.java
.
-
Compile
TwoPartNumber.java
to ensure that it has no
syntax errors.
-
Remove the
abstract
modifier from the declaration of
the TwoPartNumber
class.
-
Compile the
TwoPartNumber
class.
-
What error message was generated?
TwoPartNumber.java:16: error: TwoPartNumber is not abstract and does not overr
ide abstract method initializeUnits() in TwoPartNumber
public class TwoPartNumber
^
1 error
-
Why was this error message generated?
This error message was generated because the TwoPartNumber
class has a method in it that is not implemented but the class is not
declared to be abstract.
-
What problems could arise at run-time if this class could be compiled
as is?
Someone could create an instance of a TwoPartNumber
object
and then call it's initializeUnits()
method. But, there
would be no instructions to execute because the method is not implemented.
-
Replace the abstract modifier in the declaration of the class.
-
Remove the
abstract
modifier from the declaration of
the initializeUnits
method.
-
Compile the
TwoPartNumber
class.
-
What error message was generated?
TwoPartNumber.java:132: missing method body, or declare abstract
protected void initializeUnits();
-
Why was this error message generated?
This error message was generated because the initializeUnits()
method is declared but not implemented.
-
Replace the
abstract
modifier in the declaration of the
initializeUnits()
method.
-
Compile the
TwoPartNumber
class.
-
Why does this class compile even though it has a method with a
missing method body?
Abstract classes can contain abstract methods. The compiler only needs
to know that concrete children will implement a method with the
same signature.
-
"Comment out" the
initializeUnits
method.
-
Compile the
TwoPartNumber
class.
-
What error message was generated?
TwoPartNumber.java:67: cannot resolve symbol
symbol : method initializeUnits ()
-
Why was this error message generated?
This error message was generated because the constructor with the
signature TwoPartNumber(int, int, boolean)
calls
the initializeUnits()
method and there is no such
method.
-
"Un-comment out" the
initializeUnits
method.
-
Compile the
TwoPartNumber
class.
-
Why does this class compile even though the constructor
calls
initializeUnits()
and the initializeUnits()
method is not implemented?
The compiler knows that this method will be implemented in
all concrete derived classes. Hence, at run-time (when the
constructor that calls initializeUnits()
is executed),
there will be instructions to execute (in the derived class).
-
Why is there no chance that the code in the constructor that
calls
initializeUnits()
will be executed if there are
no concrete derived classes?
Because the TwoPartNumber
class is abstract
no instances can be created. If no instances can be created then
the code in the constructor can't be executed.
-
Open
Driver0.java
.
-
Compile
Driver0.java
.
-
What error message was generated?
Driver0.java:8: TwoPartNumber is abstract; cannot be instantiated
number = new TwoPartNumber();
-
Why was this error message generated?
This error message was generated because you cannot create an
instance of an abstract class.
-
Why doesn't the declaration of the
number
variable
generate a compile-time error message?
You can declare a variable to have an abstract type. It can later
hold a reference to an object of a derived type. (Remember the
"is a" relationship.)
2. A Brief Review:
This part of the lab will help you remember some topics that were discussed
earlier in the semester.
-
Why aren't the "constants" (e.g.,
smallsPerLarge
) in the
TwoPartNumber
class declared to be final
?
Because they can't be initialized when declared or in the constructor
(since they are initialized in the initializeUnits()
method of the concrete subclasses).
-
Does this make defects more likely?
It does. Programmers implementing the concrete subclasses need to be
very careful.
-
Why aren't the "constants" (e.g.,
smallsPerLarge
) in the
TwoPartNumber
class declared to be static
?
Because they are not attributes of the class. Different objects must
be able to have different "constants" because they may be members of
different concrete subclasses.
-
Does this make defects more likely?
No.
3. Specializing an Abstract Class:
This part of the lab will help you understand extensions/specializations
of abstract classes.
-
Open
Length.java
.
-
Is this class "concrete"? Why or why not?
It is declared to be concrete (or, more correctly, it is not
declared to be abstract). The declaration is correct because
the Length
class implements the abstract method
initializeUnits()
.
-
Open
Driver1.java
.
-
Compile and execute
Driver1
.
-
What output was generated?
Age 12:
Mike: 5 feet 5 inches
Bill: 5 feet 5 inches
== SAME ==
Age 16:
Mike: 5 feet 5 inches
Bill: 5 feet 9 inches
== DIFFERENT ==
Age 20:
Mike: 6 feet 6 inches
Bill: 6 feet 7 inches
== DIFFERENT ==
Age 70:
Mike: 6 feet 6 inches
Bill: 6 feet 6 inches
== SAME ==
-
Is this output correct?
Yes.
-
"Comment out" the implementation of the
initializeUnits()
in the Length
class.
-
Compile the
Length
class.
-
What error message was generated?
Length.java:10: Length is not abstract and does not override abstract method
initializeUnits() in TwoPartNumber
public class Length extends TwoPartNumber
-
Why was this error message generated?
This error message was generated because the class declares itself
to be a concrete extensions of the TwoPartNumber
class but does not implement the abstract
method initializeUnits()
in that class.
-
"Un-comment out" the implementation of the
initializeUnits()
in the Length
class.
-
Create a
Weight
class (containing pounds and ounces)
that extends the TwoPartNumber
class.
-
What code did you have in your implementation?
/**
* A weight in traditional U.S. units (i.e., pounds and ounces)
*
* This version extends the abstract TwoPartNumber class that
* implements most behaviors
*/
public class Weight extends TwoPartNumber
{
/**
* Default Constructor.
*/
public Weight()
{
super(0, 0);
}
/**
* Explicit Value Constructor.
*
* @param pounds The number of pounds (must be positive)
* @param ounces The number of ounces (must be positive)
*/
public Weight(int pounds, int ounces)
{
super(pounds, ounces);
}
/**
* Initialize the "constants" used by a TwoPartNumber.
*/
protected void initializeUnits()
{
smallsPerLarge = 16;
largeUnitsSingular = "pound";
largeUnitsPlural = "pounds";
smallUnitsSingular = "ounce";
smallUnitsPlural = "ounces";
}
}
-
What is the advantage of having the
Length
and Weight
classes extend
the TwoPartNumber
class?
It eliminates an enormous amount of code duplication.
If these classes did not extend TwoPartNumber
they would
both need to include all of the code in the TwoPartNumber
class, some of which is fairly complicated.
-
Suppose that, while testing the
Weight
class, you
found a fault in the changeBy()
method. Assuming the
current implementation, would you have to correct the fault in
both the Weight
class and the Length
class?
No, the fault is actually in the TwoPartNumber
class.
So, the correction would be made there and inherited by both the
Weight
and Length
classes.
-
Suppose, instead, that, after copying the
Weight
class to create a Length
, you found a fault in
the changeBy()
method. Would you have to correct the
fault in both the Weight
class and
the Length
class?
Yes, that's one of the big problems with having duplicate code (whether in
different methods or in different classes).
4. Type Safety and Abstract Classes:
This part of the lab will help you understand "type safety"
issues that sometimes arise with specializations of abstract classes.
-
Open
Driver2.java
.
-
Compile the
Driver2
class.
-
Why doesn't the expression
myLength.changeBy(myWeight)
generate a compile-time
error message?
The formal parameter of the changeBy()
method is a
TwoPartNumber
. Since Weight
extends
TwoPartNumber
the variable myWeight
is a TwoPartNumber
.
-
Execute
Driver2
.
-
What output is generated?
2 feet 1 inch plus 1 pound 9 ounces equals 4 feet 2 inches
-
Why?
The changeBy()
method operates
on TwoPartNumber
objects using "small"
units. myLength
has a value
attribute
of 25 and myWeight
has a value
attribute
of 25. So, after changeBy()
is executed,
the value
attribute of myWeight
is 50
which the toString()
method reports as 4 feet 2
inches.
-
We can, fortunately, easily correct this defect. The first step is
to make the
changeBy()
method in
the TwoPartNumber
class visible/accessible only to
derived classes that understand this issue. What change did you
make?
I made the changeBy()
method in the TwoPartNumber
class protected.
-
The second step is to add a type-safe
changeBy()
method to the Length
class. Of course, it shouldn't
duplicate code in the base class. What code did you add?
/**
* Change this Length by a given amount.
*
* @param other The amount to change this Length by
*/
public void changeBy(Length other)
{
super.changeBy(other);
}
-
The final step is to add a similar type-safe
changeBy()
method to the Weight
. What code did you add?
/**
* Change this Weight by a given amount.
*
* @param other The amount to change this Weight by
*/
public void changeBy(Weight other)
{
super.changeBy(other);
}
-
Now, re-compile
Driver2.java
. Why isn't an error generated?
This is subtle, and an artifact of the simple setup we used for
this lab.
Because Driver2
is in the same package
as TwoPartNumber
, the
protected changeBy()
method is visible/accessible to
it. This isn't a problem, though, because in a more realistic
setting they would be in different packages.
5. Collections of Abstract (and Concrete) Classes:
This part of the lab will help you understand some of the ways in
which collections of abstract classes and their concrete children
can be used, and the advantages and disadvantages of each.
-
Open
LengthDatabase.java
and
DatabaseDriver1.java
.
-
Compile and execute
DatabaseDriver1
.
-
Did it execute correctly?
Yes.
-
Create a file named
DatabaseDriver2.java
that creates
and stores Weight
objects
(rather than Length
objects) in the
LengthDatabase
.
-
What code did you have in your implementation?
import java.io.*;
import java.util.*;
/**
* A driver.
*/
public class DatabaseDriver2
{
/**
* The entry point of the application
*
* @param args The command line arguments
*/
public static void main(String[] args) throws IOException
{
Weight weight, result;
LengthDatabase database;
Scanner scanner;
String name, prompt;
prompt = "\nName to look for (Ctrl-Z or Ctrl-D to exit): ";
database = new LengthDatabase();
weight = new Weight(155, 8);
database.add("Mike", weight);
weight = new Weight(186, 2);
database.add("Bill", weight);
weight = new Weight(114, 9);
database.add("Nancy", weight);
scanner = new Scanner(System.in);
System.out.print(prompt);
while (scanner.hasNext())
{
name = scanner.nextLine();
result = database.get(name);
if (result != null)
{
System.out.println(name+" is "+result);
}
else
{
System.out.println("I don't know how heavy "+name+" is.");
}
System.out.print(prompt);
}
}
}
-
Compile your implementation of
DatabaseDriver2
.
-
Why didn't it compile?
Because the second formal parameter of the add()
method in the LengthDatabase
class is
a Length
and because the get()
method
in the LengthDatabase
class returns a Length
.
-
Open and read
TwoPartNumberDatabase.java
.
-
What are the differences between the
TwoPartNumberDatabase
class and the LengthDatabase
class?
In the TwoPartNumberDatabase
class values
is a TwoPartNumber[]
(rather than a Length[]
)),
the add()
method is passed a TwoPartNumber
(rather than a Length
), and the get()
method
returns a TwoPartNumber
(rather than a Length
).
-
What are the advantages of the
TwoPartNumberDatabase
class
(as compared to the LengthDatabase
class)?
Since TwoPartNumber
objects are less specialized than
Length
objects, the TwoPartNumberDatabase
is less specialized than the LengthDatabase
.
As a result, a TwoPartNumberDatabase
object can
be used with any kind of TwoPartNumber
object (e.g.,
Length
objects or Weight
objects).
-
What are the disadvantages of the
TwoPartNumberDatabase
class
(as compared to the LengthDatabase
class)?
Since the TwoPartNumberDatabase
class contains
TwoPartNumber
objects it could contain multiple
different specializations of TwoPartNumber
at the same time, which could lead to confusion/problems.
-
Modify
DatabaseDriver1
and
DatabaseDriver2
so that the database they
use is a TwoPartNumberDatabase
. Make sure you change
both the declaration and the instantiation.
-
Compile
DatabaseDriver1
.
-
What error message was generated?
DatabaseDriver1.java:44: incompatible types
found : TwoPartNumber
required: Length
result = database.get(name);
-
Why was this error message generated?
Because result
is declared to be a Length
but the get()
method returns (a reference to) a
TwoPartNumber
object.
-
Correct this defect (and the corresponding defect in
DatabaseDriver2
by typecasting the value returned by
database.get(name)
to the appropriate type.
-
What changes did you make?
result = (Length)database.get(name);
and
result = (Weight)database.get(name);
-
Remove the two typecasts that you just added.
-
Correct this defect (and the corresponding defect in
DatabaseDriver2
by declaring result
and length
to be TwoPartNumber
objects.
-
What changes did you make?
TwoPartNumber length, result;
and
TwoPartNumber weight, result;
-
What are the advantages and disadvantages of these two approaches
to correcting this defect?
The advantage of the first approach is that all of the
functionality of the returned object can be used, not just the
functionality that is in the
TwoPartNumber
class. So, if either
Length
or
Weight
added important functionality to the the
TwoPartNumber
class, that functionality could be used.
The disadvantage is that the typecast could cause a run-time exception
if, somehow, the wrong kind of
TwoPartNumber
object is
added to the database.
There is no risk of a run-time exception in the second approach.
The disadvantage is that only the functionality of
the TwoPartNumber
class can be used (since any
attempt to use other functionality will result in a compile-time
error message being generated).
There is actually a better way to correct this defect that we will
discuss later in the semester.
-
Add the following method to the
LengthDatabase
class
(with an appropriate comment).
public Length total()
{
Length result;
result = new Length();
for (int i=0; i<nextIndex; i++)
{
result.changeBy(values[i]);
}
return result;
}
-
What comment did you add? In other words, what functionality does
this method provide?
/**
* Calculate the total Length of all objects in this LengthDatabase.
*
* @return The total
*/
-
Try to add a similar method to the
TwoPartNumberDatabase
class. What makes it impossible?
The TwoPartNumber
class is abstract, so it is not possible
to construct an instance.
-
How might we take advantage of the functionality in
the
TwoPartNumber
database class and add type safety?
One way is to make the TwoPartNumberDatabase
class abstract
and have concrete specializations that do nothing but provide
type safety.
-
Add the following method to the
TwoPartNumberDatabase
class.
/**
* Get the total TwoPartNumber of all objects in this
* TwoPartNumberDatabase.
*
* @return The total
*/
protected TwoPartNumber totalValue()
{
TwoPartNumber result;
result = createElement();
for (int i=0; i<nextIndex; i++)
{
result.changeBy(values[i]);
}
return result;
}
-
Why doesn't the class compile?
It doesn't compile because there is no createElement()
method.
-
What is the apparent purpose of the
createElement()
method?
It looks like it is supposed to construct a TwoPartNumber
object.
-
Why is it going to be impossible to implement this method?
Again, because the TwoPartNumber
class is abstract.
-
Are there any objects that specialize
TwoPartNumber
that can be constructed?
Yes, Length
"is a" TwoPartNumber
and Weight
is a TwoPartNumber
.
-
Why shouldn't the
createElement()
method
in the TwoPartNumber
class construct
an object that it is a specialization of TwoPartNumber
?
It doesn't/shouldn't know what specializations exist. In other words,
in the future, there might be hundreds of classes that specialize
TwoPartNumber
.
-
The
TwoPartNumber
class can't implement
the createElement()
method but it's specializations
can. So, what should be true of the createElement()
method in the TwoPartNumber
class.
It should be abstract.
-
Add such a method. What code did you add?
/**
* Create a default (specialization of) a TwoPartNumber.
*
* @return The TwoPartNumber
*/
public abstract TwoPartNumber createElement();
-
Why doesn't the class compile?
It doesn't compile because it has an abstract method (and, hence,
objects of this class can't be instantiated).
-
Make the
TwoPartNumberDatabase
class abstract.
What code did you add?
public abstract class TwoPartNumberDatabase
-
Create a
WeightDatabase
class (just to be different)
that extends the TwoPartNumberDatabase
class and
provides all of the desired functionality (including
the public Weight total()
method) in a type-safe way.
What code is in this class?
/**
* A simple database that contains weights indexed by a name.
*/
public class WeightDatabase extends TwoPartNumberDatabase
{
/**
* Default Constructor.
*/
public WeightDatabase()
{
super();
}
/**
* Add a Weight to the database. The Weight
* can be referred to (i.e., indexed by) the given name
*
* @param name The name of the Weight
* @param value The Weight
*/
public void add(String name, Weight value)
{
super.add(name, value);
}
/**
* Create a default (specialization of) a TwoPartNumber.
*
* @return The TwoPartNumber
*/
public TwoPartNumber createElement()
{
return new Weight();
}
/**
* Get the Weight with the given name (or null if
* no such name exists).
*
* @param name The name of the Weight
* @return The Weight (or null)
*/
public Weight get(String name)
{
return (Weight)super.get(name);
}
/**
* Get the total TwoPartNumber of all objects in this
* TwoPartNumberDatabase.
*
* @return The total
*/
public Weight total()
{
return (Weight)super.totalValue();
}
}
-
How much work would it be to create a similar class for other
specializations of
TwoPartNumber
?
Very little but, as we will see later in the semester, there is an even
better way.