In my earlier post on Diamond, I described four places where the diamond operator can be used:
- field or local variable initializer (61%)
- right-hand side of assignment statement (33%)
- method argument (4%)
- return statement (2%)
(The percentages refer to the frequency of occurrence in code found by the automated “diamond finder.”)
Now, just because you can use diamond in some code doesn’t mean that you should. So, when should you use it?
An observation before we begin. If you have some code that provides generic type arguments, and you use diamond, the compiler will almost always infer the type arguments that you would have put there anyway. Using diamond will only rarely change the semantics of your program. The decision about whether or not to use diamond is entirely driven by things like style, readability, and clarity.
Let’s step through the different use sites for diamond and see how it affects these aspects of the code.
1. Field or Local Variable Initializer
An example of this usage is as follows. Without diamond, the code looks like this:
private static Map<ClassLoader, Map<List<String>, Object>> loaderToCache
= new WeakHashMap<ClassLoader, Map<List<String>, Object>>();
And with diamond, it looks like this:
private static Map<ClassLoader, Map<List<String>, Object>> loaderToCache
= new WeakHashMap<>();
If you want to know what the compiler will fill into the diamond for you, it’s right there in the declaration on the left-hand side. The code without diamond is redundant, and the code with diamond is much shorter and loses no information compared to before. This is pretty clearly a win. Since this is the most common usage (over 60%) even using diamond only in this situation helps quite a bit.
2. Right-hand Side of Assignment Statement
Now consider this code:
perms = new ArrayList<>();
What type arguments are inferred here? Well, you have to hunt down the declaration of the perms variable. But, no big deal. If you want to use the perms variable, such as to call a method on it, you have to know what type it is. And if you know that it’s a List or a Collection and you want to add something to it, you have to know what the type argument is. So not having the type arguments in the constructor doesn’t really hurt anything. Plus, if you want to see the type, you can usually just ask your IDE.
(On NetBeans 7, the gesture that works for me is a bit odd. First, hold down Control and click on some whitespace in your code — not on a symbol, since that will navigate to its declaration. Then, still holding Control, hover over the constructor that uses diamond. The full declaration of the constructor being called, including type arguments, should be displayed in a tooltip. Of course, you can always ask to navigate to the declaration of the variable.)
This is a point on which reasonable people can disagree. My diamond conversion changeset for the core libraries included diamond in assignment statements. The primary reviewer was Joe Darcy, who as Coin project lead is of course quite familiar with diamond. Subsequent diamond conversion changesets were for the security libraries, and the security team was uncomfortable using diamond in assignment statements. Given that they’re the ones who have to maintain this code in the long term, I decided it was best to conform to their wishes and not do diamond conversion for assignment statements in their area.
Personally I’m fine with using diamond in assignment statements, but your opinion might differ.
3. Method Argument
It’s possible to use diamond in the argument for a method call. This is fairly rare, occurring in only 4% of the cases examined. There are some complications with type inference, so it cannot be used in certain cases. But it’s hard to tell which cases these are. Consider for example the Collections.synchronizedSet method. Its declaration is as follows:
<T> Set<T> synchronizedSet(Set<T> s)
That is, it’s a generic method that has a type parameter T; it takes an argument of type Set of T and returns a value that’s also Set of T. It’s possible, though rare, to specify the generic type argument explicitly. Here’s how it’s done:
Set<Number> set0 = Collections.<Number>synchronizedSet(new HashSet<Number>());
It’s not necessary to specify the generic type argument explicitly in this case. The following code works just as well (and is much more common):
Set<Number> set1 = Collections.synchronizedSet(new HashSet<Number>());
Where does the generic type argument come from? In this case the compiler infers it from the type argument from the constructor, and the result of the inference is that T is Number. Now let’s try to use diamond:
Set<Number> set2 = Collections.synchronizedSet(new HashSet<>());
Unfortunately, this does not work. Why not? In the previous case, the compiler used the type argument Number from the HashSet constructor call to determine the value of the type argument T. When diamond is used, the compiler needs first to fill in the diamond before it can establish the value of T. But in order to fill in the diamond, the compiler needs to know what type is expected as the argument for synchronizedSet(), and this type includes the type parameter T. So we have a circular dependency. The compiler can’t infer the right type, so the compilation fails. The compiler doesn’t flag the circularity as an error, though. What it does is start off by attempting to use Object in the diamond and then proceeding from there. The actual error message is as follows:
error: incompatible types
Set<Number> set2 = Collections.synchronizedSet(new HashSet<>());
required: Set<Number>
found: Set<Object>
This is a bit odd, but that’s where the Set<Object> comes from. But note that the following statement does work:
Set<Object> set3 = Collections.synchronizedSet(new HashSet<>());
Now this is weird. The compiler attempts to fill in the diamond with Object, and this ends up working, so the statement compiles successfully.
The compiler doesn’t always start with Object. It will use additional type information if it’s available, such as from other method arguments. Consider this example:
Set<Number> set4 = Collections.synchronizedSet(new HashSet<>(set1));
This also works. The difference here is that we’re passing set1 as an argument to the HashSet constructor. This supplies enough information to the compiler about the type that should go into the diamond, and this in turn supplies the information to establish the value of the T type parameter of the synchronizedSet() method.
What’s the point of all this? The type inference that goes on works pretty well for variable initializers and assignments, but it can break down in some obscure ways for method arguments, especially for generic methods. This can make the code obscure, too. In the set4 example above, what type is inferred for the diamond? One might think that it has to be Number, but that’s not always the case. It really depends on the type of set1, which is declared elsewhere. We know that set1 is HashSet<Number>, whose type argument Number happens to match the type argument on the left-hand side. It looks like this type is inferred from the left-hand side, but in fact it isn’t. The following example illustrates this.
Set<? extends Number> set6 = Collections.synchronizedSet(new HashSet<>(set5));
What gets inferred for the diamond? We can’t tell, other than that it must be a Number or a subclass of Number. It really depends on the type of set5. It might be Number or it might be AtomicLong. Indeed, since I haven’t provided the declaration for set5, it goes to show that we can’t tell how the diamond gets filled in simply by looking at this statement. We have to go look elsewhere.
As you can see, there’s considerable subtlety surrounding the use of diamond in method arguments, and there are some obscure interactions with the compiler’s type inference algorithm. It might be quite difficult for a programmer to figure out what type gets filled into the diamond. In some cases diamond can’t be used, and it results in a compiler error that might be puzzling.
For these reasons we’ve concluded that it’s probably not a good style to use diamond within method arguments even when it would be legal to do so. In doing my diamond conversion over the core libraries, I made sure to avoid using diamond in method arguments. In fact I had put some diamonds in method arguments in early on, and when we decided to avoid using diamond in these cases, I went back in and pulled them out.
4. Return Statement
Finally, it’s possible to use diamond in the return statement. In this case the diamond is inferred from the return type of the method. This is similar to an assignment statement, since the type of the expression in the return statement must be assignment compatible to the return type of the method. The method’s return type might be declared fairly far away from the return statement, but this isn’t necessarily any worse than assigning to an object’s field, which might also be declared far away. For short methods, like the following,
List<Foo> makeFooList() {
int capacity = ...; // compute initial capacity
return new ArrayList<>(capacity);
}
it’s pretty much a no-brainer to use diamond. But, overall, the opportunity to use diamond in return statements is quite rare, so it’s probably not worth worrying too much about whether or not to use diamond for these cases.
Summary
The first two cases — field or variable initializers, and assignment statements — together comprise well over 90% of the opportunities for using diamond. I’d recommend using diamond in those cases. Even if you’re not comfortable using diamond in assignment statements, you can still get a lot of benefit using diamond just in initializers. Diamond in return statements are rare enough not to worry about very much, though it’s probably reasonable to use diamond in the obvious cases. Finally, it’s probably a good idea to stay away from diamond in method arguments. When it’s legal to use, it might be hard to see how the diamond is filled in, and when it’s not legal, it might be hard to figure out why.