一、泛型出现的原因
首先,我们先看一个例子,如下:
public class ListErr { public static void main(String[] args) { //创建一个只想保存字符串的List集合 List strList = new ArrayList(); strList.add("Hello World"); strList.add("Good Morning"); strList.add("你好"); //"不小心"把一个Integer对象"丢进"了集合 strList.add(5); for(int i = 0; i < strList.size(); i++){ //因为List里取出的全部是Object,所以必须强制类型转换 //最后一个元素将出现ClassCastException异常 // java.lang.ClassCastException:java.lang.Integer cannot be cast to java.lang.String String str = (String) strList.get(i); } } }
上面的程序创建了一个List集合,并且只是希望该List对象保存字符串对象,但是我们没有办法添加任何的限制,所以程序中“不小心”丢进了一个Integer对象,这将导致程序发生java.lang.ClassCastException异常。因为编译阶段正常,而运行时会出现“java.lang.ClassCastException”异常,因此导致此类错误编码过程中不易发现。通过上面程序,可以使我们了解到Java集合的两个问题:
a.集合对元素类型没有任何限制,这样可能引发一些问题:例如想创建一个只能保存Dog对象的集合,但程序也可以轻易地将Cat对象“丢”进去,所以可能引发异常;
b.由于把对象“丢进”集合时,集合丢失了对象的状态信息,集合只知道它们存储的是Object类型,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既增加了编程的复杂度,也可能引发ClassCastException;
那么有没有什么方法解决上面的问题呢?这就是Java SE5的重大变化之一:泛型的概念。
二、泛型的含义
一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会
很大。泛型可以很好的解决此问题。
泛型(Generic),即“参数化类型”。一提到参数,最能让我们联想到的就是定义方法时的形参,调用此方法时传递实参。那么参数化类型怎么理解呢?其实就
是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用或调用时传入具体的类型。
对于前面的ListErr类,可以使用泛型改进这个程序:
public class GenericList { public static void main(String[] args) { //创建一个只想保存字符串的List集合 List<String> strList = new ArrayList<String>(); strList.add("Hello World"); strList.add("Good Morning"); strList.add("你好"); //下面代码将引起编译错误 strList.add(5);//01 for(int i = 0; i < strList.size(); i++){ //下面代码无须强制类型转换 String str = strList.get(i);//02 } } }
上面程序将在01处引起编译错误,因为strList集合只能添加String对象,所以不能将Integer对象"丢进"该集合。而且程序在02处不需要进行强制类型转换,因为strList对象可以“记住”它的所有集合元素都是String类型。
JDK1.5的泛型有一个很重要的设计原则:如果一段代码在编译时系统没有产生:"[unchecked] 未经检查的转换"警告,则程序在运行时不会引发
"ClassCastException"异常。
三、深入泛型
所谓泛型:就是允许在定义类、接口时指定类型形参,这个类型形参将在声明变量、创建对象时确定(即传入实际的参数类型、也可称为类型形参)。JDK1.5
之后集合框架中的全部类和接口都增加了泛型支持,因此可以在声明变量创建对象时传入类型形参。
1、定义泛型接口、类
接下类我们首先看一下接口List、ArrayList的部分代码:
public interface List<E> extends Collection<E> { int size(); boolean isEmpty(); boolean contains(Object o); Iterator<E> iterator(); Object[] toArray(); <T> T[] toArray(T[] a); boolean add(E e); boolean remove(Object o); boolean containsAll(Collection<?> c); boolean addAll(Collection<? extends E> c); boolean addAll(int index, Collection<? extends E> c); boolean removeAll(Collection<?> c); }
可以看到,在List接口中采用泛型化定义之后,<E>中的E表示类型形参,可以接收具体的类型实参,并且此接口定义中,凡是出现E的地方均表示相同的接受自外部的类型实参。
自然的ArrayList作为List的实现类,其定义形式为:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; } public ArrayList() { super(); this.elementData = EMPTY_ELEMENTDATA; } public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); size = elementData.length; // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } //省略 }
通过上面的介绍可以发现,我们可以为任何类增加泛型声明(并不是只有集合类才可以使用泛型声明,虽然泛型是集合类的重要使用场所),
自定义泛型类和泛型接口与上述Java源码中的List和ArrayList类似,如下,我们看一个简单的泛型类和方法:
public class Apple<T> { private T data; public Apple(T data){ this.data = data; } public void setData(T data){ this.data = data; } public T getData(){ return this.data; } public static void main(String[] args) { //因为传给T形参的是String类型,所以构造函数只接受String类型 Apple<String> a1 = new Apple<String>("苹果"); System.out.println(a1.getData()); //因为传给T形参的是Double实际类型,所以构造器的参数只能是Double或者double Apple<Double> a2 = new Apple<Double>(1.23); System.out.println(a2.getData()); } }
上面程序定义了一个带泛型声明的Apple<T>类,实际使用Apple<T>类时会为T形参传入实际类型,这样就可以生成如Apple<String>、Apple<Double>等等形式的多个逻辑子类(物理上并不存在)。
注意:当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。例如为Apple<T>类定义构造器,其构造器名依然是Apple,而不是Apple<T>,但调用该构造器时却可以使用Apple<T>的形式,当然应该为T形参传入实际的类型参数。
2、从泛型类派生子类
当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类来派生子类,但值得指出的是,当使用这些接口、父类时不能再包含类型形参。例如下面代码就是错误的:
//定义类继承Apple类,Apple类不能跟类型形参 public class A extends Apple<T>{}
正确的写法应该是:
public class A extends Apple<String>{}
如果从Apple<String>类派生子类,则在Apple类中所有使用T类型形参的地方都将被替换成String类型,即它的子类将会继承到String
getData()和void setData(String data)两个方法,如果子类需要重写父类的方法,必须注意这一点。此处不再赘述。
注意:方法中的形参(这种形参代表变量、常量、表达式等数据),只有当定义方法时才可以使用形参,当调用方法(使用方法)时必须为这些数据形参传入实际的数据;与此类似的是:类、接口中的类型形参,只有在定义类、接口时才可以使用类型形参,当使用类、接口时应为类型形参传入实际的类型。
四、类型通配符
通过上面的结论,我们可以知道List<String>和List<Object>实际上都是List类型,那现在就存在一个疑问了,因为Object是String的父类,那么List<Object>也是List<String>类型的父类吗?现在我们来验证一下:
//定义一个如此的方法 public void test(List<Object> ol){ for(int i = 0; i < ol.size(); i++){ System.out.println(ol.get(i)); } }
表面上看起来,上面方法声明没有问题,这个方法声明确实没有任何问题。问题是调用该方法传入实际参数值时可能不是我们所期望的,如下调用此方法:
//创建一个List<String>对象 List<String> strList = new ArrayList<String>(); //将strList作为参数来调用前面的test方法 test(strList);//01
编译上面程序,将在01处发生如下编译错误:
The method test(List<Object>) in the type Apple<T> is not applicable for the arguments (List<String>)
显然得到List<Object>不是List<String>的父类,那么它们的父类是谁呢?为了表示各种泛型的父类,我们需要使用类型通配符,类型通配符是一个问号(?),将问号作为类型实参传给泛型类,例如List<?>。这个问号(?)被称为通配符,它的元素类型可以匹配任何类型。因此可以把List<?>在逻辑上认为是List<Object>、List<String>等所有List<具体类型实参>的父类(类型通配符一般是使用?代替具体的类型实参,注意是类型实参而不是类型形参)。
虽然使用类型通配符可以适应于任何支持泛型声明的接口和类,但这种带通配符的泛型仅表示它是各种泛型的父类,并不能把元素加入到其中,例如如下代码将会引起编译错误:
List<?> c = new ArrayList<String>(); //下面程序引起编译时错误 c.add(new Object());
因为我们不知道上面程序中c的集合里元素的类型,所以不能向其中添加对象。根据前面的List<E>接口定义的代码可以发现:add方法有类型参数E作为集合的元素类型,所以我们传给add的参数必须是E类的对象或者其子类的对象。但因为在该例中不知道E是什么类型,所以程序无法将任何对象“丢进”该集合。唯一的例外是null,它是所有引用类型的实例。如何解决这个问题呢,下面介绍的类型通配符的上限和下限将解决这个问题。
1、类型通配符上限现在有一个方法是这样的,如下:
public void test(List<?> c){ for(int i = 0; i < c.size(); i++){ System.out.println(c.get(i)); } }
现在我们对类型实参进一步限制:只能是Number类及其子类。此时,需要用到类型通配符上限,将上面的代码修改为如下:
public void test(List<? extends Number> c){ for(int i = 0; i < c.size(); i++){ System.out.println(c.get(i)); } }
同理,如果只能是Integer类本身及其父类呢?此时,需要用到类型通配符下限,示例代码为如下:
public void test(List<? super Integer> c){ for(int i = 0; i < c.size(); i++){ System.out.println(c.get(i)); } }
同样可以使用类型通配符上下限设置类型形参。
五、类型的擦除
在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但是为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定类型参数。如果没有为这个泛型类指定类型参数,则该类型参数被陈祚一个raw type(原始类型),默认是该声明该参数时指定的第一个上限类型。
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,则所有在尖括号之间的类型信息都被扔掉了。比如说一个List<String>类型被转换为List,则该List对集合元素的类型检查变成了类型变量的上限(即Object)。下面程序示范了这种擦除:
public class Apple<T extends Number> { private T data; public Apple(T data){ this.data = data; } public void setData(T data){ this.data = data; } public T getData(){ return this.data; } }
public class TestErasure{ public static void main(String[] args) { Apple<Integer> a = new Apple<Integer>(6); //01 //a的getSize方法返回Integer对象 Integer as = a.getData(); //把a对象赋给Apple变量,会丢失尖括号里的类型信息 Apple b = a; //02 //b只知道sizede类型是Number Number size1 = b.getData(); //下面代码引起编译错误 Integer size2 = b.getData(); //03 } }
从上面程序可知,当把a赋给一个不带泛型信息的b变量时,编译器就会丢失a对象的泛型信息,即使所有尖括号里的信息都被丢失。但因为Apple的类型形参的上限是Number类,所有编译器依然知道b的getSize方法返回Number类型,但具体是Number的哪个子类就不清楚了。这就是类型的擦除。
六、话外篇
实际上,泛型对其所有可能的类型参数,都具有同样的行为,从而可以把相同的类型当成许多不同的类来处理。与此完全一致的是,类的静态变量和方法也在所有的实例间共享,所以在静态方法、静态初始化或者静态变量的声明和初始化中不允许使用类型形参。同样还要注意的是,Java中没有所谓的泛型数组一说。
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型。
针对泛型,最主要的还是需要理解它的思想、目的及其原理。