Java 泛型歧义方法



我无法理解以下代码背后的行为。任何帮助理解将不胜感激。

class Binder {
<T> void bind(Class<T> clazz, Type<T> type) {
System.out.println("clazz type");
}
<T> void bind(T obj, Type<T> type) {
System.out.println("obj type");
}
}
class Type<T> {
Type(T obj) { }
}
Binder binder = new Binder();
binder.bind(String.class, new Type<String>("x")) //works
binder.bind(Object.class, new Type<Object>(new Object()))  //ambiguous

上面的代码将失败

ERROR: reference to bind is ambiguous
both method <T>bind(java.lang.Class<T>,Type<T>) in Binder and method <T>bind(T,Type<T>) in Binder match

如果我要删除每个方法的第二个参数,则两个绑定调用都将执行第一个方法

class Binder {
<T> void bind(Class<T> clazz) {
System.out.println("clazz");
}
<T> void bind(T obj) {
System.out.println("obj");
}
}
Binder binder = new Binder();
binder.bind(String.class)
binder.bind(Object.class)

以上将打印"clazz"两次。

我认为这种行为在JLS 15.12.2.5选择最具体的方法中得到了充分的解释:

非正式的直觉是,如果第一个方法处理的任何调用都可以传递给另一个方法而不会产生编译时错误,则一个 [适用的] 方法比另一个 [适用方法] 更具体。

为了以另一种方式说明这一点,如果以下任一语句为真,则一种方法比另一种方法更具体:

  • 在对第一个方法的有效调用中传递的任何参数也可以在第二个方法的有效调用中传递。
  • 在第二个方法的有效调用中传递
  • 的任何参数也可以在第一个方法的有效调用中传递。

除非第一种方法和第二种方法相同,否则这些陈述中最多有一个可以为真。


选择最具体方法的一个重要点是,仅当多个方法适用于给定参数时,才需要这样做。

binder.bind(String.class, new Type<String>("x"))不是模棱两可的,因为<T> void bind(T, Type<T>)方法不适用:如果将Type<String>传递给该方法,则唯一可以推断出T的类型是String(因为Type<T>不是Type<Object>)。

因此,您必须将String传递给该方法。String.class是一个Class<String>,而不是一个String,所以该方法不适用,所以没有歧义需要解决,因为只有一种可能的方法 -<T> void bind(Class<T>, Type<T>)- 适用。


在模棱两可的情况下,我们将Type<Object>作为第二个参数传递。这意味着,如果两个重载都适用,则第一个参数需要分别是Class<Object>ObjectObject.class确实是这两件事,因此两种重载都适用。

为了证明这些是模棱两可的重载,我们可以找到一个反例来反驳"第一种方法处理的任何调用都可以传递给另一个方法"的说法。

这里的关键词是any:这与此处传递的特定参数无关,而仅与方法签名中的类型有关。

  • 成功的调用(binder.bind(String.class, new Type<String>("x")))无法调用bind(T, Type<T>)重载,因为String.class不是String
  • binder.bind("", new Type<String>(""))无法调用bind(Class<T>, Type<T>)重载,因为""是一个String,而不是一个Class<String>

QED。

这也可以通过为其中一个方法指定不同的名称(例如bind2)并尝试传递这些参数来证明。

<T> void bind(Class<T> clazz, Type<T> type) { ... }
<T> void bind2(T obj, Type<T> type) { ... }
binder.bind(String.class, new Type<String>("x")); // compiles
binder.bind2(String.class, new Type<String>("x")); // doesn't compile
binder.bind("", new Type<String>("x")) // doesn't compile
binder.bind2("", new Type<String>("x")) // compiles

指定不同的名称消除了歧义的可能性,因此您可以直接查看参数是否适用。


在 1 参数的情况下,您可以传递给<T> void bind(Class<T>)的任何内容也可以传递给<T> void bind(T)。这是因为Class<T>Object的子类,在第二种情况下,绑定T退化为Object,所以它接受任何东西。

因此,<T> void bind(Class<T>)<T> void bind(T)更具体

。重做上面的重命名演示:

<T> void bind3(Class<T> clazz) { ... }
<T> void bind4(T obj) { ... }
binder.bind3(String.class); // compiles
binder.bind4(String.class); // compiles
binder.bind3("") // doesn't compile
binder.bind4("") // compiles

显然,String.class可以同时传递给bind3bind4的事实并不能证明没有一个参数可以被bind3接受,但不能被bind4接受。我首先陈述了一种非正式的直觉,所以我将以">真的,没有"的非正式直觉结束。

让我修改一下系统输出,以便我理解:

public class Binder
{
class Type<T>
{
Type( T obj )
{
System.out.println( "Type class: " + obj.getClass( ) );
}
}
}

我们可以逐个测试每个案例:

对象调用如何不明确?

1) 类上的测试对象调用:

<T> void bind( Class<T> clazz, Type<T> type )
{               
System.out.println( "test clazz bind" );
System.out.println( "Clazz class: " + clazz );
}
@Test
public void bind_Object( )
{
Binder binder = new Binder( );
binder.bind(Object.class, new Type<Object>(new Object());
}

输出:

Type class: class java.lang.Object
test clazz bind
Clazz class: class java.lang.Object

我的解释:

在本例中,选择 T 作为对象。所以函数声明变成了bind(Class<Object> obj, Type<Object>)这很好,因为我们正在打电话bind(Object.class, new Type<Object)Object.class is assignable to Class<Object>的地方,所以这个电话很好。

2) T上的测试对象调用:

<T> void bind( T obj, Type<T> type )
{
System.out.println( "test obj bind" );
System.out.println( "Obj class: " + obj.getClass() );
}
@Test
public void bind_Object( )
{
Binder binder = new Binder( );
binder.bind(Object.class, new Type<Object>(new Object());
}

输出:

Type class: class java.lang.Object
test obj bind
Obj class: class java.lang.Class

我的解释:

在这种情况下,选择 T 作为对象。所以函数声明变得bind(Object obj, Type<Object>)这很好,因为我们调用的是bind(Object.class, new Type<Object), Class<Object>可分配给Object作为第一个参数。

因此,这两种方法都适用于 Object 调用。但是为什么字符串调用不是模棱两可的呢?让我们测试一下:

字符串调用如何不模棱两可?

3) 类上的测试字符串调用:

<T> void bind( Class<T> clazz,Type<T> type )
{
System.out.println( "test clazz bind" );
System.out.println( "Clazz class: " + clazz );
}
@Test
public void bind_String( )
{
Binder binder = new Binder( );
binder.bind( String.class, new Type<String>( "x") );
}

输出:

Type class: class java.lang.String
test clazz bind 
Clazz class: class java.lang.String

我的解释:

在这种情况下,选择 T 作为字符串。所以函数声明变得bind(Class<String> clazz, Type<String> type)这很好,因为我们调用的是肯定可分配bind(String.class, new Type<String)。T绑定怎么样?

4) 在 T 上调用测试字符串:

<T> void bind( T obj, Type<T> type )
{
System.out.println( "test obj bind" );
System.out.println( "Obj class: " + obj.getClass() );
}
@Test
public void bind_String( )
{
Binder binder = new Binder( );
binder.bind( String.class, new Type<String>( "x") );
}

输出:

编译器错误

我的解释:

在这种情况下,选择 T 作为字符串。所以函数声明变得bind(String obj, Type<String> type)这不行,因为我们用bind(String.class, new Type<String)调用。String.class which means Class<String>.因此,我们尝试使用不可分配的(Class, Type<String)输入调用(String, Type<String>)函数。