Using tables and files: the student database examples, part 1

A student database handling program

In our previous set of notes we introduced the type Table defined in a completely abstract way as a Java interface. The interface could then be implemented in various ways. The contents of the tables were given by an abstract class KeyObject. The reason for this was generalisation. The class KeyObject gave only that information that is necessary for tables to work. This means the same code could be used for a variety of purposes: instead of writing separate code to give us a database of people, a database of books, a database of cars, and so on, we can just use the same code for all of these purposes.

We will consider a simple database which handles student records. As with our previous examples, we need a front-end which interacts with the human user of the system. Here is some code for it:

import java.io.*;
import java.util.*;

class StudentDatabase1
{
// Database implemented with unordered partly-filled array

 public static void main(String[] args)
 throws IOException
 {
  Table table = new Table1();
  BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
  BufferedReader file;
  Student student;
  String name;
  int mark;
  char ch;
  do {
      System.out.print(": ");
      StringTokenizer toks = new StringTokenizer(in.readLine());
      ch = toks.nextToken().charAt(0);
      switch(ch)   
         {
          case 'a' :
            table.add(new Student(in));
            break;
          case 'd' :
            name = toks.nextToken();
            if(!table.delete(name))
               System.out.println("No student named "+name);
            break;
          case 'f' :
            file = new BufferedReader(new FileReader(toks.nextToken()));
            try {
                 while(true)
                    table.add(new Student(file));
                }
            catch(Exception e) {}
            file.close();
            break;     
          case 'm':
            name = toks.nextToken();
            mark = Integer.parseInt(toks.nextToken());
            student = (Student) table.retrieve(name);
            if(student==null)
               System.out.println("No student named "+name);
            else
               student.addMark(mark);
            break;
          case 'p':
            table.print(System.out);
            break;
          case 'q':
            break;
          case 'r':
            name = toks.nextToken();
            student = (Student) table.retrieve(name);
            if(student==null)
               System.out.println("No student named "+name);
            else
               System.out.println(student);
            break;
          case 'w':
            name = toks.nextToken();
            PrintStream out = new PrintStream(new FileOutputStream(name));
            table.print(out);
            out.close();
            break;
          default:
            System.out.println(" a - add, d - delete, f - file read");
            System.out.println(" m - add mark, p - print, q - quit");
            System.out.println(" r - retrieve, w - file write ");
         }
     }
  while(ch!='q');
 }
}
It is important to note that, as before, this is not intended as a good model for a real database-handling program. In order to avoid distractions and complications, the code is written with minimal allowances for the user. Unless the user types in commands in exactly the expected format, the code will crash. By "crash" is meant that somewhere a command is executed in circumstances where that command cannot be normally executed. An example might be to attempt to convert a string to an integer using Integer.parseInt where the string contains non-numerical characters. The word "crash" suggests a dramatic and damaging event, and its origin as a computing term associates it with such things. But here by "crash" all that is meant is that an exception is thrown, which happens whenever the Java interpreter is intructed to do something that can't normally be handled, and there is no code to handle the exception (i.e. a try-catch statement in the appropriate place). The program will come to a halt, but no real damage is caused.

Of course, a program for use with real users should not come to a halt in this way (though we have all encountered commercially marketed programs that do). The code should be written in such a way that it handles such things as the user typing things in the wrong format, and it should give appropriate error messages and invite the user to continue. A good interface (note the difference between the use of this word here and its completely different use as a Java term) should be robust, meaning the programmer has worked to anticipate any unintended things the user of the system might do, and handle them in an appropriate way.

However, here we want to concentrate on the basics. It would make the above code very much longer and hide the basics of what we are trying to show if we incorporated error-handling statements into it.

Although the comment on the code says that it is a "database implemented using unordered partly-filled array", there is just one line which makes it that:

   Table table = new Table1();
The variable table is declared to be of the abstract type Table, but is assigned a value of type Table1. This is permitted, since is a subtype of Table. Nowhere else in the code is there anything which relies on table referring to a Table1 object. All the methods called on it come from the interface Table. Therefore, if we wished the database to be implemented in some other way, say by unordered linked lists, we could just assign table to an object that implements the abstract type Table in this way, by changing the above line to:
   Table table = new Table3();

Using external files

One feature of our database handling code is that it can read records from and write records to external files. "External" here means outside the program, as it is possible to have a file that exists purely during the execution of a program. Here we can read data from a file that existed before the program started and write it to a file that will remain on the system when the program's execution finishes. A file here can be considered just a long list of characters, and is a way of storing data outside the confines of a program being executed.

In this course, we have dealt with input using something which is part of Java's java.io package rather than use the common approach, adopted by many introductory programming textbooks, of defining and using our own input/output package. So in any program which has to take input from the command window, we declare a BufferedReader variable and assign it to a new BufferedReader object constructed to take input from the command window. We do this by:

  BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
Although the variable is called in, and the convention of calling such a variable by this name is usual, there is nothing special about the name in and we could have called it anything. If we want to read a line from the command window, we call in.readline() which returns that line as a string. As the class BufferedReader does not have methods for reading parts of lines (actually it also has a method called just read which reads an individual character) we have to break a line into its parts using a StringTokenizer

However, a BufferedReader object can also be constructed which reads from an external file rather from the command window. If name is a variable of type String then new BufferedReader(new FileReader(name)) constructs a BufferedReader object which reads from the file whose name is the string referred to by name. The name is is used in the context of the directory from which the Java program is being run. The same readLine method is used to read from the file as was used to read from the command window. Once a line is read from the file, the next call of readLine on the same BufferedReader object will read the next line from the file and so on.

In the database program, the command f followed by a file name will cause a series of records to be read from the file named, converted to objects of type Student, and added to the database. This is done by:

     file = new BufferedReader(new FileReader(toks.nextToken()));
     try {
          while(true)
             table.add(new Student(file));
         }
     catch(Exception e) {}
     file.close();
where file is a variable that was declared previously of type BufferedReader, and toks is the StringTokenizer constructed from the line of text that was the complete command (with the first token from it being the string "f"). Note the use of an infinite loop and try-catch to keep on reading until an exception is thrown, which will happen when the end of the file is reached, or text is found in the file which is not in the format required to be a student record. Normally a file is closed using the close method once use of it has finished, although any unclosed files are closed automatically when a program terminates. keep on reading.

If an attempt is made to construct a FileReader (which is then used to construct a BufferedReader) with a name which doesn't refer to any file, a FileNotFoundException is thrown. So if we wanted more robust code that noted when an attempt was made to read from a non-existent file and prompted the user to enter another file name in that case, the following could be used:

     Str str = toks.nextToken();
     while(true)
        try {
             file = new BufferedReader(new FileReader(str));
             break;
            }
        catch(FileNotFoundException e)
            {
             System.out.print("File not found, enter another name: ")
             str=in.readLine();
            }
     try {
          while(true)
             table.add(new Student(file));
         }
     catch(Exception e) {}
     file.close();
So if the BufferedReader is constructed, the next statement is break which causes execution to break out of the inifinite loop, but if it is not created beause the file is not found, execution jumps to the catch part and prompts for and reads a new name. Since the break statement was not reached, the loop is not exited so the process of trying to create a BufferedReader is repeated. Properly robust code would also give the user an opportunity to abort the f command at this point rather than enter another file name, as it may be that the user will not want to continue if s/he finds that a file that ought to have been there wasn't.

The class Student has a constructor, used here, which takes a BufferedReader as its argument. This means that it is left to that class to have the code which reads off a student record and converts it to a Student object. The code in our database program for adding an individual student to the database is:

     table.add(new Student(in));
In this case, the BufferedReader that refers to the command window rather than a file is passed as the argument to the constructor. It means that a student record should be typed following the a command in exactly the same format as it appears in a file. The expression new Student(in) is a call to the constructore for Student, it returns a new Student object with the side-effect of reading text from the command window. This student object becomes the argument of the call to the method add on the variable table which refers to the database being manipulated by the program.

We have already mentioned the method print in the interface Table which takes a PrintStream object as its argument. Then table.print(System.out) causes a representation of the complete table in the form of a sequence of student records to be printed on the command window. This can be done since System.out is actually an object of type PrintStream. It is used to implement the command p in the database program. The command to the database program w followed by a file name causes the same as p to be printed out, but to a file named by the name given, not to the command window. The code:

     name = toks.nextToken();
     PrintStream out = new PrintStream(new FileOutputStream(name));
     table.print(out);
     out.close();
does this. This shows how a PrintStream object can be constructed which prints to the file named by the string referred to by the variable name. A PrintStream object takes the methods print and println we are familiar with using attached to System.out. One thing to be aware of here is that when a PrintStream object connected with a given name is created as here, if there was an existing file with the same name (in the context of the directory from which the program was called), that file is replaced by the new file. If there isn't an existing file with the given name, a file of that name is created.

Java input/output from files is rather complicated, with a variety of different classes available in the java.io package. Part of the complication comes from Java's use of the 16-bit character set, while files are generally represented using the 8-bit ASCII convention. Also as Java has gone through several revisions, it has to retain within its library old classes where new ones doing the same thing but in a better way have been introduced in order to retain backward compatibility (in other words, so that old programs still run on the updated versions of Java). What is given in these notes is intended to be just enough to allow you to read and write from external files. It is not the place of this course to go into details of the variety of things provided by the Java system.

Extending KeyObject

Any object that is put into databases as represented by our Table type must be of a class which is a subclass of KeyObject. So the class for such an object must provide an implementation of the method getKey, but not the other methods in KeyObject as their code is given there. It must also provide a constructor and any methods which are particular to objects of that class. Here is the code for Student as used in the student database code above.
import java.io.*;
import java.util.*;

class Student extends KeyObject
{
 private String surname;
 private String firstNames;
 private Date dob;
 private Marks marks;

 public Student(BufferedReader file)
 throws IOException
 {
  StringTokenizer toks = new StringTokenizer(file.readLine());
  firstNames="";
  surname=toks.nextToken();
  while(toks.hasMoreTokens())
     {
      firstNames+=surname+" ";
      surname=toks.nextToken();
     }
  dob = new Date(file.readLine());
  marks = new Marks(file.readLine());
 }

 public String getKey()
 {
  return surname;
 }
 
 public String toString()
 {
  return firstNames+surname+'\n'+dob+'\n'+marks;
 }

 public void addMark(int m)
 {
  marks.addMark(m);
 }

 private static class Date
 {
  int day,month,year;

  Date(String str)
  {
   StringTokenizer toks = new StringTokenizer(str,"/");
   day=Integer.parseInt(toks.nextToken());
   month=Integer.parseInt(toks.nextToken());
   year=Integer.parseInt(toks.nextToken());
  }

  public String toString()
  {
   return day+"/"+month+"/"+year;
  }
 }

 private static class Marks
 {
  static final int MAX=24;
  int[] array;
  int count;

  Marks(String str)
  {
   StringTokenizer toks = new StringTokenizer(str);
   count=0;
   array = new int[MAX];
   for(; toks.hasMoreTokens(); count++)
      array[count] = Integer.parseInt(toks.nextToken());
  }

  void addMark(int m)
  { 
   array[count++]=m;
  }

  public String toString()
  {
   String str = "";
   for(int i=0; i<count; i++)
      str+=array[i]+" ";
   return str;
  }
 }
}
The key used for a Student object is the student's surname. This is not really suitable as more than one person may have the same surname, but each object should have a unique key. However, we shall use the surname just for demonstration purposes. The other information stored in a Student object is the student's date of birth and a list of exam marks. These are represented by separate nested classes within class Student. The Date class has three integer fields for day, month and year, and the Marks class uses the partly-filled array and count method of representing a collection of integes. Both Date and Marks have constructors which take a string as their argument so they are responsible for converting the line of text representing a date or a set of marks to a Date or a Marks object. Having the constructors for these classes take a string as their argument has the useful effect of ensuring that if we want to change the text representation of these classes, the only place within the code we have to change is within the classes themselves (providing the text representation is resticted to a single line).

The code expects a student record to take three lines of text in a file. The first line gives the student's name. The last word in the name is taken to be the surname. The second line gives the date of birth. This must consist of three numbers separated by a / character between the first and second and second and third number (the use of a StringTokenizer with the second argument "/" when it is constructed means that the / character is taken to be the separator character rather than the space chcracter). The third line consists of a list of marks, which must be numbers separated by spaces. Since the code is written to be short and simple rather than robust, if the record is not in this format, the code crashes. This would obviously not be sensible in a realistic program. Note, however, that dealing with it by a dialogue which prints an error message and asks for new input would not be sensible either, because the code is designed to read information from a file, not interactively from a human user. There is no check either that the date given is a valid date, or that the marks are within any sort of range (for example, between 0 and 100). A sensible way of handling these errors would be for the Student constructor to throw its own types of exception, but we shall not deal with that here.

Student objects are not immutable. There is a method, addMark designed to represent updating a student record by adding a new mark to the collection of marks. A Student object contains a Marks object to hold its collection of marks, and addMarks called on the Student object causes a method also called addMarks, but defined within the nested class Marks, to be called on the Marks object referenced by the marks field of the Student object.

Executing the database commands

As we have already said, to add a new students to the database in our program, you have to type the command a followed by the student record in the same format in would appear in a file, that is the name on one line, the date of birth using just "/" as a separator between numbers on the next line, and the marks all on the third and final line. This is not intended to be a user-friendly interface! Although some would disagree, making interfaces user-friendly is not a bad thing, and this non-use-friendly interface is not given as a model of good practice. Rather, this is not a course on human-computer interaction, and I have decided to use very simple interface programs in order to avoid distraction from the main issue, which is the algorithms and data structures.

The command d followed by a surname (on the same line this time) causes the delete method to be called on the table, with that surname as its parameter. A message is printed if there is no student with the given surname in the database, indicated by the delete method returning false. Otherwise the record of the student with the given name is deleted with no further indication from the program. A more user-friendly interface might confirm the deletion has happened, or even prompt you to make sure that is what you want.

We have already covered how the command f followed by a file name causes the sudent records in a file to be read and added to the database. The method add is used to add each one separately.

The command m followed by a surname followed by a number causes that number to be added to the list of marks for the student with the given surname. The code

     student = (Student) table.retrieve(name);
retrieves the Student object from the database using name as the key. Note that, as the return type of method retrieve is KeyObject, it is necessary to use type-casting to assign the variable student, which is of type Student, to refer to it. This is because Student is a more specific type than KeyObject. In general a KeyObject value could be of some other type which also extends KeyObject, even though as the code here is written it could only be of type Student. The retrieve method returns null if it is called with its argument a string which isn't the key to any KeyObject in the database. That is why in the next line of code for the m command there is a check to see if the value returned from retrieve is null and if so, a message is printed noting that no student of the given name was found:
    if(student==null)
       System.out.println("No student named "+name);
    else
       student.addMark(mark);
Otherwise, the method addMark is called on the Student object retrieved from the database. Note that while the variable student is at this point one reference to the object, it is a reference to the same object that is still in the database, and addMark is a mutating method. Therefore the addition of the mark to the object's collection of marks applies to the object stored in the database as well as the one referenced by student, as they are the same thing.

The command p calls the print method of the database object, but passes System.out as its argument, so the contents of the database are printed to the command window. The command q is the quit command. Both these commands consist of just the single letter.

The command r followed by a name retrieves the object whose key is that name, in the same way that m does. However, the object is printed on the screen by making it the argument to a call to println on System.out. This causes the toString method of the object to be used automatically.

The command w followed by a filename causes the print method to be called on the database object, as with the p command, but its argument is a PrintStream object created from the file name rather than System.out, causing the contents of the database to be printed to a file of the given name.


Matthew Huntbach

Last modified: 18th October 2001