Lecture. Exceptions and File IO
[comp1110-2026s2-lec03b] - August 13, 2026
- Fabian Muehlboeck
Lecture. Exceptions and File IO
[comp1110-2026s2-lec03b]- August 13, 2026
- Fabian Muehlboeck
1. Exceptions
[comp1110-88] - Fabian Muehlboeck
1. Exceptions
[comp1110-88]- Fabian Muehlboeck
1.1. Why Java Programs Crash
[comp1110-89] - Fabian Muehlboeck
1.1. Why Java Programs Crash
[comp1110-89]- Fabian Muehlboeck
Generally, programs crash when they try to execute an invalid operation. For example, so far we have seen that a program crashes when we try to access an array at an invalid index (say a negative index or in index larger than or equal to the array's size). Another reason might be that we are trying to divide a number by zero, or to parse an integer from a string that does not represent a number. There are many more potential reasons, some of which are related to things computers simply cannot or should not do, and some of which just relate to things that should not happen within the logic of a particular program.
In many modern programming languages, including Java, all of these kinds of problems are handled through a single exception mechanism. Exceptions are thrown at the point where the program itself or the language runtime detects an error. They then try to find an exception handler, which may execute code in response to the exception, and potentially re-throw it. If no handler is found, the program crashes, and simply prints information about the exception that made it crash.
In Java, exceptions are objects belonging to certain exception classes, for example NumberFormatException or ArrayIndexOutOfBoundsException.
The different classes make it possible to distinguish between different kinds of exceptions, and because exceptions are objects, they can carry relevant data about the particular problem that caused the exception to be thrown.
In general, the exception classes belong to one of three groups:
- Checked Exceptions
This is the standard kind of exception in Java, meant to express both error cases in the normal program logic and certain standard kinds of errors that are common and should usually be handled gracefully. Java is a bit of an outlier among major programming languages in that its compiler enforces that a programmer either handles such exceptions where they can occur or explicitly declares that such exceptions might be thrown within their code.
- Runtime Exceptions
These encompass all exceptions we have seen in the course so far. These are either very special or so common that it would be highly impractical to demand that they be checked or annotated everywhere they could occur, and so get an exemption from the above rule.
- Errors
These are the kinds of problems that might occur in the execution of a program, but are not necessarily the program's fault, and in any case, one cannot reasonably expect that a program can recover from them. Generally, you should not try to handle those.
Construction 1.2. Try-Catch-Finally
[comp1110-90] - Fabian Muehlboeck
Construction 1.2. Try-Catch-Finally
[comp1110-90]- Fabian Muehlboeck
Like if, while, and switch, try-catch-finally is a control flow construct,
written as follows:
You can generally have arbitrarily many catch-blocks after a try-block, and at most one finally-block. There must be at least one catch-block or a finally-block after a try-block.
Example 1.3. Parsing integers safely
[comp1110-91] - Fabian Muehlboeck
Example 1.3. Parsing integers safely
[comp1110-91]- Fabian Muehlboeck
Consider the following program:
The program asks the user for two numbers and then calculates their product. This calculation only makes sense if the user actually enters integers. If they do not, the program crashes:
The crash happens in the line that defines the variable x above, specifically when calling Integer.parseInt with a string that is not a number.
The error message describes both the kind of exception (java.lang.NumberFormatException, in short NumberFormatException) and the place where it happened.
We can handle this error by wrapping the call to Integer.parseInt in a try-catch block.
Because we do this twice, we factor this functionality out into a new function, like so:
Here, getNumber becomes a safer version of the combination of Integer.parseInt and IO.readln - it keeps asking the user again and again until they enter a string that parses without throwing an exception.
Definition 1.4. Null
[comp1110-2R] - Liam O'Connor, Fabian Muehlboeck
Definition 1.4. Null
[comp1110-2R]- Liam O'Connor, Fabian Muehlboeck
In Java, null is a special value that can be used as an instance of any reference type.
It is also the default value of any reference type.
That is, when you create an array of a reference type without giving the specific values in it,
for example using new String[10], the array will initially be filled with null.
Besides being a default value, null may be used intentionally to mark the optional absence of values.
More modern language often use special option types for this, and either disallow null entirely or require a special marker on a type to make it "nullable".
This is because when null can be anywhere, it is usually the cause of many bugs. Tony Hoare called the invention of null his "billion-dollar mistake".
The thing about null is that while you can assign it to any variable of a reference type or return it from any method that needs to return something of a reference type,
you cannot use it in any interesting way without causing a NullPointerException.
The only things that are safe to do are assignments of it, passing it as an argument (this may cause a bug further down the line) and comparisons using the == operator.
In particular, if null has been assigned to a variable x, any use of the "dot" operator to access a field or method of x will cause an exception.
Example 1.5. NullPointerException
[comp1110-92] - Fabian Muehlboeck
Example 1.5. NullPointerException
[comp1110-92]- Fabian Muehlboeck
If you try to access a field or method of a null value, Java will raise a NullPointerException:
This program crashes:
2. Files and File I/O
[comp1110-94] - Fabian Muehlboeck
2. Files and File I/O
[comp1110-94]- Fabian Muehlboeck
So far, we have navigated the file system and created directories using a terminal emulator,
and we have created and edited files using an editor like nano.
We can do the same things programmatically in Java, which lets our programs store data for the long term and exchange data with other programs more easily.
Construction 2.1. The File Class
[comp1110-95] - Fabian Muehlboeck
Construction 2.1. The File Class
[comp1110-95]- Fabian Muehlboeck
Java's File class specifies objects that represent abstract file paths, and through them, the directories and files in your file system.
To create an instance, you need to use the new operator, and give a path as an argument.
With the above f, you can now call a number of methods:
f.exists()Returns a boolean that is true if the file or directory exists, false if it does not
f.delete()Deletes the file or directory. A directory has to be empty for this to work.
f.getName()Returns the name of the file or directory - useful if you are just given a
Fileobject without having created it.f.getParent()Returns a different
Fileobject representing the parent directory of the file or directory, if that exists (the root directort "/" has no parent, so this would returnnull)f.isDirectory()Returns true if the path represents a directory
f.isFile()Returns true if the path represents a file
f.listfiles()If
frepresents a directory, returns an array containingFileobjects representing all the files contained in the directoryf.mkdir()Creates a directory at the path represented by
f
You can further use it as an argument to create FileReaders and FileWriters.
2.2. Character Encodings
[comp1110-7A] - Fabian Muehlboeck
2.2. Character Encodings
[comp1110-7A]- Fabian Muehlboeck
Files are essentially arrays of bytes stored on your computer.
Sometimes that is fine and the bytes are all we care about - this is the case for Java's .class files, which are not really human-readable.
For our purposes, however, we are mostly concerned with text files, which should consist of human-readable characters.
On modern computers, bytes consist of 8 bits (0-or-1 values), which means a byte can have 256 possible values, as can Java's char data type.
That is enough to store the latin alphabet, arabic numerals, and a number of standard punctuation characters, as in ASCII, but it is not enough to store various other ways of writing and the much larger set of special symbols people use, including emojis.
These days, most text files (especially on the web) are encoded using UTF-8, which has the advantage of being compatible with the important parts of ASCII, in that the first 128 symbols are encoded identically, so any two files that only use characters in that range look the same in UTF-8 and ASCII. Other symbols may use more than one byte, which makes things a bit more complicated.
On the other hand, Java internally encodes its Strings using UTF-16. Luckily, this is mostly something that you do not need to worry about. Java will by default assume that files are encoded in UTF-8 and do the necessary translation.
We mentions this only because of two things: if you exchange data with some other system and start seeing strange symbols, it might be an encoding issue, at which point you should follow up on this more. More importantly for us, though, is that the fact that different encodings exist explains the way that reading and writing files works in Java.
Construction 2.3. Reading Files
[comp1110-96] - Fabian Muehlboeck
Construction 2.3. Reading Files
[comp1110-96]- Fabian Muehlboeck
Interacting with files is risky business, much more so than all the other things we have done in the course so far. This is because they are shared with other programs and users that might work with them at the same time we do. Files might exist one moment and then not anymore, without our program having done anything, just because some other program deleted it. They might exist but cannot be opened, because the user running our program does not have permission to read it, or because there is some deeper problem with the file system.
For that reason, Java specifies a number of checked exceptions that you need to deal with when opening files and reading, usually under the umbrella of IOException.
In principle, when opening a file, you can have access to the raw bytes in the file.
However, for now, we use the FileReader class, which implicitly assumes that the file is encoded using UTF-8 (though you can specify a different encoding when creating the FileReader; that won't be necessary in this course).
The FileReader's role is to interpret the files in the appropriate encoding, and so it's core functionality is to read UTF-16 (Java's internal encoding) code points one by one.
It has a utility method that gives you all lines in a file at once, but that is often undesirable, especially for large files.
To be able to easily read a file line-by-line, we have to use a BufferedReader.
A BufferedReader requires some underlying reader, like a FileReader, to provide the actual data.
The main thing it does is provide a readLine() method, which reads the next line in the file since the last time you called it.
This is a way in which objects have behavior that changes their state. A FileReader starts at the beginning of a file and moves forward in it with every byte that it reads.
As the BufferedReader asks it to keep reading data until it reaches a line ending, each call of readLine() moves the internal state ahead by as many bytes as are needed to represent the line.
Construction 2.4. try-with-resources
[comp1110-97] - Fabian Muehlboeck
Construction 2.4. try-with-resources
[comp1110-97]- Fabian Muehlboeck
Opening a file reserves resources on your computer.
Conflicts over such resources may arise when multiple programs try to access the resources at the same time.
Additionally, not every operation on such resources will necessarily happen immediately.
For example, reading from and writing to files is somewhat expensive, so we use something like BufferedReader and (coming up) BufferedWriter, which try to combine several smaller operations into bigger ones, meaning that a number of smaller writes will not immediately take effect until there are enough of them.
If your program crashes in the meantime, it is likely that such data will be lost.
Closing an open file forces any remaining data to be written and any required resources to be returned to the operating system.
Therefore, it is a good idea to keep things clean and tidy, and close an open file as soon as you do not need it anymore (at least for the time being - you can always reopen it later).
All readers and writers have a close() method to do just that.
However, given that lots of exceptions can happen when interacting with files, guaranteeing that the file will be closed regardless of what happens can be tricky.
By default, this is what the finally block of the try-catch-finally construct is for:
Since this is a very standard pattern, a variant of try-catch-finally, called "try-with-resources", makes this more concise:
In this case, the BufferedReader and the FileReader will automatically be close in that order (reversed order of declaration) whenever program execution leaves the try-with-resources block.
The two code blocks here are not perfectly equivalent because the ordering of closing the reader and handling the IOException is different, but that typically does not really matter.
Construction 2.5. Writing Files
[comp1110-98] - Fabian Muehlboeck
Construction 2.5. Writing Files
[comp1110-98]- Fabian Muehlboeck
Writing files shares many considerations with reading files.
As such, the pattern is rather similar, but instead of a FileReader, we use a FileWriter, and instead of a BufferedReader, we use a BufferedWriter.
The BufferedWriter does not have a writeLine or printLine method.
Instead, you have to use the write method and add a newline character yourself:
Here, "\n" is a string consisting of a single newline character '\n'. Backslashes \ start what is called an escape sequence, expressing characters that are not easily printable.
To include a backslash itself, you have to write "\\".
The newline character '\n' (also called "line feed" or "LF") is the standard way of expressing line breaks on Linux and modern MacOS.
(Very) old Macs used a different character, '\r' (called "carriage return" or "CR", representing the idea of resetting the position of your typewriter to the start of a line).
Not to be outdone, Windows programs represent line breaks using a combination of the two, as "\r\n" (also described as "CRLF").
However, most Windows programs also recognize '\n' on its own as a line break (and BufferedReader considers any of the three to be terminating a line).
For this reason, it is usually fine to stick with just using '\n' as line break if you do not need to keep compatibility with specific Windows programs.
Alternatively, you can use BufferedReader's newLine method, which will write a system-specific line break (i.e. usually either '\n' or "\r\n") to your file at the current position:
Finally, BufferedReader does not necessarily write everything to your file right away.
If you want to make sure that everything you have written so far makes it to your file even if your program crashes at some later point, use the method flush().
In any case, flush will automatically be part of closing the file, say at the end of a try-with-resources.
Example 2.6. Reading and Writing from a File
[comp1110-99] - Fabian Muehlboeck
Example 2.6. Reading and Writing from a File
[comp1110-99]- Fabian Muehlboeck
In the lecture, we wrote a program that asks the user for some input, and if it has been run before, will display the input it got the last time we ran the program. We picked the name ".storage" for a file that would store the last input in between program runs. The "." at the start of the filename means this file should normally be hidden, as it is meant as internal data for programs, not for users to interact with outside of it. You can still find it and edit it manually if you wish.