泛型概述
由来
泛型是JDK 1.5的一项新特性,在Java语言处于还没有出现泛型的版本时,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。例如在哈希表的存取中,JDK 1.5之前使用HashMap的get()方法,返回值就是一个Object对象,由于Java语言里面所有的类型都继承于java.lang.Object,那Object转型为任何对象成都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个Object到底是个什么类型的对象。在编译期间,编译器无法检查这个Object的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多ClassCastException的风险就会被转嫁到程序运行期之中。
伪泛型
许多人都认为c++模板template和java泛型generic这两个概念是等价的,不过,各种语言是怎么实现该功能,以及为什么这么做,却千差万别.
在C++中,模板本质上就是一套宏指令集,只是换了个名头,编译器会针对每种类型创建一份模板代码的副本。有个证据可以证明这一点:MyClass<Foo>不会与MyClass<Bar>共享静态变量。然而,两个MyClass<Foo>实例则会共享静态变量。但在Java中,MyClass类的静态变量会由所有MyClass实例共享,无论类型参数相与否。
Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原始类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,ArrayList<int>与ArrayList<String>就是同一个类。所以说泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型被称为伪泛型。
泛型使用
泛型分类
- 泛型类
/**
* 泛型类
* @param <T> 此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 在实例化泛型类时,必须指定T的具体类型
*/
class Generic<T> {
/**
* key这个成员变量的类型为T,T的类型由外部指定
*/
private T key;
/**
* @param key 形参key的类型也为T,T的类型由外部指定
*/
public Generic(T key) {
this.key = key;
}
/**
* @return 返回值类型为T,T的类型由外部指定
*/
public T getKey() {
return key;
}
}
- 泛型接口
/**
* 泛型接口
*
* @param <T>
*/
interface Generator<T> {
public T next();
}
/**
* 1. 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
* 即:class FruitGenerator<T> implements Generator<T>{
* 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
*/
class Fruit1Generator<T> implements Generator<T> {
@Override
public T next() {
return null;
}
}
/**
* 2. 传入泛型实参时:
* 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
* 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
* 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
* 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
*/
class Fruit2Generator implements Generator<String> {
private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}
- 泛型方法
class StaticGenerator<E> {
/**
* 泛型方法
* 需要添加额外的泛型声明
*/
public <T> void push(T data) {
}
/**
* 泛型方法 静态方法使用泛型必须为泛型方法,如果定义
* 如:public static void show(E t){..},此时编译器会提示错误信息:
* "'StaticGenerator.this' cannot be referenced from a static context"
*/
public static <T> void show(T t) {
}
/**
* 泛型类中的包含泛型的方法,不是泛型方法
*/
public E get() {
return null;
}
}
泛型通配符
看到Generic<Integer>不能被看作为Generic<Number>的子类,故无法使用多态,但可以使用如下形式接收不同泛型类型的Generic对象
public void showKeyValue1(Generic<?> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参 。再直白点的意思就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。
泛型限定
-
? extends SomeClass
这种限定,说明的是只能接收SomeClass及其子类类型,所谓的“上限” -
? super SomeClass
这种限定,说明只能接收SomeClass及其父类类型,所谓的“下限”
限定有以下规则
- 不管该限定是类还是接口,统一都使用关键字extends
- 可以使用&符号给出多个限定
- 如果限定既有接口也有类,那么类必须只有一个,并且放在首位置
原理
类型擦除
在JAVA的虚拟机中并不存在泛型,泛型只是为了完善java体系,增加程序员编程的便捷性以及安全性而创建的一种机制,在JAVA虚拟机中对应泛型的都是确定的类型,在编写泛型代码后,java虚拟中会把这些泛型参数类型都擦除,用相应的确定类型来代替,代替的这一动作叫做类型擦除,而用于替代的类型称为原始类型,在类型擦除过程中,一般使用第一个限定的类型来替换,若无限定则使用Object
class Test<? extends Comparable>
{
private T t;
public void show(T t)
{
}
}
虚拟机进行翻译后的原始类型:
class Test
{
private Comparable t;
public void show(Comparable t)
{
}
}
用反射来看泛型的机制(甚至可以破坏)
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
try {
list.getClass().getMethod("add", Object.class).invoke(list, "hello");
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
在程序中定义了一个ArrayList泛型类型实例化为Integer的对象,如果直接调用add方法,那么只能存储整形的数据。不过当我们利用反射调用add方法的时候,却可以存储字符串。这说明了Integer泛型实例在编译之后被擦除了,只保留了原始类型。
原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。
类型擦除引起的问题及解决办法
先检查、再编译
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add("hello"); // 编译报错
}
因为类型擦除是在编译期完成的,在运行的时候就会忽略泛型,为了保证在运行的时候不出现类型错误,就需要在编译时检查是否满足泛型要求(类型检查)。
类型检查的依据
public static void main(String[] args) {
// 1. 方式1
ArrayList<Integer> list1 = new ArrayList<>();
// 2. 方式2
ArrayList list2 = new ArrayList<Integer>();
list1.add(1);
list1.add("hello"); // 该句编译报错
list2.add(1);
list2.add("hello"); // 该句编译正常
}
注释1和2都没有编译错误:第一种情况,在使用list1时与完全使用泛型参数一样的效果,因为new ArrayList()只是在内存中新开辟一个存储空间,它并不能判断类型,而真正涉及类型检查的是它的引用,所以在调用list1的时候会进行类型检查。同理,第二种情况,就不会进行类型检查。
泛型参数化类型没有继承关系
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
push(list1); // 编译报错
}
public static void push(ArrayList<Object> list) {
}
可以通过泛型通配符解决
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
push(list1);
}
public static void push(ArrayList<?> list) {
}
类型擦除与多态的冲突和解决方法
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
class DateInter extends Pair<Date> {
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}
可以看到,父类和子类的方法中参数类型不同,如果是在普通的继承关系中,这完全不是重写,而是重载;但是如果在泛型中呢?
public static void main(String[] args) {
DateInter dateInter = new DateInter();
dateInter.setValue(new Date());
dateInter.setValue(new Object()); // 编译报错
}
无法接收Object类型参数,可见在泛型中确实是重写了,而不是重载。具体原理如下,编译class后再通过jad工具反编译出代码如下
class Pair
{
public Pair()
{
}
public Object getValue()
{
return value;
}
public void setValue(Object obj)
{
value = obj;
}
private Object value;
}
class DateInter extends Pair
{
DateInter()
{
}
public void setValue(Date date)
{
super.setValue(date);
}
public Date getValue()
{
return (Date)super.getValue();
}
public volatile void setValue(Object obj)
{
setValue((Date)obj);
}
public volatile Object getValue()
{
return getValue();
}
}
由于DateInter继承Pair<Date>,但是Pair在类型擦除后还有一个public volatile void setValue(Object obj)
方法,这和那个public void setValue(Date date)
出现重载,但是程序本意却是不需要public volatile void setValue(Object obj)
的,故通过桥方法调用了public void setValue(Date date)
方法,达到了重写的效果。
泛型数组
看到了很多文章中都会提起泛型数组,经过查看sun的说明文档,在java中是”不能创建一个确切的泛型类型的数组”的。
也就是说下面的这个例子是不可以的:
List<String>[] ls = new ArrayList<String>[10];
而使用通配符创建泛型数组是可以的,如下面这个例子:
List<?>[] ls = new ArrayList<?>[10];
这样也是可以的,但是仍存在ClassCastException问题
List<String>[] ls = new ArrayList[10];
假如泛型数组允许创建,代码如下
// Not really allowed.
List<String>[] lsa = new List<String>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Unsound, but passes run time store check
oa[1] = li;
// Run-time error: ClassCastException.
String s = lsa[1].get(0);
这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。
解决办法:
- 采用通配符方式
下面采用通配符的方式是被允许的,对于通配符的方式,最后取出数据是要做显式的类型转换的
// OK, array of unbounded wildcard type.
List<?>[] lsa = new List<?>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Correct.
oa[1] = li;
// Run time error, but cast is explicit.
String s = (String) lsa[1].get(0);
- 采用List方式
List<List<String>> lists = new ArrayList<>();
错误方式
// 编译不会报错,但存在潜在的运行时ClassCastException
List<String>[] ls = new ArrayList[10];