Thursday, 30 April 2015

Cheating with Exceptions - Java 8 Lambdas

Leaving aside the religious debate about Checked vs Runtime exceptions, there are times where due to poorly constructed libraries, dealing with checked examples can drive you insane.

Consider this snippet of code which you might want to write:

public void createTempFileForKey(String key) {
  Map<String, File> tempFiles = new ConcurrentHashMap<>();
  //does not compile because it throws an IOException!!
  tempFiles.computeIfAbsent(key, k -> File.createTempFile(key, ".tmp"));
}

For it to compile you need to catch the exception which leaves you with this code:

public void createTempFileForKey(String key) {
    Map<String, File> tempFiles = new ConcurrentHashMap<>();
    tempFiles.computeIfAbsent(key, k -> {
        try {
            return File.createTempFile(key, ".tmp");
        }catch(IOException e) {
            e.printStackTrace();
            return null;
        }
    });
}

Although this compiles, the IOException has effectively been swallowed.  The user of this method should be informed that an Exception has been thrown.

To address this you could wrap the IOException in a generic RuntimeException as below:

public void createTempFileForKey(String key) throws RuntimeException {
    Map<String, File> tempFiles = new ConcurrentHashMap<>();
    tempFiles.computeIfAbsent(key, k -> {
        try {
            return File.createTempFile(key, ".tmp");
        }catch(IOException e) {
            throw new RuntimeException(e);
        }
    });
}

This code does throw an Exception but not the actual IOException which was intended to be thrown by the code. It's possible that those in favour of RuntimeExceptions only would be happy with this code especially if the solution could be refined to created a customised IORuntimeException.  Nevertheless the way most people code, they would expect their method to be able to throw the checked IOException from the File.createTempFile method.   

The natural way to do this is a little convoluted and looks like this:

public void createTempFileForKey(String key) throws IOException{
        Map<String, File> tempFiles = new ConcurrentHashMap<>();
        try {
            tempFiles.computeIfAbsent(key, k -> {
                try {
                    return File.createTempFile(key, ".tmp");
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        }catch(RuntimeException e){
            if(e.getCause() instanceof IOException){
                throw (IOException)e.getCause();
            }
        }
}

From inside the lambda, you would have to catch the IOException, wrap it in a RuntimeException and throw that RuntimeException. The lambda would have to catch the RuntimeException unpack and rethrow the IOException. All very ugly indeed!

In an ideal world what we need is to be able to do is to throw the checked exception from within the lambda without having to change the declaration of computeIfAbsent. In other words, to throw a check exception as if it were an runtime exception. But unfortunately Java doesn't let us do that...

That is not unless we cheat! Here are two methods for doing precisely what we want, throwing a checked exception as if it were a runtime exception.

Method 1 - Using generics:

    public static void main(String[] args){
        doThrow(new IOException());
    }

    static void doThrow(Exception e) {
        CheckedException.<RuntimeException> doThrow0(e);
    }

    static <E extends Exception>
      void doThrow0(Exception e) throws E {
          throw (E) e;
    }

Note that we have create and thrown an IOException without it being declared in the main method.

Method 2 - Using Unsafe:

public static void main(String[] args){
        getUnsafe().throwException(new IOException());
    }

    private static Unsafe getUnsafe(){
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            return (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }

Again we have managed to throw an IOException without having declared it in the method.

Whichever method you prefer we are now free to write the original code in this way:

public void createTempFileForKey(String key) throws IOException{
        Map<String, File> tempFiles = new ConcurrentHashMap<>();

        tempFiles.computeIfAbsent(key, k -> {
            try {
                return File.createTempFile(key, ".tmp");
            } catch (IOException e) {
                throw doThrow(e);
            }
        });
    }
    
    private RuntimeException doThrow(Exception e){
        getUnsafe().throwException(e);
        return new RuntimeException();
    }

    private static Unsafe getUnsafe(){
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            return (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }

The doThrow() method would obviously be encapsulated in some utility class leaving your code in createTempFileForKey() pretty clean.

5 comments:

  1. Lombok even has an annotation for this trick:
    http://projectlombok.org/features/SneakyThrows.html

    ReplyDelete
  2. Quite nice trick, but you still have to return null from the computeIfAbsent method.

    ReplyDelete
  3. This is an awesome post. Really very informative and creative contents. This concept is a good way to enhance the knowledge. Excellent post.
    Are you looking for the best Java training institute in Chennai?
    Come and learn with Aorta, the best java training institute in Chennai offering the best platform to learn and get the depth of Java.
    java training in chennai
    java course in chennai

    ReplyDelete
  4. Our digital marketing course in Chennai is targeted at those who are desirous of taking advantage of career opportunities in digital marketing. Marketing professionals who are presently using traditional marketing to meet their business objectives. Start-ups, Entrepreneurs, Business Owners who desire to make use of online media to improve their business goals and objectives.

    Digital Marketing Course in Chennai
    Digital Marketing Training in Chennai
    Online Digital Marketing Training
    SEO Training in Chennai

    ReplyDelete