泛型几乎完全是经过编译器检查的文档。在运行时(
java.exe
)不知道它们是什么——大多数泛型一开始就无法通过编译(“rasure”)。
因此,在编译时将其“转换”为其他内容是无关紧要的。Generics
完全
让编译器检查你的工作。它在运行时不执行任何操作。
那又怎样
做
是吗?它将事物联系在一起。它告诉编译器:有一些类型,我们不知道它是什么,但在这些地方(不止一个地方,将一件事链接到其他地方是没有用的),它是相同的类型,无论它是什么。这种情况的任何“用法”都可以重新定义它是什么类型,只要它们保持“链接”。
例如,简单地想象一下这种方法:
public Object print(Object o) {
System.out.println(o);
return o;
}
一种简单的方法,你可以在使用时使用它来动态打印东西。例如,您可能想要小写传入用户名,以便根据用户名表(不区分大小写)对其进行检查,如下所示:
users.get(username.toLowerCase())
,但也许您希望看到输入的原始用户名,因此您可以执行以下操作:
users.get(print(username).toLowerCase());
但那行不通!的返回类型
print
方法是
Object
和
对象
没有
.toLowerCase()
方法
。
是的,你有眼睛。我也是:我们可以清楚地看到
打印
显然是一个字符串,所以它应该工作,但它不工作。这不是javac的疏忽——这是有意的行为:
稍后更改方法的主体
宣称是事实
关于您的API,以及在这个特定版本中会发生什么。
public Object print(Object o)
声明
今天和永远(或者至少,直到你说你的API不再向后兼容),取1个参数,可以是任何对象,这个方法返回一些东西。什么不,但是,它至少是一个对象(不是特别有用)。
今天
它会把你传给它的东西还给它。但是明天呢?谁知道呢。
所以,让我们解决这个问题,让它再次发挥作用。添加泛型。我们要
链接
具有返回类型的参数的类型:
public <T> T print(T object) {
System.out.println(object);
return object;
}
现在
maps.get(print(username).toLowerCase())
确实有效!太棒了
它之所以有效,只是因为你链接了类型:你告诉编译器:嘿,这个打印方法?每次调用它时,都会有一些类型。对它没有任何限制(好吧,它必须是Object或其某个子类型,所以不是基元,除此之外,任何东西都可以)。返回类型和参数类型都是该类型。哪种类型在当时最方便。因此,给定该规则,编译器会选择
String
成为那种类型,现在代码可以工作了,太好了。
这是
完全是编译器在做
.如果你对字节码进行反编译,你会注意到print方法只是
public Object print(Object object)
,而是
呼叫者
这有点改变-反编译
users.get(print(username).toLowerCase())
行中,您注意到编译器已注入
铸造
尽管java行不包含强制转换。那是因为
编译器
意识到这样做是安全的,
因为
泛型的一部分。
这同样适用于您的代码。你像第一个片段一样写它(用T),这样就可以了:
class Highrise extends Building {}
Highrise[] highrises = ....;
List<Highrise> asList = fromArrayToList(highrises);
如果你使用了代码中的第三个片段,那就不起作用了。事实上,第三个片段是
破碎的
。毕竟,我可以称之为:
List<Building> highrises = ...;
highrises.add(new LogCabin());
毕竟,驾驶室就是一座建筑。考虑到我可以琐碎地写:
Building b = new LogCabin();
如果
highrises.add(new LogCabin())
如果高层建筑是
List<Building>
但是你刚刚在高层建筑列表中添加了一个小木屋,你把它弄坏了。
使用泛型要实现的第二件事是
方差
给定一个X的列表,它不能仅仅被视为一个Y的列表,其中Y是一个超类型。这完全有意义:
Building b = new LogCabin();
但这不起作用:
List<Building> b = new ArrayList<LogCabin>();
为什么?好吧,因为你可以调用
b.add(new Highrise())
现在你的小木屋列表中有一座高层建筑:
List<LogCabin> logCabins = new ArrayList<LogCabin>();
List<Building> buildings = logCabins;
buildings.add(new Highrise());
LogCabin lc = logCabins.get(0);
上面的代码证明了为什么不变性是必要的。
? extends
允许您选择协方差,但是,为了解决上述问题,您不能调用
.add
在
List<? extends>
任何东西即使只是
List<?>
(是
List<? extends Object>
(好吧,为了变得迂腐,你可以打电话
.add(null)
-字面意思
null
文字,因为
无效的
literal同时是所有引用类型,但通常这是非常无用的)。
通过禁用“添加”,它现在不再重要。让你将木屋列表分配给建筑列表的问题是,这意味着你可以将非木屋建筑添加到列表中,但如果
add
不起作用
完全
,它不再重要。因此:
List<LogCabin> logCabins = new ArrayList<LogCabin>();
List<? extends Building> buildings = logCabin;
buildings.add(new Highrise());
现在你知道为什么了
extends
是有用的。
最好记住两件事来理解泛型:
-
类型变量以这种方式命名是有充分理由的:有一个Type,但你不知道它是什么
int x;
说“有一个整数,不知道是0、1、5、1238123,谁知道呢?它有一个用于任何特定调用的特定文件,但每次调用都可能不同”-
<T>
说有某种类型,谁知道它是什么。
-
它们将事物联系在一起。如果他们不把事情联系起来,那就没用,或者是语言黑客。类型var必须至少在两个位置使用。
两个地方的用法示例:
class ArrayList<T> {
void add(T elem) { ... }
T get(int idx) { ... }
void addAll(List<? extends T> elems) {}
}
至少有4个地方使用了T,这很好。实现联动。