Today, while working on a project for a college “Design Patterns” course (Java 11 required), I discovered a problem with the access restriction of the access modifier that can be bypassed by declaring var. I know how var is used, it's just a syntactic sugar that leaves the type inference to the compiler.
I can't figure out what type of alias the var is actually here:
Here is my simplified code:
public abstract class Parent {
protected abstract static class InnerParent {
public InnerParent self() {
return this;
}
}
}
public class Child extends Parent {
public static class InnerChild extends InnerParent {}
}
import anotherpackage.Child;
/**
* Compiling with Java 11:
*/
public class Main {
public static void main(String[] args) {
// As we expected a compilation error: The returned static type does not match the expected type
// Child.InnerChild innerChild = new Child.InnerChild().self();
// As we expected a compilation error: Parent.InnerParent is package visible (protected)
// Parent.InnerParent innerChild = new Child.InnerChild().self();
// Why does it compile and run correctly here?
// var is just syntactic sugar for the compiler type, it should be a Parent.InnerParent alias here,
// why is var allowed to transgress the protected access restriction?
var innerChild = new Child.InnerChild().self(); // perhce' non da' errore ? var e' un alias di cosa ?
System.out.println(innerChild);
System.out.println(innerChild.getClass().getName());
}
}
I've also asked ChatGPT, but it's not responding as well as I'd like, and I'm not sure it's correct:
Why
var
Works
- Inferred Type: The inferred type for
var innerChild
isParent.InnerParent
.- Access Rules: Since the type is inferred and not explicitly written in the code, the compiler doesn't enforce access restrictions for the declared variable.
I found a new problem: why can't I access getClass()
?
However it is possible to compile this way.
System.out.println(((Object) innerChild).getClass().getName());
// OUTPUT: com.github.lorenzoyang.anotherpackage.Child$InnerChild
The answer is: var
can represent anything the compiler can reason about as a type, even non-denotable ones: It lets you kick the can down the road. The error you ended up with has existed since before the introduction of var
which proves this isn't a problem with var specifically; it's an intended (but somewhat odd) effect that matches the java lang spec.
Let's get into the specifics and break it down. But first:
GPT is a tool that produces an authoritative answer. As in, it'll pretty much keep trying to answer your question until something rolls out that looks authoritative. It can be a complete load of hogwash and in this case, indeed it is. Generally asking GPT for objective answers is a really bad idea: Sure, for simple questions it seems magically amazing, but then - it was a simple question. There are many ways to get the answer to a simple question. It's not what you should optimize for - that would be answers to complex questions. And GPT is very bad at that. In the sense that you can't tell. It'll give you a great sounding answer.
Remember this: Asking a GPT about language esoterica questions is ridiculous. Don't ever do it.
I'm not going to address the GPT answers any further. They are useless (possibly wrong, possibly not wrong. Any hint it gives might be relevant, or not. Hence, useless).
It's a language/compiler question so we refer to the source, the JLS:
If the LocalVariableType is var, then let T be the type of the initializer expression when treated as if it did not appear in an assignment context, and were thus a standalone expression (§15.2). The type of the local variable is the upward projection of T with respect to all synthetic type variables mentioned by T (§4.10.5).
And also note this clarification from the example box:
Note that some variables declared with var cannot be declared with an explicit type, because the type of the variable is not denotable.
That last part is implicit in the actual definition (in that the definition does not at any point state that the type that the compiler infers for var
has to be denotable, you must therefore infer that it does not have to be; the note in the example box calls this out).
And it's key to understanding what's going on here. Here's an example of undenotable type use that's easier to follow than what this question has found:
class Example {
public static void main(String[] args) {
Object o = new Object() {
void test() {
System.out.println("HELLO!");
}
};
o.test(); // compiler error.
var p = new Object() {
void test() {
System.out.println("HELLO!");
}
};
p.test(); // works!!
}
void asAReminder() {
new Object() {
void test() {
System.out.println("This has always worked");
}
}.test();
}
}
Paste the above into a file, compile it - error on the line that says 'compiler error'. Remark it out, compile again, run it - works fine.
Which is bizarre in the sense that there is nothing you could possibly replace var
with that makes this work. What's happening is that p
's type is inferred to be the type of the anonymous local class and that type does have a test()
method. This has always been part of java (well, since 1.2 or so, decades ago at this point), it's just that var
now lets you declare local variables with that type. The asAReminder()
method shows this; that code 'works' (compiles, and runs as you'd expect) in any java version of the past 2 decades.
Hence, var
can denote InnerParent
just fine here. It's not 'denotable' - if you literally replace var
in your example with InnerParent
it would fail to compile because InnerParent
isn't accessible, but var
merely represents the type of the expression the way the compiler treats it. And the compiler has to support it. After all, this is perfectly legal:
Object o = new Child.InnerChild().self();
The compiler is going to have to deal with the fact that the expression being assigned to o
here is InnerParent
(the def of self()
says so, after all), and will therefore have to figure out in context that this is just fine.
We can prove that too!
var
Try to replace your main
definition with the following:
public class Main {
public static void main(String[] args) {
new Child.InnerChild().self().getClass();
}
}
That code will fail to compile, with the exact same error:
Object.getClass() is defined in an inaccessible class or interface
In other words, in java 8 (which doesn't support var
at all - var
as a feature was added in java 10) we can have the same concept: An expression which is, itself, legal, and whose type isn't accessible in the context we wrote it. It 'works' but the type is extremely poisonous: If you 'touch' it, almost anything you care to do to it will be a compiler error; even invoking final
methods inherited directly from the obviously accessible java.lang.Object
. Pretty much the only thing you can do with them is treat them as an accessible type first (by casting or assigning to a variable) or pass them to another method verbatim.
var
merely lets you kick the can down the road.