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 程序的健壮性和可读性。

相关推荐
激动的兔子13 小时前
Windows系统的回收站文件加载慢无法删除该如何解决
windows
石像鬼₧魂石15 小时前
Termux ↔ Windows 靶机 反向连接实操命令清单
linux·windows·学习
通往曙光的路上17 小时前
授权vvvvvv
java·开发语言·windows
晨尘光18 小时前
【Windows 下FlatBuffers 编译.fbs文件并应用】
c++·windows
TE-茶叶蛋19 小时前
Windows安装Flutter开发环境
windows·flutter
库库林_沙琪马20 小时前
1、nacos
windows
徐子元竟然被占了!!20 小时前
Linux-top
linux·运维·windows
Bruce_Liuxiaowei21 小时前
Windows系统映像劫持:网络安全中的“李代桃僵”战术
windows·安全·web安全
bleach-21 小时前
内网渗透之横向移动&持久化远程控制篇——利用ipc、sc、schtasks、AT,远程连接的winrm,wmic的使用和定时任务的创建
网络·windows·安全·web安全·网络安全·系统安全·安全威胁分析