概述
泛型的本质是参数化类型,通常用于输入参数、存储类型不确定的场景。相比于直接使用 Object 的好处是:编译期强类型检查、无需进行显式类型转换。
类型擦除
正确理解泛型概念的首要前提是理解类型擦除(type erasure)。 Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的List
很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:
1、 泛型类并没有自己独有的 Class 类对象。比如并不存在 List
2、 静态变量是被泛型类的所有实例所共享的。对于声明为 MyClass
3、 泛型的类型参数不能用在 Java 异常处理的 catch 语句中。因为异常处理是由 JVM在运行时刻来进行的。由于类型信息被擦除,JVM 是无法区分两个异常类型MyException
当类型信 息被擦 除之后 ,上述类的声明变成了 class MyString implementsComparable。但是这样的话,类 MyString 就会有编译错误,因为没有实现接口Comparable 声明的 int compareTo(Object)方法。这个时候就由编译器来动态生成这个方法。
实例分析
了解了类型擦除机制之后,就会明白编译器承担了全部的类型检查工作。编译器禁止某些泛型的使用方式,正是为了确保类型的安全性。以上面提到的 List
这段代码中,inspect方法接受List
通配符与上下界
在使用泛型类的时候,既可以指定一个具体的类型,如 List
如上所示,试图对一个带通配符的泛型类进行操作的时候,总是会出现编译错误。其原因在于通配符所表示的类型是未知的。因为对于 List>中的元素只能用 Object 来引用,在有些情况下不是很方便。在这些情况下,可以使用上下界来限制未知类型的范围。如 List extends Number>说明 List中可能包含的元素类型是 Number 及其子类。而 List super Number>则说明 List 中包含的是 Number 及其父类。当引入了上界之后,在使用类型的时候就可以使用上界类中定义的方法。比如访问 List extends Number>的时候,就可以使用 Number类的 intValue 等方法。
泛型中 ? 可以用来做通配符,单纯 ? 匹配任意类型。< ? extends T > 表示类型的上界是 T,参数化类型可能是 T 或 T 的子类:
从上面代码中可以看出来,赋值是参数化类型为 Fruit 和其子类的集合都可以成功,通配符类型无法实例化。为啥上面代码中的 add 全部编译失败了呢?因为 fruits 集合并不知道实际类型是 Fruit、Apple 还是 Food,所以无法对其赋值。
除了 extends 还有一个通配符 super,< ? super T > 表示类型的下界是 T,参数化类型可以是 T 或 T 的超类:
看上面代码可知,super 通配符类型同样不能实例化,Fruit 和其超类的集合均可赋值。这里 add 时 Fruit 及其子类均可成功,为啥呢?因为已知 fruits 的参数化类型必定是 Fruit 或其超类 T,那么 Fruit 及其子类肯定可以赋值给 T。
归根到底,还是“子类对象可以赋值给超类引用,而反过来不行”这一规则导致 extends 和 super 通配符在 add 操作上表现如此的不同。同样地,也导致 super 限定的 fruits 中 get 到的元素不能赋值给 Fruit 引用,而 extends 则可以。
总结一下就是:
1、extends 可用于的返回类型限定,不能用于参数类型限定。即作为返回值的时候是可以的,但是作为参数类型限定的时候是不可以的,就像上面的代码写的,add方法全部不可以用,但是super却可以用。
2、super 可用于参数类型限定,不能用于返回类型限定。super和extend正好相反
3、带有 super 超类型限定的通配符可以向泛型对易用写入,带有 extends 子类型限定的通配符可以向泛型对象读取。
类型系统
在Java中,大家比较熟悉的是通过继承机制而产生的类型体系结构。比如String继承自Object。根据 Liskov替换原则 ,子类是可以替换父类的。当需要Object类的引用的时候,如果传入一个String对象是没有任何问题的。但是反过来的话,即用父类的引用替换子类引用 的时候,就需要进行强制类型转换。编译器并不能保证运行时刻这种转换一定是合法的。这种自动的子类替换父类的类型转换机制,对于数组也是适用的。String[]可以替换Object[]。但是泛型的引入,对于这个类型系统产生了一定的影响。正如前面提到的List
1、相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。即List
2、 当泛型类的类型声明中使用了通配符的时候,其子类型可以在两个维度上分别展开。如对 Collection extends Number>来说,其子类型可以在 Collection 这个维度上展开,即 List extends Number>和 Set extends Number>等;也可以在Number 这个层次上展开,即 Collection
3、 如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。理解了上面的规则之后,就可以很容易的修正实例分析中给出的代码了。只需要把List
开发自己的泛型类
泛型类与一般的 Java 类基本相同,只是在类和接口定义上多出来了用<>声明的类型参数。一个类可以有多个类型参数,如 MyClass
泛型的规则
1、泛型的参数类型只能是类(包括自定义类),不能是简单类型。
2、同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
3、泛型的类型参数可以有多个
4、泛型的参数类型可以使用 extends 语句,习惯上称为“有界类型”
5、泛型的参数类型还可以是通配符类型,例如 Class
泛型的使用场景
当类中要操作的引用数据类型不确定的时候,过去使用 Object 来完成扩展,JDK 1.5后推荐使用泛型来完成扩展,同时保证安全性。
最佳实践
在使用泛型的时候可以遵循一些基本的原则,从而避免一些常见的问题。
1、 在代码中避免泛型类和原始类型的混用。比如 List
2、 在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念。由于具体的类型是未知的,很多操作是不允许的。
3、 泛型类最好不要同数组一块使用。你只能创建 new List>[10]这样的数组,无法创建 new List
4、 不要忽视编译器给出的警告信息。
注意事项
1、在泛型类中,可以T t;这样声明,但是不能在前面加上static,因为静态变量在类实例化之前就存在了,会造成编译错误。同理在泛型接口中连T t;这样都不可以定义,因为在接口中,默认是由public static final 修饰的
2、类型没有确定的时候,不能创建实例化对象。如T t=new T();这样是编译错误的,首先是语法上不支持这样写,其次是实例化对象的分配内存的时候,实例化对象的内存多大应该是确定的,而T是不确定的,所以内存的大小也是不确定的。
引用(本文章只供本人学习以及学习的记录,如有侵权,请联系我删除)
Java 深度历险(五)——Java 泛型
深入理解 Java 泛型