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 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();
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.
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.
KeyObject
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.
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 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.
Last modified: 18th October 2001