How do nested type arguments work?
Why does the declaration
Set<Set<String>> var = new HashSet<Set<String>>();
work but the declaration
Set<Set<String>> var = new HashSet<HashSet<String>>();
choke?
I'm aware that 'top level' (not sure if that's the correct phrase here) generics in a declaration play by different rules than those inside the pointy brackets, but I'm interested to learn the reason. Not an easy question to google, so I thought I'd 开发者_如何学运维try you guys.
It's because you could circumvent the type system if it were allowed. The property you desire is called covariance. If collections were covariant then you'd be able to do this:
Set<Set<String>> var = new HashSet<HashSet<String>>();
var.add(new TreeSet<String>());
A TreeSet is a type of Set, and so static type checking would not prevent you from inserting a TreeSet into var. But var expects HashSets and HashSets only, not any old type of Set.
Personally, I always write your first declaration:
Set<Set<String>> var = new HashSet<Set<String>>();
The outer class needs to have a conrete implementation, but there's usually no need to nail down the inner class to HashSet specifically. If you create a HashSet of Sets you are good to go. Whether you then proceed to insert a series of HashSets into var is your choice later in the program, but no need to restrict the declaration.
For what it's worth, arrays in Java are covariant, unlike the collection classes. This code will compile and will throw a runtime exception instead of being caught at compile time.
// Arrays are covariant, assignment is permitted.
Object[] array = new String[] {"foo", "bar"};
// Runtime exception, can't convert Integer to String.
array[0] = new Integer(5);
The reason is that Set<Set<String>>
is not equivalent to Set<HashSet<String>>
! A Set<Set<String>>
may contain any type of Set<String>
, while a Set<HashSet<String>>
may only contain HashSet<String>
.
If Set<Set<String>> set = new HashSet<HashSet<String>>()
were legal, you could also do this without any error:
Set<HashSet<String>> setOfHashSets = new HashSet<HashSet<String>>();
Set<Set<String>> set = setOfHashSets;
set.add(new TreeSet<String>());
HashSet<String> wrong = set.iterator().next(); // ERROR!
It is, however, legal to use a bounded wildcard here:
Set<? extends Set<String>> set = setOfHashSets;
This is allowed because now, the type of object the set contains is ? extends Set<String>
... in other words, "some specific but unknown class that implements Set<String>
". Since you don't know exactly what the specific type of Set<String>
it is allowed to contain is, you aren't allowed to add anything to it (except null
)... you might be wrong, leading to an error later like in my first example.
Edit:
Note that the "top level" generics you refer to in your question are called parameterized types, meaning types that take one or more type parameters. The reason that Set<Set<String>> set = new HashSet<Set<String>>()
is legal is that HashSet<T>
implements Set<T>
and is therefore a subtype of Set<T>
. Note, however, that the type parameter T
must match! If you have another type S
that is a subtype of T
, a HashSet<S>
(or just a Set<S>
even) is not a subtype of Set<T>
! I explained the reason for that above.
This is exactly the situation here.
Set<Set<String>> set = new HashSet<Set<String>>();
If we replace Set<String>
with T
here, we get Set<T> set = new HashSet<T>()
. It's easy to see, then, that the actual type arguments involved match and that the type on the right side of the assignment is a subtype of the type on the left.
Set<Set<String>> set = new HashSet<HashSet<String>>();
Here we have to replace Set<String>
and HashSet<String>
with T
and S
respectively, where S
is a subtype of T
. With that done, we get Set<T> set = new HashSet<S>()
. As I described above, HashSet<S>
is not a subtype of Set<T>
, so the assignment is illegal.
That's because of the way the generics work in Java.
Set<? extends Set<String>> var = new HashSet<HashSet<String>>();
The difference is simple, by allowing Set<Set<String>> var = new HashSet<Set<String>>
you're allowing var
to only accept a values of type Set<String>
(of which HashSet
is of Set
).
As for Set<Set<String>> var = new HashSet<HashSet<String>>();
, not only won't it compile, because the inner type of var
expects Set<String>
but it finds an HashSet<String>
. This would mean that var.add(new TreeSet<String>());
would be erronous (type incompatibility between HashSet
and TreeSet
).
Hope this helps.
Lets reduce your example to something simpler
//Array style valid an Integer is a Number
Number[] numArr = new Integer[10]
//Generic style invalid
List<Number> list1 = new ArrayList<Integer>();
//Compiled (erased) List valid, pre generics (java 1.4)
List list2 = new ArrayList();
The first line is the code with covariant arrays, which is legal java code. The next line contains a simple example for your problem with Lists of Integer and Number which is invalid. In the last line we have the valid and erased non generic lists.
Lets add an item to our Numbers, 1.5 seems like a reasonable number to me^^
//this will compile but give us a nice RuntimeException
numArr[0] = 1.5f
//This would compile and thanks to type erasure
//would even run without exception
list1.add(1.5f)
RuntimeExceptions should not happen in valid code, but numArr can only hold integers and not Numbers as one would belive. Generics catch this error at compile time as they are not covariant.
And here is the reason why these lists of Number and Integer cannot be accepted as the same. The methods provided by both Lists accept different arguments, the Integer list is more limited by only accepting Integers. This means that the interfaces provided by both Lists are not compatible.
List<Number>.add(Number n)
List<Integer>.add(Integer n)
The same is valid for your Sets
Set<Set<String>>.add(Set<String> s)
HashSet<HashSet<String>>.add(HashSet<String> s)
The add and other methods of both types are not compatible.
(second try at answer, hope i didn't mindread someone this time)
精彩评论