在所有方法调用上允许类型见证有什么意义?

What is the point of allowing type witnesses on all method calls?

假设我们有如下两种方法:

public static <T> T genericReturn() { /*...*/ }
public static String stringReturn() { /*...*/ }

在调用任何方法时,无论是否有要求,都可以提供类型见证:

String s;
s = Internet.<String>genericReturn(); //Type witness used in return type, returns String
s = Internet.<Integer>stringReturn(); //Type witness ignored, returns String

但是我在 Java 中根本没有看到任何实际用途,除非无法推断类型(这通常表明存在更大的问题)。此外,当它没有被正确使用时它会被简单地忽略这一事实似乎违反直觉。那么在 Java 中加入这个有什么意义呢?

来自JLS §15.2.12.1

  • If the method invocation includes explicit type arguments, and the member is a generic method, then the number of type arguments is equal to the number of type parameters of the method.

This clause implies that a non-generic method may be potentially applicable to an invocation that supplies explicit type arguments. Indeed, it may turn out to be applicable. In such a case, the type arguments will simply be ignored.

后面有理由

This rule stems from issues of compatibility and principles of substitutability. Since interfaces or superclasses may be generified independently of their subtypes, we may override a generic method with a non-generic one. However, the overriding (non-generic) method must be applicable to calls to the generic method, including calls that explicitly pass type arguments. Otherwise the subtype would not be substitutable for its generified supertype.

沿着这个推理思路,让我们构建一个例子。假设在 Java 1.4 中,JDK 有一个 class

public class Foo
{
    /** check obj, and return it */
    public Object check(Object obj){ ... }
}

一些用户写了一个专有的 class 扩展 Foo 并覆盖 check 方法

public class MyFoo extends Foo
{
    public Object check(Object obj){ ... }
}

当Java 1.5引入泛型时,Foo.check被泛化为

    public <T> T check(T obj)

雄心勃勃的向后可比性目标要求 MyFoo 仍然在 Java 1.5 中编译而无需修改; MyFoo.check[Object->Object] 仍然是 Foo.check[T->T] 的重写方法。

现在,根据上述理由,因为编译:

    MyFoo myFoo = new MyFoo();

    ((Foo)myFoo).<String>check("");

这也必须编译:

    myFoo.<String>check("");

尽管 MyFoo.check 不是通用的。


这听起来有点牵强。但即使我们相信这个论点,解决方案仍然过于宽泛和过分。 JLS 可以收紧它,以便 myFoo.<String,String>checkobj.<Blah>toString() 是非法的,因为类型参数 arity 不匹配。他们可能没有时间解决这个问题,所以他们只是选择了一条简单的路线。

当类型推断不起作用时,您需要类型见证(菱形中的类型)(参见 http://docs.oracle.com/javase/tutorial/java/generics/genTypeInference.html

为此给出的示例是菊花链式调用,例如:

processStringList(Collections.emptyList());

其中 processStringList 定义为:

void processStringList(List<String> stringList) 
{
    // process stringList
}

这将导致错误,因为它无法将 List<Object> 转换为 List<String>。因此,需要证人。尽管您可以分多个步骤执行此操作,但这样会方便得多。

想知道为什么 "Type Witness" 在 Java 中被扔来扔去? :D

要理解这一点,我们应该从 Type Inference 开始讲故事。

Type inference is a Java compiler's ability to look at each method invocation and corresponding declaration to determine the type argument (or arguments) that make the invocation applicable. The inference algorithm determines the types of the arguments and, if available, the type that the result is being assigned, or returned. Finally, the inference algorithm tries to find the most specific type that works with all of the arguments.

如果上述算法仍然无法确定类型,我们可以"Type Witness"明确说明我们需要什么类型。例如:

public class TypeWitnessTest {

    public static void main(String[] args) {
        print(Collections.emptyList());
    }

    static void print(List<String> list) {
        System.out.println(list);
    }
}

以上代码无法编译:

TypeWitnessTest.java:11: error: method print in class TypeWitnessTest cannot be applied to given types;
            print(Collections.emptyList());
            ^
  required: List<String>
  found: List<Object>
  reason: actual argument List<Object> cannot be converted to List<String> by method invocation conversion
1 error

所以,您有 Type Witness 来解决这个问题:

public class TypeWitnessTest {

    public static void main(String[] args) {
        print(Collections.<String>emptyList());
    }

    static void print(List<String> list) {
        System.out.println(list);
    }
}

这是可编译的并且工作正常,但是在 Java 8 中得到了更多改进:
JEP 101: Generalized Target-Type Inference

PS:我从基础开始,以便其他 Whosebug 读者也能受益。

编辑:

在非通用 Witness 上键入 Witness

public class InternetTest {
    public static void main(String[] args) {
        String s;
        s = Internet.<String>genericReturn(); //Witness used in return type, returns a string
        s = Internet.<Integer>stringReturn(); //Witness ignored, returns a string
    }
}

class Internet {
    public static <T> T genericReturn() { return null; }
    public static String stringReturn() { return null; }
}

我尝试使用 javac 1.6.0_65 模拟 @Rogue 示例,但编译失败并出现以下错误:

javac InternetTest.java 
InternetTest.java:5: stringReturn() in Internet cannot be applied to <java.lang.Integer>()
        s = Internet.<Integer>stringReturn(); //Witness ignored, returns a string
                    ^
1 error

@Rogue:如果您使用的版本比我使用的版本早,请告诉我您的 javac 版本。如果你是那么现在是不允许的。 :P

这是因为向后 and/or 向前兼容(在源代码级别)。

想象一下在 JDK 7 中为某些 类 的 Swing 引入泛型参数。它也可能发生在方法中(即使有限制)。如果结果证明是个坏主意,您可以删除它们,使用它的源代码仍然可以编译。在我看来,这就是允许这样做的原因,即使它没有被使用。

虽然灵活性有限。如果您引入了 n 类型的类型参数,如果 m != 0m != n.

,您以后不能更改为 m 类型(以源兼容的方式)

(我知道这可能无法回答您的问题,因为我不是 Java 的设计者,这只是我的 idea/opinion。)