20

次のテスト プログラムは、何か役に立つことを行う、より複雑なプログラムから派生したものです。Eclipse コンパイラーで正常にコンパイルされます。

import java.util.ArrayList;
import java.util.List;

public class InferenceTest
{
    public static void main(String[] args)
    {
        final List<Class<? extends Foo<?, ?>>> classes =
            new ArrayList<Class<? extends Foo<?, ?>>>();
        classes.add(Bar.class);
        System.out.println(makeOne(classes));
    }

    private static Foo<?, ?> makeOne(Iterable<Class<? extends Foo<?, ?>>> classes)
    {
        for (final Class<? extends Foo<?, ?>> cls : classes)
        {
            final Foo<?, ?> foo = make(cls); // javac error here
            if (foo != null)
                return foo;
        }
        return null;
    }

    // helper used to capture wildcards as type variables
    private static <A, B, C extends Foo<A, B>> Foo<A, B> make(Class<C> cls)
    {
        // assume that a real program actually references A and B
        try
        {
            return cls.getConstructor().newInstance();
        }
        catch (final Exception e)
        {
            return null;
        }
    }

    public static interface Foo<A, B> {}

    public static class Bar implements Foo<Integer, Long> {}
}

ただし、Oracle JDK 1.7 javac では、次のように失敗します。

InferenceTest.java:18: error: invalid inferred types for A,B; inferred type does not
 conform to declared bound(s)
            final Foo<?, ?> foo = make(cls);
                                      ^
    inferred: CAP#1
    bound(s): Foo<CAP#2,CAP#3>
  where A,B,C are type-variables:
    A extends Object declared in method <A,B,C>make(Class<C>)
    B extends Object declared in method <A,B,C>make(Class<C>)
    C extends Foo<A,B> declared in method <A,B,C>make(Class<C>)
  where CAP#1,CAP#2,CAP#3 are fresh type-variables:
    CAP#1 extends Foo<?,?> from capture of ? extends Foo<?,?>
    CAP#2 extends Object from capture of ?
    CAP#3 extends Object from capture of ?
1 error

どのコンパイラが正しいですか?

上記の出力の疑わしい側面の 1 つは、CAP#1 extends Foo<?,?>. 型変数の境界はCAP#1 extends Foo<CAP#2,CAP#3>. この場合、推論さCAP#1れた境界は宣言された境界に準拠します。CAP#1ただし、C は実際には であると推測されるはずですが、エラー メッセージは A と B に関するものであるため、これはおかしなことかもしれません。


26 行目を次のように置き換えると、両方のコンパイラがプログラムを受け入れることに注意してください。

private static <C extends Foo<?, ?>> Foo<?, ?> make(Class<C> cls)

Fooただし、キャプチャされたパラメーターの型を参照することはできません。

更新:両方のコンパイラで同様に受け入れられます(ただし、役に立たない)のは次のとおりです。

private static <A, B, C extends Foo<? extends A, ? extends B>>
    Foo<? extends A, ? extends B> make(Class<C> cls)

それは本質的にABを として自明に推論するObjectため、どのような状況でも明らかに役に立ちません。ただし、javacワイルドカード境界でのみ推論を実行し、境界をキャプチャしない以下の私の理論に信憑性を与えます。誰もより良いアイデアを持っていない場合、これが(残念な)答えかもしれません. (更新終了)


この質問全体がTL;DRである可能性が高いことは理解していますが、他の誰かがこの問題に遭遇した場合に備えて続行します...

JLS 7、§15.12.2.7 Inferring Type Arguments Based on Actual Argumentsに基づいて、次の分析を行いました。

A << FA = F、またはの形式の制約が与えられた場合A >> F:

最初に、フォームの制約が 1 つありますA << F。これは、型がメソッド呼び出しの変換によってA型に変換可能であることを示します ( §5.3 )。ここでは、 とです。他の制約形式 (および) は、推論アルゴリズムが再帰的に発生することに注意してください。FAClass<CAP#1 extends Foo<CAP#2, CAP#3>>FClass<C extends Foo<A, B>>A = FA >> F

次に、次のルールCであると推測する必要があります。CAP#1

(2.) それ以外の場合、制約が次の形式の場合A << F:

  • が を含む型式である場合、 が型式であるというF形式のスーパータイプを有する場合、このアルゴリズムは制約 に再帰的に適用されます。G<..., Yk-1, U, Yk+1, ...>UTjAG<..., Xk-1, V, Xk+1, ...>VV = U

ここでGClass、 、UおよびTjC、およびVですCAP#1。に再帰的に適用すると、次CAP#1 = Cの制約が発生するはずC = CAP#1です。

(3.) それ以外の場合、制約が次の形式の場合A = F:

  • の場合F = Tj、制約Tj = Aが暗示されます。

ここまでの分析は、javac の出力と一致しているようです。Aおそらく、分岐点は、 と を推論しようとし続けるかどうかBです。たとえば、このルールが与えられた場合

  • 次のいずれかのスーパー タイプをF持つifG<..., Yk-1, ? extends U, Yk+1, ...>UTjA
    • G<..., Xk-1, V, Xk+1, ...>Vは型式です。
    • G<..., Xk-1, ? extends V, Xk+1, ...>.

次に、このアルゴリズムが再帰的に制約に適用されますV << U

CAP#1がワイルドカード (キャプチャされたもの) であると見なされる場合、この規則が適用され、 UasFoo<A, B>およびVasを使用して推論が再帰的に続行されFoo<CAP#2, CAP#3>ます。上記のように、これにより と が得A = CAP#2られB = CAP#3ます。

ただし、CAP#1が単なる型変数の場合、どのルールもその境界を考慮していないようです。おそらく、仕様のセクションの最後にあるこの譲歩は、そのような場合を指しています。

型推論アルゴリズムは、実際にうまく機能するように設計されたヒューリスティックと見なす必要があります。目的の結果を推測できない場合は、代わりに明示的な型パラメーターを使用できます。

明らかに、ワイルドカードを明示的な型パラメーターとして使用することはできません。:-(

4

2 に答える 2

10

問題は、次の推論制約から開始することです。

class<#1>, #1 <: Foo<?, ?>

これにより、C のソリューション、つまり C = #1 が得られます。

次に、C が宣言された境界に準拠しているかどうかを確認する必要があります。C の境界は Foo であるため、次のチェックで終了します。

#1 <: Foo<A,B>

これは次のように書き換えることができます

Bound(#1) <: Foo<A, B>

したがって:

Foo<?, ?> <: Foo<A, B>

ここで、コンパイラは LHS のキャプチャ変換を行います (ここで #2 と #3 が生成されます)。

Foo<#2, #3> <: Foo<A, B>

つまり

A = #2

B = #3

したがって、解は { A = #2, B = #3, C = #1 } です。

それは有効な解決策ですか?その質問に答えるには、型置換後に、推論された型が推論変数の境界と互換性があるかどうかを確認する必要があります。

[A:=#2]A <: Object
#2 <: Object - ok

[B:=#3]B <: Object
#3 <: Object - ok

[C:=#1]C <: [A:=#2, B:=#3]Foo<A, B>
#1 <: Foo<#2, #3>
Foo<?, ?> <: Foo<#2, #3>
Foo<#4, #5> <: Foo<#2, #3> - not ok

したがって、エラー。

推論とキャプチャされた型の間の相互作用に関しては、仕様が十分に規定されていないため、異なるコンパイラ間で切り替えると異なる動作をするのはごく普通のことです (ただし、良くありません!)。ただし、これらの問題のいくつかは、コンパイラの観点と JLS の観点の両方から取り組んでいるため、このような問題は中期的に修正されるはずです。

于 2013-07-03T18:22:49.240 に答える
1

私が気づいた2つのこと:

  1. CAP#1はワイルドカードではなく、キャプチャ変換のため型変数です。

  2. U最初のステップで、JLS はそれが型式であり、型パラメーターであると述べていますJLS は、型式が何であるかを明示的に定義していませんが、私の直感では、型パラメーターの境界が含まれていると感じています。その場合は、と になります。型推論アルゴリズムに従います。TjUC extends Foo<A,B>VCAP#1 extends Foo<CAP#2, CAP#3>

V = U->C = CAP#1そしてFoo<CAP#2, CAP#3> = Foo<A, B>

型推論アルゴリズムを上記に引き続き適用すると、 and になりA= CAP#2ますB=CAP#3

Oracle のコンパイラのバグを発見したと思います

于 2013-07-03T02:48:03.567 に答える