JavaEE之泛型

52 阅读12分钟

认识泛型

泛型介绍

  • 定义类、接口、方法时,同时声明了一个或者多个类型变量(如:),称为泛型类、泛型接口,泛型方法、它们统称为泛型
    public class ArrayList<E>{
        ...
    }
    
  • 作用:泛型提供了在编译阶段约束所能操作的数据类型,并自动进行检查的能力!(先确定类型,再创建对象)
  • 这样可以避免强制类型转换,及其可能出现的异常。
  • 泛型的本质:把具体的数据类型作为参数传给类型变量。

剖析ArrayList是如何使用泛型进行类型校验的

  • 可以看到如果不指定ArrayList中的泛型参数的类型,可以向ArrayList对象中存入任何类型的值,但当我们指定了ArrayList的参数后,ArrayList对象就只能存入泛型参数指定的类型的值了,如下方代码所示:
import java.util.ArrayList;
public class Demo041 {

    public static void main(String[] args) {
        // 目标:使用ArrayList存储数据,感受泛型在编译阶段约束类型,不用强制类型转换
         //1.不使用泛型操作数据(每个元素类型是Object)
        ArrayList list1 = new ArrayList();
                list1.add("hello");
                list1.add(100);
                list1.add(3.14);
                //获取第一个元素
        Object one = list1.get(0);
                //得到真实类型
        String oneStr = (String) one;
                //输出第一个元素的字符的个数
        System.out.println("list1第一个元素字符串的字符个数:"+oneStr.length());

                //2.使用泛型操作数据
        ArrayList<String> list2 = new ArrayList<>();
                list2.add("hello");
                // list2.add(100); 报错,在编译阶段会约束类型必须为字符串
         // list2.add(3.14); 报错,在编译阶段会约束类型必须为字符串
         //获取第一个元素
        String a = list2.get(0);
                //输出第一个元素的字符的个数
        System.out.println("list2第一个元素字符串的字符个数:"+a.length());
    }
}

image.png

  • 可以按住ctrl+左键进入ArrayList类的源码 image.png

  • 提供了三个构造方法,其中有一个构造方法有一个向上通配泛型就需要链式传入创建ArrayList时指定的泛型参数 image.png

    • ArrayList(int)接收一个int参数,用来创建一个有初始容量的容器,可以一次分配足够多的空间避免扩容带来的性能损失
    • ArrayList()用来创建一个空容器
    • ArrayList(Collection<? extends E>),这个构造方法是一个容器转换的方法,Collectrion作为容器的父类,可以持有任意子类的容器对象,同时里面用到了向上通配泛型,该泛型不会检查指定类型的子类类型,再保证最大兼容的同时避免了精度丢失。这个向上统配泛型就是我们创建ArrayList对象时指定的泛型参数,该构造方法运行后就会将方法内的其他容器转换为ArrayList容器
  • 可以看到类中的成员方法的原型都有使用泛型参数的,这样既可以不用因为不同参数重复造轮子,又可以利用创建ArrayList对象时指定的泛型参数,做类型校验

    image.png

    image.png

自定义泛型类

语法:

修饰符 class 类名<类型变量,类型变量,…> { 

}


public class ArrayList<E>{
    . . .
}
  • 注意:类型变量建议用大写的英文字母,常用的有:E、T、K、V
    • E - Element(元素)最常见于集合类,表示集合中存储的元素类型
    • T - Type(类型)最通用的类型参数,表示任意类型
    • K - Key(键) ​ 和 V - Value(值)用于映射关系,成对出现,用于表示键和值的类型

应用实例

  • 在后面学的设计模式中,有一个种设计模式叫做装饰器模式,即为在不改变原有接口及其实现的情况下增强功能,如下方代码所示,可以给ArrayList设计一个类增强功能:
    • 这里就是利用了自定义泛型类,创建类的对象时先指定类型,指定的类型,利用泛型参数,链式指定了类中的成员变量和成员方法的泛型
    import java.util.ArrayList;
    public class HeiMaArrayList<E>{
    
        //定义数组集合
    private ArrayList<E> arrayList = new ArrayList<>();
    
        public void add(E e){
            System.out.println("开始自己的集合数组添加元素。。。");
            arrayList.add(e);
        }
    
        public E get(int index){
            System.out.println("开始自己的集合数组查询元素。。。");
            return arrayList.get(index);
        }
    
        public void remove(E e){
            arrayList.remove(e);
        }
    
        @Override
        public String toString() {
            arrayList.forEach(e-> System.out.println(e));
            return "";
        }
    }
    public class Demo051 {
    
        public static void main(String[] args) {
            //目标:使用自己的泛型类集合数组操作数据
    
        HeiMaArrayList<String> list = new HeiMaArrayList<>();
            list.add("hello");
            list.add("world");
            System.out.println("list添加元素后:");
            System.out.println(list);
            String s = list.get(0);
            System.out.println("第一个元素:"+s);
            list.remove("hello");
            System.out.println("list移除第一个元素后:");
            System.out.println(list);
        }
    }
    

自定义泛型接口

语法

修饰符 interface 接口名<类型变量,类型变量,…> { 

}

public interface A<E>{
    . . .
}

应用实战

  • 需求
    • 现在有两个分别用于存放学生类实例化对象和教师类实例化对象的容器,需要实现一个工具类用于操作两个容器
  • 分析:
    • 单独实现两个工具类(不推荐):面对复杂业务开发场景需要在代码中写明白遇到那个对象就调用与之对应工具类的实例类中的方法,会导致代码出现大量的if-elseif-else
    • 设计一个工具类接口用泛型做类型检验,分别为不同类型实现接口(推荐):大大减少了复杂开发场景的代码量,操作容器对象只需要调用接口并传入对应容器内元素的类作为泛型参数
  • 代码实现:
    • DataOperator操作容器的工具接口
    public interface DataOperator<T> {
    
        void add(T t);
    
        void update(T t);
    
        void delete(T t);
    
        T get(int index);
    }
    
    • Student学生类
    public class Student {
    
        private int stuNo; //学号
    private String StuName; //学生姓名
    
    public Student() {
        }
    
        public Student(String stuName, int stuNo) {
            StuName = stuName;
            this.stuNo = stuNo;
        }
    
        public String getStuName() {
            return StuName;
        }
    
        public void setStuName(String stuName) {
            StuName = stuName;
        }
    
        public int getStuNo() {
            return stuNo;
        }
    
        public void setStuNo(int stuNo) {
            this.stuNo = stuNo;
        }
    }
    
    • Teacher教师类
    public class Teacher {
    
        private String name;
        private String hobby;
    
        public Teacher(){}
        public Teacher(String name, String hobby) {
            this.name = name;
            this.hobby = hobby;
        }
    
        public String getHobby() {
            return hobby;
        }
    
        public void setHobby(String hobby) {
            this.hobby = hobby;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
    
    • StudentDataOperator操作学生容器的工具实现类
    public class StudentDataOperator implements DataOperator<Student>{
        @Override
        public void add(Student student) {
            System.out.println("添加学生信息");
        }
    
        @Override
        public void update(Student student) {
            System.out.println("修改学生信息");
        }
    
        @Override
        public void delete(Student student) {
            System.out.println("删除学生信息");
        }
    
        @Override
        public Student get(int index) {
            System.out.println("查询学生信息");
            return null;
        }
    }
    
    • TeacherDataOperator操作教师容器的实现类
    public class TeacherDataOperator implements DataOperator<Teacher> {
        @Override
        public void add(Teacher teacher) {
            System.out.println("添加老师信息");
        }
    
        @Override
        public void update(Teacher teacher) {
            System.out.println("修改老师信息");
        }
    
        @Override
        public void delete(Teacher teacher) {
            System.out.println("删除老师信息");
        }
    
        @Override
        public Teacher get(int index) {
            System.out.println("查询老师信息");
            return null;
        }
    }
    
    • Demo061主类
    public class Demo061 {
    
        public static void main(String[] args) {
            //创建老师对象
    Teacher teacher = new Teacher();
            DataOperator<Teacher> dataOperator = new TeacherDataOperator();
            dataOperator.add(teacher);
            dataOperator.get(1);
            dataOperator.update(teacher);
            dataOperator.delete(teacher);
    
            //创建学生对象
            // Student student = new Student();
            // DataOperator<Student> dataOperator = new StudentDataOperator();
            // studentDataOperator.add(student);
            // studentDataOperator.update(student);
            // studentDataOperator.delete(student);
            // studentDataOperator.get(0);
        }
    }
    
    • 除了直接调用接口,也可以结合多态和构造函数达到强制类型检查的目的
    package DataOperator;
    
    public class SchoolBox<E> {
        E data;
        private DataOperator<E> dataOperator;
    
        public SchoolBox(DataOperator<E> dataOperator) {
            this.dataOperator = dataOperator;
        }
    
        public DataOperator<E> getDataOperator() {
            return dataOperator;
        }
    
    }
    
    package DataOperator;
    public class Demo061 {
    
        public static void main(String[] args) {
            SchoolBox<Teacher> eSchoolBox = new SchoolBox<Teacher>(new TeacherDataOperator());
            eSchoolBox.getDataOperator().add(new Teacher());
            eSchoolBox.getDataOperator().update(new Teacher());
            eSchoolBox.getDataOperator().delete(new Teacher());
    
        }
    }
    
    如果传入的对象和先确定的类型检查不匹配则会报错 image.png

泛型方法

语法

修饰符 <类型变量,类型变量,…>  返回值类型 方法名(形参列表) {

}

非泛型类中使用泛型方法方式

public static <T> void test(T t){
    
}

泛型类中使用泛型方法

public E get(int index){
    return arrayList.get(index);
}

例子

package com.itheima._07静态泛型方法与泛型通配符上下限;

/**
* @Description Demo071
* @Author songyu
* @Date 2025-09-30  14:46
*/
public class Demo071 {

    public static void main(String[] args) {

        Student[] students = new Student[10];
        Teacher[] teachers = new Teacher[2];
        printArrayLength(students);
        printArrayLength(teachers);
    }

    // 泛型类中使用泛型方法语法 :  static <T> 返回值  方法名(T 变量名){}
public static <T> void printArrayLength(T[] array){
        System.out.println("数组元素的个数:"+array.length);
    }
}

class Student{}
class Teacher{}

泛型通配符

引入

  • 在前面的ArrayList源码剖析中,可以看到有一个构造方法ArrayList(Collection<? extends E>),这个构造方法是一个容器转换的方法,Collectrion作为容器的父类,可以持有任意子类的容器对象,同时里面用到了向上通配泛型,该泛型不会检查指定类型的子类类型,再保证最大兼容类型的同时限制了不能传入处理不了的类型。这个向上统配泛型就是我们创建ArrayList对象时指定的泛型参数,该构造方法运行后就会将方法内的其他容器转换为ArrayList容器,这就是我们对泛型通配符的初步了解

什么是通配符

  • 通配符:就是 “?” ,可以在“使用泛型”的时候代表一切类型; E T K V 是在定义泛型的时候使用。
  • 泛型的上下限
    • 泛型上限: ? extendsCar: ?能接收的必须是Car或者其子类 。
    • 泛型下限: ? superCar : ? 能接收的必须是Car或者其父类。

实例代码

class Che{}
class Car extends Che{}
class BMW extends Car{}
class Cat{}
import java.util.List;
public class Demo072 {

    public static void main(String[] args) {
        //目标:泛型通配符,上限和下限
         // List<?> :?可以代表任意类型,和List<Object>一样
         // List<Car>:  写死类型,只能是Car, 父类或子类都不可以, 使用最多
         // List<? extends Car> :泛型的上限,?可以代表Car和Car的子类
         // List<? super Car> :泛型的下限,?可以代表Car和Car的父类
         // 注意 不带泛型的List都可以被上面接收,但是不建议不使用泛型集合

         //分别创建不同类型的List对象
        List<Che> cheList = List.of(new Che());
        List<Car> carList = List.of(new Car());
        List<BMW> bmwList = List.of(new BMW());
        List<Cat> catList = List.of(new Cat());

        //打印上面4个集合
        printCar1(cheList);
        printCar1(carList);
        printCar1(bmwList);
        printCar1(catList);

        // printCar2(cheList); 报错,因为不是car
        printCar2(carList);
        // printCar2(bmwList);报错,因为不是car
         // printCar2(catList);报错,因为不是car

         // printCar3(cheList); 报错,因为不是Car及其子类
        printCar3(carList);
        printCar3(bmwList); //正确,因为是Car的子类
         // printCar3(catList);报错,因为不是Car及其子类

        printCar4(cheList);
        printCar4(carList);
        // printCar4(bmwList);报错,因为不是Car及其父类
      // printCar4(catList);报错,因为不是Car及其父类

}

    public static void printCar1(List<?> list){
        System.out.println(list);
    }
    public static void printCar2(List<Car> list){
        System.out.println(list);
    }
    public static void printCar3(List<? extends Car> list){
        System.out.println(list);
    }
    public static void printCar4(List<? super Car> list){
        System.out.println(list);
    }

}

泛型支持的类型

  • 泛型不支持基本数据类型,只能支持对象类型(引用数据类型)。 包装类

    包装类就是把基本类型的数据包装成对象的类型。

    基本数据类型对应的包装类(引用数据类型)
    byteByte
    shortShort
    intInteger
    longLong
    charCharacter
    floatFloat
    doubleDouble
    booleanBoolean
  • 关于包装数据类型的详解,请查看JavaEE之包装类型包装类 - 掘金

泛型擦除

什么是泛型擦除?

  • Java 中的泛型仅仅存在于编译期,一旦代码通过了编译器的类型检查并生成 .class 字节码文件后,所有的泛型信息都会被编译器“擦除”,替换为它们的上界(通常是 Object)。
  • 简单来说,在 JVM 运行期间,Java 是不知道泛型存在的List<String>List<Integer> 在运行时的类型都是同一个纯粹的 List(原始类型)。

泛型擦除的具体过程

  • 当你编写带有泛型的代码并进行编译时,Java 编译器主要做了以下三件事:

    • 类型检查: 确保你存入集合或传入方法的对象类型是正确的。如果把 Integer 放入 List<String>,编译器会报错。
    • 擦除类型: 将所有的泛型参数(如 <T><E>)替换为 Object。如果泛型有上界限制(例如 <T extends Number>),则替换为上界类型(Number)。
    • 自动强转: 在获取数据时,编译器会自动在字节码中插入类型强制转换的代码,以保证开发者拿到的数据类型是正确的。
  • 代码对比示例

    • 编译前的代码(你写的):

      List<String> list = new ArrayList<String>();
      list.add("Hello");
      String text = list.get(0);
      
    • 编译后的代码(JVM 实际执行的逻辑):

      List list = new ArrayList(); // 泛型 <String> 被擦除了
      list.add("Hello");
      String text = (String) list.get(0); // 编译器自动帮你加上了强转
      

为什么 Java 要这么设计

  • Java 在 JDK 1.5 才引入了泛型。为了保证向后兼容性(Backward Compatibility) ,也就是让 JDK 1.5 编译出来的代码依然能在 JDK 1.4 的老版本 JVM 上运行,并且老版本没有泛型的代码也能和新版本的泛型代码无缝混用,Java 核心团队选择了“泛型擦除”这种妥协方案。(这意味着 JVM 的底层指令系统不需要做任何修改,所有的泛型魔法都在编译器层面就处理完了。)
  • 但这个做法实际上也带来了很多问题,是 Java 历史上最具争议的设计之一,在技术圈里经常被吐槽为“假泛型”或“历史包袱”,是一个充满了妥协的决定

泛型擦除带来的限制与影响

  • 因为运行时的 JVM 不知道泛型的存在,这给 Java 开发带来了一些常见的限制和“坑”:

    • 无法在运行时使用 instanceof 判断泛型真实类型

      • ❌ 错误:if (obj instanceof List<String>) (编译报错)
      • ✅ 正确:if (obj instanceof List<?>) (只能判断它是个 List)
  • 泛型不能使用基本数据类型

    • 因为泛型擦除后会变成 Object,而基本数据类型(如 int, double)不是 Object 的子类。

      • ❌ 错误:List<int>
      • ✅正确:List<Integer> (必须使用包装类)
  • 无法实例化泛型对象或泛型数组

    • ❌ 错误:T obj = new T(); (由于 T 被擦除为 Object,编译器不知道到底该 new 哪个具体的类)
    • ❌ 错误:T[] array = new T[10]; (同理,无法在运行时确定数组的确切类型)
  • 方法重载冲突(这是最令广大开发者头大的缺点)

    • 下面的两个方法在开发者看来参数不同,但无法编译,因为擦除后它们的方法签名完全一样(都是 print(List)):

      public void print(List<String> list) { }
      public void print(List<Integer> list) { } // 编译报错:方法签名冲突
      
  • 总的来说Java 的泛型本质上是一颗语法糖,它是给编译器看的,用来在写代码时提供更强的类型安全检查,并免去手动强转的麻烦。一旦进入运行阶段,大家就又“众生平等”地变回了 Object