【Java结构化梳理】泛型-初步了解-下

前两篇文章,我们了解了泛型是什么,泛型相关的问题。这篇文章,我会将初步了解阶段做个总结,把泛型从出现到发展串联一遍,结尾会付上我简单实现的源码。

一、泛型系统的发展:

解读:

1、 泛型之前(JDK1.4及之前),Java 用 Object 实现"能装任何类型",但完全没有类型安全。

java 复制代码
// JDK 1.4 及之前
List list = new ArrayList();   // 没有 <String>,这就是"原始类型"

list.add("hello");
list.add(100);               // 编译不报错!
list.add(new Date());         // 什么都能放进去

// 取出来必须强转
String s = (String) list.get(0);  // ✅ 运行时成功
String s2 = (String) list.get(1); // ❌ 运行时 ClassCastException!

Java想在满足"能装任何类型"同时,又能把类型检查从运行时提前到编译期,提升系统的高可用性。

2、加泛型的两种方式

因为泛型是<>+占位符+类型不确定,所以需要底层支持。

1)修改 JVM,在运行时保留并识别泛型类型。

好处:功能强大(可以 new T()、instanceof)

坏处:需要改 JVM 规范、所有旧代码可能受影响、成本极高!

想想,一定要推行的话,使用者需要强升 Java 版本,多高的代价。

2)不改 JVM,编译器层面实现(擦除 Erasure)

好处:不用动 JVM、版本兼容、成本低

坏处:功能受限

3、通配符是在有限范围内最大扩展泛型的应用范围

没有通配符的写法:

java 复制代码
// 没有通配符,你只能这样写方法

// 方法1:只能处理 String 列表
void processStringList(List<String> list) { }

// 方法2:只能处理 Integer 列表  
void processIntegerList(List<Integer> list) { }

// 方法3:只能处理 Number 列表
void processNumberList(List<Number> list) { }

// 问题:
processStringList(new ArrayList<String>());    // ✅ 只能这个
processStringList(new ArrayList<Integer>());   // ❌ 编译错误!

// 想要一个"能处理任何列表"的方法?没有通配符做不到!

加上通配符

java 复制代码
// 一个方法搞定所有数字类型列表
void processNumberList(List<? extends Number> list) { }

processNumberList(new ArrayList<Integer>());  // ✅
processNumberList(new ArrayList<Double>());   // ✅
processNumberList(new ArrayList<Long>());     // ✅
processNumberList(new ArrayList<Number>());   // ✅

// 一个方法搞定所有可读场景
int sum(List<? extends Number> list) {
    int total = 0;
    for (Number n : list) {
        total += n.intValue();
    }
    return total;
}

通配符,真正的让泛型从"安全"走向"实用"。

二、泛型的依赖链路

流程图是让 AI 整理生成:

1、第一层(基础依赖),OOP 继承体系

为什么说OOP 继承体系是泛型的基础依赖呢?

java 复制代码
// 没有继承体系,这行代码根本写不出来
public class Box<T extends Number> { }

// 因为 "extends" 本身就是 OOP 的关键字!
// 它依赖 "Number 是 Object 的子类" 这个 OOP 关系

泛型完全遵循OOP 继承体系的依赖规则,没有 extends 关键字、没有继承关系,泛型就无法约束类型参数的范围,只能操作 Object

2、编译器机制

编译器需要识别<>,做类型检查(引入泛型的关键,将类型检查提前到编译器),类型推断,字节码生成,泛型擦除。

类型推断

java 复制代码
// 旧写法(Java 5-6):左右两边都要写类型
ArrayList<String> list = new ArrayList<String>();  // 冗余

// 新写法(Java 7+):右边用 <> 省略,编译器自动推断
ArrayList<String> list = new ArrayList<>();        // ✅ 编译器推断出是 String

3、第三层(核心机制)

类型擦除的机制,最重要的是确定了边界。

无边界 = T 只能当 Object 用

java 复制代码
// 没有边界
public class Box<T> {
    private T value;
    
    // 你能对 value 做什么?
    
    value.toString();     // ✅ Object 有这个方法
    value.equals(other);  // ✅ Object 也有
    
    value.intValue();     // ❌ 编译错误!Object 没有
    value.length();       // ❌ 编译错误!Object 也没有
}

有边界 = 能用更多方法

java 复制代码
// 有边界
public class Box<T extends Number> {
    private T value;
    
    value.toString();      // ✅ Object 有
    value.equals(other);   // ✅ Object 有
    
    value.intValue();      // ✅ Number 有!
    value.doubleValue();   // ✅ Number 也有!
    value.longValue();     // ✅ 都有!
}

4、核心四要素:

占位符、类型擦除、边界、通配符

1、占位符 T:

底层原理:类型参数,编译器把 T 当作一个符号,记录下来,编译时做检查,然后替换掉。

java 复制代码
public class Box<T> {
    private T value;
    // T 是什么?不知道,等使用时再决定
}
// T 就是一个占位符,叫"类型参数"

2、类型擦除:

底层原理:编译器把所有 <T> 替换成它的上界类型(默认是 Object)。运行时 JVM 看到的只有原始类型。

java 复制代码
编译前(源码)          编译后(字节码)
──────────────         ──────────────

Box<String>     →      Box           (T 被替换为 Object)
Box<Integer>    →      Box           (也是 Object)

<T extends Number>
Box<Integer>    →      Box<Number>    (T 被替换为边界 Number)

3、边界-限制范围

底层原理:通过继承关系,限制 T 的范围

java 复制代码
// 无边界 → 擦除为 Object
public class Box<T>

// 有上界 → 擦除为 Number
public class Box<T extends Number>
// 这里用到了继承关系!T 必须是 Number 的子类

4、通配符:增加灵活性

底层原理 :基于类型论中的协变/逆变理论,不是 Java 发明的,而是数学上的概念。

java 复制代码
List<? extends Number>  // 协变
List<? super Integer>   // 逆变

5、它们如何组合工作?

以一段代码为例,看完整流程:

java 复制代码
// 你写的代码
public class Pair<T> {
    private T first;
    private T second;
    
    public void setFirst(T first) { this.first = first; }
    public T getFirst() { return first; }
}

// 使用时
Pair<Integer> p = new Pair<>();
p.setFirst(100);
// p.setFirst("hello"); 编译报错
Integer i = p.getFirst();

1、解析类型参数

发现 T,是待确认类型;

在使用时Pair<Integer> p = new Pair<>();确认是 Integer 类型

2、类型检测

p.setFirst(100);

// p.setFirst("hello"); 编译报错

Integer 类型可以,非 Integer 不行。

3、插入强制转换

Integer i = p.getFirst() → 实际变为 Integer i = (Integer) p.getFirst()

4、类型擦除

擦除后,实际执行是

java 复制代码
class Pair {                          // T 被替换为 Object
    Object getFirst() { return first; }  // 返回的是 Object!
}

// 这行代码就变成了:
Object obj = p.getFirst();            // 拿到的是 Object!
Integer i = (Integer) obj;           // 需要强转才能赋给 Integer

5、运行时执行

运行时只有 Pair 类,Object 类,没有泛型。

三、升华总结

泛型是一种类型参数化机制,组成部分有:<>、占位符、边界、通配符,用来使方法、接口等具有通用性。泛型的底层原理:占位符+边界+擦除+通配符,是既保证了类型通用性,类型检查提前到编译期,又兼容了历史版本(JDK1.4 以前),对 JVM影响范围最小的设计方案。

四、最后给大家举个栗子

java 复制代码
package com.example.springbootuniq.generics;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

public class ExtendsAndSuper {

    public static void main(String[] args) {

        List<Apple> apples = new ArrayList<>();
        apples.add(new Apple("红富士", "红色", BigDecimal.valueOf(10.01)));
        apples.add(new Apple("黄元帅", "黄色", BigDecimal.valueOf(9.01)));
        apples.add(new Apple("青苹果", "青色", BigDecimal.valueOf(8.01)));

        List<Banana> bananas = new ArrayList<>();
        bananas.add(new Banana("香蕉", "黄色", BigDecimal.valueOf(5.01)));
        bananas.add(new Banana("八角香蕉", "黄色", BigDecimal.valueOf(6.01)));
        bananas.add(new Banana("进口香蕉", "绿色", BigDecimal.valueOf(4)));
        List<Fruit> fruits = new ArrayList<>();

        // 实现集合间的通用数据转移
        convert(apples, fruits);
        convert(bananas, fruits);
        System.out.println("水果的总数量是" + fruits.size());

        // 通用数据公共处理方法
        BigDecimal sum = sum(fruits);
        System.out.println("水果的总金额是" + sum);
        BigDecimal applePrice = sum(apples);
        System.out.println("苹果的总金额是" + applePrice);
        BigDecimal bananaPrice = sum(bananas);
        System.out.println("香蕉的总金额是" + bananaPrice);

        // 再加一个苹果
        add(fruits);

        // 会员价:水果都打八折
        applyDiscount(fruits, 0.8);

        // 检测桥方法
        detectBridgeMethods(Apple.class);

        // 清空
        clear(fruits);


    }

    /**
     * 泛型通配符最标准、最推荐的用法之一,用于实现集合间的通用数据转移。
     *
     * @param original
     * @param target
     */

    public static <T> void convert(List<? extends T> original, List<? super T> target) {
        System.out.println("? extends T 运行时类型:" + original.getClass());
        System.out.println("? super T 运行时类型:" + target.getClass());
        for (T t : original) {
            target.add(t);
        }
    }

    /**
     * 上界通配符 ? extends T (只读/生产者)
     * 场景:当你只需要从集合中读取数据,而不需要写入时。
     * 特点:可以获取元素,类型视为 T;不能添加任何元素(除了 null),因为编译器不知道列表实际持有的具体子类是什么
     *
     * @param fruits
     * @return
     */
    public static BigDecimal sum(List<? extends Fruit> fruits) {
        // 只能读不能写,编译不通过:fruits.add(new Apple("苹果", "红色", BigDecimal.valueOf(10.01)))
        System.out.println("List<? extends Fruit> 运行时类型:" + fruits.getClass());
        return fruits.stream().map(Fruit::getPrice).reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    /**
     * 下界通配符 ? super T (只写/消费者)
     * 场景:当你需要向集合中写入数据,但不需要从集合中读取数据时。
     * 特点:可以添加元素,类型视为 T;不能获取元素(因为编译器不知道列表实际持有的具体子类是什么)
     *
     * @param list
     */
    public static void add(List<? super Fruit> list) {
        System.out.println("List<? super Fruit> 运行时类型:" + list.getClass());
        list.add(new Apple("烂苹果", "黑色", BigDecimal.valueOf(1.01)));
    }

    /**
     * 无界通配符 ? (未知类型)
     * 场景:当你完全不关心集合中的元素类型,或者方法逻辑与元素类型无关时。
     *
     * @param list
     */

    public static void clear(List<?> list) {
        System.out.println("List<?> 运行时类型:" + list.getClass());
        list.clear();
    }

    // 如果你需要一个既读又写的方法,不要使用通配符,而是直接使用泛型类型参数 <T> 和 List<T>。
    public static void applyDiscount(List<Fruit> fruits, double discountRate) {
        // 1. 获取容器运行时类型
        System.out.println("容器运行时类型: " + fruits.getClass().getSimpleName());
        for (Fruit fruit : fruits) { // 【读】获取当前价格
            // 2. 【关键验证】获取每个元素在堆内存中的真实类型
            String realType = fruit.getClass().getSimpleName();


            BigDecimal oldPrice = fruit.getPrice();
            BigDecimal newPrice = oldPrice.multiply(BigDecimal.valueOf(discountRate));
            fruit.setPrice(newPrice); // 【写】修改对象内部状态
            System.out.println("正在处理: [" + realType + "] - 原价: " + fruit.getPrice() + "------> 折后价: " + fruit.getPrice());
            // 注意:如果是要替换列表中的对象引用,则需要 list.set(index, newFruit),这也要求列表类型精确匹配
        }
    }


    /**
     * 检测指定类中是否包含桥方法,并打印详细信息
     * 桥方法是编译器为了处理泛型擦除后的多态性而自动生成的合成方法
     *
     * @param clazz 要检测的类
     */
    public static void detectBridgeMethods(Class<?> clazz) {
        System.out.println("=== 检测类: " + clazz.getName() + " 中的桥方法 ===");

        // 获取所有声明的方法(包括私有、保护、公有,但不包括继承的)
        java.lang.reflect.Method[] methods = clazz.getDeclaredMethods();
        boolean foundBridge = false;

        for (java.lang.reflect.Method method : methods) {
            // isBridge() 是 Java 反射 API 中专门用于判断是否为桥方法的方法
            if (method.isBridge()) {
                foundBridge = true;
                System.out.println("发现桥方法:");
                System.out.println("  - 方法名: " + method.getName());
                System.out.println("  - 返回类型: " + method.getReturnType().getSimpleName());
                System.out.println("  - 参数类型: " + java.util.Arrays.toString(method.getParameterTypes()));
                System.out.println("  - 修饰符: " + java.lang.reflect.Modifier.toString(method.getModifiers()));
                System.out.println("  - 是合成方法: " + method.isSynthetic()); // 桥方法通常也是合成方法
                System.out.println("--------------------");
            }
        }

        if (!foundBridge) {
            System.out.println("未检测到桥方法。");
        }
        System.out.println();
    }


}
java 复制代码
package com.example.springbootuniq.generics;

import java.math.BigDecimal;

public class Fruit {
    private String name;
    private String color;
    private BigDecimal price;
    public Fruit(String name, String color, BigDecimal price) {
        this.name = name;
        this.color = color;
        this.price = price;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getColor() {
        return color;
    }
    public void setColor(String color) {
        this.color = color;
    }
    public BigDecimal getPrice() {
        return price;
    }
    public void setPrice(BigDecimal price) {
        this.price = price;
    }
}
java 复制代码
package com.example.springbootuniq.generics;

import java.math.BigDecimal;

public class Apple extends Fruit{
    public Apple(String name, String color, BigDecimal price) {
        super(name, color, price);
    }
}
java 复制代码
package com.example.springbootuniq.generics;

import java.math.BigDecimal;

public class Banana extends Fruit{
    public Banana(String name, String color, BigDecimal price) {
        super(name, color, price);
    }
}
相关推荐
逝水如流年轻往返染尘1 小时前
JAVA中的String类
java
一只叫煤球的猫1 小时前
ThreadForge 1.2.0 发布:让 Java 并发代码更好写,这次补齐了高阶编排、示例与观测能力
java·设计模式·设计
counting money1 小时前
Spring框架基础(依赖注入-半注解形式)
java·后端·spring
CN-Dust1 小时前
【C++】for循环例题专题
java·c++·算法
SmartRadio1 小时前
ESP32-S3 双模式切换实现:兼顾手机_路由器连接与WiFi长距离通信 (采用Arduino代码框架)
开发语言·智能手机·esp32·长距离wifi
njsgcs2 小时前
solidworks自动标注折弯4 无向图 c#
开发语言·c#·solidworks
染夕陌木2 小时前
RPC/服务调用框架中“方法无法应用到给定类型”错误的通用排查指南
java·ide·rpc
大大杰哥2 小时前
String常用方法
java