views:

187

answers:

3

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

A: 

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.

TofuBeer
Even if you changed this code to use interfaces, you could reproduce the same behavior by declaring the interface package-private. For example, MessagePrinterInternal could be an interface in the API package.In fact, the production code I based this question on does exactly that.
noahz
If the interface is package only nothing outside of the package can see it. If you only code to the interface (declare all variables and parameters as the interface) then there should be no issue.Regardless havin a package called API contain things other than interfaces, enums, factories, and exceptions does not make sense to me.
TofuBeer
Sorry though of another thing base in what you said - an API should not have package private thi fs since it is meant to be a publicly consumed thing.
TofuBeer
A: 

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.

irreputable
Some classes aren't meant to be ever be in a compilation classpath, because they are only meant to be used at runtime. For example, the Sun classes shipped with Sun Java.
noahz
for example? most sun classes are in rt.jar, which is in the classpath when you compile.
irreputable
It's commonly accepted as a worst-practice to use the com.sun classes directly:http://java.sun.com/products/jdk/faq/faq-sun-packages.html
noahz
+1  A: 

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.

FelixM
Yes, but the method that uses MessagePrinterInternal is package-private. The class MyApp.java will never be able to get access to it. So it's counter-intuitive.
noahz
It may be counter-intuitive to you, but I think he is correct. Just try it and see.
Stephen C
His answer simply restates my question. Of course the source needs to be changed in order to compile. My question: where in the JLS does it define this compilation failure as expected behavior?
noahz