电脑故障

位置:IT落伍者 >> 电脑故障 >> 浏览文章

JDK 5.0中的泛型类型学习


发布日期:2019/9/6
 

JDK 中增加的泛型类型是 Java 语言中类型安全的一次重要改进但是对于初次使用泛型类型的用户来说泛型的某些方面看起来可能不容易明白甚至非常奇怪在本月的Java 理论和实践Brian Goetz 分析了束缚第一次使用泛型的用户的常见陷阱您可以通过讨论论坛与作者和其他读者分享您对本文的看法(也可以单击本文顶端或底端的讨论来访问这个论坛

表面上看起来无论语法还是应用的环境(比如容器类)泛型类型(或者泛型)都类似于 C++ 中的模板但是这种相似性仅限于表面Java 语言中的泛型基本上完全在编译器中实现由编译器执行类型检查和类型推断然后生成普通的非泛型的字节码这种实现技术称为擦除(erasure)(编译器使用泛型类型信息保证类型安全然后在生成字节码之前将其清除)这项技术有一些奇怪并且有时会带来一些令人迷惑的后果虽然范型是 Java 类走向类型安全的一大步但是在学习使用泛型的过程中几乎肯定会遇到头痛(有时候让人无法忍受)的问题

注意本文假设您对 JDK 中的范型有基本的了解

泛型不是协变的

虽然将集合看作是数组的抽象会有所帮助但是数组还有一些集合不具备的特殊性质Java 语言中的数组是协变的(covariant)也就是说如果 Integer 扩展了 Number(事实也是如此)那么不仅 Integer 是 Number而且 Integer[] 也是 Number[]在要求 Number[] 的地方完全可以传递或者赋予 Integer[](更正式地说如果 Number 是 Integer 的超类型那么 Number[] 也是 Integer[] 的超类型)您也许认为这一原理同样适用于泛型类型 —— List<Number> 是 List<Integer> 的超类型那么可以在需要 List<Number> 的地方传递 List<Integer>不幸的是情况并非如此

不允许这样做有一个很充分的理由这样做将破坏要提供的类型安全泛型如果能够将 List<Integer> 赋给 List<Number>那么下面的代码就允许将非 Integer 的内容放入 List<Integer>

List<Integer> li = new ArrayList<Integer>();

List<Number> ln = li; // illegal

lnadd(new Float());

因为 ln 是 List<Number>所以向其添加 Float 似乎是完全合法的但是如果 ln 是 li 的别名那么这就破坏了蕴含在 li 定义中的类型安全承诺 —— 它是一个整数列表这就是泛型类型不能协变的原因

其他的协变问题

数组能够协变而泛型不能协变的另一个后果是不能实例化泛型类型的数组(new List<String>[] 是不合法的)除非类型参数是一个未绑定的通配符(new List<?>[] 是合法的)让我们看看如果允许声明泛型类型数组会造成什么后果

List<String>[] lsa = new List<String>[]; // illegal

Object[] oa = lsa; // OK because List<String> is a subtype of Object

List<Integer> li = new ArrayList<Integer>();

liadd(new Integer());

oa[] = li;

String s = lsa[]get();

最后一行将抛出 ClassCastException因为这样将把 List<Integer> 填入本应是 List<String> 的位置因为数组协变会破坏泛型的类型安全所以不允许实例化泛型类型的数组(除非类型参数是未绑定的通配符比如 List<?>)

构造延迟

因为可以擦除功能所以 List<Integer> 和 List<String> 是同一个类编译器在编译 List<V> 时只生成一个类(和 C++ 不同)因此在编译 List<V> 类时编译器不知道 V 所表示的类型所以它就不能像知道类所表示的具体类型那样处理 List<V> 类定义中的类型参数(List<V> 中的 V)

因为运行时不能区分 List<String> 和 List<Integer>(运行时都是 List)用泛型类型参数标识类型的变量的构造就成了问题运行时缺乏类型信息这给泛型容器类和希望创建保护性副本的泛型类提出了难题

比如泛型类 Foo

class Foo<T> {

public void doSomething(T param) { }

}

在这里可以看到一种模式 —— 与泛型有关的很多问题或者折衷并非来自泛型本身而是保持和已有代码兼容的要求带来的副作用

泛化已有的类

在转化现有的库类来使用泛型方面没有多少技巧但与平常的情况相同向后兼容性不会凭空而来我已经讨论了两个例子其中向后兼容性限制了类库的泛化

另一种不同的泛化方法可能不存在向后兼容问题这就是 CollectionstoArray(Object[])传入 toArray() 的数组有两个目的 —— 如果集合足够小那么可以将其内容直接放在提供的数组中否则利用反射(reflection)创建相同类型的新数组来接受结果如果从头开始重写 Collections 框架那么很可能传递给 CollectionstoArray() 的参数不是一个数组而是一个类文字

interface Collection<E> {

public T[] toArray(Class<T super E> elementClass);

}

因为 Collections 框架作为良好类设计的例子被广泛效仿但是它的设计受到向后兼容性约束所以这些地方值得您注意不要盲目效仿

首先常常被混淆的泛型 Collections API 的一个重要方面是 containsAll()removeAll() 和 retainAll() 的签名您可能认为 remove() 和 removeAll() 的签名应该是

interface Collection<E> {

public boolean remove(E e); // not really

public void removeAll(Collection<? extends E> c); // not really

}

但实际上却是

interface Collection<E> {

public boolean remove(Object o);

public void removeAll(Collection<?> c);

}

为什么呢?答案同样是因为向后兼容性xremove(o) 的接口表明如果 o 包含在 x 中则删除它否则什么也不做如果 x 是一个泛型集合那么 o 不一定与 x 的类型参数兼容如果 removeAll() 被泛化为只有类型兼容时才能调用(Collection<? extends E>)那么在泛化之前合法的代码序列就会变得不合法比如

// a collection of Integers

Collection c = new HashSet();

// a collection of Objects

Collection r = new HashSet();

cremoveAll(r);

如果上述片段用直观的方法泛化(将 c 设为 Collection<Integer>r 设为 Collection<Object>)如果 removeAll() 的签名要求其参数为 Collection<? extends E> 而不是 noop那么就无法编译上面的代码泛型类库的一个主要目标就是不打破或者改变已有代码的语义因此必须用比从头重新设计泛型所使用类型约束更弱的类型约束来定义 remove()removeAll()retainAll() 和 containsAll()

在泛型之前设计的类可能阻碍了显然的泛型化方法这种情况下就要像上例这样进行折衷但是如果从头设计新的泛型类理解 Java 类库中的哪些东西是向后兼容的结果很有意义这样可以避免不适当的模仿

擦除的实现

因为泛型基本上都是在 Java 编译器中而不是运行库中实现的所以在生成字节码的时候差不多所有关于泛型类型的类型信息都被擦掉换句话说编译器生成的代码与您手工编写的不用泛型检查程序的类型安全后进行强制类型转换所得到的代码基本相同与 C++ 不同List<Integer> 和 List<String> 是同一个类(虽然是不同的类型但都是 List<?> 的子类型与以前的版本相比在 JDK 中这是一个更重要的区别)

擦除意味着一个类不能同时实现 Comparable<String> 和 Comparable<Number>因为事实上两者都在同一个接口中指定同一个 compareTo() 方法声明 DecimalString 类以便与 String 与 Number 比较似乎是明智的但对于 Java 编译器来说这相当于对同一个方法进行了两次声明

public class DecimalString implements Comparable<Number> Comparable<String> { } // nope

擦除的另一个后果是对泛型类型参数是用强制类型转换或者 instanceof 毫无意义下面的代码完全不会改善代码的类型安全性

public <T> T naiveCast(T t Object o) { return (T) o; }

编译器仅仅发出一个类型未检查转换警告因为它不知道这种转换是否安全naiveCast() 方法实际上根本不作任何转换T 直接被替换为 Object与期望的相反传入的对象被强制转换为 Object

擦除也是造成上述构造问题的原因即不能创建泛型类型的对象因为编译器不知道要调用什么构造函数如果泛型类需要构造用泛型类型参数来指定类型的对象那么构造函数应该接受类文字(Fooclass)并将它们保存起来以便通过反射创建实例

结束语

泛型是 Java 语言走向类型安全的一大步但是泛型设施的设计和类库的泛化并非未经过妥协扩展虚拟机指令集来支持泛型被认为是无法接受的因为这会为 Java 厂商升级其 JVM 造成难以逾越的障碍因此采用了可以完全在编译器中实现的擦除方法类似地在泛型 Java 类库时保持向后兼容也为类库的泛化方式设置了很多限制产生了一些混乱的令人沮丧的结构(如 ArraynewInstance())这并非泛型本身的问题而是与语言的演化与兼容有关但这些也使得泛型学习和应用起来更让人迷惑更加困难

上一篇:上海达内学员赴贝尔阿尔卡特面试题分享

下一篇:晒晒上海方面的面试题目