As I was browsing through JVM 8.0 Specification, I saw the following in chapter 4.7.5. The Exceptions Attribute:
A method should throw an exception only if at least one of the following three criteria is met:
- The exception is an instance of RuntimeException or one of its subclasses.
- The exception is an instance of Error or one of its subclasses.
- The exception is an instance of one of the exception classes specified in the exception_index_table just described, or one of their subclasses.(Irina’s note: it is listed in the “throws” clause of the method)
These requirements are not enforced in the Java Virtual Machine; they are enforced only at compile time.
I decided to check how would Oracle’s JVM act in case a method throws checked exception, not listed in the “throws” clause. What I needed was a method that throws one checked exception like this:
import java.io.IOException;
public class ThrowsTest {
public void f(int a) throws IOException {
if (a < 0)
throw new IOException();
}
public static void main(String[] args) throws NumberFormatException, IOException {
new ThrowsTest().f(Integer.valueOf(args[0]));
}
}
After compiling at the command line with "javac ThrowsTest.java", one could easily test that negative command-line argument causes an IOException:
>java ThrowsTest -2
Exception in thread "main" java.io.IOException
at ThrowsTest.f(ThrowsTest.java:7)
at ThrowsTest.main(ThrowsTest.java:17)
What I would try to do is replace in the class file the construction of java.io.IOException with another checked exception - for example, java.lang.Exception, without updating the "throws" clause, and check if the JVM really throws it. To do so it would be easier if I simply change the bytes in the class file to point to an exception, which is already known to the class file. I.e. it is part of the Static Pool of the class. I am adding another method:
public void g(int a, int b) throws Exception {
if (a + b < 0) {
throw new Exception();
}
}
The key here is to have exactly the same exception constructor - in this case, I have chosen one with zero parameters.
I could simply open the ThrowsTest.class in a HEX editor. To identify the method I used also a helper tool: Java Class File Editor. In it I was able to inspect the correct indexes of the old exception and the new exception in the Constant Pool:
Then I looked at the Byte code of the method. It is located in an Attribute with name "Code" for the method void (int):
It is visible that the "new" instruction creates an instance of java.io.IOException (index 2 in the Constant Pool above) , and "invokespecial" finishes the construction by calling the constructor (index 3 in the Constant Pool above). That are the two values I need to change. The new values should be java.lang.Exception with index 4 and the constructor ()V with index 5. To identify the location in the binary file, I opened it in HEX and found the first occurrence of "invocespecial" - bytecode b7. Right after it the value was 3, now changed to 5. Two instructions back is the "new" instruction with operand 2, now changed to 4:
When I now run the same test, I have the following result:
>java ThrowsTest -2
Exception in thread "main" java.lang.Exception
at ThrowsTest.f(ThrowsTest.java:7)
at ThrowsTest.main(ThrowsTest.java:17)
Decompilation of the class with CFR - another java decompiler shows the following non-compilable code:
/*
* Decompiled with CFR 0_119.
*/
import java.io.IOException;
public class ThrowsTest {
public void f(int n) throws IOException {
if (n < 0) {
throw new Exception();
}
}
public void g(int n, int n2) throws Exception {
if (n + n2 < 0) {
throw new Exception();
}
}
public static void main(String[] arrstring) throws NumberFormatException, IOException {
new ThrowsTest().f(Integer.valueOf(arrstring[0]));
}
}
So indeed Oracle's JVM did allow an inconsistent class to run - it throws checked exception not listed in its "Exceptions" attribute of the method.