Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Function.identity() break type reification but t -> t does not?

Answers found at Java 8 lambdas, Function.identity() or t->t seem to imply that Function.identity() is almost always equivalent to t -> t. However, in the testcase seen below, replacing t -> t by Function.identity() results in a compiler error. Why is that?

public class Testcase {

    public static <T, A, R, K, V> Collector<T, A, R> comparatorOrdering(
            Function<? super T, ? extends K> keyMapper,
            Function<? super T, ? extends V> valueMapper,
            Comparator<? super K> keyComparator,
            Comparator<? super V> valueComparator) {
        return null;
    }

    public static void main(String[] args) {    
        Map<Integer, String> case1 = Stream.of(1, 2, 3).
                collect(comparatorOrdering(t -> t, t -> String.valueOf(t),
                        Comparator.naturalOrder(), Comparator.naturalOrder()));
        Map<Integer, String> case2 = Stream.of(1, 2, 3).
                collect(comparatorOrdering(Function.identity(), t -> String.valueOf(t),
                        Comparator.naturalOrder(), Comparator.naturalOrder()));
    }
}

Case 1 compiles just fine but case 2 fails with:

method comparatorOrdering in class Testcase cannot be applied to given types;
                collect(comparatorOrdering(Function.identity(), t -> String.valueOf(t),
  required: Function<? super T#1,? extends K>,Function<? super T#1,? extends V>,Comparator<? super K>,Comparator<? super V>
  found: Function<Object,Object>,(t)->Strin[...]Of(t),Comparator<T#2>,Comparator<T#3>
  reason: inferred type does not conform to upper bound(s)
    inferred: Object
    upper bound(s): Comparable<? super T#4>,T#4,Object
  where T#1,A,R,K,V,T#2,T#3,T#4 are type-variables:
    T#1 extends Object declared in method <T#1,A,R,K,V>comparatorOrdering(Function<? super T#1,? extends K>,Function<? super T#1,? extends V>,Comparator<? super K>,Comparator<? super V>)
    A extends Object declared in method <T#1,A,R,K,V>comparatorOrdering(Function<? super T#1,? extends K>,Function<? super T#1,? extends V>,Comparator<? super K>,Comparator<? super V>)
    R extends Object declared in method <T#1,A,R,K,V>comparatorOrdering(Function<? super T#1,? extends K>,Function<? super T#1,? extends V>,Comparator<? super K>,Comparator<? super V>)
    K extends Object declared in method <T#1,A,R,K,V>comparatorOrdering(Function<? super T#1,? extends K>,Function<? super T#1,? extends V>,Comparator<? super K>,Comparator<? super V>)
    V extends Object declared in method <T#1,A,R,K,V>comparatorOrdering(Function<? super T#1,? extends K>,Function<? super T#1,? extends V>,Comparator<? super K>,Comparator<? super V>)
    T#2 extends Comparable<? super T#2>
    T#3 extends Comparable<? super T#3>
    T#4 extends Comparable<? super T#4> declared in method <T#4>naturalOrder()

My environment is Windows 10, 64-bit, Oracle JDK build 1.8.0_92-b14.

UPDATE: Seeing as this compiles under ecj, I have a follow-up question: Is this a bug in javac? What does the JLS have to say about this case?

like image 637
Gili Avatar asked Jun 26 '16 03:06

Gili


2 Answers

The difference between i -> i and Function.identity() is apparent in situation where (1) collectors are nested and (2) upcast is needed somewhere in deep level of nesting.

Example: Suppose we are to classify elements in List<Object> into map with particular lists by element class. (It resembles Guava ClassToInstanceMap except the value is List, so something like hypothetical ClassToInstanceMultimap.) The downstream collector toList() normally collects values into List<Object>. However, if the value type of map is wildcard type List<?>, type inference cannot simply match it. The solution is to adapt collector to pretend it collects List<?>, which is done by intermediate collectingAndThen collector.

The point is now, what should the finisher function look like? Lambda i -> i behaves like Function<List<Object>, List<?>>, which allows upcast. Function.identity() with fixed input and output type T gives no room for upcast we need, hence code won't compile.

import static java.util.function.Function.identity;
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;

public static void main(String[] args) {
    class Apple {}
    class Pear {}
    List<Object> list = List.of(new Apple(), new Pear(), new Apple());
    Map<Class<?>, List<Object>> obj;  
    obj = list.stream().collect(groupingBy(Object::getClass, toList())); // compiles
    Map<Class<?>, List<?>> wild;
    wild = list.stream().collect(groupingBy(Object::getClass, toList())); // wont compile
    wild = list.stream().collect(groupingBy(Object::getClass, collectingAndThen(toList(), i -> i)));  // compiles
    wild = list.stream().collect(groupingBy(Object::getClass, collectingAndThen(toList(), identity())));  // wont compile
    wild = list.stream().collect(groupingBy(Object::getClass, collectingAndThen(toList(), (List<Object> i) -> (List<?>)i)));  // compiles
    System.out.println(wild);
}
like image 143
Tomáš Záluský Avatar answered Nov 15 '22 12:11

Tomáš Záluský


Ecj is able to infere the correct(?) type argument (Integer) to match the constraints. Javac for some reason comes to a different result.

Thats not the first time javac/ecj behave differently in inference of type parameters.

In that case you can give javac a hint with Function.<Integer>identity() to make it compileable with javac.

For the difference between Function.identity() and t->t:

  • Function.identity() is Function<T,T>
  • t->t in that case is Function<? super Integer, ? extends Integer>

So t->t is more flexible in the methods it can match to.

like image 33
k5_ Avatar answered Nov 15 '22 14:11

k5_