本文是笔者系统学习Java泛型的完整笔记涵盖了从基础语法到底层原理的全部知识点。无论你是初次接触泛型的新手还是想查漏补缺的老手相信都能从中受益。一、背景为什么需要泛型在Java 1.52004年引入泛型之前我们是这样写代码的// 没有泛型的时代 public class Box { private Object content; public void setContent(Object content) { this.content content; } public Object getContent() { return content; } } // 使用时的问题 Box box new Box(); box.setContent(hello); // 取出时必须强转且容易出错 String str (String) box.getContent(); // 如果误存入其他类型编译期无法发现 box.setContent(123); String wrong (String) box.getContent(); // 运行时ClassCastException泛型的核心价值痛点泛型解决方案类型不安全编译时类型检查提前发现错误强制转换繁琐消除强制转换代码更简洁代码复用性差类型参数化一套代码服务多种类型泛型的本质是将类型作为参数让代码在编译时就确定类型既保证了类型安全又实现了代码复用。二、泛型类与泛型接口2.1 泛型类的定义与使用// 定义泛型类T是类型参数Type Parameter public class BoxT { private T content; public void setContent(T content) { this.content content; } public T getContent() { return content; } } // 使用泛型类 BoxString stringBox new Box(); // 钻石操作符类型推断 stringBox.setContent(Hello泛型); String str stringBox.getContent(); // 无需强转 // 尝试放入错误类型编译报错 // stringBox.setContent(123); // ❌ 编译错误2.2 泛型接口// 泛型接口 public interface GeneratorT { T generate(); } // 实现方式1实现类保留泛型 public class GenericGeneratorT implements GeneratorT { private T data; public GenericGenerator(T data) { this.data data; } Override public T generate() { return data; } } // 实现方式2实现类指定具体类型 public class StringGenerator implements GeneratorString { Override public String generate() { return Hello; } }2.3 继承中的泛型规则子类继承泛型父类时父类的泛型参数必须落地// 父类 class GenericT { private T data; public T getData() { return data; } } // 方式1子类也保留泛型类型一致 class ChildGeneric1T extends GenericT { } // 方式2子类明确具体类型 class ChildGeneric2 extends GenericString { } // ❌ 错误写法子类不泛型父类类型也不明确 // class ChildGeneric3 extends Generic { } // 编译报错三、泛型方法3.1 什么是泛型方法泛型方法是在方法声明时定义了自己的类型参数的方法与类是否泛型无关。public class Utility { // 泛型方法T声明类型参数位于返回值之前 public static T T getMiddle(T... args) { return args[args.length / 2]; } public static T void printArray(T[] array) { for (T element : array) { System.out.print(element ); } } } // 使用 String middle Utility.getMiddle(A, B, C); // T推断为String Utility.printArray(new Integer[]{1, 2, 3}); // T推断为Integer3.2 为什么必须写T关键问题方法的类型参数必须在使用前声明。// ❌ 编译报错找不到符号 T public static T getMiddle(T... args) { return args[0]; } // ✅ 正确T声明了类型参数 public static T T getMiddle(T... args) { return args[0]; }类比就像使用变量前必须先声明一样使用类型参数前也必须先声明。3.3 泛型方法 vs 使用Object对比项泛型方法使用Object类型安全✅ 编译时检查❌ 运行时才暴露消除强转✅❌ 需要手动强转类型一致性✅ 保证❌ 无法保证// 泛型方法类型安全无需强转 public static T T getFirst(ListT list) { return list.get(0); } // 使用Object需要强转可能出错 public static Object getFirst(List? list) { return list.get(0); } // 调用String s (String) getFirst(list); // 强转有风险3.4 泛型类中的静态方法重要规则静态方法不能使用类的泛型参数。public class BoxT { private T content; // ✅ 非静态方法可以使用 T public T getContent() { return content; } // ❌ 编译报错静态方法不能使用类的泛型参数 // public static void staticMethod(T t) { } // ✅ 静态方法可以有自己的类型参数 public static U void staticMethod(U u) { System.out.println(u); } }原因类的泛型参数在创建对象时确定而静态方法在类加载时就存在生命周期不匹配。四、类型通配符Wildcard4.1 为什么需要通配符泛型是不变invariant的BoxApple和BoxFruit没有继承关系即使Apple extends Fruit。BoxFruit fruitBox new Box(); BoxApple appleBox new Box(); // ❌ 编译报错类型不匹配 // fruitBox appleBox; // 想让一个方法处理所有水果箱子就需要通配符 public static void printBox(BoxFruit box) { } // 只能接收 BoxFruit public static void printBox(Box? box) { } // 可以接收任何 Box4.2 无界通配符?javapublic static void printBox(Box? box) { System.out.println(box.getContent()); // box.setContent(hello); // ❌ 不能写入具体类型除了null } // 可以传入任何类型的Box printBox(new BoxString()); printBox(new BoxInteger());4.3 上界通配符? extends T// Box里装的是Fruit或其子类 public static void printFruit(Box? extends Fruit box) { Fruit f box.getContent(); // ✅ 读取一定是Fruit // box.setContent(new Apple()); // ❌ 不能写入 }规则? extends T→只能读不能写除null。4.4 下界通配符? super Tjava// Box里装的是Apple或其父类 public static void addApple(Box? super Apple box) { box.setContent(new Apple()); // ✅ 可以写入Apple // Apple a box.getContent(); // ❌ 读取只能是Object }规则? super T→只能写读出来是Object。4.5 PECS原则终极口诀Producer Extends, Consumer Super场景推荐写法原因生产者从容器中读取数据? extends T保证读取到的一定是T或其子类消费者向容器中写入数据? super T保证能写入T及其子类既读又写不要用通配符用T需要保持类型一致性4.6Tvs?的区别对比T类型参数?通配符本质声明一个类型变量使用一个未知类型能否多次使用✅ 可以❌ 每次独立作用范围整个类或方法仅当前声明位置能否写入✅ 可以受限取决于上下界// T多次使用保持类型一致 public static T void swap(BoxT box1, BoxT box2) { T temp box1.getContent(); box1.setContent(box2.getContent()); box2.setContent(temp); } // ?每次独立不能多次引用 public static void process(Box? box1, Box? box2) { // ? temp box1.getContent(); // ❌ 编译错误 }五、类型擦除Type Erasure5.1 什么是类型擦除核心定义Java泛型是编译时的概念在运行时泛型类型信息会被编译器擦除替换为原始类型。// 编译时 ListString stringList new ArrayList(); ListInteger integerList new ArrayList(); // 运行时字节码 List stringList new ArrayList(); List integerList new ArrayList(); // 两者在运行时完全相同5.2 为什么设计成擦除式为了向后兼容Java 1.5才引入泛型为了能让旧代码无缝迁移必须让新代码在运行时和旧代码保持一致。5.3 擦除的具体规则擦除规则示例无界类型 → ObjectT→Object有界类型 → 第一个边界T extends Number→Number多个边界 → 第一个边界T extends Number Comparable→Number5.4 擦除带来的限制限制说明变通方案不能用instanceof检查泛型类型list instanceof ListString编译报错只检查原始类型不能new T()运行时T被擦除为Object传入ClassT并反射创建不能new T[]运行时无法确定数组类型使用Array.newInstance()静态上下文不能使用泛型参数生命周期不匹配使用泛型方法5.5 补偿机制桥接方法为了保持多态编译器会自动生成桥接方法Bridge Method// 源码 class MyList implements ListString { public void add(String item) { } } // 擦除后编译器生成桥接方法 class MyList implements List { public void add(String item) { } // 用户方法 public void add(Object item) { // 桥接方法自动生成 add((String) item); } }六、泛型和数组6.1 核心矛盾数组在运行时必须知道具体类型泛型在运行时类型被擦除。→ 两者天然冲突。// ✅ 数组运行时知道类型 String[] strArray new String[10]; Integer[] intArray new Integer[10]; // strArray.getClass() → String[] // intArray.getClass() → Integer[] // ❌ 泛型运行时丢失类型 ListString strList new ArrayList(); ListInteger intList new ArrayList(); // strList.getClass() → ArrayList // intList.getClass() → ArrayList6.2 规则可以声明不能直接创建java// ✅ 可以声明带泛型的数组引用 BoxString[] boxArray; // ❌ 不能直接创建带泛型的数组 // boxArray new BoxString[10]; // 编译报错 // ✅ 可以通过反射创建 boxArray (BoxString[]) new Box[10]; // 有警告但可行 // ✅ 最佳实践使用 Array.newInstance() public static T T[] createArray(ClassT clazz, int size) { SuppressWarnings(unchecked) T[] array (T[]) Array.newInstance(clazz, size); return array; }6.3 为什么Array.newInstance()可以因为通过ClassT对象在运行时获取了具体类型绕过了泛型擦除的限制。七、最佳实践与速查表7.1 泛型使用决策表场景推荐写法示例定义一个容器类泛型类class BoxT方法的返回值和参数需要类型一致泛型方法T T getFirst(ListT)只需要读取数据? extends TList? extends Number只需要写入数据? super TList? super Integer既要读又要写不用通配符用Tvoid swap(BoxT, BoxT)完全不关心类型?List?7.2 常见泛型符号符号含义常用场景TType类型通用类型参数EElement元素集合元素类型KKey键Map的键VValue值Map的值RResult结果返回结果类型?未知类型通配符7.3 重要规则速记静态上下文不能使用类的泛型参数不能创建泛型数组new T[10]不允许不能instanceof泛型类型泛型类型在运行时被擦除PECS原则ProducerextendsConsumersuper子类继承父类时父类的泛型必须落地钻石操作符让类型推断更简洁八、结语泛型是Java语言中一个精妙而强大的设计。它通过编译时类型检查保证了类型安全通过类型擦除实现了向后兼容通过通配符提供了灵活的类型约束。理解泛型的核心在于把握两个对立统一类型参数T与类型通配符?一个用于定义和保持一致性一个用于使用和增加灵活性编译时安全与运行时擦除编译时严格的类型检查运行时零开销的兼容性掌握泛型你的Java代码将更加安全、优雅和通用。
郑州网站建设
网页设计
企业官网