2

When compiling a Client, which uses some implementation of interface I (e.g. O), the classfile for I must also be present on the classpath. What is strange, this is only a case for javac, as Eclipse compiler (ECJ) does not require I for compilation.

What makes JDK requires supertypes for compilation, where ECJ compiles just fine?

It is not default methods, as was commented in the bug report, and the compatibility guide also agrees:

When compiling a class against another class implementing an interface which is defined in yet another class file, such class file (where interface is defined) must be available in the class path used by javac during compilation. This is a new requirement as of JDK 8 - a failure to do so will result in a compilation error.


UPDATE:

  • Similar question: Java 8 interface/class loader changes?
  • It doesn't matter if I.doit() is default or plain abstract method, the behavior is the same
  • It of course matters whether I.doit() is overridden in O or not; if not overriden, then ECJ also reaches to I for the definition of doit()

The interface (api/a/I.java):

package a;
public interface I {
    default void doit() {
        System.out.println("In I");
    }
}

Implementation (impl/b/O.java):

package b;
public class O implements a.I {
    public void doit() {
        System.out.println("In O");
    }
}

Client (client/c/Client.java):

package c;
import b.O;
public class Client {
    public void test() {
        O o = new O();
        o.doit();
    }
    public static void main(String[] args) {
        new Client().test();
    }
}

A Makefile:

# bug report:
#   Javac requires interface on classpath when using impl
#   https://bugs.openjdk.java.net/browse/JDK-8055048
#
# compatibility guide:
#   http://www.oracle.com/technetwork/java/javase/8-compatibility-guide-2156366.html
#   (Synopsis: Interfaces need to be present when compiling against their implementations)
# 
# ECJ downloaded from:
#   http://central.maven.org/maven2/org/eclipse/jdt/core/compiler/ecj/4.6.1/ecj-4.6.1.jar

ifeq (${V}, ecj)
JC := java -jar ecj-4.6.1.jar -8
else
JC := javac -source 1.8 -target 1.8 -implicit:none
endif

rebuild: clean lib client

lib: api/a/I.class impl/b/O.class

client: lib client/c/Client.class

clean:
    rm -f api/a/I.class impl/b/O.class client/c/Client.class

%.class: %.java
    ${JC} ${OPT} $<

impl/b/O.class: OPT = -cp api
client/c/Client.class: OPT = -cp impl

A log:

$ make V=ecj rebuild                                                                                                                                                                                               
rm -f api/a/I.class impl/b/O.class client/c/Client.class
java -jar ecj-4.6.1.jar -8  api/a/I.java
java -jar ecj-4.6.1.jar -8 -cp api impl/b/O.java
java -jar ecj-4.6.1.jar -8 -cp impl client/c/Client.java

$ make rebuild
rm -f api/a/I.class impl/b/O.class client/c/Client.class
javac -source 1.8 -target 1.8 -implicit:none  api/a/I.java
javac -source 1.8 -target 1.8 -implicit:none -cp api impl/b/O.java
javac -source 1.8 -target 1.8 -implicit:none -cp impl client/c/Client.java
client/c/Client.java:8: error: cannot access I
                o.doit();
                 ^
  class file for a.I not found
1 error
make: *** [client/c/Client.class] Error 1
pwes
  • 2,040
  • 21
  • 30
  • Are you sure, that in the case of ECJ it's not on the classpath? – D. Kovács Jul 11 '17 at 09:12
  • Yes, see the log. This is a separated test case from much more complicated case, where Eclipse does just fine compiling without transitive dependencies, while javac (run from Gradle CI script) complains. – pwes Jul 11 '17 at 09:15
  • I would like to see what ECJ does when you remove `doit()` from class `O` but leave everything else unchanged.... – piet.t Jul 11 '17 at 09:20
  • Probable ECJ bug actually. – D. Kovács Jul 11 '17 at 09:20
  • @piet.t Then ECJ throws `The type a.I cannot be resolved. It is indirectly referenced from required .class files`. Which makes sense, since now it must know whether there is `doit()` method in a inheritance chain – pwes Jul 11 '17 at 09:36
  • @D.Kovács And how this bug manifests itself in compilation result? What is a breaking case in your opinion? – pwes Jul 11 '17 at 09:37
  • 2
    This compatibility guide describes the *actual behavior* of `javac`, it is *not* a specification about how compilers have to work in general. Not that the change behavior of `javac` [has been noticed already](https://stackoverflow.com/q/26799874/2711488). – Holger Jul 11 '17 at 09:47
  • @pwes one would have to dig into the JLS and JVMSpec and see which behaviour is actually according to spec. If ECJ is not according to spec (that's my hunch, **but it's only a hunch**), then it's a bug in ECJ. A bug is not necessarily is breaking behaviour, but it can also be simply "not conforming to spec". – D. Kovács Jul 11 '17 at 09:59
  • @D.Kovács In linked docs JLS and JVM are never mentioned, only *a tool*, `javac`, is described as more restrictive than ECJ, or handles a case which ECJ doesn't and it should (and I can't see it), in which case it is indeed a bug in ECJ – pwes Jul 11 '17 at 10:29
  • @pwes I haven't got an idea about the doc you linked. The JLS and JVM Spec are the specification which define what a compiler must, must not, could do. Everything else is circumstantial. There are some possibilities: - javac is correct, this is a feature, derived from the JLS/JVMs (ECJ bug) - javac is incorrect, the feature should be implemented (bug in javac) - javac is lazy, the feature is optional (both compilers are according to spec, no bug) – D. Kovács Jul 11 '17 at 11:29
  • In any case, it gives me a headache on CI Gradle builds of OSGi bundles, when in Eclipse compilation was fine. :) – pwes Jul 11 '17 at 11:32

2 Answers2

6

There seems to be a misunderstanding on the purpose of the Compatibility Guide for JDK 8.

This is not a specification about how a compiler or environment should behave, it’s a document about how the JDK does behave, to spot potential compatibility problems. This does not imply that another compiler has to exhibit exactly the same behavior.

The reason why it mentions that particular behavior, is, because javac changed its behavior from JDK 7 to JDK 8 and this can cause compatibility problems.

As explained here, the formal process is described as searching for all potentially applicable member methods for a method invocation, but it doesn’t say that short-cuts aren’t allowed when the correctness of a program can be guaranteed.

So that bug report has been closed, because the new behavior is within the specification, not necessarily because an alternative behavior would violate it.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Hm, looking for "Potentially Applicable Methods" seem like a good point. However, I'm to weak in JLS/JVM to find a case where it matters, i.e. to find a case where it is desirable to hava `javac` enforcement instead of ECJ's reluctance and report it as a bug in ECJ – pwes Jul 11 '17 at 10:22
  • 5
    I consider both behaviors compliant with the specification. I think, the main reason to implement it just like the formal specification, without any shortcuts, is to keep the compiler’s code simpler, as the whole process is quiet complex. If the ECJ developers think, they still can maintain the code with supporting that shortcut, they may continue to do so. In either case, we should never consider the presence of such shortcuts to be a guaranteed behavior. – Holger Jul 11 '17 at 10:28
3

What if O does not override doit()?

Then Client must still be able to call doit() since it's part of I's contract, but this informatin is missing in O's class-file.

You might ask "Why not include the default-methods definition in O's class-file?". This would break the intent of introducing default-methods in the first place: Classes compiled with pre-Java-8 compilers should still work in Java 8 and the interfaces new methods should be available.

piet.t
  • 11,718
  • 21
  • 43
  • 52
  • As I written in the question, it's not a case with default methods only. Remove the default method, and the different behavior of ECJ and javac still occurs. Also, because the information about `doit()` is present in `O`, there is no need in checking in `I`. – pwes Jul 11 '17 at 10:19