Objectos Weekly #009: Java varargs and non-accessible types

Marcio Endo
January 15, 2023

Welcome to Objectos Weekly issue #009.

While working on Objectos Code this past week I faced a compilation error I had never seen before. With the benefit of hindsight, it is perhaps an obvious point. But I did not anticipate it.

It is an interaction between varargs parameters and non-accessible types.

Let's begin.

A hypothetical library

Suppose we are developing a Java library. We want it to be used in the following way:

package application; 

import build.Pom;

public class MyBuild extends Pom {
  @Override
  protected final void project() {
    dependencies(
      dependency("com.example", "library-a", "1.0.0")
    );
  }
}

So we write the following class:

package build;

public abstract class Pom {

  protected abstract void project();
  
  protected final void dependencies(
      Dependency dep) {
    ...
  }
  
  protected final Dependency dependency(
      String groupId, String artifactId, 
      String version) {
    ...
  }
  
}

We just want to get a feel of how the API is going to used. In other words, we are not interested in how things will be implemented yet. So the method bodies are not shown.

For the same reason, we declare Dependency as an interface:

package build;

public interface Dependency {}

It is a public interface in the build package, the same package of the Pom class. We are not worried about its implementation for now.

With the Pom and Dependency types created, our MyBuild test class compiles without errors.

Great. Let's move to the next step.

Reducing the Dependency access level

We can argue that the Dependency interface is an implementation detail. As it currently stands, the interface is of no use outside the Pom class and its subclasses. Therefore, it would be nice if we could prevent instances of Dependency to escape this context.

We can perhaps achieve it by making it a package-private interface:

package build;
// default access control
interface Dependency {}

After this change our MyBuild test class still compiles without errors.

Note that it still compiles even though it is defined at a different package. In other words, even though Dependency is not accessible to the MyBuild class, the following compiles:

dependencies(
  dependency("com.example", "library-a", "1.0.0")
);

On the other hand, the following does not compile:

// does not compile
Dependency dep;
dep = dependency("com.example", "library-a", "1.0.0");
dependencies(dep);

As Dependency is not accessible.

So we have succeeded in our goal: instances of Dependency cannot escape the Pom class (as far as I can tell).

Allowing more dependencies

Our application is growing and we need to add more dependencies:

package application; 

import build.Pom;

public class MyBuild extends Pom {
  @Override
  protected final void project() {
    dependencies(
      dependency("com.example", "library-a", "1.0.0"),
      dependency("com.example", "library-b", "2.0.0"),
      dependency("com.example", "library-c", "3.0.0")
    );
  }
}

So we update the Pom class:

package build;

public abstract class Pom {

  protected abstract void project();
  
  protected final void dependencies(
      Dependency dep1) {
    ...
  }

  protected final void dependencies(
      Dependency dep1, Dependency dep2) {
    ...
  }

  protected final void dependencies(
      Dependency dep1, Dependency dep2, 
      Dependency dep3) {
    ...
  }
  
  protected final Dependency dependency(
      String groupId, String artifactId, 
      String version) {
    ...
  }
  
}

Great! Our MyBuild test class continues to compile without errors.

Allowing even more dependencies

You are probably seeing where this is going: we need to allow an arbitrary number of dependencies.

So we merge all of the dependencies methods into a single varargs method:

package build;

public abstract class Pom {

  protected abstract void project();
  
  protected final void dependencies(
      Dependency... dependencies) {
    ...
  }
  
  protected final Dependency dependency(
      String groupId, String artifactId, 
      String version) {
    ...
  }
  
}

But now our MyBuild test class fails to compile. Here's the error message from javac:

$ javac -d /tmp src/main/java/{application,build}/*.java
src/main/java/application/MyBuild.java:13: error: 
method dependencies in class Pom cannot be applied to given types;

    dependencies(
                ^
  required: Dependency[]
  found:    Dependency,Dependency,Dependency
  reason: formal varargs element type Dependency 
          is not accessible from class MyBuild
1 error

What happened?

A not so hypothetical library

I faced this problem while working on Objectos Code recently.

I did not anticipate the error would happen. So knowing the theory behind a feature, varargs in this case, does not mean I won't misuse it.

Of course the error makes perfect sense: an array instance is created at the varargs parameter.

Varargs evaluation

In our example, the dependencies parameter:

protected final void dependencies(
    Dependency... dependencies)

Is formally called a variable arity formal parameter: it is declared as a single construct but can accept an arbitrary number of arguments. When the method is invoked the actual arguments must be evaluated.

Section 15.12.4.2 of the JLS deals with argument evaluation:

If the method being invoked is a variable arity method m, it necessarily has n > 0 formal parameters. The final formal parameter of m necessarily has type T[] for some T, and m is necessarily being invoked with k ≥ 0 actual argument expressions.

If m is being invoked with k ≠ n actual argument expressions, or, if m is being invoked with k = n actual argument expressions and the type of the k'th argument expression is not assignment compatible with T[], then the argument list (e1, ..., en-1, en, ..., ek) is evaluated as if it were written as (e1, ..., en-1, new |T[]| { en, ..., ek }), where |T[]| denotes the erasure (§4.6) of T[].

I must say the wording is quite dense to me. We are mostly interested in the part at the end.

Varargs evaluation of our example

Let's apply the JLS definition of the previous section to our MyBuild test class. Here is the dependencies invocation:

dependencies(
  dependency("com.example", "library-a", "1.0.0"),
  dependency("com.example", "library-b", "2.0.0"),
  dependency("com.example", "library-c", "3.0.0")
);

The dependencies method declare a single formal parameter. This single parameter is a variable arity one, hence n = 1.

We are invoking the method with three arguments so we have k = 3.

So the invocation is actually compiled to:

dependencies(new Dependency[] {
  dependency("com.example", "library-a", "1.0.0"),
  dependency("com.example", "library-b", "2.0.0"),
  dependency("com.example", "library-c", "3.0.0")
});

Therefore, if Dependency is not accessible at the call-site, a compilation error occurs.

Fixing the compilation error

Let's fix this compilation error.

A first option is to make Dependency public again:

package build;

public interface Dependency {}

But, as mentioned before, Dependency is mostly an implementation detail. We do not want it to be a top-level public type.

So, as an alternative, we could convert it to a protected nested type of Pom. Like so:

package build;

public abstract class Pom {

  // it is now a nested type
  protected interface Dependency {}

  protected abstract void project();
  
  protected final void dependencies(Dependency... dependencies) {
    ...
  }
  
  protected final Dependency dependency(
      String groupId, String artifactId, String version) {
    ...
  }
  
}

Both solutions make our MyBuild test class compiles without errors.

Let's work together

Do you feel that adding features to your Java applications is taking longer as time goes by? Perhaps I can help. Let's get in touch.

You can find my contacts on this page. All work will be provided via Objectos Software LTDA based in São Paulo, Brazil.

Until the next issue of Objectos Weekly

So that's it for today. I hope you enjoyed reading.

The source code of all of the examples are in this GitHub repository.

Please send me an e-mail if you have comments, questions or corrections regarding this post.