On this page:
1 Abstract Classes
2 Understanding Constructors
2.1 Overloading Constructors:   Assuring Data Integrity.
2.2 Overloading Constructors:   Providing Defaults.
2.3 Overloading Constructors:   Expanding Options.
3 Understanding Constructors, Redux
6.8

Lab 4: Working with Abstract Classes

Goals: The goals of this lab are to learn how to design and use abstract classes, and to design customized constructors that both provide more convenient programming interfaces and also assure the integrity of the data that the instances of the class represent.

1 Abstract Classes

The following class diagram represents a library system that records the books that have been borrowed. There are three kinds of books: regular books, reference books, and audio books.

Reference books can be taken out for just two days, while other kinds of books may be borrowed for two weeks. The overdue fees are 10 cents per day for reference books and regular books, and 20 cents per day for audio books.

Audio books and regular books have both authors and titles; reference books only have titles.

2 The day when the book is taken out and the day due are counted as days since the library opened on New Year’s Day in 2001. So, for example, an audio book taken out recently would be recorded as taken out on the day 6371 with due date on the day 6385.

               +-------+
               | IBook |
               +-------+
                  / \
                  ---
                   |
       ---------------------------------------
       |                  |                  |
+---------------+  +---------------+  +---------------+
| Book          |  | RefBook       |  | AudioBook     |
+---------------+  +---------------+  +---------------+
| String title  |  | String title  |  | String title  |
| String author |  | int dayTaken  |  | String author |
| int dayTaken  |  +---------------+  | int dayTaken  |
+---------------+                     +---------------+

For all methods, think carefully whether they should be designed being implemented solely in the abstract class, implemented solely in the concrete classes, or implemented in the abstract class and then overridden in some of the concrete classes.

2 Understanding Constructors

This problem reviews and elaborates on the Date example from class. Start by designing a simple Date class with three integer fields representing the month, day and year of the date. Create an ExamplesDate class, and create at least three examples of dates. Create a run configuration for your project and make sure that your data examples compile.

2.1 Overloading Constructors: Assuring Data Integrity.

Java data definitions let us capture the types of our data, and so help catch some kinds of errors statically. But sometimes the types are not enough to capture the full meaning of data and the restrictions on what values can be used to initialize different fields. For example, representing calendar dates with three integers for the day, month, and year ensures that we can’t mistakenly use textual date descriptions. But we know that the month must be between 1 and 12, and the day must be between 1 and 31 (though there are additional restrictions on the day, depending on the month and whether we are in a leap year). We might also consider restricting the year to the range 1500—2100, to describe (semi-)recent history.

Suppose we make the following Date examples:

// Good dates Date d20100228 = new Date(2010, 2, 28); // Feb 28, 2010 Date d20091012 = new Date(2009, 10, 12); // Oct 12, 2009  
// Bad date Date dn303323 = new Date(-30, 33, 23);

Of course, the third example is just nonsense. While complete validation of dates (months, leap-years, etc...) is a study topic in itself, for the purposes of practicing constructors, we will simply make sure that the month is between 1 and 12, the day is between 1 and 31, and the year is between 1500 and 2100.

Did you notice the repetition in the description of validity? It suggests we start with a few helper methods (an early abstraction):

Do this quickly - do not spend much time on it - maybe do just the method validDay and leave the rest for later - for now just returning true regardless of the input. (Such temporary method bodies are called stubs, their goal is to make the rest of program design possible.)

Now change the Date constructor to the following:

Date(int year, int month, int day){
if (this.validYear(year)) {
this.year = year;
}
else {
throw new IllegalArgumentException("Invalid year: " + Integer.toString(year));
}
 
if (this.validMonth(month)) {
this.month = month;
}
else {
throw new IllegalArgumentException("Invalid month: " + Integer.toString(month));
}
 
if (this.validDay(day)) {
this.day = day;
}
else {
throw new IllegalArgumentException("Invalid day: " + Integer.toString(day));
}
}

What kinds or errors can happen at runtime? An illegal argument error is-a runtime error; a null pointer exception is-a runtime error, ... so IllegalArgumentException, NullPointerException and many others are subclasses of RuntimeException.

To signal an error or some other exceptional runtime condition, we throw an instance of the RuntimeException class. Actually, we can be more specific than merely "Something went wrong at runtime", and use an instance of the IllegalArgumentException, which is a subclass of the RuntimeException class used to indicate "An invalid argument was passed to a method or constructor".

If the program ever executes a statement like:

throw new ...Exception("... message ...");

Java stops the program and signals the error through the constructed instance of the Exception. For our purposes, this will simply terminate the program and print the given error message.

The tester library provides methods to test constructors that should throw exceptions:

boolean t.checkConstructorException(Exception e,
String className,
... constr args ...);

For example, the following test case verifies that our constructor throws the correct exception with the expected message, if the supplied year is 53000:

Don’t worry for now about how providing the name of a class is sufficient to allow the tester library to construct your class. The tester library uses many advanced features of Java to make it easier to write tests; we’ll encounter and explain some of them later in the course.

Another of those advanced features is visible here too: notice that the tester library can test for the presence of an error, and continue running subsequent tests. Other than the tester library, no code that we write in this course can do this.

t.checkConstructorException(
// the expected exception new IllegalArgumentException("Invalid year: 53000"),
 
// the *name* of the class (as a String) whose constructor we invoke "Date",
 
// the arguments for the constructor 53000, 12, 30);

Run your program with this test. Now change the test by providing an incorrect message, incorrect exception (e.g. NoSuchElementException), or by supplying arguments that do not cause an error, and see that the test(s) fail.

Java provides the class RuntimeException with a number of subclasses that can be used to signal different types of dynamic errors. Later we will learn how to handle errors and design new subclasses of the class RuntimeException to signal errors specific to our programs.

Do Now!

Revise the constructor above to remove the remaining duplication: Revise validNumber to return the given number if it lies in the range, rather than true, and throw a given error message if it doesn’t, rather than return false. Revise the constructor to use this helper three times. Confirm that your tests still pass. Do you still need validDay, validYear or validMonth?

2.2 Overloading Constructors: Providing Defaults.

When entering dates for the current year it is tedious to continually enter 2017. We can provide an additional constructor that only requires the month and day, assuming the year should be 2017. Note that this constructor too should ensure that the month and day are valid; just because we’re specifying a good year doesn’t mean the other fields are correct! How might we do this, without duplicating code? After all, we’ve already written a constructor that handles the error checking; we shouldn’t have to write that code again.

In lecture, we’ve seen how to invoke constructors on a superclass when one class inherits from another. But our Date class isn’t inheriting from anything; we want to reuse another constructor we’ve already defined on this class. We can use the following syntax to achieve this:

Date(int month, int day){
this(2017, month, day);
}

This syntax is analogous to using super(...) to invoke a constructor on the superclass, but instead means “invoke another (overloaded) constructor on this class”.

Add examples that use only the month and day to see that the constructor works properly. Include tests with invalid month, day or year values as well.

2.3 Overloading Constructors: Expanding Options.

The user may want to enter the date in the form: Jan 20 2013. To make this possible, we can add another constructor:

Date(String month, int day, int year){
...
}

Our first task is to convert a String that represents a month into a number. We can do it in a helper method getMonthNo:

// Convert a three letter month into the numeric value int getMonthNo(String month){
if(month.equals("Jan")){ return 1; }
else if (month.equals("Feb")){ return 2; }
else if (month.equals("Mar")){ return 3; }
else if (month.equals("Apr")){ return 4; }
...
else {
throw new IllegalArgumentException("Invalid month");
}
}

(There may be more efficient ways to provide the list of valid names for the months; for now we are just focusing on the fact that this is possible.)

Ideally, we would like to use this method as follows:

Date(String month, int day, int year){
// Invoke the primary constructor, with a valid month this(year, this.getMonthNo(month), day);
}

But Java will not permit us to invoke a method on our object while still in the middle of invoking another constructor on our object. (Which happens first, evaluating the call to getMonthNo or the call to the other constructor?) So instead, we have to temporarily provide dummy data and immediately fix it:

Date(String month, int day, int year){
// Invoke the primary constructor, with a valid (but made-up) month this(year, 1, day);
 
// Re-initialize the month to the given one this.month = this.getMonthNo(month);
}

Complete the implementation, and check that it works correctly.

3 Understanding Constructors, Redux

Repeat the process of the previous section (a main constructor, a constructor with defaults, and a constructor with different parameters), this time representing times. A Time can be represented as an hour between 0 and 23, minute between 0 and 59, and second between 0 and 59. Make sure your main constructor enforces these constraints, and throws an exception if they are violated. Define a convenience constructor that takes just hours and minutes, and provides a default value of zero seconds for the time. Define another constructor that takes an hour between 1 and 12, minutes between 0 and 59, and a boolean flag isAM that is true for the morning hours, and initializes the Time’s fields accordingly. Be sure you have removed as much code duplication as you can from these three constructors.

Test your implementation carefully: what are the corner cases you should worry about?