Objectos Weekly #007: Using var with non-accessible types
Welcome to Objectos Weekly issue #007.
I had hopes of sending out this issue on Sunday, December 25th. It turns out it is hard to get anything done after you had a very large meal and stayed up late with your family the night before. To those who celebrate it, I hope you had a happy Christmas.
I will probably take a small break this week. So the next issue might only come out on January 8th or later.
To all subscribers and readers, thank you! As someone (re-)building a business, it means a lot. I wish you all a Happy New Year.
Let's begin.
Local-variable type inference
Java 10 introduced local-variable type inference to the language.
It allows you to declare a local variable using the var
contextual keyword:
var outputStream = new ByteArrayOutputStream();
The explicit type information on the left-hand side of the declaration is not required; it is inferred by the compiler instead.
Non-denotable types
The JEP responsible for delivering this language feature was JEP 286.
It mentions that, using var
to declare local variables, might increase the programmer's exposure to non-denotable types.
I first learned about non-denotable types in issue #065 of Sip of Java by Billy Korando.
To understand non-denotable types, consider the following example. Note that it does not compile:
// does not compile!
Object o = new Object() {
public void canYouSeeMe() {
System.out.println("I can see you!");
}
};
o.canYouSeeMe();
It declares an anonymous class having a canYouSeeMe()
method.
An instance is created and assigned to the variable o
of type Object
.
Lastly, it tries to invoke the canYouSeeMe()
method.
The class is anonymous and, therefore, we cannot refer to it using a name.
In other words, the class' type is non-denotable.
This is the reason why we declared the variable o
with the type of its immediate superclass instead: Object
.
The information that the class declares a canYouSeeMe
method is, therefore, lost.
Retaining type information with var
Let's change the local variable declaration to use the var
keyword:
// compiles without errors
var o = new Object() {
public void canYouSeeMe() {
System.out.println("I can see you!");
}
};
o.canYouSeeMe();
The code now compiles without errors.
It prints I can see you!
to the console.
So, by using the var
keyword, we were able to retain the information regarding the canYouSeeMe
method existence.
Non-accessible types
I recently realized that using var
allows you to partially escape an access control restriction.
More specifically, you can declare a variable of a type having package access outside the package in which it is defined.
It is somewhat similar to using it to refer to a non-denotable type.
A hypothetical library
Suppose a Box
class declared with the package access:
package escape.api;
// no access modifiers
class Box {
private final String value;
Box(String value) {
this.value = value;
}
public String get() {
return value;
}
}
Please bear with me. This particular example is not meant to be practical.
Our Box
class is used in a Widget
abstract class:
package escape.api;
public abstract class Widget {
public abstract void execute();
protected final void consume(Box box) {
System.out.println(box.get());
}
protected final Box produce(String value) {
return new Box(value);
}
}
It declares two protected methods: produce
and consume
.
The methods are meant to be used in tandem.
The first one returns a Box
instance while the second one accepts a Box
instance.
Let's use the Widget
class.
We must implement the execute
method to do so:
package escape.app;
import escape.api.Widget;
public class Example01 extends Widget {
@Override
public void execute() {
consume(produce("Hello world!"));
}
public static void main(String[] args) {
new Example01().execute();
}
}
The Example01
class is defined at a different package than our Box
class.
The former, therefore, is not accessible from the latter.
But we do not mention the Box
class directly in the source code.
So the class compiles without errors.
When executed, it prints Hello world!
to the console.
Introducing a local variable
As mentioned, Box
is not accessible from our example class.
So, if we were to introduce a local variable of type Box
, our code should fail to compile:
package escape.app;
import escape.api.Box;
import escape.api.Widget;
// does not compile!
public class Example02 extends Widget {
@Override
public void execute() {
Box box = produce("Hello world!");
consume(box);
}
}
Compilation fails with the following message:
$ javac -d /tmp src/main/java/escape/{api,app}/*
src/main/java/escape/app/Example02.java:8: error:
Box is not public in escape.api;
cannot be accessed from outside package
import escape.api.Box;
^
src/main/java/escape/app/Example02.java:18: error:
Box is not public in escape.api;
cannot be accessed from outside package
Box box = produce("Hello world!");
^
2 errors
This is expected.
Remember, Box
was declared with the package access.
But what if we declare the local variable using the var
keyword instead?
Using var
to declare the local variable
If we replace the Box
type with the var
keyword:
package escape.app;
import escape.api.Widget;
// compiles without errors!
public class Example03 extends Widget {
@Override
public void execute() {
var box = produce("Hello world!");
consume(box);
}
public static void main(String[] args) {
new Example03().execute();
}
}
The code now compiles without errors!
So, by using the var
keyword, we are able to declare a local variable of a non-accessible type.
When executed, it prints Hello world!
.
Invoking a Box
method
What if we try to invoke the get
method declared in Box
?
Well, the following does not compile:
package escape.app;
import escape.api.Widget;
public class Example04 extends Widget {
@Override
public void execute() {
var box = produce("Hello world!");
// compilation error!
// cannot invoke method get()
System.out.println(box.get());
}
}
Compilation with javac
fails with the following message:
$ javac -d /tmp src/main/java/escape/{api,app}/*
src/main/java/escape/app/Example04.java:19: error:
Box.get() is defined in an
inaccessible class or interface
System.out.println(box.get());
^
1 error
Invoking a Object
method
Perhaps more interestingly the following does not compile either:
package escape.app;
import escape.api.Widget;
public class Example05 extends Widget {
@Override
public void execute() {
var box = produce("Hello world!");
// compilation error!
System.out.println(box.toString());
}
}
Here's the error message from javac
:
$ javac -d /tmp src/main/java/escape/{api,app}/*
src/main/java/escape/app/Example05.java:15: error:
Object.toString() is defined in an
inaccessible class or interface
System.out.println(box.toString());
^
1 error
So it seems we cannot invoke any method at all. The only thing we can do with it, it seems, is use it as the argument of a method.
An actual library
The hypothetical library example is not so hypothetical after all.
I am currently working on Objectos Code. It is an open-source Java library for generating Java source code. I use it internally at Objectos. The first public alpha release should be out in Q1 of 2023.
Re-thinking the API
I am experimenting with a new API and considering a re-balance of tradeoffs. My idea is for the following Objectos Code:
import objectos.code.JavaTemplate;
public class Example extends JavaTemplate {
@Override
protected final void definition() {
_package("com.example");
_public(); _class("Box"); body(
_private(), _final(), t(String.class), id("value"),
_public(), id("Box"), t(String.class), id("value"), block(
assign(n(_this(), "value"), n("value"))
),
_public(), t(String.class), id("get"), block(
_return(n("value"))
)
);
}
}
To generate the following Java code:
package com.example;
public class Box {
private final String value;
public Box(String value) {
this.value = value;
}
public String get() {
return value;
}
}
Please note this is still in the realm of the ideas. And as I mentioned before: there are tradeoffs.
Caveat Emptor
You might have noticed in the previous example that the t
method declares a type.
And the same t(String.class)
invocation is used to declare the type of:
-
the
value
field; -
the constructor parameter; and
-
the return type of the
get
method.
As those declarations have the same type, one might be tempted to do the following:
import objectos.code.JavaModel.ClassType;
// not accessible -> ^^^^^^^^^
import objectos.code.JavaTemplate;
public class WrongDoNotDoThis extends JavaTemplate {
@Override
protected final void definition() {
_package("com.example");
// do not do this!!!
ClassType str = t(String.class);
_public(); _class("Box"); body(
_private(), _final(), str, id("value"),
_public(), id("Box"), str, id("value"), block(
assign(n(_this(), "value"), n("value"))
),
_public(), str, id("get"), block(
_return(n("value"))
)
);
}
}
Well, the API must not be used like that. The return value of any of the methods must not be "re-used".
The reason is the API "records" all of the method invocations and then "plays them back" again to generate the source code in the correct order.
So, if you have three locations where the type String
occurs, you need three t(String.class)
method invocations.
The previous example would not compile though.
ClassType
is nested in a type with package access and it cannot be accessed outside the objectos.code
package.
But, as we have seen, if we use var
instead, then the following will compile without errors:
import objectos.code.JavaTemplate;
public class WrongDoNotDoThis extends JavaTemplate {
@Override
protected final void definition() {
_package("com.example");
// `var` compiles without errors
var str = t(String.class);
_public(); _class("Box"); body(
_private(), _final(), str, id("value"),
_public(), id("Box"), str, id("value"), block(
assign(n(_this(), "value"), n("value"))
),
_public(), str, id("get"), block(
_return(n("value"))
)
);
}
}
So, in this case, there's not that can be done:
-
either a runtime exception must be thrown; or
-
the template should be allowed to generate invalid Java code.
The second option is not as bad as it sounds: the invalid generated code will be eventually compiled by a Java compiler.
So, either way, a compiler will catch the error.
Let's work together
Please know that I will be open for freelance consulting work in 2023. Do you have a legacy Java application that needs some looking at? 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.