Java基础06 泛型

  • 什么是泛型?为什么要使用泛型?
    • 泛型的核心是类型参数,这允许调用者使用代码时指定具体类型,使我们可以编写一套代码来对不同类型对象重用
    • 类是对象的模板,泛型类是普通类的模板
    • 不使用泛型的会产生的问题(以ArrayList底层维护Object数组为例)
      • 获取元素时需要向下强转,不安全
      • 插入元素时没有类型检查,会导致获取元素时强转错误
    • 使用泛型好处(就是解决了不使用泛型的缺点)泛型代码被调用时应传入具体类型参数,所以
      • 获取元素时无需强转
      • 插入元素时会进行类型检查
  • 泛型类
    类有若干个类型参数,作用域为整个类;编写代码时就将我们定义的类型参数当做一个真实存在的类型使用即可,可以作为返回值类型、参数类型、字段类型、局部变量类型

    public class GenericTest<T> {
        private T value;
    
        public GenericTest(T value) {
            this.value = value;
        }
    
        public T getValue() {
            return value;
        }
    
        public void setValue(T value) {
            this.value = value;
        }
        
    }
  • 泛型方法
    泛型方法可以再普通类中也可以在泛型类中,定义时类型参数放在修饰符后返回值前

    //获得数组的中间元素
    public static <T> T getMiddle(T...arr) {
        return arr[arr.length >> 1];
    }
  • 类型参数的限定
    我们在编写某些操作时需要传入的类型实现某种功能,希望它继承自某各类、实现某个接口,这时就需要类型参数的限定
    比如我们想要编写一个通用的查找最小值的函数,那么就需要传入的类型实现Comparable接口

    //注意使用的是extends关键字
    public static <T extends Comparable> T getMin(T[] arr) {...}
  • 类型擦除
    虚拟机中没有泛型类型的对象,所有的对象都属于普通类;我们编写的泛型代码在虚拟机中会将类型变量替换为其第一个限定类型(没有限定类型就Object)

    //没有限定类型,替换为Object
    public class GenericTest {
        private Object value;
    
        public GenericTest(Object value) {
            this.value = value;
        }
    
        public Object getValue() {
            return value;
        }
    
        public void setValue(Object value) {
            this.value = value;
        }
    
    }

    这不就没有泛型了吗?不,真正帮我们实现泛型的应该是编译器,我们调用泛型代码传入的类型参数就是让编译器知道应该如何进行类型检查和强转

    • 类型擦除遇上继承与多态
      public class Father<T> {
          private T value;
      
          public T getValue() {
              return value;
          }
      
          public void setValue(T value) {
              this.value = value;
          }
      }
      public class Child extends Father<String> {
          @Override
          public String getValue() {
              return super.getValue();
          }
      
          @Override
          public void setValue(String value) {
              super.setValue(value);
          }
      }

      我们写的似乎合情合理,但是类型擦除后就会出现问题!

      • 先来看一下setValue()方法
        在类型擦除后,父类中的setValue()方法变为

        public void setValue(Object value) {
            this.value = value;
        }

        而我们在子类中所谓的重写是

        public void setValue(String value) {
            super.setValue(value);
        }

        我们没有成功覆盖setValue()方法!当需要多态性的时候调用的仍是父类的setValue()
        那我们该怎么办?其实还是编译器帮我们解决了这个问题!编译器会在子类中生成一个桥方法

        public void setValue(Object value) {
            setValue((String)value);//调用子类setValue()
        }

        桥方法的名字也很形象,我们通过这个方法间接地实现了多态性!

      • 再来看一下getValue()方法
        类型擦除后,父类中的getValue()方法变为

        public Object getValue() {
            return value;
        }

        而我们在子类中所谓的重写是

        public String getValue() {
            return super.getValue();
        }

        我在“重载与重写”中提到过,这种情况(子类重写方法的返回值类型是父类的返回值类型的子类型)是允许的

    • 类型擦除遇上数组—不能实例化参数化类型数组和泛型数组(可以声明)
      • 参数化类型数组(建议先参考“Java基础01”中关于数组的介绍)
        Father<String>[] fathers = new Father<String>[10];//假设可以创建
        Object[] objects = fathers;
        objects[0] = new Father<Integer>();
        //数组会记住所存储的元素类型,本应该在运行时出现ArrayStoreException,但由于类型擦除并不会出现错误,以致于后续获取元素时会强转错误
      • 泛型数组(待补充)
  • 通配符类型:

    • 类型限定通配符
      这种机制模拟了继承的关系,使得GenericTest<Father>和GenericTest<Child>有了类似于父类和子类的关系

      //子类型限定通配符
      GenericTest<? extends Father> generic = new GenericTest<Child>(new Child());
      
      //编译器只知道参数类型应该是Father及其子类,但无法确定具体的类型  所以报错
      generic.setValue(new Father());//报错
      
      //编译器只知道返回类型是Father及其子类,所以最低限度用Father接收
      Father value = generic.getValue();
      GenericTest<? super Child> generic = new GenericTest<Father>(new Father());
      
      //编译器只知道参数类型应该是Child及其父类,但无法确定具体的类型,所以只能传入Child类型
      generic.setValue(new Child());
      
      //编译器只知道返回类型是Child及其父类,所以只能用最高级父类Object接收
      Object value = generic.getValue();//只能用Object接收
  • 类型变量与通配符的对比
    • 类型变量T代表了一种确定的类型,而通配符?代表了一种不确定的类型
    • 其实,我认为能用通配符解决的问题泛型方法也都能够解决
    • 类型变量对象可读可写,通配符对象只读
      //Collections中reverse方法的定义
      public static void reverse(List<?> list){***}
      
      //当然也可以使用泛型方法来定义
      public static <T extends String> void reverse(List<T> list){***}

      这两种方式在功能的实现上并没有区别

      public <T> void func(List<T> list, T t) {
         list.add(t);
      }

      体现了类型参数变量可读可写的性质

      //Collections中的copy方法
      public static <T> void copy(List<? super T> dest, List<? extends T> src){***}
      
      //当然也可以这么写
      public static <T, E extends T> void copy(List<? super T> dest, List<E> src){***}

      我们可以看出

      • src集合不会进行修改,使用通配符也就更加的简洁
      • 通配符有着类型参数没有的特性——子类型限定super

西瓜要挖着吃

一个通往大佬之路的男人

You may also like...

发表评论

电子邮件地址不会被公开。 必填项已用*标注