5.2  Understanding Constructors: Data Integrity; Signaling Errors

Goals

In this part of this lab you will practice the use of constructors in assuring data integrity and providing a better interface for the user.

Designing constructors to assure integrity of data.

The data definitions at times do not capture the meaning of data and the restrictions on what values can be used to initialize different fields. For example, if we have a class that represents a date in the calendar using three integers for the day, month, and year, we know that our program is interested only in some years (maybe between the years 1500 and 2500), 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).

Suppose we use the Date class to check for overdue books.

// to represent a calendar date
class Date {
  int year;
  int month;
  int day;

  Date(int year, int month, int day){
    this.year = year;
    this.month = month;
    this.day = day; 
  }
}

and a simple set of examples:

class ExamplesDates {
 ExamplesDates() {}

  // good dates
  Date d20060928 = new Date(2010, 2, 28);     // February 28, 2010
  Date d20071012 = new Date(2009, 10, 12);    // Oct 12, 2009

  // bad dates
  Date b34453323 = new Date(3445, 33, 23); 
}

Look at the third example of a date.

Of course, the third example is pure nonsense. Only the year is possibly valid - still not really an expected value. To validate the date completely (taking into account all the special cases for different months, as well as leap years, and the change of the calendar at several times in the history) is a project on its own. For the purposes of learning about the use of constructors, we will only make sure that the month is between 1 and 12, the day is between 1 and 31, and the year is between 1500 and 2500.

Did you notice the repetition in the description of the valid parts of the date? This suggests, we start with the following methods:

Design at least one of these methods - you can finish the others at home. For the purposes of being able to test at least the part of the program that is completed, have the other methods produce true for the time being. (We call such temporary method definitions stubs.)

Once you have done so, change the constructor for the class Date as follows:

  Date(int year, int month, int day){
    if (this.validYear(year))
      this.year = year;
    else
      throw new IllegalArgumentException("Invalid year in Date.");

    if (this.validMonth(month))
      this.month = month;
    else
      throw new IllegalArgumentException("Invalid month in Date.");

    if (this.validDay(day))
      this.day = day;
    else
      throw new IllegalArgumentException("Invalid day in Date."); 
  }

This example show you how you can signal errors in Java. The class IllegalArgumentException is a subclass of the RuntimeException. Including the clause

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

in the code causes the program to terminate and print the specified error message.

We want to make sure that this constructor will indeed accept only the valid dates.

The tester library version 1.3.5 released on 5 February 2010 (please, download the new version) allows us to test this constructor.

It provides two test cases:

t.checkConstructorException(String testName, 
  Exception e, String className, 
  Arg1Type arg1, Arg2Type arg2, ...);

t.checkConstructorException(
  Exception e, String className, 
  Arg1Type arg1, Arg2Type arg2, ...);

The following test case verifies that the constructor throws the correct exception with the expected message, if the supplied year is 3000:

t.checkConstructorException(
  new IllegalArgumentException("Invalid year in Date."),
  "Date", 3000, 12, 30);

Run the program with this test. Now change the test by providing an incorrect message, incorrect exception (e.g. NoSuchElementException), or by supplying data that do not cause the constructor to throw an exception. Observe the messages that come with the failed tests.

Java provides the class RuntimeException with a number of subclasses that can be used to signal different types of errors.

We will learn how to design a new subclass of the RuntimeException class that is designed to deal with errors specific to our program at some later date.

Overloading constructors to provide flexibility for the user: providing defaults.

When entering dates in the current year it is tedious to always have to enter 2010. We can make avoid the need to type in the year by providing an additional constructor that requires the user to give only the day and month and assumes that the year is the current year (2010 in our case).

Remembering the single point of control rule, we make sure that the new overloaded constructor defers all of the work to the primary full constructor:

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

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

Overloading constructors to provide flexibility for the user: expanding the options.

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

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

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

  // convert a three letter month code into the numeric month value
  // return 13 if the month code is not valid
  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 {return 13;}}}}}}}}}}}}
  }

Our constructor can then invoke this method as follows:

Date(int year, String month, int day){
  if (this.validYear(year))
    this.year = year;
  else
    throw new IllegalArgumentException("Invalid year in Date.");

  if (this.validMonth(this.getMonthNo(month)))
    this.month = this.getMonthNo(month);
  else
    throw new IllegalArgumentException("Invalid month in Date.");

  if (this.validDay(day))
    this.day = day;
  else
    throw new IllegalArgumentException("Invalid day in Date."); 
}

To check that it works, allow the user to enter only the first three months ("Jan", "Feb", and "Mar"). The rest is tedious, and in a real program it would be designed differently.

5.3  Methods for Lists

Solve the following problem in the lab or at home as a part of your portfolio. We will refer to these examples in lectures this week and the next week.

Start a new project BookLists with the classes Book, MtLoB, ConsLoB, the interface ILoB, and, of course, the Examples class. The (usual) Book class is given by the following class diagram:

  +---------------+
  | Book          |
  +---------------+
  | String title  |
  | String author |
  | int year      |
  +---------------+

Design the following methods for these classes:

    1. hasTitle that determines whether a list contains a book with the given title.

    2. hasAuthor that determines whether a list contains a book written by the given author.

    3. hasYear that determines whether a list contains a book published in the given year.

    1. onlyTitle that determines whether a list contains only books with the given title.

    2. onlyAuthor that determines whether a list contains only books written by the given author.

    3. onlyYear that determines whether a list contains only books published in the given year.

    1. allWithTitle that produces from this list a list that contains only books with the given title.

    2. allByAuthor that produces from this list a list that contains only books written by the given author.

    3. allInYear that produces from this list a list that contains only books published in the given year.