Mysterious line in stack trace

2020-07-06 02:13发布

问题:

While investigating a stack trace discrepancy when composing another answer, I came across a behavior I do not understand. Consider the following test program (this is as far down as I could narrow it):

interface TestInterface <U> {
    void test (U u);
}

static class Test <T extends Test<T>> implements TestInterface<T> { // line 11
    @Override public void test (T t) {
        throw new RuntimeException("My exception"); // line 13
    }
}

static class TestA extends Test<TestA> { }
static class TestB extends Test<TestB> { }

public static void main (String[] args) throws Exception {

    try {
        Test a = new TestA();
        Test b = new TestB();
        a.test(b);        
    } catch (Exception x) {
        x.printStackTrace(System.out);
    }

    try {
        TestInterface a = new TestA();
        Test b = new TestB();
        a.test(b);        
    } catch (Exception x) {
        x.printStackTrace(System.out);
    }

    try {
        TestInterface a = new TestA();
        TestInterface b = new TestB();
        a.test(b);        
    } catch (Exception x) {
        x.printStackTrace(System.out);
    }

}

Lines 11 and 13 are labelled in the above snippet and it can be run on ideone. The output of that program is:

java.lang.RuntimeException: My exception
    at Ideone$Test.test(Main.java:13)
    at Ideone.main(Main.java:25)
java.lang.RuntimeException: My exception
    at Ideone$Test.test(Main.java:13)
    at Ideone$Test.test(Main.java:11)
    at Ideone.main(Main.java:33)
java.lang.RuntimeException: My exception
    at Ideone$Test.test(Main.java:13)
    at Ideone$Test.test(Main.java:11)
    at Ideone.main(Main.java:41)

My question is: Why is line 11 in the stack trace for the second and third test cases? The difference between the three test cases there are the declared types of a and b.

Line 11 (the class declaration line) is only present under the following conditions:

  1. If Test implements an interface, and
  2. If the exception is thrown from the interface method, and
  3. If the interface takes a type parameter, and
  4. If the class declaration's type parameter contains extends Test<T> (line 11 is not included if it is declared as class Test<T>), and
  5. If the method is called on the TestInterface type rather than the Test type.

Noting that:

  • It is definitely my exception being thrown (message and stack trace).
  • No other exceptions are thrown if I do not throw mine.
  • I have reproduced this with the Oracle JDK 1.7, and 1.8 on Windows, and 1.8 on Ideone. However, 1.7 includes a stack trace element on line 1 instead of 11 (which is doubly weird).

What is happening here? How is that line ending up in the stack trace and why does it not appear if both objects are declared as Test?

Here is the original program that prompted this, where line 55 of java.lang.Enum is present if a is declared as Comparable but not present when it is declared as Enum. Line 55 is the declaration of Enum in the JDK source, line 180 is an explicitly thrown ClassCastException.

回答1:

You're looking at the effects of a bridge method!

The test method declared in TestInterface has erasure test(Object), but the test method declared in Test has erasure test(Test). A method lookup for the test(Object) method won't find the test(Test) method, so Java actually puts separate test(Object) and test(Test) methods in Test's bytecode.

Your first trial uses the test(Test) method, which behaves as you expected. Your other trials use the test(Object) method, which is a synthetic bridge method that just calls the test(Test) method. This bridge method doesn't really have a line number, so it shows up in the stack trace with the fairly arbitrary line number of 11.