Java 泛型(Generics) 综述
一. 引子
- 一般的类和方法。仅仅能使用详细类型:要么是基本类型。要么是自己定义类型。假设要编写能够应用于多种类型的代码,这样的刻板的限制对代码的束缚就会非常大。
- 多态算是一种泛化机制,但对代码的约束还是太强(要么继承父类。要么实现接口)。
- 有很多原因促成了泛型的出现。而最引人注目的一个原因,就是为了创造容器类。
(泛型的主要目的之中的一个就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性)
比如,在 Java 实现加入泛型前。ArrayList 仅仅维护一个 Object 类型的数组:
public class ArrayList{
private Object[] elementData;
...
public Object get(int i){ ... }
public void add(Object o){ ... }
...
}
显然。这样的实现存在两个问题:
- 当获取一个值时,必须进行强制类型转换:
ArrayList list = new ArrayList();
...
String str = (String)list.get(0);
- 该 ArrayList 没有错误检查,即能够向数组中加入不论什么类型的对象:
list.add(new Integer(1));
向上述的动态数组中加入一个整形对象,程序在编译时和执行时都不会出错。但当我们强制将 get 结果转型时,就会抛出 ClassCastException 异常,程序将会出错。
对于我们的容器类,它是能够存储不论什么对象的,但我们在使用时,一次仅仅想也仅仅应该向当中放入一种对象。基于这样的需求,提出了类型參数化的概念,即泛型。
二. 泛型基础
1.概念
- “泛型”术语:适用于很多很多的类型
- “泛型”本质:实现的类型參数化的概念。使代码能够应用于多种类型
- “泛型”核心:告诉编译器想使用什么类型(指定详细类型參数或对其进行限制)。然后编译器帮你处理一切细节(类型安全检查等)
“泛型”初衷:
(1). 希望类或方法具备最广泛的表达能力,即通过解耦类或方法与所使用的类型之间的约束;
(2). 对容器类而言,泛型在保证容器类能够存储不论什么类型对象的同一时候,又保证了容器类一旦声明自己将要保存的元素类型时,就不可再保存其它类型了,比如:
ArrayList<Fruit> list = new ArrayList<Fruit>();
fruits.add(new Fruit()); // OK
fruits.add(new Apple()); // OK
fruits.add(new Orange()); // OK
fruits.add(new Object()); // Error
上述代码表明了该容器仅仅能保存 Fruit类型 的对象,由于 Apple 也是一种 Fruit,所以其也能够保存 Apple类型对象。但对于不属于 Fruit类型 的对象。编译器杜绝将其放入列表中。
简单地说:泛型 = 编译时的类型检查 + 编译时的类型擦除(编译器插入 checkcast 等) + 执行时的自己主动类型转换。**所以。我们在理解和应用泛型时,一定要从编译期和执行时两个视角去分析。
- 类型參数判断: 当使用泛型类时。必须在创建对象的时候指定类型參数的值;而使用泛型方法时。通常不必指明參数类型(编译器类型參数判断的能力是有限的,有时我们必须显式的指明类型參数;但大多数情况下,编译器能够胜任这项任务),由于编译器会为我们找出详细类型(如 例1 所看到的)。可是编译器的类型推导能力是有限的。这样的情况下,我们必须进行显式的类型实例化(如 例2 所看到的)。
例1:
在 Java 1.7 以后,我们能够这样创建一个ArrayList:
ArrayList<String> list = new ArrayList<>;
list 变量的类型就决定了它引用的动态数组所能存储的元素类型,即后者的类型參数能够从变量中判断出。
例2:
public class TypeInference {
public static <E> Set<E> union(Set<? extends E> s1, Set<?
extends E> s2) {
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
public static void main(String[] args) {
Set<Integer> integers = new HashSet<Integer>();
Set<Double> doubles = new HashSet<Double>();
Set<Number> numbers = null;
//编译器的类型判断能力有限
numbers = TypeInference.union(integers, doubles); // Error
numbers = TypeInference.<Number>union(integers, doubles); // OK
}
}
如例2所看到的,为 numbers 赋值时须要显式对类型參数实例化。
2.定义与语法
- 泛型类(參数化类)
public class Holder<T>{}
- 泛型接口(參数化接口)
public interface Generator<T>{}
- 泛型方法(參数化方法;所在类能够是泛型类,也能够不是;能够独立于类而产生变化;细粒度)
public <T> void f(T x){}
3.注意事项
- 仅仅有当你希望使用的參数类型比某个详细类型(以及它的全部子类型)更加泛化时————也就是说。当你的代码能够跨多个类工作时,使用泛型才有所帮助;否则,使用多态就能够满足要求。
public class HasF {
public void f(){...}
}
//以下两种实现方式所取得的效果是一样的
//泛型实现
class Manipulator1<T extends HasF>{
private T obj;
public Manipulator1(T x){
this.obj = x;
}
public void manipulate(){
obj.f();
}
}
//多态实现
class Manipulator2{
private HasF obj;
public Manipulator2(HasF x){
this.obj = x;
}
public void manipulate(){
obj.f();
}
}
- 泛型类的识别(误区)
先看以下两段代码:
// 第一段代码
public class Pair<T> {
private T first;
private T second;
public Pair(T first, T second){
this.first = first;
this.second = second;
}
public void setFirst(T first){
this.first = first;
}
public T getFirst(){
return first;
}
public void setSecond(T second){
this.second = second;
}
public T getSecond(){
return second;
}
}
// 第二段代码
public class DateInterval extends Pair<Date> { // 时间间隔类
public DateInterval(Date first, Date second){
super(first, second);
}
@Override
public void setSecond(Date second) {
super.setSecond(second);
}
@Override
public Date getSecond(){
return super.getSecond();
}
}
由泛型类的定义可知,Pair<T>
是一个泛型类。由于在类名后面有类型參数;类DateInterval 后面没有跟类型參数列表。因此该类就是一个 T 被替换为 Date 的实体类,其从 Pair<Date>
泛型类型 继承得到的方法列表。与泛型彻底无关。
对泛型类
LinkedList<T>
的类型參数T
实例化所得到的不同泛型类型的理解下图中,
LinkedList<String>
。LinkedList<Point>
和LinkedList<PolyLine>
是三种不同的类型,就像 Integer 和 String 一样,是两种互不同样的类型。可是,三者共享同一个 Class 对象,换句话说。三者在执行期的类型是一样的,但在编译期依据类型參数的不同成为截然不同的类型,以下代码可为例证。
public class TestClassTypes {
public static void main(String[] args) {
LinkedList<String> proverbs = new LinkedList<>();
LinkedList<Object> numbers = new LinkedList<>();
System.out.println("numbers class name: " + numbers.getClass().getName()); // Output: java.util.LinkedList
System.out.println("proverbs class name: " + proverbs.getClass().getName()); // Output: java.util.LinkedList
System.out.println("Compare Class objects: " + numbers.getClass().equals(proverbs.getClass())); // Output:true
// 由于 LinkedList<String> 与 LinkedList<Object> 在编译期根本就是不同类型。所以以下代码编译不能通过:
proverbs = (LinkedList<String>)numbers; // 相似于:把 Integer 类型实例强制转型为 String实例 赋给 String引用
// 每一个类都是 Object 的子类
Object obj = (Object)numbers;
System.out.println("obj class name " + obj.getClass().getName()); // Output: java.util.LinkedList
// 会有转型安全的异常
proverbs = (LinkedList<String>)obj;
System.out.println("obj in proverbs class name " + obj.getClass().getName()); // Output:java.util.LinkedList
}
}
- 在泛型类中, static 域或方法无法訪问泛型类的类型參数。若静态方法须要使用泛型能力。就必须使其成为泛型方法(不与泛型类共享类型參数)
在一个类中。static 域或方法都是该类的 Class对象的成员,而我们知道泛型所创造出来的全部类型都共享一个 Calss对象, 因此实质上不受泛型參数限制,所以例如以下代码根本不能通过编译:
public class Test2<T> {
public static T one; //编译错误
public static T show(T one){ //编译错误
return null;
}
}
可是要注意区分以下的一种情况:
public class Test2<T> {
public static <T> T show(T one){//这是正确的
return null;
}
}
由于这是一个泛型方法
。在泛型方法中使用的 类型參数T
是自己在方法中定义的T
,而不是泛型类中的 T。
- 限制泛型可用的类型
在定义泛型类别时,预设能够使用不论什么的类型来实例化泛型类中的类型。可是假设想要限制使用泛型的类别时,即要求仅仅能使用某个特定类型或者其子类型才干实例化该类型时,使用 extends 关键字指定这个类型必须是继承或者实现某个接口。一般地。当没有指定泛型继承的类型或实现的接口时。默认等价于使用 T extends Object
,因此,默认情形下不论什么类型都能够作为參数插入。特别地。为类型參数设定的第一个边界能够是类类型或接口类型,类型參数的第一个边界之后的随意额外边界都仅仅能是接口类型。同一时候,一般将标记性接口放到靠后位置,这些类型參数之间有 & 相连接。
publc class MyClass<T extends Number & Serilizable>{
...
}
- 在调用泛型方法的时候,能够指定泛型,也能够不指定泛型
在不指定泛型的情况下。泛型变量的类型为该方法中的几种类型的同一个父类的最小级,直到Object; 在指定泛型的时候。该方法中的几种类型必须是该泛型实例类型或者其子类。
//代码演示样例
public class Test2{
public static void main(String[] args) {
/**不指定泛型的时候*/
Integer i = Test2.add(1, 2); //这两个參数都是Integer,所以T为Integer类型
Number f = Test2.add(1, 1.2); //这两个參数一个是Integer,一个是Double,所以取同一父类的最小级。为Number
Object o = Test2.add(1, "asd"); //这两个參数一个是Integer,一个是String,所以取同一父类的最小级。为Object
System.out.println(i.getClass().getName()); //输出: java.lang.Integer
System.out.println(f.getClass().getName()); //输出: java.lang.Double
System.out.println(o.getClass().getName()); //输出: java.lang.String
/**指定泛型的时候*/
int a = Test2.<Integer>add(1, 2); //指定了Integer。所以仅仅能为Integer类型或者其子类
int b = Test2.<Integer>add(1, 2.2); //编译错误,指定了Integer,不能为Double
Number c = Test2.<Number>add(1, 2.2); //指定为Number,所以能够为Integer和Double
}
//这是一个简单的泛型方法
public static <T> T add(T x,T y){
return y;
}
}
注意,这个样例中的两个输出是java.lang.Double和java.lang.String。而不是java.lang.Number和
java.lang.Object:
System.out.println(f.getClass().getName()); //输出: java.lang.Double
System.out.println(o.getClass().getName()); //输出: java.lang.String
实际上,这个问题涉及泛型机制和多态两点。
在样例中。类型參数T被编译器用Number替换。这是没问题的,由于不管整形还是浮点型都属于数型,这是由多态机制保证的。可是。不管x还是y。它们本质上还是各自的类型不会发生不论什么改变。
要注意的是,这里的getClass()方法返回的变量的实际类型,即执行时类型而非编译时类型,因此返回y的类型是double而非number。
- 泛型的兼容性
(1) 从泛型类型生成的不论什么类型的引用都能存储到相应的原生类型的变量中
LinkedList list = new LinkedList<String>();
这样编写代码是合法兼容的,可是,不应该将这作为日常编程习惯的一部分,由于这样的实践存在固有的风险:由于类型安全性检查是针对引用的,所以上述写法和例如以下写法实质上是一样的:
LinkedList list = new LinkedList();
(2) 从原生类型生成的引用能存储到不论什么类型的泛型类型的变量中
LinkedList<String> list1 = new LinkedList();
LinkedList<Integer> list2 = new LinkedList();
这样编写代码是合法兼容的,可是,由于我们能够将一个已经原生的 LinkedList对象 直接赋值此类引用,尽管在之后在加入元素是会进行类型安全检查,但之前的 LinkedList对象 所存储的元素可能五花八门,给程序带来隐患。详细请參照下图:
ArrayList 中 get 方法的源代码:
/**
* Returns the element at the specified position in this list.
*
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
RangeCheck(index);
return (E) elementData[index]; // 仅仅会让编译器觉得该方法所返回的值是 E 类型,但此处转型不会起到预期效果。由于执行时 T 会被替换为 Object !!!
}
primitive类型
不能够作为类型參数(八大类型)
- 若使用泛型方法能够代替将整个类泛型化。那么就应该使用泛型方法
- 泛型方法与可变參数列表能够非常好的共存
public static <T> void f(T... args){}
三. 通配符及泛型的逆变和协变
1、 通配符
(1) 无界通配符
我们知道,通过为泛型类的每一个类型形參提供类型实參,能够表达由这个泛型类定义的集合中的特定类型。比如,为了设定存储 String 的 ArrayList<String>
,就将类型參数设定为 String。所以动态数组类型就是 ArrayList<String>
。
若不想为泛型类的类型參数提供详细类型,能够将參数设定为 “?”,这就是通配符的作用, 通配符类型能够表示不论什么类或接口类型
。
ArrayList<?> list = new ArrayList<String>();
list = new ArrayList<Double>();
list.add(e); // e cannot be resolved to a variable
System.out.println(list1.size()); // OK
list 变量是ArrayList<?>
类型。所以能将指向随意类型的ArrayList<>
对象的引用存储在当中。但由于 list 是通配符类型參数的结果。所以存储引用的实际类型并不知道,因而无法使用这个变量调用不论什么与类型參数有关的方法。
特别地,在 Java 集合框架中,对于參数值是未知类型的容器类。仅仅能读取当中元素。不能向当中加入元素。 由于。其类型未知,所以编译器无法识别加入元素的类型和容器的类型是否兼容。唯一的例外是 NULL(对 Null 而言,无所谓类型)。
(2) 深入理解无界通配符
我们有必要对以下三种类型进行区分:
- List : 持有不论什么Object类型 的 原生List。编译器不会对原生类型进行安全检查;
List<?>
:具有某种特定类型 的 非原生List。编译器会进行安全检查;List<Object>
: 编译器觉得List<Object>
是List<?>
的子类型;
public class Wildcards {
// Raw argument:
static void rawArgs(Holder holder, Object arg) {
holder.set(arg); // Warning:
holder.set(new Wildcards()); // Same warning
// OK, but type information has been lost:
Object obj = holder.get();
}
// Similar to rawArgs(), but errors instead of warnings:
static void unboundedArg(Holder<?
> holder, Object arg) {
// holder.set(arg); // Error:
// holder.set(new Wildcards()); // Same error
// OK, but type information has been lost:
Object obj = holder.get();
}
}
2、 向上转型 / 通配符的上界 / 协变
在引入通配符的上界这一概念时,我们先看一下数组的一种特殊行为:基类型的数组引用
能够被赋予导出类型的数组
,如以下的代码所看到的:
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // 编译期、执行期都 OK
fruit[1] = new Jonathan(); // 编译期、执行期都 OK
fruit[3] = new Fruit(); // 编译期 OK、执行期抛出 java.lang.ArrayStoreException(由于 fruit 的执行时类型是 Apple[], 而不是 Fruit[] 或 Orange[])
// 说明 Fruit[] 是 Apple[] 的父类型
System.out.println(Fruit[].class.isAssignableFrom(Apple[].class)); // true
}
}
由此能够说明:
由 12 行可知。该行代码编译期正常,则进一步说明:编译器的类型检查是针对引用的(Fruit型数组能够放入Fruit及其子类型对象);但在执行时,由于
fruit引用
实际上指的是一个Apple数组
,而作为Apple数组
则仅仅能够向当中放入Apple及其子类型对象,因此当放入 Fruit对象时。抛出异常。由 15 行可知,Fruit[] 是 Apple[] 的父类型,因此依据Java多态特性,前者能够指向后者对象。
我们知道。泛型的主要目标之中的一个就是将这样的错误检查移到编译期。那么,假设我们用泛型容器代替数组,那将会发生什么呢?
public class NonCovariantGenerics {
List<Fruit> flist = new ArrayList<Apple>(); // Compile Error: Type Mismatch
}
由以上代码能够知道。编译期根本不同意我们这么做。试想,假设编译期同意我们这样做,该容器就同意存入不论什么类型的对象。仅仅要它是一种Fruit。而不像数组那样会抛出执行时异常,违背了泛型的初衷(泛型保证容器的类型安全检查)。
所以,在编译期看来,List<Fruit> 和 List<Fruit> 根本就是两种不同的类型,并无不论什么继承关系
。
可是,有时你想要在以上两个类型之间建立某种向上转型关系,这就引出了通配符的上界。
比如:
public class GenericsAndCovariance {
public static void main(String[] args) {
// 同意我们向上转型,向数组那样
List<? extends Fruit> flist = Arrays.asList(new Apple());
// Compile Error: can’t add any type of object:
flist.add(new Apple()); // Compile Error
flist.add(new Fruit()); // Compile Error
flist.add(new Object()); // Compile Error
flist.add(null); // Legal but uninteresting
// We know that it returns at least Fruit:
Fruit f = flist.get(0);
Object o = flist.get(0);
Apple a = flist.get(0); // Compile Error:Type mismatch
flist.contains(new Apple()); // OK
flist.indexOf(new Apple()); // OK
}
}
对于上述样例。flist 的类型就是List<? extends Fruit>了,但这并不意味着能够向这个 List 能够加入不论什么类型的 Fruit。甚至于不能加入 Apple。尽管编译器知道这个 List 持有的是 Fruit,但并不知道其详细持有哪种特定类型(可能是List<Fruit>。List<Apple>,List<Orange>。List<Jonathan>),所以编译器不知道该加入那种类型的对象才干保证类型安全(add 方法的參数为 “? extends Fruit” ),因而编译器杜绝不论什么加入不论什么类型的 Fruit。可是。对于诸如 get(int index)【我们进行读取操作时,编译器是同意的,而且编译器还知道 List 中的不论什么一个对象至少具有 Fruit类型】、contains(Object o) 和 indexof(Object o) 等操作,由于其參数类型不涉及通配符,因此编译器同意调用这些操作。
因此。一旦执行这样的向上转型,我们就丢掉向当中加入不论什么对象的能力。更一般地,编译器会直接拒绝对參数列表中涉及通配符的方法的调用。
因此,这意味着将由泛型类的设计者来决定哪些调用地安全的,并使用 Object类型 作为其參数类型,比如 contains 方法和 indexof 方法。比如,
public class Holder<T> {
private T value;
public Holder() {
}
public Holder(T val) {
value = val;
}
public void set(T val) {
value = val;
}
public T get() {
return value;
}
public boolean equals(Object obj) {
return value.equals(obj);
}
public static void main(String[] args) {
Holder<Apple> Apple = new Holder<Apple>(new Apple());
Apple d = Apple.get();
Apple.set(d);
Holder<?
extends Fruit> fruit = Apple; // OK
Fruit p = fruit.get();
d = (Apple) fruit.get(); // Returns ‘Fruit’。类型擦除,返回上界
// No warning,执行时异常 java.lang.ClassCastException
Orange c = (Orange) fruit.get();
// fruit.set(new Apple()); // Cannot call set(),參数列表含通配符
// fruit.set(new Fruit()); // Cannot call set()。參数列表含通配符
fruit.equals(d); // OK,參数列表不含通配符
}
}
3、超类型通配符 / 通配符的下界 / 逆变
我们能够使用超类型通配符
指定通配符的下界, 方法是<? super MyClass>
,甚至能够用在类型參数上<?
super MyClass>(尽管我们不能对泛型參数给出一个超类型边界。即不能声明<T super MyClass>)。这使得我们能够安全的传递一个对象到泛型类型中,因此。有了超类型通配符。就能够向 Collection 写入了,例如以下图所看到的:
由图片可知,參数 apples 是 Apple 或 Apple的某种基类型 (比如:Fruit,Object,…) 的 List。也就是说,该 List 能够是 List<Apple>, List<Fruit> 或 List<Object>等,但不管详细指的是哪一种,我们向当中加入 Apple 或 Apple的子类型 总是安全的。但编译器不同意向该 List 放入一个 Fruit 对象, 由于 该List 的类型可能是 List<Apple> , 这样将会违背泛型的本意。
对于List<?
super Apple>,在读取容器元素时,由于该容器所包括的元素可能是 Object类型、 Fruit类型 和 Apple类型。因此。从容器所读取到的元素仅仅能确定是 Object类型的。如以下图片所看到的:
4、协变与逆变
逆变与协变用来描写叙述类型转换(type transformation)后的继承关系,其定义:假设 A,B 表示类型。f(⋅)表示类型转换。≤ 表示继承关系(比方,A ≤ B 表示A是B的子类);
- f(⋅) 是逆变(contravariant)的,当 A≤B 时有 f(B)≤f(A) 成立;
- f(⋅) 是协变(covariant)的。当 A≤B 时有 f(A)≤f(B) 成立;
- f(⋅) 是不变(invariant)的。当 A≤B 时上述两个式子均不成立。即f(A)与f(B)相互之间没有继承关系。
接下来。我们看看Java中的常见类型转换的协变性、逆变性或不变性:
(1).泛型
令f(A) = ArrayList<A>
,那么f(⋅) 是逆变、协变还是不变的呢?假设是逆变,则ArrayList<Integer>
是ArrayList<Number>
的父类型。假设是协变,则ArrayList<Integer>
是ArrayList<Number>
的子类型。假设是不变,二者没有相互继承关系。由于实际上ArrayList<Number>
和ArrayList<Integer>
无关,所以泛型是不变
的。
(2).数组
令f(A) = A[],easy证明数组是协变的:
Number[] numbers = new Integer[3];
5、实现泛型的协变与逆变
我们知道Java 中的泛型是不变的
,可我们有时须要实现泛型的逆变与协变,怎么办呢? 这时,通配符 ?
派上了用场:
<?
实现了extends>
泛型的协变
,比方:
ArrayList<? extends Apple> l3 = new ArrayList<>();
ArrayList<?
extends Fruit> l4 = new ArrayList<>();
l4 = l3;
对于 ArrayList<? extends Apple> 类型
,我们知道其表示某种详细类型(仅仅是没有确定下来),可是不管其详细指的是ArrayList<Apple> 类型
还是ArrayList<Jonathan> 类型
,都是能够赋给ArrayList<?
extends Fruit> 类型的引用的,反之则不能够。
因此。我们能够觉得ArrayList<?
extends Fruit> 类型是ArrayList<?
extends Apple> 类型的父类型,故 <? extends>
实现了泛型的协变。
<? super>
实现了泛型的逆变
。比方:
ArrayList<? super Apple> l1 = new ArrayList<>();
ArrayList<?
super Fruit> l2 = new ArrayList<>();
l1 = l2;
对于 ArrayList<? super Fruit> 类型
,我们知道其表示某种详细类型(仅仅是没有确定下来),可是不管其详细指的是ArrayList<Fruit> 类型
还是ArrayList<Object> 类型
,都是能够赋给ArrayList<?
super Apple> 类型的引用的,反之则不能够。
因此,我们能够觉得ArrayList<? super Apple> 类型
是ArrayList<? super Fruit> 类型
的父类型。故 <? super>
实现了泛型的逆变。
6、PECS 准则 (producer-extends, consumer-super)
我们知道 <?>
表示:我想使用 Java泛型 来编写代码。而不是用原生类型;可是在当前这样的情况下,我并不能确定下泛型參数的详细类型,因此用?
表示不论什么某种类型
。
因此,依据我们对通配符的了解,使用无界通配符的泛型类不能够写数据,而在读取数据时。所赋值的引用也仅仅能是 Object 类型。那么,我们到底怎样向泛型类写入、读取数据呢?
《Effective Java2》给出了答案: PECS : producer(读取)-extends, consumer(写入)-super。换句话说,假设输入參数表示一个 T 的生产者。就使用<?
extends T>。假设输入參数表示一个 T 的消费者。就使用<? super T>。总之。通配符类型能够保证方法能够接受它们应该接受的參数,并拒绝那些应该拒绝的參数。 比方,一个简单的 Stack API :
public class Stack<E>{
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
如今要实现 pushAll(Iterable<E> src)
方法,将实现 Iterable 接口的 src 的元素逐一入栈:
public void pushAll(Iterable<E> src){
for(E e : src)
push(e)
}
那么问题就来了:假设有一个实例化 Stack<Number>
的对象 stack(类型參数被实例化为Number)。显然。 我们向这个 stack 中加入 Integer型
或Float型
元素都是能够的,由于这些元素本来就是Number型
的。因此。 src 就包括但不限于 Iterable<Integer>
与 Iterable<Float>
两种可能;这时,在调用上述 pushAll方法
时。编译器就会产生 type mismatch
错误。
原因是显而易见的。由于Java中泛型是不变的,Iterable<Integer>
与 Iterable<Float>
都不是 Iterable<Number>
及其子类型中的一种。所以,我们对 pushAll方法
的设计就存在逻辑上的问题。因此,应改为
// Wildcard type for parameter that serves as an E producer
public void pushAll(Iterable<?
extends E> src) {
for (E e : src)
push(e);
}
这样,我们就能够实现将 实现Iterable接口
的 E类型的容器
中的元素读取到我们的 Stack 中。
那么,假设如今要实现 popAll(Collection<E> dst)方法
,将 Stack 中的元素依次取出并加入到 dst 中。假设不用通配符实现:
// popAll method without wildcard type - deficient!
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
同样地,假设有一个实例化 Stack<Number>
的对象 stack , dst 为 Collection<Object>
,显然。这是合理的。
但假设我们调用上述的 popAll(Collection<E> dst)方法
,编译器会报出 type mismatch
错误,编译器不同意我们进行这样的操作。
原因是显而易见的。由于 Collection<Object>
不是 Collection<Number>
及其子类型的一种。所以,我们对 popAll方法
的设计就存在逻辑上的问题。
因此。应改为
// Wildcard type for parameter that serves as an E consumer
public void popAll(Collection<?
super E> dst) {
while (!isEmpty())
dst.add(pop());
}
这样,我们就能够实现将 Stack<E>
中的 元素读取到我们的 Collection 中 。
在上述样例中,在调用 pushAll方法时 src生产了 E实例(produces E instances),在调用 popAll方法时 dst消费了 E实例(consumes E instances)
。Naftalin与Wadler 将 PECS 称为 Get and Put Principle。
此外,我们再来学习一个样例: java.util.Collections 的 copy 方法(JDK1.7)。它的目的是将全部元素从一个列表(src)拷贝到还有一个列表(dest)中。
显然,在这里,src 是生产者,它负责产生 T类型的实例;dest 是消费者,它负责消费 T类型的实例。
这完美地诠释了 PECS :
// List<?
extends T> 类型的 src 囊括了全部 T类型及其子类型 的列表
// List<? super T> 类型的 dest 囊括了全部能够将 src中的元素加入进去的 List种类
public static <T> void copy(List<? super T> dest, List<?
extends T> src) {
// 将 src 拷贝到 dest 中
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
故有PECS总结:
输入參数是生产者时。用 ? extends T ;
输入參数是消费者时,用 ?
super T ;
输入參数既是生产者又是消费者时,那么通配符类型没什么用了:由于你须要的是严格类型匹配,这是不用不论什么通配符而得到的。
无界通配符<?>
既不能做生产者(读出来的是Object),又不能做消费者(写不进去)。
四. 编译器怎样处理泛型?
通常情况下。一个编译器处理泛型有两种方式:
1、Code Specialization
在实例化一个泛型类或泛型方法时都产生一份新的目标代码(字节码or二进制代码)。比如,针对一个泛型list。可能须要针对string。integer。float产生三份目标代码。
2、Code Sharing
对每一个泛型类仅仅生成唯一的一份目标代码;该泛型类的全部实例都映射到这份目标代码上。在须要的时候执行类型检查和类型转换。
C++中的模板(template)是典型的Code specialization实现
C++编译器会为每一个泛型类实例生成一份执行代码。执行代码中integer list和string list是两种不同的类型。
这样会导致代码膨胀(code bloat)。只是有经验的C++程序猿能够有技巧的避免代码膨胀。 另外。在引用类型系统中,这样的方式会造成空间的浪费。由于引用类型集合中元素本质上都是一个指针。不是必需为每一个类型都产生一份执行代码。而这也是Java编译器中採用Code sharing方式处理泛型的主要原因。
Java 是典型的Code sharing实现
Java编译器通过Code sharing方式为每一个泛型类型创建唯一的字节码表示。而且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的。
五. 类型擦除
1、要点
类型擦除: 通过移除泛型类定义的类型參数并将定义中每一个类型变量替换成相应类型參数的非泛型上界(第一个边界)。得到原生类型(raw type)
类型擦除是 Java 泛型实现的一种折中。以便在不破坏现有类库的情况下。将泛型融入Java,而且保证兼容性。
(泛型出现前后的Java类库互相兼容)
类型擦除指的是通过类型參数合并,将泛型类型实例关联到同一份字节码(Class 对象)上。
编译器仅仅为泛型类型生成一份字节码。并将事实上例关联到这份字节码上。类型擦除的
关键
在于从泛型类型中清除类型參数的相关信息,而且在必要的时候加入类型检查和类型转换的方法。擦除是在
编译期
完毕的。类型擦除能够简单的理解为将泛型java代码转换为普通java代码,仅仅只是编译器更直接点,将泛型java代码直接转换成普通java字节码。泛型类型仅仅有在
静态类型检查期间
才会出现。在此之后,程序中的全部泛型类型都将被擦除,并替换为它们的非泛型上界
。因此,在泛型代码内部。无法获得不论什么有关泛型參数类型的信息。
2、编译器是怎样配合类型擦除的?
3、类型擦除的主要过程
对于Pair<>
//代码演示样例 A
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
Pair<>的原始类型为:
//代码演示样例 B
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
以下类型擦除演示样例:
//代码演示样例 1
interface Comparable <A> {
public int compareTo( A that);
}
//代码演示样例 2
final class NumericValue implements Comparable <NumericValue> {
priva byte value;
public NumericValue (byte value) { this.value = value; }
public byte getValue() { return value; }
public int compareTo( NumericValue that) { return this.value - that.value; }
}
//代码演示样例 3
class Collections {
public static <A extends Comparable<A>> A max(Collection <A> xs) {
Iterator<A> xi = xs.iterator();
A w = xi.next();
while(xi.hasNext()) {
A x = xi.next();
if(w.compareTo(x) < 0)
w = x;
}
return w;
}
}
//代码演示样例 4
final class Test {
public static void main (String[] args) {
LinkedList<NumericValue> numberList = new LinkedList<NumericValue> ();
numberList.add(new NumericValue((byte)0));
numberList.add(new NumericValue((byte)1));
NumericValue y = Collections.max( numberList );
}
}
类型擦除后:
//代码演示样例 1
interface Comparable {
public int compareTo( Object that);
}
//代码演示样例 2
final class NumericValue implements java.lang.Comparable{
//域
private byte value;
//构造器
public NumericValue(byte);
//方法
public int compareTo(NumericValue);
public volatile int compareTo(java.lang.Object); //桥方法
public byte getValue( );
}
//代码演示样例 3
class Collections {
public static Comparable max(Collection xs) {
Iterator xi = xs.iterator();
Comparable w = (Comparable) xi.next();
while (xi.hasNext()) {
Comparable x = (Comparable) xi.next();
if (w.compareTo(x) < 0) w = x;
}
return w;
}
}
//代码演示样例 4
final class Test {
public static void main (String[ ] args) {
LinkedList numberList = new LinkedList();
numberList.add(new NumericValue((byte)0)); ,
numberList.add(new NumericValue((byte)1));
NumericValue y = (NumericValue) Collections.max( numberList );
}
}
第一个泛型类被擦除后, A被替换为最左边界 Object。
由于Comparable是一个泛型接口,所以Comparable的类型參数NumericValue被擦除掉并将相关參数置换为 Object,可是这直接导致 NumericValue 没有实现接口(重写)Comparable的compareTo(Object that)方法,于是编译器充当好人。加入了一个桥方法(由编译器在编译时自己主动加入)
。
第二个演示样例中限定了类型參数的边界。A必须为Comparable的子类,依照类型擦除的过程,先将全部的类型參数替换为最左边界Comparable。得到终于的擦除后结果。
六. 泛型带来的问题及解决方法
1、以參数化类型与原始类型的兼容性说明引用是类型检查所针对的对象
public class Test10 {
public static void main(String[] args) {
ArrayList<String> arrayList1=new ArrayList();
arrayList1.add("1"); //编译通过
arrayList1.add(1); //编译错误
String str1=arrayList1.get(0); //返回类型就是 String
ArrayList arrayList2=new ArrayList<String>();
arrayList2.add("1"); //编译通过
arrayList2.add(1); //编译通过
Object object=arrayList2.get(0); //返回类型就是 Object
new ArrayList<String>().add("11"); //编译通过
new ArrayList<String>().add(22); //编译错误
String string=new ArrayList<String>().get(0); //返回类型就是 String
}
}
因此我们能够得出结论:类型检查就是针对引用的。谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检測,而无关它真正引用的对象。
2、全部动作都发生在边界处(对传递进来的值,编译器进行额外的检查。对真正传递出去的值,编译器自己主动插入的转型)
由于类型擦除的问题,所以全部的泛型类型最后都会被替换为原始类型。
这样就引起了一个问题,既然都被替换为原始类型,那么为什么我们在获取的时候。不须要进行强制类型转换呢?
先看以下非泛型演示样例:
// 代码片段1
public class SimpleHolder {
private Object obj;
public void setObj(Object obj) {
this.obj = obj;
}
public Object getObj() {
return obj;
}
public static void main(String[] args) {
SimpleHolder holder = new SimpleHolder();
holder.setObj("Item");
String s = (String)holder.getObj();
}
}
反编译这个类。得到以下代码片段:
public void setObj(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: putfield #2; //Field obj:Ljava/lang/Object;
5: return
public java.lang.Object getObj();
Code:
0: aload_0
1: getfield #2; //Field obj:Ljava/lang/Object;
4: areturn
public static void main(java.lang.String[]);
Code:
0: new #3; //class SimpleHolder
3: dup
4: invokespecial #4; //Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5; //String Item
11: invokevirtual #6; //Method setObj:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #7; //Method getObj:()Ljava/lang/Object;
18: checkcast #8; //class java/lang/String
21: astore_2
22: return
现将泛型应用到上述代码,例如以下:
// 代码片段 2
public class GenericHolder<T> {
private T obj;
public void setObj(T obj) {
this.obj = obj;
}
public T getObj() {
return obj;
}
public static void main(String[] args) {
GenericHolder<String> holder = new GenericHolder<String>();
holder.setObj("Item");
String s = holder.getObj();
}
}
反编译这个类。得到以下代码片段:
public void setObj(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: putfield #2; //Field obj:Ljava/lang/Object;
5: return
public java.lang.Object getObj();
Code:
0: aload_0
1: getfield #2; //Field obj:Ljava/lang/Object;
4: areturn
public static void main(java.lang.String[]);
Code:
0: new #3; //class GenericHolder
3: dup
4: invokespecial #4; //Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5; //String Item
11: invokevirtual #6; //Method setObj:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #7; //Method getObj:()Ljava/lang/Object;
18: checkcast #8; //class java/lang/String
21: astore_2
22: return
在上述应用泛型的代码中,将
String s = holder.getObj();
替换为
holder.getObj();
反编译后,有代码片段:
public void setObj(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: putfield #2; //Field obj:Ljava/lang/Object;
5: return
public java.lang.Object getObj();
Code:
0: aload_0
1: getfield #2; //Field obj:Ljava/lang/Object;
4: areturn
public static void main(java.lang.String[]);
Code:
0: new #3; //class GenericHolder
3: dup
4: invokespecial #4; //Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5; //String Item
11: invokevirtual #6; //Method setObj:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #7; //Method getObj:()Ljava/lang/Object;
18: pop
19: return
}
首先,代码片段 1 和代码片段 2 二者所产生的字节码是同样的。看第15,它调用的是getObj()方法,返回值是Object,说明类型擦除了。
然后第18。它做了一个checkcast操作,即检查类型#8。 在上面找#8引用的类型,它是一个String类型,即作String类型的强转。所以不是在get方法里强转的,是在你调用的地方强转的。对进入setObj()的类型进行检查是不须要的,由于这将由编译器执行。
而对从getObj()返回的值进行转型仍旧是须要的,但这与你自己必须执行的操作是一样的–此处它将由编译器自己主动插入。也就是说,在泛型中。
全部动作都发生在边界处
– 对传递进来的值进行额外的编译器检查。并由编译器自己主动插入对传递出去的值的转型。其次,在未将 getObj() 的值赋给String时,由代码片段可知,编译器并未自己主动插入转型代码。可见所谓
编译器自己主动插入对传递出去的值的转型
的前提条件
是:其必须是真正传递出去,即必须赋值给引用.(当然,尽管 getObj() 的返回值的类型是 Object, 可是事实上质上是一个 String, 因此直接进行操作 “ getObj() instanceof String ”时,返回值也是 true.)
再看一段代码:
public class GenericArray<T> {
private T[] array;
public GenericArray(int sz) {
array = (T[]) new Object[sz];
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
public T[] rep() { return array; }
public static void main(String[] args) {
GenericArray<Integer> gai = new GenericArray<Integer>(10);
gai.put(0, new Integer(4));
gai.get(0);
Integer i = gai.get(0);
// This causes a ClassCastException:
Integer[] ia = gai.rep();
// This is OK:
Object[] oa = (Object[])gai.rep();
}
}
反编译得代码段:
public class GenericArray extends java.lang.Object{
public GenericArray(int);
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: aload_0
5: iload_1
6: anewarray #2; //class java/lang/Object
9: checkcast #3; //class "[Ljava/lang/Object;"
12: putfield #4; //Field array:[Ljava/lang/Object;
15: return
public void put(int, java.lang.Object);
Code:
0: aload_0
1: getfield #4; //Field array:[Ljava/lang/Object;
4: iload_1
5: aload_2
6: aastore
7: return
public java.lang.Object get(int);
Code:
0: aload_0
1: getfield #4; //Field array:[Ljava/lang/Object;
4: iload_1
5: aaload
6: areturn
public java.lang.Object[] rep();
Code:
0: aload_0
1: getfield #4; //Field array:[Ljava/lang/Object;
4: areturn
public static void main(java.lang.String[]);
Code:
0: new #5; //class GenericArray
3: dup
4: bipush 10
6: invokespecial #6; //Method "<init>":(I)V
9: astore_1
10: aload_1
11: iconst_0
12: new #7; //class java/lang/Integer
15: dup
16: iconst_4
17: invokespecial #8; //Method java/lang/Integer."<init>":(I)V
20: invokevirtual #9; //Method put:(ILjava/lang/Object;)V
23: aload_1
24: iconst_0
25: invokevirtual #10; //Method get:(I)Ljava/lang/Object;
28: pop
29: aload_1
30: iconst_0
31: invokevirtual #10; //Method get:(I)Ljava/lang/Object;
34: checkcast #7; //class java/lang/Integer
37: astore_2
38: aload_1
39: invokevirtual #11; //Method rep:()[Ljava/lang/Object;
42: checkcast #12; //class "[Ljava/lang/Integer;"
45: astore_3
46: aload_1
47: invokevirtual #11; //Method rep:()[Ljava/lang/Object;
50: checkcast #3; //class "[Ljava/lang/Object;"
53: astore 4
55: return
}
结合上面的结论,细致观察反编译后代码中 checkcast
都用在什么地方。加深对边界就是发生动作的地方
和自己主动转型发生在调用处(须要检验两种类型时)
的理解。
25显示调用后,直接pop,而31显示在调用处,还要进行 checkcast 操作;
由于类型擦除,操作39之后。进行 checkcast 操作。强转为 Ljava.lang.Integer ,可是由代码【 array = (T[]) new Object[sz]; 】可知,其 new 的是 Object 数组,是不可能成功强转到 Integer 数组的,就像 Object 对象不能成功强转到 Integer 对象一样。会在执行时抛出 ClassCastException 异常;
由于类型擦除,操作47之后,进行 checkcast 操作,由于 rep() 返回的即为 Object 数组。而其要赋给的引用也是 Object[] ,因此不会抛出不论什么异常。
3、类型擦除与多态的冲突及其解决的方法
先看两段代码:
// 第一段代码
public class Pair<T> {
private T first;
private T second;
public Pair(T first, T second){
this.first = first;
this.second = second;
}
public void setFirst(T first){
this.first = first;
}
public T getFirst(){
return first;
}
public void setSecond(T second){
this.second = second;
}
public T getSecond(){
return second;
}
}
// 第二段代码
public class DateInterval extends Pair<Date> { // 时间间隔类
public DateInterval(Date first, Date second){
super(first, second);
}
@Override
public void setSecond(Date second) {
super.setSecond(second);
}
@Override
public Date getSecond(){
return super.getSecond();
}
public static void main(String[] args) {
DateInterval interval = new DateInterval(new Date(), new Date());
Pair<Date> pair = interval; //超类,多态
Date date = new Date(2000, 1, 1);
System.out.println("原来的日期:"+pair.getSecond());
System.out.println("set进新日期:"+date);
pair.setSecond(date);
System.out.println("执行pair.setSecond(date)后的日期:"+pair.getSecond());
}
}
原本子类重写父类的方法。无可非议。
可是泛型类的类型擦除造成了一个问题,Pair的原始类型
中存在方法:
public void setSecond(Object second);
DateInterval中的方法:
public void setSecond(Date second);
我们的本意是想重写父类Pair中的setSecond方法。可是从方法签名上看。这全然是两个不同的方法,类型擦除与多态产生了冲突
。而实际情况呢?执行DateInterval的main方法,我们看到
public void setSecond(Date second)的确重写了public void setSecond(Object second)方法。这是怎样做到的呢?
使用Java类分析器对其进行分析,结果:
public class DateInterval extends Pair{
//构造器
public DateInterval(java.util.Date, java.util.Date);
//方法
public void setSecond(java.util.Date);
public volatile void setSecond(java.lang.Object); //方法 1
public java.util.Date getSecond( ); //方法 2
public volatile java.lang.Object getSecond( ); //方法 3。它难道不会和方法 2 冲突?
public static void main(java.lang.String[]);
}
方法1和方法3是我们在源代码中不曾定义的,它肯定是由编译器生成的。这种方法称为 桥方法(bridge method)。真正覆写超类方法的是它。语句pair.setSecond(date)实际上调用的是方法1[public volatile void setSecond(Object)],通过这种方法再去调用public void setSecond(Date)。这个桥方法的实际内容是:
public void setSecond(Object second){
this.setSecond((java.util.Date) second);
}
这样的结果就符合面向对象中多态的特性了。实现了方法的动态绑定。
可是,这样的做法给我们带来了一种错觉。就觉得public void setSecond(Date)覆写了泛型类的public void setSecond(Object)【事实上也不是重写,二者方法參数都不同】,假设我们在DateInterval中添加一个方法:
public void setSecond(Object obj){
System.out.println("覆写超类方法!");
}
编译器会报例如以下错误:Name clash: The method setSecond(Object) of type DateInter has the same erasure as setSecond(T) of type Pair<T> but doesn't override it.
即,同一个方法不能被重写两次。
为了实现多态,我们知道方法3也是由编译器生成的桥方法。方法擦除带来的第二个问题就是:由编译器生成的桥方法 public volatile java.lang.Object getSecond() 方法和 public java.util.Date getSecond() 方法。从方法签名的角度看是两个全然同样的方法,它们怎么能够共存呢? 假设是我们自己编写Java代码,这样的代码是无法通过编译器的检查的。可是虚拟机却是同意这样做的,由于虚拟机通过參数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态同意自己做这个看起来“不合法”的事情。
补充说明:
从JDK1.5開始。在一个方法覆盖还有一个方法时能够指定一个更严格(窄)的返回类型。它的机制也是同样使用的桥方法。
比如。
public class A {
public List getList(){
return null;
}
}
public class ASub extends A{
@Override
public ArrayList getList(){
return null;
}
}
使用Java 类分析器对ASub分析可得:
public class ASub extends A{
//域
//构造器
public ASub( );
//方法
public java.util.ArrayList getList( );
public volatile java.util.List getList( ); //桥方法
}
4、泛型类型变量不能是基本数据类型
类型參数不能是基本类型。也就是说。没有ArrayList<double>
,仅仅有ArrayList<Double>
。
由于当类型擦除后,ArrayList的原始类型变为Object,可是Object类型不能存储double值
,仅仅能引用Double
的值。
解决之道: 使用基本类型的包装器类以及Java SE5的自己主动包装机制。
5、转型和警告
使用带有泛型类型參数的转型或 instanceof 不会有不论什么效果
,比如:
class FixedSizeStack<T> {
private int index = 0;
private Object[] storage;
public FixedSizeStack(int size) {
storage = new Object[size];
}
public void push(T item) {
storage[index++] = item;
}
public T pop() {
//Warnning: Unchecked cast from Object to T
return (T) storage[--index];
}
}
public class GenericCast {
public static final int SIZE = 10;
public static void main(String[] args) {
FixedSizeStack<String> strings = new FixedSizeStack<String>(SIZE);
for (String s : "A B C D E F G H I J".split(" "))
strings.push(s);
for (int i = 0; i < SIZE; i++) {
String s = strings.pop();
System.out.print(s + " ");
}
}
}
由于擦除的原因,T 被擦除到它的第一个边界 Object。因此pop()实际上仅仅是将Object转型为Object。换句话说,pop()方法实际上并没有执行不论什么转型。
6、不论什么在执行时须要知道确切类型信息的操作都将无法工作
instanceof操作
的右操作数不能带有泛型类型參数;
new 操作 :
能够 new 泛型类型(eg: ArrayList,…),但不能 new 泛型參数(T,…);
泛型数组 :
不能够创建带有泛型类型參数的数组(若须要收集參数化类型对象,能够直接使用 ArrayList:ArrayList<Pair<String>>最安全且有效。
);
转型 :
带有泛型类型參数的转型不会有不论什么效果;
比如:
关于由类型擦除引起的 instance of T,new T 和创建数组T 等问题,能够引入类型标签Class<T>
来解决,比如:
class Building {}
class House extends Building {}
public class ClassTypeCapture<T> {
Class<T> kind;
public ClassTypeCapture(Class<T> kind) {
this.kind = kind;
}
public boolean f(Object arg) {
return kind.isInstance(arg);
}
public static void main(String[] args) {
ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class);
System.out.println(ctt1.f(new Building())); // true
System.out.println(ctt1.f(new House())); // true
ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class);
System.out.println(ctt2.f(new Building())); // true
System.out.println(ctt2.f(new House())); // true
}
}
7、实现參数化接口
一个类不能实现同一个泛型接口的两种变体,由于擦除的原因。这两个变体会成为同样的接口,比如:
public Person implements Comparable<Person>{ ... } // OK
class HonorPerson extends Person implements Comparable<HonorPerson>{ ... } // Error
HonorPerson 类不能编译,由于擦除会将Comparable<Person>
和Comparable<HonorPerson>
简化为同样的接口 Comparable。 上面的代码意味着反复实现同样的接口。
可是,以下的代码能够通过编译:
public Person implements Comparable{ ... } // OK
class HonorPerson extends Person implements Comparable{ ... } // OK
这样的区别在于:编译器对泛型的特别处理方式。
8、异常中使用泛型的问题
由于类型擦除的原因。将泛型应用于异常是非常受限的。
catch 语句不能捕获泛型类型的异常。由于在编译期和执行时都必须知道异常的确切类型。
不能抛出也不能捕获泛型类的对象
事实上。
泛型类扩展Throwable都不合法
(Exception是Throwable的子类)。比如:以下的定义将不会通过编译
public class Problem<T> extends Exception{......}
为什么不能扩展Throwable,由于异常都是在执行时捕获和抛出的,而在编译的时候,泛型信息全都会被擦除掉,那么,假设上面的编译可行,那么,再看以下的定义:
try{
}catch(Problem<Integer> e1){
...
}catch(Problem<Number> e2){
...
}
在执行时。类型信息被擦除后,那么两个地方的catch都变为原始类型Object。那么也就是说。这两个地方的catch变的一模一样,就“相当于”以下的这样:
try{
}catch(Problem<Object> e1){
...
}catch(Problem<Object> e2){
...
}
这当然就是不行的, 就好像catch了两个一模一样的普通异常,编译器就不能通过编译一样。
- 不能再catch子句中使用泛型变量
比如:
public static <T extends Throwable> void doWork(Class<T> t){
try{
...
}catch(T e){ //编译错误
...
}
}
由于泛型信息在编译的时候已经变为原始类型,也就是说上面的 T 会变为原始类型Throwable。那么假设能够再catch子句中使用泛型变量。那么。以下的定义呢:
public static <T extends Throwable> void doWork(Class<T> t){
try{
...
}catch(T e){ //编译错误
...
}catch(IndexOutOfBounds e){
}
}
依据异常捕获的原则。一定是子类在前面,父类在后面,那么上面就违背了这个原则。
所以java为了避免这样的情况,禁止在catch子句中使用泛型变量。
- 类型变量能够使用在异常声明中
public static<T extends Throwable> void doWork(T t) throws T{
try{
...
}catch(Throwable realCause){
t.initCause(realCause);
throw t;
}
此时,尽管T也会被擦除为Throwable,但由于用在声明中,因此是合法的。
9、类型擦除后的冲突
当泛型类型被擦除后,创建条件不能产生冲突:
class Pair<T> {
public boolean equals(T value) {
return null;
}
}
考虑Pair<>:
public boolean equals(T value){}
擦除后变为
boolean equals(Object)
这与 Object.equals 方法是冲突的
。当然,补救的办法是又一次命名引发错误的方法。
10、动态类型安全
先看以下代码:
public class CheckedList {
@SuppressWarnings("unchecked")
static void oldStyleMethod(List probablyDogs) { //原生List
probablyDogs.add(new Cat());
}
public static void main(String[] args) {
List<Dog> dogs1 = new ArrayList<Dog>();
oldStyleMethod(dogs1); // Quietly accepts a Cat
List<Dog> dogs2 = Collections.checkedList(
new ArrayList<Dog>(), Dog.class);
try {
oldStyleMethod(dogs2); // Throws an exception
} catch(Exception e) {
System.out.println(e);
}
// Derived types work fine:
List<Pet> pets = Collections.checkedList(
new ArrayList<Pet>(), Pet.class);
pets.add(new Dog());
pets.add(new Cat());
}
} /* Output:
java.lang.ClassCastException: Attempt to insert class typeinfo.pets.Cat
element into collection with element type class typeinfo.pets.Dog
使用 Collections 的静态方法:checkedCollection( ), checkedList( ), checkedMap( ), checkedSet( ),
checkedSortedMap( ) 和 checkedSortedSet( ) 能够在执行时便知道罪魁祸首在哪里,而不必等到将对象从容器中取出时。
引用:
《Java编程思想(第四版)》
《Effective Java2》 泛型章节
java泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题
Java泛型-类型擦除
Java泛型2—泛型的本质
Java中的逆变与协变
Java类分析器
Java 泛型(Generics) 综述的更多相关文章
- Java 泛型(Generics)
Generics, 类似C++中的模版. 允许在定义类和接口的时候使用类型参数(type parameters), 声明的类型参数在使用的时候用具体的类型来替换. 如 ArrayList<Str ...
- java 泛型基础问题汇总
泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数.这种参数类型可以用在类.接口和方法的创建中,分别称为泛型类.泛型接口.泛型方法. Java语言引 ...
- 【Java心得总结三】Java泛型上——初识泛型
一.函数参数与泛型比较 泛型(generics),从字面的意思理解就是泛化的类型,即参数化类型.泛型的作用是什么,这里与函数参数做一个比较: 无参数的函数: public int[] newIntAr ...
- Java深度历险(五)——Java泛型
作者 成富 发布于 2011年3月3日 | 注意:QCon全球软件开发大会(北京)2016年4月21-23日,了解更多详情!17 讨论 分享到:微博微信FacebookTwitter有道云笔记邮件 ...
- Java泛型学习笔记 - (七)浅析泛型中通配符的使用
一.基本概念:在学习Java泛型的过程中, 通配符是较难理解的一部分. 主要有以下三类:1. 无边界的通配符(Unbounded Wildcards), 就是<?>, 比如List< ...
- java 深度探险 java 泛型
Java泛型(generics)是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter).声明的类型参数在使用时用具体的类型来替换.泛型最主要的应用是在JD ...
- Java泛型总结
1. 什么是泛型?泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类.可以把类型参数看作是使用参数化类型时指定的类型的 ...
- Java泛型的好处
java 泛型是java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数.这种参数类型可以用在类.接口和方法的创建中,分别称为泛型类.泛型接口.泛型方法. ...
- java泛型的讲解
java泛型 什么是泛型? 泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类.可以把类型参数看作是使用参数化类型时指 ...
随机推荐
- 批处理/命令行合并js,递归合并子目录js文件
for /r %%i in (*.js) do type "%%i">>xxx-all.js java -jar yuicompressor.jar --type js ...
- 〖Android〗依据资源信息,Mock Android资源
#!/bin/bash - #=============================================================================== # # F ...
- 【PHP+JS】uploadify3.2 和 Ueditor 修改上传文件 大小!!
一.写在最开始: 前提条件:服务器php.ini 已经修改了变量[ upload_max_filesize ],可以设定为8M,一般8M足够用了.(重启) 1.uploadify3.2 修改文件大小: ...
- 解决sublime的中文乱码
1.Sublime text 3 中文文件名显示方框怎么解决 在sublime text 3中,Preference, Settings-User,最后加上一行"dpi_scale" ...
- 基于swagger进行接口文档的编写
0. 前言 近期忙于和各个银行的代收接口联调,根据遇到的问题,对之前编写的接口进行了修改,需求收集和设计接口时想到了方方面面,生产环境下还是会遇到意想不到的问题,好在基本的执行逻辑已确定,因此只是对接 ...
- 【CAS单点登录视频教程】 第06集【完】 -- Cas认证 学习 票据认证FormsAuthentication
目录 ----------------------------------------- [CAS单点登录视频教程] 第06集[完] -- Cas认证 学习 票据认证FormsAuthenticati ...
- 利用 log-pilot + elasticsearch + kibana 搭建 kubernetes 日志解决方案
开发者在面对 kubernetes 分布式集群下的日志需求时,常常会感到头疼,既有容器自身特性的原因,也有现有日志采集工具的桎梏,主要包括: 容器本身特性: 采集目标多:容器本身的特性导致采集目标多, ...
- swift3 生成UUID
swift3 生成UUID //获取UUID func getUUID() -> String { let uuidRef = CFUUIDCreate(nil) let uuidStringR ...
- [转]GAN论文集
really-awesome-gan A list of papers and other resources on General Adversarial (Neural) Networks. Th ...
- 【RS】Amazon.com recommendations: item-to-item collaborative filtering - 亚马逊推荐:基于物品的协同过滤
[论文标题]Amazon.com recommendations: item-to-item collaborative filtering (2003,Published by the IEEE C ...