我偶然发现了一段代码,让我想知道它成功编译的原因:
public class Main {
public static void main(String[] args) {
String s = newList(); // why does this line compile?
System.out.println(s);
}
private static <T extends List<Integer>> T newList() {
return (T) new ArrayList<Integer>();
}
}
有趣的是,如果我使用newList
修改方法<T extends ArrayList<Integer>>
的签名,它就不再有用了。
评论后更新&amp;响应: 如果我将泛型类型从方法移到类中,代码就不再编译了:
public class SomeClass<T extends List<Integer>> {
public void main(String[] args) {
String s = newList(); // this doesn't compile anymore
System.out.println(s);
}
private T newList() {
return (T) new ArrayList<Integer>();
}
}
答案 0 :(得分:33)
如果在方法中声明类型参数,则允许调用者为其选择实际类型,只要该实际类型将满足约束。该类型不必是实际的具体类型,它可以是抽象类型,类型变量或交集类型,在其他更通俗的单词中,假设类型。因此,as said by Mureinik可能会有一种类型扩展String
并实施List
。我们不能手动为调用指定交集类型,但我们可以使用类型变量来演示逻辑:
public class Main {
public static <X extends String&List<Integer>> void main(String[] args) {
String s = Main.<X>newList();
System.out.println(s);
}
private static <T extends List<Integer>> T newList() {
return (T) new ArrayList<Integer>();
}
}
当然,newList()
无法满足返回此类型的期望,但这是该方法的定义(或实现)的问题。在将ArrayList
投射到T
时,您应该收到“未选中”警告。唯一可能的正确实现是在这里返回null
,这使得该方法毫无用处。
重复初始语句的要点是泛型方法的调用者选择类型参数的实际类型。相反,当您使用
声明通用类时public class SomeClass<T extends List<Integer>> {
public void main(String[] args) {
String s = newList(); // this doesn't compile anymore
System.out.println(s);
}
private T newList() {
return (T) new ArrayList<Integer>();
}
}
type参数是类合同的一部分,因此创建实例的人将选择该实例的实际类型。实例方法main
是该类的一部分,必须遵守该合同。你不能选择你想要的T
; T
的实际类型已设置,在Java中,您通常无法找到T
的内容。
泛型编程的关键点是编写独立于为类型参数选择实际类型的代码。
但请注意,您可以使用您喜欢的任何类型创建另一个独立实例,并调用该方法,例如
public class SomeClass<T extends List<Integer>> {
public <X extends String&List<Integer>> void main(String[] args) {
String s = new SomeClass<X>().newList();
System.out.println(s);
}
private T newList() {
return (T) new ArrayList<Integer>();
}
}
这里,新实例的创建者选择该实例的实际类型。如上所述,实际类型不需要是具体类型。
答案 1 :(得分:18)
我猜这是因为List
是一个接口。如果我们忽略String
为final
一秒的事实,理论上你可以有一个extends String
的类(意思是你可以将它分配给s
)但是implements List<Integer>
(意味着可以从newList()
返回)。一旦将返回类型从接口(T extends List
)更改为具体类(T extends ArrayList
),编译器就可以推断出它们不能彼此分配,并产生错误。
这当然是因为String
实际上是final
而失败,我们可以期望编译器将此考虑在内。恕我直言,这是一个错误,虽然我必须承认我不是编译器专家,并且可能有一个很好的理由在此时忽略final
修饰符。
答案 2 :(得分:6)
我不知道为什么这个编译。另一方面,我可以解释如何充分利用编译时检查。
因此,newList()
是一个通用方法,它有一个类型参数。如果指定此参数,则编译器将为您检查:
无法编译:
String s = Main.<String>newList(); // this doesn't compile anymore
System.out.println(s);
通过编译步骤:
List<Integer> l = Main.<ArrayList<Integer>>newList(); // this compiles and works well
System.out.println(l);
指定type参数
类型参数仅提供编译时检查。这是设计使然,java使用type erasure作为泛型类型。为了使编译器适合您,您必须在代码中指定这些类型。
在创建实例时键入参数
最常见的情况是指定对象实例的模式。即列表:
List<String> list = new ArrayList<>();
在这里我们可以看到List<String>
指定列表项的类型。另一方面,新ArrayList<>()
没有。它使用diamond operator代替。即java编译器 infers 基于声明的类型。
方法调用时的隐式类型参数
调用静态方法时,必须以其他方式指定类型。有时您可以将其指定为参数:
public static <T extends Number> T max(T n1, T n2) {
if (n1.doubleValue() < n2.doubleValue()) {
return n2;
}
return n1;
}
您可以像这样使用它:
int max = max(3, 4); // implicit param type: Integer
或者像这样:
double max2 = max(3.0, 4.0); // implicit param type: Double
方法调用时的显式类型参数:
比如说,这就是你可以创建一个类型安全的空列表的方法:
List<Integer> noIntegers = Collections.<Integer>emptyList();
将类型参数<Integer>
传递给方法emptyList()
。唯一的限制是你必须指定类。即你不能这样做:
import static java.util.Collections.emptyList;
...
List<Integer> noIntegers = <Integer>emptyList(); // this won't compile
运行时类型令牌
如果这些技巧都不能帮助您,那么您可以指定runtime type token。即你提供一个类作为参数。一个常见的例子是EnumMap:
private static enum Letters {A, B, C}; // dummy enum
...
public static void main(String[] args) {
Map<Letters, Integer> map = new EnumMap<>(Letters.class);
}