Java-泛型

本篇我们来讲解 Java 泛型~

1. 泛型是什么?为什么要用泛型?

  • 核心概念 :泛型是 JDK 5 引入的特性,允许在定义类、接口或方法时使用类型参数 (Type Parameter)。你可以将这个类型参数看作一个占位符,表示某种具体的类型,但具体是什么类型,要到使用这个类、接口或方法时才指定。
  • 解决的问题
    1. 类型安全 :在 JDK 5 之前,集合类(如 ArrayList, HashMap)默认存储 Object 类型。当你从集合中取出元素时,需要强制转型(Casting)为你期望的类型。如果实际存储的类型与你强制转型的目标类型不匹配,就会在运行时 抛出 ClassCastException。泛型在编译时 就检查类型是否匹配,大大减少了这类运行时错误。

      • 没有泛型的问题示例

        java 复制代码
        ArrayList list = new ArrayList(); // 存储 Object
        list.add("Hello");
        list.add(123); // 不小心存入了 Integer
        String str = (String) list.get(1); // 编译通过,但运行时抛出 ClassCastException!
      • 使用泛型的改进

        java 复制代码
        ArrayList<String> list = new ArrayList<>(); // 指定存储 String
        list.add("Hello");
        // list.add(123); // 编译错误!编译器阻止存入 Integer
        String str = list.get(0); // 无需强制转型,直接是 String
    2. 消除强制转型:如上例所示,使用泛型后,从集合中取出元素时不需要再进行强制转型,代码更简洁清晰。

    3. 提高代码复用性 :你可以编写适用于多种类型的通用代码。例如,一个 List<T> 接口可以用于存放任何类型 T 的元素,而不需要为 StringInteger 等分别编写 StringListIntegerList

2. 泛型类

  • 定义 :在类名后面用尖括号 <> 声明一个或多个类型参数。这些参数可以在类体中像普通类型一样使用(作为字段类型、方法参数类型、方法返回类型等)。

  • 语法

    java 复制代码
    public class ClassName<T1, T2, ..., Tn> {
        // 类体可以使用 T1, T2, ..., Tn
    }
  • 实例化 :创建泛型类的对象时,在类名后的尖括号 <> 中指定具体的类型参数(称为类型实参 ,Type Argument)。

    java 复制代码
    ClassName<具体类型1, 具体类型2, ..., 具体类型n> obj = new ClassName<>();
  • 示例 :定义一个简单的泛型 Box

    java 复制代码
    public class Box<T> {
        private T content; // T 表示某种类型的内容
    
        public void setContent(T content) {
            this.content = content;
        }
    
        public T getContent() {
            return content;
        }
    }
  • 使用

    java 复制代码
    Box<String> stringBox = new Box<>(); // T 被指定为 String
    stringBox.setContent("Hello Generics!");
    String message = stringBox.getContent(); // 直接是 String,无需转型
    
    Box<Integer> intBox = new Box<>(); // T 被指定为 Integer
    intBox.setContent(42);
    int number = intBox.getContent(); // 自动拆箱为 int
  • 注意

    • 泛型类可以有多个类型参数,如 Pair<K, V>
    • 类型参数通常用单个大写字母表示(如 TEKV),但这只是约定俗成。
    • 实例化时,构造函数后的 <> 称为菱形语法(Diamond Operator),允许省略类型实参(编译器会根据声明推断)。

3. 泛型接口

  • 定义:与泛型类类似,在接口名后声明类型参数。

  • 语法

    java 复制代码
    public interface InterfaceName<T1, T2, ..., Tn> {
        // 接口方法可以使用 T1, T2, ..., Tn
    }
  • 实现

    • 方式一 :实现类在实现接口时指定具体类型。

      java 复制代码
      public interface Producer<T> {
          T produce();
      }
      
      public class StringProducer implements Producer<String> {
          @Override
          public String produce() {
              return "Generated String";
          }
      }
    • 方式二 :实现类本身也声明为泛型类,类型参数与接口一致。

      java 复制代码
      public class GenericProducer<T> implements Producer<T> {
          @Override
          public T produce() {
              // ... 生产 T 类型对象的逻辑 ...
              return result;
          }
      }
      • 使用时再指定具体类型:GenericProducer<Integer> intProducer = new GenericProducer<>();

4. 泛型方法

  • 定义:在方法签名上声明类型参数,该方法可以在不同类型上操作。

  • 语法 :在方法的返回类型之前(或修饰符之后)用尖括号 <> 声明类型参数。

    java 复制代码
    public <T1, T2, ..., Tn> 返回类型 方法名(参数列表) {
        // 方法体可以使用 T1, T2, ..., Tn
    }
  • 特点

    • 类型参数的作用域仅限于该方法本身。
    • 泛型方法可以定义在普通类中,也可以定义在泛型类中(此时,泛型方法的类型参数可以与类的类型参数同名但含义不同)。
    • 编译器通常能根据传入的参数类型推断出类型实参。
  • 示例

    java 复制代码
    public class Util {
        // 泛型方法:交换数组中两个元素的位置
        public static <T> void swap(T[] array, int i, int j) {
            T temp = array[i];
            array[i] = array[j];
            array[j] = temp;
        }
    
        // 泛型方法:查找数组中最大值 (要求 T 实现了 Comparable<T>)
        public static <T extends Comparable<T>> T max(T[] array) {
            if (array == null || array.length == 0) return null;
            T maxVal = array[0];
            for (T element : array) {
                if (element.compareTo(maxVal) > 0) {
                    maxVal = element;
                }
            }
            return maxVal;
        }
    }
  • 使用

    java 复制代码
    Integer[] intArray = {1, 5, 3, 2};
    Util.swap(intArray, 1, 2); // <Integer> 被编译器推断出来
    Integer maxInt = Util.max(intArray); // 同样推断出 <Integer>
    
    String[] strArray = {"apple", "banana", "cherry"};
    String maxStr = Util.max(strArray); // 推断出 <String>
  • 注意 :示例中的 <T extends Comparable<T>>类型边界 (Type Bound),用于约束 T 必须是实现了 Comparable<T> 接口的类型,这样方法体内才能安全地调用 compareTo 方法。我们稍后会详细讲解边界。

5. 类型擦除

  • 关键机制 :Java 泛型是通过类型擦除 (Type Erasure)实现的。这意味着:

    • 编译时 ,编译器会检查泛型代码的类型安全(确保你放入 List<String> 的是 String)。
    • 编译后 ,生成的字节码(.class 文件)中,所有的类型参数都会被擦除掉,替换成它们的上界 (如果没有指定上界,则替换成 Object),并在必要的地方插入强制转型。
  • 目的:为了兼容 JDK 5 之前的代码(非泛型集合类)。

  • 示例

    java 复制代码
    // 源代码 (编译前)
    List<String> list = new ArrayList<>();
    list.add("Hi");
    String s = list.get(0);
    
    // 经过类型擦除后的等效代码 (编译后,近似表示)
    List list = new ArrayList(); // 类型参数 <String> 被擦除
    list.add("Hi"); // 添加 String 没问题
    String s = (String) list.get(0); // 编译器插入的强制转型
  • 影响

    1. 无法获取运行时类型参数 :例如 List<String>.classnew T() 都是不合法的,因为运行时 T 已经不存在了(被擦除为 Object 或边界类型)。
    2. 不能创建参数化类型的数组 :如 new List<String>[10] 通常会导致编译警告或错误,因为数组需要确切知道其元素类型,而擦除后 List<String>List<Integer> 在运行时都是 List,数组无法区分。
    3. 泛型类的不同实例化共享同一个类Box<String>.class == Box<Integer>.class 结果为 true

6. 通配符: ?

  • 目的:增加泛型的灵活性,表示"未知类型"。主要用于方法参数、局部变量,有时也用于字段。

  • 类型

    1. 无界通配符 <?>:表示任何类型。

      • 用途 :当你编写的方法只需要读取集合元素(作为 Object 或某个公共父类使用),而不关心具体类型时。

      • 示例

        java 复制代码
        public static void printList(List<?> list) {
            for (Object obj : list) { // 元素被当作 Object 处理
                System.out.println(obj);
            }
            // list.add(new Object()); // 错误!不能添加 (除了 null),因为不知道具体类型
        }
      • 限制 :不能向声明为 List<?> 的变量添加除 null 以外的任何元素(因为你不知道里面具体是什么类型)。

    2. 上界通配符 <? extends UpperBound>:表示 UpperBound 类型或其子类型

      • 用途 :支持协变(Covariance)。你可以安全地从这样的结构中读取 元素(读取的元素至少是 UpperBound 类型),但通常不能添加 元素(除了 null)。

      • 示例

        java 复制代码
        public static double sumOfList(List<? extends Number> list) {
            double sum = 0.0;
            for (Number num : list) { // 安全读取,每个元素都是 Number 或其子类
                sum += num.doubleValue();
            }
            return sum;
            // list.add(new Integer(1)); // 错误!不能添加,可能是 List<Double>
        }
        
        List<Integer> intList = Arrays.asList(1, 2, 3);
        List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
        sumOfList(intList); // OK, Integer extends Number
        sumOfList(doubleList); // OK, Double extends Number
    3. 下界通配符 <? super LowerBound>:表示 LowerBound 类型或其父类型

      • 用途 :支持逆变(Contravariance)。你可以安全地写入 元素(写入的元素是 LowerBound 或其子类),但读取时只能当作 Object(因为不知道具体父类是什么)。

      • 示例

        java 复制代码
        public static void addNumbers(List<? super Integer> list) {
            for (int i = 1; i <= 5; i++) {
                list.add(i); // 安全写入,Integer 是 LowerBound
            }
            // Integer num = list.get(0); // 错误!读取出来可能是 Number 或 Object
            Object obj = list.get(0); // 只能当作 Object 读取
        }
        
        List<Number> numList = new ArrayList<>();
        List<Object> objList = new ArrayList<>();
        addNumbers(numList); // OK, Number super Integer
        addNumbers(objList); // OK, Object super Integer
  • PECS 原则 :Producer-Extends, Consumer-Super。

    • 如果你需要一个结构提供 (生产)元素(Producer),使用 <? extends T>
    • 如果你需要一个结构接受 (消费)元素(Consumer),使用 <? super T>
    • 如果一个结构同时生产 消费,你可能需要使用确切的类型参数 T

7. 类型边界

  • 目的 :约束类型参数可以代表哪些类型。使用 extends 关键字(在泛型中,extends 可以表示类继承或接口实现)。

  • 语法

    • 单个边界<T extends ClassOrInterface>
    • 多个边界<T extends ClassA & InterfaceB & InterfaceC>(类只能有一个且必须在第一个,接口可以有多个)。
  • 示例

    java 复制代码
    // T 必须是 Number 或其子类
    public class NumericBox<T extends Number> {
        private T value;
        // ... getter, setter ...
        public double getValueAsDouble() {
            return value.doubleValue(); // 安全调用,因为 T 是 Number
        }
    }
    
    // T 必须实现 Comparable 接口,并且能够和自己比较 (Comparable<T>)
    public static <T extends Comparable<T>> T max(T a, T b) {
        return (a.compareTo(b) > 0) ? a : b;
    }
    
    // 多个边界:T 必须是 Serializable 的子类 且 实现 Comparable
    public class SerializableComparable<T extends Serializable & Comparable<T>> {
        // ...
    }
  • 注意 :边界在编译时被检查,确保类型安全。类型擦除时,类型参数会被替换为其最左边的边界 (或 Object 如果没有边界)。

8. 泛型在继承和子类型中的规则

  • 泛型类本身Box<Number>Box<Integer> 没有 继承关系。即使 IntegerNumber 的子类,Box<Integer> 也不是 Box<Number> 的子类。
  • 通配符与子类型
    • List<? extends Number>List<?> 的子类型。
    • List<Number>List<? super Number> 的子类型? (不是直接的父子关系,但 List<Number> 可以赋值给 List<? super Number> 变量)
    • 更重要的关系由通配符捕获:List<Integer> 可以赋值给 List<? extends Number> 变量(因为 Integer extends Number)。
    • List<Number> 可以赋值给 List<? super Integer> 变量(因为 Number super Integer)。

9. 边界用例和限制

  • 不能实例化类型参数new T() 是非法的,因为运行时 T 被擦除。

    • 变通方法 :通过反射(需要 Class<T> clazz 参数)或工厂模式。
  • 不能用于静态上下文 :类的类型参数不能用于静态方法或静态字段,因为静态成员属于类,而类型参数属于实例。

    java 复制代码
    public class Box<T> {
        // private static T staticField; // 错误!
        // public static T staticMethod() { ... } // 错误!
        public static <U> U genericStaticMethod(U u) { ... } // OK,泛型方法有自己的类型参数
    }
  • 不能创建基本类型的参数化类型 :泛型类型参数必须是引用类型。不能有 List<int>,只能用 List<Integer>。自动装箱/拆箱缓解了这个问题。

  • 不能抛出或捕获泛型类的实例catch (T e) 是不允许的。泛型类也不能直接或间接继承 Throwable

  • 方法重载冲突 :类型擦除可能导致两个方法签名在编译后变得相同,引起编译错误。

    java 复制代码
    public class Example {
        public void print(List<String> list) { ... }
        public void print(List<Integer> list) { ... } // 编译错误!擦除后都是 print(List)
    }

总结

泛型通过类型参数、类型擦除、通配符和类型边界等机制,提供了强大的类型安全性和代码复用能力。理解类型擦除是深入掌握泛型行为的关键,而通配符(尤其是 extendssuper)则提供了处理不同类型集合时的灵活性。遵循 PECS 原则有助于正确使用通配符。虽然泛型有一些限制(主要是由类型擦除带来的),但它们极大地提升了 Java 程序的健壮性和可读性。

相关推荐
开开心心就好14 小时前
发票合并打印工具,多页布局设置实时预览
linux·运维·服务器·windows·pdf·harmonyos·1024程序员节
獨枭14 小时前
PyCharm 跑通 SAM 全流程实战
windows
仙剑魔尊重楼15 小时前
音乐制作电子软件FL Studio2025.2.4.5242中文版新功能介绍
windows·音频·录屏·音乐·fl studio
PHP小志15 小时前
Windows 服务器怎么修改密码和用户名?账户被系统锁定如何解锁
windows
专注VB编程开发20年16 小时前
vb.net datatable新增数据时改用数组缓存
java·linux·windows
仙剑魔尊重楼17 小时前
专业音乐制作软件fl Studio 2025.2.4.5242中文版新功能
windows·音乐·fl studio
rjc_lihui18 小时前
Windows 运程共享linux系统的方法
windows
失忆爆表症18 小时前
01_项目搭建指南:从零开始的 Windows 开发环境配置
windows·postgresql·fastapi·milvus
阿昭L18 小时前
C++异常处理机制反汇编(三):32位下的异常结构分析
c++·windows·逆向工程
梦帮科技1 天前
Node.js配置生成器CLI工具开发实战
前端·人工智能·windows·前端框架·node.js·json