<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>训练与减脂笔记</title><description>哑铃减脂完整计划（172cm / 72kg 零基础，含饮食方案、热身、训练 A/B、小空间有氧、一周安排、重量对照、进度追踪），以及 Java 面试复习笔记。</description><link>https://blog-akm.pages.dev/</link><item><title>从 getClass 说起：理解 Class 对象与泛型擦除</title><link>https://blog-akm.pages.dev/notes/01-Java%E5%9F%BA%E7%A1%80/01-%E6%B3%9B%E5%9E%8B/01-%E4%BB%8EgetClass%E8%AF%B4%E8%B5%B7/</link><guid isPermaLink="true">https://blog-akm.pages.dev/notes/01-Java%E5%9F%BA%E7%A1%80/01-%E6%B3%9B%E5%9E%8B/01-%E4%BB%8EgetClass%E8%AF%B4%E8%B5%B7/</guid><description>从 getClass() 切入，讲透 Class 对象、运行时真实类型与泛型擦除的边界，附可运行验证代码与 7 道面试问法。</description><pubDate>Tue, 15 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;所属：Java 面试笔记 / 01-Java基础 / 01-泛型 ｜ 学习计划：D1
前置：无 ｜ 后续：&lt;a href=&quot;/notes/01-Java%E5%9F%BA%E7%A1%80/01-%E6%B3%9B%E5%9E%8B/02-%E6%93%A6%E9%99%A4%E4%B9%8B%E5%90%8E/&quot;&gt;02-擦除之后&lt;/a&gt;（这篇讲「擦除是什么」，那篇讲「擦除之后怎么办」）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;0. 本文要解决什么&lt;/h2&gt;
&lt;p&gt;面试里问泛型擦除，很多人上来就背&quot;运行时被擦除&quot;。但如果你连 &lt;code&gt;getClass()&lt;/code&gt; 返回的是什么、&lt;code&gt;Class&lt;/code&gt; 对象是什么都说不清，后面全是空中楼阁。&lt;/p&gt;
&lt;p&gt;本文从 &lt;code&gt;getClass()&lt;/code&gt; 切入，把下面这条链讲透：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;getClass() → Class 对象 → 运行时真实类型 → 泛型擦除
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;学完你应该能回答：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getClass()&lt;/code&gt; 返回什么？&lt;/li&gt;
&lt;li&gt;同一个类的 &lt;code&gt;Class&lt;/code&gt; 对象是不是同一个？&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getClass()&lt;/code&gt;、&lt;code&gt;.class&lt;/code&gt;、&lt;code&gt;instanceof&lt;/code&gt; 有什么区别？&lt;/li&gt;
&lt;li&gt;为什么 &lt;code&gt;List&amp;lt;String&amp;gt;&lt;/code&gt; 和 &lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt; 的 &lt;code&gt;getClass()&lt;/code&gt; 相等？&lt;/li&gt;
&lt;li&gt;泛型信息到底还在不在？&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;1. getClass() 是什么&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;getClass()&lt;/code&gt; 定义在 &lt;code&gt;Object&lt;/code&gt; 里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final native Class&amp;lt;?&amp;gt; getClass();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拆开看：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;修饰&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;public&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;所有类都能调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;final&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不能被子类重写&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;native&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;由 JVM 底层实现&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;返回 &lt;code&gt;Class&amp;lt;?&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回运行时真实类型对应的 Class 对象&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String s = &quot;abc&quot;;
Class&amp;lt;?&amp;gt; c = s.getClass();
System.out.println(c.getName()); // java.lang.String
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;一句话&lt;/strong&gt;：&lt;code&gt;getClass()&lt;/code&gt; 返回对象在运行时的真实类型对应的 &lt;code&gt;Class&lt;/code&gt; 对象。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Class 对象是什么&lt;/h2&gt;
&lt;p&gt;JVM 加载每个类时，会在方法区（JDK 8+ 是 Metaspace）里为它建立一份类元数据，同时在堆上创建一个 &lt;code&gt;java.lang.Class&lt;/code&gt; 对象作为这份元数据的入口。我们平时拿到的就是这个 &lt;code&gt;Class&lt;/code&gt; 对象，它对外暴露：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类名、包名&lt;/li&gt;
&lt;li&gt;父类、实现的接口&lt;/li&gt;
&lt;li&gt;字段、方法、构造器&lt;/li&gt;
&lt;li&gt;注解&lt;/li&gt;
&lt;li&gt;泛型签名（部分）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以把它理解成：&lt;strong&gt;某个类在内存里的说明书&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class&amp;lt;?&amp;gt; c = String.class;
System.out.println(c.getName());       // java.lang.String
System.out.println(c.getSimpleName()); // String
System.out.println(c.getSuperclass()); // class java.lang.Object
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Class&lt;/code&gt; 本身也是类，全名 &lt;code&gt;java.lang.Class&lt;/code&gt;。&lt;code&gt;Class&lt;/code&gt; 对象也是对象，只不过它代表的是&quot;类&quot;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;面试加分点：严格说，&lt;strong&gt;类元数据在 Metaspace 的 &lt;code&gt;InstanceKlass&lt;/code&gt; 里，堆上的 &lt;code&gt;Class&lt;/code&gt; 对象只是它的镜像&lt;/strong&gt;（HotSpot 中 &lt;code&gt;Class&lt;/code&gt; 对象的 &lt;code&gt;_klass&lt;/code&gt; 指针回指 &lt;code&gt;InstanceKlass&lt;/code&gt;）。说&quot;Class 对象保存在方法区&quot;是不准确的——它是堆对象，强引用着方法区的元数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 同一个类只有一个 Class 对象&lt;/h2&gt;
&lt;p&gt;在&lt;strong&gt;同一个类加载器&lt;/strong&gt;下，一个类只会对应一个 &lt;code&gt;Class&lt;/code&gt; 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String s1 = &quot;a&quot;;
String s2 = &quot;b&quot;;
System.out.println(s1.getClass() == s2.getClass()); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;String s = &quot;abc&quot;;
System.out.println(s.getClass() == String.class); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;补充：不同类加载器加载同一个类名，会得到不同的 &lt;code&gt;Class&lt;/code&gt; 对象。这是类加载隔离的基础，后面 JVM 篇再展开。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;4. getClass()、.class、instanceof 的区别&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;String s = &quot;abc&quot;;

Class&amp;lt;?&amp;gt; c1 = s.getClass();      // 运行时通过对象获取
Class&amp;lt;?&amp;gt; c2 = String.class;      // 编译期类字面量
boolean b = s instanceof String; // 判断是不是某类型（含子类）
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;写法&lt;/th&gt;
&lt;th&gt;时机&lt;/th&gt;
&lt;th&gt;判断依据&lt;/th&gt;
&lt;th&gt;是否含子类&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;obj.getClass()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;运行时&lt;/td&gt;
&lt;td&gt;真实类型&lt;/td&gt;
&lt;td&gt;否，精确匹配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;X.class&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;编译期&lt;/td&gt;
&lt;td&gt;类字面量&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;obj instanceof X&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;运行时&lt;/td&gt;
&lt;td&gt;是否属于 X 或子类&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;关键对比：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Object o = new String(&quot;abc&quot;);

System.out.println(o instanceof String);          // true
System.out.println(o.getClass() == String.class); // true
System.out.println(o.getClass() == Object.class); // false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;o&lt;/code&gt; 的静态类型是 &lt;code&gt;Object&lt;/code&gt;，运行时真实类型是 &lt;code&gt;String&lt;/code&gt;。&lt;code&gt;getClass()&lt;/code&gt; 看运行时真实类型，所以返回 &lt;code&gt;String.class&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 泛型擦除&lt;/h2&gt;
&lt;p&gt;Java 泛型是&lt;strong&gt;编译期&lt;/strong&gt;的。编译完之后，泛型参数被擦除，运行时不存在。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;String&amp;gt; a = new ArrayList&amp;lt;&amp;gt;();
List&amp;lt;Integer&amp;gt; b = new ArrayList&amp;lt;&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译后，字节码里都是原始类型（raw type）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List a = new ArrayList();
List b = new ArrayList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;擦除规则（&lt;strong&gt;擦到第一个上界&lt;/strong&gt;）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;声明&lt;/th&gt;
&lt;th&gt;擦除后&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Object&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;T extends Number&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Number&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;T extends Comparable&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Comparable&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;class Box&amp;lt;T&amp;gt; { T value; }
// 擦除后 ≈ class Box { Object value; }

class NumBox&amp;lt;T extends Number&amp;gt; { T value; }
// 擦除后 ≈ class NumBox { Number value; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;一句话&lt;/strong&gt;：泛型是给编译器看的，运行时不存在。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 为什么 &lt;code&gt;List&amp;lt;String&amp;gt;&lt;/code&gt; 和 &lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt; 的 getClass() 相等&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;String&amp;gt; a = new ArrayList&amp;lt;&amp;gt;();
List&amp;lt;Integer&amp;gt; b = new ArrayList&amp;lt;&amp;gt;();

System.out.println(a.getClass());                 // class java.util.ArrayList
System.out.println(b.getClass());                 // class java.util.ArrayList
System.out.println(a.getClass() == b.getClass()); // true
System.out.println(a.getClass() == List.class);   // false
System.out.println(a instanceof List);            // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逐条解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;a&lt;/code&gt;、&lt;code&gt;b&lt;/code&gt; 都是 &lt;code&gt;new ArrayList&amp;lt;&amp;gt;()&lt;/code&gt; 创建，运行时真实类型都是 &lt;code&gt;ArrayList&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;同一个类加载器下，&lt;code&gt;ArrayList&lt;/code&gt; 只有一个 &lt;code&gt;Class&lt;/code&gt; 对象。&lt;/li&gt;
&lt;li&gt;所以 &lt;code&gt;a.getClass() == b.getClass()&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a.getClass()&lt;/code&gt; 是 &lt;code&gt;ArrayList.class&lt;/code&gt;，不是 &lt;code&gt;List.class&lt;/code&gt;，所以和 &lt;code&gt;List.class&lt;/code&gt; 不等。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a instanceof List&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;，因为 &lt;code&gt;ArrayList&lt;/code&gt; 实现了 &lt;code&gt;List&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;核心结论&lt;/strong&gt;：泛型参数不影响运行时类型，&lt;code&gt;getClass()&lt;/code&gt; 只看真实类。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 泛型信息真的全没了吗&lt;/h2&gt;
&lt;p&gt;不是。分两个层面：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层面&lt;/th&gt;
&lt;th&gt;泛型信息&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;对象实例&lt;/td&gt;
&lt;td&gt;没有&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;类文件声明（字段、方法、父类）&lt;/td&gt;
&lt;td&gt;可能保留在 &lt;code&gt;Signature&lt;/code&gt; 属性里&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;反射读取声明处&lt;/td&gt;
&lt;td&gt;可以读到&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Demo {
    private List&amp;lt;String&amp;gt; names;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Field f = Demo.class.getDeclaredField(&quot;names&quot;);

System.out.println(f.getType());        // interface java.util.List
System.out.println(f.getGenericType()); // java.util.List&amp;lt;java.lang.String&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getType()&lt;/code&gt;：擦除后的原始类型 &lt;code&gt;List&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getGenericType()&lt;/code&gt;：带泛型签名的 &lt;code&gt;ParameterizedType&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;进一步拿泛型参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (f.getGenericType() instanceof ParameterizedType) {
    ParameterizedType pt = (ParameterizedType) f.getGenericType();
    Type[] args = pt.getActualTypeArguments();
    System.out.println(args[0]); // class java.lang.String
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;JDK 16+ 可以写成 &lt;code&gt;instanceof ParameterizedType pt&lt;/code&gt; 的模式匹配，省掉强转。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;这和 Spring 依赖注入是什么关系（一个常见错误说法）&lt;/h3&gt;
&lt;p&gt;不少笔记写&quot;Spring 通过 &lt;code&gt;getGenericType&lt;/code&gt; 知道要注入 &lt;code&gt;UserService&lt;/code&gt;&quot;，&lt;strong&gt;这是错的&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Autowired
private UserService userService; // 这个字段根本没有泛型参数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里 Spring 用的是 &lt;code&gt;Field.getType()&lt;/code&gt;（即 &lt;code&gt;UserService.class&lt;/code&gt;）做 byType 匹配，&lt;strong&gt;和 &lt;code&gt;getGenericType&lt;/code&gt; 无关&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;getGenericType&lt;/code&gt; 真正起作用的场景是&lt;strong&gt;字段类型本身带类型参数&lt;/strong&gt;时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Autowired
private List&amp;lt;UserService&amp;gt; services;      // 要先知道元素类型，才能筛出候选 bean

@Autowired
private Repository&amp;lt;User&amp;gt; userRepository; // Spring Data 按泛型生成代理

abstract class BaseService&amp;lt;T&amp;gt; {          // 泛型父类声明的字段，
    @Autowired                           // 需要沿继承链解析 T
    protected T dao;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些场景 Spring 走的是 &lt;code&gt;ResolvableType&lt;/code&gt;：内部会调 &lt;code&gt;getGenericType()&lt;/code&gt;，并把父类/接口上的类型变量一并解析出来，而不是简单看 &lt;code&gt;getType()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一句话&lt;/strong&gt;：&lt;code&gt;getType()&lt;/code&gt; 拿擦除后的类型，&lt;code&gt;getGenericType()&lt;/code&gt; 拿带签名的类型；Spring 只在需要解析类型参数时才用后者。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 为什么不能 new T[]&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Box&amp;lt;T&amp;gt; {
    T[] arr = new T[10]; // 编译不通过
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因有两层：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数组是具体化类型（reified）&lt;/strong&gt;：数组在运行时必须知道元素类型，才能做存取检查（&lt;code&gt;ArrayStoreException&lt;/code&gt; 就是这么来的）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;泛型被擦除&lt;/strong&gt;：运行时不知道 &lt;code&gt;T&lt;/code&gt; 是什么，JVM 无法创建&quot;类型正确&quot;的数组。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;而且数组是&lt;strong&gt;协变&lt;/strong&gt;的（&lt;code&gt;String[]&lt;/code&gt; 是 &lt;code&gt;Object[]&lt;/code&gt; 的子类型），泛型是&lt;strong&gt;不变&lt;/strong&gt;的，两者混用会破坏类型安全，所以 Java 直接从编译期禁止。&lt;/p&gt;
&lt;p&gt;硬写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;T[] arr = (T[]) new Object[10]; // 编译警告，运行时堆污染
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这叫&lt;strong&gt;堆污染&lt;/strong&gt;。运行时它只是 &lt;code&gt;Object[]&lt;/code&gt;，你往里放 &lt;code&gt;String&lt;/code&gt;，别人当成 &lt;code&gt;Integer[]&lt;/code&gt; 用就会 &lt;code&gt;ClassCastException&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static &amp;lt;T&amp;gt; T[] wrong() {
    return (T[]) new Object[]{&quot;a&quot;, &quot;b&quot;}; // 擦除后退化为 Object[]
}

Integer[] ints = wrong(); // 运行时 ClassCastException
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正确做法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;T[] arr = (T[]) Array.newInstance(clazz, 10);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;clazz&lt;/code&gt; 是 &lt;code&gt;Class&amp;lt;T&amp;gt;&lt;/code&gt;，从外部传入。这也是很多框架要求传 &lt;code&gt;Class&lt;/code&gt; 对象的原因。&lt;/p&gt;
&lt;p&gt;另外，可变参数 &lt;code&gt;T...&lt;/code&gt; 底层也是数组，编译器同样会给警告；&lt;code&gt;@SafeVarargs&lt;/code&gt; 能抑制警告的前提是&lt;strong&gt;方法不往这个数组里写东西&lt;/strong&gt;。完整实验见 &lt;a href=&quot;/notes/01-Java%E5%9F%BA%E7%A1%80/01-%E6%B3%9B%E5%9E%8B/02-%E6%93%A6%E9%99%A4%E4%B9%8B%E5%90%8E/&quot;&gt;02-擦除之后&lt;/a&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;9. 擦除带来的两个连锁反应&lt;/h2&gt;
&lt;h3&gt;9.1 重载冲突&lt;/h3&gt;
&lt;p&gt;擦除后签名相同，编译直接报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void m(List&amp;lt;String&amp;gt; a) {}
void m(List&amp;lt;Integer&amp;gt; b) {} // 编译错误：name clash，擦除后都是 m(List)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;想区分只能换方法名，或者把参数容器换成不同类型。&lt;/p&gt;
&lt;h3&gt;9.2 桥方法（bridge method）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Node&amp;lt;T&amp;gt; {
    public T get() { return null; }
}

class StringNode extends Node&amp;lt;String&amp;gt; {
    @Override
    public String get() { return &quot;s&quot;; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;擦除后父类方法签名是 &lt;code&gt;Object get()&lt;/code&gt;。为了让&quot;父类引用调用子类实现&quot;的多态成立，编译器会在子类合成一个桥方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class StringNode extends Node&amp;lt;String&amp;gt; {
    public String get() { return &quot;s&quot;; }

    // 编译器合成，字节码里真实存在
    // public Object get() { return this.get(); }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 &lt;code&gt;javap -c -p StringNode&lt;/code&gt; 能看到两个 &lt;code&gt;get&lt;/code&gt;。这解释了&quot;重写后反射 &lt;code&gt;getDeclaredMethods()&lt;/code&gt; 为什么会多一个方法&quot;，也是&quot;泛型擦除后多态仍然有效&quot;的底层原因。字节码长什么样、&lt;code&gt;Method.isBridge()&lt;/code&gt; 怎么用，见 &lt;a href=&quot;/notes/01-Java%E5%9F%BA%E7%A1%80/01-%E6%B3%9B%E5%9E%8B/02-%E6%93%A6%E9%99%A4%E4%B9%8B%E5%90%8E/&quot;&gt;02-擦除之后&lt;/a&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;10. 完整验证代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;

public class ErasureDemo {

    public static void main(String[] args) throws Exception {
        test1();
        test2();
        test3();
        test4();
    }

    // getClass / .class / instanceof
    static void test1() {
        Object o = new String(&quot;abc&quot;);
        System.out.println(o instanceof String);          // true
        System.out.println(o.getClass() == String.class); // true
        System.out.println(o.getClass() == Object.class); // false
    }

    // 泛型擦除
    static void test2() {
        List&amp;lt;String&amp;gt; a = new ArrayList&amp;lt;&amp;gt;();
        List&amp;lt;Integer&amp;gt; b = new ArrayList&amp;lt;&amp;gt;();

        System.out.println(a.getClass());                 // class java.util.ArrayList
        System.out.println(b.getClass());                 // class java.util.ArrayList
        System.out.println(a.getClass() == b.getClass()); // true
        System.out.println(a.getClass() == List.class);   // false
        System.out.println(a instanceof List);            // true
    }

    // 反射读取声明处泛型
    public static class Demo {
        private List&amp;lt;String&amp;gt; names;
    }

    static void test3() throws Exception {
        Field f = Demo.class.getDeclaredField(&quot;names&quot;);
        System.out.println(f.getType());        // interface java.util.List
        System.out.println(f.getGenericType()); // java.util.List&amp;lt;java.lang.String&amp;gt;

        if (f.getGenericType() instanceof ParameterizedType pt) {
            Type[] args = pt.getActualTypeArguments();
            System.out.println(args[0]); // class java.lang.String
        }
    }

    // 堆污染：能编译，运行时才炸
    @SuppressWarnings(&quot;unchecked&quot;)
    static &amp;lt;T&amp;gt; T[] wrong() {
        return (T[]) new Object[]{&quot;a&quot;, &quot;b&quot;};
    }

    static void test4() {
        try {
            Integer[] ints = wrong();
            System.out.println(ints[0]);
        } catch (ClassCastException e) {
            System.out.println(&quot;ClassCastException: &quot; + e.getMessage());
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;javac -Xlint:unchecked ErasureDemo.java&lt;/code&gt; 会提示 &lt;code&gt;test4&lt;/code&gt; 里的堆污染。&lt;code&gt;test1&lt;/code&gt; 里的 &lt;code&gt;new String(&quot;abc&quot;)&lt;/code&gt; 只是为了制造&quot;静态类型 ≠ 运行时类型&quot;的对比，真实代码里不要这么写。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;11. 面试问法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：&lt;code&gt;getClass()&lt;/code&gt; 返回什么？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;返回对象运行时真实类型对应的 &lt;code&gt;Class&lt;/code&gt; 对象。同一个类加载器下，一个类只有一个 &lt;code&gt;Class&lt;/code&gt; 对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q2：&lt;code&gt;getClass()&lt;/code&gt;、&lt;code&gt;.class&lt;/code&gt;、&lt;code&gt;instanceof&lt;/code&gt; 有什么区别？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;getClass()&lt;/code&gt; 运行时通过对象拿真实类型，精确匹配、不含子类；&lt;code&gt;.class&lt;/code&gt; 是编译期类字面量；&lt;code&gt;instanceof&lt;/code&gt; 运行时判断是否属于某类型或子类。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q3：为什么 &lt;code&gt;List&amp;lt;String&amp;gt;&lt;/code&gt; 和 &lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt; 的 &lt;code&gt;getClass()&lt;/code&gt; 相等？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;泛型编译期被擦除，运行时 &lt;code&gt;a&lt;/code&gt;、&lt;code&gt;b&lt;/code&gt; 都是 &lt;code&gt;ArrayList&lt;/code&gt; 实例；同一个类加载器下 &lt;code&gt;ArrayList&lt;/code&gt; 只有一个 &lt;code&gt;Class&lt;/code&gt; 对象，所以相等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q4：泛型信息运行时真的没有吗？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对象实例上没有。但字段、方法、父类声明处的泛型签名保留在 Class 文件的 &lt;code&gt;Signature&lt;/code&gt; 属性里，通过 &lt;code&gt;getGenericType()&lt;/code&gt; 等反射 API 可以读到。注意：Spring 的普通字段注入用的是 &lt;code&gt;getType()&lt;/code&gt;，不是 &lt;code&gt;getGenericType()&lt;/code&gt;；后者只在解析类型参数（&lt;code&gt;List&amp;lt;UserService&amp;gt;&lt;/code&gt;、&lt;code&gt;Repository&amp;lt;User&amp;gt;&lt;/code&gt;、泛型父类字段）时才用得上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q5：为什么不能 &lt;code&gt;new T[]&lt;/code&gt;？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;数组是具体化类型，运行时需要元素类型来做存取检查；泛型被擦除，运行时不知道 &lt;code&gt;T&lt;/code&gt;。要创建泛型数组得用 &lt;code&gt;Array.newInstance(clazz, n)&lt;/code&gt; 显式传 &lt;code&gt;Class&lt;/code&gt;，否则 &lt;code&gt;(T[]) new Object[n]&lt;/code&gt; 只是把风险推迟成堆污染。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q6：Java 为什么要擦除泛型？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;向后兼容。泛型是 Java 5 引入的，为了不改 JVM 指令集、让新代码能和老库（&lt;code&gt;List&lt;/code&gt;、&lt;code&gt;Collection&lt;/code&gt; 这类没有泛型的旧字节码）在同一套类型系统里互操作，语言层选择了&quot;编译期检查 + 运行期擦除&quot;。代价就是运行时拿不到泛型实参，才会有 &lt;code&gt;new T[]&lt;/code&gt; 被禁止、重载冲突、需要桥方法这些现象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q7：擦除带来了哪些连锁问题？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;四类：① 运行时无法判断泛型实参（&lt;code&gt;instanceof List&amp;lt;String&amp;gt;&lt;/code&gt; 非法）；② 不能创建泛型数组（&lt;code&gt;new T[]&lt;/code&gt;）；③ 擦除后签名相同导致重载冲突（&lt;code&gt;m(List&amp;lt;String&amp;gt;)&lt;/code&gt; 与 &lt;code&gt;m(List&amp;lt;Integer&amp;gt;)&lt;/code&gt; 不能共存）；④ 子类重写泛型方法时编译器要合成桥方法维持多态。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;12. 小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getClass()&lt;/code&gt; 返回运行时真实类型的 &lt;code&gt;Class&lt;/code&gt; 对象；同一个类加载器下，一个类只有一个 &lt;code&gt;Class&lt;/code&gt; 对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Class&lt;/code&gt; 对象是堆对象，类元数据本体在 Metaspace 的 &lt;code&gt;InstanceKlass&lt;/code&gt; 里。&lt;/li&gt;
&lt;li&gt;泛型是编译期的，运行时被擦除，擦到第一个上界。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;List&amp;lt;String&amp;gt;&lt;/code&gt; 和 &lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt; 的实例运行时都是 &lt;code&gt;ArrayList&lt;/code&gt;，&lt;code&gt;getClass()&lt;/code&gt; 相等。&lt;/li&gt;
&lt;li&gt;声明处的泛型签名保留在 Class 文件里，反射可读；但 Spring 普通字段注入用的是 &lt;code&gt;getType()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;不能 &lt;code&gt;new T[]&lt;/code&gt;，要 &lt;code&gt;Array.newInstance(clazz, n)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;擦除的连锁反应：重载冲突、桥方法、堆污染、无法 &lt;code&gt;instanceof&lt;/code&gt; 泛型实参。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;记忆钩子一句话：&lt;strong&gt;&quot;泛型只活在编译期，&lt;code&gt;Class&lt;/code&gt; 对象只认运行时真实类型。&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;13. 下一步&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 手跑第 10 节代码（含 &lt;code&gt;javac -Xlint:unchecked&lt;/code&gt;），确认全部输出。&lt;/li&gt;
&lt;li&gt;[ ] 用 &lt;code&gt;javap -c -p&lt;/code&gt; 看一眼桥方法的字节码。&lt;/li&gt;
&lt;li&gt;[ ] 闭卷复述第 11 节 7 个问题。&lt;/li&gt;
&lt;li&gt;[ ] 写下一篇：&lt;a href=&quot;/notes/01-Java%E5%9F%BA%E7%A1%80/01-%E6%B3%9B%E5%9E%8B/02-%E6%93%A6%E9%99%A4%E4%B9%8B%E5%90%8E/&quot;&gt;02-擦除之后&lt;/a&gt;——桥方法字节码、重载冲突、堆污染完整实验、泛型数组与 &lt;code&gt;@SafeVarargs&lt;/code&gt;、&lt;code&gt;instanceof&lt;/code&gt; 泛型实参、静态成员为什么不能用 &lt;code&gt;T&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>akm</author></item><item><title>擦除之后：桥方法、重载冲突与堆污染</title><link>https://blog-akm.pages.dev/notes/01-Java%E5%9F%BA%E7%A1%80/01-%E6%B3%9B%E5%9E%8B/02-%E6%93%A6%E9%99%A4%E4%B9%8B%E5%90%8E/</link><guid isPermaLink="true">https://blog-akm.pages.dev/notes/01-Java%E5%9F%BA%E7%A1%80/01-%E6%B3%9B%E5%9E%8B/02-%E6%93%A6%E9%99%A4%E4%B9%8B%E5%90%8E/</guid><description>泛型擦除落地之后的六个实际问题——编译器插入的 checkcast、桥方法与 isBridge、重载冲突、堆污染完整实验、泛型数组的正确创建、instanceof 泛型实参与静态成员。</description><pubDate>Tue, 15 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;所属：Java 面试笔记 / 01-Java基础 / 01-泛型 ｜ 学习计划：D1
前置：&lt;a href=&quot;/notes/01-Java%E5%9F%BA%E7%A1%80/01-%E6%B3%9B%E5%9E%8B/01-%E4%BB%8EgetClass%E8%AF%B4%E8%B5%B7/&quot;&gt;01-从getClass说起&lt;/a&gt; ｜ 后续：&lt;a href=&quot;/notes/01-Java%E5%9F%BA%E7%A1%80/01-%E6%B3%9B%E5%9E%8B/03-%E9%80%9A%E9%85%8D%E7%AC%A6%E4%B8%8EPECS/&quot;&gt;03-通配符与PECS&lt;/a&gt;（这篇讲擦除的补偿机制，那篇讲编译期怎么用通配符表达读写方向）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;0. 本文要解决什么&lt;/h2&gt;
&lt;p&gt;上一篇讲的是&lt;strong&gt;擦除是什么&lt;/strong&gt;：泛型只活在编译期，运行时 &lt;code&gt;List&amp;lt;String&amp;gt;&lt;/code&gt; 和 &lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt; 都是 &lt;code&gt;List&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这一篇讲&lt;strong&gt;擦除之后怎么办&lt;/strong&gt;。既然运行时没有泛型信息，那下面这些事编译器是怎么兜住的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;list.get(0)&lt;/code&gt; 返回的是 &lt;code&gt;Object&lt;/code&gt;，为什么能直接赋给 &lt;code&gt;String&lt;/code&gt;？&lt;/li&gt;
&lt;li&gt;子类重写了泛型方法，多态为什么还能成立？&lt;/li&gt;
&lt;li&gt;为什么两个只差泛型参数的方法不能重载？&lt;/li&gt;
&lt;li&gt;堆污染的 &lt;code&gt;ClassCastException&lt;/code&gt; 到底抛在哪里？&lt;/li&gt;
&lt;li&gt;泛型数组到底怎么创建才是对的？&lt;/li&gt;
&lt;li&gt;为什么 &lt;code&gt;instanceof List&amp;lt;String&amp;gt;&lt;/code&gt; 编译不过？&lt;/li&gt;
&lt;li&gt;静态字段/静态方法为什么不能用类的泛型参数？&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 擦除之后，编译器在背后插了 checkcast&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;static String first(List&amp;lt;String&amp;gt; list) {
    return list.get(0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;擦除后 &lt;code&gt;get&lt;/code&gt; 的签名是 &lt;code&gt;Object get(int)&lt;/code&gt;，那这一行凭什么能编译通过？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;javac AfterErasureDemo.java
javap -c -p AfterErasureDemo
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;  static java.lang.String first(java.util.List&amp;lt;java.lang.String&amp;gt;);
    Code:
       0: aload_0
       1: iconst_0
       2: invokeinterface #31,  2           // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
       7: checkcast     #37                 // class java/lang/String
      10: areturn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（常量池编号随文件内容变化，看 &lt;code&gt;checkcast&lt;/code&gt; 那一行即可。）&lt;/p&gt;
&lt;p&gt;答案在 &lt;code&gt;checkcast&lt;/code&gt;：&lt;strong&gt;编译器在每个需要具体类型的位置自动插入类型检查指令&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以泛型的类型安全是两层：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层&lt;/th&gt;
&lt;th&gt;做什么&lt;/th&gt;
&lt;th&gt;谁保证&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;编译期&lt;/td&gt;
&lt;td&gt;检查你写的类型对不对&lt;/td&gt;
&lt;td&gt;javac&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;运行期&lt;/td&gt;
&lt;td&gt;在取值处插入 &lt;code&gt;checkcast&lt;/code&gt; 兜底&lt;/td&gt;
&lt;td&gt;编译器生成的字节码&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这也解释了后面堆污染为什么&quot;编译能过、运行才炸&quot;——炸的就是这条 &lt;code&gt;checkcast&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 桥方法：多态为什么还能成立&lt;/h2&gt;
&lt;h3&gt;2.1 问题从哪来&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Node&amp;lt;T&amp;gt; {
    public T get() { return null; }
}

class StringNode extends Node&amp;lt;String&amp;gt; {
    @Override
    public String get() { return &quot;s&quot;; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;擦除后：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;父类方法：&lt;code&gt;Object get()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;子类方法：&lt;code&gt;String get()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是两个&lt;strong&gt;签名不同&lt;/strong&gt;的方法。那么 &lt;code&gt;Node&amp;lt;String&amp;gt; n = new StringNode(); n.get();&lt;/code&gt; 是怎么调到子类实现的？按方法表（vtable）找，父类那格存的是 &lt;code&gt;get()Ljava/lang/Object;&lt;/code&gt;，而子类只提供了 &lt;code&gt;get()Ljava/lang/String;&lt;/code&gt;，对不上，多态直接失效。&lt;/p&gt;
&lt;h3&gt;2.2 编译器的解法：合成桥方法&lt;/h3&gt;
&lt;p&gt;编译器在子类里额外生成一个方法，把调用转发给真正的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class StringNode extends Node&amp;lt;String&amp;gt; {
    public String get() { return &quot;s&quot;; }

    // 编译器合成，源码里看不到，字节码里真实存在
    // public Object get() { return this.get(); }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;javap -c -p StringNode
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class StringNode extends Node&amp;lt;java.lang.String&amp;gt; {
  public java.lang.String get();
    Code:
       0: ldc           #7                  // String s
       2: areturn

  public java.lang.Object get();
    Code:
       0: aload_0
       1: invokevirtual #9                  // Method get:()Ljava/lang/String;
       4: areturn
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面第二个 &lt;code&gt;get()&lt;/code&gt;——参数是 &lt;code&gt;Object&lt;/code&gt;、返回 &lt;code&gt;Object&lt;/code&gt; 的那个——就是编译器合成的桥方法。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;javap&lt;/code&gt; 默认不显示 &lt;code&gt;ACC_BRIDGE&lt;/code&gt; 标志，加 &lt;code&gt;-v&lt;/code&gt; 能看到 &lt;code&gt;flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;2.3 怎么在代码里识别&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;for (Method m : StringNode.class.getDeclaredMethods()) {
    System.out.println(m + &quot; bridge=&quot; + m.isBridge() + &quot; synthetic=&quot; + m.isSynthetic());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public java.lang.String StringNode.get() bridge=false synthetic=false
public java.lang.Object StringNode.get() bridge=true synthetic=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;实用结论&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;反射遍历方法时会多出一个桥方法，统计&quot;类有几个方法&quot;时别被它骗了。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getMethod(&quot;get&quot;)&lt;/code&gt; / &lt;code&gt;getDeclaredMethod(&quot;get&quot;)&lt;/code&gt; 在桥方法存在时行为容易踩坑，框架里普遍用 &lt;code&gt;Method.isBridge()&lt;/code&gt; 过滤，Spring 还有专门的 &lt;code&gt;BridgeMethodResolver&lt;/code&gt; 来从桥方法找回原始方法。&lt;/li&gt;
&lt;li&gt;桥方法不是泛型独有的：&lt;strong&gt;协变返回类型&lt;/strong&gt;（&lt;code&gt;Object clone()&lt;/code&gt; → &lt;code&gt;Person clone()&lt;/code&gt;）同样会生成桥方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 重载冲突：擦除后签名撞车&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;void m(List&amp;lt;String&amp;gt; a) {}
void m(List&amp;lt;Integer&amp;gt; b) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name clash: m(java.util.List&amp;lt;java.lang.Integer&amp;gt;) in Demo and
m(java.util.List&amp;lt;java.lang.String&amp;gt;) in Demo have the same erasure
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重载的判定依据是&lt;strong&gt;擦除后的签名&lt;/strong&gt;（方法名 + 参数类型的擦除结果），上面两个擦除后都是 &lt;code&gt;m(List)&lt;/code&gt;，所以不构成重载，而是重复定义。&lt;/p&gt;
&lt;p&gt;对比着记：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void m(List&amp;lt;String&amp;gt; a) {}
void m(Collection&amp;lt;String&amp;gt; b) {}   // 合法：擦除后是 List 和 Collection，不同
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;想表达&quot;同一件事、不同泛型参数&quot;时，只能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;换方法名（&lt;code&gt;mList&lt;/code&gt; / &lt;code&gt;mSet&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;换参数容器（&lt;code&gt;List&lt;/code&gt; vs &lt;code&gt;Set&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;换参数个数或位置，把泛型参数挤到别的地方去。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这也是为什么很多 API 里会出现 &lt;code&gt;of&lt;/code&gt; / &lt;code&gt;ofList&lt;/code&gt; 这种看起来多余的命名——不是作者懒，是签名撞了。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 堆污染：异常抛在&quot;读&quot;的地方，不是&quot;写&quot;的地方&lt;/h2&gt;
&lt;p&gt;01 里只说了&quot;堆污染&quot;这个词，这里做完整实验。&lt;/p&gt;
&lt;h3&gt;4.1 最小复现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;static void testHeapPollution() {
    List&amp;lt;String&amp;gt; strings = new ArrayList&amp;lt;&amp;gt;();

    List raw = strings;        // 原始类型，绕开编译期检查
    raw.add(123);              // ① 写入成功，这里不报错

    String s = strings.get(0); // ② ClassCastException 在这里抛
    System.out.println(s);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Exception in thread &quot;main&quot; java.lang.ClassCastException:
    class java.lang.Integer cannot be cast to class java.lang.String
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键点&lt;/strong&gt;：污染发生在 ①，但异常抛在 ②。因为 &lt;code&gt;add&lt;/code&gt; 的擦除签名是 &lt;code&gt;add(Object)&lt;/code&gt;，放什么进去 JVM 都不管；真正做类型检查的是编译器插在 &lt;code&gt;get&lt;/code&gt; 之后的那条 &lt;code&gt;checkcast&lt;/code&gt;（见 §1）。&lt;/p&gt;
&lt;p&gt;所以 &lt;code&gt;-Xlint:unchecked&lt;/code&gt; 警告不要关：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;javac -Xlint:unchecked AfterErasureDemo.java
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;AfterErasureDemo.java:43: 警告: [unchecked] 对作为原始类型List的成员的add(E)的调用未经过检查
        raw.add(123);                  // ① 成功，这里不报错
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（第 8 节的完整代码不再用 &lt;code&gt;@SuppressWarnings&lt;/code&gt; 把警告压掉，所以读者照抄命令就能看到同一条警告；示例里只把 &lt;code&gt;broken(T...)&lt;/code&gt; 那条 vararg 警告留在第 8 节说明。）&lt;/p&gt;
&lt;h3&gt;4.2 换个马甲：方法参数传原始类型&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;static void fill(List&amp;lt;String&amp;gt; list) {
    ((List) list).add(123);   // 编译警告，但编译通过
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用方完全看不出 &lt;code&gt;fill&lt;/code&gt; 会往 &lt;code&gt;List&amp;lt;String&amp;gt;&lt;/code&gt; 里塞 &lt;code&gt;Integer&lt;/code&gt;，坑就是这么埋下的。&lt;/p&gt;
&lt;h3&gt;4.3 可变参数与 &lt;code&gt;@SafeVarargs&lt;/code&gt; 的边界&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 安全：只读这个数组
@SafeVarargs
static &amp;lt;T&amp;gt; List&amp;lt;T&amp;gt; listOf(T... items) {
    return new ArrayList&amp;lt;&amp;gt;(Arrays.asList(items));   // 转发给另一个 @SafeVarargs 方法，不写入
}

// 不安全：往数组里写了 &quot;不是 T&quot; 的东西
static &amp;lt;T&amp;gt; void broken(T... items) {
    Object[] raw = items;        // 合法：数组协变
    raw[0] = new Object();       // 调用点传 String 字面量时，这里 ArrayStoreException
}

broken(&quot;a&quot;, &quot;b&quot;);                // 调用点创建的是 String[]，不是 Object[]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;T...&lt;/code&gt; 底层是数组，而且这个数组是&lt;strong&gt;调用点&lt;/strong&gt;创建的，运行时类型由调用点推断：传 &lt;code&gt;String&lt;/code&gt; 字面量得到 &lt;code&gt;String[]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以 &lt;code&gt;@SafeVarargs&lt;/code&gt; 不是编译器的验证结论，而是一个承诺：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;能加：方法只读这个数组（转发给 &lt;code&gt;Arrays.asList&lt;/code&gt;、&lt;code&gt;List.of&lt;/code&gt; 这类）。&lt;/li&gt;
&lt;li&gt;不能加：方法往数组里写、或者把数组交给别人去写。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;编译器不会替你检查这个承诺——写了，运行时照样炸。&lt;/p&gt;
&lt;p&gt;使用限制：只能标注在 &lt;code&gt;static&lt;/code&gt;、&lt;code&gt;final&lt;/code&gt;、&lt;code&gt;private&lt;/code&gt;（JDK 9+）方法或构造器上，因为否则子类可以重写并破坏承诺。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 泛型数组：正确写法与它为什么带警告&lt;/h2&gt;
&lt;h3&gt;5.1 直接 &lt;code&gt;new T[]&lt;/code&gt; 不行，&lt;code&gt;(T[]) new Object[n]&lt;/code&gt; 是骗自己&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;T[] a = new T[10];                   // 编译错误：generic array creation

T[] b = (T[]) new Object[10];        // 编译警告，运行时元素类型还是 Object
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二种的运行时类型是 &lt;code&gt;Object[]&lt;/code&gt;。你把它当 &lt;code&gt;String[]&lt;/code&gt; 用，一旦有人往里放别的类型，异常还是抛在&lt;strong&gt;读取处&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Integer[] ints = (Integer[]) new Object[]{&quot;a&quot;, &quot;b&quot;};   // 读的时候就炸
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.2 唯一正确的方式：显式传 Class&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import java.lang.reflect.Array;

@SuppressWarnings(&quot;unchecked&quot;)
static &amp;lt;T&amp;gt; T[] newArray(Class&amp;lt;T&amp;gt; type, int n) {
    return (T[]) Array.newInstance(type, n);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;String[] a = newArray(String.class, 3);
System.out.println(a.getClass());   // class [Ljava.lang.String;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么这条是安全的：&lt;code&gt;Array.newInstance(type, n)&lt;/code&gt; 出来的数组&lt;strong&gt;元素类型真的是 &lt;code&gt;T&lt;/code&gt;&lt;/strong&gt;，不存在堆污染，只是编译器不知道，所以需要一个 &lt;code&gt;@SuppressWarnings(&quot;unchecked&quot;)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这也解释了框架里那些&quot;莫名其妙&quot;的签名——为什么 &lt;code&gt;TypeHandler&lt;/code&gt;、&lt;code&gt;Converter&lt;/code&gt;、&lt;code&gt;BaseMapper&lt;/code&gt; 都要你把 &lt;code&gt;Class&amp;lt;T&amp;gt;&lt;/code&gt; 传进去：&lt;strong&gt;运行时唯一的类型来源就是你显式给的那个 &lt;code&gt;Class&lt;/code&gt; 对象&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;5.3 集合转数组的惯用法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
String[] a1 = list.toArray(new String[0]);        // 老写法
String[] a2 = list.toArray(String[]::new);        // JDK 11+，等价且更清晰
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;toArray(T[])&lt;/code&gt; 的泛型参数在这里的作用是&quot;借你用一下元素类型&quot;，JDK 内部也是走反射创建数组。传 &lt;code&gt;new String[0]&lt;/code&gt; 不会白创建——JDK 会检查数组长度不够就另建一个，够用就直接用。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 为什么不能 &lt;code&gt;instanceof List&amp;lt;String&amp;gt;&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Object o = new ArrayList&amp;lt;&amp;gt;();
if (o instanceof List&amp;lt;String&amp;gt;) { }   // 编译错误：illegal generic type for instanceof
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因很直接：运行时 &lt;code&gt;ArrayList&amp;lt;String&amp;gt;&lt;/code&gt; 和 &lt;code&gt;ArrayList&amp;lt;Integer&amp;gt;&lt;/code&gt; 是同一个类，这个判断&lt;strong&gt;没有可判定的运行时语义&lt;/strong&gt;。编译器宁可报错也不让你写出一个永远为真（或永远为假）的条件。&lt;/p&gt;
&lt;p&gt;能写的是无界通配符：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (o instanceof List&amp;lt;?&amp;gt;) { }        // 合法：只判断&quot;是不是 List&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果真需要拿到泛型实参，只能回到&lt;strong&gt;声明处&lt;/strong&gt;，用反射（01 §7 讲过）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Field f = Demo.class.getDeclaredField(&quot;names&quot;);
Type t = f.getGenericType();
if (t instanceof ParameterizedType pt) {
    Type arg = pt.getActualTypeArguments()[0];
    System.out.println(arg);   // class java.lang.String
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这读的是&lt;strong&gt;字段声明&lt;/strong&gt;，不是实例。而且如果声明写的是 &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt;，这里拿到的是 &lt;code&gt;TypeVariable&lt;/code&gt;（&lt;code&gt;T&lt;/code&gt;），不是具体类——这也是 Spring &lt;code&gt;ResolvableType&lt;/code&gt; 存在的原因：它得沿着父类、接口一层层把 &lt;code&gt;T&lt;/code&gt; 解出来。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 静态成员为什么不能用类的泛型参数&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Box&amp;lt;T&amp;gt; {
    static T value;                  // 编译错误
    static T get() { return null; }  // 编译错误
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;error: non-static type variable T cannot be referenced from a static context
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三条理由：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;静态成员属于类，不属于实例&lt;/strong&gt;。&lt;code&gt;T&lt;/code&gt; 是在&quot;你拿到一个 &lt;code&gt;Box&amp;lt;String&amp;gt;&lt;/code&gt; 引用&quot;时才有意义的，静态上下文里没有这个信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;擦除后每个类只有一份静态区&lt;/strong&gt;。如果允许，&lt;code&gt;Box&amp;lt;String&amp;gt;.value&lt;/code&gt; 和 &lt;code&gt;Box&amp;lt;Integer&amp;gt;.value&lt;/code&gt; 会是同一块内存，类型根本没法保证。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;static&lt;/code&gt; 方法不能调用实例的泛型参数，因为它在类初始化阶段就可能被调用，此时连实例都没有。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;正确写法：&lt;strong&gt;静态方法自己声明类型参数&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Box {
    static &amp;lt;T&amp;gt; T get(Class&amp;lt;T&amp;gt; type) { return null; }   // T 是方法自己的，与任何实例无关
}

class Box2&amp;lt;T&amp;gt; {
    static &amp;lt;U&amp;gt; U pick(U a, U b) { return a; }          // U 与方法绑定
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;区分清楚：&lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt; 写在类上叫&lt;strong&gt;类泛型参数&lt;/strong&gt;，写在方法上叫&lt;strong&gt;方法泛型参数&lt;/strong&gt;。静态方法想要泛型，只能走后者。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 完整验证代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class AfterErasureDemo {

    public static void main(String[] args) {
        testCheckcast();
        testBridge();
        testHeapPollution();
        testVarargs();
        testArray();
        testInstanceof(new ArrayList&amp;lt;String&amp;gt;());
    }

    // 1) checkcast：用 javap -c -p 看字节码
    static String first(List&amp;lt;String&amp;gt; list) {
        return list.get(0);   // 编译后，get 指令后面跟着一条 checkcast java/lang/String
    }

    static void testCheckcast() {
        System.out.println(first(Arrays.asList(&quot;a&quot;, &quot;b&quot;)));
    }

    // 2) 桥方法
    static void testBridge() {
        Node&amp;lt;String&amp;gt; n = new StringNode();
        System.out.println(n.get());   // s——靠桥方法维持多态

        // getDeclaredMethods 的顺序不保证，看内容即可
        for (Method m : StringNode.class.getDeclaredMethods()) {
            System.out.println(m + &quot; bridge=&quot; + m.isBridge() + &quot; synthetic=&quot; + m.isSynthetic());
        }
    }

    // 3) 堆污染：写入不报错，读取才炸
    static void testHeapPollution() {
        List&amp;lt;String&amp;gt; strings = new ArrayList&amp;lt;&amp;gt;();

        List raw = strings;
        raw.add(123);                  // ① 成功，这里不报错

        try {
            String s = strings.get(0); // ② 异常在这里抛
            System.out.println(s);
        } catch (ClassCastException e) {
            System.out.println(&quot;读的时候才炸: &quot; + e.getMessage());
        }
    }

    // 4) 可变参数：数组由调用点创建
    @SafeVarargs
    static &amp;lt;T&amp;gt; List&amp;lt;T&amp;gt; listOf(T... items) {
        return new ArrayList&amp;lt;&amp;gt;(Arrays.asList(items));   // 只读，安全
    }

    static &amp;lt;T&amp;gt; void broken(T... items) {
        Object[] raw = items;          // 合法：数组协变
        raw[0] = new Object();         // 调用点传 String 字面量时 → ArrayStoreException
    }

    static void testVarargs() {
        System.out.println(listOf(&quot;a&quot;, &quot;b&quot;));
        try {
            broken(&quot;a&quot;, &quot;b&quot;);
        } catch (ArrayStoreException e) {
            System.out.println(&quot;变参数组被写脏: &quot; + e.getMessage());
        }
    }

    // 5) 泛型数组：显式传 Class
    @SuppressWarnings(&quot;unchecked&quot;)
    static &amp;lt;T&amp;gt; T[] newArray(Class&amp;lt;T&amp;gt; type, int n) {
        return (T[]) Array.newInstance(type, n);
    }

    static void testArray() {
        String[] a = newArray(String.class, 2);
        a[0] = &quot;x&quot;;
        System.out.println(a.getClass() + &quot; &quot; + a[0]);   // class [Ljava.lang.String; x
    }

    // 6) instanceof：只能无界通配符
    static void testInstanceof(Object o) {
        System.out.println(o instanceof List&amp;lt;?&amp;gt;);   // true
        // o instanceof List&amp;lt;String&amp;gt;   // 编译错误：illegal generic type for instanceof
    }
}

// 桥方法用到的两个类：放成顶层类，javap StringNode 的输出才干净
class Node&amp;lt;T&amp;gt; {
    public T get() { return null; }
}

class StringNode extends Node&amp;lt;String&amp;gt; {
    @Override
    public String get() { return &quot;s&quot;; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a
s
public java.lang.String StringNode.get() bridge=false synthetic=false
public java.lang.Object StringNode.get() bridge=true synthetic=true
读的时候才炸: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader &apos;bootstrap&apos;)
[a, b]
变参数组被写脏: java.lang.Object
class [Ljava.lang.String; x
true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译期一共两条 &lt;code&gt;unchecked&lt;/code&gt; 警告，都在&lt;strong&gt;声明/调用点&lt;/strong&gt;而不是运行点：一条在 &lt;code&gt;raw.add(123)&lt;/code&gt;（原始类型 &lt;code&gt;List&lt;/code&gt; 的 &lt;code&gt;add&lt;/code&gt;），一条在 &lt;code&gt;broken(T...)&lt;/code&gt; 的声明处——“参数化 vararg 类型 T 的堆可能已受污染”。只读的 &lt;code&gt;listOf(T...)&lt;/code&gt; 不报，这是编译器对可变参数泛型能给出的唯一提示。（上面第 3 行 &lt;code&gt;ClassCastException&lt;/code&gt; 的完整信息在 JDK 9+ 会附带 module/loader 说明，不同版本尾注略有差异。）&lt;/p&gt;
&lt;p&gt;再补两条命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;javap -c -p AfterErasureDemo | grep -A2 checkcast   # 看编译器插的类型检查
javap -v StringNode | grep -A3 ACC_BRIDGE          # 看桥方法的标志位（输出 flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;9. 面试问法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：擦除之后 &lt;code&gt;list.get(0)&lt;/code&gt; 凭什么能直接赋给 &lt;code&gt;String&lt;/code&gt;？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;编译器在取值处自动插入了 &lt;code&gt;checkcast&lt;/code&gt; 指令。泛型的类型安全是两层：编译期由 javac 检查你写的类型，运行期由编译器生成的 &lt;code&gt;checkcast&lt;/code&gt; 兜底。堆污染的异常就是抛在这条指令上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q2：桥方法是什么？为什么需要它？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;子类重写泛型方法后，擦除让父类方法签名变成 &lt;code&gt;Object get()&lt;/code&gt;、子类方法是 &lt;code&gt;String get()&lt;/code&gt;，两者签名不一致。为了让父类引用调用时仍能命中子类实现，编译器在子类合成一个 &lt;code&gt;Object get()&lt;/code&gt; 的桥方法，把调用转发给 &lt;code&gt;String get()&lt;/code&gt;。识别方式：&lt;code&gt;Method.isBridge()&lt;/code&gt; / &lt;code&gt;isSynthetic()&lt;/code&gt;；协变返回类型的重写也会生成桥方法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q3：为什么两个只差泛型参数的方法不能重载？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;重载的判定依据是擦除后的签名。&lt;code&gt;m(List&amp;lt;String&amp;gt;)&lt;/code&gt; 和 &lt;code&gt;m(List&amp;lt;Integer&amp;gt;)&lt;/code&gt; 擦除后都是 &lt;code&gt;m(List)&lt;/code&gt;，编译器报 &lt;code&gt;name clash ... have the same erasure&lt;/code&gt;。只能换方法名，或者换参数容器（&lt;code&gt;List&lt;/code&gt; / &lt;code&gt;Collection&lt;/code&gt;）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q4：堆污染是什么？为什么异常不在写入时抛？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;往&quot;名义上有泛型、实际擦除了&quot;的容器里塞进类型不符的元素。写入时 &lt;code&gt;add&lt;/code&gt; 的擦除签名是 &lt;code&gt;add(Object)&lt;/code&gt;，JVM 不检查；真正检查的是编译器插在&lt;strong&gt;读取处赋值点&lt;/strong&gt;的 &lt;code&gt;checkcast&lt;/code&gt;，所以异常抛在 &lt;code&gt;String s = list.get(0)&lt;/code&gt; 那一行。这也是 &lt;code&gt;-Xlint:unchecked&lt;/code&gt; 警告不能关的原因。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q5：泛型数组怎么创建才是对的？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Array.newInstance(clazz, n)&lt;/code&gt; 并显式传入 &lt;code&gt;Class&amp;lt;T&amp;gt;&lt;/code&gt;，因为运行时唯一的类型来源就是 &lt;code&gt;Class&lt;/code&gt; 对象。&lt;code&gt;(T[]) new Object[n]&lt;/code&gt; 只是把问题推迟成堆污染。集合转数组用 &lt;code&gt;list.toArray(new T[0])&lt;/code&gt; 或 JDK 11+ 的 &lt;code&gt;list.toArray(T[]::new)&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q6：为什么不能 &lt;code&gt;instanceof List&amp;lt;String&amp;gt;&lt;/code&gt;？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;运行时 &lt;code&gt;List&amp;lt;String&amp;gt;&lt;/code&gt; 和 &lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt; 是同一个类，这个判断没有可判定的运行时语义，编译器直接报 &lt;code&gt;illegal generic type for instanceof&lt;/code&gt;。只能写 &lt;code&gt;instanceof List&amp;lt;?&amp;gt;&lt;/code&gt;；想拿泛型实参必须回到字段/方法/父类的&lt;strong&gt;声明处&lt;/strong&gt;用反射读 &lt;code&gt;getGenericType()&lt;/code&gt;，而且声明若是 &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt;，拿到的还是 &lt;code&gt;TypeVariable&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q7：静态方法为什么不能用类的泛型参数？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;静态成员属于类而不是实例，&lt;code&gt;T&lt;/code&gt; 只在拿到具体实例（&lt;code&gt;Box&amp;lt;String&amp;gt;&lt;/code&gt;）时才有意义；擦除后每个类只有一份静态区，&lt;code&gt;Box&amp;lt;String&amp;gt;.value&lt;/code&gt; 和 &lt;code&gt;Box&amp;lt;Integer&amp;gt;.value&lt;/code&gt; 会是同一块内存。静态方法要泛型，必须自己声明方法级类型参数：&lt;code&gt;static &amp;lt;T&amp;gt; T get(Class&amp;lt;T&amp;gt; type)&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;10. 小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;擦除之后，所有类型安全靠两个机制补：&lt;strong&gt;编译期检查 + 编译器插入的 &lt;code&gt;checkcast&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;桥方法（&lt;code&gt;ACC_BRIDGE&lt;/code&gt;）是&quot;擦除后多态仍然成立&quot;的代价；反射里要记得过滤桥方法。&lt;/li&gt;
&lt;li&gt;重载看擦除后的签名，泛型参数不同不算重载。&lt;/li&gt;
&lt;li&gt;堆污染的异常抛在&lt;strong&gt;读取处&lt;/strong&gt;，写入处静默通过。&lt;/li&gt;
&lt;li&gt;泛型数组只能 &lt;code&gt;Array.newInstance(clazz, n)&lt;/code&gt;；&lt;code&gt;@SafeVarargs&lt;/code&gt; 是承诺，不是验证。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;instanceof&lt;/code&gt; 不能带泛型实参，只能 &lt;code&gt;List&amp;lt;?&amp;gt;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;静态成员不能用类泛型参数，静态泛型方法要自己声明 &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;记忆钩子：&lt;strong&gt;&quot;能擦的都擦了，擦不掉的靠 &lt;code&gt;checkcast&lt;/code&gt; 和桥方法兜住。&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;11. 下一步&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 手跑第 8 节代码，对照参考输出。&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;javap -c -p&lt;/code&gt; 找 &lt;code&gt;checkcast&lt;/code&gt;，&lt;code&gt;javap -v&lt;/code&gt; 找 &lt;code&gt;ACC_BRIDGE&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;[ ] 闭卷复述第 9 节 7 个问题。&lt;/li&gt;
&lt;li&gt;[ ] 写下一篇：&lt;code&gt;03-通配符与PECS&lt;/code&gt;（&lt;code&gt;? extends&lt;/code&gt; / &lt;code&gt;? super&lt;/code&gt;、PECS 原则、通配符捕获、&lt;code&gt;Comparable&amp;lt;? super T&amp;gt;&lt;/code&gt; 为什么长这样）。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>akm</author></item><item><title>通配符与 PECS：? extends 与 ? super 到底在限制谁</title><link>https://blog-akm.pages.dev/notes/01-Java%E5%9F%BA%E7%A1%80/01-%E6%B3%9B%E5%9E%8B/03-%E9%80%9A%E9%85%8D%E7%AC%A6%E4%B8%8EPECS/</link><guid isPermaLink="true">https://blog-akm.pages.dev/notes/01-Java%E5%9F%BA%E7%A1%80/01-%E6%B3%9B%E5%9E%8B/03-%E9%80%9A%E9%85%8D%E7%AC%A6%E4%B8%8EPECS/</guid><description>泛型不变性、? extends / ? super 的读写边界、PECS 原则、CAP#1 捕获错误与 Collections.swap 的取舍、Comparable&lt;? super T&gt; 为什么长这样，附 javac 实测报错、JDK 真实签名与源码。</description><pubDate>Tue, 15 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;所属：Java 面试笔记 / 01-Java基础 / 01-泛型 ｜ 学习计划：D1
前置：&lt;a href=&quot;/notes/01-Java%E5%9F%BA%E7%A1%80/01-%E6%B3%9B%E5%9E%8B/02-%E6%93%A6%E9%99%A4%E4%B9%8B%E5%90%8E/&quot;&gt;02-擦除之后&lt;/a&gt; ｜ 后续：04-泛型在框架中的应用（待写）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;0. 本文要解决什么&lt;/h2&gt;
&lt;p&gt;前两篇讲的是&lt;strong&gt;运行期&lt;/strong&gt;：泛型被擦除了，于是有桥方法、&lt;code&gt;checkcast&lt;/code&gt;、堆污染。&lt;/p&gt;
&lt;p&gt;这一篇换个角度，只看&lt;strong&gt;编译期&lt;/strong&gt;：既然擦除后大家都是 &lt;code&gt;List&lt;/code&gt;，javac 凭什么还能在编译期把类型错误拦住？靠的就是通配符这套&quot;方向性&quot;规则。&lt;/p&gt;
&lt;p&gt;要回答的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为什么 &lt;code&gt;List&amp;lt;String&amp;gt;&lt;/code&gt; 能赋给 &lt;code&gt;List&amp;lt;?&amp;gt;&lt;/code&gt;，却不能赋给 &lt;code&gt;List&amp;lt;Object&amp;gt;&lt;/code&gt;？&lt;/li&gt;
&lt;li&gt;&lt;code&gt;List&amp;lt;? extends Number&amp;gt;&lt;/code&gt; 为什么&quot;能读不能写&quot;？到底是谁在禁止 &lt;code&gt;add&lt;/code&gt;？&lt;/li&gt;
&lt;li&gt;&lt;code&gt;List&amp;lt;? super Integer&amp;gt;&lt;/code&gt; 为什么&quot;能写不能读&quot;？读出来为什么只能是 &lt;code&gt;Object&lt;/code&gt;？&lt;/li&gt;
&lt;li&gt;PECS 是什么？看到一个方法签名怎么判断方向写得对不对？&lt;/li&gt;
&lt;li&gt;编译错误里的 &lt;code&gt;CAP#1&lt;/code&gt;（capture）是什么？&lt;/li&gt;
&lt;li&gt;&lt;code&gt;swap(List&amp;lt;?&amp;gt;, int, int)&lt;/code&gt; 为什么编译不过，JDK 自己怎么绕过去？&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Comparable&amp;lt;? super T&amp;gt;&lt;/code&gt; 里的 &lt;code&gt;? super&lt;/code&gt; 到底解决了什么问题？&lt;/li&gt;
&lt;li&gt;泛型方法什么时候该写 &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt;、什么时候只用通配符？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;运行环境：JDK 17（Temurin 17.0.16）。下面所有报错和输出都是真跑出来的，JDK 源码摘自 &lt;code&gt;lib/src.zip&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 泛型是不变的（invariant）&lt;/h2&gt;
&lt;h3&gt;1.1 一个编译不过的赋值&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;Object&amp;gt; list = new ArrayList&amp;lt;String&amp;gt;();   // 想当然的写法
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;javac BadInvariant.java
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;BadInvariant.java:4: 错误: 不兼容的类型: ArrayList&amp;lt;String&amp;gt;无法转换为List&amp;lt;Object&amp;gt;
        List&amp;lt;Object&amp;gt; list = new ArrayList&amp;lt;String&amp;gt;();
                            ^
1 个错误
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直觉上 &lt;code&gt;String&lt;/code&gt; 是 &lt;code&gt;Object&lt;/code&gt; 的子类，&lt;code&gt;List&amp;lt;String&amp;gt;&lt;/code&gt; 就该是 &lt;code&gt;List&amp;lt;Object&amp;gt;&lt;/code&gt; 的子类。Java 偏偏不允许——这不叫&quot;没实现&quot;，叫&lt;strong&gt;不变性（invariance）&lt;/strong&gt;：&lt;code&gt;List&amp;lt;A&amp;gt;&lt;/code&gt; 与 &lt;code&gt;List&amp;lt;B&amp;gt;&lt;/code&gt; 之间，除了通配符，没有任何继承关系。&lt;/p&gt;
&lt;h3&gt;1.2 为什么必须这样设计：数组的教训&lt;/h3&gt;
&lt;p&gt;数组是&lt;strong&gt;协变&lt;/strong&gt;的，Java 允许这样写，于是留下一个运行期漏洞：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Object[] arr = new String[3];    // 编译通过
arr[0] = 1;                      // 编译通过！类型错误被推迟到运行期
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;运行期: java.lang.ArrayStoreException: java.lang.Integer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;arr&lt;/code&gt; 的静态类型是 &lt;code&gt;Object[]&lt;/code&gt;，看起来什么都能放；但它的运行期类型是 &lt;code&gt;String[]&lt;/code&gt;，JVM 直到执行 &lt;code&gt;aastore&lt;/code&gt; 才发现类型不对。&lt;strong&gt;数组的协变等于把类型检查推迟到了运行期。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;List&amp;lt;Object&amp;gt; list = new ArrayList&amp;lt;String&amp;gt;()&lt;/code&gt; 合法，同样的漏洞会出现，而且更糟——擦除之后连&quot;运行期类型&quot;都没得查：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;Object&amp;gt; list = new ArrayList&amp;lt;String&amp;gt;();   // 假设合法
list.add(1);                                   // 编译期看是 List&amp;lt;Object&amp;gt;，合法
String s = list.get(0);                        // 运行期 checkcast：Integer -&amp;gt; String，炸
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以泛型选了不变：&lt;strong&gt;编译期能拒绝的错误，绝不留到运行期。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;1.3 四条赋值规则（先记方向）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;声明&lt;/th&gt;
&lt;th&gt;能接受哪些实参&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;List&amp;lt;Object&amp;gt; a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;只有 &lt;code&gt;List&amp;lt;Object&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不变，最严格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;List&amp;lt;?&amp;gt; b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;任何 &lt;code&gt;List&amp;lt;X&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无界通配符，所有 &lt;code&gt;List&lt;/code&gt; 的父类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;List&amp;lt;? extends Number&amp;gt; c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;List&amp;lt;Number&amp;gt;&lt;/code&gt;、&lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt;、&lt;code&gt;List&amp;lt;Double&amp;gt;&lt;/code&gt;…&lt;/td&gt;
&lt;td&gt;上界通配符，向下兼容所有子类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;List&amp;lt;? super Integer&amp;gt; d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt;、&lt;code&gt;List&amp;lt;Number&amp;gt;&lt;/code&gt;、&lt;code&gt;List&amp;lt;Object&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;下界通配符，向上兼容所有父类型&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;实测：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;?&amp;gt; anything = new ArrayList&amp;lt;String&amp;gt;();                  // 可以
List&amp;lt;? extends Number&amp;gt; producer = new ArrayList&amp;lt;Integer&amp;gt;();  // 可以
producer = new ArrayList&amp;lt;Double&amp;gt;();                          // 换个子类型也行
producer = new ArrayList&amp;lt;Number&amp;gt;();                          // 上界本身也行
List&amp;lt;? super Integer&amp;gt; consumer = new ArrayList&amp;lt;Integer&amp;gt;();
consumer = new ArrayList&amp;lt;Number&amp;gt;();
consumer = new ArrayList&amp;lt;Object&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一句话：&lt;strong&gt;通配符是用来&quot;放宽接收范围&quot;的，方向由 &lt;code&gt;extends&lt;/code&gt; / &lt;code&gt;super&lt;/code&gt; 决定。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. &lt;code&gt;? extends&lt;/code&gt;：只读的生产者&lt;/h2&gt;
&lt;h3&gt;2.1 能读到什么&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;? extends Number&amp;gt; list = new ArrayList&amp;lt;Integer&amp;gt;();
Number n = list.get(0);      // 编译通过，静态类型是上界 Number
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;get&lt;/code&gt; 的静态返回类型是 &lt;code&gt;? extends Number&lt;/code&gt; 捕获后的类型，编译器统一读成上界 &lt;code&gt;Number&lt;/code&gt;：不管实际是 &lt;code&gt;Integer&lt;/code&gt; 还是 &lt;code&gt;Double&lt;/code&gt;，都是 &lt;code&gt;Number&lt;/code&gt; 的子类型，读出来安全。&lt;/p&gt;
&lt;h3&gt;2.2 为什么不能写&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;list.add(1);       // 编译不过
list.add(1.0);     // 编译不过
list.add(null);    // 唯一合法的写入
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;javac BadExtends.java
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;BadExtends.java:6: 错误: 不兼容的类型: int无法转换为CAP#1
        list.add(1);                         // 写：编译不过
                 ^
  其中, CAP#1是新类型变量:
    CAP#1从? extends Number的捕获扩展Number
BadExtends.java:7: 错误: 不兼容的类型: double无法转换为CAP#1
（第 7 行 list.add(1.0) 报同样的错，略）
2 个错误
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键点：&lt;strong&gt;报错的主体不是 &lt;code&gt;Number&lt;/code&gt;，而是 &lt;code&gt;CAP#1&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;list&lt;/code&gt; 声明成 &lt;code&gt;List&amp;lt;? extends Number&amp;gt;&lt;/code&gt; 时，真实元素类型对调用方是未知的——可能是 &lt;code&gt;Integer&lt;/code&gt;，也可能是 &lt;code&gt;Double&lt;/code&gt;。编译器用一个内部符号 &lt;code&gt;CAP#1&lt;/code&gt; 表示&quot;某个具体的、但此处不知道是谁的 &lt;code&gt;Number&lt;/code&gt; 子类型&quot;，而 &lt;code&gt;add&lt;/code&gt; 的形参就是这个 &lt;code&gt;CAP#1&lt;/code&gt;。既然不知道它是谁，任何具体值都塞不进去：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;塞 &lt;code&gt;Integer&lt;/code&gt;：万一是 &lt;code&gt;List&amp;lt;Double&amp;gt;&lt;/code&gt;，就堆污染了；&lt;/li&gt;
&lt;li&gt;塞 &lt;code&gt;Double&lt;/code&gt;：万一是 &lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt;，同样堆污染；&lt;/li&gt;
&lt;li&gt;塞 &lt;code&gt;null&lt;/code&gt;：&lt;code&gt;null&lt;/code&gt; 是所有引用类型的值，与具体类型无关，唯一安全。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以&quot;上界通配符只能读、不能写&quot;的准确含义是：&lt;strong&gt;不是限制你，是编译器真的不知道往哪个类型里写。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;2.3 和数组协变对照&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;数组&lt;/th&gt;
&lt;th&gt;泛型 + &lt;code&gt;? extends&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;语法&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Number[] a = new Integer[2];&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;List&amp;lt;? extends Number&amp;gt; l = new ArrayList&amp;lt;Integer&amp;gt;();&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;是否协变&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;写&lt;/td&gt;
&lt;td&gt;&lt;code&gt;a[0] = 1.5;&lt;/code&gt; 编译通过，运行期 &lt;code&gt;ArrayStoreException&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;l.add(1.5)&lt;/code&gt; 编译期直接拒绝&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;读&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Number n = a[0];&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Number n = l.get(0);&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;同一个协变想法，数组靠运行期兜底，泛型靠编译期拦下——区别就是泛型把&quot;写的时候不能做什么&quot;记录进了类型。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. &lt;code&gt;? super&lt;/code&gt;：只写的消费者&lt;/h2&gt;
&lt;h3&gt;3.1 能写什么&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;? super Integer&amp;gt; list = new ArrayList&amp;lt;Number&amp;gt;();
list.add(1);                          // 编译通过
list.add(Integer.valueOf(2));         // 编译通过
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;? super Integer&lt;/code&gt; 表示&quot;某个 &lt;code&gt;Integer&lt;/code&gt; 的父类型&quot;：可能是 &lt;code&gt;Integer&lt;/code&gt;、&lt;code&gt;Number&lt;/code&gt;、&lt;code&gt;Object&lt;/code&gt;。不管具体是哪个，&lt;strong&gt;&lt;code&gt;Integer&lt;/code&gt; 一定是它的子类型&lt;/strong&gt;，所以写入 &lt;code&gt;Integer&lt;/code&gt; 永远合法。&lt;/p&gt;
&lt;h3&gt;3.2 为什么读出来只能是 &lt;code&gt;Object&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Object o = list.get(0);      // 可以
Integer i = list.get(0);     // 编译不过
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;javac BadSuper.java
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;BadSuper.java:5: 错误: 不兼容的类型: CAP#1无法转换为Integer
        Integer i = list.get(0);             // 读：编译不过，只能当 Object
                            ^
  其中, CAP#1是新类型变量:
    CAP#1从? super Integer的捕获扩展Object 超 Integer
1 个错误
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意报错里的描述：&lt;code&gt;CAP#1 扩展 Object 超 Integer&lt;/code&gt;。&lt;code&gt;? super Integer&lt;/code&gt; 的&lt;strong&gt;上界是 &lt;code&gt;Object&lt;/code&gt;&lt;/strong&gt;（&lt;code&gt;Integer&lt;/code&gt; 只是下界），所以取值只能保证是 &lt;code&gt;Object&lt;/code&gt;。想拿到具体类型，只能强转，或者改用泛型方法（见第 5 节）。&lt;/p&gt;
&lt;h3&gt;3.3 对照表&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;List&amp;lt;? extends Number&amp;gt;&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;List&amp;lt;? super Integer&amp;gt;&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;实际可能是&lt;/td&gt;
&lt;td&gt;&lt;code&gt;List&amp;lt;Number&amp;gt;&lt;/code&gt;、&lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt;、&lt;code&gt;List&amp;lt;Double&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt;、&lt;code&gt;List&amp;lt;Number&amp;gt;&lt;/code&gt;、&lt;code&gt;List&amp;lt;Object&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get(0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Number&lt;/code&gt; ✓&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Object&lt;/code&gt; ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;add(1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✗ CAP#1&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;add(1.0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✗ CAP#1&lt;/td&gt;
&lt;td&gt;✗（&lt;code&gt;Double&lt;/code&gt; 不是 &lt;code&gt;Integer&lt;/code&gt; 的子类型）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;add(null)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;角色&lt;/td&gt;
&lt;td&gt;生产者（只读）&lt;/td&gt;
&lt;td&gt;消费者（只写）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;4. PECS：Producer Extends, Consumer Super&lt;/h2&gt;
&lt;h3&gt;4.1 规则&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;参数是&quot;生产者&quot;（方法从它读数据）→ 用 &lt;code&gt;? extends T&lt;/code&gt;；参数是&quot;消费者&quot;（方法往它写数据）→ 用 &lt;code&gt;? super T&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;判断只有一句话：&lt;strong&gt;站在方法体里看，这个集合是被读还是被写？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;被读 → &lt;code&gt;extends&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;被写 → &lt;code&gt;super&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;既读又写 → 不能用通配符，改用方法级类型参数 &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt;（第 5 节）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2 自己写三个签名&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/** 只读：生产者用 extends */
static double sum(List&amp;lt;? extends Number&amp;gt; src) { ... }

/** 只写：消费者用 super */
static void fill(List&amp;lt;? super Integer&amp;gt; dest, int n) { ... }

/** 一读一写：src 是生产者、dest 是消费者，各用各的 */
static &amp;lt;T&amp;gt; void copy(List&amp;lt;? super T&amp;gt; dest, List&amp;lt;? extends T&amp;gt; src) { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;$ java PecsDemo
sum(List&amp;lt;Integer&amp;gt;) = 6.0
sum(List&amp;lt;Double&amp;gt;)  = 4.0
sum(List.of(1, 2L, 3.0f, 4.0)) = 10.0
fill -&amp;gt; [0, 1, 2]
copy -&amp;gt; [a, b]
before sort: [3, 1, 2]
after sort:  [3, 2, 1]
copied:      [3, 2, 1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sum&lt;/code&gt; 一行通吃 &lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt; / &lt;code&gt;List&amp;lt;Double&amp;gt;&lt;/code&gt; / 混合数字列表；写成 &lt;code&gt;sum(List&amp;lt;Number&amp;gt;)&lt;/code&gt; 就没有这个能力（&lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt; 不是 &lt;code&gt;List&amp;lt;Number&amp;gt;&lt;/code&gt;，回到第 1 节的不变性）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;copy&lt;/code&gt; 是最能说明 PECS 的例子：&lt;code&gt;dest&lt;/code&gt; 用 &lt;code&gt;super&lt;/code&gt;、&lt;code&gt;src&lt;/code&gt; 用 &lt;code&gt;extends&lt;/code&gt;，一次同时用上两条规则，而且&lt;strong&gt;调用方不需要指定 &lt;code&gt;T&lt;/code&gt;&lt;/strong&gt;——编译器从两个实参推断出 &lt;code&gt;T = String&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;4.3 JDK 里到处都是 PECS&lt;/h3&gt;
&lt;p&gt;用反射把 JDK 的真实签名打出来（&lt;code&gt;JdkSignatures.java&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Collection.addAll  boolean addAll(java.util.Collection&amp;lt;? extends E&amp;gt;)
List.sort          void sort(java.util.Comparator&amp;lt;? super E&amp;gt;)
Collections.copy   &amp;lt;T extends java.lang.Object&amp;gt; void copy(java.util.List&amp;lt;? super T&amp;gt;, java.util.List&amp;lt;? extends T&amp;gt;)
Collections.fill   &amp;lt;T extends java.lang.Object&amp;gt; void fill(java.util.List&amp;lt;? super T&amp;gt;, T)
Collections.max    &amp;lt;T extends java.lang.Object extends java.lang.Comparable&amp;lt;? super T&amp;gt;&amp;gt; T max(java.util.Collection&amp;lt;? extends T&amp;gt;)
Collections.sort   &amp;lt;T extends java.lang.Comparable&amp;lt;? super T&amp;gt;&amp;gt; void sort(java.util.List&amp;lt;T&amp;gt;)
Collections.addAll &amp;lt;T extends java.lang.Object&amp;gt; boolean addAll(java.util.Collection&amp;lt;? super T&amp;gt;, T[])
Stream.forEach     void forEach(java.util.function.Consumer&amp;lt;? super T&amp;gt;)
Stream.map         &amp;lt;R extends java.lang.Object&amp;gt; java.util.stream.Stream&amp;lt;R&amp;gt; map(java.util.function.Function&amp;lt;? super T, ? extends R&amp;gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;翻译成人话：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;JDK API&lt;/th&gt;
&lt;th&gt;为什么是这个方向&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Collection.addAll(Collection&amp;lt;? extends E&amp;gt; c)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;参数是元素来源（生产者）→ &lt;code&gt;extends&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Collections.addAll(Collection&amp;lt;? super T&amp;gt; c, T... elements)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;参数是被填充的目标（消费者）→ &lt;code&gt;super&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Collections.copy(List&amp;lt;? super T&amp;gt; dest, List&amp;lt;? extends T&amp;gt; src)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;教科书式的一读一写&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Collections.fill(List&amp;lt;? super T&amp;gt; list, T obj)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;只写 → &lt;code&gt;super&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;List.sort(Comparator&amp;lt;? super E&amp;gt; c)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;比较器&lt;strong&gt;消费&lt;/strong&gt;集合元素 → &lt;code&gt;super&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stream.forEach(Consumer&amp;lt;? super T&amp;gt; action)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;同上，动作消费元素 → &lt;code&gt;super&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stream.map(Function&amp;lt;? super T, ? extends R&amp;gt; mapper)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;入参消费 &lt;code&gt;super T&lt;/code&gt;、出参生产 &lt;code&gt;extends R&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Collections.max(Collection&amp;lt;? extends T&amp;gt;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;只读（生产者）→ &lt;code&gt;extends&lt;/code&gt;，返回值就是 &lt;code&gt;T&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;记法：&lt;strong&gt;看到 &lt;code&gt;? extends&lt;/code&gt; 就知道它在读，看到 &lt;code&gt;? super&lt;/code&gt; 就知道它在写。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;顺带解释一个老问题：为什么 &lt;code&gt;Stream.map&lt;/code&gt; 的参数里同时出现 &lt;code&gt;? super T&lt;/code&gt; 和 &lt;code&gt;? extends R&lt;/code&gt;？因为一个 &lt;code&gt;Function&lt;/code&gt; &lt;strong&gt;既消费又生产&lt;/strong&gt;——入参是消费者，出参是生产者，一个参数位就要写两条规则。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Collections.copy&lt;/code&gt; 的源码最能说明问题（JDK 17，&lt;code&gt;java.base/java/util/Collections.java&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static &amp;lt;T&amp;gt; void copy(List&amp;lt;? super T&amp;gt; dest, List&amp;lt;? extends T&amp;gt; src) {
    int srcSize = src.size();
    if (srcSize &amp;gt; dest.size())
        throw new IndexOutOfBoundsException(&quot;Source does not fit in dest&quot;);

    if (srcSize &amp;lt; COPY_THRESHOLD ||
        (src instanceof RandomAccess &amp;amp;&amp;amp; dest instanceof RandomAccess)) {
        for (int i=0; i&amp;lt;srcSize; i++)
            dest.set(i, src.get(i));        // ← T 从 src 来、进 dest 去，两个方向都对得上
    } else { ... }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;src.get(i)&lt;/code&gt; 静态类型是 &lt;code&gt;? extends T&lt;/code&gt;（是 &lt;code&gt;T&lt;/code&gt;），&lt;code&gt;dest.set(i, T)&lt;/code&gt; 要求 &lt;code&gt;? super T&lt;/code&gt;（能接 &lt;code&gt;T&lt;/code&gt;）。&lt;strong&gt;PECS 的本质就是让这一行能编译&lt;/strong&gt;。另外注意 &lt;code&gt;dest&lt;/code&gt; 必须预先有足够长度，否则抛 &lt;code&gt;IndexOutOfBoundsException&lt;/code&gt;——这也是它和 &lt;code&gt;addAll&lt;/code&gt; 的分工差别。&lt;/p&gt;
&lt;h3&gt;4.4 方向写反了会怎样&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;static void sum(List&amp;lt;Number&amp;gt; src)                                    // 太窄：List&amp;lt;Integer&amp;gt; 传不进来
static &amp;lt;T&amp;gt; void copy(List&amp;lt;? extends T&amp;gt; dest, List&amp;lt;? super T&amp;gt; src)    // 方向反了
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二种会在&lt;strong&gt;方法体里&lt;/strong&gt;报错：&lt;code&gt;dest&lt;/code&gt; 的 &lt;code&gt;add&lt;/code&gt;/&lt;code&gt;set&lt;/code&gt; 形参是 &lt;code&gt;CAP#2&lt;/code&gt;，&lt;code&gt;src&lt;/code&gt; 的元素是 &lt;code&gt;CAP#1&lt;/code&gt;，编译器无法证明两者相同。所以有个好用自检法：&lt;strong&gt;方法体能编译过，就说明签名方向写对了。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 通配符捕获：&lt;code&gt;CAP#1&lt;/code&gt; 与 &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt; 辅助方法&lt;/h2&gt;
&lt;h3&gt;5.1 一个&quot;看起来显然&quot;却编译不过的 swap&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;static void swap(List&amp;lt;?&amp;gt; list, int i, int j) {
    list.set(i, list.get(j));      // 想让 i 位置存原来 j 位置的值
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;javac BadCapture.java
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;BadCapture.java:4: 错误: 不兼容的类型: Object无法转换为CAP#1
        list.set(i, list.get(j));
                            ^
  其中, CAP#1是新类型变量:
    CAP#1从?的捕获扩展Object
1 个错误
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;get&lt;/code&gt; 返回 &lt;code&gt;Object&lt;/code&gt;，而 &lt;code&gt;set&lt;/code&gt; 要的是 &lt;code&gt;CAP#1&lt;/code&gt;（&lt;code&gt;List&amp;lt;?&amp;gt;&lt;/code&gt; 里那个未知的元素类型）。编译器无法证明&quot;取出来的东西&quot;和&quot;塞进去的位置&quot;是同一个类型，于是拒绝。&lt;/p&gt;
&lt;h3&gt;5.2 两种解法：&lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt; 辅助方法 vs 原始类型&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;解法一：私有辅助方法把捕获类型&quot;命名&quot;下来&lt;/strong&gt;（Effective Java 第 31 条的写法）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void swap(List&amp;lt;?&amp;gt; list, int i, int j) {
    swapHelper(list, i, j);            // 对外保持最宽松的签名
}

private static &amp;lt;T&amp;gt; void swapHelper(List&amp;lt;T&amp;gt; list, int i, int j) {
    T tmp = list.get(i);               // 类型被命名成 T，get/set 就对上了
    list.set(i, list.get(j));
    list.set(j, tmp);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;解法二：JDK 自己用的是原始类型&lt;/strong&gt;（&lt;code&gt;java.base/java/util/Collections.java&lt;/code&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SuppressWarnings({&quot;rawtypes&quot;, &quot;unchecked&quot;})
public static void swap(List&amp;lt;?&amp;gt; list, int i, int j) {
    // instead of using a raw type here, it&apos;s possible to capture
    // the wildcard but it will require a call to a supplementary
    // private method
    final List l = list;
    l.set(i, l.set(j, l.get(i)));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JDK 的注释把这个取舍写得很清楚：&lt;strong&gt;通配符是可以捕获的，但代价是加一个辅助方法&lt;/strong&gt;；标准库为了少一个方法、多一个 &lt;code&gt;@SuppressWarnings&lt;/code&gt;，选了原始类型。（&lt;code&gt;l.set(j, l.get(i))&lt;/code&gt; 还能把返回值当&quot;原来 j 位置的值&quot;用，顺手完成交换。）&lt;/p&gt;
&lt;p&gt;两种解法都对，面试时能把取舍讲出来就是加分项：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;解法&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;代价&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt; 辅助方法&lt;/td&gt;
&lt;td&gt;全程类型安全，无 &lt;code&gt;@SuppressWarnings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;多一个私有方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;原始类型 &lt;code&gt;List&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;代码短&lt;/td&gt;
&lt;td&gt;关掉类型检查，必须自己保证安全&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;$ java CaptureDemo
swap -&amp;gt; [3, 2, 1]
size=3, get(0)=3, class=Integer
after remove(0): [2, 1]
after clear(): []
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;捕获的本质是&quot;编译器在调用点推断出一个具体类型&quot;，而 &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt; 让这个推断结果能被写出来、被复用。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;5.3 &lt;code&gt;List&amp;lt;?&amp;gt;&lt;/code&gt; 到底能干什么&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;?&amp;gt; unknown = ints;
unknown.size();          // ✓
unknown.get(0);          // ✓ 结果是 Object
unknown.remove(0);       // ✓ 调的是 remove(int index)，与元素类型无关
unknown.clear();         // ✓
unknown.add(null);       // ✓ 只有 null
unknown.add(1);          // ✗ 编译不过
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以 &lt;code&gt;List&amp;lt;?&amp;gt;&lt;/code&gt; 的语义是：&lt;strong&gt;&quot;我不关心元素类型，也绝不往里放东西&quot;&lt;/strong&gt;——常出现在只用 &lt;code&gt;size&lt;/code&gt;、判空、遍历打印的 API 上。&lt;/p&gt;
&lt;h3&gt;5.4 &lt;code&gt;List&amp;lt;Object&amp;gt;&lt;/code&gt;、&lt;code&gt;List&amp;lt;?&amp;gt;&lt;/code&gt;、&lt;code&gt;List&amp;lt;? extends Object&amp;gt;&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;static void a(List&amp;lt;?&amp;gt; list) { }
static void a(List&amp;lt;? extends Object&amp;gt; list) { }   // 与方法一冲突
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;BadOnes.java:5: 错误: 已在类 BadOnes中定义了方法 a(List&amp;lt;?&amp;gt;)
        static void a(List&amp;lt;? extends Object&amp;gt; list) { }      // ① 通配符没带来任何区别：签名冲突
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;?&amp;gt;&lt;/code&gt; 与 &lt;code&gt;&amp;lt;? extends Object&amp;gt;&lt;/code&gt; 是同一个类型，重载直接冲突——&lt;strong&gt;写 &lt;code&gt;? extends Object&lt;/code&gt; 没有任何额外表达力&lt;/strong&gt;，多写只是噪音。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;能 add 什么&lt;/th&gt;
&lt;th&gt;能接受哪些实参&lt;/th&gt;
&lt;th&gt;什么时候用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;List&amp;lt;Object&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;任何对象&lt;/td&gt;
&lt;td&gt;只有 &lt;code&gt;List&amp;lt;Object&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;真的需要往同一个列表里放各种类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;List&amp;lt;?&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;只能 &lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;所有 &lt;code&gt;List&amp;lt;X&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;只读：&lt;code&gt;size&lt;/code&gt; / 判空 / 遍历&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;List&amp;lt;? extends Object&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;同 &lt;code&gt;List&amp;lt;?&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;同 &lt;code&gt;List&amp;lt;?&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;别写，等价于 &lt;code&gt;List&amp;lt;?&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;5.5 什么时候用 &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt;、什么时候用通配符&lt;/h3&gt;
&lt;p&gt;判据（Effective Java 第 31 条）：&lt;strong&gt;类型参数在方法签名的多个位置出现、需要把它们关联起来时用 &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt;；只出现一个位置、只为放宽接收范围时用通配符。&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;写法&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;参数只读，与返回值无关&lt;/td&gt;
&lt;td&gt;&lt;code&gt;void print(List&amp;lt;?&amp;gt; list)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;参数只读，返回值跟元素类型挂钩&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;T&amp;gt; T first(List&amp;lt;? extends T&amp;gt; list)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;两个参数类型必须一致&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;T&amp;gt; void copy(List&amp;lt;? super T&amp;gt; dest, List&amp;lt;? extends T&amp;gt; src)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;参数既读又写&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;T&amp;gt; void sort(List&amp;lt;T&amp;gt; list)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一个常被追问的点：&lt;strong&gt;&lt;code&gt;static &amp;lt;T&amp;gt; void print(List&amp;lt;T&amp;gt; list)&lt;/code&gt; 也能编译，为什么不算好签名？&lt;/strong&gt; 因为 &lt;code&gt;T&lt;/code&gt; 在方法体里没有任何用途，调用方却多了一层推断负担；语义上它和 &lt;code&gt;print(List&amp;lt;?&amp;gt;)&lt;/code&gt; 完全等价。这种属于&quot;为了泛型而泛型&quot;，评审时应改回 &lt;code&gt;List&amp;lt;?&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. &lt;code&gt;Comparable&amp;lt;? super T&amp;gt;&lt;/code&gt;：&lt;code&gt;? super&lt;/code&gt; 最经典的用法&lt;/h2&gt;
&lt;h3&gt;6.1 先把问题摆出来&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Base implements Comparable&amp;lt;Base&amp;gt; { ... }
class Sub extends Base { }        // 自己没实现 Comparable，继承来的是 Comparable&amp;lt;Base&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写一个取最大值的泛型方法，先按直觉写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static &amp;lt;T extends Comparable&amp;lt;T&amp;gt;&amp;gt; T max(List&amp;lt;? extends T&amp;gt; list) { ... }

Sub s = max(List.of(new Sub(3), new Sub(1)));   // 希望 T = Sub
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;javac BadComparable.java
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;BadComparable.java:20: 错误: 不兼容的类型: 推论变量 T 具有不兼容的上限
        Sub s = max(List.of(new Sub(3), new Sub(1)));   // 要求 T = Sub
                   ^
    等式约束条件：Base
    下限：Sub,Comparable&amp;lt;T&amp;gt;
  其中, T是类型变量:
    T扩展已在方法 &amp;lt;T&amp;gt;max(List&amp;lt;? extends T&amp;gt;)中声明的Comparable&amp;lt;T&amp;gt;
1 个错误
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;T extends Comparable&amp;lt;T&amp;gt;&lt;/code&gt; 要求&quot;&lt;code&gt;T&lt;/code&gt; 自己实现了 &lt;code&gt;Comparable&amp;lt;T&amp;gt;&lt;/code&gt;&quot;。&lt;code&gt;Sub&lt;/code&gt; 只实现了 &lt;code&gt;Comparable&amp;lt;Base&amp;gt;&lt;/code&gt;，不满足，推断失败。真实项目里最常见的触发场景是 &lt;code&gt;java.sql.Timestamp&lt;/code&gt;：它 &lt;code&gt;extends java.util.Date&lt;/code&gt;，而 &lt;code&gt;Date implements Comparable&amp;lt;Date&amp;gt;&lt;/code&gt;，所以 &lt;code&gt;Timestamp&lt;/code&gt; 并不实现 &lt;code&gt;Comparable&amp;lt;Timestamp&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;6.2 把 &lt;code&gt;T&lt;/code&gt; 放宽成&quot;能比较 &lt;code&gt;T&lt;/code&gt; 的类型&quot;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;static &amp;lt;T extends Comparable&amp;lt;? super T&amp;gt;&amp;gt; T max(List&amp;lt;? extends T&amp;gt; list) {
    T best = list.get(0);
    for (T t : list) if (t.compareTo(best) &amp;gt; 0) best = t;
    return best;
}

Sub s = max(List.of(new Sub(3), new Sub(1), new Sub(2)));   // 编译通过，返回类型就是 Sub
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;$ java ComparableDemo
max(Sub)     = Sub(3)  运行期类型=Sub
max(String)  = c
max(Integer) = 9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Comparable&amp;lt;? super T&amp;gt;&lt;/code&gt; 的意思是&quot;&lt;strong&gt;&lt;code&gt;T&lt;/code&gt; 的某个父类型实现了 &lt;code&gt;Comparable&lt;/code&gt;&lt;/strong&gt;&quot;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Sub&lt;/code&gt; → &lt;code&gt;Comparable&amp;lt;Base&amp;gt;&lt;/code&gt; ✓（&lt;code&gt;Base&lt;/code&gt; 是 &lt;code&gt;Sub&lt;/code&gt; 的父类）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String&lt;/code&gt; → &lt;code&gt;Comparable&amp;lt;String&amp;gt;&lt;/code&gt; ✓（&lt;code&gt;T&lt;/code&gt; 自己也算 &lt;code&gt;? super T&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Integer&lt;/code&gt; → &lt;code&gt;Comparable&amp;lt;Integer&amp;gt;&lt;/code&gt; ✓&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;换成 &lt;code&gt;Comparable&amp;lt;T&amp;gt;&lt;/code&gt;，只有第二、三种能过。&lt;strong&gt;&lt;code&gt;? super&lt;/code&gt; 在这里买到的是&quot;允许沿继承链比较&quot;，代价只是签名长一点。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;6.3 JDK 的原始签名与那个 &lt;code&gt;Object &amp;amp;&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public static &amp;lt;T extends Object &amp;amp; Comparable&amp;lt;? super T&amp;gt;&amp;gt; T max(Collection&amp;lt;? extends T&amp;gt; coll) {
    Iterator&amp;lt;? extends T&amp;gt; i = coll.iterator();
    T candidate = i.next();
    while (i.hasNext()) {
        T next = i.next();
        if (next.compareTo(candidate) &amp;gt; 0)
            candidate = next;
    }
    return candidate;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Comparable&amp;lt;? super T&amp;gt;&lt;/code&gt;：逻辑需要，理由见 6.2。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object &amp;amp;&lt;/code&gt;：擦除后 &lt;code&gt;T extends Comparable&amp;lt;? super T&amp;gt;&lt;/code&gt; 的上界是 &lt;code&gt;Comparable&lt;/code&gt;，返回类型会变成 &lt;code&gt;Comparable&lt;/code&gt;；写上 &lt;code&gt;Object &amp;amp;&lt;/code&gt; 之后上界变成 &lt;code&gt;Object&lt;/code&gt;，才能与泛型时代之前 &lt;code&gt;max(Collection)&lt;/code&gt; 的签名保持&lt;strong&gt;二进制兼容&lt;/strong&gt;。这是给老字节码兜底的产物，不是逻辑需要。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;被追问&quot;这个 &lt;code&gt;Object &amp;amp;&lt;/code&gt; 是干嘛的&quot;，答&quot;保持与擦除前签名的兼容&quot;就够了。&lt;/p&gt;
&lt;h3&gt;6.4 记忆钩子&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;? super&lt;/code&gt; 出现在边界里，永远在说同一件事：&lt;strong&gt;&quot;我只需要一个能接受 &lt;code&gt;T&lt;/code&gt; 的容器/比较器，不需要它精确等于 &lt;code&gt;T&lt;/code&gt;。&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 嵌套通配符：外层不变、内层才放开&lt;/h2&gt;
&lt;p&gt;泛型不变性对嵌套同样成立，而且很容易踩：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;List&amp;lt;? extends Number&amp;gt;&amp;gt; outer = new ArrayList&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt;();   // 编译不过！
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;javac MoreRules.java
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;MoreRules.java:21: 错误: 不兼容的类型: ArrayList&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt;无法转换为List&amp;lt;List&amp;lt;? extends Number&amp;gt;&amp;gt;
        List&amp;lt;List&amp;lt;? extends Number&amp;gt;&amp;gt; outer = new ArrayList&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt;();
                                             ^
1 个错误
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 &lt;code&gt;new ArrayList&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt;()&lt;/code&gt; 的类型是 &lt;code&gt;ArrayList&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt;&lt;/code&gt;，外层实参是 &lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt;，而要求的是 &lt;code&gt;List&amp;lt;? extends Number&amp;gt;&lt;/code&gt;——&lt;strong&gt;外层没写通配符，就必须精确相等&lt;/strong&gt;，&lt;code&gt;List&amp;lt;Integer&amp;gt;&lt;/code&gt; 不等于 &lt;code&gt;List&amp;lt;? extends Number&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;两种正确写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 写法一：外层不放通配符 → 先建空表，再往里 add（元素类型正是 List&amp;lt;? extends Number&amp;gt;）
List&amp;lt;List&amp;lt;? extends Number&amp;gt;&amp;gt; outer = new ArrayList&amp;lt;&amp;gt;();
outer.add(new ArrayList&amp;lt;Integer&amp;gt;(List.of(1, 2)));

// 写法二：外层也放开 → 可以直接用子类型初始化
List&amp;lt;? extends List&amp;lt;? extends Number&amp;gt;&amp;gt; outer2 = new ArrayList&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;outer -&amp;gt; [[1, 2]]
outer2 -&amp;gt; []
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;规则一句话：&lt;strong&gt;通配符只放宽它所在的那一层，不向下传递。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 完整验证代码&lt;/h2&gt;
&lt;h3&gt;8.1 文件清单&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;gen03/
├── CovarianceDemo.java      # 数组协变 vs 泛型不变
├── PecsDemo.java            # sum / fill / copy + JDK 自带 API
├── CaptureDemo.java         # swap 的捕获辅助方法
├── ComparableDemo.java      # Comparable&amp;lt;? super T&amp;gt;
├── JdkSignatures.java       # 反射打印 JDK 真实签名
├── MoreRules.java           # 赋值规则、嵌套通配符
├── BadInvariant.java        # 故意编译失败：不变性
├── BadExtends.java          # 故意编译失败：? extends 不能写
├── BadSuper.java            # 故意编译失败：? super 只能读成 Object
├── BadCapture.java          # 故意编译失败：CAP#1
├── BadComparable.java       # 故意编译失败：Comparable&amp;lt;T&amp;gt; 太严
└── BadOnes.java             # 故意编译失败：4 个常见错误合集
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;javac CovarianceDemo.java PecsDemo.java CaptureDemo.java ComparableDemo.java JdkSignatures.java MoreRules.java
java  CovarianceDemo &amp;amp;&amp;amp; java PecsDemo &amp;amp;&amp;amp; java CaptureDemo &amp;amp;&amp;amp; java ComparableDemo &amp;amp;&amp;amp; java MoreRules &amp;amp;&amp;amp; java JdkSignatures
javac BadInvariant.java BadExtends.java BadSuper.java BadCapture.java BadComparable.java BadOnes.java   # 看报错
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.2 CovarianceDemo.java&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class CovarianceDemo {
    public static void main(String[] args) {
        // 数组是协变的：编译通过，运行期才炸
        Object[] arr = new String[3];
        try {
            arr[0] = 1;                      // 把 Integer 塞进 String[]
        } catch (ArrayStoreException e) {
            System.out.println(&quot;运行期: &quot; + e);
        }
        // 泛型是不变的：下面这行连编译都过不了（见 BadInvariant.java）
        // List&amp;lt;Object&amp;gt; list = new ArrayList&amp;lt;String&amp;gt;();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;运行期: java.lang.ArrayStoreException: java.lang.Integer
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.3 PecsDemo.java&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import java.util.*;

public class PecsDemo {
    /** 只读：生产者用 extends */
    static double sum(List&amp;lt;? extends Number&amp;gt; src) {
        double s = 0;
        for (Number n : src) s += n.doubleValue();
        return s;
    }

    /** 只写：消费者用 super */
    static void fill(List&amp;lt;? super Integer&amp;gt; dest, int n) {
        for (int i = 0; i &amp;lt; n; i++) dest.add(i);
    }

    /** 一读一写：src 是生产者、dest 是消费者，各用各的 */
    static &amp;lt;T&amp;gt; void copy(List&amp;lt;? super T&amp;gt; dest, List&amp;lt;? extends T&amp;gt; src) {
        for (T t : src) dest.add(t);
    }

    public static void main(String[] args) {
        System.out.println(&quot;sum(List&amp;lt;Integer&amp;gt;) = &quot; + sum(List.of(1, 2, 3)));
        System.out.println(&quot;sum(List&amp;lt;Double&amp;gt;)  = &quot; + sum(List.of(1.5, 2.5)));
        System.out.println(&quot;sum(List.of(1, 2L, 3.0f, 4.0)) = &quot; + sum(List.of(1, 2L, 3.0f, 4.0)));

        List&amp;lt;Number&amp;gt; numbers = new ArrayList&amp;lt;&amp;gt;();
        fill(numbers, 3);                    // List&amp;lt;Number&amp;gt; 是 List&amp;lt;? super Integer&amp;gt;
        System.out.println(&quot;fill -&amp;gt; &quot; + numbers);

        List&amp;lt;Object&amp;gt; objects = new ArrayList&amp;lt;&amp;gt;();
        copy(objects, List.of(&quot;a&quot;, &quot;b&quot;));    // List&amp;lt;Object&amp;gt; dest + List&amp;lt;String&amp;gt; src
        System.out.println(&quot;copy -&amp;gt; &quot; + objects);

        // JDK 自带的同一原则：
        List&amp;lt;Integer&amp;gt; ints = new ArrayList&amp;lt;&amp;gt;();
        Collections.addAll(ints, 3, 1, 2);   // addAll(Collection&amp;lt;? extends E&amp;gt;)
        System.out.println(&quot;before sort: &quot; + ints);
        ints.sort(Comparator.reverseOrder()); // sort(Comparator&amp;lt;? super E&amp;gt;)
        System.out.println(&quot;after sort:  &quot; + ints);
        List&amp;lt;Integer&amp;gt; copyTarget = Arrays.asList(new Integer[3]);
        Collections.copy(copyTarget, ints);  // copy(List&amp;lt;? super T&amp;gt;, List&amp;lt;? extends T&amp;gt;)
        System.out.println(&quot;copied:      &quot; + copyTarget);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sum(List&amp;lt;Integer&amp;gt;) = 6.0
sum(List&amp;lt;Double&amp;gt;)  = 4.0
sum(List.of(1, 2L, 3.0f, 4.0)) = 10.0
fill -&amp;gt; [0, 1, 2]
copy -&amp;gt; [a, b]
before sort: [3, 1, 2]
after sort:  [3, 2, 1]
copied:      [3, 2, 1]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.4 CaptureDemo.java&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import java.util.*;

public class CaptureDemo {
    /** 对外保持最宽松的签名 */
    static void swap(List&amp;lt;?&amp;gt; list, int i, int j) {
        swapHelper(list, i, j);
    }

    /** 私有辅助方法用 &amp;lt;T&amp;gt; 把这个 capture 命名下来 */
    private static &amp;lt;T&amp;gt; void swapHelper(List&amp;lt;T&amp;gt; list, int i, int j) {
        T tmp = list.get(i);
        list.set(i, list.get(j));
        list.set(j, tmp);
    }

    public static void main(String[] args) {
        List&amp;lt;Integer&amp;gt; ints = new ArrayList&amp;lt;&amp;gt;(List.of(1, 2, 3));
        swap(ints, 0, 2);
        System.out.println(&quot;swap -&amp;gt; &quot; + ints);

        List&amp;lt;?&amp;gt; unknown = ints;
        System.out.println(&quot;size=&quot; + unknown.size() + &quot;, get(0)=&quot; + unknown.get(0)
                + &quot;, class=&quot; + unknown.get(0).getClass().getSimpleName());
        unknown.remove(0);
        System.out.println(&quot;after remove(0): &quot; + unknown);
        unknown.clear();
        System.out.println(&quot;after clear(): &quot; + unknown);
        // unknown.add(1);                   // 编译不过：捕获类型无法确定
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;swap -&amp;gt; [3, 2, 1]
size=3, get(0)=3, class=Integer
after remove(0): [2, 1]
after clear(): []
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.5 ComparableDemo.java&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import java.util.*;

public class ComparableDemo {
    static class Base implements Comparable&amp;lt;Base&amp;gt; {
        final int v;
        Base(int v) { this.v = v; }
        @Override public int compareTo(Base o) { return Integer.compare(v, o.v); }
        @Override public String toString() { return getClass().getSimpleName() + &quot;(&quot; + v + &quot;)&quot;; }
    }

    static class Sub extends Base {
        Sub(int v) { super(v); }
    }

    /** 子类自己没实现 Comparable&amp;lt;Sub&amp;gt;，只从父类继承了 Comparable&amp;lt;Base&amp;gt;，所以必须写 ? super T */
    static &amp;lt;T extends Comparable&amp;lt;? super T&amp;gt;&amp;gt; T max(List&amp;lt;? extends T&amp;gt; list) {
        T best = list.get(0);
        for (T t : list) if (t.compareTo(best) &amp;gt; 0) best = t;
        return best;
    }

    public static void main(String[] args) {
        Sub s = max(List.of(new Sub(3), new Sub(1), new Sub(2)));   // T = Sub，返回值就是 Sub，不用强转
        System.out.println(&quot;max(Sub)     = &quot; + s + &quot;  运行期类型=&quot; + s.getClass().getSimpleName());
        System.out.println(&quot;max(String)  = &quot; + max(List.of(&quot;b&quot;, &quot;c&quot;, &quot;a&quot;)));
        System.out.println(&quot;max(Integer) = &quot; + max(List.of(5, 9, 7)));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;max(Sub)     = Sub(3)  运行期类型=Sub
max(String)  = c
max(Integer) = 9
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.6 JdkSignatures.java&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import java.lang.reflect.*;
import java.util.*;
import java.util.stream.Stream;

public class JdkSignatures {
    static void show(Class&amp;lt;?&amp;gt; owner, String name, Class&amp;lt;?&amp;gt;... params) {
        try {
            Method m = owner.getMethod(name, params);
            StringBuilder sb = new StringBuilder();
            for (TypeVariable&amp;lt;Method&amp;gt; tv : m.getTypeParameters()) {
                sb.append(&quot;&amp;lt;&quot;).append(tv.getName());
                for (Type b : tv.getBounds()) sb.append(&quot; extends &quot;).append(b.getTypeName());
                sb.append(&quot;&amp;gt; &quot;);
            }
            String ps = Stream.of(m.getGenericParameterTypes()).map(Type::getTypeName).reduce((a, b) -&amp;gt; a + &quot;, &quot; + b).orElse(&quot;&quot;);
            System.out.printf(&quot;%-34s %s%s %s(%s)%n&quot;, owner.getSimpleName() + &quot;.&quot; + name, sb, m.getGenericReturnType().getTypeName(), name, ps);
        } catch (Exception e) {
            System.out.println(&quot;ERR &quot; + name + &quot;: &quot; + e);
        }
    }

    public static void main(String[] args) {
        show(Collection.class, &quot;addAll&quot;, Collection.class);
        show(List.class, &quot;sort&quot;, Comparator.class);
        show(Collections.class, &quot;copy&quot;, List.class, List.class);
        show(Collections.class, &quot;fill&quot;, List.class, Object.class);
        show(Collections.class, &quot;max&quot;, Collection.class);
        show(Collections.class, &quot;sort&quot;, List.class);
        show(Collections.class, &quot;addAll&quot;, Collection.class, Object[].class);
        show(Stream.class, &quot;forEach&quot;, java.util.function.Consumer.class);
        show(Stream.class, &quot;map&quot;, java.util.function.Function.class);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.7 MoreRules.java&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import java.util.*;

public class MoreRules {
    public static void main(String[] args) {
        // 通配符是「父类型」的方向：? extends 让多个具体实参都能赋给同一个变量
        List&amp;lt;? extends Number&amp;gt; producer = new ArrayList&amp;lt;Integer&amp;gt;();
        System.out.println(producer.getClass().getSimpleName() + &quot; -&amp;gt; &quot; + producer.size());
        producer = new ArrayList&amp;lt;Double&amp;gt;();          // 换一个实现类也行
        producer = new ArrayList&amp;lt;Number&amp;gt;();          // 上界本身也行
        producer.add(null);                          // 唯一的合法写入
        System.out.println(&quot;add(null) 之后 size=&quot; + producer.size());

        // ? super 同理
        List&amp;lt;? super Integer&amp;gt; consumer = new ArrayList&amp;lt;Integer&amp;gt;();
        consumer = new ArrayList&amp;lt;Number&amp;gt;();
        consumer = new ArrayList&amp;lt;Object&amp;gt;();
        consumer.add(42);
        System.out.println(&quot;consumer -&amp;gt; &quot; + consumer);

        // 嵌套：外层不变，声明成 List&amp;lt;List&amp;lt;? extends Number&amp;gt;&amp;gt; 之后，只能先建空表再 add
        List&amp;lt;List&amp;lt;? extends Number&amp;gt;&amp;gt; outer = new ArrayList&amp;lt;&amp;gt;();
        outer.add(new ArrayList&amp;lt;Integer&amp;gt;(List.of(1, 2)));   // 元素类型正是 List&amp;lt;? extends Number&amp;gt;
        // 外层也放开，才能直接用 ArrayList&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; 初始化
        List&amp;lt;? extends List&amp;lt;? extends Number&amp;gt;&amp;gt; outer2 = new ArrayList&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt;();
        System.out.println(&quot;outer -&amp;gt; &quot; + outer);
        System.out.println(&quot;outer2 -&amp;gt; &quot; + outer2);

        // List&amp;lt;Object&amp;gt; 与 List&amp;lt;?&amp;gt; 的区别
        List&amp;lt;Object&amp;gt; objects = new ArrayList&amp;lt;&amp;gt;();
        objects.add(&quot;随便放&quot;);                        // List&amp;lt;Object&amp;gt; 能写
        List&amp;lt;?&amp;gt; anything = objects;                  // 任何 List&amp;lt;X&amp;gt; 都能赋给它
        Object first = anything.get(0);              // 只能读成 Object
        System.out.println(&quot;anything.size=&quot; + anything.size() + &quot;, first=&quot; + first);
        System.out.println(&quot;List&amp;lt;Object&amp;gt; 能 add，List&amp;lt;?&amp;gt; 只能读、remove、clear&quot;);

        // 数组协变 vs 泛型不变，放一起对比
        Number[] nums = new Integer[2];              // 数组：编译通过，运行期可能炸
        try { nums[0] = 1.5; } catch (ArrayStoreException e) { System.out.println(&quot;数组协变: &quot; + e); }
        System.out.println(&quot;数组协变是编译期&apos;允许&apos;、运行期兜底；泛型是编译期直接拒绝&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;ArrayList -&amp;gt; 0
add(null) 之后 size=1
consumer -&amp;gt; [42]
outer -&amp;gt; [[1, 2]]
outer2 -&amp;gt; []
anything.size=1, first=随便放
List&amp;lt;Object&amp;gt; 能 add，List&amp;lt;?&amp;gt; 只能读、remove、clear
数组协变: java.lang.ArrayStoreException: java.lang.Double
数组协变是编译期&apos;允许&apos;、运行期兜底；泛型是编译期直接拒绝
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.8 故意编译失败的文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import java.util.*;

public class BadOnes {
    static void a(List&amp;lt;?&amp;gt; list) { }
    static void a(List&amp;lt;? extends Object&amp;gt; list) { }      // ① 通配符没带来任何区别：签名冲突

    static void b(List&amp;lt;?&amp;gt; list) {
        list.add(&quot;x&quot;);                                  // ② 无界通配符不能写入（null 除外）
    }

    static Number c(List&amp;lt;? extends Number&amp;gt; list) {
        List&amp;lt;Number&amp;gt; copy = list;                       // ③ 通配符不能反向赋给具体实参
        return copy.get(0);
    }

    static void d() {
        List&amp;lt;Object&amp;gt; objects = new ArrayList&amp;lt;String&amp;gt;(); // ④ 泛型不变：List&amp;lt;String&amp;gt; 不是 List&amp;lt;Object&amp;gt;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;BadOnes.java:5: 错误: 已在类 BadOnes中定义了方法 a(List&amp;lt;?&amp;gt;)
BadOnes.java:8: 错误: 不兼容的类型: String无法转换为CAP#1
  其中, CAP#1是新类型变量:
    CAP#1从?的捕获扩展Object
BadOnes.java:12: 错误: 不兼容的类型: List&amp;lt;CAP#1&amp;gt;无法转换为List&amp;lt;Number&amp;gt;
  其中, CAP#1是新类型变量:
    CAP#1从? extends Number的捕获扩展Number
BadOnes.java:17: 错误: 不兼容的类型: ArrayList&amp;lt;String&amp;gt;无法转换为List&amp;lt;Object&amp;gt;
4 个错误
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;③ 那条尤其值得记：&lt;code&gt;List&amp;lt;? extends Number&amp;gt;&lt;/code&gt; 只能&lt;strong&gt;读&lt;/strong&gt;，想把它当 &lt;code&gt;List&amp;lt;Number&amp;gt;&lt;/code&gt; 用是反向赋值，编译器连类型都不认。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;9. 面试问法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：为什么 &lt;code&gt;List&amp;lt;String&amp;gt;&lt;/code&gt; 不能赋给 &lt;code&gt;List&amp;lt;Object&amp;gt;&lt;/code&gt;？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;泛型是不变的。如果允许，就会出现 &lt;code&gt;List&amp;lt;Object&amp;gt; l = new ArrayList&amp;lt;String&amp;gt;(); l.add(1);&lt;/code&gt; 这种编译期合法、运行期 &lt;code&gt;checkcast&lt;/code&gt; 爆炸的代码。数组是协变的，正好演示了这个后果——&lt;code&gt;Object[] a = new String[3]; a[0] = 1;&lt;/code&gt; 编译通过、运行期 &lt;code&gt;ArrayStoreException&lt;/code&gt;。泛型选择了&quot;编译期就拒绝&quot;，代价是需要通配符来表达协变/逆变。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q2：&lt;code&gt;List&amp;lt;? extends Number&amp;gt; l = new ArrayList&amp;lt;Integer&amp;gt;(); l.add(1);&lt;/code&gt; 为什么编译不过？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;因为 &lt;code&gt;? extends Number&lt;/code&gt; 的实际元素类型对调用方未知，编译器用 &lt;code&gt;CAP#1&lt;/code&gt; 表示&quot;某个 &lt;code&gt;Number&lt;/code&gt; 的子类型&quot;，&lt;code&gt;add&lt;/code&gt; 的形参就是这个 &lt;code&gt;CAP#1&lt;/code&gt;。塞 &lt;code&gt;Integer&lt;/code&gt; 万一是 &lt;code&gt;List&amp;lt;Double&amp;gt;&lt;/code&gt; 就污染了，所以只有在类型确定时才允许写。读没问题，&lt;code&gt;l.get(0)&lt;/code&gt; 静态类型是 &lt;code&gt;Number&lt;/code&gt;；&lt;code&gt;add(null)&lt;/code&gt; 也允许，因为 &lt;code&gt;null&lt;/code&gt; 与具体类型无关。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q3：&lt;code&gt;List&amp;lt;? super Integer&amp;gt;&lt;/code&gt; 能写什么、能读成什么？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;能写 &lt;code&gt;Integer&lt;/code&gt;（以及它的子类），因为 &lt;code&gt;? super Integer&lt;/code&gt; 的每个可能取值（&lt;code&gt;Integer&lt;/code&gt;/&lt;code&gt;Number&lt;/code&gt;/&lt;code&gt;Object&lt;/code&gt;）都能接受 &lt;code&gt;Integer&lt;/code&gt;。读出来只能保证是 &lt;code&gt;Object&lt;/code&gt;，因为上界是 &lt;code&gt;Object&lt;/code&gt;；想要具体类型得强转或改用泛型方法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q4：PECS 是什么？怎么判断方向？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Producer Extends, Consumer Super：参数是数据来源（被读）用 &lt;code&gt;? extends T&lt;/code&gt;，参数是数据去向（被写）用 &lt;code&gt;? super T&lt;/code&gt;。站在方法体里看&quot;这个集合是被读还是被写&quot;即可。JDK 里例子很多：&lt;code&gt;Collections.copy(List&amp;lt;? super T&amp;gt; dest, List&amp;lt;? extends T&amp;gt; src)&lt;/code&gt;、&lt;code&gt;List.sort(Comparator&amp;lt;? super E&amp;gt;)&lt;/code&gt;、&lt;code&gt;Stream.map(Function&amp;lt;? super T, ? extends R&amp;gt;)&lt;/code&gt;（一个参数位既消费又生产，所以两条规则同时出现）。签名方向写反，方法体编译不过——这也是自检手段。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q5：编译错误里的 &lt;code&gt;CAP#1&lt;/code&gt; 是什么？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;通配符捕获（capture conversion）。编译器把 &lt;code&gt;?&lt;/code&gt; 或 &lt;code&gt;? extends X&lt;/code&gt; 换成一个内部的、调用点唯一的类型变量。&lt;code&gt;List&amp;lt;?&amp;gt; l; l.set(i, l.get(j))&lt;/code&gt; 报 &lt;code&gt;Object无法转换为CAP#1&lt;/code&gt; 就是它：&lt;code&gt;get&lt;/code&gt; 给的是 &lt;code&gt;Object&lt;/code&gt;，&lt;code&gt;set&lt;/code&gt; 要的是那个未知的 &lt;code&gt;CAP#1&lt;/code&gt;。解法是用 &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt; 辅助方法把这个类型命名下来，或者像 &lt;code&gt;Collections.swap&lt;/code&gt; 那样用原始类型 + &lt;code&gt;@SuppressWarnings&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q6：&lt;code&gt;Collections.swap(List&amp;lt;?&amp;gt;, int, int)&lt;/code&gt; 是怎么实现的？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;JDK 17 用的是原始类型：&lt;code&gt;final List l = list; l.set(i, l.set(j, l.get(i)));&lt;/code&gt;，并加 &lt;code&gt;@SuppressWarnings({&quot;rawtypes&quot;,&quot;unchecked&quot;})&lt;/code&gt;。源码注释写得很直白：&quot;通配符是可以捕获的，但那需要额外的私有辅助方法&quot;——所以标准库选了原始类型这条捷径。自己写代码时更推荐 &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt; 辅助方法，全程类型安全。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q7：&lt;code&gt;Comparable&amp;lt;? super T&amp;gt;&lt;/code&gt; 里的 &lt;code&gt;? super&lt;/code&gt; 能不能去掉？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不能，去掉会明显收窄适用范围。&lt;code&gt;T extends Comparable&amp;lt;T&amp;gt;&lt;/code&gt; 要求元素类型自己实现 &lt;code&gt;Comparable&amp;lt;自己&amp;gt;&lt;/code&gt;；现实里大量类的比较能力来自父类（典型：&lt;code&gt;java.sql.Timestamp extends Date&lt;/code&gt;，&lt;code&gt;Date implements Comparable&amp;lt;Date&amp;gt;&lt;/code&gt;），此时 &lt;code&gt;Comparable&amp;lt;T&amp;gt;&lt;/code&gt; 不成立、推断直接报&quot;推论变量 T 具有不兼容的上限&quot;。写成 &lt;code&gt;Comparable&amp;lt;? super T&amp;gt;&lt;/code&gt; 就是允许&quot;某个父类型实现了 &lt;code&gt;Comparable&lt;/code&gt;&quot;。&lt;code&gt;Collections.max&lt;/code&gt;/&lt;code&gt;Collections.sort&lt;/code&gt; 都是这个签名；&lt;code&gt;max&lt;/code&gt; 里那个 &lt;code&gt;Object &amp;amp;&lt;/code&gt; 是为了与泛型前 &lt;code&gt;max(Collection)&lt;/code&gt; 的擦除签名保持二进制兼容。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q8：方法参数什么时候用通配符、什么时候用 &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt;？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;类型参数在签名的多个位置出现、需要建立关联（参数与参数、参数与返回值）时用 &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt;，例如 &lt;code&gt;&amp;lt;T&amp;gt; void copy(List&amp;lt;? super T&amp;gt;, List&amp;lt;? extends T&amp;gt;)&lt;/code&gt;、&lt;code&gt;&amp;lt;T&amp;gt; T first(List&amp;lt;? extends T&amp;gt;)&lt;/code&gt;；只出现在一个位置、纯为放宽接收范围时用通配符，例如 &lt;code&gt;void print(List&amp;lt;?&amp;gt;)&lt;/code&gt;。写成 &lt;code&gt;static &amp;lt;T&amp;gt; void print(List&amp;lt;T&amp;gt;)&lt;/code&gt; 能编译，但 &lt;code&gt;T&lt;/code&gt; 毫无用途，等价于 &lt;code&gt;List&amp;lt;?&amp;gt;&lt;/code&gt;，属于噪音。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;10. 小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;泛型&lt;strong&gt;不变&lt;/strong&gt;：&lt;code&gt;List&amp;lt;String&amp;gt;&lt;/code&gt; 不是 &lt;code&gt;List&amp;lt;Object&amp;gt;&lt;/code&gt;；数组是协变的，代价是 &lt;code&gt;ArrayStoreException&lt;/code&gt; 留到运行期。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;? extends T&lt;/code&gt; = &lt;strong&gt;只读&lt;/strong&gt;：&lt;code&gt;get&lt;/code&gt; 得到 &lt;code&gt;T&lt;/code&gt;，&lt;code&gt;add&lt;/code&gt; 除 &lt;code&gt;null&lt;/code&gt; 外一律编译不过，报错主体是 &lt;code&gt;CAP#1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;? super T&lt;/code&gt; = &lt;strong&gt;只写&lt;/strong&gt;：&lt;code&gt;add(T)&lt;/code&gt; 可以，&lt;code&gt;get&lt;/code&gt; 只能读成 &lt;code&gt;Object&lt;/code&gt;（上界是 &lt;code&gt;Object&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PECS&lt;/strong&gt;：Producer Extends, Consumer Super；&lt;code&gt;Stream.map(Function&amp;lt;? super T, ? extends R&amp;gt;)&lt;/code&gt; 一个参数位同时体现两条规则。&lt;/li&gt;
&lt;li&gt;捕获（&lt;code&gt;CAP#1&lt;/code&gt;）用 &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt; 辅助方法命名下来；&lt;code&gt;Collections.swap&lt;/code&gt; 用原始类型 + &lt;code&gt;@SuppressWarnings&lt;/code&gt; 走捷径，取舍写在源码注释里。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;List&amp;lt;?&amp;gt;&lt;/code&gt;：只读、&lt;code&gt;remove&lt;/code&gt;/&lt;code&gt;clear&lt;/code&gt;/&lt;code&gt;size&lt;/code&gt; 可用、只能 &lt;code&gt;add(null)&lt;/code&gt;；&lt;code&gt;&amp;lt;? extends Object&amp;gt;&lt;/code&gt; 与它完全等价。&lt;/li&gt;
&lt;li&gt;边界里的 &lt;code&gt;? super&lt;/code&gt;：&lt;code&gt;&amp;lt;T extends Comparable&amp;lt;? super T&amp;gt;&amp;gt;&lt;/code&gt; 让子类继承父类比较能力时也能用，&lt;code&gt;Collections.max&lt;/code&gt; 就是这么写的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;记忆钩子：&lt;strong&gt;&quot;extends 读、super 写；&lt;code&gt;CAP#1&lt;/code&gt; 不认识，&lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt; 帮它起个名。&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;11. 下一步&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 手跑第 8 节代码，对照参考输出。&lt;/li&gt;
&lt;li&gt;[ ] 故意编译失败的那几个文件，逐个把报错读一遍，确认自己能解释 &lt;code&gt;CAP#1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;[ ] 随便挑一个 JDK 泛型 API（&lt;code&gt;Stream.map&lt;/code&gt;、&lt;code&gt;Collections.addAll&lt;/code&gt;）解释它为什么是这个方向。&lt;/li&gt;
&lt;li&gt;[ ] 闭卷复述第 9 节 8 个问题。&lt;/li&gt;
&lt;li&gt;[ ] 写下一篇：&lt;code&gt;04-泛型在框架中的应用&lt;/code&gt;（Spring &lt;code&gt;ResolvableType&lt;/code&gt;、&lt;code&gt;Class&amp;lt;T&amp;gt;&lt;/code&gt; 的 API 设计、Jackson &lt;code&gt;TypeReference&lt;/code&gt;、MyBatis 的泛型 Mapper）。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>akm</author></item></channel></rss>