Java泛型详解:从入门到精通,彻底搞懂参数化类型

一、前言:为什么需要泛型?

在泛型出现之前(Java 5之前),我们使用集合时会面临一个致命问题:类型不安全,且需要大量强制类型转换,代码繁琐且易出错。

先看一个没有泛型的反面案例,感受一下痛点:

import java.util.ArrayList;
import java.util.List;

public class NoGenericDemo {
    public static void main(String[] args) {
        // 没有泛型,ArrayList默认存储Object类型
        List list = new ArrayList();
        list.add("Java泛型"); // 存入字符串
        list.add(123); // 存入整数(语法不报错,类型混乱)
        list.add(true); // 存入布尔值
        
        // 取值时必须强制转换,否则报错
        String str = (String) list.get(0); // 正常转换
        String num = (String) list.get(1); // 运行时报错:ClassCastException
    }
}

运行结果会抛出 ClassCastException(类型转换异常),因为我们试图将整数类型强制转换成字符串类型。

这个案例暴露了无泛型的3个核心问题:

  • 类型不安全:集合可以存储任意类型的数据,编译期无法校验,只能在运行时暴露异常,风险高。
  • 代码繁琐:每次取值都需要强制类型转换,增加代码冗余。
  • 可读性差:从代码中无法直接看出集合存储的是什么类型的数据,需要阅读上下文才能判断。

而泛型的出现,就是为了解决这些问题——将数据类型参数化,让类/接口/方法在定义时不指定具体类型,在使用时再明确类型,实现“一次定义,多种使用”,同时保证类型安全。

二、泛型的核心定义

泛型(Generic),本质是参数化类型,即把“数据类型”当作一个参数,传递给类、接口或方法。

核心思想:在定义时不明确具体类型,在使用时(创建对象、调用方法)再指定具体类型,让代码更通用、更安全、更简洁。

举个简单的例子,用泛型优化上面的集合案例:

import java.util.ArrayList;
import java.util.List;

public class GenericDemo {
    public static void main(String[] args) {
        // 用泛型指定集合存储String类型
        List<String> list = new ArrayList<String>();
        list.add("Java泛型"); // 正常存入
        // list.add(123); // 编译期直接报错,无法存入非String类型
        // list.add(true); // 编译期报错
        
        // 取值时无需强制转换,直接获取String类型
        String str = list.get(0);
        System.out.println(str); // 输出:Java泛型
    }
}

优化后,编译期就会校验数据类型,不允许存入非指定类型的数据,彻底避免了运行时类型转换异常,同时省去了强制转换的代码,可读性也大幅提升。

补充:Java 7之后,支持“菱形语法”,创建对象时可以省略后面的泛型类型,编译器会自动推断,代码更简洁:

// 菱形语法(推荐写法)
List<String> list = new ArrayList<>();

三、泛型的3种核心使用方式

泛型的使用场景主要分为3类:泛型类、泛型接口、泛型方法,我们逐个讲解,结合代码示例,通俗易懂。

1. 泛型类:类定义时指定泛型参数

泛型类是最常用的场景,在类名后面加上 <T>(T是泛型参数,可自定义名称,常用T、E、K、V),表示该类是一个泛型类,T会在创建对象时被具体类型替换。

语法格式:

public class 类名<泛型参数1, 泛型参数2,...> {
    // 泛型参数可以作为成员变量、方法参数、返回值
    private 泛型参数 变量名;
    
    public 泛型参数 方法名(泛型参数 参数名) {
        return 变量名;
    }
}

实战案例:自定义一个泛型容器类,用于存储任意类型的数据(模拟简单的ArrayList):

// 泛型类,T表示要存储的数据类型(自定义参数名,也可以用E、K等)
public class GenericContainer<T> {
    // 泛型成员变量
    private T data;
    
    // 构造方法,参数为泛型类型
    public GenericContainer(T data) {
        this.data = data;
    }
    
    // 泛型方法(getter),返回值为泛型类型
    public T getData() {
        return data;
    }
    
    // 泛型方法(setter),参数为泛型类型
    public void setData(T data) {
        this.data = data;
    }
    
    // 测试
    public static void main(String[] args) {
        // 1. 创建存储String类型的容器
        GenericContainer<String> strContainer = new GenericContainer<>("Java泛型详解");
        String strData = strContainer.getData();
        System.out.println("String容器数据:" + strData);
        
        // 2. 创建存储Integer类型的容器
        GenericContainer<Integer> intContainer = new GenericContainer<>(100);
        Integer intData = intContainer.getData();
        System.out.println("Integer容器数据:" + intData);
        
        // 3. 创建存储Boolean类型的容器
        GenericContainer<Boolean> boolContainer = new GenericContainer<>(true);
        Boolean boolData = boolContainer.getData();
        System.out.println("Boolean容器数据:" + boolData);
    }
}

运行结果:

String容器数据:Java泛型详解
Integer容器数据:100
Boolean容器数据:true

说明:泛型类的核心是“一次定义,多种使用”,同一个GenericContainer类,既可以存储String,也可以存储Integer、Boolean等任意类型,且保证类型安全。

注意:泛型参数不能是基本数据类型(如int、double、boolean),必须使用对应的包装类(Integer、Double、Boolean),因为泛型在编译后会被擦除为Object类型,而基本数据类型不能直接赋值给Object。

2. 泛型接口:接口定义时指定泛型参数

泛型接口和泛型类用法类似,在接口名后面加上泛型参数,实现接口时需要指定具体的泛型类型,或者继续保留泛型参数(成为泛型类)。

语法格式:

public interface 接口名<泛型参数> {
    泛型参数 方法名();
}

实战案例:自定义一个泛型接口,用于数据转换:

// 泛型接口:将T类型的数据转换为R类型
public interface Converter<T, R> {
    R convert(T source);
}

// 实现泛型接口,指定T为String,R为Integer(将字符串转为整数)
class StringToIntegerConverter implements Converter<String, Integer> {
    @Override
    public Integer convert(String source) {
        // 模拟转换逻辑(实际开发中需处理异常)
        return Integer.parseInt(source);
    }
}

// 实现泛型接口,指定T为Integer,R为String(将整数转为字符串)
class IntegerToStringConverter implements Converter<Integer, String> {
    @Override
    public String convert(Integer source) {
        return String.valueOf(source);
    }
}

// 测试
public class GenericInterfaceDemo {
    public static void main(String[] args) {
        Converter<String, Integer> strToInt = new StringToIntegerConverter();
        Integer num = strToInt.convert("123");
        System.out.println("字符串转整数:" + num); // 输出:123
        
        Converter<Integer, String> intToStr = new IntegerToStringConverter();
        String str = intToStr.convert(456);
        System.out.println("整数转字符串:" + str); // 输出:456
    }
}

补充:如果实现泛型接口时,不指定具体的泛型类型,那么实现类需要成为泛型类,例如:

// 不指定具体类型,实现类成为泛型类
class MyConverter<T, R> implements Converter<T, R> {
    @Override
    public R convert(T source) {
        // 自定义转换逻辑
        return null;
    }
}

3. 泛型方法:方法定义时指定泛型参数

泛型方法和泛型类、泛型接口不同:泛型方法是在方法本身定义泛型参数,即使所在的类不是泛型类,也可以定义泛型方法。

核心语法:在方法的返回值类型前面加上 <泛型参数>,表示该方法是泛型方法。

语法格式:

public <泛型参数> 泛型参数 方法名(泛型参数 参数名) {
    return 参数名;
}

实战案例:定义一个泛型方法,用于打印任意类型的数据,同时返回该数据:

public class GenericMethodDemo {
    // 泛型方法:<T> 表示该方法是泛型方法,T是泛型参数
    public <T> T printData(T data) {
        System.out.println("打印数据:" + data);
        return data;
    }
    
    // 测试
    public static void main(String[] args) {
        GenericMethodDemo demo = new GenericMethodDemo();
        
        // 调用泛型方法,传入String类型,编译器自动推断T为String
        String str = demo.printData("Java泛型方法"); // 输出:打印数据:Java泛型方法
        
        // 调用泛型方法,传入Integer类型,编译器自动推断T为Integer
        Integer num = demo.printData(12345); // 输出:打印数据:12345
        
        // 调用泛型方法,传入Boolean类型,编译器自动推断T为Boolean
        Boolean bool = demo.printData(false); // 输出:打印数据:false
    }
}

关键注意点:泛型方法的 <T> 不能省略,否则编译器会把T当作一个具体的类(如果没有定义T类,会报错)。

拓展:泛型方法常用来编写通用工具类,例如Java源码中的 Collections.sort() 方法,就是一个泛型方法,支持对任意类型的集合进行排序。

四、泛型的核心原理:类型擦除

这是泛型的重点,也是面试高频考点——很多同学会疑惑:“泛型在编译后,到底是什么样子的?”

答案:Java泛型是编译期语法糖,在编译阶段会进行“类型擦除”,即把泛型参数替换为其上限类型(默认是Object),运行时JVM并不知道泛型的存在。

简单来说:泛型只在编译期有效,运行期会被擦除

1. 类型擦除示例

看下面的代码,编译前后的对比:

// 编译前:泛型写法
List<String> strList = new ArrayList<>();
strList.add("Java");
String str = strList.get(0);

// 编译后:类型擦除,泛型参数被擦除为Object
List strList = new ArrayList();
strList.add("Java");
String str = (String) strList.get(0);

可以看到,编译后,<String> 被擦除了,List变成了默认的Object类型,取值时的强制转换也被编译器自动加上了——这就是为什么我们在使用泛型时,不需要手动强制转换的原因(编译器帮我们做了)。

2. 类型擦除的注意事项

类型擦除会带来一些“坑”,需要重点注意:

  • 泛型参数不能用于instanceof判断:因为运行时泛型被擦除,JVM无法判断List和List的区别,例如:
List<String> strList = new ArrayList<>();
// 错误:Cannot perform instanceof check against parameterized type List<String>
if (strList instanceof List<String>) { 
    // 逻辑处理
}

// 正确写法:只判断List类型,不判断泛型参数
if (strList instanceof List) {
    // 逻辑处理
}
  • 不能创建泛型参数的实例:因为编译后泛型参数被擦除为Object,无法确定具体类型,例如:
public <T> void createInstance() {
    // 错误:Cannot instantiate the type T
    T t = new T(); 
}

解决方案:可以通过反射创建泛型实例(后续实战部分会讲解)。

  • 泛型数组不能直接创建:同样是因为类型擦除,无法确定数组的具体类型,例如:
// 错误:Cannot create a generic array of List<String>
List<String>[] strListArray = new List<String>[10];

// 正确写法:先创建Object数组,再强制转换
List<String>[] strListArray = (List<String>[]) new List[10];

五、泛型通配符:?、? extends T、? super T

当我们不确定泛型参数的具体类型,或者需要限制泛型参数的范围时,就需要用到泛型通配符。泛型通配符主要有3种:?(无界通配符)、? extends T(上界通配符)、? super T(下界通配符)。

1. 无界通配符:?

? 表示“任意类型”,相当于 ? extends Object,适用于“只读取数据,不修改数据”的场景(因为不确定具体类型,修改会有类型安全风险)。

示例:定义一个方法,打印任意类型的List集合:

import java.util.List;

public class WildcardDemo {
    // 无界通配符:? 表示任意类型的List
    public void printList(List<?> list) {
        for (Object obj : list) {
            System.out.println(obj);
        }
    }
    
    public static void main(String[] args) {
        List<String> strList = List.of("Java", "泛型", "通配符");
        List<Integer> intList = List.of(1, 2, 3);
        
        WildcardDemo demo = new WildcardDemo();
        demo.printList(strList); // 打印字符串列表
        demo.printList(intList); // 打印整数列表
    }
}

注意:使用无界通配符的List,不能添加任何元素(除了null),因为不确定具体类型,添加元素会有类型安全风险:

List<?> list = new ArrayList<String>();
list.add(null); // 允许
// list.add("Java"); // 错误:无法确定list的具体类型,不能添加元素

2. 上界通配符:? extends T

? extends T 表示“泛型参数必须是T类型或T的子类”,即泛型参数的上限是T。

核心场景:只读取数据(因为子类对象可以赋值给父类引用,读取时可以安全转型为T类型),不能修改数据(因为不确定具体是T的哪个子类,添加元素可能导致类型不匹配)。

示例:定义一个方法,计算任意Number子类(Integer、Double、Float)的List总和:

import java.util.List;

public class UpperBoundDemo {
    // 上界通配符:? extends Number,表示泛型参数是Number或其子类
    public double sum(List<? extends Number> list) {
        double total = 0.0;
        for (Number num : list) {
            // 读取数据,转型为Number,安全
            total += num.doubleValue();
        }
        return total;
    }
    
    public static void main(String[] args) {
        List<Integer> intList = List.of(1, 2, 3);
        List<Double> doubleList = List.of(1.5, 2.5, 3.5);
        
        UpperBoundDemo demo = new UpperBoundDemo();
        System.out.println("整数列表总和:" + demo.sum(intList)); // 输出:6.0
        System.out.println("小数列表总和:" + demo.sum(doubleList)); // 输出:7.5
    }
}

注意:上界通配符的List,同样不能添加元素(除了null),例如:

List<? extends Number> list = new ArrayList<Integer>();
list.add(null); // 允许
// list.add(123); // 错误:不确定list是Integer还是Double,无法添加

3. 下界通配符:? super T

? super T 表示“泛型参数必须是T类型或T的父类”,即泛型参数的下界是T。

核心场景:只修改数据(因为可以安全地将T类型的对象添加到集合中,父类引用可以接收子类对象),读取数据时只能转型为Object类型(因为不确定具体是T的哪个父类)。

示例:定义一个方法,向任意Number父类(Number、Object)的List中添加Integer元素:

import java.util.ArrayList;
import java.util.List;

public class LowerBoundDemo {
    // 下界通配符:? super Integer,表示泛型参数是Integer或其父类
    public void addInteger(List<? super Integer> list) {
        // 修改数据,添加Integer类型,安全
        list.add(1);
        list.add(2);
        list.add(3);
    }
    
    public static void main(String[] args) {
        // List<Number> 是Integer的父类,符合要求
        List<Number> numberList = new ArrayList<>();
        // List<Object> 是Integer的父类,符合要求
        List<Object> objectList = new ArrayList<>();
        
        LowerBoundDemo demo = new LowerBoundDemo();
        demo.addInteger(numberList);
        demo.addInteger(objectList);
        
        System.out.println("numberList:" + numberList); // 输出:[1, 2, 3]
        System.out.println("objectList:" + objectList); // 输出:[1, 2, 3]
    }
}

注意:下界通配符的List,读取数据时只能转型为Object:

List<? super Integer> list = new ArrayList<Number>();
list.add(123); // 允许
// Integer num = list.get(0); // 错误:无法确定list的具体类型,只能转型为Object
Object obj = list.get(0); // 正确

通配符总结(重点记)

通配符 含义 核心场景 能否添加元素
? 任意类型 只读取,不修改 只能添加null
? extends T T或T的子类 只读取,不修改 只能添加null
? super T T或T的父类 只修改,不读取(或只读Object) 可以添加T或T的子类

口诀:上界读,下界写(上界通配符适合读取,下界通配符适合写入)。

六、泛型实战:自定义通用工具类

结合前面的知识点,我们实战编写一个泛型通用工具类,实现“对象拷贝”功能,支持任意类型的对象拷贝(模拟Spring的BeanUtils),加深对泛型的理解。

import java.lang.reflect.Field;

/**
 * 泛型通用工具类:对象拷贝
 * 支持将源对象(Source)的属性拷贝到目标对象(Target)
 */
public class GenericCopyUtil {

    /**
     * 泛型方法:对象拷贝
     * @param source 源对象
     * @param targetClass 目标对象的Class
     * @param <S> 源对象类型
     * @param <T> 目标对象类型
     * @return 拷贝后的目标对象
     */
    public static <S, T> T copy(S source, Class<T> targetClass) {
        if (source == null) {
            return null;
        }
        try {
            // 反射创建目标对象实例(解决泛型不能直接new的问题)
            T target = targetClass.getDeclaredConstructor().newInstance();
            
            // 获取源对象和目标对象的所有字段
            Field[] sourceFields = source.getClass().getDeclaredFields();
            Field[] targetFields = targetClass.getDeclaredFields();
            
            // 遍历字段,将源对象的字段值拷贝到目标对象
            for (Field sourceField : sourceFields) {
                sourceField.setAccessible(true); // 允许访问私有字段
                Object value = sourceField.get(source); // 获取源字段值
                
                for (Field targetField : targetFields) {
                    targetField.setAccessible(true);
                    // 字段名相同、类型相同,才进行拷贝
                    if (sourceField.getName().equals(targetField.getName()) 
                            && sourceField.getType().equals(targetField.getType())) {
                        targetField.set(target, value);
                        break;
                    }
                }
            }
            return target;
        } catch (Exception e) {
            throw new RuntimeException("对象拷贝失败", e);
        }
    }
    
    // 测试
    public static void main(String[] args) {
        // 源对象:User
        User source = new User(1, "Java博主", 25);
        // 拷贝到目标对象:UserDTO
        UserDTO target = GenericCopyUtil.copy(source, UserDTO.class);
        
        System.out.println("源对象:" + source);
        System.out.println("目标对象:" + target);
    }
    
    // 源对象类
    static class User {
        private Integer id;
        private String name;
        private Integer age;
        
        // 构造方法、getter/setter、toString省略(实际开发中需补充)
        public User(Integer id, String name, Integer age) {
            this.id = id;
            this.name = name;
            this.age = age;
        }
        
        @Override
        public String toString() {
            return "User{id=" + id + ", name='" + name + "', age=" + age + "}";
        }
    }
    
    // 目标对象类(DTO)
    static class UserDTO {
        private Integer id;
        private String name;
        private Integer age;
        
        // 无参构造、toString省略
        public UserDTO() {}
        
        @Override
        public String toString() {
            return "UserDTO{id=" + id + ", name='" + name + "', age=" + age + "}";
        }
    }
}

运行结果:

源对象:User{id=1, name='Java博主', age=25}
目标对象:UserDTO{id=1, name='Java博主', age=25}

说明:这个泛型工具类可以实现任意类型的对象拷贝,只要两个对象的字段名和类型相同,就可以快速拷贝,体现了泛型的“通用性”。

七、泛型常见坑点(面试高频)

结合前面的知识点,总结6个泛型常见坑点,避免大家在开发中踩坑,同时应对面试提问。

坑1:泛型参数不能是基本数据类型

原因:泛型擦除后会转为Object类型,而基本数据类型不能直接赋值给Object,必须使用包装类。

// 错误:泛型参数不能是int(基本数据类型)
List<int> list = new ArrayList<>();

// 正确:使用包装类Integer
List<Integer> list = new ArrayList<>();

坑2:泛型类不能定义静态泛型成员

原因:静态成员属于类,而泛型参数是在创建对象时确定的,静态成员初始化时,泛型参数还未确定。

public class GenericClass<T> {
    // 错误:静态泛型成员不允许
    public static T staticData;
    
    // 错误:静态泛型方法也不允许(泛型参数属于类,不是方法)
    public static T staticMethod() {
        return null;
    }
}

注意:静态泛型方法是允许的,但泛型参数必须在方法本身定义(即前面讲的泛型方法),而不是使用类的泛型参数:

public class GenericClass {
    // 正确:静态泛型方法,泛型参数在方法上定义
    public static <T> T staticGenericMethod(T data) {
        return data;
    }
}

坑3:泛型擦除导致的类型转换异常

虽然泛型会自动帮我们做类型转换,但如果手动强制转换,依然会出现异常:

List<String> strList = new ArrayList<>();
strList.add("Java");

// 强制转换为List<Integer>,编译不报错(因为类型擦除),运行时报错
List<Integer> intList = (List<Integer>) (List<?>) strList;
Integer num = intList.get(0); // 运行时报错:ClassCastException

坑4:通配符使用不当导致的编译错误

混淆上界、下界通配符的使用场景,比如用上界通配符添加元素,用下界通配符读取元素:

// 上界通配符,不能添加元素
List<? extends Number> list1 = new ArrayList<Integer>();
// list1.add(123); // 错误

// 下界通配符,读取元素不能转型为具体类型
List<? super Integer> list2 = new ArrayList<Number>();
list2.add(123);
// Integer num = list2.get(0); // 错误
Object obj = list2.get(0); // 正确

坑5:泛型数组的创建问题

不能直接创建泛型数组,必须先创建Object数组,再强制转换:

// 错误:Cannot create a generic array of List<String>
List<String>[] strListArray = new List<String>[10];

// 正确:强制转换
List<String>[] strListArray = (List<String>[]) new List[10];

坑6:泛型参数不能用于catch语句

原因:泛型擦除后,无法确定具体的异常类型,catch语句需要明确的异常类型。

// 错误:Cannot use type parameters in catch clause
public <T extends Exception> void handleException() {
    try {
        // 业务逻辑
    } catch (T e) { // 错误
        e.printStackTrace();
    }
}

八、总结与拓展

总结:

  1. 泛型的本质是参数化类型,解决类型不安全、代码繁琐的问题。
  2. 泛型只在编译期有效,运行期会进行类型擦除(默认擦除为Object)。
  3. 泛型通配符:上界(? extends T)适合读,下界(? super T)适合写。
  4. 开发中要避开泛型的常见坑点,尤其是类型擦除和通配符的使用。

拓展:泛型在实际开发中的应用非常广泛,比如:

  • 集合框架:ArrayList、HashMap<K,V> 等,都是泛型类。
  • 框架封装:Spring的RestTemplate、MyBatis的Mapper接口,都大量使用泛型。
  • 通用工具类:如本文编写的对象拷贝工具类,用泛型实现通用性。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐