1. Introduction
In this tutorial, we’ll explore the meaning and origins of the Kotlin error: ‘public’ function exposes its ‘public/*package*/’ return type. We’ll need some basic experience with Java access modifiers to deeply understand the cause.
2. Access Modifiers in Java
To understand why this error occurs, let’s have a quick refresher on Java’s access modifiers:
- public
- package-private/default
- private
- protected
They define the rules of accessing classes and their members. Now, let’s write a Java class called ImageSource:
public abstract class ImageSource {
public abstract MyImage produceImage();
static class MyImage {
}
}
And then, let’s also write another Java class, called ImageConsumer, in a different package:
public class ImageConsumer {
public void consumeImage(ImageSource is) {
ImageSource.MyImage image = is.produceImage();
}
}
So, the question is – would this code compile? Actually, no, because the MyImage class is package-private, and it cannot be used outside its package. Here, the class ImageConsuemr is not in the same package as the ImageProducer and MyImage classes. Therefore, javac rightfully tells us that we cannot access MyImage from the ImageConsumer class.
To fix this, we would have to place ImageConsumer in the same package as ImageSource. Or, we can simply extend the visibility of the MyImage class to public.
3. Kotlin Case
So, how is it related to the Kotlin compilation error? Well, let’s now assume that we want to use these two classes in Kotlin code. What we want to do, in particular, is to extend the ImageSource class.
As we all know, Kotlin has great interoperability with Java, so we can easily do that:
package that.differs.from.image_source
class RemoteImageSource : ImageSource() {
override fun produceImage() : MyImage {
}
}
As we can see, we have our RemoteImageSource Kotlin class, which extends the ImageSource class. Because the productImage() method is abstract, we have to provide an implementation of it.
Here comes the problem: RemoteImageSource is not in the same package as ImageSource. Still, it has to override the produceImage() method, which returns the package-private MyImage class. And now, we are at a stalemate. We need to override the method, but we cannot access the MyImage class.
4. The Error Explained
If we try to compile this Kotlin code, the kotlinc compiler will issue an error. More precisely, we’ll get two different errors. One error occurs because the MyImage class is inaccessible in RemoteImageSource. The other error is specific to the Kotlin compiler and is the main topic of this article: ‘public’ function exposes its ‘public/*package*/’ return type. Let’s discuss the latter in detail.
We get this error when a Kotlin function’s return type is less accessible than the function itself. In our case, we have the Kotlin function produceImage(), which has public access. On the other hand, we have MyImage class, which is package-private. But if that would compile, this package-private modifier won’t make any sense. In this case, anyone could invoke the produceImage() function and access the MyImage class, regardless of its access modifier.
5. Solutions to the Problem
Now, we know what’s going on. So, what can we do to solve it? All of our solutions would have to:
- Make the MyImage class accessible from the calling function
- Not extend the accessibility context of the MyImage class
Therefore, there are multiple ways to overcome this issue.
5.1. Extending Exposed Class Visibility
We can make the MyImage class public. This approach addresses two concerns. First, the MyImage class becomes accessible to any code, including our function. Second, we no longer extend the visibility context of the class. So, by making MyImage class public, we can expose it to anyone, and nothing gets violated here.
5.2. Adhering to Existing Visibility Restrictions
The previous solution works in many cases, but let’s assume a more complex case: Sometimes, we don’****t have access to the source code of this ImageSource class we extend. In this case, the solution is trickier.
First, we need to make MyImage class accessible, so we put our RemoteImageSource class in the same package as MyImage class. For instance, assume MyImage was in the package abc.bcd.efg, in this case, our RemoteImageSource class should be in the abc.bcd.efg package as well. The Kotlin compiler can easily handle it. This is how we solve the first issue. In Java, that would suffice, but Kotlin tries to really care about the visibility of the MyImage class.
So, secondly, in Kotlin, we have to reduce the visibility of RemoteImageSource. It doesn’t have any visibility modifiers, so it’s public by default. So, we have to make it either internal or private. Why? Because the Kotlin compiler needs to be sure that this MyImage class won’t be accessible to anyone outside.
By making our RemoteImageSource class private, no one outside this class would be able to access it, and therefore, no one would be able to access MyImage, either. Similarly, marking RemoteImageSource class as internal would make it, and hence, MyImage, accessible only in this compilation unit. Thus, the Kotlin compiler can ensure that the visibility of the MyImage class is honoured.
6. Conclusion
In this article, we’ve explored the root cause of this visibility context exposure problem. The root cause of it is, again, exposure to type beyond its accessibility. There are two solutions to this problem. The simple and painless solution is to change the visibility of the problematic type to public. The more complicated solution is to mark our Kotlin class as internal or private and place it in the same package as this type.
As always, the source code for this article can be found over on GitHub.