The Dates Examples: Part 2

Extending dates to birthdates

Storing a sequence of dates in an array is all very well, but it is not very useful. It would be much more useful if there were some information attached to each date. For example, suppose each date was the date of birth of some person, and was stored with their name. This would be something that might actually come in handy. Here is a program, very similar to the Dates2 example, that reads a file of names and birthdates, stores them in an array, and sorts them according to age.
     1  import java.io.*;
     2  
     3  class BirthDates1
     4  {
     5  //  Read a number of birth dates from a file, store them in an array
     6  //  and sort them
     7
     8     static final int MAXDATES=100;
     9
    10     public static void main(String[] args) throws IOException
    11     {
    12      String filename;
    13      BirthDate [] data;
    14      int count=0;
    15      BufferedReader in = Text.open(System.in),inFile;
    16      for(;;)
    17          try
    18             {
    19              System.out.print("\nEnter file name to read from: ");
    20              filename=Text.readString(in);
    21              inFile=Text.open(filename);
    22              break;
    23             }
    24          catch(FileNotFoundException e)
    25             {
    26              System.out.println("No file of that name found");
    27             }
    28      data = new BirthDate[MAXDATES];
    29      try
    30         {
    31          for(;;)
    32             data[count++] = readBirthDate(inFile);
    33         }
    34      catch(IOException e)
    35         {
    36         }
    37      catch(ArrayIndexOutOfBoundsException e)
    38         {
    39          System.out.print("Maximum number of birthdates ("+MAXDATES);
    40          System.out.println(") exceeded");
    41         }
    42      count--;
    43      DateSorter.sort(data,count);
    44      System.out.println("The birthdates read are (after sorting):");
    45      for(int i=0;i<count;i++)
    46         System.out.println(data[i]);
    47     }
    48
    49     public static BirthDate readBirthDate(BufferedReader reader)
    50     throws IOException
    51     {
    52      int d,m,y;
    53      String n;
    54      d=Text.readInt(reader);
    55      m=Text.readInt(reader);
    56      y=Text.readInt(reader);
    57      n=Text.readString(reader);
    58      return new BirthDate(d,m,y,n);
    59     }
    60
    61 } 
A suitable file for use with this program is given in the directory containing the dates examples, called birthdates.

It might be thought this would require the writing of a new class for BirthDate something like:

class Birthdate
{
int day,month, year;
String name;

... code for all the methods required

}
which would duplicate a lot of the effort used in writing the class Date. However, Java, in common with other object-oriented languages, has a way in which a class may be written which is based on a class which is already written. This is called inheritance. Inheritance extends the idea of reuse we have already mentioned. Inheritance allows us not only to reuse classes we have already defined anywhere where the same class would be useful, but also to reuse classes by extending them with added detail to construct new classes. Here is the Java code for BirthDate which is actually used by the program BirthDates1 above:
 1  class BirthDate extends Date
 2  {
 3   String name;
 4
 5   public BirthDate(int d,int m,int y,String n)
 6   {
 7    super(d,m,y);
 8    name=n;
 9   } 
10
11   public String toString()
12   {
13    return super.toString()+"    "+name;
14   }
15
16  }
Line 1 says that BirthDate specialises Date. Another way of putting it is to say that Date is a superclass of BirthDate or, alternatively, BirthDate is a subclass of BirthDate. A third way of putting it is to say that a BirthDate "is a kind of" Date. The effect is that a BirthDate object has all the attributes of a Date object, that is a year, a month and a day field, all of type int, but in addition a name field of type String as indicated on line 3 of BirthDate. It also has the same methods as a Date object, except where they have been overridden. A method is overridden in a subclass if the subclass has a new method of the same name and arguments. So here lines 11-14 override the original toString() of Date. However, lessThan(Date d) is not overridden. This means that BirthDate objects can be compared using lessThan which will behave just like they were Date objects - you do not have to mention it in the code for class BirthDate since it is implied by the extends Date on line 1. Since the code for BirthDate makes use of the code for Date, that code has to be available for its use. For now, that can be done by having the file Date.class in the same directory as BirthDate.java when BirthDate.java is compiled (you do not need to have Date.java available).

The overriding of toString means that a BirthDate object has its own toString method, one which prints the name as well as the date. However, in this case the new method makes use of the old method, indicated by super.toString() on line 13. A call to a method prefixed by super. means use the method from the superclass of the class it is being called in.

The constructor for BirthDate objects, given on lines 5-9 also makes use of the equivalent for Date objects. The call to super on line 7 calls the constructor method for Date first, which assigns its argument d to the day field, m to the month field and y to the year field, but after doing this does, on line 8, the additional assignment of n to the name field. A constructor for a subclass must either call super on its first line in this way, or have the effect of calling super() (i.e. the constructor with no arguments) if it is not given (there will be a compiler error if no call to super is given, but the superclass does not have a zero-argument constructor).

One particularly useful thing about inheritance is that code written to deal with objects of the superclass can also deal with objects of the subclass. A good example is shown here in the BirthDates1 example. On line 43 you can see that the call to the sort method of the DateSorter class is just the same as that used previously to sort an array of dates - we don't have to write any new sorting methods as the code to sort an array of objects of type Date can be used to sort an array of objects of type BirthDate.

Late binding

The use of code which deals with objects of one class for objects of a subclass is known as polymorphism. This brings in another issue known as late binding or dynamic binding. Our sort example used the lessThan method from class Date for objects of type BirthDate because BirthDate inherited it from Date. But what if BirthDate had its own lessThan method? We could give it one which is different from the lessThan method of Date by adding the following method to the code for class BirthDate:
1  public boolean lessThan(Date date)
2  {
3   if(date instanceof BirthDate)
4      return (name.compareTo(((BirthDate) date).name)<0);
5   else
6      return super.lessThan(date);
7  }
This version of lessThan is written to make BirthDate objects compare each other using alphabetic ordering of the name field attached to each date, rather than the date order of the year, month and day fields. If you add this method to the file BirthDate.java, recompile that file, and run BirthDates1 again, you will find the names and dates are printed out with the names in alphabetical order. By changing just BirthDate, the sorting method changes from sorting by date to sorting by name even though you didn't change or even recompile the code for it.

The reason for this is that a program which deals with objects of type Date will use the lessThan method of BirthDate if there is one and the objects it is comparing are actually BirthDate objects when the code is run - so it doesn't decide which actual method to use until the code is run (hence "late" or "dynamic" binding of method names to actual methods specified in code).

Note that in order for this new version of lessThan to override and hence replace the version in class Date it has to have the same header (in line 1) as that in Date, in this case public boolean lessThan(Date date). It wouldn't override if it had the header public boolean lessThan(BirthDate date). Because of this it has to test whether its argument date really is of type BirthDate - this is done on line 3 using the operator instanceof which is actually a key word in Java. The test x instanceof t where x is a Java expression, and t a class name is true if the expression evaluates to an instance of the class name. If the Date object date being compared with the BirthDate object is not itself of type BirthDate, the lessThan test of Date is used, indicated on line 6, so comparison is still by date.

Line 4 gives the comparison of names. It is complicated because first of all it is necessary to convert date to its real BirthDate type, which is done using type casting (which we saw previously to do things like turn an int into a double), hence (BirthDate) date. Having done that, the name field is taken from the BirthDate object, and passed as an argument to the compareTo method of the name field of the object lessThan is attached to. Remember that the name field is a String and Strings are themselves objects. One of the methods in class String is compareTo which takes another String as an argument and returns an integer as the result. The integer is less than zero if the first String comes before the second alphabetically, it is 0 is they are identical, and greater than 0 otherwise. The arithmetic operators < and > don't work with Strings. If this is confusing, here is an extended version of the new lessThan:

 1   public boolean lessThan(Date date)
 2  {
 3   BirthDate birthdate;
 4   String birthdatename;
 5   int comp;
 6   boolean testresult;
 7   if(date instanceof BirthDate)
 8      {
 9       birthdate = (BirthDate) date;
10       birthdatename=birthdate.name;
11       comp=name.compareTo(birthdatename);
12       testresult=(comp<0);
13       return testresult;
14      }
15   else
16      return super.lessThan(date);
17  }
It does exactly the same, but breaks what is done just on line 4 above into its component steps on lines 9-13, requiring the additional variables declared on lines 3-6 to hold the results of each step.

Making your own exceptions

You may have noticed that the date programs presented do not check whether the dates are valid. In fact they will accept any combination of three integers as day, month and year. This is obviously unsatisfactory, we would like to be able to check for combination which can't match valid dates - any month less than one or greater than 12, any day outside the number of days in the given month (bearing in mind the complication over February and leap years). We have already seen the idea of exceptions as Java's way of dealing with invalid input for example incorrect number formats, so an obvious way of dealing with invalid dates would be to use an exception. This can easily be done, but here it is a form of execption we are creating ourselves rather than one provided as part of the Java system.

Here is a version of the constructor method to be used in the class Date which throws an exception we have decided to call DateException:

 1  public Date(int d,int m,int y) throws DateException
 2  {
 3   day=d;
 4   month=m;
 5   year=y;
 6   if(month<1||month>12||day<1)
 7      throw new DateException(toString());
 8   else if((month==4||month==6||month==9||month==11)&&day>30)
 9      throw new DateException(toString());
10   else if(month==2)
11     {
12      if(year%4==0)
13         {
14          if(day>29)
15             throw new DateException(toString());
16         }
17      else if(day>28)
18         throw new DateException(toString());
19     }
20   else if(day>31)
21      throw new DateException(toString());
22  }
This is almost correct except it does not deal with the variation on leap years which means that years of the form xx00 are not leap years unless the number xx is divisible by 4. Correcting it to deal with that is left as an exercise.

To indicate that a call new Date(d,m,y) may cause a DateException the words throws DateException are added to the end of line 1. A new DateException is created on lines 9, 15, 18 and 21. When a new exception is created it takes as an argument some string detailing the problem. In this case, the string is just the print-out of the date as given by its toString() method. The keyword throw is used to indicate a user-programmed exception throw. Like a system exception the effect is that execution immediately halts and goes to the place where the exception is caught.

Having used DateException we need to have a separate class defining it. Here it is:

1  class DateException extends Exception
2  {
3   DateException(String s)
4   {
5    super(s);
6   }
7  }
The object type Exception is built in to Java. A new exception can be defined by extending this type, as indicated on line 1 of DateException. Apart from its name a DateException is just like an Exception so it simply calls its super constructor with its own string argument and has no other methods.

To illustrate the use of DateException we give a version of the program which simply asked for a number of dates and read them in as typed by the user:

     1  import java.io.*;
     2
     3  class Dates3
     4  {
     5
     6  //  Read a number of dates and store them in an array.
     7
     8     public static void main (String[] args) throws IOException
     9     {
    10      Date [] data;
    11      int number,count;
    12      int d,m,y;
    13      BufferedReader in = Text.open(System.in);
    14      System.out.print("Type the number of dates that will be entered: ");
    15      number=Text.readInt(in);
    16      data = new Date[number];
    17      for(count=0; count<number; count++)
    18         {
    19          System.out.println("Enter date "+(count+1)+": ");
    20          System.out.print("   Day: ");
    21          d=Text.readInt(in);
    22          System.out.print(" Month: "); 
    23          m=Text.readInt(in);
    24          System.out.print("  Year: ");
    25          y=Text.readInt(in);
    26          try 
    27             {
    28              data[count] = new Date(d,m,y);
    29             }
    30          catch(DateException e)
    31             {
    32              System.out.println("Invalid date: "+e.getMessage());
    33              count--;
    34             }
    35         }
    36      DateSorter.sort(data,count);
    37      System.out.println("The dates entered were:");
    38      for(count=0;count<number;count++)
    39         System.out.println(data[count]);
    40     }
    41
    42  }
Lines 26-34 show where the original program (Dates1) was modified to catch DateExceptions. Note that an exception has a method, getMessage() which is used to give the string that was given when the exception was created. It is used on line 32 to print the invalid date. As we don't want invalid dates to be counted, line 33 decreases the value in the variable count by 1 so that next time round the loop count has the same value as previously rather than the next one.

Having modified Date so that it throws DateExceptions, we have to modify BirthDate so that it can deal with them:

     1  class BirthDate extends Date
     2  {
     3   String name;
     4   
     5   public BirthDate(int d,int m,int y,String n) throws DateException
     6   {
     7    super(d,m,y);
     8    name=n;
     9   } 
    10
    11   public String toString()
    12   {
    13    return name+"   "+super.toString();
    14   }
    15
    16  } 
As you can see, the only change made is on line 5, meaning that if a DateException occurs when the call to the superconstructor on line 7 is made, it is just thrown back to whatever called the constructor for BirthDate. Note that here a DateException is treated just like any other checked exception - as it is not caught you have to indicate on the method header that it may be thrown, and it is automatically thrown if it occurs. Here is the code for a new version of the BirthDates program which deals with invalid dates by catching the DateExceptions:
     1  import java.io.*;
     2  
     3  class BirthDates2
     4  {
     5  //  Read a number of birth dates from a file, store them in an array
     6  //  and sort them
     7  
     8     static final int MAXDATES=100;
     9  
    10     public static void main(String[] args) throws IOException
    11     {
    12      String filename;
    13      BirthDate [] data;
    14      int count=0;
    15      BufferedReader in = Text.open(System.in),inFile;
    16      for(;;)
    17          try
    18             {
    19              System.out.print("\nEnter file name to read from: ");
    20              filename=Text.readString(in);
    21              inFile=Text.open(filename);
    22              break;
    23             }
    24          catch(FileNotFoundException e)
    25             {
    26              System.out.println("No file of that name found");
    27             }
    28      data = new BirthDate[MAXDATES];
    29      try
    30         {
    31          for(;;)
    32             try
    33                {
    34                 data[count++] = readBirthDate(inFile);
    35                }
    36             catch(DateException e)
    37                {
    38                 System.out.println("Invalid date "+e.getMessage());
    39                 count--;
    40                }
    41         }
    42      catch(IOException e)
    43         {
    44         }
    45      catch(ArrayIndexOutOfBoundsException e)
    46         {
    47          System.out.print("Maximum number of birthdates ("+MAXDATES);
    48          System.out.println(") exceeded");
    49         }
    50      count--;
    51      DateSorter.sort(data,count);
    52      System.out.println("The birthdates read are (after sorting):");
    53      for(int i=0;i<count;i++)
    54         System.out.println(data[i]);
    55     }
    56  
    57     public static BirthDate readBirthDate(BufferedReader reader)
    58     throws IOException,DateException
    59     {
    60      int d,m,y;
    61      String n;
    62      d=Text.readInt(reader);
    63      m=Text.readInt(reader);
    64      y=Text.readInt(reader);
    65      n=Text.readString(reader);
    66      return new BirthDate(d,m,y,n);
    67     }
    68  
    69  }
A DateException will occur when a call to create a new BirthDate throws one. In this case it is thrown yet again, as indicated on line 58. It is caught on line 36. The single statement on line 34 may cause a DateException if an invalid date is entered, an IOException if an attempt is made to read past the end of the input file, or an ArrayIndexOutOfBoundsException if an attempt is made to read more birthdates than the maximun storeable in the array. But a DateException is caught inside the loop, so that execution carries on reading birthdates (again, the variable count has to be decreased by 1), whereas the other two exceptions are caught outside the loop, on lines 42 and 45, causing execution to exit the loop.
Matthew Huntbach

These notes were produced as part of the course Introduction to Programming as it was given in the Department of Computer Science at Queen Mary, University of London during the academic years 1998-2001.

Last modified: 21 Oct 1998