一、为什么必须学泛型?(核心痛点)
很多开发者写了好几年Java,只会、<K,V>,却根本不知道泛型解决了什么问题、底层原理是什么,面试一问就废,项目写代码全靠惯性。
在没有泛型的JDK1.4及以前,集合存储数据存在两个致命问题:
1. 类型强制转换,代码冗余且极易报错
所有集合默认存储 Object 类型,取出数据必须手动强转,代码臃肿,重复劳动多。
2. 编译无校验,运行才报错,风险极高
编译阶段无法校验集合存储的数据类型,随意存入不同类型数据,只有运行时才会抛出 ClassCastException,线上隐患极大。
无泛型反例Demo(JDK1.4旧式写法)
java
import java.util.ArrayList;
public class GenericOldDemo {
public static void main(String[] args) {
// 无泛型,默认存储Object
ArrayList list = new ArrayList();
list.add("Java泛型");
list.add(123); // 编译不报错!允许存入数字
list.add(true); // 编译不报错!允许存入布尔值
// 手动强转,冗余且危险
String result = (String) list.get(1);
// 运行报错:ClassCastException,整型无法转字符串
System.out.println(result);
}
}
有泛型优化Demo(JDK1.5+)
java
import java.util.ArrayList;
public class GenericNewDemo {
public static void main(String[] args) {
// 限定集合只能存储String类型
ArrayList<String> list = new ArrayList<>();
list.add("Java泛型");
// list.add(123); 编译直接报错,提前拦截错误
// list.add(true); 编译直接报错
// 无需强转,直接获取对应类型
String result = list.get(0);
System.out.println(result);
}
}
泛型核心作用总结(一句话) :编译期类型校验,消除强制转换,让代码更安全、更简洁、更通用。
⚠️ 重点:泛型只在编译阶段生效,运行时会被擦除,这是后续所有原理、坑点的核心!
二、泛型核心定义与基础语法
1. 什么是泛型?
泛型(Generic)是JDK1.5引入的参数化类型 机制,简单说就是:把数据类型当作参数传递。
普通方法传递的是「数值参数」,泛型传递的是「类型参数」,可以让类、方法、接口适配任意数据类型,同时保留类型安全性。
2. 泛型常用占位符(规范约定,面试高频)
Java官方约定了通用占位符,可读性极强,禁止随意自定义单字母以外的符号:
- T:Type,代表任意单个类型(最常用)
- K:Key,键类型(Map集合专用)
- V:Value,值类型(Map集合专用)
- E:Element,集合元素类型(List/Set专用)
- R:Return,返回值类型
- ?:通配符,代表任意未知类型
三、泛型三大核心用法(类、方法、接口)
1. 泛型类(最常用)
定义类时声明泛型占位符,实例化类时指定具体数据类型,适配通用对象封装场景(如工具类、实体包装类)。
实战Demo:通用数据返回包装类(项目高频场景)
java
// 定义泛型类,T为任意数据类型<T> {
private Integer code; // 响应码
private String msg; // 响应信息
private T data; // 通用返回数据(适配任意类型)
// 无参、有参构造、get/set
public Result() {}
public Result(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
// 成功静态方法<T<T> success(T data) {
return<>(200, "操作成功", data);
}
// 失败静态方法
<T><T> fail(String msg) {
return new<>(500, msg, null);
}
// getter & setter
public Integer getCode() { return code; }
public void setCode(Integer code) { this.code = code; }
public String getMsg() { return msg; }
public void setMsg(String msg) { this.msg = msg; }
public T getData() { return data; }
public void setData(T data) { this.data = data; }
}
// 测试类
class TestGenericClass {
public static void main(String[] args) {
// 1. 返回<String> strResult = Result.success("查询成功");
System.out.println(strResult.getData());
// 2. 返回整数数据
<Integer> intResult = Result.success(100);
System.out.println(intResult.getData());
// 3. 返回自定义<User> userResult = Result.success(new User("张三", 20));
System.out.println(userResult.getData().getName());
}
}
// 自定义实体类
class User {
private String name;
private Integer age;
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public Integer getAge() { return age; }
}
2. 泛型方法(独立通用方法)
泛型方法独立于类泛型,无需类声明泛型,单个方法实现通用适配,适合工具类通用方法(如数组拷贝、数据转换)。
⚠️ 核心语法:方法返回值前必须`,告知虚拟机这是泛型类型参数。
实战Demo:通用数组打印、数据转换工具方法
java
public class GenericMethodUtil {
/**
* 通用数组打印方法,支持任意类型数组
*/
<T> void printArray(T[] array) {
for (T t : array) {
System.out.print(t + " ");
}
System.out.println();
}
/**
* 通用数据转换方法
*/
<T> T getDefaultValue(T value) {
return value;
}
// 测试
public static void main(String[] args) {
// 打印字符串数组
String[] strArr = {"Java", "Python", "Go"};
printArray(strArr);
// 打印整型数组
Integer[] intArr = {1, 2, 3, 4, 5};
printArray(intArr);
// 通用数据获取
String str = getDefaultValue("泛型工具方法");
Integer num = getDefaultValue(999);
System.out.println(str + " " + num);
}
}
3. 泛型接口(框架底层高频)
定义接口时声明泛型,实现类可指定具体类型,框架底层大量使用(、<T>、My`)。
实战Demo:通用数据校验接口
java
// 泛型接口
public<T> {
// 通用校验方法
boolean verify(T data);
}
// 实现类1:校验字符串
class StringVerify implements<String> {
@Override
public boolean verify(String data) {
return data != null && !data.isEmpty();
}
}
// 实现类2:校验整数
class IntegerVerify implements<Integer> {
@Override
public boolean verify(Integer data) {
return data != null && data > 0;
}
}
// 测试
class TestGenericInterface {
public static void main(String[] args) {
StringVerify stringVerify = new StringVerify();
System.out.println(stringVerify.verify("test")); // true
System.out.println(stringVerify.verify("")); // false
IntegerVerify integerVerify = new IntegerVerify();
System.out.println(integerVerify.verify(10)); // true
System.out.println(integerVerify.verify(-1)); // false
}
}
四、泛型通配符 ? 与上下边界(面试重中之重)
很多开发者分不清 T 和?:T 是确定的类型参数,? 是未知的通配符类型。
通配符主要用于方法参数接收,适配多种类型的集合,分为三种用法:无边界、上边界、下边界。
1. 无边界通配符 ?
代表任意未知类型,可接收任意泛型集合,只能读取,不能写入(除null)。
java
import java.util.ArrayList;
import java.util.List;
public class GenericWildcard {
// 接收任意类型的List
public<?> list) {
for (Object o : list) {
System.out.print(o + " ");
}
// list.add(123); 编译报错!无法写入未知类型
}
public static void main(String[] args) {
<String> strList = List.of("a", "b", "c");
<Integer> intList = List.of(1, 2, 3);
printList(strList);
printList(intList);
}
}
2. 上边界通配符 ? extends E
语法 :? extends 父类
含义 :接收当前类及所有子类类型
特点 :只能读,不能写
场景:只读数据场景,如数据查询、遍历
通俗举例
有父类 Animal,子类 Dog、Cat,<? extends Animal>。
3. 下边界通配符 ? super E
语法 :? super 子类
含义 :接收当前类及所有父类类型
特点 :可以写,读取只能用Object接收
场景:数据写入、保存场景(集合新增数据)
通俗举例
List<? super Dog> 。
4. 上下边界核心总结(必背面试题)
- 上边界extends:上限父类,只读不写,用于查询
- 下边界super:下限子类,可写可读,用于存储
- PECS原则:Producer Extends,Consumer Super(生产者用extends,消费者用super)
五、泛型擦除(底层核心原理)
1. 什么是泛型擦除?
Java泛型是伪泛型 ,所有泛型语法只在编译阶段生效,编译完成后,虚拟机会擦除所有泛型信息,回归原始类型(RawType)。
简单说:代码运行时,没有泛型。
2. 擦除规则
- 无边界泛:擦除为
Object - 上边界泛型:擦除为边界类型
Animal - 下边界泛型:擦除为父类原始类型
3. 擦除验证Demo
java
import java.util.ArrayList;
public class GenericErase {
public static void main(String[] args) {<String> str<>();<Integer> int<>();
// 编译后泛型擦除,两个集合类型完全一致
System.out.println(strList.getClass() == intList.getClass()); // true
}
}
输出结果为 true ,足以证明:运行时 ArrayList<Integer>和ArrayList<String> 是同一个类。
4. 泛型擦除带来的经典问题
问题:无法创建泛型数组、无法用泛型类型实例化对象、静态方法不能使用类泛型。
原因:运行时泛型信息已丢失,虚拟机无法识别具体类型。
六、泛型高频坑点与避坑指南(实战必看)
坑点1:泛型不支持多态传递
<Dog>不是Animal 的子类,无继承关系,直接赋值编译报错。
List<Animal> list = new ArrayList<Dog>(); // 编译报错!
解决方案 :使用上边界通配符List<? extends Animal>
坑点2:静态方法不能使用类定义的泛型
类泛型需要实例化对象时确定类型,静态方法属于类,早于对象初始化,无法获取类型参数。
解决方案 :静态方法单独声明自定义泛型 <T>
坑点3:基本类型不能作为泛型参数
<int>、List<double> 编译报错,泛型只支持引用类型。
解决方案 :使用包装类 Integer、Double
七、项目实战场景总结
结合多年开发经验,梳理工作中90%会用到泛型的场景:
- 统一响应结果封装:如本文的Result泛型类,全局统一接口返回格式
- 工具类通用方法:数据拷贝、类型转换、集合遍历、排序工具
- 框架底层封装:MyBatis Mapper、Spring Bean、集合框架、流式编程
- 自定义通用集合/缓存工具:适配任意数据类型存储
- DTO/VO通用封装:分页结果、列表数据统一封装
八、文末面试真题总结(高频)
- 泛型的作用是什么?为什么需要泛型?
- 编译期类型校验,提前拦截类型异常,规避运行时ClassCastException;
- 消除代码强制类型转换,简化代码、减少冗余;
- 实现代码通用复用,一套代码适配多种数据类型,无需重载多套方法;
- 提升代码可读性与安全性,是集合框架、工具类、框架底层通用封装的核心基础。
- T 和 ? 的区别?
- T(类型参数):是确定的具体类型,在类/方法初始化时被指定,可用于定义变量、赋值、读写数据;
- ?(通配符):是未知的模糊类型,仅用于方法参数接收泛型集合,无法定义变量,多数场景仅支持读取,不能写入数据;
- 简单总结:T是「已知确定类型」,?是「未知兼容类型」。
- extends 和 super 通配符的区别?PECS原则?
? extends E(上边界):接收E及E的子类,只读不写,适用于数据生产者(向外提供数据);
? super E(下边界):接收E及E的父类,可写、读取仅能使用Object接收,适用于数据消费者(接收存储数据);
PECS原则:Producer Extends,Consumer Super。生产者(读数据)用上边界extends,消费者(写数据)用下边界super。 - 什么是泛型擦除?带来了哪些问题?
泛型擦除是Java伪泛型的核心特性,泛型仅在编译阶段生效,用于类型校验,编译完成后JVM会擦除所有泛型标识,还原为原始Raw类型。
带来的问题:- 运行时无法获取泛型真实类型;
- 无法创建泛型数组、无法直接new泛型对象;
- 静态方法无法使用类定义的泛型;
- 泛型类型无法参与重载、instanceof判断。
List<String>和List<Object>有继承关系吗?
没有任何继承关系。即便String是Object的<Object>的子类。Java泛型不支持协变,目的是防止类型转换异常,避免集合存入非法数据。如需兼容多种类型,需使用通配符? extends Object。- 静态方法为什么不能使用类泛型?
类上定义的泛型需要在实例化对象时确定具体类型。而静态方法属于类,独立于对象存在,加载优先级高于对象初始化,此时泛型T的具体类型尚未确定,虚拟机无法做类型校验,因此禁止使用。静态方法如需泛型,需单独声明自定义泛型。
结语
泛型看似简单,实则是Java进阶、框架源码阅读、面试的核心基础。真正吃透泛型,核心是理解参数化类型 和编译期生效、运行时擦除的本质,记住「extends读、super写」的核心规则,就能解决99%的泛型问题。