接上篇博文 《Java范型那些事(一)》
参考oracle官网对于范型的介绍 : https://docs.oracle.com/javase/tutorial/extra/generics/legacy.html
目录
8. 使用类字面常量(class literals)作为运行时类型标记
6. 范型与遗留代码(未使用范型的代码)互操作
到目前为止,我们所有的例子都假设了一个理想化的世界,每个人都在使用最新版本的Java编程语言,它支持泛型。
唉,实际情况并非如此。 在早期版本的语言中已经编写了数百万行代码,并且它们不会在一夜之间全部转换。
稍后,在“将未使用范型的代码(下文简称遗留代码,或者旧代码)转换为使用泛型”部分中,我们将解决将旧代码转换为使用泛型的问题。 在本节中,我们将关注一个更简单的问题:遗留代码和泛型代码如何互操作? 这个问题有两个部分:在泛型代码中使用遗留代码和在遗留代码中使用泛型代码。
6.1 在泛型代码中使用遗留代码
你如何使用遗留代码,同时仍然在自己的代码中享受泛型的好处?
举个例子:例如,假设您要使用com.Example.widgets包。 Example.com的人们推出了一个库存控制系统,其亮点如下所示
package com.Example.widgets;
public interface Part {...}
public class Inventory {
/**
*将新装配添加到库存数据库。
*新装配名为name,由一个parts集合组成。
*集合parts的所有元素都必须支持Part接口
**/
public static void addAssembly(String name, Collection parts) {...}
public static Assembly getAssembly(String name) {...}
}
public interface Assembly {
// 返回一个Parts集合
Collection getParts();
}
现在当你调用addAssembly()方法时,你肯定会传正确的参数,即一个Part对象的集合。而范型天生就是为了这种情况而生的。
package com.mycompany.inventory;
import com.Example.widgets.*;
public class Blade implements Part {
...
}
public class Guillotine implements Part {
}
public class Main {
public static void main(String[] args) {
Collection<Part> c = new ArrayList<Part>();
c.add(new Guillotine()) ;
c.add(new Blade());
Inventory.addAssembly("thingee", c);
Collection<Part> k = Inventory.getAssembly("thingee").getParts();
}
}
大多数人的第一直觉是Collection真正意味着Collection <Object>。但是,正如我们之前看到的那样,在需要Collection <Object>的地方传递Collection <Part>是不安全的。更准确地说,类型Collection表示一些未知类型的集合,就像Collection <?>一样。
但等等,这也不是正确的!考虑调用getParts(),它返回一个Collection。然后将其分配给k,这是Collection <Part>。如果调用的结果是Collection <?>,则赋值将是错误。
实际上,分配是合法的,但它会生成未经检查的警告。而这个警告是必须要有的,因为事实是编译器无法保证其正确性。我们无法检查getAssembly()中的遗留代码,以确保返回的集合确实是Parts的集合。代码中使用的类型是Collection,可以合法地将所有类型的对象插入到这样的集合中。
那么,这不应该是一个错误吗?从理论上讲,是的;但实际上,如果范型代码要调用遗留代码,则必须允许这样做。由程序员来决定,在这种情况下,赋值是安全的,因为getAssembly()的契约表示它返回了一个Parts集合,即使类型签名没有显示这一点。
因此原始类型非常类似于通配符类型,但它们并不严格地进行类型检查。这是一个深思熟虑的设计决策,允许泛型与预先存在的遗留代码进行互操作。
从范型代码调用遗留代码本质上是危险的;一旦将范型代码与非泛型遗留代码混合,泛型类型系统通常提供的所有安全保证都是无效的。但是,你仍然比没有使用泛型更好。至少你知道你的代码是一致的。
目前还有更多非范型代码,然后是范型代码,并且不可避免地会出现需要混合的情况。
如果您发现必须混合使用旧代码和范型代码,请密切注意未经检查的警告。仔细考虑如何证明产生警告的代码的安全性。
如上面代码中编译器抛出的警告:
但是如果将Assembly接口中的getParts()方法的返回类型改为Collection<Part>,即将遗留的代码改成范型的形式,则可以消除该警告。
6.2 类型擦除和转换
先看以下一段代码,最后一行是 return ys.iterator().next();
在未经检查的调用add方法试图在原始类型的List中添加元素时,编译器报出警告;如果我们忽略警告并尝试执行此代码,它将在我们尝试使用错误类型时报错:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String。
在运行时,此代码的行为类似于:
public String loophole(Integer x) {
List ys = new LinkedList;
List xs = ys;
xs.add(x);
return(String) ys.iterator().next(); // run time error
}
发生上述的原因是,泛型是由Java编译器实现的一种称为擦除的前后转换。您(几乎)可以将其视为源到源的转换,其中loophole()的范型本将转换为非泛型版本。
因此,即使存在未经检查的警告,Java虚拟机的类型安全性和完整性也不会存在风险。
基本上,擦除消除(或擦除)所有泛型类型信息。抛出尖括号之间的所有类型信息,例如,像List <String>这样的参数化类型被转换为List。类型变量的所有剩余使用都由类型变量的上限(通常为Object)替换。并且,每当结果代码不是类型正确时,就会插入适当类型的强制转换,就像在loophole()的最后一行一样。
擦除的全部细节超出了本教程的范围,但我们刚才给出的简单描述与事实差不多。了解一下这一点很好,特别是如果你想做更复杂的事情,比如将现有的API转换为使用泛型(参见后续章节:使用泛型转换遗留代码),或者只是想了解为什么事情就是这样。
6.3 在遗留代码中使用范型代码
现在我们考虑一下相反的情况,即API端将其遗留代码改成使用范型,但是部分客户端还没有改成使用范型,修改后的API端代码:
package com.Example.widgets;
public interface Part {
...
}
public class Inventory {
/**
*将新装配添加到库存数据库。
*新装配名为name,由一个parts集合组成。
*集合parts的所有元素都必须支持Part接口
**/
public static void addAssembly(String name, Collection<Part> parts) {...}
public static Assembly getAssembly(String name) {...}
}
public interface Assembly {
// 返回一个 Parts 集合
Collection<Part> getParts();
}
客户端代码如下:
客户端代码是在引入泛型之前编写的,但它使用包com.Example.widgets和集合库,两者都使用泛型类型。 客户端代码中泛型类型声明的所有用法都是原始类型。
第1行生成未经检查的警告,因为原始Collection正在传递到期望Part类型集合的位置,并且编译器无法确保原始集合确实是Part集合。
作为替代方法,您可以使用source 1.4标志编译客户端代码,确保不会生成警告。 但是,在这种情况下,您将无法使用JDK 5.0中引入的任何新语言功能。
⚠️注意:所以当API(或者称为被调用端)改成范型形式时,客户端(或者称为调用端)也应该相应的改成范型形式,以避免警告的产生。
7. cast 和 instance of
泛型类在其所有实例之间共享这一事实的另一个含义是,询问实例它是否是泛型类型的特定调用的实例通常是没有意义的:
下面的代码会报出未经检查的警告,因为在运行时,因为这不是运行时系统要检查的东西
运行时不存在类型变量。 这意味着它们在时间和空间上都不会产生性能开销,这很好。 不幸的是,这也意味着你无法在类型转换时可靠地使用它们。即下面的代码也会报未经检查的警告:
7.1 范型数组
oracle官网文档有如下一段描述:
The component type of an array object may not be a type variable or a parameterized type, unless it is an (unbounded) wildcard type.You can declare array types whose element type is a type variable or a parameterized type, but not array objects.
即:在java中是”不能创建一个确切的泛型类型的数组”的。
也就是说下面的这个例子是不可以的:
List<String>[] ls = new ArrayList<String>[10];
而使用通配符创建泛型数组是可以的,如下面这个例子:
List<?>[] ls = new ArrayList<?>[10];
这样也是可以的,但会有未经检查的警告:
类似地,尝试创建元素类型为类型变量的数组对象会导致编译时错误,因为由于类型变量在运行时不存在,因此无法确定实际的数组类型:
解决这些限制的方法是使用范型类作为运行时类型标记,如下一节中所述
8. 使用类字面常量(class literals)作为运行时类型标记
JDK 1.5 加入了范型,java.lang.Class是范型的。下面这是一个有趣的例子,它将范型用于集合类以外的东西。
既然Class有一个类型参数T,你可能会问,T代表什么?它代表Class对象所代表的类型。
例如,String.class的类型是Class <String>,Serializable.class的类型是Class <Serializable>。这可用于改善反射代码的类型安全性。
特别是,由于Class中的newInstance()方法现在返回一个T,因此在反射创建对象时可以获得更精确的类型
例如,假设您需要编写一个执行数据库查询的工具类方法,以SQL字符串形式给出,并返回数据库中与该查询匹配的对象集合。
一种方法是显式传入工厂对象,编写如下代码:
interface Factory<T> { T make();}
public <T> Collection<T> select(Factory<T> factory, String statement) {
Collection<T> result = new ArrayList<T>();
/* Run sql query using jdbc */
for (/* Iterate over jdbc results. */) {
T item = factory.make();
/* Use reflection and set all of item's
* fields from sql results.
*/
result.add(item);
}
return result;
}
然后可以通过匿名内部类的形式,这样来调用:
select(new Factory<EmpInfo>(){
public EmpInfo make() {
return new EmpInfo();
}}, "selection strin
或者通过创建一个继承自Factory接口的实现类:
class EmpInfoFactory implements Factory<EmpInfo> {
...
public EmpInfo make() {
return new EmpInfo();
}
}
select(getMyEmpInfoFactory(), "selection string");
上述解决方案的缺点是它需要:
- 在调用站点使用详细的匿名内部类,
- 或为每个使用的类型声明一个接口实现类,并在调用时传递创建的工厂对象,这有点不自然。
而将类用作工厂对象是很自然的,然后可以通过反射使用它。今天(不使用泛型)代码可能写成:
Collection emps = sqlUtility.select(EmpInfo.class, "select * from emps");
...
public static Collection select(Class c, String sqlStatement) {
Collection result = new ArrayList();
/* Run sql query using jdbc. */
for (/* Iterate over jdbc results. */ ) {
Object item = c.newInstance();
/* Use reflection and set all of item's
* fields from sql results.
*/
result.add(item);
}
return result;
}
但是,这不会给我们提供我们想要的精确类型的集合。 既然Class是范型的,我们可以转而这样来写该段代码:
Collection<EmpInfo> emps = sqlUtility.select(EmpInfo.class, "select * from emps");
...
public static <T> Collection<T> select(Class<T> c, String sqlStatement) {
Collection<T> result = new ArrayList<T>();
/* Run sql query using jdbc. */
for (/* Iterate over jdbc results. */ ) {
T item = c.newInstance();
/* Use reflection and set all of item's
* fields from sql results.
*/
result.add(item);
}
return result;
}
上面的代码以类型安全的方式为我们提供了精确的集合类型。
这种使用类字面常量(class literals)作为运行时类型标记的技术是一个非常有用的技巧。 例如,这是一种在新API中广泛用于操作注释的习惯用法。