1

I have this code that I try to convert the method reference ("String::length") into equivalent lambda expression.

 Stream<String> s = Stream.of("hello", "your name", "welcome", "z");

 List<String> list = s.sorted((a, b) -> Comparator.comparingInt(String::length).compare(a, b)).collect(toList());

 // List<String> list = s.sorted((a, b) -> Comparator.comparingInt( p -> {return ((String)p).length();}).compare(a, b)).collect(toList());

The only way it works is outlined in the commented line. I need to cast the argument "p".

Seems like at compile time it specifies the type of the argument "p" to be an Object if I use the lambda expression and I need to cast explicitly. See below:

<Object> Comparator<Object> java.util.Comparator.comparingInt(ToIntFunction<? super Object> keyExtractor)

When I use a String::length method reference then at compile time the implicit argument is correctly understood as a String instance. What is so special in this case? See below.

<String> Comparator<String> java.util.Comparator.comparingInt(ToIntFunction<? super String> keyExtractor)
Mureinik
  • 297,002
  • 52
  • 306
  • 350
Khanna111
  • 3,627
  • 1
  • 23
  • 25
  • 1
    This should work too (without cast): `(String p) -> p.length()`. – tsolakp Mar 02 '18 at 21:00
  • 2
    `(a, b) -> Comparator.comparingInt(String::length).compare(a, b)` = `Comparator.comparingInt(String::length)::compare` = `Comparator.comparingInt(String::length)` – shmosel Mar 02 '18 at 21:01
  • 2
    I think what OP is asking is why in `p -> p.length()` the `p` variable is not recognized as `String` by Java compiler. – tsolakp Mar 02 '18 at 21:06
  • @tsolakp: yes you are correct. – Khanna111 Mar 02 '18 at 21:07
  • 1
    @Khanna11. I think it is because that call to `comparingInt` does not specify what type will be passed to it and by default it assumes an Object. To give a hint to compiler you need to either use method reference, my previous commented syntax or this: `Comparator.comparingInt( p -> p.length() )`. – tsolakp Mar 02 '18 at 21:11
  • @tsolakp: I think you might be up to something here when you mention "hint". Hopefully that is documented someplace. – Khanna111 Mar 02 '18 at 21:13
  • @tsolakp or simply `Comparator.comparingInt((String p) -> p.length())....` – Ousmane D. Mar 02 '18 at 21:13
  • @tsolakp: perhaps you can put that in an answer so I can tick it. – Khanna111 Mar 02 '18 at 21:20
  • That unnessary indirection confuses the compiler just like us. Actually, it's the chaining of method invocations. When you use, `s.sorted(Comparator.comparingInt(p -> p.length())) .collect(toList())`, there is no problem. – Holger Mar 03 '18 at 10:52

3 Answers3

3

Instead of using an anonymous lambda that would implement the functional Comparator interface, just use a straight-up Comparator:

List<String> list = 
    s.sorted(Comparator.comparingInt(String::length)).collect(toList());
Mureinik
  • 297,002
  • 52
  • 306
  • 350
3

EDIT About why the type of p is not inferred.

The type of p is not automatically inferred to be String for very much the same reasons why it is not inferred in the following example:

String a = "a";
String b = "b";
Comparator.comparingInt(p -> p.length).compare(a, b);

Here, it fails with the message that Object does not have method length. To understand why, consider a (very rough approximation of) the abstract syntax tree of this expression:

                                                                  Apply
                                                                 /     \
                                                                /       \
                                                               /         \
                                                              /           \
                                             _______________ /             \__
                                            /                                 \
                                           /                                   \
                                          /                                     \
                                  MemberSelection                             ArgumentList(2)
                                  /             \______                       /             \
                                 /                     \                     /               \
                           Apply                      compare              a: String     b: String
                     _____/     \______
                    /                  \
                   /                    \
          MemberSelection             Lambda
          /          |                 |    \
         /           |                 |     \
Comparator     comparingInt       Variable    MemberSelection
                                       |          /      \
                                       p         p       length

As you can see, the type information about String is completely in the right part of the AST, whereas the variable binder p and the entire closure are on a left branch of the AST.

It just happens to be that the type inference algorithm always works locally in top-down fashion. Once it descends into the left subtree and fails to infer the type of p, it will not walk back up the tree and search in the right subtree for additional hints. That would be too complicated to implement, and the further the type-checker would walk away from the problematic binder p, the less clear the error messages about failed type inference would become. The type inference algorithm does not make any attempts to typecheck the entire program globally.


You don't need the (a, b) -> ... part at all, the Comparator.compare(...) already produces a comparator:

List<String> list = s.
  sorted(Comparator.comparingInt(String::length)).
  collect(toList());

does what you probably wanted.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • 2
    I know that. The question is why the "p" is not recognized as a String. – Khanna111 Mar 02 '18 at 21:12
  • @Khanna111 I've added a detailed discussion about why the type inference algorithm fails in this case. – Andrey Tyukin Mar 02 '18 at 21:43
  • 1
    @AndreyTyukin I understand what you mean by "left to right", but this is a pretty misleading characterization of how Java's type inference works. But you are correct that _in this case_ -- an invocation expression `a.b()`, where the type of `a` has type variables -- we cannot do member selection for `b()` before we know those variables. So when the receiver in a chain is a generic method invocation _and_ we cannot infer those type variables solely from the method arguments, we get stuck. – Brian Goetz Mar 03 '18 at 19:45
  • @BrianGoetz The above sketch is not an exact representation of what's going on inside the javac. I'm even pretty sure that the real AST does not contain any Apply nodes at all, because Java is not an FP language, so `Comparator.comparingInt` will not be considered a function. It's rather a hypothetical AST for a hypothetical compiler of a Java-like language. That's not the point. The point is that `p` and `String` end up in vastly different subtrees no matter how you represent it, and the typechecker fails to infer type of `p` before it even sees the `String` in another subtree. – Andrey Tyukin Mar 03 '18 at 20:48
  • @BrianGoetz in the previous comment, by "Java-like" I mean that the typechecker will not come back from the "left" subtree with some weird universally quantified `forall a . Length => Comparator`, and then happily continue the search for an instance of some `Length`-"typeclass" or something like that. – Andrey Tyukin Mar 03 '18 at 20:55
2

The full lambda for String::length is:

(String p) -> p.length()

It can also be written using a block, but it's more common to use the simpler expression above:

(String p) -> { return p.length(); }

If the compiler can infer the type, you may omit it:

p -> p.length()

But you're using this:

Comparator.comparingInt(p -> p.length())

That is all the compiler sees when it needs to infer type of p. So, what is p? No idea, the compiler says. So you must explicitly specify the type:

// Explicit type in parameter list
Comparator.comparingInt((String p) -> p.length())

// Explicit type on generic type parameter
Comparator.<String>comparingInt(p -> p.length())

// You can do both, but why would you?
Comparator.<String>comparingInt((String s) -> s.length())

// Explicit type of referenced method
Comparator.comparingInt(String::length)

Notice that none of the code uses casts. The above is all type-safe, unlike the code you wrote with a cast. Do not use cast!

All 4 calls above returns a Comparator<String>. That comparator can be used to e.g. sort a List<String>, but will give compile error if you try to sort any other type of list.

When you cast like this:

Comparator.comparingInt(p -> ((String) p).length())

it returns a Comparator<Object>, which means that you could give that comparator when trying to sort any type of list, e.g. a List<Integer>. It will compile, but fail at runtime. The use of a cast has made to code not type-safe. As I said, don't do that.

Andreas
  • 154,647
  • 11
  • 152
  • 247