Skip to content

Safety Guide

zleonov edited this page Dec 7, 2023 · 2 revisions

WARNING

Unchecked Java circumvents Java's exception handling mechanisms and can lead to horrible, often very hard to debug errors, when misused.

In the words of Brian Goetz (Java Language Architect):

Warning

Just because you don’t like the rules, doesn’t mean its a good idea to take the law into your own hands. Your advice is irresponsible because it places the convenience of the code writer over the far more important considerations of transparency and maintainability of the program.

How to use Unchecked Java safely

First, and I can't believe I am saying this, if you are wondering whether you should use Unchecked Java, you are probably safer off without it.

If you are going to use it, you have to ensure that the caller will catch all possible checked exceptions that you are circumventing with Unchecked Java.

Consider this method which calculates the total size of a list of files:

public static long getSize(final List<Path> files) {

    final long size = files.stream().mapToLong(file -> {
        try {
            return Files.size(file);
        } catch (final IOException cause) {
            throw new RuntimeException("Unable to get file size of " + file, cause);
        }
    }).sum();

    return size;
}

Notice that you have no choice but to rethrow the IOException wrapped in a RuntimeException because the underlying ToLongFunction interface does not throw checked exceptions.

With Unchecked Java we can make our code much more readable:

import static software.leonov.common.util.function.CheckedToLongFunction.unchecked;

...

public static long getSize(final List<Path> files) { // does not throw any checked exceptions
    return files.stream().mapToLong(unchecked(Files::size)).sum();
}

But what you don't realize is that we just put the users of our API (and the JVM) in an impossible position. The signature of the getSize method does not include an IOException. There is no reason for the caller to assume or prepare for the possibility that an I/O error of some sort can occur. This can be easily remedied by adding a throws clause to our method:

public static long getSize(final List<Path> files) throws IOException { // fixed
    return files.stream().mapToLong(unchecked(Files::size)).sum();
}

In some contexts it may make sense to throw a super exception:

public static long getSize(final List<Path> files) throws Exception { // throws Exception instead of IOException
    return files.stream().mapToLong(unchecked(Files::size)).sum();
}

Unchecked Java and code smell

Most good APIs and libraries attempt to prevent their users from shooting themselves in the foot. But in the hands of a reckless developer Unchecked Java can make it easier to write bad code.

Suppose you are writing a method that stores the results of an SQL select query to a file. It becomes your responsibility to close all the all streams and resources (Connections, Statements, ResultSets, and etc...) before the method returns. A naïve user may decide to abuse Unchecked Java and simply propagate all checked exceptions as if they were unchecked, neglecting proper handling I/O resources.

Even if the method signature includes the necessary throws clause, careless use of Unchecked Java can result in risky and problematic code.

When to use Unchecked Java?

If you are an experienced Java developer I hope you are beginning to see that correctly using Unchecked Java can lead to a substantial reduction of boiler plate code while significantly enhancing readability.

I recently used Unchecked Java while writing long running tasks involving a dynamic (not known until runtime) number of I/O operations submitted to a ThreadPoolExecutor. Remember that you are guaranteed that any possible errors which occur in a Runnable or Callable task will be stored in the returned Future object.

With Unchecked Java I was able to significantly reduce the number of nested try/catch blocks and remove the surplus stack traces resulting from wrapping IOExceptions in RuntimeExceptions.