Why does Kotlin prohibit exposing restricted-visibility types?

Example:

interface Foo

internal abstract class AbstractFoo

class SomeFoo : AbstractFoo()

Does not compile:

‘public’ subclass exposes its ‘internal’ supertype AbstractFoo

Why exactly is this a problem?

OK, so maybe it’s because AbstractFoo can expose itself through its methods, e.g.:

internal abstract class AbstractFoo {
  abstract val self: AbstractFoo
}

But then, why is this a problem? The type can still be inaccessible to the caller from another module: they won’t be able to use self as an AbstractFoo, but they will simply see it as… some supertype that is visible. For example, Foo. OK, that might be hard to implement in Kotlin because it could implement many interfaces, and Kotlin doesn’t have union types; fine, let’s say it will be the closest visible superclass, or Any.

Any variant is, IMO, better than disallowing this kind of code altogether: these variants don’t violate access rules, and the properties like self aren’t expected to be useful outside the module anyway.

I think this question is roughly the same as:

Kotlin compiles this AbstractFoo class as the following:

public abstract class AbstractFoo {
   public abstract AbstractFoo getSelf();
}

So as you can see it becomes public since Java doesn’t have internal modifier. i.e. the class exposes itself from the Java-to-Kotlin viewpoint. Which in some way is a violation of access rules.

However, it’s only my guess.

No, I don’t think so. The quoted question proposes restricted visibility members of interfaces, which is a different thing. This question ponders why Kotlin prohibits from exposing an internal type: wha’s exactly the problem here? The object in question can be shared anyway via a reference to any visible supertype, so why can’t Kotlin do the same in this situation?

The reason is this. Kotlin’s default visibility is public, and the goal is that you don’t have to pollute your internal and private classes with explicit visibility modifier. Declaring your class as private or internal should be enough to hide it from the outside world. Now, if this class happens to be open and you inherit from it a public class, then you accidentally leak details that were originally supposed to be private/internal to the outside world. This is error-prone (hard to notice) and that is what the above restriction is designed to fix.

It does cause some practical problems, so additional mechanisms are needed, like the one explained here: https://youtrack.jetbrains.com/issue/KT-22396

2 Likes

Thanks for the explanation, Roman! I must admit that’s a good reason. Voting for your ticket (though the annotation name seems a bit counterintuitive to me)

Roman, it might seem little off topic but, what do you think about the idea to allow internal and protected members of the interface through the same mechanism you suggest in 22396 issue.
It’s explained in this issue and this topic.

For example, some framework can define the interface as below:

interface EventListener {
	@PublishedApi protected fun onEvent(e: Event)
	@PublishedApi internal fun fireEvent(e: Event) { onEvent(e)}
}

And a developer can implement it like so:

class UiController : EventListener {
	override fun onEvent(e: Event) { TODO("some implementation")}
	...
}

Now the framework can call fireEvent method on UiController instance. Which will in its turn call onEvent.
An advantage of this usage is that wherever the UiController is exposed its onEvent method is inaccessible. As the result, we have more strong encapsulation.

There are some JVM level limitations (especially for bytecode below 1.8), but with default methods it does make sense to have additional non-public methods there (you can even require a method in implementers that is only used through the default method).