私は、ジェネリック型に大きく依存するプロジェクトに取り組んでいます。その主要コンポーネントの1つは、いわゆるTypeToken
です。これは、実行時にジェネリック型を表し、それらにいくつかのユーティリティ関数を適用する方法を提供します。 JavaのType Erasureを回避するために、中括弧表記({}
)を使用して、自動生成されたサブクラスを作成しています。これにより、型が再定義可能になります。
TypeToken
が基本的に行うことこれはTypeToken
の非常に単純化されたバージョンで、元の実装よりもはるかに寛容です。ただし、このアプローチを使用しているので、実際の問題がこれらのユーティリティ関数のいずれかにないことを確認できます。
public class TypeToken<T> {
private final Type type;
private final Class<T> rawType;
private final int hashCode;
/* ==== Constructor ==== */
@SuppressWarnings("unchecked")
protected TypeToken() {
ParameterizedType paramType = (ParameterizedType) this.getClass().getGenericSuperclass();
this.type = paramType.getActualTypeArguments()[0];
// ...
}
基本的に、この実装はほぼすべての状況で完全に機能します。ほとんどの型の処理に問題はありません。次の例は完全に機能します。
TypeToken<List<String>> token = new TypeToken<List<String>>() {};
TypeToken<List<? extends CharSequence>> token = new TypeToken<List<? extends CharSequence>>() {};
型をチェックしないため、上記の実装では、TypeVariablesを含め、コンパイラが許可するすべての型が許可されます。
<T> void test() {
TypeToken<T[]> token = new TypeToken<T[]>() {};
}
この場合、type
はGenericArrayType
をコンポーネントタイプとして保持するTypeVariable
です。これはまったく問題ありません。
ただし、ラムダ式内でTypeToken
を初期化すると、状況が変わり始めます。 (型変数は上記のtest
関数から取得されます)
Supplier<TypeToken<T[]>> sup = () -> new TypeToken<T[]>() {};
この場合、type
はGenericArrayType
のままですが、コンポーネントタイプとしてnull
を保持します。
しかし、匿名の内部クラスを作成している場合、状況は再び変化し始めます。
Supplier<TypeToken<T[]>> sup = new Supplier<TypeToken<T[]>>() {
@Override
public TypeToken<T[]> get() {
return new TypeToken<T[]>() {};
}
};
この場合、コンポーネントタイプは再び正しい値(TypeVariable)を保持します
少し明確にするために:これはプログラムにとって「関連する」問題ではありません。なぜなら、私は解決不可能な型をまったく許可しないからです。しかし、それはまだ理解したい興味深い現象です。
一方、私はこのトピックに関するいくつかの研究を行ってきました。 Java Language Specification§15.12.2.2 で、それと何か関係があるかもしれない式を見つけました-「適用可能性に関連する」、例外として「暗黙的に型付けされたラムダ式」に言及。明らかに、これは間違った章ですが、式は型推論に関する章を含む他の場所で使用されます。
しかし、正直に言うと、:=
やFi0
のようなこれらの演算子のすべてが、詳細を理解するのを本当に難しくしているのはどういう意味なのか、まだわかりません。誰かがこれを少し明確にできて、これが奇妙な振る舞いの説明かもしれないなら、私はうれしいです。
私はそのアプローチをもう一度考え、結論に至りました。「適用性に関係ない」ためにコンパイラが型を削除しても、代わりにコンポーネント型をnull
に設定することは正当化されません最も寛大なタイプのオブジェクト。言語デザイナーがそうすることを決めた単一の理由を考えることはできません。
最新バージョンのJava(以前8u191
を使用しました)で同じコードを再テストしました。残念なことに、Javaの型推論は変更されていませんが、改善されました...
公式のJava Bug Database/Trackerのエントリを数日前にリクエストしましたが、受け入れられました。レポートをレビューした開発者が優先度P4をバグに割り当てたため、修正されるまでしばらく時間がかかります。レポートは here で確認できます。
Tom Hawtinへの大きな叫び-これはJava SE自体の本質的なバグかもしれないと言及するためのタックライン。しかし、Mike Strobelによる報告はおそらく彼のおかげで私よりもずっと詳細になるでしょう。しかし、レポートを書いたとき、Strobelの答えはまだ得られていませんでした。
tldr:
javac
には、ラムダが埋め込まれた内部クラスの間違った囲い込みメソッドを記録するバグがあります。その結果、actualを囲むメソッドの型変数は、それらの内部クラスによって解決できません。- _
Java.lang.reflect
_ API実装には、おそらく2組のバグがあります。
- 一部のメソッドは、存在しない型が検出されたときに例外をスローするものとして文書化されていますが、決して実行されません。代わりに、null参照を伝播できます。
- さまざまな
Type::toString()
は、タイプを解決できない場合に、現在NullPointerException
をスローまたは伝播します。
答えは、ジェネリックを使用するクラスファイルで通常発行されるジェネリックシグネチャに関するものです。
通常、1つ以上のジェネリックスーパータイプを持つクラスを作成すると、Javaコンパイラは、クラスのスーパータイプの完全にパラメーター化されたジェネリックシグネチャを含むSignature
属性を発行します。 。私は これらについて前に書いた ですが、短い説明はこれです:それらなしでは、たまたまジェネリック型を消費することはできませんジェネリック型としてソースコード。型の消去により、型変数に関する情報はコンパイル時に失われます。その情報が追加のメタデータとして含まれていない場合、IDEもコンパイラも型がジェネリックであることを認識せず、そのように使用できません。また、コンパイラは、タイプセーフを実施するために必要なランタイムチェックを発行することもできません。
javac
は、型変数またはパラメーター化された型を含む署名を持つ型またはメソッドの汎用署名メタデータを出力します。これが、匿名型の元の汎用スーパータイプ情報を取得できる理由です。たとえば、ここで作成された匿名タイプ:
_TypeToken<?> token = new TypeToken<List<? extends CharSequence>>() {};
_
...このSignature
を含む:
_LTypeToken<Ljava/util/List<+Ljava/lang/CharSequence;>;>;
_
このことから、_Java.lang.reflection
_ APIは(匿名の)クラスに関する一般的なスーパータイプ情報を解析できます。
しかし、TypeToken
が具象型でパラメーター化されている場合、これがうまく機能することは既にわかっています。 typeパラメーターにtype variableが含まれる、より関連性の高い例を見てみましょう。
_static <F> void test() {
TypeToken sup = new TypeToken<F[]>() {};
}
_
ここで、次の署名を取得します。
_LTypeToken<[TF;>;
_
理にかなっていますよね?ここで、_Java.lang.reflect
_ APIがこれらの署名から一般的なスーパータイプ情報を抽出する方法を見てみましょう。 Class::getGenericSuperclass()
をじっと見ると、最初に行うことはgetGenericInfo()
の呼び出しであることがわかります。以前にこのメソッドを呼び出さなかった場合、ClassRepository
がインスタンス化されます:
_private ClassRepository getGenericInfo() {
ClassRepository genericInfo = this.genericInfo;
if (genericInfo == null) {
String signature = getGenericSignature0();
if (signature == null) {
genericInfo = ClassRepository.NONE;
} else {
// !!! RELEVANT LINE HERE: !!!
genericInfo = ClassRepository.make(signature, getFactory());
}
this.genericInfo = genericInfo;
}
return (genericInfo != ClassRepository.NONE) ? genericInfo : null;
}
_
ここで重要なのは、getFactory()
の呼び出しです。
_CoreReflectionFactory.make(this, ClassScope.make(this))
_
ClassScope
は重要なビットです。これは、型変数の解決スコープを提供します。型変数名を指定すると、スコープは一致する型変数を検索します。見つからない場合は、 'outer'またはenclosingスコープが検索されます:
_public TypeVariable<?> lookup(String name) {
TypeVariable<?>[] tas = getRecvr().getTypeParameters();
for (TypeVariable<?> tv : tas) {
if (tv.getName().equals(name)) {return tv;}
}
return getEnclosingScope().lookup(name);
}
_
そして最後に、すべての鍵(ClassScope
から):
_protected Scope computeEnclosingScope() {
Class<?> receiver = getRecvr();
Method m = receiver.getEnclosingMethod();
if (m != null)
// Receiver is a local or anonymous class enclosed in a method.
return MethodScope.make(m);
// ...
}
_
型変数(たとえば、F
)がクラス自体(たとえば、匿名の_TypeToken<F[]>
_)に見つからない場合、次のステップはenclosing method。逆アセンブルされた匿名クラスを見ると、次の属性があります。
_EnclosingMethod: LambdaTest.test()V
_
この属性が存在するということは、computeEnclosingScope
がジェネリックメソッドstatic <F> void test()
に対してMethodScope
を生成することを意味します。 test
は型変数W
を宣言しているため、外側のスコープを検索するとそれが見つかります。
これに答えるには、ラムダがどのようにコンパイルされるかを理解する必要があります。ラムダの本体 移動 合成静的メソッドへ。ラムダを宣言した時点で、invokedynamic
命令が発行され、その命令に最初にヒットしたときにTypeToken
実装クラスが生成されます。
この例では、ラムダ本体用に生成された静的メソッドは次のようになります(逆コンパイルされた場合)。
_private static /* synthetic */ Object lambda$test$0() {
return new LambdaTest$1();
}
_
...ここで_LambdaTest$1
_は匿名クラスです。それを分解して、属性を調べてみましょう。
_Signature: LTypeToken<TW;>;
EnclosingMethod: LambdaTest.lambda$test$0()Ljava/lang/Object;
_
ラムダの匿名型outsideをインスタンス化した場合と同様に、署名には型変数W
が含まれます。 しかしEnclosingMethod
は合成法を指します。
合成メソッドlambda$test$0()
は、型変数W
を宣言しません。さらに、lambda$test$0()
はtest()
で囲まれていないため、W
の宣言は内部に表示されません。匿名クラスには、スコープ外にあるためクラスが知らない型変数を含むスーパータイプがあります。
getGenericSuperclass()
を呼び出すと、_LambdaTest$1
_のスコープ階層にW
が含まれないため、パーサーはそれを解決できません。コードの記述方法により、この未解決の型変数により、null
が汎用スーパータイプの型パラメーターに配置されます。
ラムダが型変数をインスタンス化したnot型変数(たとえば、_TypeToken<String>
_)をインスタンス化した場合、この問題は発生しません。
(i)javac
。にバグがありますJava仮想マシン仕様 §4.7.7 ( " EnclosingMethod
属性」)状態:
Javaコンパイラーの責任は、_
method_index
_で識別されるメソッドが、これを含むクラスの最も近い字句的に囲むメソッドであることを保証することですEnclosingMethod
属性。 (エンファシス鉱山)
現在、javac
は囲むメソッドを決定しているようですafterラムダリライタはそのコースを実行し、その結果、EnclosingMethod
属性は存在しなかったメソッドを参照します字句スコープ内。 EnclosingMethod
がactual語彙的に囲むメソッドを報告した場合、そのメソッドの型変数はラムダ埋め込みクラスによって解決でき、コードは期待される結果を生成します。
間違いなく、署名パーサー/リファイアーがnull
型の引数をParameterizedType
(@ tom-hawtin-tacklineが指摘しているように、補助的な効果を持つ) toString()
がNPEをスローするなど)。
私の バグレポートEnclosingMethod
の問題がオンラインになりました。
(ii)_Java.lang.reflect
_とそのサポートAPIには間違いなく複数のバグがあります。
メソッドParameterizedType::getActualTypeArguments()
は、「実際の型引数のいずれかが存在しない型宣言を参照している場合」TypeNotPresentException
をスローするものとして文書化されています。この説明は、型変数がスコープ内にない場合をほぼカバーしています。 GenericArrayType::getGenericComponentType()
は、「基礎となる配列型の型が存在しない型宣言を参照している」場合に同様の例外をスローする必要があります。現在、いずれの状況でも、どちらもTypeNotPresentException
をスローしないようです。
また、さまざまな_Type::toString
_オーバーライドは、NPEまたはその他の例外をスローするのではなく、未解決の型の正規名を入力するだけでよいと主張します。
これらのリフレクション関連の問題に関するバグレポートを提出しました。リンクが公開されたら投稿します。
囲んでいるメソッドで宣言された型変数を参照できるようにする必要がある場合、ラムダでそれを行うことはできません。より長い匿名型構文にフォールバックする必要があります。ただし、ラムダバージョンは他のほとんどの場合に機能するはずです。囲んでいるclassで宣言された型変数を参照することもできるはずです。たとえば、これらは常に機能するはずです。
_class Test<X> {
void test() {
Supplier<TypeToken<X>> s1 = () -> new TypeToken<X>() {};
Supplier<TypeToken<String>> s2 = () -> new TypeToken<String>() {};
Supplier<TypeToken<List<String>>> s3 = () -> new TypeToken<List<String>>() {};
}
}
_
残念ながら、ラムダが最初に導入されてから明らかにこのバグが存在し、最新のLTSリリースでは修正されていないことを考えると、バグが修正された後も、クライアントのJDKにバグが残っていると仮定する必要がありますまったく修正されました。
回避策として、TypeTokenの作成をラムダから別のメソッドに移動し、完全に宣言されたクラスの代わりにラムダを使用できます。
static<T> TypeToken<T[]> createTypeToken() {
return new TypeToken<T[]>() {};
}
Supplier<TypeToken<T[]>> sup = () -> createTypeToken();
仕様の関連部分は見つかりませんでしたが、ここに部分的な答えがあります。
確かにnull
であるコンポーネントタイプにはバグがあります。明確にするために、これはTypeToken.type
上記のメソッドからGenericArrayType
にキャストし、getGenericComponentType
メソッドを呼び出します(うん!)。 APIドキュメントでは、返されたnull
が有効かどうかを明示的に言及していません。ただし、toString
メソッドはNullPointerException
をスローするため、間違いなくバグがあります(少なくともJava私が使用しているバージョン)では)。
bugs.Java.comアカウントを持っていないので、報告できません。誰かがすべきです。
生成されたクラスファイルを見てみましょう。
javap -private YourClass
これにより、次のようなリストが作成されます。
static <T> void test();
private static TypeToken lambda$test$0();
明示的なtest
メソッドには型パラメーターがありますが、合成ラムダメソッドにはないことに注意してください。あなたは次のようなものを期待するかもしれません:
static <T> void test();
private static <T> TypeToken<T[]> lambda$test$0(); /*** DOES NOT HAPPEN ***/
// ^ name copied from `test`
// ^^^ `Object[]` would not make sense
なぜそうならないのですか。おそらく、これは型型パラメーターが必要なコンテキストのメソッド型パラメーターであり、驚くほど異なるものだからです。また、明らかに明示的な表記法がないため、ラムダにメソッド型パラメーターを許可しないという制限もあります(一部の人々は、これが貧弱な言い訳のように思われるかもしれません)。
結論:少なくとも報告されていないJDKバグが1つあります。 reflect
APIおよび言語のこのラムダ+ジェネリック部分は、私の好みではありません。