Java 泛型

一、为什么要用泛型?

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();

这样所有代码都能正常工作了

四、泛型通配符

泛型通配符我建议大家来看这篇文章

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容