仿制药的仿制药如何运作?
虽然我确实理解了generics的一些角落情况,但我遗漏了以下示例的内容。
我有以下课程
1 public class Test { 2 public static void main(String[] args) { 3 Test t = new Test(); 4 List<Test> l =Collections.singletonList(t); 5 } 6 }
第4行给出了错误
Type mismatch: cannot convert from List<Test> to List<Test>`.
显然,编译器认为不同?
并不是真的平等。 虽然我的直觉告诉我,这是正确的。
任何人都可以提供一个例子,如果第4行合法,我会得到运行时错误吗?
编辑:
为了避免混淆,我通过具体的赋值替换了第3行中的=null
正如肯尼在评论中指出的那样,你可以通过以下方式解决这个问题:
List> l = Collections.>singletonList(t);
这立刻告诉我们操作并不安全 ,它只是有限推理的受害者。 如果它不安全,以上就不会编译。
由于在如上所述的通用方法中使用显式类型参数仅作为提示起作用,我们可以推测这里需要的是推理引擎的技术限制。 实际上,Java 8编译器目前预计会对类型推断进行许多改进 。 我不确定你的具体案例是否会得到解决。
那么,实际发生了什么?
好吧,我们得到的编译错误表明, Collections.singletonList
的类型参数T
被推断为capture
capture
。 换句话说,通配符具有与之关联的一些元数据,将其链接到特定上下文。
- 考虑捕获通配符(
capture extends Foo>
)的最佳方法是作为相同边界的未命名类型参数(即
,但无法引用T
)。 - “释放”捕获function的最佳方法是将其绑定到generics方法的命名类型参数。 我将在下面的示例中演示这一点。 请参阅Java教程“通配符捕获和帮助方法” (感谢参考@WChargin)以供进一步阅读。
假设我们想要一个移动列表的方法,包装到后面。 然后我们假设我们的列表有一个未知(通配符)类型。
public static void main(String... args) { List extends String> list = new ArrayList<>(Arrays.asList("a", "b", "c")); List extends String> cycledTwice = cycle(cycle(list)); } public static List cycle(List list) { list.add(list.remove(0)); return list; }
这很好用,因为T
被解析为capture extends String>
capture extends String>
,不是? extends String
? extends String
。 如果我们改为使用这个循环的非generics实现:
public static List extends String> cycle(List extends String> list) { list.add(list.remove(0)); return list; }
它将无法编译,因为我们没有通过将其分配给类型参数来使捕获可访问。
所以这开始解释为什么singletonList
的使用者会受益于类型提交者解析T
to Test
Test
,从而返回List
List
而不是List
List
。
但为什么不能将另一个分配给另一个人呢?
为什么我们不能只分配List
List
到List
List
?
好吧,如果我们考虑capture extends Number>
capture extends Number>
相当于一个匿名类型参数,其上限为Number
,那么我们可以将这个问题变成“为什么不进行以下编译?” (它没有!):
public static List> assign(List> t) { return t; }
这有充分的理由不编译。 如果确实如此,那么这将是可能的:
//all this would be valid List> doubleTests = null; List> numberTests = assign(doubleTests); Test integerTest = null; numberTests.add(integerTest); //type error, now doubleTests contains a Test
那么为什么要明确的工作呢?
让我们回到开头。 如果以上是不安全的,那么为什么允许这样做:
List> l = Collections.>singletonList(t);
为此,它意味着允许以下内容:
Test> capturedT; Test extends Number> t = capturedT;
好吧,这不是有效的语法,因为我们不能明确地引用捕获,所以让我们使用与上面相同的技术来评估它! 让我们将捕获绑定到另一个“assign”变体:
public static Test extends Number> assign(Test t) { return t; }
这成功编译。 并且不难看出为什么它应该是安全的。 这是类似的用例
List extends Number> l = new List();
没有潜在的运行时错误,它只是在编译器静态确定它的能力之外。 每当您进行类型推断时,它会自动生成一个新的 extends Number>
extends Number>
,两个捕获不被视为等效。
因此,如果通过为其指定
来从singletonList的调用中删除推断:
List> l = Collections.>singletonList(t);
它工作正常。 生成的代码与您的调用是合法的没有什么不同,它只是编译器的一个限制,它无法自己解决这个问题。
推理创建捕获和捕获不兼容的规则是阻止本教程示例编译然后在运行时爆炸的原因:
public static void swap(List extends Number> l1, List extends Number> l2) { Number num = l1.get(0); l1.add(0, l2.get(0)); l2.add(0, num); }
是的,语言规范和编译器可能会变得更加复杂,除了它之外告诉你的例子,但事实并非如此,而且它很容易解决。
也许这可以解释编译器的问题:
List extends Number> myNums = new ArrayList();
此genric通配符列表可以包含从Number扩展的任何元素。 因此可以为其分配一个整数列表。 但是现在我可以将一个Double添加到myNums,因为Double也从Number扩展,这将导致运行时问题。 所以编译器禁止对myNums的每次写访问,我只能在其上使用read方法,因为我只知道我得到的东西可以转换为Number。
所以编译器抱怨你可以用这样的通配符generics做很多事情。 有时他会对你可以确保安全和正常的事情感到生气。
但幸运的是,有一个技巧可以解决这个错误,所以你可以自己测试什么可以打破这个:
public static void main(String[] args) { List extends Number> list1 = new ArrayList(); List> list2 = copyHelper(list1); } private static List> copyHelper(List list) { return Collections.singletonList(list); }
原因是编译器不知道您的通配符类型是相同的类型。
它也不知道您的实例是null
。 尽管null
是所有类型的成员,但在类型检查时,编译器仅考虑声明的类型 ,而不考虑变量的值可能包含的类型。
如果代码被执行,它不会导致exception,但这只是因为该值为null。 仍然存在潜在的类型不匹配,这就是编译器的工作 – 禁止类型不匹配。
看一下类型擦除 。 问题是“编译时间”是Java必须强制执行这些generics的唯一机会,所以如果它通过它就无法判断你是否试图插入无效的东西。 这实际上是一件好事,因为这意味着一旦程序编译,Generics就不会在运行时产生任何性能损失。
让我们试着以另一种方式看你的例子(让我们使用两种扩展数字但行为非常不同的类型)。 考虑以下程序:
import java.math.BigDecimal; import java.util.*; public class q16449799 { public T val; public static void main(String ... args) { q16449799 t = new q16449799<>(); t.val = new BigDecimal(Math.PI); List> l = Collections.singletonList(t); for(q16449799 i : l) { System.out.println(i.val); } } }
这个输出(正如人们所期望的那样):
3.141592653589793115997963468544185161590576171875
现在假设您提供的代码不会导致编译器错误:
import java.math.BigDecimal; import java.util.concurrent.atomic.AtomicLong; public class q16449799 { public T val; public static void main(String ... args) { q16449799 t = new q16449799<>(); t.val = new BigDecimal(Math.PI); List> l = Collections.singletonList(t); for(q16449799 i : l) { System.out.println(i.val); } } }
您对输出的期望是什么? 你不能合理地将BigDecimal强制转换为AtomicLong(你可以从BigDecimal的值构造一个AtomicLong,但是转换和构造是不同的东西,并且Generics被实现为编译时糖以确保强制转换成功)。 至于@ KennyTM的评论,一个具体的类型正在寻找你的初始例子,但尝试编译这个:
import java.math.BigDecimal; import java.util.*; public class q16449799 { public T val; public static void main(String ... args) { q16449799 extends Number> t = new q16449799(); t.val = new BigDecimal(Math.PI); List> l = Collections.>singletonList(t); for(q16449799 extends Number> i : l) { System.out.println(i.val); } } }
当您尝试将值设置为t.val
这将会出错。