一:为什么引入泛型机制:
假如我们想要实现一个String数组,并且要求它可以动态改变大小,这时会想到使用ArrayList来聚合String对象。然而,过一阵子,我们想要实现一个大小可以改变的Date对象数组,这时我们希望可以重用之前写过的那个针对String对象的ArrayList实现。
//在JAVA 5之前,ArrayList的实现大致如下:
public class ArrayList{
public Object get(int i){...}
public void add(Object obj){...}
...
private Object[] elementData;
}
从上面代码可以看到,用于向ArrayList中添加元素的add方法接受一个Object型参数,从ArrayList获取指定元素的get方法也返回一个Object类型的对象,Object对象数组elementData存放这个ArrayList中的对象,也就是说无论你向ArrayList中放入什么类型的对象,到了它的内部,都是一个Object对象。
所以基于继承的泛型实现会带来两个问题:
1. GET方法:每次调用get方法都会返回一个Object对象,每次都需要强制类型转换为我们需要的类,麻烦。
2. ADD方法:加入向我们的String对象的ArrayList中加入一个File对象,编译器不会报错,而这不是我们想要的。
所以从JAVA 5开始,ArrayList在使用时可以加上一个类型参数,这个类型参数用来指明ArrayList中的元素类型,类型参数的引入解决了以上的两个问题。代码如下:
ArrayList<String> s = new ArrayList<String>();
s.add("abc");
String s = s.get(0);//无需强制转换
s.add(123);//编译错误,只能向其中添加String对象
编译器获知ArrayList的类型参数String后,便会替我们完成强制类型转换以及类型检查工作
二:泛型类:
先定义一个简单的Box类:
public class Box {
private String object;
public void set (String object){this.object = objec}
public String get(){return object;}
}
这是最常见的做法,这样做的一个坏处是Box里面现在只能装入String类型的元素,今后如果还要装入Integer等其他类型的元素,还必须要另外重写一个Box,代码得不到复用,使用泛型可以解决。
public class Box<T>{
private T t;
public void set(T t){this.t = t;}
public T get(){return t;}
}
这样Box类就可以复用,可以将T替换成任何我们想要的类型
Box<Integer> integerBox = new Box<Integer>();
Box<Double> integerBox = new Box<Double>();
Box<String> integerBox = new Box<String>();
三:泛型方法:
所谓泛型方法,就是带有类型参数的方法,它既可以定义在泛型类中也可以定义在普通类中。
public class ArrayAlg {
public static <T> T getMiddle(T[] a){
return a[a.length/2];
}
}
以上代码中的getMiddle方法既为一个泛型方法,定义的格式是类型变量放在修饰符的后面,返回类型的前面。可以看到,以上泛型方法可以针对各种类型的数组调用,在这些数组的类型已知有限时,也可以用重载实现,不过编码效率很低。调用以上方法如下:
String[] strings = {"aa","bb","cc"};String middle = ArrayAlg.getMiddle(strings);
四:边界符:
现在我们要实现这样一个功能:查找一个泛型数组中大于某个特定元素的个数,可以这样实现:
public static <T> int countGreaterThan(T[] anArray,T elem){
int count = 0;
for(T e : anArray){
if(e > elem)
++count;
}
return count;
}
有一个明显的错误,因为除了short,int,double,long,float.byte,char等原始类型参数,其他的类并不一定能使用操作符”>“,所以编译器报错,解决方法——使用边界符。
Public interface Comparable<T>{
public int compareTo(T o);
}
做一个类似下面这种声明,等于告诉编译器类型参数T代表的都是实现了Comparable接口的类,这样等于告诉编译器它们都至少实现了comparaTo方法。
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray,T elem){
int count = 0;
for(T e : anArray){
if(e > elem)
++count;
}
return count;
}
五:通配符:
在某些情况下,泛型类或者泛型方法想要对自己的类型参数进一步加一些限制。比如,想要限定类型参数只能为某个类的子类或者只能为实现某个接口的类。相关语法——(BoundingType为一个类或者一个接口,可以多于一个,用”&“链接即可)
为了更清楚了解通配符,举个例子还是借用上面的Box类,假设添加一个这样的的方法:
Public void boxTest(Box<Number> n ){...}
那么Box n 允许接受什么类型的参数呢?我们是否能够传入Box或者Box呢?答案是否定的。虽然Integer和Double是Number的子类,但是在泛型中Box或者Box与Box之间没有任何关系!!!
定义几个简单的类,下面将用到:
class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}
下面这个例子中,我们创建了一个泛型类Reader,然后在f1()中当我们尝试Fruit f = fruitReader.readExact(apples);编译器会报错,因为List和List之间没有任何关系。
public class GenericReading {
static List<Apple> apples = Arrays.asList(new Apple());
static List<Fruit> fruit= Arrays.asList(new Fruit());
static class Reader<T> {
T readExact(List<T> list){
return list.get(0);
}
}
static void f1(){
Reader<Fruit> fruitReader = new Reader<Fruit>();
//ERRORS:List<Fruit> cannot be applet to List<Apple>
//Fruit f = fruitReader.readExact(apples);
}
public static void main (...){
f1();
}
}
但是按照通常思维,Apple和Fruit之间肯定是存在联系的,然而编译器却无法识别,那怎么在泛型代码中解决?——通配符!!!
static class CovariantReader<T> {
T readCovariant(List<? extends T> list){
return list.get(0);
}
}
static void f2(){
CovariantReader<Fruit> fruitReader = new CovariantReader<Fruit>();
Fruit f = fruitReader.readCovariant(fruit);
Fruit a = fruitReader.readCovariant(apples);
}
public static void main(String[] args){
f2();
}
这样就相当于告诉编译器,fruitReader的readCovariant方法接受的参数只要满足Fruit的子类就行(包括Fruit自身),这样子类和父类直接的关系也就关联上了。
六:PECS原则:
上面我们看到了类似
public class GenericsAndCovariance {
public static void main(String[] args){
List<? extends Fruit> flist = new ArrayList<Apple>();
//Compile ERRORS:cannot add any type of object:
//flist.add(new Apple());
//flist.add(new Orange());
//flist.add(new Fruit());
//flist.add(null);//Legal but uninteresting
Fruit f = flist.get(0);
}
}
答案否定,JAVA编译器不允许这样做,为什么呢?从编译器的角度去考虑,因为<? extends Fruit> flist
它自身可以有多种含义:
List<? extends Fruit> flist = new ArrayList<Fruit>();
List<? extends Fruit> flist = new ArrayList<Apple>();
List<? extends Fruit> flist = new ArrayList<Orange>();
- 当我们尝试add一个Apple的时候,filst可能指向new ArrayList();
- 当我们尝试add一个Orange的时候,flist可能指向new ArrayList();
- 当我们尝试add一个Fruit的时候,这个Fruit可以是任何类型的Fruit,而flist可能指向某种特定类型的Fruit,编译器无法识别所以会报错。
所以对于实现了
public class GenericWriting {
static List<Apple> apple = new ArrayList<Apple>();
static List<Fruit> fruit = new ArrayList<Fruit>();
static <T> void writeExact(List<T> list,T item){
list.add(item);
}
static void f1(){
writeExact(apples,new Apple());
writeExact(fruit,new Apple());
}
static <T> void writeWithWildcard(List<? super T>,T item){
list.add(item);
}
static void f2(){
writeWithWildcard(apples,new Apple());
writeWithWildcard(fruit,new Apple());
}
public static void main(String[] args) {
f1();
f2();
}
}
这样我们就可以往容器里面添加元素了,但是使用super的坏处是以后不能get容器里面的元素,原因很简单,还是从容器角度去考虑,对于List
List<? super Apple> list = new ArrayList<Apple>();
List<? super Apple> list = new ArrayList<Fruit>();
List<? super Apple> list = new ArrayList<Orange>();
当尝试通过list来get一个Apple时,可能会get到一个Fruit,这个Fruit可以是Orange等其他类型的Fruit。
根据上面例子,可以总结一个规律:“Producer Extends,Consumer Super”
- Producer Extends”—如果你需要一个只读LIst,用它来Producer T,那么使用? extends T。
- “Consumer Super” —如果你需要一个只写List,用它来consumer T,那么使用?super T。
- 如果需要同时读取以及写入,那么就不能使用通配符了。
阅读一些JAVA集合类的源码,可以发现通常将两者结合使用,如下:
public class Collections {
public static <T> void copy(List<? super T> dest,List<? extends T>src) {
for (int i=0;i<src.size();i++)
dest.set(i,src.get(i));
}
}
七:类型擦除:
类型擦除就是说JAVA反省只能用于在编译期间的静态类型检查,然后编译器生成的代码会擦除相应的类型信息,这样到了运行期间实际上JVM根本就不知道泛型所代表的具体类型。
目的:因为JAVA泛型1.5之后才被引入的,为了保持向下的兼容性,所以只能做类型擦除来兼容以前的非泛型代码。