抱歉标题太差。如果能帮我想一个更好的标题就好了,但我甚至不知道该如何描述我的问题。
假设我有以下类型。
interface M<K, V> {}
interface F<I, O> {}
假设我创建了一个实现这两个接口的类。
record A<A1, A2> (A1 a1, A2 a2)
implements
M<A1, A2>,
F<A1, A2>
{}
好的,太棒了。现在我可以A
在任何地方使用M
或 了F
。
现在我的问题来了。我尝试执行以下操作,但因编译器错误而失败。
public class DoublyNestedGenerics
{
interface M<K, V> {}
interface F<I, O> {}
record A <A1, A2> (A1 a1, A2 a2)
implements
M<A1, A2>,
F<A1, A2>
{}
private
static
<
D1,
D2,
D extends
M<D1, D2>
& F<D1, D2>
>
D
canItWork(final D1 d1, final D2 d2)
{
// return new A(d1, d2);
throw new UnsupportedOperationException();
}
}
好的,一切都编译通过了。但是,如果我取消注释返回并替换异常,则会收到以下编译错误。
DoublyNestedGenerics.java:28: warning: [rawtypes] found raw type: A
return new A(d1, d2);
^
missing type arguments for generic class A<A1,A2>
where A1,A2 are type-variables:
A1 extends Object declared in record A
A2 extends Object declared in record A
好的,说得通。让我将返回值改为这样。
return new A<>(d1, d2);
按下编译按钮会出现以下编译器错误。
DoublyNestedGenerics.java:28: error: incompatible types: cannot infer type arguments for A<>
return new A<>(d1, d2);
^
reason: no instance(s) of type variable(s) A1,A2 exist so that A<A1,A2> conforms to D
where A1,A2,D,D1,D2 are type-variables:
A1 extends Object declared in record A
A2 extends Object declared in record A
D extends M<D1,D2>,F<D1,D2> declared in method <D1,D2,D>canItWork(D1,D2)
D1 extends Object declared in method <D1,D2,D>canItWork(D1,D2)
D2 extends Object declared in method <D1,D2,D>canItWork(D1,D2)
1 error
我没有完全理解这个错误,所以我决定简化这个问题。
我改变了方法,改为使用一些硬编码类型,而不仅仅是D1
和D2
。新方法如下所示。
private
static
<
D extends
M<Integer, Integer>
& F<Integer, Integer>
>
D
canItWorkAttempt2(final Integer d1, final Integer d2)
{
// return new A<>(d1, d2);
throw new UnsupportedOperationException();
}
交换注释并按编译后出现以下编译器错误。
DoublyNestedGenerics.java:44: error: incompatible types: cannot infer type arguments for A<>
return new A<>(d1, d2);
^
reason: no instance(s) of type variable(s) A1,A2 exist so that A<A1,A2> conforms to D
where A1,A2,D are type-variables:
A1 extends Object declared in record A
A2 extends Object declared in record A
D extends M<Integer,Integer>,F<Integer,Integer> declared in method <D>canItWorkAttempt2(Integer,Integer)
1 error
好的,小得多,因此更容易解析。
让我印象深刻的一件事是它说A1 extends Object declared in record A
。
嗯,不对——应该是Integer
,而不是Object
。也许推理需要一些帮助。所以我把返回值改成了这个。
return new A<Integer, Integer>(d1, d2);
这导致了以下编译器错误。
DoublyNestedGenerics.java:44: error: incompatible types: A<Integer,Integer> cannot be converted to D
return new A<Integer, Integer>(d1, d2);
^
where D is a type-variable:
D extends M<Integer,Integer>,F<Integer,Integer> declared in method <D>canItWorkAttempt2(Integer,Integer)
1 error
进展!我现在开始有了怀疑,为了证实这些怀疑,我决定大大简化我的问题。我创建了以下类。
record N (Integer n1, Integer n2)
implements
M<Integer, Integer>,
F<Integer, Integer>
{}
编译没有问题。很好,让我们尝试更改返回类型以改用此类型。这是我将返回更改为的内容。
return new N(d1, d2);
满怀希望,我按下了编译按钮。
DoublyNestedGenerics.java:50: error: incompatible types: N cannot be converted to D
return new N(d1, d2);
^
where D is a type-variable:
D extends M<Integer,Integer>,F<Integer,Integer> declared in method <D>canItWorkAttempt2(Integer,Integer)
1 error
非常令人失望。但我发现了其他问题。错误信息中显示where D is a type-variable: D extends M<Integer,Integer>,F<Integer,Integer>
。
他们使用了extends这个词。无奈之下,我尝试添加以下类型。
interface C<C1, C2> extends M<C1, C2>, F<C1, C2> {}
record N2 (Integer n1, Integer n2) implements C<Integer, Integer> {}
然后,从那时起,我改变了我的回报,这样说。
final C<Integer, Integer> blah = new N2(d1, d2);
return blah;
然后我按下了编译。
DoublyNestedGenerics.java:55: error: incompatible types: C<Integer,Integer> cannot be converted to D
return blah;
^
where D is a type-variable:
D extends M<Integer,Integer>,F<Integer,Integer> declared in method <D>canItWorkAttempt2(Integer,Integer)
1 error
这时候,我心里很烦躁。
错误消息以某种方式告诉我C<Integer,Integer> cannot be converted to D
。它还告诉我D is a type-variable
,具体来说,D extends M<Integer,Integer>,F<Integer,Integer>
这正是 C 所做的。但是,它仍然不起作用?
这让我想到了这里。我遗漏了什么?如果有人愿意提出建议,我再次希望有人能帮我起草一个更好的标题。
最后,这里是完整的代码,以防难以理解。
public class DoublyNestedGenerics
{
interface M<K, V> {}
interface F<I, O> {}
record A <A1, A2> (A1 a1, A2 a2)
implements
M<A1, A2>,
F<A1, A2>
{}
record N (Integer n1, Integer n2)
implements
M<Integer, Integer>,
F<Integer, Integer>
{}
interface C<C1, C2> extends M<C1, C2>, F<C1, C2> {}
record N2 (Integer n1, Integer n2) implements C<Integer, Integer> {}
private
static
<
D1,
D2,
D extends
M<D1, D2>
& F<D1, D2>
>
D
canItWork(final D1 d1, final D2 d2)
{
// return new A<>(d1, d2);
throw new UnsupportedOperationException();
}
private
static
<
D extends
M<Integer, Integer>
& F<Integer, Integer>
>
D
canItWorkAttempt2(final Integer d1, final Integer d2)
{
final C<Integer, Integer> blah = new N2(d1, d2);
return blah;
// return new N(d1, d2);
// throw new UnsupportedOperationException();
}
}
您忽略的是
D
不是由canItWork
方法参数化的。它是由方法的调用者参数化的。您说得对,A
满足 的界限D
,但其他类型也一样。该canItWork
方法无法知道调用者期望哪种类型。检查以下内容:
这里,的调用者
test
期望的是一个实例,OtherFooBar<String, Stirng>
但得到的却是的一个实例FooBar<String, String>
。这两种类型是不兼容的。这种情况就是return new FooBar<T, U>(t, u)
编译失败的原因。OtherFooBar<String, String> _ = test("Hello", "World!");
根据Java 语言规范,此行完全合法。由此可见,的实现test
不能被允许编译。否则,您最终会得到非类型安全的代码,这首先就否定了使用静态类型语言(如 Java)的主要原因。因此,由于至少有一种情况return new FooBar<T, U>(t, u);
会导致类型不安全的代码,因此必须无条件拒绝它(即无法编译)。不受控制的演员阵容
test
您可以通过转换结果来编译上述示例(带有警告) :但是有一个原因会发出警告,因为现在您可以轻松地在
ClassCastException
运行时遇到这种情况,并且无需对示例进行任何其他更改。更糟糕的是,强制转换对于调用者来说是隐式的,这使得代码看起来是安全的,但实际上并非如此。只有当您知道它在所有情况下都会成功时,才应该使用未经检查的强制转换(有时我们比编译器更了解,这就是强制转换等功能存在的原因)。这里的核心问题是,Java 的类型推断不会自动识别出 A<Integer, Integer>(或 N 或 N2)是类型变量 D 的有效实例,即使 D 同时扩展了 M<Integer, Integer> 和 F<Integer, Integer>。关键点在于 Java 的泛型是不变的,这意味着即使 C<Integer, Integer> 扩展了 M<Integer, Integer> 和 F<Integer, Integer>,也并不意味着可以将 C<Integer, Integer> 实例分配给限制为 M<Integer, Integer> 和 F<Integer, Integer> 的类型变量 D。
类型推断与交叉类型斗争当你声明:
<D 扩展 M<D1,D2> & F<D1,D2>>
Java 尝试在调用点推断 D,但它本身并不“知道” A<D1, D2> 应该是要返回的实例。即使 A<D1, D2> 实现了这两个接口,Java 的泛型系统也需要与 D 完全匹配的类型。
缺乏直接类型匹配
返回新的A<>(d1,d2);
这里返回了 A<D1, D2>,但是 Java 无法识别 A<D1, D2> 与 D 完全匹配,因为 D 是一个类型变量,而不是明确命名的类。
类型变量 D 不是具体类型约束:
D 扩展了 M<Integer, Integer> 和 F<Integer, Integer>
意味着 D 可以是实现两个接口的任何类型。但由于 D 不是具体类型,因此 Java 不知道 A<Integer, Integer> 是 D 可接受的具体实现。
您可以强制 Java 识别具体的返回类型,而不必依赖 D 的泛型。
解决方案 1:使用显式转换
修改返回语句以明确转换:
或对于 N:
这是可行的,因为作为程序员,你知道 A<Integer, Integer> 满足 D 的界限,但 Java 的类型系统不会自动推断这一点。
解决方案 2:更改方法签名
不要让 D 作为类型变量,而是返回一个具体的接口类型:
这是有效的,因为现在该方法具有固定的返回类型,这与 Java 的期望一致。
解决方案 3:定义工厂方法
如果您确实需要保留 D,则可以添加工厂方法来强制类型约束:
这有助于 Java 识别 A<D1, D2> 是 D 的有效候选者