Question

I've come across an oddity of the JLS, or a JavaC bug (not sure which). Please read the following and provide an explanation, citing JLS passage or Sun Bug ID, as appropriate.

Suppose I have a contrived project with code in three "modules" -

  1. API - defines the framework API - think Servlet API
  2. Impl - defines the API implementation - think Tomcat Servlet container
  3. App - the application I wrote

Here are the classes in each module:

API - MessagePrinter.java

package api;

public class MessagePrinter {

    public void print(String message) {
        System.out.println("MESSAGE: " + message);
    }
}

API - MessageHolder.java (yes, it references an "impl" class - more on this later)

package api;

import impl.MessagePrinterInternal;

public class MessageHolder {

    private final String message;

    public MessageHolder(String message) {
         this.message = message;
    }

    public void print(MessagePrinter printer) {
        printer.print(message);
    }

    /**
     * NOTE: Package-Private visibility.
     */ 
    void print(MessagePrinterInternal printer) {
        printer.print(message);    
    }

}

Impl - MessagePrinterInternal.java - This class depends on an API class. As the name suggests, it is intended for "internal" use elsewhere in my little framework.

package impl;

import api.MessagePrinter;

/**
 * An "internal" class, not meant to be added to your
 * application classpath. Think the Tomcat Servlet API implementation classes.
 */
public class MessagePrinterInternal extends MessagePrinter {

    public void print(String message) {
        System.out.println("INTERNAL: " + message);
    }
}

Finally, the sole class in the App module...MyApp.java

import api.MessageHolder;
import api.MessagePrinter;

public class MyApp {

    public static void main(String[] args) {
        MessageHolder holder = new MessageHolder("Hope this compiles");
        holder.print(new MessagePrinter());
    }

}

So, now I attempt to compile my little application, MyApp.java. Suppose my API jars are exported via a jar, say api.jar, and being a good citizen I only referencd that jar in my classpath - not the Impl class shiped in impl.jar.

Now, obviously there is a flaw in my framework design in that the API classes shouldn't have any dependency on "internal" implementation classes. However, what came as a surprise is that MyApp.java didn't compile at all.

javac -cp api.jar src\MyApp.java
src\MyApp.java:11: cannot access impl.MessagePrinterInternal class file for impl.MessagePrinterInternal not found

    holder.print(new MessagePrinter());
                 ^
      1 error

The problem is that the compiler is trying to resolve the version print() to use, due to method overloading. However, the compilation error is somewhat unexpected, as one of the methods is package-private, and therefore not visible to MyApp.

So, is this a javac bug, or some oddity of the JLS?

Compiler: Sun javac 1.6.0_14

Was it helpful?

Solution

There is is nothing wrong with JLS or javac. Of course this doesn't compile, because your class MessageHolder references MessagePrinterInternal which is not on the compile classpath if I understand your explanation right. You have to break this reference into the implementation, for example with an interface in your API.

EDIT 1: For clarification: This has nothing to do with the package-visible method as you seem to think. The problem is that the type MessagePrinterInternal is needed for compilation, but you don't have it on the classpath. You cannot expect javac to compile source code when it doesn't have access to referenced classes.

EDIT 2: I reread the code again and this is what seems to be happening: When MyApp is compiled, it tries to load class MessageHolder. Class MessageHolder references MessagePrinterInternal, so it tries to load that also and fails. I am not sure that is specified in the JLS, it might also depend on the JVM. In my experience with the Sun JVM, you need to have at least all statically referenced classes available when a class is loaded; that includes the types of fields, anything in the method signatures, extended classses and implemented interfaces. You could argue that this is counter-intuitive, but I would respond that in general there is very little you do with a class where such information is missing: you cannot instantiate objects, you cannot use the metadata (the Class object) etc. With that background knowledge, I would say the behavior you see is expected.

OTHER TIPS

First off I would expect the things in the api package to be interfaces rather than classes (based on the name). Once you do this the problem will go away since you cannot have package access in interfaces.

The next thing is that, AFAIK, this is a Java oddity (in that it doesn't do what you would want). If you get rid of the public method and make the package on private you will get the same thing.

Changing everything in the api package to be interfaces will fix your problem and give you a cleaner separation in your code.

I guess you can always argue that javac can be a little bit smarter, but it has to stop somewhere. it's not human, human can always be smarter than a compiler, you can always find examples that make perfect sense for a human but dumbfound a compiler.

I don't know the exact spec on this matter, and I doubt javac authors made any mistake here. but who cares? why not put all dependencies in the classpath, even if some of them are superficial? doing that consistently makes our lives a lot easier.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top