Why does it matter if I use a method reference or a lambda here?
Asked Answered
D

3

8

When I try to compile this code

import java.util.Optional;

public class GenericTest {

    public static void main(String[] args) {
        Optional.empty().map(o -> getStringClass(o)).orElse(String.class);
    }

    static Class<?> getStringClass(Object arg) {
        return String.class;
    }

}

javac will fail with the following error:

GenericTest.java:6: error: method orElse in class Optional cannot be applied to given types;
                Optional.empty().map(o -> getStringClass(o)).orElse(String.class);
                                                            ^
  required: Class<CAP#1>
  found: Class<String>
  reason: argument mismatch; Class<String> cannot be converted to Class<CAP#1>
  where T is a type-variable:
    T extends Object declared in class Optional
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
1 error

But if I use a method reference instead, javac will compile the code just fine:

import java.util.Optional;

public class GenericTest {

    public static void main(String[] args) {
        Optional.empty().map(GenericTest::getStringClass).orElse(String.class);
    }

    static Class<?> getStringClass(Object arg) {
        return String.class;
    }

}

Why does it make a difference if I use a method reference or a lambda expression?

According to my understanding, both the method reference and the lambda have the type Function<Object,Class<?>>, so I don't see the difference here.
The eclipse java compiler (ecj) won't compile both versions by the way.

Drone answered 25/5, 2019 at 11:18 Comment(7)
Which version of javac are you using?Gaeta
Version please, it fails to compile for me in both ways. Java-8Outmaneuver
javac 1.8.0_172 and javac 10.0.2. I know Java 10 is not recent, but I have it installed, so..Drone
Which Java implementation do you use? Where did you get it from, Oracle?Pear
java version "1.8.0_172" Java(TM) SE Runtime Environment (build 1.8.0_172-b11) Java HotSpot(TM) 64-Bit Server VM (build 25.172-b11, mixed mode)Drone
are you compiling this with eclipse by any chance?Quiroz
The second code works fine one ideone.Drone
E
6

This is a known limitation of the compiler's type inference system, it doesn't work with chained method invocations like in your first code snippet.

Possible workarounds? Use an explicitly typed lambda expression:

Optional.empty()
        .map((Function<Object, Class<?>>) o -> getStringClass(o))
        .orElse(String.class);

or an exact method reference (as you've already tried):

Optional.empty().map(GenericTest::getStringClass).orElse(String.class);

or add a type witness:

Optional.empty().<Class<?>>map(o -> getStringClass(o)).orElse(String.class);

Related post with a similar issue.

Exemplify answered 25/5, 2019 at 11:55 Comment(1)
Do you have any idea why the first piece of code doen't work but the second does? I know how I can avoid this kind of error, that is NOT the question. Please read the question again, and if you have an idea why it behaves differently, feel free to post an explanation.Drone
Q
6

Method chaining strikes again. You can read here of why the designers of this feature found it complicated to implement (it has to do with the fact that lambda's/method references are poly expressions - their types are context dependent). Such a feature would require some extra burden on the compiler - and while your example would be a fairly trivial example to solve, javac has to care about a lot more than triviality; thus this is not yet implemented, even in java-12.

The easiest solution IMO, since this has to do with method chaining (and in your case this is possible), is not to chain:

Optional<Class<?>> first = Optional.empty().map(o -> getStringClass(o));
Class<?> second = first.orElse(String.class);
Quiroz answered 25/5, 2019 at 12:17 Comment(5)
Yeah, not to chain is an appropriate solution, but I was asking why I can compile the second piece of code.Drone
@JohannesKuhn you can't... Either some weird javac version or you use eclipse ecj with an old versionQuiroz
@JohannesKuhn right. I don't really care what IDEA shows, at all, only what javac does and they indeed compile just fine, which is not what I would expect.Quiroz
Yes. Yes, it compiles fine. And that is unexpected. And that's my question here, because I'm not smart enough to answer this question myself. I don't care how I can avoid this error, just "can someone explain this (for me unexpected) behavior".Drone
@JohannesKuhn this looks like the explanation... I will delete this answer soon, as you are correctQuiroz
G
0

As others have said it's a limitation with how type infereance works. There is some detail as to why here: https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.13.2

Basically, when you use the lambda there is no type on the argument you're passing here:

Optional.empty().map(o -> getStringClass(o)).orElse(String.class);

Since there is no type argument (and no ability to provide one) this will be an error. However with method refrences it's able to infer the type from the method parameters so it will work just fine.

Guberniya answered 2/5, 2022 at 0:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.