1

With Java 17 I want to compare map entries based upon some criteria, where the map entries are Java classes associated with some value, like this:

Comparator<Map.Entry<Class<?>, Integer>> MY_COMPARATOR =
    new Comparator<Map.Entry<Class<?>, Integer>>() { … }

Later I have a list of a list of classes and their associated values (whether it's counts, memory size, or whatever). The twist is that my code is so refined (tongue in cheek) that I happen to know that all these classes extend from FooBar:

List<Map.Entry<Class<? extends FooBar>, Integer>> foobarValues = new ArrayList<>();

Now I just want to sort those map entries using MY_COMPARATOR (to find the smallest/largest classes, the classes most encountered, or whatever MY_COMPARATOR uses as its criteria). But this doesn't work:

java.util.Collections.sort(foobarValues, MY_COMPARATOR);

Java (actually Eclipse 2022-09) tells me:

The method sort(List<T>, Comparator<? super T>) in the type Collections is not applicable for the arguments (List<Map.Entry<Class<? extends FooBar>,Integer>>, Comparator<Map.Entry<Class<?>,Integer>>)

I've been working with Java generics for a long time, and my best simplified explanation of what's happening is that the ? super T in Collections.sort() "hides" the lower layer of generics, and that ? super Map.Entry<Class<?>> has no relation to ? super Map.Entry<Class<? extends FooBar>>. (But still, really?.) Actually my mind can't quite grasp the full intricacies of the problem.

But let's say I don't care so much about the problem; I just want the code to work with no warnings. You and I know that the comparator can work with any class that is possible to exist, so it will never cause a ClassCastException and it will never cause heap pollution, and it will never even sneak into your house at night and kill your bamboo plant.

How can I tell Java that it's OK for me to use this comparator? This does not work:

Collections.sort(foo, (Comparator<Map.Entry<Class<? extends FooBar>, Integer>>)MY_COMPARATOR);

I would be happy casting the list being sorted, but this doesn't work either:

Collections.sort((List<Map.Entry<Class<?>, Integer>>)foo, MY_COMPARATOR);

This does work!

Collections.sort(foo, (Comparator<Map.Entry<Class<? extends FooBar>, Integer>>)(Object)comparator);

But to use it I have to add @SuppressWarnings("unchecked"), and Java 17 javac with -Xlint:all still complains, and besides I feel dirty using it.

I would be content with some workaround that would allow me to suppress the warning with @SuppressWarnings("unchecked") as long as javac lint still didn't complain. But the intermediate cast to Object is too much of a kludge for me to accept.

What should I be doing?

Garret Wilson
  • 18,219
  • 30
  • 144
  • 272
  • Your List is of type `...Class extends FooBar>...` but the comparator is simply `...Class>...`, as you saw. The casting further on in your question essentially confirms that is the issue. Why not define your `Comparator` as ` extends FooBar>` to begin with? – Rogue Oct 05 '22 at 16:13
  • 1
    @Rogue Probably because the comparator isn't specific to comparing map entries that have `Class extends FooBar>` as keys. – Sweeper Oct 05 '22 at 16:15
  • I suppose that's fair, but I'm pretty sure the language semantics for generics will bite you in that case (nested generics don't play very well here). From my understanding, a `Foo>` and a `Foo>` (unrelated A/B/X/Y classes) are just as different as `Foo` and `Foo` for this kind of type inference resolution. – Rogue Oct 05 '22 at 16:17
  • `MY_COMPARATOR` can work with any class in the world. Why should I constrain it to `FooBar` classes? And my list is really `List, Integer>>`, as it itself is parameterized in a captured generic type, so I don't even know what type it is ahead of time. But why should I care what type of class it is? I know `MY_COMPARATOR` can handle it! And yes, the "language semantics for generics [are] bit[ing] [me] in [this] case". The question is: we all agree this should work and no harm will come from it. And it's even logical. So how do I work around it? – Garret Wilson Oct 05 '22 at 16:20
  • I'll even be happy with a small kludge that I can suppress warnings on, but intermediate casting to `Object` is too much I think. Plus it still leaves warnings. – Garret Wilson Oct 05 '22 at 16:22
  • 2
    Change your declaration to `Comparator, Integer>> MY_COMPARATOR …` There is, by the way, no need to resort to `Collections.sort(…)` anymore. You can simply call `foobarValues.sort(MY_COMPARATOR);` – Holger Oct 05 '22 at 16:31
  • Wow, Holger, that did it!! (You don't know how many combinations of `extends` and `super` I tried. ) Please place your comment in a separate answer; I want to open a bounty to award it and show my appreciation. And thanks for reminding me about `List.sort()`. Yes, this is super-old code I'm trying to finally clean up. (One comment in that file said, "works on Eclipse 3.4M3 but not on Sun JDK 1.6.0_03-b05", so it was a while ago. ) – Garret Wilson Oct 05 '22 at 16:38

1 Answers1

1

I would suggest that you declare the comparator by applying the PECS principle. It should have the type:

Comparator<Map.Entry<? extends Class<?>, Integer>>

Both type parameters for Map.Entry should have ? extends (though just the first one is enough to fix the error) because a map entry is a producer of its keys and value types.

You could also add ? super to the type parameter of Comparator, since the comparator is a consumer of map entries, but that is not needed here because it is actually already written in the method signature of sort.

void sort(Comparator<? super E> c)

After that, you can write:

foobarValues.sort(MY_COMPARATOR);

If you cannot change the type of MY_COMPARATOR, one way to get the compiler to "just shut up" is to cast to Object first:

(Comparator<Map.Entry<Class<? extends FooBar>, Integer>>) (Object) MY_COMPARATOR

Casting from Object to a parameterised type is an unchecked cast, which is presumably what you are looking for.

This unchecked cast would be safe as far as I know, since a map entry cannot be used as a consumer of its key type. You can only get, but not set, its key.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • Holger had mentioned this as well, and indeed `Comparator, Integer>>` works! I'm grateful for the answer! I had went over the PECS principle, but in my mind that would give me `Comparator extends Map.Entry, Integer>>`. I'll have to say I'm a 90% expert on Java generics, but that last 10% is a doozy! haha – Garret Wilson Oct 05 '22 at 16:43
  • @GarretWilson See my edit. `Comparator` is a consumer here, not a producer! – Sweeper Oct 05 '22 at 16:45
  • Gah, good catch. Even with PECS in mind, I glossed over applying `extends` on `Class`. – Rogue Oct 05 '22 at 16:48
  • Oh, so you're saying that `sort(Comparator super E>)` is the same as my using `Comparator super Map.Entry>`? So in this case, the `? extends Class>` is functioning as a _producer_ to the `Map.Entry<>`, while `Comparator<>` is functioning as a _consumer_ to `sort()`? – Garret Wilson Oct 05 '22 at 16:54
  • I mean **if** `sort` were only declared to take `Comparator`, you would have to apply PECS at the `Comparator` level and write `Comparator super ...>` too. Since it's declared to take `Comparator super E>`, you don't have to do that, but it's fine if you do. `Map.Entry extends Class>, Integer>` reads "`Map.Entry` is a producer of `Class>`" and `sort(Comparator super E>)` reads "`sort` takes in a comparator, that is a consumer of `E`s". I think you should go through the PECS post again... @GarretWilson – Sweeper Oct 05 '22 at 17:02
  • 1
    @GarretWilson a common source of confusion is that types like `Map.Entry, Integer>` and `Map.Entry, Integer>` are *invariant*. The type arguments are types with wildcards, but the generic type (here `Map.Entry<…>`) has no wildcards. So neither of those types is a subtype of the other. But both are subtypes of `Map.Entry extends Class>, Integer>` (which is a type with wildcards). So, if the comparator can sort elements of type `Map.Entry extends Class>, Integer>`, it can also sort elements of the subtype `Map.Entry, Integer>`… – Holger Oct 05 '22 at 17:07
  • "So neither of those types is a subtype of the other." Yeah, I was getting that part. But … oh, I think I see: you've made `Map.Entry extends Class>, Integer>` a more general class by making the second level of generics non-specific. `Map.Entry` isn't a subtype of `Map.Entry`, but by saying `Map.Entry extends Z, Integer>` you take the type compatibility problem away at that level by making that level not a specific type. Something like that. It's hard to explain in simple words, but I think intuitively I understand. Thank you. – Garret Wilson Oct 05 '22 at 17:15