泛型编程

泛型编程

参考博客

Java 泛型,你了解类型擦除吗?

java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一

java为什么不支持泛型数组?

基本内容

  • 泛型问题:

    • 什么是泛型?
    • 为什么要引入泛型?
    • 泛型的优点在哪里?
  • 推导过程

    • 初始阶段

      1. 我们在学习数据结构的时候通常会自己手动实现栈,队列,哈希表等数据结构
      2. 我们通常为了方便起见,都是将数据类型默认设置成为的 Integer 类型
      3. 那么你是否思考过,如果下次需要将 String 类型的元素存入你写的数据结构要怎么办
      4. 显然,我们最笨的办法就是再写一个存储 String 类型的数据结构类
      // 存储 Integer 元素的类
      class StackOfInteger
      {
      private int[] stack;
      private int maxSize;
      private int top;
      ...
      }

      // 存储 String 元素的类
      class StackOfString
      {
      private String[] stack;
      private int maxSize;
      private int top;
      ...
      }
      ...
      1. 那么现在我们再提出一个要求,需要存储 Double 类型的元素的数据结构
      2. 很无奈,我们只能再次重复一遍数据结构的代码,仅仅只为了改动一个数据类型

      每次都为不同的数据类型重写一个类显然是非常不合理的 -> 代码量会陡然增大

    • 中级阶段

      • 方式:
        1. 相对于每次都重写类的方式,利用 Java 多态的特性显然更加符合常理
        2. 每次都可以任意传入自己想要的类型 -> 所有类都是 Object 子类 -> 所有类型都可以被==向上转型==
        3. 采用 Object 的方式显然避免了代码量过大的问题
      class Stack
      {
      private Object[] stack;
      private int maxSize;
      private int top;

      ...// 省略构造方法...

      public void push(Object val)
      {
      if (isFull())
      {
      System.out.println("栈已满!");
      return;
      }
      stack[++top] = val;
      }

      public Object pop()
      {
      if (isEmpty())
      {
      return null;
      }
      Object val = stack[top];
      top--;
      return val;
      }
      }

      • 问题:
        1. 每次取出元素时都需要主动进行==强制类型转换==
        2. 每个数据结构中都存储同一类型元素(Integer)的时候 -> 将另一类型的元素(String)存储进去也不会报错 -> 强制转换读取元素使才会报错
      class MyTest
      {
      public static void main(String[] args)
      {
      Stack stack = new Stack(...);
      stack.push("String");
      // 显然在这个位置想要使用字符串的特性是需要强制转换的
      String string = (String) stack.pop();
      stack.push(114514);
      Integer integer = (Integer) stack.pop();

      while(stack.isEmpty())
      {
      /*
      1. 显然如果栈中有其他类型的数据被强制转换成 Integer 类型是显然报错的
      2. 我们无法在编译阶段就知道会出现类型转换错误,有时甚至难以得知栈中到底存储的是什么元素
      */
      Integer integer = (Integer) stack.pop();
      }

      }
      }
    • 最终阶段

      1. 我们不需要提前指定任何==具体类型== -> 使用==类型参数==来表示我们并不知道现在需要使用哪种具体类型
      2. 我们只需要在使用数据结构的时候指定具体类型就可以
      3. 采用这种方式就能够避免 Object 方式中的强制转换造成的错误
      class Stack<T>
      {
      private T[] stack;
      private int maxSize;
      private int top;
      ...

      public void push(T val)
      {
      ...
      }

      public T pop()
      {
      ...
      }
      }

      class MyTest
      {
      public static void main(String[] args)
      {
      // 后面菱形中不用再填入具体类型 -> 前面已经填入可以自动推导
      Stack<String> strStack = new Stack<>();
      strStack.push("shinobu");
      strStack.push("fuyusakaiori");
      // 采用 Object 的方式中是不会在编译阶段报错的 -> 采用泛型编程是会在此处直接报错
      strStack.push(1234);
      }
      }
  • 类型参数:

    • 引入:既然具体变量可以作为实际参数传入,那么是否具体的类型是否也可以作为实际参数传入呢? -> 答案显然是可以的

    • 概念:将类型由原来的==具体的类型参数化==,类似于方法中的变量参数

    • 表现形式:使用<>表示类型参数

      • T:表示任何类型
      • E(Element):表示集合中的元素类型
      • K(Key):表示映射中键的类型
      • V(Value):表示映射值的类型
      • S(SubType):

      注:5 种表示方式没有任何实质区别 -> 仅仅只是为了增强代码可读性

  • 总结

    • 最初采用==重写不同类==适应不同类型的需要

    • 改进采用 ==Object 方式(多态)== 适应不同类型的需要

      注:Java 5 之前全部都是采用这种多态的方式实现“泛型”

    • 最后采用==泛型==非常容易就适应不同类型的需要

      • 概念:泛型的本质是为了==参数化类型== -> 不创建新的类型的情况下,通过泛型指定具体类型来控制形参的具体类型
      • 优点:
        • 泛型提供一种==扩展能力==,使得数据类别可以像参数一样由外部传递进来,更符合面向抽象开发的软件编程宗旨
        • 泛型又提供了一种==类型检测==的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过
        • 泛型提高了程序代码的==可读性==,能够一目了然猜测出代码要操作的数据类型
  • 思考:Java 中是否真的实现了泛型机制呢? -> 答案是否定的

    /*
    1. Java 虚拟机中并没有真正的实现泛型 -> 采用了类型擦除机制 -> 之后解释
    2. C# C++ 都是彻底实现了泛型
    */
    public static void main(String[] args)
    {
    List<String> l1 = new ArrayList<String>();
    List<Integer> l2 = new ArrayList<Integer>();
    System.out.println(l1.getClass() == l2.getClass()); // 得到的答案是 true 而不是 false
    }

进阶内容

泛型使用

泛型类

  • 概念:集合框架中对于泛型类的使用非常常见(ArrayList<T>HashMap<K,V>…)

  • 表现形式:使用 类型参数==声明==当前类是泛型类

  • 特点:

    • 泛型类中的构造方法不需要声明泛型

    • 泛型类中的所有方法和属性都可以使用 类型参数 -> 不能够使用未被声明的类型参数,诸如

    • 泛型类并不强制要求所有方法和属性都使用 类型参数

    • 泛型类的具体类型的指定

      1. 可以显示的指定具体的类型
      2. 也可以不指定具体的类型 -> 这样就可以存放任何类型的元素,不会编译错误 -> 实际上相当于 Object 的方式

      注:传入的参数只能够是类型,不能够是基本数据类型(int,double,float)

  • 总结:==泛型类就相当于普通类的工厂== -> 想要什么样的类就可以生产什么样的类

/* 
不可以使用通配符声明泛型类
class Generic<?>
{
这是错误的
}
*/
class Generic<T>
{
private T key;

// 不能够写成 public Generic<T>() 这种形式
public Generic ()
{
System.out.println("泛型类启动...");
}

public Generic(T key)
{
this.key = key;
System.out.println("泛型类启动..." + this.key);
}

public T getKey()
{
return key;
}

}
// 测试
class MyTest
{
public static void main(String[] args)
{
// 1. 可以显示的指定具体的类型
Generic<String> generic = new Generic<>("shinobu");
// 2. 也可以不指定具体的类型 -> 这样就可以存放任何类型的元素,不会编译错误 -> 实际上相当于 Object 的方式
Generic generic = new Generic("shinobu");
Generic generic = new Generic(1234);
// 3. 可以使用通配符 -> 之后会提到
Generic<?> generic = new Generic<>("shinobu");
// 注意:这样声明是完全可以的,但是调用方法时会受到很大的限制,具体原因在统配符处解释
// 简单解释:? 虽然可以代表任何类型,但是它不能够去匹配各种类型
}
}

泛型接口

  • 概念:
    • 集合框架中对于泛型接口的使用也非常常见(List<T>Map<K,V>…)
    • 泛型接口通常搭配泛型类或者普通类使用
  • 表现形式:使用 类型参数==声明==当前接口是泛型接口
  • 特点
    • 泛型接口的具体类型指定
      1. 我们可以现在明确指定泛型接口的类型参数
      2. 我们也可以使用泛型类的类型参数,而不是具体指定一个
      3. 不能够使用通配符作为类型参数 -> 直接编译报错
interface Generator<T>
{
T getGenerator();
}
// 普通类实现泛型接口 -> 我们需要明确指定泛型接口的类型参数
class Person implements Generator<String>
{
// IDEA 会自动将类型参数转换为实际类型
public String getGenerator()
{
return null;
}
}

class Person<T> implements Generator<String>
{

}
// 这种实现方式:接口的类型参数就会依赖泛型类的类型参数
class Person<T> implements Generator<T>
{

}

泛型方法

  • 概念:方法前必须使用类型参数 <T> 进行声明才是泛型方法

    仅仅只是使用泛型类的类型参数的方法并不是泛型方法

  • 表现形式:定义方法时,在返回值之前声明类型参数

  • 特点

    • 普通泛型方法

      • 泛型方法可以将返回值定义为 void 也可以使用 <T>
      • 泛型方法需要定义自己的类型参数 <T>
        1. 泛型方法自己的类型参数和泛型类的类型参数==完全不同== -> 即使都使用 <T> 作为类型参数
        2. 泛型方法中的形参可以使用自己定义的类型参数 <E> 也可以使用泛型类定义的类型参数 <T>
    • 静态泛型方法

      • 静态方法不能够使用泛型类定义的类型参数,只能够使用自己的

      注:静态字段是不可以使用类型参数声明类型的

class Generic<T>
{
private static T filed; // 注意:这是错误的
...
// 注意:这个并不是泛型方法,这个只是一个使用了泛型类的类型参数的普通方法
public void getMethod(T method)
{

}

// 1. 泛型方法采用了<T>类型参数,泛型类也采用了<T>,但是这两个类型参数是完全不同的
public <T> T genericMethod(T method)
{
return method;
}

// 2. 注意:这种使用方式是可以的,但是静态方法不行,无论是否是泛型方法
public <E> void genericMethod(E method1,T method2)
{

}

// 1. 注意:这种使用方式是错误的
public static <E> E genericMethod(T method)
{
return method;
}

// 2. 注意:自己定义的是 E 就只能够使用的 E,而不能够使用 T
public static <E> void genericMethod(E method)
{

}


}

泛型限定

  • 概念:对传入类型参数中的具体类型做出限制 -> 不能够传入任意的具体类型

  • 方式

    • 上界

      • 概念:

        1. 如果限制条件是实体类 -> 传入的具体类型==必须是限制类的子类==
        2. 如果限制条件是接口 -> 传入的具体类型==必须实现限制接口==
      • 特点:

      • 测试

        • 限定条件是实体类
        class Person
        {
        ...
        }

        class Student extends Person
        {
        ...
        }

        // 显然只能够是 Person 的子类才能够作为具体类型传入
        public static <T extends Person> void genericMethod(T person)
        {
        System.out.println("泛型测试...");
        }

        public statci void main(String[] args)
        {
        genericMethod(new Student());// 合法
        genericMethod(new String()); // 不合法 -> 编译阶段就会报错
        }

        • 限定条件是接口
        interface PlayGame
        {
        ...
        }

        class Game implements PlayGame
        {

        }

        // 1. 接口是普通的接口 -> 显然传入的具体类型必须实现这个接口
        public static <T extends PlayGame> void genericMethod(T person)
        {
        System.out.println("泛型测试...");
        }

        /*
        2. 接口是泛型接口
        2.1 泛型接口使用类型参数 -> 传入的具体类型只要实现了 Comparator 接口就可以了 -> 从原来的任意范围缩小到有限的范围
        2.2 泛型接口使用具体类型 -> 这样写实际上限制死了传入的具体类型 -> 从原来的任意范围缩小的一个点
        */
        public static <T extends Comparator<T>> void genericMethod(T person)
        {
        System.out.println("泛型测试...");
        }
        // 这样传入的类型就只能够是 String 类型了
        public static <T extends Comparator<String>> void genericMethod(T person)
        {
        System.out.println("泛型测试...");
        }


    • 上界:类型参数不能够使用 super 关键字作为上界进行限制

      注:泛型通配符中却是可以 super 关键字进行限定的

    // 这种声明式错误的
    public static <T super Student> void genericMethod(T student)
    {
    /*
    原因分析:
    1. 类型擦除导致的
    2. 虚拟机执行类型擦除之后会导致 <> 中存放 T 类型而不是 Student 类型
    3. 而 T 类型也会被擦除称为 Object 类型
    4. 导致无论任何具体类型都可以传入,那么 super 的限制就没有任何实际意义了
    */
    }

泛型继承规则

  • 普通泛型继承规则

    • 图示
    image-20210427224737542
    • 代码:

      class Person
      {
      ...
      }

      class Student extends Person
      {
      ...
      }

      public static void method(List<Person> people)
      {
      ...
      }

      public static void main(String[] args)
      {
      List<People> people = new ArrayList<>();
      List<Student> students = new ArrayList<>();
      method(people); // 编译通过
      method(students); // 编译无法通过
      }
  • 通配符泛型继承规则

    • 下界限制继承

      image-20210428213751390
    • 上界限制继承

      image-20210428213829294

泛型通配符

  • 概念:在我们根本不知道要传入何种具体类型时,将通配符作为==具体类型==传入

  • 表现形式:<?>

  • 特点:

  • 类型

    • 无限定通配符

      1. 通配符不可以用于声明变量

        public static void main(String[] args)
        {
        /*
        1. 通配符声明是直接报错的 -> 因为通配符不是任何具体的类型,并不等价于 Object
        */
        ? element = null;
        }
      2. 通配符可以用于局部变量的声明中 -> 并非没有实际意义

        注:

        • 表示对于这个泛型类我们不关心存储的元素类型,只关心和元素类型无关的方法 -> 诸如获取集合长度,判断集合是否为空…
        • 用于方法的形参中可以解除泛型的严格限制
        • 提高了代码的可阅读性
        /*
        1. 正如之前所提到的可以使用通配符作为具体参数类型传入
        2. 因为几乎无法调用泛型类中的任何方法
        */
        List<?> people = new ArrayList<>();
        // 根据上面的声明传入通配符
        List<?>
        {
        // 显然我们不可能声明一个 ? 类型的变量去接收
        ? getElement();
        // 显然根据第一条规则赋值方法是根本不可能使用的
        void setElement(? element);
        // 但是我们只要类中存在不涉及 ? 类型的方法,仍然是可以调用的
        int size()
        {
        // 这种不需要涉及到使用 ? 类型的方法都是可以调用的
        ...
        }
        }
      3. 通配符用于方法中

        /*
        1. List<String>,List<Integer> 所有类型的集合都可以作为参数传入
        2. 而不是传入某个特定的泛型类
        */
        public static void method(List<?> list)
        {
        ...
        }
    • 通配符子类型限定

      1. 限定通配符声明局部变量

        /*
        1. 不同于无限定通配符几乎丧失了所有读写的能力
        2. 子类型限定通配符仍然具有读取的能力
        3. 原因我也不知道
        */
        List<? extends Person> people = new ArrayList<>();
        people.get();// 编译通过
        people.add();// 编译不通过
      2. 限定通配符用于方法中

        /*
        1. 限定了作为形参传入的类型 -> List<String>,List<Integer> 都不可以传入,只有 List<Student> 可以传入
        2. List<?> 是不可以被捕获的,但是 List 可以
        */
        public static void method(List<? extends Person> list)
        {
        ...
        }
    • 通配符超类型限定

      1. 限定通配符声明局部变量

        /*
        1. 不同于无限定通配符几乎丧失了所有读写的能力
        2. 超类型限定通配符仍然具有存储的能力
        3. 但是存储的类型只可以是 Student 类型 -> 其父类都是不可以存储的
        */
        List<? super Student> student = new ArrayList<>();
        student.add(new Student()); // 编译通过
        student.get(); // 编译不通过
      2. 限定通配符用于方法中

        /*
        1. 限定了作为形参传入的类型 -> List<String>,List<Integer> 都不可以传入,只有 List<Student> List<Person> List<Object> 可以传入
        2. List<?> 是不可以被捕获的,但是 List 可以
        */
        public static void method(List<? super Student> list)
        {
        ...
        }

    注:如果局部变量中使用无限定通配符,那么形参中也必须使用无限定通配符才能够捕获,只要形参有限定都是不可以捕获无限定通配符到的

类型擦除

  • 引入

    • 问题:

      List<String> strings = new ArrayList<String>();
      List<Integer> integers = new ArrayList<Integer>();
      /*
      1. 这两个集合类的类型相同吗?因为传入的具体类型不同所以反射得到的类型也不同?还是说是相同的
      2. 答案是相同的
      */
      System.out.println(strings.getClass() == integers.getClass());
    • 历史背景

      • Java 与 C#

        1. 底层层面:

          • Java 并没有实现真正的泛型化 -> 采用==类型擦除式泛型==;
          • C# 实现了真正的泛型 -> 采用==具现化泛型==
        2. 源码层面:

          • Java 采用的是在原有的集合类中的直接进行泛型化

            /*
            1. 原有的不带泛型的集合类(ArrayList,List...)可以声明,但是在源码中已经不存在了
            2. 只提供了带有泛型的集合类
            */
            ArrayList<T> List<T>...
          • C# 采用的是增加带有泛型的集合类,和原有的不带有泛型的集合类进行区分

      • Java 采取如此策略的原因

        1. 底层原因:

          • 采用具现化泛型则意味着需要对字节码文件,虚拟机的规范做出修改
          • Java 语言规范中保证老版本的源文件在新版本的 JDK 中同样能够正常编译
          • 既然字节码文件的格式,以及虚拟机的规范都发生了变化,那么老版本的源文件显然是不可能新版本的 JDK 上编译的

          注:这就导致 Java 仅仅只是在源码层面实现了泛型

        2. 原码原因:

          • 既然不用在字节码和虚拟机的层面去实现泛型,那么源码层面的泛型怎么实现呢?
          • Java 采用在原有的集合类的基础上直接进行泛型化,而不是新增加泛型类
          • Java 在需要增加泛型机制时已经经过了数十年的发展,许多方法中已经使用原有的集合类作为返回值
          • 如果此时再添加新的泛型集合类显然会将集合类库变得更加庞大(骂的人更多)
    • 遗留问题:原有的集合类如何新版本的泛型集合类交互呢?-> 让原有的集合类作为原始类成为所有泛型集合类的父类

      ArrayList<String> strings = new ArrayList<>();
      ArrayList arrayList = new ArrayList();
      arrayList = strings; // 编译可以通过
  • 概念:源代码在进入虚拟机之前所有的类型参数都会被==抹去==

  • 规则:

    • 无限定类型参数:

      1. 所有==类型参数==全部被替换成为 Object
      2. ==泛型类==会转换成为==原始类==

      任何需要使用泛型的地方都会采用强制转换

      从最简单的泛型擦除规则可以看出来,泛型在 Java 中仅仅是相当于一种语法糖,底层仍然采用的是多态去实现泛型

      /*
      进入虚拟机之前的源代码
      */
      class Generic<T>
      {
      T genericFiled;

      public T method(T first, T second)
      {
      ...
      }
      }
      /*
      进入虚拟机之后的源代码
      */
      class Generic
      {
      Object genericFiled;

      public Object method(Object first, Object second)
      {
      ...
      }
      }

      public static void main*(String[] args)
      {
      //源码层面: 类型参数将会被抹去生成原类型
      Generic<String> generic = new Generic<String>();
      // 虚拟机层面:
      Generic generic = new Generic();
      // 虚拟机中:使用带有泛型的变量时是需要进行强制转换的
      String str = (String)generci.method("123","456");
      }
    • 有限定类型参数

      • 限定类

        1. 所有类型参数都会转换为==限定的类==
        2. 泛型类会==继承==当前这个限定类
        /*
        进入虚拟机之前的源代码
        */
        class Generic<T extends Person>
        {
        T genericFiled;

        public T method(T first, T second)
        {
        ...
        }
        }
        /*
        进入虚拟机之后的源代码
        */
        class Generic
        {
        Person genericFiled;

        public Person method(Person first, Person second)
        {
        ...
        }
        }

        public static void main(String[] args)
        {
        //源码层面: 类型参数将会被抹去生成原类型
        Generic<Student> generic = new Generic<Student>();
        // 虚拟机层面:
        Generic generic = new Generic();
        // 虚拟机中:使用带有泛型的变量时是需要进行强制转换的
        Person str = (Person)generci.method(new Student(),new Student());
        }
      • 限定接口

        • 规则

          1. 所有类型参数都会转换为==限定的接口==
          2. 泛型类会==实现==当前这个限定接口
        • 问题:如果限定条件中有多个接口?虚拟机如何选择?

          解决方式:虚拟机会选择==第一个接口==作为限定的接口

          影响:所以限定接口的顺序会导致虚拟机选择的接口不一样,导致强转的类型不一样

        /*
        进入虚拟机之前的源代码
        */
        class Generic<T extends Comparable & Serializable>
        {
        T genericFiled;

        public T method(T first, T second)
        {
        ...
        }
        }
        /*
        进入虚拟机之后的源代码
        */
        class Generic
        {
        // 选择第一个限定接口作为具体的类型
        Comparable genericFiled;

        public Comparable method(Comparable first, Comparable second)
        {
        ...
        }
        }
  • 类型擦除产生的问题:

    • 重载问题

      /*
      1. List<String> List<Integer> 根据类型擦除机制 -> 显然这两个参数的类型是一致的都是 List
      2. 重载必须保证相同方法名中的参数类型必须不一致,这里的参数类型一致所以显然重载失败
      */
      public static void method1(List<String> strings)
      {

      }

      public static void method1(List<Integer> integers)
      {

      }
      //------------------------------------------------------------------
      /*
      1. 重载必须保证方法签名不同,但是返回类型并不属于方法名中的一部分,所以编译是根本无法通过的
      2. 但是在虚拟机中的重载并不是这样的
      3. 虚拟机中只要保证 返回类型 + 方法签名 是不同的就可以证明两个方法不同 -> 是可以共存的
      */
      public static String method1(List<String> strings)
      {

      }

      public static int method1(List<Integer> integers)
      {

      }

    • 继承问题

      class Person<T>
      {
      T first;

      public void setFirst(T first)
      {
      this.first = first;
      }

      public T getFirst()
      {
      return first;
      }
      }

      class Student extends Person<String>
      {
      /*
      1. 子类重写父类的方法所以理所应当调用的应该是子类的的方法
      2. 但是由于类型擦除导致父类新增了两个方法
      public void setFirst(Object first)
      public Object getFirst()
      3. 显然这两个新增的方法也会被子类继承,但是这样就达不到重写的效果,虚拟机可能根本不知道到底调用哪个方法
      4. 桥方法诞生用于解决这个问题:
      虚拟机中采用 方法嵌套调用的方式解决
      public void setFirst(Object first)
      {
      this.setFirst((String) first) 强制转换调用子类的方法
      }
      */
      @Override
      public void setFirst(String first)
      {
      super.first = first;
      }

      @Override
      public String getFirst()
      {
      return super.getFirst();
      }
      }
    • 自动拆装箱问题

      • 回顾:前面提到类型参数不能够接收基本数据类型(int double…)
      • 原因:类型擦除后,显然需要在 intObject 之间进行强制转换,这显然是不可以的 -> 只能够允许传入 int 的包装类 Integer
      • 影响:调用方法时会进行很多次的无意义的自动拆装箱,导致 Java 泛型的效率非常低
    • 数组问题:不能够声明泛型数组

      /*
      以下代码都是不可以运行的
      */
      public static void main(String[] args)
      {
      // 源码层面
      ArrayList<String>[] strings = new ArrayList<>[20];
      // 虚拟机中
      Object[] strings = new Object[20];
      // 显然此时任何类型都可以作为元素加入数组中 -> 在编译阶段并不会报错
      strings[0] = new Integer(1);
      // 但是虚拟机执行这行代码时,会检查存储类型是否正确,最后会抛出存储类型异常
      // 有个问题,虚拟机怎么知道原有的存储类型是什么?
      }

泛型与反射

Author: Fuyusakaiori
Link: http://example.com/2021/08/16/java/generic/泛型机制/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.