Module 12, Part 1: File I/O#

In this first part of Module 12 we study how to perform input/output (I/O) operations on files.

Reading and Writing Data from/to Files#

The Java API provides several ways to read data from files, and to write data into files. In this section we explore one method to perform file I/O that is very similar to how we perform console I/O, by discussing:

  1. how to create objects of the class ‘File’

  2. how to read data from a File object

  3. how to write data into a File object

Creating File Objects#

The Java API includes a class named java.io.File, and an object of that class represents either a file or a directory. For instance, if we plan to work on a file named test.txt in the current directory, we can create a corresponding object of the class java.io.File by writing: (try this on the Java shell!)

var file = new java.io.File("test.txt");

We can now call various methods of our new file — for instance, file.exists() returns a boolean telling whether a file named test.txt exists or not in the current directory.

Note

We will not focus on the methods of the class java.io.File, but they can be very useful for many purposes: you can have a look at the documentation if you want to know more.

Reading Data from a File#

To read data from a file, we can create a java.util.Scanner object, similar to the ones we typically use to read inputs from the console. The difference is that, instead of creating a Scanner object attached to System.in, we create a Scanner object attached to the File object from which we want to read data.

However, we need to be careful: creating a Scanner attached to a File object may throw a java.io.FileNotFoundException if the file does not exist. Such an exception is checked and therefore we will need to decide how our code can handle it (as we did in Example 69). This is illustrated in Example 71 below.

Example 71 (Reading all lines in a file)

Suppose we have a file called blinkenlights.txt containing this blinkenlights warning message:

ACHTUNG!  ALLES LOOKENSPEEPERS!
Alles touristen und non-technischen looken peepers!
Das computermachine ist nicht fuer gefingerpoken und mittengrabben.
Ist easy schnappen der springenwerk, blowenfusen und poppencorken
mit spitzensparken.  Ist nicht fuer gewerken bei das dumpkopfen.
Das rubbernecken sichtseeren keepen das cotten-pickenen hans in das
pockets muss; relaxen und watchen das blinkenlichten.

The program below reads all lines of this file and prints them on screen, by:

  • creating a File object for the file blinkenlights.txt;

  • creating a Scanner object attached to that file, inside a try-catch statement that handles a possible java.io.FileNotFoundException;

  • reading from the scanner as long as new lines are available, similarly to how we would read inputs from the console.

 1class DisplayFile {
 2    public static void main(String[] args) {
 3        var filename = "blinkenlights.txt";
 4        var file = new java.io.File(filename);
 5
 6        try {
 7            var scanner = new java.util.Scanner(file); // May throw IOException
 8            scanner.useLocale(java.util.Locale.ENGLISH);
 9
10            System.out.println("** Contents of the file '" + filename + "' **");
11            while (scanner.hasNextLine()) {
12                var line = scanner.nextLine();
13                System.out.println(line);
14            }
15            scanner.close();
16        } catch (java.io.FileNotFoundException e) {
17            System.out.println("Error: " + e.getMessage());
18        }
19    }
20}

You can experiment with the program above, e.g. by running it, and then changing the filename on line 3 with a non-existent file: you will see that an exception will be thrown on line 7 and then caught on line 15.

Writing Data into a File#

To write data into a file, we can create an object of the class java.io.PrintWriter attached to the file we wish to write data into. So, if file is a File object, then we can create a PrintWriter object attached to it, by writing e.g.:

var printer = new java.io.PrintWriter(file);

Note that:

  • if the given file does not already exist on disk, the PrintWriter constructor above will create it. The file will be initially empty, and it will grow when data is written into it;

  • if the given file already exists on disk, the PrintWriter constructor will truncate it, i.e., turn it into an empty file; the file will grow when data is written into it. As a consequence, the original file contents will be lost;

  • the PrintWriter constructor might throw a java.io.FileNotFoundException if the file cannot be created: such an exception is checked and therefore we will need to decide how our code handles it (as we did in Example 69).

Importantly, objects of the class java.io.PrintWriter provide methods that are very similar to those offered by the object System.out that we use for console output. (In fact, the class of the object System.out is java.io.PrintStream, and its constructor and methods are almost identical to the class java.io.PrintWriter we are using here for file I/O). Therefore, we can use the printer object created above to call methods that have a name and a behaviour similar to what we already know via System.out, such as:

  • printer.print("Hello") to write the string Hello into the file to which the printer object is attached;

  • printer.println("Hej") to write the string Hej followed by a line separator into the file to which the printer object is attached.

Important

When the printer object is not needed any more, we should close it by calling printer.close().

The use of a PrintWriter object is illustrated in Example 71 below.

Example 72 (Writing data into a file)

The program below writes all numbers from 0 to 99 (in a 10 \(\times\) 10 arrangement) in the file test.txt, by:

  • creating a File object for the file test.txt;

  • creating a PrintWriter object attached to that file, inside a try-catch statement that handles a possible java.io.IOException;

  • writing strings into the stream, using the methods .print(...) and .println(...).

 1class WriteFile {
 2    public static void main(String[] args) {
 3        var filename = "test.txt";
 4        var file = new java.io.File(filename);
 5
 6        try {
 7            var printer = new java.io.PrintWriter(file);
 8
 9            for (var i = 0; i < 10; i++) {
10                for (var j = 0; j < 10; j++) {
11                    var padding = (i > 0) ? "" : "0"; // Initial '0' if needed
12                    printer.print(padding + ((i * 10) + j) + " ");
13                }
14                printer.println(""); // Append line separator
15            }
16            printer.close();
17        } catch (java.io.IOException e) {
18            System.out.println("Error: " + e.getMessage());
19        }
20    }
21}

If you run this program, it will create a file called test.txt (if it does not already exist) and its contents will look like:

00 01 02 03 04 05 06 07 08 09 
10 11 12 13 14 15 16 17 18 19 
20 21 22 23 24 25 26 27 28 29 
30 31 32 33 34 35 36 37 38 39 
40 41 42 43 44 45 46 47 48 49 
50 51 52 53 54 55 56 57 58 59 
60 61 62 63 64 65 66 67 68 69 
70 71 72 73 74 75 76 77 78 79 
80 81 82 83 84 85 86 87 88 89 
90 91 92 93 94 95 96 97 98 99 

Concluding Remarks#

You should now have an understanding of how to read and write data from/to files.

The Java API includes a large number of classes and methods for dealing with files, and you may use them when writing larger programs: see the References and Further Readings for some pointers.

References and Further Readings#

You are invited to read the following sections of the reference book:

  • Section 11.6 - “I/O Exceptions”

  • Section 5.5 - “Iterators” - subsection “Reading Text Files”

As mentioned above, the class of the object System.out is java.io.PrintStream, which has constructors and methods that are almost identical to the class java.io.PrintWriter that we have used for writing data into a file. Still, it is possible to use PrintStream instead of PrintWriter for writing data into a file: the difference between the two classes is quite subtle and is important for some applications — but it is not crucial for the simple programs addressed in this course, where the two classes can be used interchangeably.

If you are curious, you can also explore the documentation of the following classes, that contains useful information (e.g., how to create a PrintWriter or PrintStream or a Scanner directly with a file name, without first creating a File object):

Lab and Weekly Assessments#

During the lab you can try begin your work on the weekly assessments below.

Important

01 - Mini Grep#

The goal of this assessment is to write a program that works like a simplified version of the “grep” command-line utility.

Edit the file MiniGrep.java provided in the handout, and write a program that takes two command-line arguments:

  • the first argument is a string to be searched;

  • the second argument is the name of a file.

First, the program must check whether it is being executed with exactly two arguments. If not, it must print and error and end immediately. For instance, if the program is executed as:

java MiniGrep.java aaa bbb ccc ddd

then the program must print the following message, and then end:

Error: expected 2 arguments, got 4 instead

If the program is being executed with exactly two arguments, then it must read each line of the file with the name specified as the second argument, and print each line that contains the string given as first argument. If the file does not exist, the program must print an error and end.

For instance, suppose that the program is executed as:

java MiniGrep.java Hello hello-world.txt

If the file hello-world.txt does not exist, the program must print the following message, and then end:

Error: file hello-world.txt not found

Now, suppose instead that the file hello-world.txt exists, and contains the following lines:

Hello, human!
This is a test
I just wanted to say: Hello, World!
The file ends here

Then, the program must print each line that contains the string Hello (which was given as first command-line argument). Therefore, the program must print:

Hello, human!
I just wanted to say: Hello, World!

When you are done, submit the modified file MiniGrep.java on DTU Autolab.

Warning

The automatic grading on DTU Autolab includes some additional secret checks that test your submission with more command-line arguments and input files. After you submit, double-check your grading result on DTU Autolab: if the secret checks fail, then your solution is not correct, and you should fix it and resubmit.

Hint

  • You might want to revise how to access the array of command-line arguments.

  • To read all lines in a file, you can follow Example 71.

  • To check whether a String object str1 contains another string str2, you could write the string search code yourself, e.g. by adapting the solution to the assessment 09 - Sub-Array

    Otherwise, you can call the method str1.contains(str2) (provided by all objects of type String): you can find more details here. Note that, according to the documentation, the method .contains(...) takes as argument any object of type CharSequence — which is an interface defined in the Java API that is implemented by the class String (and this is why the method .contains(...) can take an object of type String as argument).

02 - Cow Say, Part 2#

This is a follow-up to the assessment 02 - Cow Say, and the starting point is its solution, i.e. the file CowSay.java (you can use either your own file, or the one provided by the teacher as a solution to 02 - Cow Say).

The purpose of this assessment is to extend the program CowSay.java so it can optionally read its input from a file, and write its output into a file. More in detail, the updated program must behave as follows:

  • If CowSay.java is executed with no command-line arguments, then it must keep working as in 02 - Cow Say — i.e., it must read it input from the terminal, and then print its output on the terminal.

  • If CowSay.java is executed with one command-line argument, then it must read its input from the file specified by the command-line argument, and then print its output on the terminal. You can assume that the input file contains exactly one line of text. For instance, if the program is executed as:

    java CowSay.java joke1.txt
    

    then the program must read the joke contained in the file joke1.txt, and display on the terminal a cow telling that joke.

  • If CowSay.java is executed with two command-line arguments, then it must read its input from a file specified by the first command-line argument, and then write its output into a file specified by the second command-line argument. You can assume that the input file contains exactly one line of text; the program must not display any output on the terminal. For instance, if the program is executed as:

    java CowSay.java joke2.txt output.txt
    

    then the program must read the joke contained in the file joke2.txt, and write into the file output.txt the text representing a cow telling that joke.

When you are done, submit the modified file CowSay.java on DTU Autolab.

Hint

  • You may want to revise how to read the command-line arguments of a Java program.

  • Depending on how you structure your code, you may need to declare that the main method of the class CowSay might throw a java.io.IOException. Therefore, the method may look like:

    public static void main(String[] args) throws java.io.IOException {
        ...
    }
    
  • You can reuse and adapt most of the code of the solution to 02 - Cow Say (either yours or the teacher’s) by observing that its input/output operations use an object of type Scanner to read text, and the object System.out (which has type java.io.PrintStream) to produce output. Therefore, you could adapt the code (and avoid duplication) by moving most of it into a utility method like:

    private static void readTextPrintCow(java.util.Scanner scanner,
                                         java.io.PrintStream out)
    

    which reads a line from the given scanner object and produces output using the given out object. When this method is called, the argument scanner might be attached either to System.in or to an input file; instead, the argument out might be either the object System.out or a PrintStream object attached to an output file…

03 - Maze, Part 2#

This is a follow-up to the assessment 02 - Maze, and the starting point is its solution, i.e. the file Maze.java (you can use either your own file, or the one provided by the teacher as a solution to 02 - Maze).

Your task is to add the capability of loading a maze from a file, and saving a maze in a file. The contents of a file containing a maze look as follows:

6,5
### X
#   #
# #  
##  #
## ##
# #  

The first line of the file contains the number of rows and columns of the maze separated by a comma , (in this example, 6 rows and 5 columns), and the remaining lines contain each row of the maze, with one character per cell (the meaning of each character is described in 02 - Maze).

Note

When a maze with n columns is saved in a file, each row of the maze must contain exacty n characters. For instance, in the file contents shown above, each row of the maze contains 5 characters (some of them are spaces: you can see them more clearly by selecting the text).

Edit the file Maze.java and implement the following static methods:

  • public static char[][] load(String fname) throws java.io.IOException
    

    This static method loads a maze from a text file called fname. You can assume that the contents of the input file respect the description above.

  • public static void save(char[][] maze, String fname) throws java.io.IOException
    

    This static method saves the given maze in a file called fname. If the file already exists, this static method must overwrite it.

When you are done, submit the modified file Maze.java on DTU Autolab.

Hint

  • To extract the number of rows and columns of the maze from the input file, you might split the first input line (which is a String) around the separator ",". For more details, see Example 35.

  • To retrieve the character at position n of a string object str, you can use str.charAt(n). For more details, see Some Useful String Methods.

  • To convert the content of a String object into an integer value, you can use the static method Integer.parseInt(...) (provided by the Java API). For instance, Integer.parseInt("42") returns the int value 42.

Warning

The automatic grading on DTU Autolab includes some additional secret checks that test your submission by loading and saving other mazes. After you submit, double-check your grading result on DTU Autolab: if the secret checks fail, then your solution is not correct, and you should fix it and resubmit.