コンテキスト
私は、一般的なタイプに大きく依存しているプロジェクトに取り組んでいます。その重要な要素の1つは、いわゆるでTypeToken
、実行時にジェネリック型を表現し、それらにいくつかのユーティリティ関数を適用する方法を提供します。Javaの型消去を回避するために、私は(中括弧表記を使用しています{}
、これは型reifiableになりますので、自動的に生成されたサブクラスを作成します)。
どのような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
保持A 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)
結果として質問
- 何がラムダ例でTypeVariableはどうなりますか?なぜ、型推論は、ジェネリック型を尊重しないのですか?
- 明示的に宣言し、暗黙的に宣言した例との違いは何ですか?型推論が唯一の違いですか?
- どのように私は定型明示的な宣言を使用せずにこの問題を解決することができますか?私は、コンストラクタが例外をスローするかどうかを確認したいので、これはユニットテストでは特に重要になります。
それを少し明確にする:これは私がすべてでは解像不可能なタイプを許可していませんが、それはまだ私が理解したいのですが興味深い現象ですので、プログラムのために、「関連」だ問題ではありません。
私の研究
アップデート1
その間、私はこのトピックに関するいくつかの研究を行ってきました。ではJava言語仕様§15.12.2.2「の適用に関連する」、言及「暗黙に型付けされたラムダ式」の例外として-私はそれとは何かを持っているかもしれない表現を見つけました。もちろん、それは間違った章ですが、式は型推論についての章を含め、他の場所で使用されています。
しかし、正直に言うと:私は本当にまだのようなものを演算子の何すべてを考え出したていない:=
か、Fi0
それは本当にハードに詳細にそれを理解するためにするものを意味します。誰かがこのAビットを明確にすることができれば、これは奇妙な行動の説明であるかもしれない場合、私は喜んでいると思います。
アップデート2
私は再びそのアプローチを考えとの結論に来ましたが、それは「適用に関連する」はありませんので、コンパイラが型を除去するであろうとしても、それがコンポーネントの種類を設定するために正当化しないことをnull
代わりに最も寛大なタイプの、オブジェクト。私は言語設計者がそうすることを決めた理由を、単一の理由を考えることはできません。
アップデート3
私はただのJavaの最新バージョン(私が使用して同じコードを再テストしました8u191
前)。Javaの型推論が改善されましたが、残念なことに、これは、何も変わっていません...
更新4
私は数日前に公式のJavaバグデータベース/トラッカーにエントリを要求してきたし、それだけで受け入れられました。私のレポートをレビューし、開発者がバグに優先順位P4が割り当てられているので、それが固定されますまで、それはしばらく時間がかかる場合があります。レポートを見つけることができるここに。
トムホーティンに巨大なshoutout - これは、Java SE自体に不可欠なバグであるかもしれないことを言及するためtackline。しかし、マイク・ストローベルによる報告は、おそらく彼の印象的な背景知識への道より詳細な私よりだろう。私はレポートを書いたときしかし、ストローベルの答えはまだ利用できませんでした。
tldr:
- バグがある
javac
ラムダ組み込み内部クラスの間違った囲む方法を記録しているが。その結果、上型変数実際の封入方法は、これらの内部クラスでは解決できません。- バグの二組間違いなくあります
java.lang.reflect
APIの実装は:
- いくつかの方法は、存在しない型が検出されたときに例外を投げるように文書化されているが、彼らは決してしません。代わりに、彼らはNULL参照が伝播することができます。
- 様々な
Type::toString()
オーバーライドは、現在、投げたり伝播NullPointerException
タイプは解決できないときに。
答えは通常、ジェネリック医薬品を利用するクラスファイルに放出され得る一般的な署名に関係しています。
あなたが一つ以上の一般的なスーパータイプを持つクラスを記述する場合、通常、Javaコンパイラが放出するSignature
クラスのスーパータイプ(S)の完全にパラメータ一般的な署名(複数可)を含む属性を。私がしました前に、これらについて書かれたが、短い説明はこれである:それらなしで、ジェネリック型を消費することはできないジェネリック型として使用すると、ソースコードを持つことが起こった場合を除きます。型消去に、型変数に関する情報は、コンパイル時に失われます。その情報は、余分なメタデータとして含まれていない場合は、IDEやコンパイラどちらもタイプは、一般的なだったことを知っているであろう、そしてあなたはそのように使うことができませんでした。NORコンパイラは型の安全性を強化するために必要なランタイムチェックを放出することができます。
javac
あなたは匿名型の元の一般的なスーパータイプの情報を得ることができている理由であるシグネチャ型変数またはパラメータ化された型を含む任意の種類や方法、のための汎用的な署名のメタデータを出力します。例えば、匿名型は、ここで作成しました:
TypeToken<?> token = new TypeToken<List<? extends CharSequence>>() {};
...これは含まれていSignature
:
LTypeToken<Ljava/util/List<+Ljava/lang/CharSequence;>;>;
このことから、java.lang.reflection
APIは、あなたの(匿名)クラスに関する一般的なスーパータイプの情報を解析することができます。
しかし、我々はすでにとき、これはうまく動作しますことを知っているTypeToken
具体的な種類のパラメータ化され。その型パラメータが含まれ、より関連性の例、で見てみましょう型変数を:
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
我々は気にビットがある:これは型変数のための解像度の範囲を提供します。型変数名を指定すると、範囲が一致する型の変数のために検索されます。1が見つからない場合は、「外」または囲むスコープが検索されます。
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[]>
)、次のステップで検索することです囲む方法を。私たちは解体匿名クラスを見れば、私たちは、この属性を参照してください。
EnclosingMethod: LambdaTest.test()V
この属性手段の存在computeEnclosingScope
生成されますMethodScope
ジェネリックメソッドのためにstatic <F> void test()
。以来test
型の変数を宣言しW
、我々は外側のスコープを検索するとき、我々はそれを見つけます。
だから、なぜそれがラムダの内部で動作しませんか?
これに答えるために、我々は、ラムダがコンパイル取得する方法を理解する必要があります。ラムダの本体が移動されます合成静的メソッドに。我々はラムダを宣言する時点で、invokedynamic
命令が原因となる、放射されますTypeToken
実装クラスは、我々はその命令を打つ最初の時間を生成します。
(逆コンパイル場合)この例では、ラムダ本体用に生成静的メソッドは次のようになります。
private static /* synthetic */ Object lambda$test$0() {
return new LambdaTest$1();
}
...どこLambdaTest$1
あなたの匿名クラスです。私たちの属性を検査することをしてみましょうdissassemble:
Signature: LTypeToken<TW;>;
EnclosingMethod: LambdaTest.lambda$test$0()Ljava/lang/Object;
ただ、我々は匿名型のインスタンス化の場合と同様に外ラムダのが、署名は、型変数が含まれていますW
。 しかしEnclosingMethod
を指し合成方法。
合成方法は、lambda$test$0()
型の変数を宣言していませんW
。また、lambda$test$0()
によって囲まれていないtest()
の宣言がので、W
その内部は見えません。あなたの匿名クラスは、それがスコープ外ですので、あなたのクラスが知らないことを型変数を含むスーパータイプを持っています。
私たちが呼び出すとgetGenericSuperclass()
、スコープのための階層がLambdaTest$1
含まれていないW
ので、パーサはそれを解決することはできません。コードの記述方法に起因して、この未解決型変数の結果null
、一般的なスーパータイプの型パラメータに置か取得。
なお、あなたのラムダはなかったタイプのインスタンス化していた持っていたではない(例えば、任意の型の変数を参照TypeToken<String>
)、その後、あなたがこの問題に遭遇しないだろうし。
結論
(i)はバグがありますjavac
。 Java仮想マシン仕様§4.7.7(「EnclosingMethod
属性」)状態:
によって識別方法があることを確認するためにJavaコンパイラの責任である
method_index
確かに最も近い字句的に取り囲む、この含まれているクラスのメソッドEnclosingMethod
属性を。(強調鉱山)
現在、javac
囲む方法を決定しているようだ後、ラムダ書き換えは、そのコースを実行し、その結果として、EnclosingMethod
属性もレキシカルスコープに存在しなかった方法です。場合はEnclosingMethod
報告し、実際の字句的に取り囲む方法を、そのメソッドの型変数は、ラムダ組み込みクラスによって解決することができ、あなたのコードは、期待どおりの結果が得られます。
これは、署名パーサ/ reifierサイレントができることも間違いなくバグnull
型引数はに伝播するParameterizedType
(これは、トム・ホーティン- tacklineが指摘@として、のように補助的な効果持つtoString()
NPEをスローします)。
私のバグレポートのためのEnclosingMethod
問題はオンラインです。
(ⅱ)における複数のバグ間違いなくありますjava.lang.reflect
し、それをサポートするAPIが。
この方法は、ParameterizedType::getActualTypeArguments()
Aを投げるように記載されてTypeNotPresentException
「実型引数のいずれかが存在しない型宣言を意味する」場合。その説明は、おそらく型の変数がスコープ内にない場合をカバーします。GenericArrayType::getGenericComponentType()
「基本となる配列型のタイプが存在しない型宣言を参照」場合にも、同様に例外をスローする必要があります。現在、どちらもスローするように表示されませんTypeNotPresentException
どのような状況の下で。
私はまた、様々なことを主張するだろうType::toString
オーバーライドは単にむしろNPEまたはその他の例外を投げるよりも、未解決のタイプの標準名を記入すべきです。
私はこれらの反射関連の問題のためにバグレポートを提出している、それが一般に公開されたら、私はリンクを投稿します。
回避策?
あなたは、変数を囲む方法で宣言された型を参照できるようにする必要がある場合は、ラムダでそれを行うことはできません。あなたは長い匿名型の構文にフォールバックする必要があります。しかし、ラムダバージョンは他のほとんどのケースで動作するはずです。あなたも囲むことで宣言された参照型変数のことができるようにすべきであるクラス。例えば、これらは常に動作するはずです:
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のバグの遺跡を前提とする必要がありますすべてで固定。