[Java]Comparator#reversed()のエラー解消から学ぶJavaコンパイラの弱点
きっかけ
Comparator#reversed()
という、Comparator
の逆順を返してくれる便利なメソッドを知り、使おうとしたところ、以下のエラーが出てしまいました。
var list = new ArrayList<>(List.of(2, 3, 1)); // NG: // no instance(s) of type variable(s) U exist so that Object conforms to Comparable<? super U> list.sort(Comparator.comparing(s -> s).reversed()); // OK: reversedをつけなければ list.sort(Comparator.comparing(s -> s));
調査をしたところ、以下のページにて興味深い話が載っていましたので、共有をしたいと思います。
stackoverflow.com
原因
原因は、どうやらJavaのコンパイラの、 ジェネリクスに対する遡っての解決 が、うまくできないことにあるようです。
例えば、普段よく使うStreamは、ジェネリクスは以下のように先頭から解決が行われています。
list.stream() // Collection<Integer> -> Stream<Integer> .map(i -> i + 1) // Stream<Integer> -> Stream<Integer> .toList(); // Stream<Integer> -> List<Integer>
まずこのケースについて考えます。
list.sort(Comparator.comparing(s -> s));
list#sort()
は、List<Integer>
が、Comparator<? super Integer>
を必要としています。
また、Comparator#comparing()
は、Function<? super T, ? extends U> keyExtractor
を引数としてComparator<T>
を返します。
そのため、List<Integer>
-> Comparator<Integer>
-> Function<Integer, U>
-> Function<Integer, Integer>
という風な解決が行われています。
次に、エラーとなったケースについて考えます。
list.sort(Comparator.comparing(s -> s).reversed());
list#sort()
は、List<Integer>
が、Comparator<? super Integer>
を必要としています(ここまでは一緒)。
Comparator#reversed()
は、Comparator<T>
からComparator<T>
を返すメソッドのため、上の情報を使用して、両方をComparator<Integer>
と解決します。
あとは後ろからそれを使ってComparator#comparing()
に伝えれば……、というところなのですが、どうやらJavaコンパイラは、これが解決できないようです。
これが冒頭にあげた「遡っての解決」にあたります。
再現
凄くシンプルな例で再現をしてみると以下のようになります
// こんなクラスを作って public static class Test<T> { public Test<T> method() { return this; } } // NG: // Required type: Test<String> // Provided: Test<Object> // // 変数の型のTest<String>から、method -> new<>と解決してほしいのだが、できない。 Test<String> t = new Test<>().method();
以下のように、型を明示的に宣言をするか、遡り解決をしなければ、怒られません。
// OK: 型の明示的な宣言 Test<String> t = new Test<String>().method(); // OK: 遡り解決をしない Test<String> t = new Test<>();
それでも逆順のComparatorがほしい
冒頭にあげたstack overflowにて、Brian Goetzさんという方が、以下のようにコメントしていました。
Brian Goetzさんは、OracleにてJava Language Architectをされている方のようです。
Lambdas are divided into implicitly-typed (no manifest types for parameters) and explicitly-typed; method references are divided into exact (no overloads) and inexact. When a generic method call in a receiver position has lambda arguments, and the type parameters cannot be fully inferred from the other arguments, you need to provide either an explicit lambda, an exact method ref, a target type cast, or explicit type witnesses for the generic method call to provide the additional type information needed to proceed.
翻訳・整理すると、
ラムダは暗黙的型付け(パラメータに明示的型付けがない)と明示的型付けに分けられ、メソッド参照は厳密(オーバーロードなし)と非厳密とに分けられる。
受信位置のジェネリックメソッド呼び出しにラムダ引数があり、型パラメータが他の引数から完全に推測できない場合
- 明示的ラムダ
- 正確メソッド参照
- ターゲット型キャスト
- ジェネリクスメソッドの呼び出しに対する明示的型付け
を提供して、進行に必要な追加の型情報を提供する必要があります。
折角なのでそれぞれ試してみました
// 1. 明示的ラムダ // この書き方は知らなかった……。 list.sort(Comparator.comparing((Integer s) -> s).reversed()); // 2. 正確メソッド参照 // 以下のようなメソッドを用意してあげて、メソッド参照する // public Integer integerFunction(Integer i) { // return i; // } list.sort(Comparator.comparing(this::integerFunction).reversed()); // 3. ターゲット型キャスト list.sort(Comparator.comparing((Function<Integer, Integer>) s -> s).reversed()); // 4. ジェネリクスに対する明示的型付け list.sort(Comparator.<Integer, Integer>comparing(s -> s).reversed());
あとは、Collections#reverseOrder()
を使用することで、遡り解決を避けられるようです。
list.sort(Collections.reverseOrder(Comparator.comparing(s -> s)));
最後に
コンパイラへの突っ込んだ話となりましたので、もしかしたら間違っているところもあるかもしれませんが、何かの参考になれば幸いです。
最後までお読みいただきありがとうございました。
当記事では以下のバージョンを使用しています。
>java --version openjdk 17.0.4 2022-07-19 LTS OpenJDK Runtime Environment Corretto-17.0.4.8.1 (build 17.0.4+8-LTS) OpenJDK 64-Bit Server VM Corretto-17.0.4.8.1 (build 17.0.4+8-LTS, mixed mode, sharing)