Get up to 80 % extra points for free! More info:

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/in­valid 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.


 

All articles in this section
Files and I/O in Java
Skip article
(not recommended)
Introduction to working with files in Java
Article has been written for you by David Capka Hartinger
Avatar
User rating:
No one has rated this quite yet, be the first one!
The author is a programmer, who likes web technologies and being the lead/chief article writer at ICT.social. He shares his knowledge with the community and is always looking to improve. He believes that anyone can do what they set their mind to.
Unicorn university David learned IT at the Unicorn University - a prestigious college providing education on IT and economics.
Activities