一、为什么要用泛型?
1、看一段代码
List list = new ArrayList();
list.add(1);
list.add("1");
list.add(false);
int m = list.get(0);//无法编译通过
在不用泛型的情况下,我们可以向list对象中添加任何对象,这个没问题。但是,当我们想获取数据的时候,麻烦就来了,我们不知道每个index对应数据的具体类型,我们只知道它是个Object,这往往不能满足我们的需求。
2、使用泛型的会怎么样
还是上面的代码,稍微做下改动
List<String> list = new ArrayList();
list.add(1);//报错
list.add("1");
list.add(false);//报错
使用了泛型,编译器会帮开发者做类型检查,这就保证了此list中添加的数据类型全部都是String,那么在取数据的时候,jvm会帮我们做类型强转,因为插入是经过检查的,所以获取的时候强转是安全的。
二、泛型类、泛型接口、泛型方法
1、先看泛型类
static class Boy<T>{
T toy;
}
T表示某个类型,但具体是什么类型不知道,所以我们没办法将toy初始化,因为new一个对象出来需要知道确切类型,要想解决这个问题,我们先看下泛型方法
2、泛型方法
static class Boy<T>{
T toy;
public <F extends ToyFactory<T>> T obtainToy(F f){
this.toy = f.createToy();
return this.toy;
}
}
这个泛型方法表示函数obtainToy接受一个F类型参数,这个F继承自泛型接口ToyFactory,返回一个参数化的类型T。我们来看下泛型接口
2、泛型接口
interface ToyFactory<T>{
T createToy();
}
static class Boy<T>{
T toy;
public <F extends ToyFactory<T>> T obtainToy(F f){
this.toy = f.createToy();
return this.toy;
}
}
ToyFactory这个泛型接口很简单,就一个方法createToy,返回值是一个参数化的类型T的对象。
那假设,现在现在这个Boy的一个对象想要一个想要一个玩具船,按照上面的思路,我们应该有个玩具船工厂类继承自玩具工厂类,专门生产玩具船。
看代码
interface ToyFactory<T>{
T createToy();
}
static abstract class Toy{
public abstract void play();
}
static class ToyBoat extends Toy{
public void play(){
Log.d("ToyBoat","ToyBoat surf");
}
}
//玩具船工厂函数
static class ToyBoatFactory implements ToyFactory<ToyBoat>{
public ToyBoat createToy(){
return new ToyBoat();
}
}
static class Boy<T>{
T toy;
public <F extends ToyFactory<T>> T obtainToy(F f){
this.toy = f.createToy();
return this.toy;
}
}
public static void main(String ...args){
Boy<ToyBoat> boatBoy = new Boy<>();
boatBoy.obtainToy(new ToyBoatFactory());
}
那现在还有一个问题,我们给Boy增加一个函数
public void play(){
toy.play();
}
这段代码是无法编译通过的,因为toy.play()涉及到了具体的类型,而toy是某一个类型,是不能确定的,那还有办法能让这段代码编译通过吗?看下面代码,我们改造一下Boy类
static class Boy<T extends Toy>{
T toy;
public <F extends ToyFactory<T>> T obtainToy(F f){
this.toy = f.createToy();
return this.toy;
}
}
public void play(){
toy.play();
}
我们给Boy的泛型结构加了一个上线,表示这个参数化的类型必须继承自Toy类,因此我们能够直接调用父类的函数。
三、泛型带来的问题
1、类型擦除
我们新声明个泛型类
static class Girl<T>{
T toy;
public void setToy(T t){
this.toy = t;
}
}
编译成功后,通过javap -c /path/to/.. 名利来看下生成的class文件
class com.debug.ldsplugin.MainActivity$Girl<T> {
T toy;
com.debug.ldsplugin.MainActivity$Girl();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void setToy(T);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field toy:Ljava/lang/Object;
5: return
}
关键是这一行
2: putfield #2 // Field toy:Ljava/lang/Object;
我们看到,toy的类型实际上是一个Object类型。那问题来了,上面不是说了toy是个未知的类型吗?这涉及到java的泛型机制,java中的泛型是伪泛型,因为我们看到编译后参数化的类型被擦除了,在这个例子中擦除到Object。
稍微改下代码
static class Girl<T extends Toy>{
T toy;
public void setToy(T t){
this.toy = t;
}
}
我们看到泛型被擦除到Toy
2: putfield #2 // Field toy:Lcom/debug/ldsplugin/MainActivity$Toy;
2、类型擦除带来的问题
上面我们已经提到了,涉及到需要明确具体类型的操作无法进行。那我们举一些常见的例子以及给出一些解决方案。
- 如何创建泛型数组
我们尝试通过常规方式创建一个数组
Girl<ToyBoat>[] girls = new Girl<ToyBoat>[1];//编译器报错,不让这么创建
编译器不让创建的原因,我猜测是因为java中泛型是伪泛型,它不像C#或者其他支持真泛型语言中Girl<ToyBoat>是个类型,而创建一个数组在java中是需要明确类型的,所以,这么做无法编译通过。那如果就想创建这样一个数组,应该怎么做呢?如下:
Girl<ToyBoat>[] girls = (Girl<ToyBoat>[]) new Girl[1];
也就是说我们需要创建一个类型擦除的数组,然后再强转
- 使用T[] array
还是先看代码
static class GenericArray<T>{
T[] arr;
public GenericArray(int size){
arr = (T[]) new Object[size];
}
public T get(int index){
return arr[index];
}
public void set(T t,int index){
arr[index] = t;
}
public T[] rep(){
return arr;
}
}
在构造函数里做了类型强转,这里之所以是安全的,是因为类型擦除,相当于(Object[])new Object[size],所以这是安全的,正常get和set也是没问题,问题出在rep()这个函数,这个函数的调用有可能引发崩溃,为什么是说有可能呢?我们还是看代码
GenericArray<String> genericArray = new GenericArray<>(3);
genericArray.set("aa",0);
genericArray.get(0);
genericArray.rep();
这样调用是没问题的,根据类型擦除原理我们知道,rep会返回一个Object[],我们在生成的class源码里也能看到这样一行代码
34: invokevirtual #11 // Method com/debug/ldsplugin/MainActivity$GenericArray.rep:()[Ljava/lang/Object;
我们把上面代码稍微改下
String[] s = genericArray.rep();
这时候程序就会崩溃,这是为什么呢?
我们还是看下class源码
34: invokevirtual #11 // Method com/debug/ldsplugin/MainActivity$GenericArray.rep:()[Ljava/lang/Object;
37: checkcast #12 // class "[Ljava/lang/String;"
40: astore_3
jvm把这段代码
String[] s = genericArray.rep();
拆解成了三行代码,先调用rep,然后转型,问题就出在转型上,因为没办法把Object[]转换为String[],所以异常。
为了加深理解,我们再改下代码
static class GenericArray<T extends String>{
T[] arr;
public GenericArray(int size){
arr = (T[]) new Object[size];
}
public T get(int index){
return arr[index];
}
public void set(T t,int index){
arr[index] = t;
}
public T[] rep(){
return arr;
}
}
这段代码在new GenericArray的时候就会异常,因为构造函数里尝试把Object[]转为Integer[]这是注定要失败的。
那么好的选择应该是下面这样
static class GenericArray<T extends String>{
Object[] arr;
public GenericArray(int size){
arr = new Object[size];
}
public T get(int index){
return (T)arr[index];
}
public void set(T t,int index){
arr[index] = t;
}
public T[] rep(){
return (T[])arr;
}
}
这里面rep方法会依然报错,但是我们不会忘记数组运行中的实际类型是Object,因此会减少很多麻烦。
那有没有更好的选择呢?答案是有,就是使用类型标识
static class GenericArray<T extends String>{
T[] arr;
public GenericArray(Class<T> clz,int size){
arr = (T[])Array.newInstance(clz,size);
}
public T get(int index){
return (T)arr[index];
}
public void set(T t,int index){
arr[index] = t;
}
public T[] rep(){
return arr;
}
}
GenericArray<String> genericArray = new GenericArray<>(String.class,3);
genericArray.set("aa",0);
genericArray.get(0);
String[] s = genericArray.rep();
这样所有代码都能正常工作了
四、泛型通配符
泛型通配符我建议大家来看这篇文章。