Skip to main content
Logo image

Problem Solving with Algorithms and Data Structures using Java: The Interactive Edition

Section B.3 Files

Up to this point, we’ve entered all the data for a program as data structures in our example programs or have prompted the user to enter data from the keyboard, using a Scanner with System.in. What if someone sends you a file of several months’ worth of weather data from Munich, Germany, in a file named klima.txt. (You can find this file in the code repository.)
MESS_DATUM;TMK;TXK;TNK
20200106;0.8;6.0;-2.5
20200107;2.4;4.6;-2.1
20200108;3.6;7.2;-1.3
20200109;7.7;14.5;3.6
...
20210704;17.9;22.9;14.7
20210705;18.5;22.4;13.9
20210706;21.8;31.5;15.4
20210707;18.2;20.5;16.7
20210708;16.9;20.1;14.7
The columns stand for date, average daily temperature, high temperature, and low temperature (temperatures are in °C).
If you want to find the maximum temperature and minimum temperature across the whole time period, you certainly don’t want to have to type all the numbers again at the keyboard. Instead, you want Java to be able to read the file from your disk.

Subsection B.3.1 The File Object

In order to access a file, you must use its path name to create a File object. A path name describes how to get to a file in the file system. For this chapter, we’ll presume that your data files are in the same directory as the .class file for your program. That way, the path name is the same as the file name.
The resulting object doesn’t give you access to the file contents; rather, it gives you access to information about the file, also known as the file’s metadata.
Here’s the start of a program that lets you enter a path name and find out about that file:
import java.util.Scanner;
import java.io.File; // this is a new import

public class FileInfo {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("Enter a path name: ");
        String pathName = input.nextLine();
        
        File f = new File(pathName);
Listing B.3.1.
The variable f is what, in other programming languages, is called a file handle or file descriptor. The code continues by calling some of the more useful methods in the File class:
        System.out.println("File exists: " + f.exists());
        System.out.println("File size:   " + f.length());
        System.out.println("Readable:    " + f.canRead());
        System.out.println("Writeable:   " + f.canWrite());
        System.out.println("Executable:  " + f.canExecute());
        System.out.println("Directory:   " + f.isDirectory());
        System.out.println("Normal file: " + f.isFile());
        System.out.println("Hidden file: " + f.isHidden());
    }
}
Listing B.3.2.
A couple of notes: the length method returns the file size in bytes. It is possible for a file to be neither a directory nor a “normal file”—the /dev/zero path on Linux refers to a virtual file that is neither.
The File class also has methods that let you delete files, rename them, and create directories. For details, see the Java API documentation
 2 
docs.oracle.com/en/java/javase/16/docs/api/java.base/java/io/File.html
.

Subsection B.3.2 Reading Files

Let’s write a program that opens the klima.txt file and finds the maximum and minimum daily temperature across the time period described in the file.
In order to read the contents of a file, you must open a Scanner based on the File object. But if you try code like this:
import java.util.Scanner;
import java.io.File;

public class LineCount {

    public static void main(String[] args) {        
        File f = new File("klima.txt");
        
        Scanner input = new Scanner(f);
    }
}
Listing B.3.3.
The compiler will complain (message reformatted to fit page width):
Klima.java:9: error: unreported exception
FileNotFoundException; must be caught or declared to be thrown
        Scanner input = new Scanner(f);
                        ^
1 error
The FileNotFoundException is a checked exception, and the compiler insists that you either catch it or throw it to the caller.
This will require you to import java.io.FileNotFoundException and set up a try-catch block.
import java.util.Scanner;
import java.io.File;
import java.io.FileNotFoundException;

public class Klima {

    public static void main(String[] args) {        
        File f = new File("klima.txt");
        try {
            Scanner input = new Scanner(f);
            // code goes here
            input.close();
        }
        catch (FileNotFoundException ex) {
            System.out.println("Can't find file klima.txt");
        }
    }
}
Listing B.3.4.
Now we are set to read the file’s contents. Rather than write the whole program right now, let’s start by reading the file one line at a time, printing them out to the screen, and counting the number of lines. Here’s the code that goes inside the try block. The hasNextLine method returns false when it hits the end of the file:
while (input.hasNextLine()) {
    String oneLine = input.nextLine();
    System.out.println(oneLine);
    lineCount++;
}
System.out.println("# of lines: " + lineCount);
input.close();
Listing B.3.5.
Now that we know that we can sucessfully open and read the file, we can modify the code to accomplish the task we want to do: finding the minimum and maximum temperatures.
The program will read lines one at a time and then extract the data from each line. Because Scanner’s next and nextDouble methods use whitespace to separate items, we can’t use them “as-is” here, where data items are separated by semicolons. We could solve this problem by using the useDelimiter method to change the separator to a semicolon, but that would deprive us of the opportunity to learn about a new String method and practice more with exceptions.
Let’s replace the line-counting code with this code for finding the maximum and minimum temperatures:
double max = Double.MIN_VALUE
double min = Double.MAX_VALUE;
String[] items = oneLine.split(";");
if (items.length == 4) {
    dayMax = Double.parseDouble(items[2]);
    dayMin = Double.parseDouble(items[3]);
    if (dayMax > max) {
        max = dayMax;
    }
    if (dayMin < min) {
        min = dayMin;
    }
}
Listing B.3.6.
The new String method is split. Given a delimiter, this method splits the given String into an array of strings wherever it finds the delimiter that you give it as an argument. For example, after this code executes:
String s = "sister-in-law";
String[] parts = s.split("-");
Listing B.3.7.
The parts array will be {"sister", "in", "law"}.
Similarly, the program uses oneLine.split(";") to separate the items on each line. If there aren’t four items on a line, it’s incomplete, and the program does nothing (effectively skipping over the line).
We now have a problem: the first line doesn’t have any numbers on it, and trying to use parseDouble on the titles will throw a NumberFormatException. One way to solve the problem is to read in the first line before entering the while loop and discarding the result. Another way to solve this problem (which could also occur if the file we were given had bad data in it), is to use another try-catch:
try {
    double dayMax = Double.parseDouble(items[2]);
    double dayMin = Double.parseDouble(items[3]);
}
catch (NumberFormatException ex) {
    System.out.println("Ignoring non-numeric data "
        + ex.getMessage());
}
Listing B.3.8.

Subsection B.3.3 Writing Files

There are many Java classes for reading files. In this book, we’ve been using Scanner to read input because it contains many methods to make getting input simple.
In a similar way, there are many Java classes for writing files. We’ll discuss only one of them: PrintWriter. To use this class, you must import java.io.PrintWriter
Just as you created a Scanner by using a File object as a parameter to the constructor, you can write to a disk file by creating a File object with the path you want and then use that object as a parameter to the PrintWriter constructor. And, just as the compiler required you to enclose the code in a try-catch block, you must do the same when opening a PrintWriter:
File f = new File("output.txt");
try {
    PrintWriter output = new PrintWriter(f);
    // code to write to file goes here
}
catch (FileNotFoundException ex) {
    System.out.println("Unable to open output file.");
}
Listing B.3.9.
Just like System.out, the PrintWriter object output we have created has print, println, and printf methods. Instead of writing to your screen, they write data to the file you specified.
There are two important things to know about writing files:
  1. When you open a PrintWriter to a File that does not exist, it will be created for you. If you open a PrintWriter to a File that already exists, the existing file will be deleted and re-created. Any information that was in the file will be gone, even if you never write anything to the PrintWriter.
    This means that it is always useful to use the exists method to check if a file already exists and, when possible, give the user the option to overwrite the old file or exit the program.
  2. When you do a println to a PrintWriter, the data is not written to disk immediately. Instead, it is kept in a buffer, and is written only when the buffer is full. If you exit the program with the buffer partially filled, there is a chance that it might not be written to disk. Always call the close method on your output files to make sure that the buffer is written to disk. If you run this program:
    import java.io.File;
    import java.io.FileNotFoundException;
    import java.io.PrintWriter;
    
    public class PartialWrite {
    
        public static void main(String[] args) {        
            File f = new File("write_test.txt");
            try {
                PrintWriter output = new PrintWriter(f);
                output.println("Example of writing to a file.");
                // output.close(); // uncomment this line 
            }
            catch (FileNotFoundException ex) {
                System.out.println("Can't open file write_test.txt");
            }
        }
    }
    
    Listing B.3.10.
    without closing the file, the resulting write_test.txt file will be empty. If you uncomment the output.close(); line and run the program again, the file will contain the output.

Subsection B.3.4 try with Resources

Because it is important to close files, Java has a syntax for associating Files with input and output classes as part of the try syntax. When using this syntax, the Java Virtual Machine will automatically close the input and output when the try block exits. Listing B.3.11 uses try with resources to open a PrintWriter:
File outFile = new File("output_path.txt");
try (PrintWriter outWriter = new PrintWriter(outFile)) {
    outWriter.println("Example of try with resources.");
}
catch (FileNotFoundException ex) {
    System.out.println("Error: " + ex.getMessage);
}
Listing B.3.11.
The declaration of outWriter is now in parentheses after try rather than after the block’s opening brace. We no longer need to call outWriter.close()—the JVM will automatically do the call when it exits the try-catch block.
You can declare as many input and output objects as you want inside the parentheses:
File inFile = new File("input_path.txt");
File outFile = new File("output_path.txt");
try (
    Scanner inScan = new Scanner(inFile);
    PrintWriter outWriter = new PrintWriter(outFile);
) {
    // code...
}
catch (Exception ex) {
    // error handling...
}
Listing B.3.12.
The File declarations cannot go inside the parentheses; the compiler won’t let you do that.
You have attempted of activities on this page.