当前位置: 首页 > news >正文

Java-泛型

本篇我们来讲解 Java 泛型~

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

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

  • 定义:在类名后面用尖括号<>声明一个或多个类型参数。这些参数可以在类体中像普通类型一样使用(作为字段类型、方法参数类型、方法返回类型等)。
  • 语法
    public class ClassName<T1, T2, ..., Tn> { // 类体可以使用 T1, T2, ..., Tn }
  • 实例化:创建泛型类的对象时,在类名后的尖括号<>中指定具体的类型参数(称为类型实参,Type Argument)。
    ClassName<具体类型1, 具体类型2, ..., 具体类型n> obj = new ClassName<>();
  • 示例:定义一个简单的泛型Box
    public class Box<T> { private T content; // T 表示某种类型的内容 public void setContent(T content) { this.content = content; } public T getContent() { return content; } }
  • 使用
    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. 泛型接口

  • 定义:与泛型类类似,在接口名后声明类型参数。
  • 语法
    public interface InterfaceName<T1, T2, ..., Tn> { // 接口方法可以使用 T1, T2, ..., Tn }
  • 实现
    • 方式一:实现类在实现接口时指定具体类型。
      public interface Producer<T> { T produce(); } public class StringProducer implements Producer<String> { @Override public String produce() { return "Generated String"; } }
    • 方式二:实现类本身也声明为泛型类,类型参数与接口一致。
      public class GenericProducer<T> implements Producer<T> { @Override public T produce() { // ... 生产 T 类型对象的逻辑 ... return result; } }
      • 使用时再指定具体类型:GenericProducer<Integer> intProducer = new GenericProducer<>();

4. 泛型方法

  • 定义:在方法签名上声明类型参数,该方法可以在不同类型上操作。
  • 语法:在方法的返回类型之前(或修饰符之后)用尖括号<>声明类型参数。
    public <T1, T2, ..., Tn> 返回类型 方法名(参数列表) { // 方法体可以使用 T1, T2, ..., Tn }
  • 特点
    • 类型参数的作用域仅限于该方法本身。
    • 泛型方法可以定义在普通类中,也可以定义在泛型类中(此时,泛型方法的类型参数可以与类的类型参数同名但含义不同)。
    • 编译器通常能根据传入的参数类型推断出类型实参。
  • 示例
    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; } }
  • 使用
    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 之前的代码(非泛型集合类)。
  • 示例
    // 源代码 (编译前) 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或某个公共父类使用),而不关心具体类型时。
      • 示例
        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)。
      • 示例
        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(因为不知道具体父类是什么)。
      • 示例
        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>(类只能有一个且必须在第一个,接口可以有多个)。
  • 示例
    // 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 参数)或工厂模式。
  • 不能用于静态上下文:类的类型参数不能用于静态方法或静态字段,因为静态成员属于类,而类型参数属于实例。
    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
  • 方法重载冲突:类型擦除可能导致两个方法签名在编译后变得相同,引起编译错误。
    public class Example { public void print(List<String> list) { ... } public void print(List<Integer> list) { ... } // 编译错误!擦除后都是 print(List) }

总结

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

http://www.jsqmd.com/news/84188/

相关文章:

  • 北京婚介的狂妄红娘
  • Flutter 与 OpenHarmony 深度融合:实现分布式文件共享与跨设备协同编辑系统
  • SCCLIP
  • 木材碳封存技术:应对气候变化的低科技方案
  • 为何心理学成了“隐形禁忌”?比逻辑学更让人忌惮的觉醒之力
  • Flutter 与 OpenHarmony 深度整合:构建跨设备统一通知中心系统
  • 常用软件工具的使用(2) ---- git 命令进阶 和 github
  • CLIPer
  • 新手必看!第一次装修选对公司,省心攻略全解析! - 品牌测评鉴赏家
  • Flutter 与 OpenHarmony 深度整合:构建跨设备统一剪贴板同步系统
  • NSmartProxy:一款.NET开源、跨平台的内网穿透工具
  • 用PHP8实现斗地主游戏,后端逻辑开发
  • 两个步骤,打包war,tomcat使用war包
  • 2025年12月苏州装修品牌调研:深度剖析盛世和家装饰售后服务 - 品牌测评鉴赏家
  • 新房装修怎么选?十大装企深度测评,帮你找到最优解 - 品牌测评鉴赏家
  • 高活性助眠的睡眠益生菌:科学守护你的深度睡眠 - 品牌排行榜
  • FlaskSession源码解析:从原生到扩展
  • 智能家居组态王6.55脚本动画仿真
  • 新房装修公司怎么选?2025年口碑榜单+避坑指南来了 - 品牌测评鉴赏家
  • idea修改maven的刷新引入依赖快捷键
  • 「旅行商问题 TSP 动态规划 贪心算法 数据结构 Java 代码」
  • SolidWorks装配体坐标轴匹配介绍
  • 2025年12月苏州装修品牌调研:盛世和家装饰口碑与实力分析 - 品牌测评鉴赏家
  • java 设置日期返回格式的几种方式
  • SolidWorks装配体与装配图区别介绍
  • JAVA 中dao层的实体应该属于哪个层次VO,还是DTO,或者其他
  • 第十五节:基于 Redis+MQ+DB实现高并发秒杀下的扣减方案2
  • SolidWorks工程图用途及深入介绍
  • kaggle colab cpu配置
  • Flutter 应用保活与后台任务:在 OpenHarmony 上实现定时上报