技術と日常。

日々の気が付いたこと・気になったことを残しておきます。

[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. 明示的ラムダ
  2. 正確メソッド参照
  3. ターゲット型キャスト
  4. ジェネリクスメソッドの呼び出しに対する明示的型付け

を提供して、進行に必要な追加の型情報を提供する必要があります。

折角なのでそれぞれ試してみました

// 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)