泛型学习回顾及初步总结

135 阅读4分钟

什么是泛型

参数化类型

为什么使用泛型

  1. 适用于不同的数据类型执行相同的代码
  2. 能在编译阶段就发现不合适的数据类型,并且适用指定的类型不需要强制转换

怎么使用泛型

  • 简单应用场景
    • 泛型类
    public class Person<T> {
        T t;
    }
    
    • 泛型接口
    public interface Eat<F> {
        void eat(F f);
    }
    
    • 泛型方法
    public class SpecialPerson {
        public <F> void eat(F f) {
            System.out.println("I`m eating" + f.toString());
        }
    }
    
  • 限定类型变量

    通常在上述场景的扩展中出现,以泛型类举栗。

    首先我们新建三个类,分别是Food、Fruit、Apple,不难看出他们的继承关系

    public class Food { }
    
    public class Fruit extends Food { }
    
    public class Apple extends Fruit { }
    

    如何我们想在创建Person类的时候对传入的泛型进行一定限制,就可以这样实现

    public class Person<T extends Fruit> {
        T t;
    }
    

    当我们在创建Person对象的时候发现

    Person<Apple> jack = new Person<>();  //编译通过
    Person<Food> jully = new Person<>();  //编译不通过
    

    即限定了传入的泛型必须继承自Fruit类或者是Fruit类本身

  • 通配符类型

    通常用于变量和形参的定义中

    • 泛型的上界

      我们有这样一个类

      public class SpecialPerson {
          List<Fruit> list;
      
          public SpecialPerson(List<Fruit> list) {
              this.list = list;
          }
      }
      

      我们想在实例化它的时候,让他既可以接收Fruit类型的列表又可以接收Apple类型的列表,该怎么做咧,很简单,我们稍微改造一下

      public class SpecialPerson {
          List<? extends Fruit> list;
      
          public SpecialPerson(List<? extends Fruit> list) {
              this.list = list;
          }
      }
      

      当当,这样就实现了我们的需求

      public static void main(String[] args) {
          List<Food> foods = new ArrayList<>();
          List<Fruit> fruits = new ArrayList<>();
          List<Apple> apples = new ArrayList<>();
          SpecialPerson jack = new SpecialPerson(foods);  //编译不通过
          SpecialPerson pack = new SpecialPerson(fruits); //编译通过
          SpecialPerson jave = new SpecialPerson(apples); //编译通过
      }
      

      接下来有意思的来了,我们在SpecialPerson类中添加一些操作

      public class SpecialPerson {
          List<? extends Fruit> list;
          public SpecialPerson(List<? extends Fruit> list) {
              this.list = list;
              Food food = list.get(0);   //编译通过
              Apple apple = list.get(0); //编译不通过
              list.set(0, food);         //编译不通过
              list.set(0, apple);        //编译不通过
          }
      }
      

      我们可以发现,通过这种方式定义的列表,在get列表元素时,竟然可以自动转成Food类型而不能转成Apple类型,是不是黑人问号脸,哈哈哈,因为从字面意义上来看,我们应该可以很方便的get到Apple的类型而且应该get不到Food类型,应为Apple继承自Fruit,但是结果偏偏相反,点解咧?

      我的理解是这样的,<? extends Fruit>中的extends是指在接收这个参数的时候,需要符合继承自Fruit的规则,接收完毕它的使命就结束了,但是在你拿这个参数来实际使用的就不用想着它了,但是这个参数里面装的都是写什么数据呢,我们可以逆推回去,我们知道里面装的都是Fruit和Fruit的子类,那我们get的时候用来装这些数据的容器肯定得“足够大”吧,不然“溢出来”怎么办,所以我们用一个Food来装数据是不是很安全的呀,是的,应为列表里面“最大”的数据就是Fruit了,一个Fruit肯定也是一个Food嘛;相反如果我们用Apple来装,万一列表里面有个Fruit怎么办,你能保证这个Fruit一定是Apple吗,要是是Orange呢,所以编译通过的地方是合理的。

      (这块推荐还是暂时先死记硬背着,因为太喵喵的绕了,以后用的多了,思考的多了说不定就能理解了,嗯,就是这样)

    • 泛型的下界

      和上界用法很相似,只是将<? extends Fruit>中的extends换成super,即<? super Fruit>,然后功能相反,但需要注意的是,extends中是能get不能set,而在super中是能set不能get

泛型的约束和局限性

  • 不能实例化类型变量

    即无法做到下面这样

    T t = new T(); //编译不通过
    
  • 不能在静态域或者静态方法中使用类型变量

    因为类型变量在对象创建的时候才得以确定,而静态域和方法在类加载的时候就执行了,当然,假如你的静态方法本身就是一个泛型方法就可以,因为它有嚣张的资本,游勤逮塞

  • 泛型擦除

    哇哦~看起来好专业的术语,然鹅其实就是你在编译期间看到的类型变量都是假象,在编译器编译时不管你定义了它是什么类型,它都会被擦掉,变成一个可怜巴巴的Object(当然如果你进行了< T extends XXX >这样的限定,就会被擦成XXX),使用会有

    public static void main(String[] args) {
        Person<Fruit> fruitPerson = new Person<>();
        Person<Apple> applePerson = new Person<>();
        boolean flag = fruitPerson.getClass() == applePerson.getClass();
        System.out.println(flag);  //true
    }
    
  • 类型变量不可以是8种基本数据类型,类型变量都得是对象
    Person<int> person; //编译不通过
    
  • 泛型不支持instanceof语法
    if (person instanceof Person<Fruit>) { } //编译不通过
    
  • 可以声明泛型类数组,但不能创建泛型类数组(请看大屏幕)
    Person<Fruit>[] people;        //编译通过
    people = new Person<Fruit>[4]; //编译不通过
    people = new Person<>[4];      //编译不通过
    people = new Person[4];        //编译通过
    
  • 不能捕获泛型变量类型的对象,变量类型不能派生自Exception/Throwable

    不解释,老师说用的不多,就是这么不求上进哇咔咔

    好了,本文的内容大致上就是这么多,如果有写的不对写的不好的地方,还请各位大佬不吝赐教