Bruce说学习泛型的最大障碍是类型擦除,泛型的博文我分成两篇,这篇主要是类型擦除、边界、通配符,使用泛型的约束和局限性以及可能带来的问题放在另外一篇,作者水平有限,如有错误或者不足,欢迎交流讨论。
0 前言
一般的类和方法,要么使用基本类型,要么使用自定义的类,不能编写应用与多种类型的代码,在Java增加泛型之前,泛型程序设计是使用继承实现的,即使用Object类型或者Object数组。采用继承实现会带来安全性和可读性的问题,安全性即每次都要手动进行类型强制转换,可读性即不直观。
采用泛型后,转型由编译器来完成,保证类型的正确性,具有更好的可读性。
1 泛型类与泛型方法
1.1 简单泛型类
定义泛型类,将泛型参数列表放在类名后即可
package generic;
/**
* 简单泛型类
* @author 小锅巴
* @date 2016年4月14日下午6:30:49
* http://blog.csdn.net/xiaoguobaf
*/
public class Pair<T> {
private T first;
private T second;
public Pair(){
first = null;
second = null;
}
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
public void setFirst(T first) {
this.first = first;
}
public void setSecond(T second) {
this.second = second;
}
}
1.2 泛型方法
同样,定义泛型方法将泛型参数列表放在返回值之前即可。使用泛型方法时不必指定参数类型,编译器会自己找出具体类型,这被称为类型参数推断。
package generic;
/**
* 泛型方法
* @author 小锅巴
* @date 2016年4月14日下午6:37:32
* http://blog.csdn.net/xiaoguobaf
*/
public class GenericMethod {
public <T> void f(T x){
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethod gm = new GenericMethod();
gm.f("");
gm.f(1);//使用基本类型将会被自动拆箱装箱机制转换为对应的包装类
gm.f(1.0);
gm.f(1.0f);
gm.f('c');
gm.f(gm);
}
}
/**
输出:
java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
java.lang.Character
generic.GenericMethod
*/
2 类型擦除与边界(限定)
2.1 类型擦除
JVM中所有对象属于普通类,并没有泛型类型对象,那么泛型是如何实现的呢?Java泛型是使用类型擦除来实现的,所谓类型擦除,就是在运行时具体的类型信息被擦除掉了,变成了原生类型(raw type),即去掉类型参数后的泛型类型名,类型参数则被擦除为边界类型(无边界则为Object类型)。
package generic;
import java.util.ArrayList;
/**
* 运行时类型擦除
* @author 小锅巴
* @date 2016年4月14日下午18:56:06
* http://blog.csdn.net/xiaoguobaf
*/
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1);
System.out.println(c2);
System.out.println(c1 == c2);
}
}
/**
输出:
class java.util.ArrayList
class java.util.ArrayList
true
*/
2.2 边界(限定)
重用extends关键字,用来设置泛型参数类型的限制条件。类型参数可以是类、接口或通配符(放在后面),边界中的类最多有一个,还可以有接口,接口可以有多个,如果有多个边界,类必须放在第一位,多个边界类型使用&分隔。有边界后,擦除为第一个类型。
package chapter15;
import java.awt.Color;
/**
* 边界
* @author 小锅巴
* @date 2016年4月12日下午7:36:23
* http://blog.csdn.net/xiaoguobaf
*/
interface HasColor{
java.awt.Color getColor();
}
interface Weight{
int weitht();
}
class Dimension{
public int x, y, z;
}
class Colored<T extends HasColor>{
T item;
Colored(T item){
this.item = item;
}
T getItem() {
return item;
}
//边界允许调用方法
java.awt.Color color(){
return item.getColor();
}
}
class ColoredDimension<T extends Dimension & HasColor>{//使用extends必须先类后接口
T item;
ColoredDimension(T item){
this.item = item;
}
T getItem() {
return item;
}
java.awt.Color color(){
return item.getColor();
}
int getX(){
return item.x;
}
int getY(){
return item.y;
}
int getZ(){
return item.z;
}
}
class Solid<T extends Dimension & HasColor & Weight>{//只能继承自一个类,但可以有多个接口
//使用extends关键字更接近子类的概念,如果非要强调是接口的子类型,那么就要增加一个新的关键字
T item;
Solid(T item){
this.item = item;
}
T getItem() {
return item;
}
java.awt.Color color(){
return item.getColor();
}
int getX(){
return item.x;
}
int getY(){
return item.y;
}
int getZ(){
return item.z;
}
int weight(){
return item.weitht();
}
}
class Bounded extends Dimension implements HasColor, Weight{
@Override
public int weitht() {
// TODO Auto-generated method stub
return 0;
}
@Override
public Color getColor() {
// TODO Auto-generated method stub
return null;
}
}
public class BasicBounds {
public static void main(String[] args) {
Solid<Bounded> solid = new Solid<>(new Bounded());
solid.color();
solid.getY();
solid.weight();
}
}
3 通配符
通配符包括通配符上限、通配符下限、无界通配符。
1、带有子类型限定的通配符,通配符限制为某一类型的子类型(包括该类型),使用extends关键字,一般用于从泛型对象读取(返回确定的类型),即使用子类型限定的通配符的函数有泛型返回值。
2、带有超类型限定的通配符,通配符被限制为某一类型的超类型(包括该类型),使用super关键字,一般用于向泛型对象写入(写入确定的类型),即使用超类型限定的通配符函数不会有泛型返回值。有一点可能会让人很费解,对于带有超类型限定的通配符的方法(注意是方法,不能是类),实际类型可以该类型以及其子类型,后来在java核心技术上看到一个例子,这样做是有意义的。
3、无界通配符,表示某种非原生类型的特定类型,但是还不知道是哪种类型,与原生类型看起来很像,但是不一样,原生类型可以传递给各种变体,无界通配符却不可以,它有确定的类型,只是还不确定是哪一种。
Linus Torvalds说过,Talk is cheap,show me the code。光看文字表述真没啥用,还是得看代码。
例子1:
package generic;
/**
* 通配符
* @author 小锅巴
* @date 2016年4月12日下午8:58:02
* http://blog.csdn.net/xiaoguobaf
*/
class Fruit{}
class Orange extends Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
public class Holder<T> {
private T value;
public Holder(){}
public Holder(T value){
this.value = value;
}
public T get() {
return value;
}
public void set(T value) {
this.value = value;
}
public boolean equals(Object obj){
return value.equals(obj);
}
public static void main(String[] args) {
Holder<Apple> Apple = new Holder<>(new Apple());
Apple d = Apple.get();
Apple.set(d);
// Holder<Fruit> Furit = Apple;//不能转型,编译器没那么聪明,需要通配符
// Holder<Fruit> Furit = new Fruit();//不能转型,不是Holder的泛型
// Holder<Fruit> Furit = new Holder();//由于类型擦除,可以
Holder<? extends Fruit> fruit = Apple;
Fruit p = fruit.get();
// Apple P = fruit.get();//Apple引用不能直接引用fruit,原因很简单,子类型很多,Apple只是其中一种
d = (Apple)fruit.get();//向下转型为Apple
try{
Orange c = (Orange)fruit.get();//通过编译,运行时抛出异常,转型不正确
}catch(Exception e){
System.out.println(e);
}
System.out.println(fruit.equals(d));//虽然Apple不能直接引用fruit,但是实际上fruit还是Apple类型的
// fruit.set(new Apple());//因为set的参数是? extends Fruit,可能是继承自Fruit的任何一种,不能保证安全性
// fruit.set(new Fruit());
}
}
/**
输出:
java.lang.ClassCastException: chapter15.Apple cannot be cast to chapter15.Orange
true
*/
例子2:
package generic;
/**
* 原生类型与无界统配符的区别,
* @author 小锅巴
* @date 2016年4月14日下午11:51:29
* http://blog.csdn.net/xiaoguobaf
*/
public class Wildcards1 {
//使用了原生类型,将放弃编译检查 ,因为泛型是类型擦除实现的,所以实际的holder可以是Holder的各种变体,
static void rawArgs(Holder holder, Object args){
holder.set(args);
//holder是原生类型,holder.value被擦除为Object,故args可以为任何类型,它将被转型为Object
//但是Holder是泛型的,所以传Object给set是不安全的,这里给出了警告,下面同理
holder.set(new Wildcards1());
// T t = holder.get();
//方法不是泛型的,不能这样,就算是泛型的,修改后会提示:Type mismatch: cannot convert from Object to T,原因还是因为原生类型和类型擦除
Object obj = holder.get();
}
//使用无界通配符,表示holder.value是某种具体类型的非原生Holder,但是具体是哪种,还不知道,下面两个Error都是这个原因,holder.value和arg的类型不匹配
static void unboundedArg(Holder<?> holder, Object arg){
// holder.set(arg);//
// holder.set(new Wildcards1());
// T t = holder.get();
Object obj = holder.get();//
}
public static void main(String[] args) {
Holder raw = new Holder<Long>();
raw = new Holder();
Holder<Long> qualified = new Holder<Long>();
Holder<?> unbounded = new Holder<Long>();
Holder<? extends Long> bounded = new Holder<Long>();
Long lng = 1L;
rawArgs(raw, lng);
rawArgs(qualified, lng);
rawArgs(unbounded, lng);
rawArgs(bounded, lng);
unboundedArg(raw, lng);
unboundedArg(qualified, lng);
unboundedArg(unbounded, lng);
unboundedArg(bounded, lng);
}
}
例子3:
package generic;
/**
* 带子类型限定的通配符与带超类型限定的通配符
* @author 小锅巴
* @date 2016年4月15日下午2:28:10
* http://blog.csdn.net/xiaoguobaf
*/
public class Wildcards3 {
static <T> T wildSubtype(Holder<? extends T> holder, T arg){
// holder.set(arg);//丢失向其传递任何对象能力,holder是实际类型的一种子类型,具体哪种就不知道了,所以不能写入,只能读取
// holder.set(new Object());//甚至是Objct
// holder.set(new Holder<T>());
// holder.set(new Holder());
T t = holder.get();
return t;//根据实际具体的类型返回
}
static <T> void wildSupertype(Holder<? super T> holder, T arg){
holder.set(arg);
// T t = holder.get();
//同样,holder是实际类型的一种确切父类型,具体哪种就不知道了,所以读出只能用Object引用,读出的类型与实际的类型不相符,所以不用来读出,用来写入
Object obj = holder.get();
}
static <T> void Super(Holder<? super T> holder){}
public static void main(String[] args) {
Holder raw = new Holder<Long>();
raw = new Holder();
Holder<Long> qualified = new Holder<Long>();
Holder<?> unbounded = new Holder<Long>();
System.out.println(unbounded.getClass().getSimpleName());//运行显示是原生类型Holder,原因是类型擦除
Holder<? extends Long> bounded = new Holder<Long>();
Long lng = 1L;
Long r9 = wildSubtype(raw, lng);//wildSubtype形参是通配符上限,实参希望得到在原生类型中不存在的信息,故给出警告;由于类型擦除,可以传递原生类型
Long r10 = wildSubtype(qualified, lng);
// Long r11 = wildSubtype(unbounded, lng);
//wildSubtype中的Holder参数必须是继承自T的一种,即子类型的一种,就好比Apple类继承自Fruit类,那么只能是Apple类,而不能是Orange,而unbounded不能有这个保证
Long r12 = wildSubtype(bounded, lng);
Holder<? extends Fruit> Apple = new Holder<Apple>();
wildSupertype(raw, lng);//类型不确定,是raw引起的
Super(raw);//警告同上
wildSupertype(qualified, lng);//qualified是具体的类型,无警告
// wildSupertype(unbounded, lng);//unbounded不是确定的类型,错误
// wildSupertype(bounded, lng);//bounded是Long子类型的一种,可能情况很多。这个例子不好,用Fruit更好理解
}
}
然后再来看看令人费解的带超类型通配符的方法:
package generic;
import java.util.*;
/**
* 令人费解的带超类型限定通配符
* @author 小锅巴
* @date 2016年4月12日下午8:30:10
* http://blog.csdn.net/xiaoguobaf
*/
public class GenericAndCovariance {
public static void main(String[] args) {
List<? extends Fruit> flist = new ArrayList<Apple>();//flist是Fruit的一种子类型,不一定就是Apple的
//编译器并不知道这一点,向上转型后将丢失掉向其中写入任何对象的能力,甚至是向flist中添加Apple、Object
// flist.add(new Apple());//Error
// flist.add(new Fruit());//Error
// flist.add(new Object());//Error
flist.add(null);//合法但没什么用
Fruit f1 = flist.get(0);
// Apple f2 = flist.get(0);//Error,flist中的元素是Fruit的一种子类型,但是不一定就是Apple,如果是泛型方法,则可以返回正确的实际类型
List<? extends Fruit> flist_1 = new ArrayList<Fruit>();
List<? extends Fruit> flist_2 = new ArrayList<Orange>();
List<? super Apple> flist_3 = new ArrayList<Fruit>();
// List<? super Apple> flist_4 = new ArrayList<Jonathan>();//Error,不是父类型
/**********************************************************************************************************/
flist_3.add(new Apple());
flist_3.add(new Jonathan());
//不是说好的是父类型(包括自己)么,怎么可以写入子类型?
/**********************************************************************************************************/
System.out.println(flist_3.get(0).getClass().getSimpleName());
Apple apple1 = (Apple)flist_3.get(0);
// Apple apple2 = flist_3.get(0);//Error,因为flist_3里面的内容可能是Apple的多种父类型中的一种,所以只能是Object
Object apple3 = flist_3.get(0);
// flist_3.add(new Object());//Error
// flist_3.add(new Fruit());//Error
flist_3.add(null);
}
}
我的理解,假若类型B继承类型A,若方法是带有超类型的通配符,是包括类型A的,由于B继承自A,类B与类A的关系是is-a关系,B也是类型A,对于针对类型A的方法,类型B也是适用的,所以可以写入类型B。
应用:
package exception;
import java.util.*;
/**
* 带有超类型限定通配符的应用
* @author 小锅巴
* @date 2016年4月15日下午11:41:18
* http://blog.csdn.net/xiaoguobaf
*/
public class SuperBoundary {
public static <T extends Comparable<T>> void g(T[] a){}
public static <T extends Comparable<? super T>> void h(T a){}
public static void main(String[] args) {
GregorianCalendar gregorianCalendar = new GregorianCalendar();
// g(gregorianCalendar);//Error
h(gregorianCalendar);
}
}
以下是Comparable接口和Calendar的源码截取:
public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar> {
public interface Comparable<T> {
public int compareTo(T o);
}
}
@Override
public int compareTo(Calendar anotherCalendar) {
return compareTo(getMillisOf(anotherCalendar));
}
Calendar实现了Comparable接口,而GregorianCalendar继承自Calendar,即GregorianCalendar是实现的是Comparable< Calendar >,而不是Comparable< GregorianCalendar>,所以GregorianCalendar类型不适用g方法,但是通过带有超类型限定的通配符,如h方法中所示,GregorianCalendar适用。对于类似这种情况,传入某类型的子类型就很有用。