Lesson 1 - Exceptions In Java
In this course, we're going to focus on working with files. However, before we can begin to read and write files, we'll have to talk about handling error states, which will occur a lot when working with files.
Errors will often occur in our programs. I'm not referring to errors caused due to poorly written code, we can avoid such errors pretty well. I'm mainly referring to errors caused by input/output operations, often referred to as I/O. The most common culprits for these sort of errors are user input from the console, files in general, output to a file, output to a printer, and so on. In all of these cases, there is a user who can enter invalid input or nonexistent/invalid files, unplug the printer, and so on. However, we won't let them crash our programs due to errors. Instead, we'll sanitize all of the vulnerable places in the program and inform the user about the situation.
Active error handling
The first approach of dealing with program errors is called active error
handling. We'll map all of the vulnerable spots in the program and sanitize them
with conditions. Dividing by zero is one of the most common and easily resolved
errors. Imagine a program that uses a class called Mathematics
,
which has a divide()
method. The class might look something like
this:
class Mathematics { public static int divide(int a, int b) { return a / b; } ... }
Now we use the class like this:
System.out.println("Enter the dividend and the divisor to calculate the result:"); int a = Integer.parseInt(sc.nextLine()); int b = Integer.parseInt(sc.nextLine()); System.out.println(Mathematics.divide(a, b));
If the user enters the numbers 12
and 0
, the
program crashes with an error because we can't divide by zero. However, we can
sanitize the error actively with a simple condition in the program:
System.out.println("Enter the dividend and the divisor to calculate the result:"); int a = Integer.parseInt(sc.nextLine()); int b = Integer.parseInt(sc.nextLine()); if (b != 0) System.out.println(Mathematics.divide(a, b)); else System.out.println("You cannot divide by zero.");
Now, every time we use this method we have to make sure we don't pass zero to the second parameter. Imagine if the method took 10 parameters and was used at several points in the program. It'd most likely be a nuisance to sanitize all of this method's different uses.
One solution would be to add sanitation directly into the method. However, we
would then have a new problem: What value will it return when the second
parameter is zero? We need a value that would tell us when the calculation
wasn't successful. This is a major issue because if we choose to return zero, we
wouldn't know whether the calculation causes an error or not. We have no way of
knowing whether 0
is a result or an error. Even negative numbers
won't help in this case. We could use the nullable Integer
type,
but there's an easier and more correct solution. Parsing values is the second
classic example of user input vulnerability. The next example is file operations
where the file may not exist, we don't have to have the required privileges to
access the file, or the file is being used in another program.
Passive error handling
We use exceptions when the operation is complex and it'd be
too difficult to sanitize all of the possible error states. Exceptions are
referred to as a passive error handling. We don't have to deal with the
internal logic of the method we call in any way. All we have to do is try to run
the vulnerable part of the code in a "protected mode". This mode is
slightly slower and different in that if an error occurs, we are able to catch
it and prevent the application from crashing. We refer to an error as to an
exception here. We use
try
-catch
blocks to catch
exceptions:
try { } catch (Exception e) { }
We put the vulnerable part of the code into the
try
block. If an error occurs in the try
block, its
execution is interrupted and the program jumps to the catch
block.
If everything goes as expected, the entire try
block is executed
and the catch
block is skipped. Let's test them out on our previous
example:
try { System.out.println(Mathematics.divide(a, b)); } catch (Exception e) { System.out.println("Error occurred."); }
The code is simpler since we don't need to sanitize all of the vulnerable
spots and think about everything that could go wrong. We simply wrap the
dangerous code using the try
block, and all of the errors will be
caught in the catch
block. Of course, we only put what is
absolutely necessary in the try
block, not the entire program
Now we know how to handle situations where the user enters some input that could cause an error. Exceptions have a very wide range of use which is by no means limited to file operations. We could alter our program so that it cannot be broken by the user.
In the past, we've already used the try
and catch
blocks several times, especially with date and time parsing.
Using exceptions when working with files
As mentioned before, file operations can cause many exceptions, so we
always work with files in try
-catch
blocks.
There are several other constructs that can be used with exceptions.
Finally
We are able to add a third block to the try
-catch
block called the finally
block. This block
is always executed whether an exception has occurred or not. Consider
the following method for saving application settings (we'll keep our file
handling methods simple for now):
public void saveSettings() { try { openFile("file.dat"); writeToFile(settings); } catch (Exception e) { System.out.println("An error has occurred."); } if (fileIsOpened()) closeFile(); }
The method tries to open a file and add a settings object to it. If an error
occurs, it displays a message to the console. An opened file has to be closed in
all cases. However, printing errors directly in a method isn't a great approach.
We are all aware that methods and objects, in general, should only perform the
logic, and the communication with the user should be performed in the layer
which calls them. Now, let's assign a return value of the boolean
type to the method and return true
/false
depending on
whether the operation succeeded or not:
public boolean saveSettings() { try { openFile(); writeToFile(); return true; } catch (Exception e) { return false; } if (fileIsOpened()) closeFile(); }
At first sight, it seems like the file will always be closed. However, the
code is in a method in which we call return
. As you already know,
the return
statement terminates the method and nothing after it
will be performed.
Therefore, the file would always remain open. As a result, the file could
become inaccessible. If we put the file closing part into the
finally
block, it would always be executed. Java remembers
that the try
-catch
block contained
finally
and calls the finally
block even after leaving
the try
or catch
block:
public boolean saveSettings() { try { openFile(); writeToFile(); return true; } catch (Exception e) { return false; } finally { if (fileIsOpened()) closeFile(); } }
Therefore, finally
is used for resource
cleaning routines along with exceptions, i.e. closing files, freeing
memory, and so on.
The entire situation was greatly simplified. For each file type, there are Java writer and reader classes. A method for storing settings in Java would look something more like this:
public boolean saveSettings() { FileWriter w = null; try { w = new FileWriter("file.dat"); w.write(object); return true; } catch (Exception e) { return false; } finally { if (w != null) w.close(); } }
First, we assign null
to the writer instance. Then, we try to
create the writer over the file.dat
file, in the try
block and write an object. If everything is successful, we return
true
(we also add a finally
block at the very end).
The operation could still fail for two reasons. We could be unable to write to
the file or are unable to open the file for writing at all. In both cases, we
catch the exception and return false
based on what we assume is the
reason that the method was unable to save the data. Last of all, we close the
file opened by the writer in the finally
block. Since opening the
file could be unsuccessful, we'll have to ask whether the writer was created at
all before we proceed to close it. We could call the method like this:
if (!saveSettings()) System.out.println("Unable to save settings.");
It would be even better to omit the catch
block and let the
method throw an exception. We'll suppose it'll be the method which called our
method who will catch it, not our method. It's better like that because we avoid
having to use a return value here (which could then be used for something else),
and the code is simpler:
public void saveSettings() throws Exception { FileWriter w = null; try { w = new FileWriter("file.dat"); w.write(object); } finally { if (w != null) w.close(); } }
Note that we added throws Exception
to the method header. By
this, we're saying Java that this method may throw an exception, and thanks to
that it'll stop reporting a missing catch block. We could call the method like
this:
try { saveSettings(); } catch (Exception e) { System.out.println("Unable to save settings."); }
Now, let's simplify the entire situation even more via the
try-with-resources
construct.
Try-With-Resources
Java (since version 7) makes it much easier to work with class instances to
read and write to files. The block above can be rewritten using the
try-with-resources notation which replaces the
try
and finally
blocks. The biggest advantage
to try-with-resources is that the finally
block is
generated automatically and it ensures that the instance of the reader or writer
closes the file. The saveSettings()
method with the
try-with-resources construct would look like this:
public void saveSettings() throws Exception { try (FileWriter w = new FileWriter("file.dat")) { w.write(object); } }
As you can see, the code has been extremely simplified (it still does the
same thing). However, when we call the method, we have to use the
try
-catch
block again. Remember that
try-with-resources only replaces the
try
-finally
blocks, not the catch
block!. The method, in which try-with-resources (TWR) is in, must be
called in a try
-catch
block.
Now, we're exactly where we need to be. We'll use try-with-resources for all file manipulations in further lessons. The code will be simpler and files will be closed automatically for us.
We'll return to exceptions once again, and learn how to exclusively catch
exceptions of a specific type. You'll also learn which exception classes we're
able to use in our programs and how to create your own exceptions. For now,
however, the bare minimum of information for working with files will do. I
didn't want to confuse you with complex constructs this early in the course
In the next lesson, Introduction to working with files in Java, we'll learn about writing privileges for files in Windows and try out some simple file operations.