Why Arrays.sort(T[] a, Comparator<? super T> c) infers T as Object for a 2d array?

Viewed 214

Say if I want to sort a 2d array. (just reorder the rows, don't touch data within each row).

In following snippet: all 3 cases use the same Arrays.sort(T[] a, Comparator<? super T> c) method signature. Case (a) works fine. However, just by adding a if condition to the second argument, the inference of T changes. I couldn't comprehend why.

        // array contains 3 tuples, sort it by the first element, then second element
        int[][] array1 = new int[3][2];
        array1[0] = new int[]{1,2};
        array1[1] = new int[]{2,3};
        array1[2] = new int[]{2,4};

        // Case (a): compiles good, tuple is inferred as int[]
        Arrays.sort(array1, Comparator.comparingInt(tuple -> tuple[0]));  // Arrays.sort(T[] a, Comparator<? super T> c) correctly infers that T refers to int[]

        // Case (b.1): compile error: incompatible types
        // tuple is now inferred as Object, why?
        Arrays.sort(array1,
                (a1, a2) -> a1[0] == a2[0] ?
                        Comparator.comparingInt(tuple -> tuple[1]) : Comparator.comparingInt(tuple -> tuple[0]));  

        // Case (b.2): compile error: incompatible types
        Arrays.sort(array1, Comparator.comparingInt(tuple -> tuple[0]).thenComparingInt(tuple -> tuple[1])); 
        
        // Case (c): if downcast tuple[0] to ((int[])tuple)[0], then (b) works fine. 

Update:

  1. Enlightened by the comments, I soon realized that case (b.1) is actual not valid. The lambda in (b.1) suppose to return an integer, not a comparator. E.g. Arrays.sort(array1, (a1, a2) -> a1[0] == a2[0] ? 0 : 1);
  2. In all other scenarios, I see Comparator.<int[]>comparingInt(...) forces the inference correctly.
2 Answers

Short answer: the compiler is not smart enough to infer through such complex expressions. It needs some help inferring the type:

Arrays.sort(array1, Comparator.<int[]>comparingInt(tuple -> tuple[0]).thenComparingInt(tuple -> tuple[1]));

Related JEP: http://openjdk.java.net/jeps/101

As for the ternary expression case, I think it needs further adaptation, since you need to return an int in the lambda, not a Comparator:

Arrays.sort(array1,
            (a1, a2) -> a1[0] == a2[0] ?
                    Comparator.<int[]>comparingInt(tuple -> tuple[1]).compare(a1, a2) :
                    Comparator.<int[]>comparingInt(tuple -> tuple[0]).compare(a1, a2));

After digging a bit further, here's an intuitive explanation on this behavior:

public interface Comparator<T> {
...
default Comparator<T> thenComparingInt(ToIntFunction<? super T> keyExtractor)
public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor)
...
}
  1. The public static method needs the caller to define T (as goes by the first <T> in the second method) when invoking the method. Otherwise it defaults to Object.

  2. This is unlike the non-static method, where T is already defined during instantiation.

So looking at Comparator.comparingInt(tuple -> tuple[0]);, compiler won't know what T is and hence defaults to Object.

To properly feed compiler the actual type T, we can explicitly define T in different places.

// define T when calling the method
Comparator.<int[]>comparingInt(tuple -> tuple[0]); 

// infer by return type, this is samilar to placing the rhs to Arrays.sort(int[][] a, rhs)
Comparator<int[]> c = Comparator.comparingInt(tuple -> tuple[0]); 

// explicit target type for lambda expression
Comparator.comparingInt((ToIntFunction<int[]>) tuple -> tuple[0]);

// explicit type for lambda parameter
Comparator.comparingInt((int[] tuple) -> tuple[0]);

// This works but it's doesn't change the inference of T as comments suggests below.
Comparator.comparingInt(tuple -> ((int[])tuple)[0]); 

Once it returns a Comparator<int[]> object (thus defined T as int[]), the instance method thenComparingInt is called upon knowing what T is.

Related