13

I have the follwing Java 9 module:

module com.example.a {
    exports com.example.a;
}

With an exported type:

public class Api {

    public static void foo(ImplDetail args) {}
}

And a non-exported type:

package com.example.b.internal;

public class ImplDetail {}

The exported type uses the non-exported type as a method parameter type in a public method. I'd have assumed that the compiler would reject such an inconsistent class configuration, as clients in other modules could not really invoke the foo() method as they cannot instantiate the parameter type.

To my surprise, this module is compiled successfully by javac. I can see the special case of passing null, still I'd consider such an API definition malformed and think it should not be supported, enforced by the compiler ideally.

What's the reasoning for not disallowing such case?

Gunnar
  • 18,095
  • 1
  • 53
  • 73

1 Answers1

8

Certainly using a non-exported type in an API is bad style and is quite likely to be a design error, but it's fairly clear to me that javac isn't in a position to make this be a compile-time error.

Note that it has always been possible to use a private type in a public API, going all the way back to Java 1.0.

You already noted that code outside the module can still call Api.foo(null).

There are other cases where a caller could still use this API with a non-null reference. Consider a class public class Sub extends ImplDetail in package com.example.a. This class Sub is public and is exported and so is available to code outside the module. Thus, outside code can call Api.foo(sub) using instances of Sub obtained from somewhere.

But surely, javac can tell whether there are any subtypes of ImplDetail in any exported packages, and issue a compile-time error if there aren't any? Not necessarily. Because of the possibility of separate compilation, new classes might be introduced into a module after the compilation step that includes Api. Or, for that matter, the module-info.class file could be recompiled to change the set of exported packages.

For these reasons I think it's inappropriate for javac to raise an error at the time that it compiles the Api class. Javac does have an option -Xlint:exports that will flag cases like this as a warning, however.

Something later in the build process, such as the jmod tool, or some after-the-fact module auditing tool, could also flag the use of a non-exported type being used in an exported API. I don't think anything currently does this, though.

Stuart Marks
  • 127,867
  • 37
  • 205
  • 259
  • Thanks for your detailed answer, Stuart! Deriving an exported type from a non-exported type seems equally inconsistent to me, so I'd have said that alone should warrant a compile error (for `Sub` itself). But one would probably have the same issue you describe with several compilation steps. – Gunnar Dec 17 '16 at 12:56
  • @Gunnar After considering this for a while, I realized that it's reasonable (if uncommon) to have a public class that is a subclass of a private class. In the JDK, for example, in java.lang, `StringBuffer` and `StringBuilder` are both public subclasses of `AbstractStringBuilder`, a private class. It's a bit odd to have a hierarchy SB <: ASB <: Object with an interior private class, but it's done for implementation sharing. Note however that ASB isn't exposed as a type in the API. – Stuart Marks Dec 30 '16 at 01:42
  • Thanks for the follow-up. Personally, I find such design awkward. The user of a type should be able to see the full hierarchy of a type IMO. If it's about implementation sharing, this could be done via delegation to a private shared class. – Gunnar Jan 03 '17 at 10:28
  • For those interested, I've written a [blog post](http://in.relation.to/2017/01/31/preventing-leaky-apis-with-jqassistant/) on how to prevent API definitions which expose internal types in their signatures. It's not based on Java 9, but rather on package conventions and the usage of an analysis tool for detecting any occurrence of internal types in public API signatures. – Gunnar Jan 31 '17 at 17:56