之前在写公司项目的底层框架的时候用到一些泛型,实践中涉及到一些没关注到的细节,为此专门去Oracle官网把泛型的文档学习了一遍。
Java中的泛型跟C++里面的Template(模板)是同一个类型的东西,都是为了在其他地方调用的时候可以传入各种参数类型。
在实践中,与使用泛型有相似效果的是函数重载,即根据传入参数类型的不同,选择调用不同的函数。泛型和函数重载各有利弊,需要根据使用情景来选择。如果一段代码对于不同类型的参数,可以不做类型区分地使用,比如List的add方法,这时就用泛型。而如果一段代码对于传入的参数,应该根据不同的数据类型,执行不同的语句,这时就应该使用重载。为什么?因为这时如果使用泛型,就会出现大量的instanceof判断,判断之后还有各种影响代码质量的泛型与实际类型之间的类型强转,而如果返回值也是泛型,那就更麻烦了。典型的就是之前对SharedPreferences进行封装,对于不同类型的参数执行统一的get/put方法,但是如果传入String类型,底层就要执行getString/putString方法,如果传入int,就要执行getInt/putInt方法,这样就必须使用如下的函数重载形式:
public static String get(String key, String defaultValue) { SharedPreferences sp = obtainPref(); return sp.getString(key, defaultValue); } public static int get(String key, int defaultValue) { SharedPreferences sp = obtainPref(); return sp.getInt(key, defaultValue); } public static boolean get(String key, boolean defaultValue) { SharedPreferences sp = obtainPref(); return sp.getBoolean(key, defaultValue); }
另外像JDK源码里面,StringBuilder的append方法,也是根据参数类型写了一大堆看似啰嗦的重载函数,为什么?因为方法体不一样啊。
回归正题,如果针对不同的参数类型,可以用同一段代码,还是推荐用泛型的,毕竟可以把几段代码合并成一段代码。
public class MyClass<T> { public static void main(String[] args) { MyClass<Integer> myClass = new MyClass<>(); myClass.printT(100); MyClass<Boolean> myClass2 = new MyClass<>(); myClass2.printT(true); } public void printT(T t) { System.out.print(t); }
}
请注意,这里的泛型"T",代表的只能是Object类型,不能是int,boolean,char这些基本数据类型,比如像下面这样写就是错的:
MyClass<int> myClass = new MyClass<>(); //报错 myClass.printT(1);
也就是说,其实T是继承自Object的。
那么,为什么定义的时候泛型参数必须是Object,而实际传值的时候可以是100,true这些呢?因为JDK在编译时做了一个自动装箱的处理,把int类型包装成了Integer类型,boolean类型则包装成Boolean类型。可以参考我的另一篇blog: Java暗箱操作之自动装箱与拆箱
代码里面每一个用到泛型参数 T, K, E,...都必须遵循先声明再使用的原则,即如果你提到了这些泛型名称,就必须在之前的某个地方被声明过,否则会报错。
泛型的声明位置只能是两个地方,一是类名处,二是方法处,别的地方都不能声明。第一种方式,就是上面的 public class MyClass<T> {..}这种,在类名之后加,这样在类里面所有地方都能用"T"这个泛型参数。第二种方式在方法处声明可能不太常见,之前我也不太熟悉,但项目里确实用到了,只好研究一下,声明格式类似于这样:
public <T> T printT(T t) { System.out.print(t); }
这里的T就只能作用于方法体了,而且会覆盖类上声明的泛型,例如以下代码会正常运行:
public class MyClass<T> { public static void main(String[] args) { MyClass<String> myClass = new MyClass<>(); myClass.printT(100); } public <T> T printT(T t) { System.out.print(t); return t; } }
调用时,类上的泛型是String,方法上传入的是Integer,那就以方法上的为准咯~
特别注意,方法上的泛型参数必须声明在返回值之前,public/private之后,是有固定位置的。
可以对调用时传入的泛型加限制条件,限制T必须是某个类(接口)的子类
public class MyClass<T extends Number> {...}
这里,T就只能是Number或者Number的子类Integer,Float,Long这些,传入String就是错误的。
T也可以继承自多个类,注意这里的类是泛指,包括接口在内,即写成
public class MyClass<T extends A & B & C> {...}
其中A可以是类或接口,B、C只能是接口,即多继承的话至多只能有一个是类,且必须把类写在第一个。
传入的泛型参数还可以是wildcard(通配符)
MyClass<?> myClass = new MyClass<>();
"?"是在调用时传入的东西,取代String, Integer这些实际的类型。
有两种情况会传入"?":1、调用过程中仅涉及到Object的方法,像equals()等等;2、调用过程中不依赖于泛型。最典型的是Class<?>,因为调用的Class方法基本用不到泛型。
更多内容参考Oracle官方文档:Generics