型イレージャ
jimbeさんが指摘されているように、この問題は型イレージャが原因で起こります。JVMはジェネリクスを知りません。
Javaの世界
Javaコンパイラはtestの引数の型Set<CharSequence>とSet<String>は別の型とみなします(非変)。testメソッドのシグネチャ(メソッド名と引数の型で決まる)はそれぞれ異なるので、メソッドがオーバーロードされています。
- Aのメソッド ... List<Number> test(Set<CharSequence>)
- Bのメソッド ... ArrayList<Integer> test(Set<String>)
JVMの世界
バイトコードが生成される時、型イレージャによってジェネリクスの型が消され、要素の型はraw型(Object)になります。ただし、それぞれのメソッド内部で、必要なキャスト(checkcast)はコンパイラが補います。このようにtestメソッドがオーバーライドされたことになります。
- Aのメソッド ... List test(Set)
- Bのメソッド ... ArrayList test(Set)
さて、これがコンパイルエラーにならず、仮にオーバーライドできたとすると、Aの型を持つ変数xにBのインスタンスを代入して x.test()を呼ぶとポリモーフィズムが働き、Bのtest()が呼ばれます。Aのtestの引数のSetにはStringBuilderを入れることができますが、Bのtest()は、処理の途中で、Setの要素をStringにキャストするのでエラーになります。
Java
1A x = new B();
2Set<CharSequence> s = new HashSet<>(List.of(new StringBuilder()));
3List<Number> l = x.test(s);
JavaとJVMの解釈に不整合が生じ実行時エラーがおきるため、コンパイルエラーにしているのだと思います。
【メソッドのオーバーライドについて(追記)】
メソッドがオーバーライド可能な条件
・メソッドのシグネチャが同一(メソッド名が同じ,メソッドの引数が同じ型で並びが同一)
・メソッドの戻り値の型が同一型か共変型(サブクラス、またはインターフェイスを実装するクラス)
・メソッドのアクセスレベルが同じか緩い
まずtestの戻り値が同一型か共変(covariant)型かです。
ArrayListはListを実装するので共変型
ところがジェネリクスを適用、コレクションが共変でも型パラメータが異なると非変(異なる型)になります。
ArrayList<Integer>とList<Number>は非変(nonvariant)
Set<Number>とSet<Integer>は非変
従って、Javaの世界ではメソッドの引数の型が異なるのでオーバーロード
しかし、JVMの世界ではオーバーライド(ArrayListはListを実装するので共変型)
【自然な解決案(追記)】
Aで定義するNumber , CharSequenceは抽象クラスなので、Aも抽象クラスとするのが自然です。メソッドがオーバーロードと解釈されないようにクラスに型パラメータを定義します。以下のBのtest()は、A<String, Integer>の定義に従って、AのメソッドList<Integer> test(Set<String> s)をオーバーライドします。
Java
1// A
2import java.util.List;
3import java.util.Set;
4
5public abstract class A<T extends CharSequence, U extends Number> {
6 public abstract List<U> test(Set<T> s);
7}
8
9// B
10import java.util.ArrayList;
11import java.util.Set;
12import java.util.stream.Collectors;
13
14public class B extends A<String, Integer> {
15 @Override
16 public ArrayList<Integer> test(Set<String> s) {
17 return s.stream().map(x -> Integer.valueOf(x)).collect(Collectors.toCollection(ArrayList::new));
18 }
19}
以下は参考とします
参考
testメソッドの目的は、Set<T> -> List<U>に変換することです。本質はコレクションに関係なく、TをUに変換することですから、継承によって T -> U を解決します。
Java
1import java.util.List;
2import java.util.Set;
3import java.util.stream.Collectors;
4
5public abstract class A<T extends CharSequence, U extends Number> {
6 public final List<U> test(Set<T> s) {
7 return s.stream().map(x -> convert(x)).collect(Collectors.toList());
8 }
9 public abstract U convert(T t);
10}
11
12public class B extends A<String, Integer> {
13 public Integer convert(String a) {
14 return Integer.valueOf(a);
15 }
16}
この解決法をTemplate Methodパターンと呼びます。関数型インターフェースが登場してからTemplate Methodパターンは使わず、変換用の関数 T -> U を与えるようになりました。
Java
1public class A<T extends CharSequence, U extends Number> {
2 public List<U> test(Set<T> s, Function<T,U> f) {
3 return s.stream().map(x -> f.apply(x)).collect(Collectors.toList());
4 }
5}
6public class B extends A<String, Integer> {}
もっと良い解決案があるかもしれません。
継承は使えないので、型パラメータを使ってコレクションの共通処理を記述するのが良いでしょう。
下記のような回答は推奨されていません。
このような回答には修正を依頼しましょう。
2023/01/22 22:08
2023/01/22 22:58
2023/01/29 20:30 編集
2023/01/26 20:50