17

I am trying to sort List of employees by name then age using Java8 Comparator, I have created below Comparator but it gives me a compiler error

Type mismatch: cannot convert from Comparator<Object> to <unknown>

    Comparator<String> c = Comparator.comparing(s -> s.split("\\s+")[0])
            .thenComparingInt(s -> Integer.parseInt(s.split("\\s+")[1])); //compile error

but it works if I explicitly specify the Type

    Comparator<String> c = Comparator.<String, String> comparing(s -> s.split("\\s+")[0])
            .thenComparingInt(s -> Integer.parseInt(s.split("\\s+")[1])); //works

or by creating two Compartors and chain

    Comparator<String> name = Comparator.comparing(s -> s.split("\\s+")[0]);
    Comparator<String> age = Comparator.comparingInt(s -> Integer.parseInt(s.split("\\s+")[1]));
    Comparator<String> cc = name.thenComparing(age); //works

I have specified the type Comparator<String> on the left side but why auto type inference is not finding the correct Type and expecting to specify explicitly.

Could someone clarify on this?

Here is the code

    String[] arr = { "alan 25", "mario 30", "alan 19", "mario 25" };
    Comparator<String> c = Comparator.<String, String> comparing(s -> s.split("\\s+")[0])
            .thenComparingInt(s -> Integer.parseInt(s.split("\\s+")[1]));
    List<String> sorted = Arrays.stream(arr).sorted(c).collect(Collectors.toList());
    System.out.println(sorted);

output

[alan 19, alan 25, mario 25, mario 30]
Saravana
  • 12,647
  • 2
  • 39
  • 57
  • 1
    I'm not an expert on generic type inference, but I'm guessing this is just "too much auto-inference". Probably, it can't figure out the type of the `comparing()` method since it doesn't have any type to "anchor" it to, unlike the chaining method where you explicitly give a type to the result of `comparing()`. Regardless, I think it would be more readable to write your own `Comparator`, and only call `split` once. There's no prize for squeezing code into the fewest possible lines. – ajb Nov 09 '16 at 05:07
  • 2
    Target typing doesn’t work through chained method invocations, see [here](http://stackoverflow.com/a/28834656/2711488) and [here](http://stackoverflow.com/a/26883991/2711488) and [here](http://stackoverflow.com/a/31383947/2711488). However, you can simply use `Comparator.comparing(s -> s.replaceFirst("\\s+", " "))` instead of your two stage comparator and the outcome will be the same… – Holger Nov 09 '16 at 12:01
  • `Comparator.comparing((String s) -> s.split("\\s+")[0]).thenComparingInt(s -> Integer.parseInt(s.split("\\s+")[1]))` works but why this `String` is required, it can infer from `Comparator`, is it a limitation in type inference ? BTW using `java version "1.8.0_60"` – Saravana Nov 09 '16 at 18:56
  • @Holger, I’m not sure I got that. Your one-stage comparator would work nicely if all ages were guaranteed to be 2 digits, but I believe it would sort `[alan 102, alan 25, alan 8]` into this order, which is the opposite of the desired — am I right? – Ole V.V. Nov 10 '16 at 08:49
  • And thanks, @Holger, for the very useful links to the other places where you have excellently explained the stuff. – Ole V.V. Nov 10 '16 at 08:57
  • 2
    @Ole V.V.: indeed, it’s a deficiency of the chosen example, that doesn’t show numbers not having two digits, but I still would recommend to avoid the duplicated `split` operation and implement it as a single comparator, even if it gets more complex due to the number parsing. For larger lists, it might be even more efficient to convert all elements to a `(String,int)` type first, sort them, and convert them back to the `String` form… – Holger Nov 10 '16 at 10:55

1 Answers1

12

Java needs to know a type of all variables. In many lambdas it can infer a type, but in your first code snippet, it cannot guess the type of s. I think the standard way to solve that problem would be to declare it explicitly:

    Comparator<String> c = Comparator.comparing((String s) -> s.split("\\s+")[0])
            .thenComparingInt(s -> Integer.parseInt(s.split("\\s+")[1]));

If you look at this answer, it has a similar type declaration in the argument to Comparator.comparing().

Your method, explicitly giving the type arguments of comparing(), obviously works too.

For your other method, declaring two comparators, I am pretty confident that in this case Java can infer from the String on the left side of the assignment, just as in the conventional List <String> = new ArrayList<>();. When you go on to call thenComparing() in the same expression, Java can no longer see that the type from the left side is relevant. It would be a bit like int size = new ArrayList<>().size(); This works too:

    Comparator<String> name = Comparator.comparing(s -> s.split("\\s+")[0]);
    Comparator<String> c = name.thenComparingInt(s -> Integer.parseInt(s.split("\\s+")[1]));
Community
  • 1
  • 1
Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
  • 1
    @ole-v-v Comparator.comparing((String s) -> s.split("\\s+")[0]).thenComparingInt(s -> Integer.parseInt(s.split("\\s+")[1])) works but why this String is required, it can infer from Comparator, is it a limitation in type inference ? BTW using java version "1.8.0_60" – Saravana Nov 09 '16 at 18:58
  • 2
    How do you infer that? You know that `thenComparing()` should return a `Comparator` (to fit the left-hand side of the assignment), therefore `comparing()` needs to return `Comparator`, therefore `s` needs to be a `String`. The compiler does not go through this many steps of reasoning. Yes, it’s a limitation in type inference, but I think it’s a limitation introduced on purpose because otherwise the rules would get too complicated and programmers would not understand them. This is my best guess. – Ole V.V. Nov 09 '16 at 19:20
  • 2
    The biggest problem is, that target typing through chained invocations could change the available methods for the subsequent invocation, which was actually used for determining the previous’ target type, i.e. with `foo(x).bar(y)`, the return type of `foo(x)` determines whether and which `bar(…)` methods are available, but applying target typing to `foo(x)` depends on the actually chosen `bar(…)` method. – Holger Nov 10 '16 at 11:07
  • 1
    I’m quite sure that this restriction will be lifter some day, but possibly only when the base type doesn’t change, i.e `Comparator.comparing` always returns a `Comparator`, target typing would only affect its type parameter, and the subsequently chained method is not overloaded, which is the case for `thenComparingInt`. – Holger Nov 10 '16 at 11:09
  • Another option is to use so-called *type witness* by adding a parameter to the first method call: `Comparator.comparing()` – Alexander Ivanchenko May 24 '22 at 19:01