JDK-8272715 : Incorrect attribution of method invocations of Object methods on interfaces
  • Type: CSR
  • Component: tools
  • Sub-Component: javac
  • Priority: P4
  • Status: Closed
  • Resolution: Approved
  • Fix Versions: 18
  • Submitted: 2021-08-19
  • Updated: 2022-12-19
  • Resolved: 2021-10-01
Related Reports
CSR :  
Description
Summary
-------

javac is producing suspicious results for code like (note `toString` is a mirror of a `java.lang.Object` method, the suspicious results only happen for methods that are mirror methods from `java.lang.Object`):

    interface I {
        public String toString();
        public String test();
    }
    interface J extends I {}


The suspicious results are twofold:

 * `Trees.isAccessible(..., I.toString, J)` (i.e. accessibility check for `I.toString` as a member of `J`, in some scope/context) returns false. But `I.toString` is an inherited member of `J` and should be accessible under normal circumstances.
 * invocations like `J j = null; j.toString();` will be attributed as referring to `java.lang.Object.toString`, rather than to `I.toString` (`J.toString` in classfile). This is observable either through the `Trees.getElement` API, or in the classfile

In both cases, this is different from behavior where one would use `test` instead of `toString`.

Problem
-------

As seen in the Summary section, javac is producing some suspicious results. A significant contributing factor is that javac's internal model uses `java.lang.Object` as the supertype of interface types.

Solution
--------

The proposal is for javac's behavior w.r.t. methods mirroring `java.lang.Object` methods and superinterface methods to be consistent.

Specification
-------------

The proposal is that, in the above example:

 * `Trees.isAccessible(..., I.toString, J)` will return true
 * invocations of `java.lang.Object` methods on interfaces in the classfile will use `invokeinterface` referring to the interface, which is consistent with JLS 9.2. This will be done regardless of whether the interface declares the method explicitly or not.

Comments
Here is a fun difference between JDK 17 and JDK 19 which is I think caused by this change: Consider a class ``` class A { static Comparable<?> staticField; static void check() { staticField.getClass(); } } ``` and assume now that `staticField` stores an instance of `A`. This is allowed because interface types are not checked by the verifier, i.e., a field that has the declared type of an interface can hold any Object. Now when you execute `check` on JDK 17, it works because the invocation uses a `invokevirtual` bytecode to invoke `Object.getClass`. But on JDK 19, executing it throws an `IncompatibleClassChangeError` because now the invocation uses a `invokeinterface` bytecode to invoke `Comparable.getClass` - and since `A` does not implement `Comparable` the interface method resolution must throw the exception.
19-12-2022

Reports of additional behavioral compatibility impacts: https://mail.openjdk.java.net/pipermail/compiler-dev/2022-April/019526.html
03-05-2022

I was concerned about an interaction with default methods, but interfaces are not allowed to have default methods for the objects in string. Moving to Approved.
01-10-2021

Considering the following declarations: ``` interface I { public String toString(); } interface J extends I {} interface K {} I i = ...; J j = ...; K k = ...; ``` and the following code: ``` i.toString(); j.toString(); k.toString(); ``` JDK 17 will produce classfile with the following instructions: ``` i.toString(); => invokeinterface #7, 1 // InterfaceMethod I.toString:()Ljava/lang/String; j.toString(); => invokevirtual #13 // Method java/lang/Object.toString:()Ljava/lang/String; k.toString(); => invokevirtual #13 // Method java/lang/Object.toString:()Ljava/lang/String; ``` With the change proposed here, the classfile will contain: ``` i.toString(); => invokeinterface #7, 1 // InterfaceMethod I.toString:()Ljava/lang/String; j.toString(); => invokeinterface #13, 1 // InterfaceMethod J.toString:()Ljava/lang/String; k.toString(); => invokeinterface #16, 1 // InterfaceMethod K.toString:()Ljava/lang/String; ``` I.e. all the references will be to the interface methods, rather than to the `j.l.Object` methods. The new state is consistent with JLS 9.2, which says: > If an interface has no direct superinterfaces, then the interface implicitly declares a public abstract member method m with signature s, return type r, and throws clause t corresponding to each public instance method m with signature s, return type r, and throws clause t declared in Object, unless an abstract method with the same signature, same return type, and a compatible throws clause is explicitly declared by the interface. I.e. calling `toString()` on the interface `K` from the example is not really calling the method on java.lang.Object, but it is calling a method on the interface. To the best of my knowledge, there's no change in what code is executed, because it is always some concrete method, normally from the runtime object that implements the given interface. There could possibly be a difference in performance between `invokevirtual` and `invokeinterface`, but it didn't seem to be the case for peak performance in tests done. There may be visible effects in the javap output, and when analyzing the bytecode. Core reflection or annotation processing do not allow (I believe) access to bytecode instructions/expressions, so there should be no impact there. There is a change similar to the one in bytecode for the Trees API, where for `Trees.getElement`, the existing responses are: ``` i.toString(); => I.toString() j.toString(); => java.lang.Object.toString() k.toString(); => java.lang.Object.toString() ``` With the proposed change the responses are: ``` i.toString(); => I.toString() j.toString(); => I.toString() k.toString(); => java.lang.Object.toString() ``` (Note that there is no `K.toString()` in the model that could be returned for the `k.toString()` cases.) The proposal is to apply this change for all source and target levels.
21-09-2021

Moving to Provisional, not Approved. [~jlahoda], I want to make sure I understand the scope of the change before approving the request. When an interface has an exact override of a method from Object, in various ways the method can improperly appear to originate from Object as opposed to Object. Is that correct? Does it make a difference is the interface method is a default method? If the behavioral compatibility is limited to actually executing code, than this change should have no observable impact; right? However, for various kinds of reflective operations (examining javap output, tree API, annotation processing elements API, core reflection(?), ... any thing else?) after this fix the method will be associated with the declaring interface and not object. Thanks. PS And this change would be in effect for all -source/--release levels and not just -source/--release 18.
21-09-2021