Using var
to infer types of local variables is a great tool for writing readable code.
More than that, it makes working with intersection types much more pleasant.
It has a dark underbelly, though, and while experimenting with it I found a few var
-tricks related to anonymous classes (ad-hoc fields and ad-hoc methods) that were fun to explore but I think are ultimately not fit for duty.
These tricks hinge on the fact that compiler and JVM know a richer type system than can be expressed with Java's syntax.
With var
, though, a type does not need to be written out and can instead be determined by the more powerful compiler.
This is particularly helpful when working with anonymous classes, which can not be expressed in source code.
▚Ad-hoc Fields
Every time you call a constructor, you have the chance to add some fields and methods right then and there:
Megacorp megacorp = // ...
Map<Megacorp, Address> headquarters = // ...
Object corpWithHq = new Object() {
Megacorp _corp = megacorp;
Optional<Address> _hq =
Optional.ofNullable(headquarters.get(megacorp));
};
// does not compile
System.out.println(corpWithHq._corp);
The compiler will create an anonymous subclass (in this case of Object
) and then instantiate it.
But since the subclass is only created during compilation there is no way to reference it in the source code and so corpWithHq
is declared as an Object
.
The unfortunate consequence is that the fields _corp
and _hq
can't be referenced because they aren't part of Object
's API.
If you use var
, on the other hand, things work just fine:
Megacorp megacorp = // ...
Map<Megacorp, Address> headquarters = // ...
var corpWithHq = new Object() {
Megacorp _corp = megacorp;
Optional<Address> _hq =
Optional.ofNullable(headquarters.get(megacorp));
};
// compiles
System.out.println(corpWithHq._corp);
Now, corpWithHq
's type is the anonymous subclass and you can happily toil away with the fields you added.
With var, a variable can be of an anonymous type
▚Enriching Streams
That specific example may not have been overly exciting but there are cases where this approach starts to look very appealing. I'm sure you've occasionally written a stream pipeline where you enriched the stream's elements with some other piece of information, but needed to keep both kinds of elements around. They form a pair, but Java has no tuples, so you start looking for another solution. Maybe this?
List<Megacorp> megacorps = // ...
Map<Megacorp, Address> headquarters = // ...
Optional<Megacorp> firstWithValidHq = megacorps.stream()
// we stream megacorps, but need to add addresses ...
.map(megacorp -> new Object() {
Megacorp _corp = megacorp;
Optional<Address> _hq =
Optional.ofNullable(headquarters.get(megacorp));
})
// ... only for evaluation, though ...
.filter(o -> o._hq.isPresent())
.filter(o -> isValid(o._hq.get()))
// ... in the end we can get rid of them again
.map(o -> o._corp)
.findAny();
Interestingly enough, this example works without var
and already compiles on Java 8 because the streams intermediate's type, Stream<$Anonymous>
, never needs to be expressed in source ode.
With var
, you're able to declare intermediate variables, though, which wouldn't work without it:
List<Megacorp> megacorps = // ...
Map<Megacorp, Address> headquarters = // ...
// Optional<$Anonymous>
var firstWithValidHq = megacorps.stream()
.map(megacorp -> new Object() {
Megacorp _corp = megacorp;
Optional<Address> _hq =
Optional.ofNullable(headquarters.get(megacorp));
})
.filter(o -> o._hq.isPresent())
.filter(o -> isValid(o._hq.get()))
// note that the map is gone!
.findAny();
Without the second map
, firstWithValidHq
is an Optional
containing the anonymous class with the two fields _corp
and _hq_
.
▚Evaluation
As I mentioned in the intro, I don't think this is a trick you should use frequently, if at all. First, creating the anonymous class is pretty verbose and will often span several lines. Then it mixes two non-trivial features, anonymous classes and type inference, which makes the code harder to read and understand.
What bugs me the most, though, is that it falls apart under simple refactoring. Assume the example we've seen grows a bit and somebody wants to extract two methods, one that determines the megacorp with its headquarter and another that processes it:
List<Megacorp> megacorps = // ...
Map<Megacorp, Address> headquarters = // ...
// `determineCorp` must be declared to return a concrete
// type, so no amount of `var` magic is gonna help here
var corp = determineCorp(megacorps, headquarters);
processCorp(corp);
So while refactoring is the right idea, thanks to the ad-hoc fields it's an order of magnitude more work because a type with the right fields needs to be created and used. That may very well deter developers from actually executing the refactoring and resistance to continuous improvement is not exactly a hallmark of maintainable code.
Using var with anonymous classes makes code harder to read, understand, and refactor
If you're looking for alternatives, I sometimes use Map.Entry
for pairs, which Java 9 made much more usable with the static method Map::entry
.
Beyond that you could be looking for a library that comes with tuples, something you usually find in functional libraries like Vavr.
If you're patient, you can wait for Project Amber's data classes, which will make local classes a one-liner.
▚Ad-hoc Methods
Just like fields, you can add methods:
Megacorp corp = new Megacorp(/* ... */) {
final BigDecimal SUCCESS_BOUNDARY = new BigDecimal("500000000");
boolean isSuccessful() {
return earnings().compareTo(SUCCESS_BOUNDARY) > 0;
}
boolean isEvil() {
return true;
}
};
And just like with fields, if you declare corp
as Megacorp
, the compiler will not let you use the new methods isSuccessful
and isEvil
.
With var
it does:
var corp = // like before
System.out.printf(
"Corporation %s is %s and %s.\n",
corp.name(),
corp.isSuccessful() ? "successful" : "a failure",
corp.isEvil() ? "evil" : "a failure"
);
It's the same principle as with ad-hoc fields and I have the same criticism.
▚Evaluation
Like with ad-hoc fields, the code's readability and refactorability (what a word) suffer without appreciable benefits.
Alternatively, methods like isSuccessful
and isEvil
could either be members of Megacorp
or a subclass or, if that's not possible or desirable for whatever reason, they can always be implemented as utility methods.
While that makes calling them a little less natural (Megacorps.isEvil(corp)
instead of corp.isEvil()
), it has the added benefit to enable reuse.
▚Reflection
Using local-variable type inference with var
, it is easy to add fields or methods to objects in an ad-hoc manner, simply by creating an instance of an anonymous class and assigning it to a local variable whose type is inferred.
While that is a neat trick, it does not carry its own weight:
- code becomes less readable (with anonymous classes and type inference it relies on non-trivial Java features)
- code becomes harder to refactor (those types can not readily be used in method signatures)
So instead of relying on a little magic with a lot of downsides, I recommend to stick to proven alternatives:
- instead of ad-hoc fields, use
Map::entry
, your favorite FP library's tuple types, or wait for data classes - instead of ad-hoc methods, extend the class directly or use utility functions
But take my advice with a grain of salt, I don't like anonymous classes anyways.