Skip to content

Latest commit

 

History

History
220 lines (158 loc) · 11.8 KB

File metadata and controls

220 lines (158 loc) · 11.8 KB

Java Exceptions

🖥️ Slides

🖥️ Lecture Videos

📖 Required Reading: Core Java for the Impatient

  • Chapter 5: Exceptions, Assertions, And Logging. (Only read sections 5.1-5.1.9: Exception Handling)

Java exceptions allow you to escape out of the normal execution flow of a program when something exceptional happens. You can then centrally handle the exception at a location higher in the code execution stack.

Java uses the standard try, throw, and catch syntax that are found in most programming languages. You define a block where exceptions can occur with the try statement. The try block is then followed by one or more catch blocks. For each catch block you can specify what exception type(s) the block handles. That type and any types derived from it will be caught by that block unless they also match a more specific block. The runtime will pick the block that most specifically matches your exception. If you want to handle all exceptions, then you can specify the Exception base class in your catch block. Keep in mind that it is often best to catch only the most specific exception type that will be thrown.

try {
    // Code that might throw an exception
} catch (FileNotFoundException ex) {
    // Specific file error handling
} catch (IOException ex) {
    // Other IO error handling except file not found
    /* FileNotFoundException is a subclass of
       IOExeption, but won't trigger this block. */
} catch (Exception ex) {
    // General error handling
}

Throw and Throws

You use the throw keyword followed by the allocation of a new exception in order to raise an exception.

throw new IllegalArgumentException("Missing required parameter");

When you throw an exception, the normal flow of your code is interrupted and the execution pointer skips to the closest catch block in the execution stack.

You can throw any exception from a function, but Java requires that your function signature declares all of the exceptions that the function throws. Note that the declaration requirement propagates to any function that calls a function that can throw an exception.

void top() {
    try {
        A();
    } catch (Exception ex) {
        System.out.println("this WILL execute");
    }
}

void A() throws Exception {
    B();
    System.out.println("this will NOT execute");
}

void B() throws Exception {
    C();
    System.out.println("this will NOT execute");
}

void C() throws Exception {
    throw new Exception("declarations all the way up");
    System.out.println("this will NOT execute");
}

Unchecked Exceptions

The exclusion to the throws declaration rule is when you throw what is known as an unchecked exception. Unchecked exceptions are defined as any class that is derived from the RuntimeException class. The reason for unchecked exceptions is that they can be thrown at anytime and so it is unreasonable to explicitly handle them on every function in your code. These should be caught or thrown only very rarely, as they usually indicate a bug in your code (such as a NullPointerException, which is unchecked) rather than a problem that can ocurr during execution of your program (such as a FileNotFoundException, which is checked).

A slight warning is that if you use IntelliJ's auto-suggestions about an uncaught exception, it will often generate a try/catch block with a throw new RuntimeException(); call, which will silently crash your program if reached. Remember to always replace this will something else.

Finally

You can also use the try syntax to create a block of code that always gets executed whenever the try block exits. This is called a finally block. The finally block is executed whether or not an exception is throw. If an exception is thrown, but there is no catch block, the finally method will get called, but then the exception continues up the call stack until a catch block is discovered.

try {
    // Code that may throw an exception
} finally {
    // Code that always gets called
}

Example

Consider the example of a program that requires a configuration file in order to work correctly. If the file does not exist, then you want report the error from your main function and not deep down in the initialization code where the file fails to load.

Note the use of multiple catch blocks, the use of finally, and also the necessity of declaring the exceptions that may be thrown.

import java.io.File;
import java.io.FileNotFoundException;

public class ExceptionExample {
    public static void main(String[] args) {
        // Exceptions are handled centrally for anything that happens in this scope.
        try {
            var example = new ExceptionExample();
            example.loadConfig();
        } catch (FileNotFoundException ex) {
            System.out.printf("Required file not found: %s", ex);
        } catch (Exception ex) {
            System.out.printf("General error: %s", ex);
        } finally {
            System.out.println("Program completed");
        }
    }

    private void loadConfig() throws Exception {
        loadConfigFile("user");
        loadConfigFile("system");
    }

    // Note that the function indicates that it can throw an exception.
    private void loadConfigFile(String location) throws FileNotFoundException {
        var file = new File(location);
        if (!file.exists()) {
            // Let the code above know there was an exception.
            throw new FileNotFoundException();
        }

        // Otherwise load the configuration
    }
}

Custom Exception Types

Java has many useful Exception types you can throw, but often you won't find one that matches what you need. You can create your own exception types by creating subclasses of the Exception class (or of any other exception type). Feel free to add fields to your exception classes to contain any information that might be useful about what went wrong. If you find yourself catching an exception and then checking the message string to see what kind of error it is, you may want to replace it with a custom exception type instead.

Try-With-Resources

Not closing resources, such as file handles or database connections, can lead to leaks that will cause your application to fail. The following example shows the allocation of an input stream that closes the stream after it is used. However, if an exception is thrown during the read operation the stream is not closed and the file handle is leaked. That means the resources associated with the file are never released and eventually that application will not be able to open files.

public void NoTry() throws IOException {
    FileInputStream input = new FileInputStream("test.txt");
    System.out.println(input.read());

    // If an exception is thrown this will not close the stream
    input.close();
}

To work around this, it is common to use the try/finally syntax to clean up resources that need to be closed. In the example below the stream will be closed whether or not an exception is thrown.

public void TryWithFinally() throws IOException {
    FileInputStream input = null;
    try {
        input = new FileInputStream("test.txt");
        System.out.println(input.read());
    } finally {
        if (input != null) {
            // If an exception is thrown this will not close the stream
            input.close();
        }
    }
}

As you can see by the previous example, resource cleanup introduces a lot of boilerplate code. To make this common and necessary activity easier to implement, Java introduced the try-with-resources syntax. You can use this syntax with any class that implements the closable interface. This includes things like input and output streams, readers and writers, network connections, files, and channels.

To use this syntax you place the allocation of the object as a parameter to the try keyword. The Java complier will automatically generate the finally block and call close for you.

public void tryWithResources() throws IOException {
    // Close is automatically called at the end of the try block
    try (FileInputStream input = new FileInputStream("test.txt")) {
        System.out.println(input.read());
    }
}

Where to Use catch and throws

A significant part of exception handling is deciding where to handle exceptions. When an exception is thrown, at each level of the execution stack, you can either catch the error or use throws to pass it to the next level up. Consider what needs to happen when the exception is thrown. For example, do I want my program to halt? Does a message need to be sent back to the user? Which part of my program can do that?

A good rule of thumb is to ask: after I'm done handling this exception, where can I resume normal execution? For example, a login screen might have a UI layer, which calls a login service layer, which calls a database layer. If someone tries to login with an incorrect username, the database throws an error: user not found. The login service layer can't continue its normal execution, so it throws the error to the UI layer. However, the UI layer knows how to display an "invalid username" message, so it can catch the exception and resume.

Sometimes a layer cannot handle an exception, but does have additional information about what went wrong. Then you can catch the exception and simply throw another exception, possibly of a different type, that contains that information. For example, the database layer from the previous example might throw a ValueNotFoundException, but the login service layer knows it's really an InvalidCredentialsException, so it catches and re-throws a more useful exception type.

Exceptions Should be Exceptional

Remember that exceptions should be exceptional. Do not throw exceptions for things that happen in the normal flow of your code. For example, if it is expected that sometimes a file may not be found, then that is not exceptional. Also do not throw exceptions to return values from a function. For example, a token parser should not throw exceptions in order to return tokens that it parses to anyone with a catch block.

Using exceptions for non-exceptional cases makes debugging much more difficult and creates unexpected side effects in your code that make it less maintainable.

Things to Understand

  • The difference between checked and unchecked exceptions in Java
  • How and when to handle an exception in Java
  • How and when to throw an exception in Java
  • How to create custom exception classes
  • How to use try/catch blocks
  • What finally blocks are and how to use them

Videos (40:07)

Demonstration code

📁 ExceptionRethrowingExample

📁 ExceptionThrowingExample

📁 FileReadingWithExceptions

📁 FileReadingWithoutExceptions

📁 FinallyExample

📁 ImageEditorException

📁 TryCatchExample

📁 TryWithResourcesExample