Object-Oriented Programming with Java, part I + II

cc

This material is licensed under the Creative Commons BY-NC-SA license, which means that you can use it and distribute it freely so long as you do not erase the names of the original authors. If you make changes in the material and want to distribute this altered version of the material, you have to license it with a similar free license. The use of the material for commercial use is prohibited without a separate agreement.

Authors: Arto Hellas, Matti Luukkainen
Translators to English: Emilia Hjelm, Alex H. Virtanen, Matti Luukkainen, Virpi Sumu, Birunthan Mohanathas, Etiënne Goossens
Extra material added by: Etiënne Goossens, Maurice Snoeren, Johan Talboom

The course is maintained by Technische Informatica Breda


FileIO

Reading a File

A relevant part of programming is related to stored files, in one way or in another. Let’s take the first steps in Java file handling. Java’s API provides the class File, whose contents can be read using the already known Scanner class.

If we read the desciption of the File API we notice the File class has the constructor File(String pathname), which creates a new File instance by converting the given pathname string into an abstract pathname. This means the File class constructor can be given the pathname of the file we want to open.

In the NetBeans programming environment, files have got their own tab called Files, which contains all our project files. If we add a file to a project root – that is to say outside all folders – we can refer to it by writing only the its name. We create a file object by fiving the file pathname to it as parameter:

File file = new File("file-name.txt");

System.in input stream is not the only reading source we can give to the constructor of a Scanner class. For instance, the reading source can be a file, in addition to the user keyboard. Scanner provides the same methods to read a keyboard input and a file. In the following example, we open a file and we print all the text contained in the file using the System.out.println statement. At the end, we close the file using the statement close.

// The file we read
File file = new File("filename.txt");

Scanner reader = new Scanner(file);
while (reader.hasNextLine()) {
    String line = reader.nextLine();
    System.out.println(line);
}

reader.close();

The Scanner class constructor public Scanner(File source) (Constructs a new Scanner that produces values scanned from the specified file.) throws a FileNotFoundException when the specified file is not found. The FileNotFoundException is different than RuntimeException, and we have either to handle it or throw it forward. At this point, you only have to know that the programming environment tells you whether you have to handle the exception or not. Let’s first create a try-catch block where we handle our file as soon as we open it.

public void readFile(File f) {
    // the file we read
    Scanner reader = null;

    try {
        reader = new Scanner(f);
    } catch (Exception e) {
        System.out.println("We couldn't read the file. Error: " + e.getMessage());
        return; // we exit the method
    }

    while (reader.hasNextLine()) {
        String line = reader.nextLine();
        System.out.println(line);
    }

    reader.close();
}

Another option is to delegate the exception handling responsibility to the method caller. We delegate the exception handling responsibility by adding the definition throws ExceptionType to the method. For instance, we can add throws Exception because the type of all exceptions is Exception. When a method has the attribute throws Exception, whatever chunk of code which calls that method knows that it may throw an exception, and it should be prepared for it.

public void readFile(File f) throws Exception {
    // the file we read
    Scanner reader = new Scanner(f);

    while (reader.hasNextLine()) {
        String line = reader.nextLine();
        System.out.println(line);
    }

    reader.close();
}

In the example, the method readFile receives a file as parameter, and prints all the file lines. At the end, the reader is closed, and the file is closed with it, too. The attribute throws Exception tells us that the method may throw an exception. Same kind of attributes can be added to all the methods that handle files.

Note that the Scanner object’s method nextLine returns a string, but it does not return a new line at the end of it. If you want to read a file and still maintain the new lines, you can add a new line at the end of each line:

public String readFileString(File f) throws Exception {
    // the file we read
    Scanner reader = new Scanner(f);

    String string = "";

    while (reader.hasNextLine()) {
        String line = reader.nextLine();
        string += line;
        string += "\n";
    }

    reader.close();
    return string;
}

Because we use the Scanner class to read files, we have all Scanner methods available for use. For instance the method hasNext() returns the boolean value true if the file contains something more to read, and the method next() reads the following word and returns a String object.

The following program creates a Scanner object which opens the file file.txt. Then, it prints every fifth word of the file.

File f = new File("file.txt");
Scanner reader = new Scanner(f);

int whichNumber = 0;
while (reader.hasNext()) {
    whichNumber++;
    String word = reader.next();

    if (whichNumber % 5 == 0) {
        System.out.println(word);
    }
}

Below, you find the text contained in the file, followed by the program output.

Exception handling is the process of responding to the occurrence, during computation, of exceptions – anomalous or exceptional events 
 requiring special processing – often changing the normal flow of program execution. ...
process
occurrence,
–
requiring
changing
program

Character Set Issues

When we read a text file (or when we save something into a file), Java has to find out the character set used by the operating system. Knowledge of the character set is required both to save text on the computer harddisk in binary format, and to translate binary data into text.

There have been developed standard character sets, and “UTF-8” is the most common nowadays. UTF-8 character set contains both the alphabet letters of everyday use and more particular characters such as the Japanese kanji characters or the information need to read and save the chess pawns. From a simplified programming angle, we could think a character set both as a character-number hashmap and a number-character hashmap. The character-number hashmap shows what binary number is used to save each character into a file. The number-character hashmap shows how we can translate into characters the values we obtain reading a file.

Almost each operating system producer has also got their own standards. Some support and want to contribute to the use of open source standards, some do not. If you have got problems with the use of Scandinavian characters such as ä and ö (expecially Mac and Windows users), you can tell which character set you want to use when you create a Scanner object. In this course, we always use the the “UTF-8” character set.

You can create a Scanner object which to read a file which uses the UTF-8 character set in the following way:

File f = new File("examplefile.txt");
Scanner reader = new Scanner(f, "UTF-8");

Anther thing you can do to set up a character set is using an environment variable. Macintosh and Windows users can set up an the value of the environment variable JAVA_TOOL_OPTIONS to the string -Dfile.encoding=UTF8. In such case, Java always uses UTF-8 characters as a default.

Exercise fileio-1: Printer

Create the class Printer, its constructor public Printer(String fileName) which receives a String standing for the file name, and the method public void printLinesWhichContain(String word) which initializes the FileIO, Scanner and prints the file lines which contain the parameter word (lower and upper case make difference in this excercise; for instance, “test” and “Test” are not the considered the same); the lines are printed in the same order as they are inside the file.

If the argument is an empty String, all of the file is printed.

If the file is not found, the method delegates the exception with no need for a try-catch statement; the method simply has to be defined in the following way:

public Printer {

  public void printLinesWhichContain(String word) throws Exception {
     // ...
  }

  // ...
}

The file textFile has been place into the default package of your project to help the tests. When you define the file name of the constructor of Printer, you have to write src/textfile.txt. The file contains an extract of Kalevala, a Finnish epic poem:

Siinä vanha Väinämöinen
katseleikse käänteleikse
Niin tuli kevätkäkönen
näki koivun kasvavaksi
Miksipä on tuo jätetty
koivahainen kaatamatta
Sanoi vanha Väinämöinen

The following example shows what the program should do:

Printer printer = new Printer("src/textfile.txt");

printer.printLinesWhichContain("Väinämöinen");
System.out.println("-----");
printer.printLinesWhichContain("Frank Zappa");
System.out.println("-----");
printer.printLinesWhichContain("");
System.out.println("-----");

Prints:

Siinä vanha Väinämöinen
Sanoi vanha Väinämöinen
-----
-----
Siinä vanha Väinämöinen
katseleikse käänteleikse
Niin tuli kevätkäkönen
näki koivun kasvavaksi
Miksipä on tuo jätetty
koivahainen kaatamatta
Sanoi vanha Väinämöinen

In the project, you also find the whole Kalevala; the file name is src/kalevala.txt

Exercise fileio-2: File Analysis

In this exercise, we create an application to calculate the number of lines and characters.

Exercise fileio-2.1: Number of lines

Create the class Analysis in the package file; the class has the constructor public Analysis(File file). Create the method public int lines(), which returns the number of lines of the file the constructor received as parameter.

The method cannot be “disposable”, that is to say it has to return the right value even though it is called different times in a raw. Note that after you create a Scanner object for a file and read its whole contents using nextLine method calls, you can’t use the same scanner to read the file again!

Attention: if the tests report a timeout, it probably means that you haven’t been reading the file at all, meaning that the nextLine method calls miss!

Exercise fileio-2.2: Number of Characters

Create the method public int characters() in the class Analysis; the method returns the number of characters of the file the constructor received as parameter.

The method cannot be “disposable”, that is to say it has to return the right value even though it is called different times in a raw.

You can decide yourself what to do if the constructor parameter file does not exist.

The file testFile has been place into the test package of your project to help the tests. When you define the file name of the constructor of Analysis, you have to write test/testfile.txt. The file contains the following text:

there are 3 lines, and characters
because line breaks are also
characters

The following example shows what the program should do:

File file = new File("test/testfile.txt");
Analysis analysis = new Analysis(file);
System.out.println("Lines: " + analysis.lines());
System.out.println("Characters: " + analysis.characters());
Lines: 3
Characters: 74

Exercise fileio-3: Word Inspection

Create the class WordInspection, which allows for different kinds of analyses on words. Implement the class in the package wordinspection.

The Institute for the Languages of Finland (Kotimaisten kielten tutkimuskeskus, Kotus) has published online a list of Finnish words. In this exercise we use a modified version of that list, which can be found in the exercise source folder src with the name wordList.txt; the relative path is “src/wordList.txt”. Because the word list if quite long, in fact, a shortList.txt was created in the project for the tests; the file can be found following the path “src/shortList.txt”.

If you have problems with Scandinavian letters (Mac and Windows users) create your Scanner object assigning it the “UTF-8” character set, in the following way: Scanner reader = new Scanner(file, "UTF-8"); Problems come expecially when the tests are executed.

Exercise fileio-3.1: Word Count

Create the constructor public WordInspection(File file) to your WordInspection class.

Create the method public int wordCount(), which counts the file words and prints their number. In this part, you don’t have to do anything with the words, you should only count how many there are. For this exercise, you can expect there is only one word in each row.

Exercise fileio-3.2: z

Create the method public List<String> wordsContainingZ(), which returns all the file words which contain a z; for instance, jazz and zombie.

Exercise fileio-3.3: Ending l

Create the method public List<String> wordsEndingInL(), which returns all the Finnish words of the file which end in l; such words are, for instance, kannel and sammal.

Attention! If you read the file various different times in your program, you notice that your code contains a lot of copy-paste, so far. It would be useful to think whether it would be possible to read the file in an different place, maybe inside the constructor or as a method, which the constructor calls. In such case, the methods could use a list which was read before and then create a new list which suits their search criteria. In week 12, we will come back again with an ortodox way to eliminate copy-paste.

Exercise fileio-3.4: Palindromes

Create the method public List<String> palindromes(), which returns all the palindrome words of the file. Such words are, for instance, ala and enne.

Writing a File

In section 12.1, we learnt that reading from a file happened with the help of the classes Scanner and File. The class FileWriter provides the functionality to write to a file. The FileWriter constructor is given as parameter a String illustrating the file location.

FileWriter writer = new FileWriter("file.txt");
writer.write("Hi file!\n"); // the line break has to be written, too!
writer.write("Adding text\n");
writer.write("And more");
writer.close(); // the call closes the file and makes sure the written text goes to the file

In the example we write the string “Hi file!” to the file “file.txt”; that is followed by a line break, and by more text. Note that when you use the write method, it does not produce line breaks, but they have to be added later manually.

Both the FileWriter constructor and the write method may throw an exception, which has to be either handled or the responsibility has to be delegated to the calling method. The method which is given as parameter the file name and the text to write into it can look like the following.

public class FileHandler {

    public void writeToFile(String fileName, String text) throws Exception {
        FileWriter writer = new FileWriter(fileName);
        writer.write(text);
        writer.close();
    }
}

In the above method writeToFile, we first create a FileWriter object, which writes into the fileName file stored at the location specified as parameter. After this, we write into the file using the write method. The exception the constructor and write method can possibly throw has to be handled either with the help of a try-catch block or delegating the responsibility. In the method writeToFile the responsibility was delegated.

Let’s create a main method where we call the writeToFile method of a FileHandler object. The exception does not have to be handled in the main method either, but the method can declare to throw possibly an exception throw the definition throws Exception.

public static void main(String[] args) throws Exception {
    FileHandler handler = new FileHandler();
    handler.writeToFile("diary.txt", "Dear Diary, today was a nice day.");
}

When we call the method above, we create the file “diary.txt”, where we write the text “Dear Diary, today was a nice day.”. If the file exists already, the old content is erased and the new one is written. FileWriter allows us to add text at the end of the already existing file by providing additional parameter boolean append, without erasing the existing text. Let’s add the method appendToFile() to the class FileHandler; the method appends the text received as parameter to the end of the file.

public class FileHandler {
    public void writeToFile(String fileName, String text) throws Exception {
        FileWriter writer = new FileWriter(fileName);
        writer.write(text);
        writer.close();
    }

    public void appendToFile(String fileName, String text) throws Exception {
        FileWriter writer = new FileWriter(fileName, true);
        writer.write(text);
        writer.close();
    }
}

In most of the cases, instead of writing text at the end of a file with the method append, it is easier to write all the file again.

Exercise fileio-4: File Manager

Together with the exercise body, you find the class FileManager, which contains the method bodies to read a write a file.

Exercise fileio-4.1: File Reading

Implement the method public ArrayList<String> read(String file) to return the lines of the parameter file in ArrayList form, each file line being a String contained by the ArrayList.

There are two text files to help testing the project: src/testinput1.txt and src/testinput2.txt. The methods are supposed to be used in the following way:

public static void main(String[] args) throws FileNotFoundException, IOException {
    FileManager f = new FileManager();

    for (String line : f.read("src/testinput1.txt")) {
        System.out.println(line);
    }
}

The print output should look like the following

first
second

Exercise fileio-4.2: Writing a Line

Modify the method public void save(String file, String text) so that it would write the string of the second argument into the file of the first argument. If the file already exists, the string is written over the old version.

Exercise fileio-4.3: Writing a List

Modify the method public void save(String file, ArrayList<String> texts) so that it would write the strings of the second argument into the file of the first argument; each string of the array list has to go to its own line. If the file already exists, the strings are written over the old version.

Exercise fileio-5: Two-Direction Dictionary

With this exercise, we develop the dictionary we implemented earlier, so that words can be both read and written into the file. Also, the dictionary has to translate in both directions, from Finnish into English and from English into Finnish (in this exercise, we suppose unofficially that Finnish and English do not have words which are spellt the same). Your task is creating the dictionary in the class MindfulDictionary. The class has to be implemented in the package dictionary.

Exercise fileio-5.1: Forgetful Basic Functionality

Create a parameterless constructor, as well as the methods:

  • public void add(String word, String translation) adds a word to the dictionary. Each word has only one translation; if the same word is added twice, nothing happens.
  • public String translate(String word) returns the word translation; if the word isn’t recognised, it returns null

At this point, the dictionary has to work in the following way:

MindfulDictionary dict = new MindfulDictionary();
dict.add("apina", "monkey");
dict.add("banaani", "banana");
dict.add("apina", "apfe");

System.out.println( dict.translate("apina") );
System.out.println( dict.translate("monkey") );
System.out.println( dict.translate("programming") );
System.out.println( dict.translate("banana") );

Prints:

monkey
apina
null
banaani

As you notice from the example, after adding a pair the dictionary can translate in both directions.

Exercise fileio-5.2: Removing Words

Add the method public void remove(String word), which removes the given word and its translation from your dictionary.

At this point, the dictionary has to work in the following way:

MindfulDictionary dict = new MindfulDictionary();
dict.add("apina", "monkey");
dict.add("banaani", "banana");
dict.add("ohjelmointi", "programming");
dict.remove("apina");
dict.remove("banana");

System.out.println( dict.translate("apina") );
System.out.println( dict.translate("monkey") );
System.out.println( dict.translate("banana") );
System.out.println( dict.translate("banaani") );
System.out.println( dict.translate("ohjelmointi") );

Prints:

null
null
null
null
programming

As you see, the delection happens in both ways: whether you remove a word or its translation, the dictionary loses the both the pieces of information.

Exercise fileio-5.3: Loading a File

Create the constructor public MindfulDictionary(String file) and the method public boolean load(), which loads a file whose name is given as parameter in the dictionary constructor. If opening or reading the file does not work, the method returns false and otherwise true.

Each line of the dictionary file contains a word and its translation, divided by the character “:”. Together with the exercise body, you find a dictionary file meant to help the tests. It looks like the following:

apina:monkey
alla oleva:below
olut:beer

Read the dictionary file line by line with the reader method nextLine. You can split the lines with the String method split, in the following way:

Scanner fileReader = new ...
while ( fileReader.hasNextLine() ){
   String line = fileReader.nextLine();
   String[] parts = line.split(":");   // the line is split at :

   System.out.println( parts[0] );     // the part of the line before :
   System.out.println( parts[1] );     // the part of the line after :
}

The dictionary is used in the following way:

MindfulDictionary dict = new MindfulDictionary("src/words.txt");
dict.load();

System.out.println( dict.translate("apina") );
System.out.println( dict.translate("ohjelmointi") );
System.out.println( dict.translate("alla oleva") );

Printing:

monkey
null
below

Exercise fileio-5.4: Saving Data

Create the method public boolean save(); when the method is called, the dictionary contents are written into the file whose name was given as parameter to the constructor. The method returns false if the file can’t be saved; otherwise it returns true. Dictionary files have to be saved in the form described above, meaning that the program has to be able to read its own files.

Attention: even though the dictionary can translate in both directions, only one direction has to be stored into the dictionary file. For instance, if the dictionary knows that tietokone = computer, you have to write either the line:

tietokone:computer

or the line

computer:tietokone

but not both!

It may be useful to write the new translation list over the old file; in fact, the append command which came out in the material should not be used. The final version of your dictionary should be used in the following way:

MindfulDictionary dict = new MindfulDictionary("src/words.txt");
dict.load();

// using the dictionary

dict.save();

At the beginning we load the dictionary from our file, and we save it back in the end, so that the changes made to the dictionary will be available next time, too.