The javadoc for Arrays.sort(T[] a, Comparator<? super T> c)
from the package java.util
in the standard library for Java 20 API docs reads as follows:
public static void sort(T[] a, Comparator<? super T> c) Sorts the specified array of objects according to the order induced by the specified comparator. All elements in the array must be mutually comparable by the specified comparator (that is, c.compare(e1, e2) must not throw a ClassCastException for any elements e1 and e2 in the array).
This sort is guaranteed to be stable: equal elements will not be reordered as a result of the sort.
Implementation note: This implementation is a stable, adaptive, iterative mergesort that requires far fewer than n lg(n) comparisons when the input array is partially sorted, while offering the performance of a traditional mergesort when the input array is randomly ordered. If the input array is nearly sorted, the implementation requires approximately n comparisons. Temporary storage requirements vary from a small constant for nearly sorted input arrays to n/2 object references for randomly ordered input arrays.
The implementation takes equal advantage of ascending and descending order in its input array, and can take advantage of ascending and descending order in different parts of the same input array. It is well-suited to merging two or more sorted arrays: simply concatenate the arrays and sort the resulting array.
The implementation was adapted from Tim Peters's list sort for Python ( TimSort). It uses techniques from Peter McIlroy's "Optimistic Sorting and Information Theoretic Complexity", in Proceedings of the Fourth Annual ACM-SIAM Symposium on Discrete Algorithms, pp 467-474, January 1993.
Type Parameters: T - the class of the objects to be sorted Parameters: a - the array to be sorted c - the comparator to determine the order of the array. A null value indicates that the elements' natural ordering should be used. Throws: ClassCastException - if the array contains elements that are not mutually comparable using the specified comparator IllegalArgumentException - (optional) if the comparator is found to violate the Comparator contract
Emphasis mine.
Doesn't the compiler guarantee (through the use of the type system and the generic signature of the method) that all elements of a
are in fact mutually comparable using c
and that c.compare(e1, e2)
will never throw ClassCastException
, provided c
is not null?
Presumably, the "actual" signature of toArray
, with the type parameters erased, would look like so: static void sort(Object[] a, Comparator c)
and the signature of c.compare
(let's assume c
has the static type Number
in our program) would look like so: int compare(Object o1, Object o2)
and would contain two casts to Number
in its implementation:
Number e1 = (Number) o1;
Number e2 = (Number) o2;
//...
These casts succeed if and only if the runtime type of o1
and o2
is either Number
or a sub-type of Number
.
The runtime type of o1
and o2
is either Number
or a sub-type of Number
if and only if the runtime type of a
is either Number[]
or a sub-type of Number[]
, e.g. Integer[]
.
The runtime type of a
is either Number[]
or a sub-type of Number[]
if and only if the static type of a
is either Number[]
or a sub-type of Number[]
. (if the runtime type of a
is not Number[]
or a sub-type of Number[]
but the static type of a
is, a ClassCastException
will be thrown before the body of Arrays.sort
is entered)
That the static type of a
is either Number[]
or a sub-type of Number[]
follows from the method signature static <T> void sort(T[] a, Comparator<? super T> c)
because the static type of c
is Number
(? super T
is Number
so T
is either Number
or a sub-type of Number
).
I've written some tests to try and get Arrays.sort
to throw a ClassCastException
, and only really got it when passing null
for c
instead of an actual comparator.
package org.example.basics.generics.arrays;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Comparator;
class ArraysTest {
private static final Comparator<Number> compareAsWholeNumber = new Comparator<Number>() {
@Override
public int compare(java.lang.Number o1, java.lang.Number o2) {
return Long.compare(o1.longValue(), o2.longValue());
}
};
@Test
public void sortingIntsThrowsNoException() {
Integer[] ints = new Integer[]{4, 2, 3};
Assertions.assertDoesNotThrow(() -> {
Arrays.sort(ints, compareAsWholeNumber);
});
}
@Test
public void sortingDoublesThrowsNoException() {
Double[] doubles = new Double[]{4.0, 2.0, 3.0};
Assertions.assertDoesNotThrow(() -> {
Arrays.sort(doubles, compareAsWholeNumber);
});
}
@Test
public void sortingNumbersThrowsNoException() {
Number[] numbers = new Number[]{4.0, 2, 3.0f};
Assertions.assertDoesNotThrow(() -> {
Arrays.sort(numbers, compareAsWholeNumber);
});
}
// @Test
// public void sortingObjectsIsCompileError() {
// Object[] numbers = new Number[]{4.0, 2, 3.0f};
// Arrays.sort(numbers, compareAsWholeNumber);
// }
@Test
public void sortingDisguisedStringsThrowsClassCastException() {
Throwable e = Assertions.assertThrows(ClassCastException.class, () -> {
Arrays.sort((Number[]) new Object[]{"4.0", "2", "3.0f"}, compareAsWholeNumber);
});
e.printStackTrace();
}
@Test
public void sortingDisguisedStringsThrowsClassCastExceptionAtArrayAssignment() {
Assertions.assertThrows(ClassCastException.class, () -> {
Number[] numbers = (Number[]) new Object[]{"4.0", "2", "3.0f"};
});
}
@Test
public void sortingNonComparableWithNullComparatorThrowsClassCastException() {
Number[] numbers = new Number[]{4.0, 2, 3.0f};
Throwable e = Assertions.assertThrows(ClassCastException.class, () -> {
Arrays.sort(numbers, null);
});
e.printStackTrace();
}
}
The above test suite runs successfully on my machine using Eclipse Temurin 20.0.2.
Note especially the last 4 tests.
sortingObjectsIsCompileError
: Passing an array with a static type that is neither Number[]
nor a sub-type of Number[]
to sort
is a compile error.
Uncommenting that test and trying to compile produces the following for me:
TestToArray.java:46: error: no suitable method found for sort(Object[],Comparator) Arrays.sort(numbers, compareAsWholeNumber); ^ method Arrays.<T#1>sort(T#1[],Comparator<? super T#1>) is not applicable (inference variable T#1 has incompatible bounds upper bounds: Number,Object lower bounds: Object) method Arrays.<T#2>sort(T#2[],int,int,Comparator<? super T#2>) is not applicable (cannot infer type-variable(s) T#2 (actual and formal argument lists differ in length)) where T#1,T#2 are type-variables: T#1 extends Object declared in method <T#1>sort(T#1[],Comparator<? super T#1>) T#2 extends Object declared in method <T#2>sort(T#2[],int,int,Comparator<? super T#2>)
sortingDisguisedStringsThrowsClassCastException
: As the name suggests, a ClassCastException
is indeed thrown here, but this is misleading: Printing the stacktrace to std_out yields the following (reduced for brevity):
java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.Number; ([Ljava.lang.Object; and [Ljava.lang.Number; are in module java.base of loader 'bootstrap') at org.example.basics.generics.arrays.TestToArray.lambda$sortingDisguisedStringsThrowsClassCastException$3(TestToArray.java:52) <3 internal lines> at org.example.basics.generics.arrays.TestToArray.sortingDisguisedStringsThrowsClassCastException(TestToArray.java:51) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) <9 internal lines> at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) <30 internal lines> at jdk.proxy1/jdk.proxy1.$Proxy2.stop(Unknown Source) <7 internal lines> at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Note that: 1. the failed cast is from [Ljava.lang.Object
to [Ljava.lang.Number
rather than from java.lang.Object
to java.lang.Number
as we would expect it to be for a failed cast inside the compare
method and 2. the ClassCastException
is thrown from inside the TestToArray
class and not from the Arrays
class.
The test sortingDisguisedStringsThrowsClassCastExceptionAtArrayAssignment
shows that simply assigning the array of String[]
to an array of Number[]
throws that same ClassCastException
.
The last test, sortingNonComparableWithNullComparatorThrowsClassCastException
does in fact throw the expected ClassCastException
and this is the only case where I was able to get the sort
method itself to throw this exception. Printing the stacktrace to std_out yields the following (reduced for brevity):
java.lang.ClassCastException: class java.lang.Double cannot be cast to class java.lang.Integer (java.lang.Double and java.lang.Integer are in module java.base of loader 'bootstrap') at java.base/java.lang.Integer.compareTo(Integer.java:72) at java.base/java.util.ComparableTimSort.countRunAndMakeAscending(ComparableTimSort.java:320) at java.base/java.util.ComparableTimSort.sort(ComparableTimSort.java:188) at java.base/java.util.Arrays.sort(Arrays.java:1041) at java.base/java.util.Arrays.sort(Arrays.java:1228) at org.example.basics.generics.arrays.TestToArray.lambda$sortingNonComparableWithNullComparatorThrowsClassCastException$5(TestToArray.java:67) <30 internal lines> at jdk.proxy1/jdk.proxy1.$Proxy2.stop(Unknown Source) <7 internal lines> at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Note that in this case the failed cast is from java.lang.Double to java.lang.Integer and the exception is actually thrown from the Arrays
class.
Is the only case where Arrays.sort(T[] a, Comparator<? super T> c)
can throw ClassCastException
the case where the value null
is passed for the comparator c
?
Context: I'm hoping to find out what kind of mistakes I need to make in order for the type system to no longer be able to function as a safeguard against ClassCastException
for sort
. So far it seems that the compiler will at the very least issue an "unchecked" warning which is reassuring. Is there any way to get a ClassCastException
from sort
without the compiler issuing "unchecked" warnings (and without passing null
in place of c
)?
The exception is super simple to trigger if you have raw types (whether accidental or not):
class FawltyComparators {
@Test
void test() {
final Comparator numberComparer = new NumberComparator();
final Object[] array = { 1, 2, "3", "4" };
Arrays.sort(array, numberComparer);
}
private static final class NumberComparator implements Comparator<Number> {
@Override
public int compare(final Number o1, final Number o2) {
return 0; // doesn't matter
}
}
}
But this will still give you compiler warnings:
FawltyCompators.java:17: warning: [unchecked] unchecked method invocation: method sort in class Arrays is applied to given types
Arrays.sort(array, numberComparer);
^
required: T[],Comparator<? super T>
found: Object[],Comparator
where T is a type-variable:
T extends Object declared in method <T>sort(T[],Comparator<? super T>)
FawltyCompators.java:17: warning: [unchecked] unchecked conversion
Arrays.sort(array, numberComparer);
^
required: Comparator<? super T>
found: Comparator
where T is a type-variable:
T extends Object declared in method <T>sort(T[],Comparator<? super T>)