前言
对于Java泛型,之前已经写过一篇文章来分析了,但是还有一些关键问题并没有讲清楚,今天就把这些问题说一下。
泛型中 <T>和<?>的区别
首先他们的使用场景不一样
<T>用于定义泛型类或泛型方法
1、<T>声明泛型类的类型参数
这个是<T>使用最常见的地方,我们在JDK使用泛型的容器中随处可见使用<T>声明一个泛型类的类型参数。
我们也可以自定义一个泛型类
public class Pair<T> { public T value; public T value2; public T getValue(){ return value; } public void setValue(T value){ this.value=value; } public <E extends Comparable> int shwo(E a, E b) { int num = a.compareTo(b); return num; } }
为什么这里要用类型参数?因为这是一种”约束“,为了保证Pair里的value,value2是同一个类型T
2、<T>声明泛型方法
我们一定要明白上面我们自定义的泛型类中的getValue()方法并不是一个泛型方法,这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。所以在这个方法中才可以继续使用 T 这个泛型。
下面再稍微回顾下泛型:http://blog.csdn.net/bryantlmm/article/details/78360398
@Test public void test(){ Pair<Integer> pair=new Pair<Integer> (); pair.setValue(3); Integer integer=pair.getValue(); System.out.println(integer); }
在这个test()方法中。我们分析下getValue()方法,擦除getValue()的返回类型后将返回Object类型,编译器自动插入Integer的强制类型转换。也就是说,编译器把这个方法调用翻译为两条字节码指令:
1、对原始方法Pair.getValue的调用
2、将返回的Object类型强制转换为Integer
此外,存取一个泛型域时,也要插入强制类型转换。因此,我们说Java的泛型是在编译器层次进行实现的,被称为“伪泛型”,相对于C++。泛型的实现便是编译阶段已经在代码中自动加入了类型的强制转化,将原始类型强制转化为我们实际代码中T填充的类型。我们可以这么理解,开篇我们说到我们把类型T当做一个参数,这里的T为Integer
,我们可以理解为这个T的形参在这里就是原始类型Object,实参为Integer。Pair<Integer> pair=new Pair<Integer> (); pair.setValue(3); Integer integer=pair.getValue();
其实是等价于
Object object=new Integer(3); pair.setValue(object); Object objint=pair.getValue(); Integer int=(Integer)objint;
我们要注意,Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。
泛型方法
package wangcc.generics;
import org.apache.log4j.Logger;
public class RealGenericMethod {
private static Logger logger = Logger.getLogger(RealGenericMethod.class);
//这个类是个泛型类,在上面已经介绍过
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
//我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
//这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
//所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey(){
return key;
}
/**
* 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
* 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
public E setKey(E key){
this.key = keu
}
*/
}
/**
* 这才是一个真正的泛型方法。
* 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置.
* 泛型的数量也可以为任意多个
* 如:public <T,K> K showKeyName(Generic<T> container){
* ...
* }
*/
public <T> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
//当然这个例子举的不太合适,只是为了说明泛型方法的特性。
T test = container.getKey();
return test;
}
//这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
public void showKeyValue1(Generic<Number> obj){
logger.info(obj.getKey());
// log.info("泛型测试","key value is " , obj.getKey());
}
//这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
//同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
public void showKeyValue2(Generic<?> obj){
logger.info(obj.getKey());
}
/**
* 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
* 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
* 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
public <T> T showKeyName(Generic<E> container){
...
}
*/
/**
* 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
* 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
* 所以这也不是一个正确的泛型方法声明。
public void showkey(T genericObj){
}
*/
public static void main(String[] args) {
}
}
说明:
1)public 与 返回值中间非常重要,可以理解为声明此方法为泛型方法。
2)只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
3)表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
泛型方法和泛型类的区别:泛型类的实现是在类实例化的时候实现的,而泛型方法是在调用该方法的时候实现的。
5)静态泛型方法:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法。另外对于一个static的方法而言,无法访问泛型类型的参数。所以如果static方法要使用泛型能力,就必须使其成为泛型方法。
<?>用于接收或者说使用泛型类或泛型方法
1.首先要明确通配符是肯定不能声明一个泛型类或者泛型方法的,通配符是拿来使用定义好的泛型的。
即作为一个泛型的接受者。比如用<?>声明List容器的变量类型,然后用一个实例对象给它赋值的时候就比较灵活。
List<?> list = new ArrayList<String>();
2、谨慎使用<?>
List<?>这个写法非常坑。因为,这时候通配符会捕获具体的String类型,但编译器不叫它String,而是起个临时的代号,比如”CAP#1“。所以以后再也不能往list里存任何元素,包括String。唯一能存的就是null。
Java 泛型 中<? super T>和<extends T>
<? extends T>和<? super T>是Java泛型中的“通配符(Wildcards)”和“边界(Bounds)”的概念。
- <? extends T>:是指 “上界通配符(Upper Bounds Wildcards)”
- <? super T>:是指 “下界通配符(Lower Bounds Wildcards)”
java是单继承,所有继承的类构成一棵树。
假设A和B都在一颗继承树里(否则super,extend这些词没意义)。
A super B 表示A是B的父类或者祖先,在B的上面。
A extend B 表示A是B的子类或者子孙,在B下面。
由于树这个结构上下是不对称的,所以这两种表达区别很大。假设有两个泛型写在了函数定义里,作为函数形参(形参和实参有区别):
1) 参数写成:T<? super B>,对于这个泛型,?代表容器里的元素类型,由于只规定了元素必须是B的超类,导致元素没有明确统一的“根”(除了Object这个必然的根),所以这个泛型你其实无法使用它,对吧,除了把元素强制转成Object。所以,对把参数写成这样形态的函数,你函数体内,只能对这个泛型做插入操作,而无法读
2) 参数写成: T<? extends B>,由于指定了B为所有元素的“根”,你任何时候都可以安全的用B来使用容器里的元素,但是插入有问题,由于供奉B为祖先的子树有很多,不同子树并不兼容,由于实参可能来自于任何一颗子树,所以你的插入很可能破坏函数实参,所以,对这种写法的形参,禁止做插入操作,只做读取
1、为什么要使用通配符和边界
我们之前在讲解数组的时候提过泛型通过通配符和边界实现了数组协变的功能。也就是说在某些场景下泛型也是需要有协变的功能的。那我们就来看下什么时候需要把。
使用泛型的过程中,经常出现一种很别扭的情况。
现在我们有Fruit类,和它的派生类Apple类。
class Fruit {}
class Apple extends Fruit {}
然后有一个最简单的容器:Plate类。盘子里可以放一个泛型的“东西”。我们可以对这个东西做最简单的“放”和“取”的动作:set( )和get( )方法。
package com.wangcc.MyJavaSE.genericity;
public class Plate<T> {
public T item;
public void set(T item) {
this.item=item;
}
public T get() {
return item;
}
public Plate(T item){
this.item=item;
}
}
现在我定义一个“水果盘子”,逻辑上水果盘子当然可以装苹果。
Plate<Fruit> p=new Plate<Apple>(new Apple());
但是这行代码是通不过编译的,在使用Eclipse时,Eclipse会提示你:Type mismatch: cannot convert from Plate <Apple> to Plate<Fruit>
“装苹果的盘子”无法转换成“装水果的盘子”。这个在我们的逻辑开来似乎不对。
实际上,编译器脑袋里认定的逻辑是这样的:
- 苹果 IS-A 水果
- 装苹果的盘子 NOT-IS-A 装水果的盘子
所以,就算容器里装的东西之间有继承关系,但容器之间是没有继承关系的。所以我们不可以把Plate<Apple>的引用传递给Plate<Fruit>。
为了让泛型用起来更舒服,Sun的大脑袋们就想出了
Plate<? extends Fruit>
这便是上界通配符
翻译成人话就是:一个能放水果以及一切是水果派生类的盘子。再直白点就是:啥水果都能放的盘子。这和我们人类的逻辑就比较接近了。Plate<? extends Fruit>和Plate<Apple>最大的区别就是Plate<? extends Fruit>是Plate<Fruit>以及Plate<Apple>的基类。直接的好处就是,我们可以用“苹果盘子”给“水果盘子”赋值了。
3、下界
Plate<? super Fruit>
这便是下界通配符。
一个能放水果以及一切是水果基类的盘子。Plate<? super Fruit>是Plate<Fruit>的基类,但不是Plate<Apple>的基类。
4、上下界通配符带来的问题
边界让Java不同泛型之间的转化更容易了。但是这样的转换也会带来一些问题。容器的部分功能可能失效。
上界<? extends Fruit>不能往里存,只能往外取。
<? extends Fruit>会使往盘子里放东西的set( )方法失效。但取东西get( )方法还有效。
Plate<? extends Fruit> p=new Plate<Apple>(new Apple()); // p.set(new Apple()); // p.set(new Fruit()); //读取出来的东西只能存放在Fruit或它的基类里。 Fruit f=p.get(); Object f1=p.get();
这里的set方法都是无法通过编译的。
原因是编译器只知道容器内是Fruit或者它的派生类,但具体是什么类型不知道。可能是Fruit?可能是Apple?也可能是Banana,RedApple,GreenApple?编译器在看到后面用Plate<Apple>赋值以后,盘子里没有被标上有“苹果”。而是标上一个占位符:CAP#1,来表示捕获一个Fruit或Fruit的子类,具体是什么类不知道,代号CAP#1。然后无论是想往里插入Apple或者Meat或者Fruit编译器都不知道能不能和这个CAP#1匹配,所以就都不允许。所以通配符<?>和类型参数的区别就在于,对编译器来说所有的T都代表同一种类型。比如下面这个泛型方法里,三个T都指代同一个类型,要么都是String,要么都是Integer。
public <T> List<T> fill(T... t);
但通配符<?>没有这种约束,Plate<?>单纯的就表示:盘子里放了一个东西,是什么我不知道。
下界<? super Fruit>只能往外取,不能往里存
使用下界<? super Fruit>会使从盘子里取东西的get( )方法部分失效,只能存放到Object对象里。set( )方法正常。
Plate<? super Fruit> p1=new Plate<Food>(new Food()); //p1.set(new Food());//Error p1.set(new Fruit()); p1.set(new Apple()); Object newFruit=p1.get();
因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。既然元素是Fruit的基类,那往里存粒度比Fruit小的都可以(即是Fruit的子类都可以,因为父类的引用可以指向子类,也就是说其子类对象也是Fruit对象)。但是我们注意到当我们想存入Food()的时候报错了,编译不通过,这是为什么呢。为了保护类型的一致性,因为“? super Fruit”可以是Food,也可以是Object或其他Fruit的父类,因无法确定其类型,也就不能往Plate<? super Fruit>添加Fruit的任意父类对象。且往外读取元素更费劲了,只有所有类的基类Object对象才能装下。但这样的话,元素的类型信息就全部丢失。
注意:List<?>等同于List<? extends Object>
5、PSCE原则
在《Effective in Java》中,Joshua Bloch提出里PECS原则
Producer Extends Consumer Super
- 频繁往外读取内容的,适合用上界Extends。
- 经常往里插入的,适合用下界Super。
6、如何使用上下界通配符
上面我们分析了上下界通配符的作用以及他的局限性,但是上面大多还是属于纸上谈兵。我们最后还是需要关注下在我们编码时该如何使用上下界通配符。