2

Suppose we have the following code:

public class Main {
    public static void main(String[] args) {

    }

    private static <K, V extends Comparable<V>> Map<K, V> sorted(Map<K, V> map) {
        return map.entrySet()
                .stream()
                .sorted(Comparator.comparing(Entry::getValue))
                .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
    }
}

A fairly simple snippet - we take a map and return another map that represents the same map, but sorted according to its values.

Let's change the functionality a little bit - instead of returning something based on an argument, let's introduce and work on a static object:

public class Main {
    public static void main(String[] args) {

    }

    private static Map<Integer, String> map = new HashMap<>();

    private static <K, V extends Comparable<V>> Map<K, V> sorted() {
        return map.entrySet()
                .stream()
                .sorted(Comparator.comparing(Entry::getValue))
                .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
    }
}

Note that the only thing we changed was getting rid of the argument and, instead, working with a static field. One may wonder why then introduce the generics K and V at all - after all they will always be Integer and String, respectively. But wouldn't that mean that Java can easily deduce those arguments? Why exactly does it fail with an error of:

Error:(16, 25) java: incompatible types: inference variable K has incompatible bounds
equality constraints: K,K
lower bounds: java.lang.Integer

I have seen some questions regarding similar error messages, but I couldn't get any information from there. Why exactly does this fail? It's maybe worth to mention that while the compiler produces the above error, when using IntelliJ IDEA, the Entry::getKey and Entry::getValue method references are highlighed in red and, when hovered over, the following message gets displayed:

Non-static method cannot be referenced from a static context.

Which is weird, since I see nothing alike in the actual error message from javac.

Fureeish
  • 12,533
  • 4
  • 32
  • 62
  • The type arguments are inferred at the call site. If you called `Main.sorted()`, and this were allowed, you'd get back a reference to a `Map`, but the keys and values are actually Integer and String, respectively. – Andy Turner Nov 21 '19 at 22:24
  • @AndyTurner incompatible references at runtime (however, this would actually be inspectable during compile time) are nothing new, so in the case you introduced I would either expect a compiler error or a ClassCastException. I see no reason why something like that wouldn't be allowed, but I am completely okay with the explanation that the developers in charge opted for such design to, quite possibly, avoid tricky situations. So, basically, was it simply a design choice that the *call site* relation with arguments is as it is? Could you possibly rephrase it and post an answer? – Fureeish Nov 21 '19 at 22:35

1 Answers1

1

The concrete type arguments are determined at the call site.

For example, any of these are legal calls:

Map<Void, Integer> mapA = Main.sorted();
Map<String, String> mapB = Main.sorted();
Map<Integer, GregorianCalendar> mapC = Main.sorted();

...but, given the method definition, they would all return the same value; since they are of different types, at least two of mapA..C are type-incorrect (and, in fact, all three are).

The sole point of generics is to allow you to omit explicit casts; this is advantageous, because the compiler is able to reason better than you about whether a cast (or, rather, all the casts throughout the code) are safe.

So, if you write that a method returns a Map<Void, Integer>, or whatever, the compiler is going to do everything it can to make sure that it really does return a Map<Void, Integer>. Or, to put this another way, the compiler ensures that it returns a Map where every element of the keySet() can be safely cast to a Void; and every element of values() can be safely cast to an Integer.

Thus far I have only talked about concrete type parameters: Void, Integer, String, GregorianCalendar. But the compiler doesn't draw distinction between such type parameters and type variables. Thus, if you say that a method returns a Map<K, V>, the compiler is going to do everything it can to make sure that it returns a Map where every element of the keySet() can be cast to K, and every element of the values() can be cast to V.

The important point here is that this has to work for any K and any V, because, as stated in the first line of the question, the concrete values of K and V are determined at the call site.

There are only a small number of Ks that an Integer can be safely cast to (Integer, Number, Object, Serializable); a null Integer can be cast to any other type, but the compiler doesn't know if the Integers are null. So the compiler is simply saying "I can't be sure that these casts would be safe, so I'm going disallow them".

(A similar situation applies to V).

Your method only returns a safe value when K and V are Integer and String respectively, because map is a Map<Integer, String>. As such, you don't need type variables, as the method will always return a Map<Integer, String>.

private static Map<Integer, String> sorted() {
    return map.entrySet()
            .stream()
            .sorted(Comparator.comparing(Entry::getValue))
            .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
}

(or it could be a Map<Object, String>, or whatever. The point is that you don't need type variables).

Andy Turner
  • 137,514
  • 11
  • 162
  • 243
  • Stellar, thank you. The rationale you provided the the paragraph starting with "*There are only a small number [...]*" is exactly what I was looking for - a fair explanation of a design choice. – Fureeish Nov 21 '19 at 23:10