In the last post I evaluated the benefits of using switch statements on string literals over nested if statements. Today, I am going to do the same for multi-catch blocks, also a recent addition to the JLS. This feature is formally described in the JLS 7, chapter 14.20 at http://docs.oracle.com/javase/specs/jls/se7/html/jls-14.html#jls-14.20.
Short Summary
Use multi-catch blocks everywhere you can, it reduces class file size, will reduce resident memory in the JVM when the class gets loaded, and most importantly, make the code easier to understand.
Impact on Code Readability
I produced a refactoring with my eclipse plugin that introduced multi catch blocks in all appropriate places in the entire OpenJDK 7 code base, producing a change set spanning 230 files, each with up to 5 combined catch expressions. As I am reviewing the changes for submission to the Openjdk team, it takes me 10 to 20 seconds to check that all catch blocks have the same sequence of statements, which is immediately obvious in the multi-catch construct.
package p;
import java.lang.reflect.InvocationTargetException;
public class C {
If we assume $120 per developer hour and 20 seconds to understand that all catch blocks are identical, that's will set you back 66 cents every time a developer has to comprehend that code snippet.
A complication I faced when writing the Eclipse refactoring is that it is not sufficient to identify identical blocks, and simply combine the Exception into one catch clause. Consider this code snippet, for example:
1 try{
2 // some File Operation here
3 } catch (FileNotFoundException e) {
4 } catch( IOException e) {
5 }
Combining lines 3 and 4 into
3 } catch (FileNotFoundException | IOException e) {
produces a compile error, since FileNotFoundException is derived from IOException, and the latter is all that needs to be handled. So in some cases, the multi-catch refactoring produces simple traditional catch clauses with one exception only. The example above will end up reading like this, which was likely to be the original intend anyway:
1 try{
2 // some File Operation here
3 } catch( IOException e) {
4 }
A complication I faced when writing the Eclipse refactoring is that it is not sufficient to identify identical blocks, and simply combine the Exception into one catch clause. Consider this code snippet, for example:
1 try{
2 // some File Operation here
3 } catch (FileNotFoundException e) {
4 } catch( IOException e) {
5 }
Combining lines 3 and 4 into
3 } catch (FileNotFoundException | IOException e) {
produces a compile error, since FileNotFoundException is derived from IOException, and the latter is all that needs to be handled. So in some cases, the multi-catch refactoring produces simple traditional catch clauses with one exception only. The example above will end up reading like this, which was likely to be the original intend anyway:
1 try{
2 // some File Operation here
3 } catch( IOException e) {
4 }
Impact on Class File Size
What is the minimum benefit of using multi catch in term of class file size? The following code snippet uses reflection as an example of an API that relies heavily on checked exceptions, and would benefit most.package p;
import java.lang.reflect.InvocationTargetException;
public class C {
public static void main(String[] args) {
try {
C.class.getDeclaredMethod("main", null).invoke(null, null);
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
}
}
}
Eclipse JDT produces the following byte code:
// Method descriptor #15 ([Ljava/lang/String;)V
// Stack: 3, Locals: 2
public static void main(java.lang.String[] args);
0 ldc <Class p.C> [1]
2 ldc <String "main"> [16]
4 aconst_null
5 invokevirtual java.lang.Class.getDeclaredMethod(java.lang.String, java.lang.Class[]) : java.lang.reflect.Method [17]
8 aconst_null
9 aconst_null
10 invokevirtual java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object[]) : java.lang.Object [23]
13 pop
14 goto 18
17 astore_1
18 return
Exception Table:
[pc: 0, pc: 14] -> 17 when : java.lang.NoSuchMethodException
[pc: 0, pc: 14] -> 17 when : java.lang.SecurityException
[pc: 0, pc: 14] -> 17 when : java.lang.IllegalAccessException
[pc: 0, pc: 14] -> 17 when : java.lang.IllegalArgumentException
[pc: 0, pc: 14] -> 17 when : java.lang.reflect.InvocationTargetException When using separate catch blocks, the amount of code increases by almost 50%, since the compiler doesn't detect that it produced the same code sequence for each catch block. This is the least benefit case, if the blocks are actually duplicate, the saving is much higher.
// Method descriptor #15 ([Ljava/lang/String;)V
// Stack: 3, Locals: 2
public static void main(java.lang.String[] args);
0 ldc <Class p.C> [1]
2 ldc <String "main"> [16]
4 aconst_null
5 invokevirtual java.lang.Class.getDeclaredMethod(java.lang.String, java.lang.Class[]) : java.lang.reflect.Method [17]
8 aconst_null
9 aconst_null
10 invokevirtual java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object[]) : java.lang.Object [23]
13 pop
14 goto 34
17 astore_1
18 goto 34
21 astore_1
22 goto 34
25 astore_1
26 goto 34
29 astore_1
30 goto 34
33 astore_1
34 return
Exception Table:
[pc: 0, pc: 14] -> 17 when : java.lang.IllegalAccessException
[pc: 0, pc: 14] -> 21 when : java.lang.IllegalArgumentException
[pc: 0, pc: 14] -> 25 when : java.lang.reflect.InvocationTargetException
[pc: 0, pc: 14] -> 29 when : java.lang.NoSuchMethodException
[pc: 0, pc: 14] -> 33 when : java.lang.SecurityException
The moral of the story is: multi-catch blocks are not only syntactic sugar that result in the byte code, it's a feature that helps both the developer and the compiler to reduce code. Finally an optimization without a trade-off!
C.class.getDeclaredMethod("main", null).invoke(null, null);
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
}
}
}
Eclipse JDT produces the following byte code:
// Method descriptor #15 ([Ljava/lang/String;)V
// Stack: 3, Locals: 2
public static void main(java.lang.String[] args);
0 ldc <Class p.C> [1]
2 ldc <String "main"> [16]
4 aconst_null
5 invokevirtual java.lang.Class.getDeclaredMethod(java.lang.String, java.lang.Class[]) : java.lang.reflect.Method [17]
8 aconst_null
9 aconst_null
10 invokevirtual java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object[]) : java.lang.Object [23]
13 pop
14 goto 18
17 astore_1
18 return
Exception Table:
[pc: 0, pc: 14] -> 17 when : java.lang.NoSuchMethodException
[pc: 0, pc: 14] -> 17 when : java.lang.SecurityException
[pc: 0, pc: 14] -> 17 when : java.lang.IllegalAccessException
[pc: 0, pc: 14] -> 17 when : java.lang.IllegalArgumentException
[pc: 0, pc: 14] -> 17 when : java.lang.reflect.InvocationTargetException When using separate catch blocks, the amount of code increases by almost 50%, since the compiler doesn't detect that it produced the same code sequence for each catch block. This is the least benefit case, if the blocks are actually duplicate, the saving is much higher.
// Method descriptor #15 ([Ljava/lang/String;)V
// Stack: 3, Locals: 2
public static void main(java.lang.String[] args);
0 ldc <Class p.C> [1]
2 ldc <String "main"> [16]
4 aconst_null
5 invokevirtual java.lang.Class.getDeclaredMethod(java.lang.String, java.lang.Class[]) : java.lang.reflect.Method [17]
8 aconst_null
9 aconst_null
10 invokevirtual java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object[]) : java.lang.Object [23]
13 pop
14 goto 34
17 astore_1
18 goto 34
21 astore_1
22 goto 34
25 astore_1
26 goto 34
29 astore_1
30 goto 34
33 astore_1
34 return
Exception Table:
[pc: 0, pc: 14] -> 17 when : java.lang.IllegalAccessException
[pc: 0, pc: 14] -> 21 when : java.lang.IllegalArgumentException
[pc: 0, pc: 14] -> 25 when : java.lang.reflect.InvocationTargetException
[pc: 0, pc: 14] -> 29 when : java.lang.NoSuchMethodException
[pc: 0, pc: 14] -> 33 when : java.lang.SecurityException
The moral of the story is: multi-catch blocks are not only syntactic sugar that result in the byte code, it's a feature that helps both the developer and the compiler to reduce code. Finally an optimization without a trade-off!