泛型基础
"泛型不是语法糖,而是类型安全的基石。编译期的每一个警告,都是运行时潜在的炸弹——忽略它们,总有一天会在生产环境炸给你看。" —— 白歌,飞翔科技架构师
1. 没有泛型的黑暗时代
在JDK 1.4及之前,Java没有泛型。开发者使用Object作为"万能类型"来编写通用容器:
// JDK 1.4 时代的"万能"集合
List students = new ArrayList();
students.add(new Student("小崔", 22));
students.add(new Student("白歌", 28));
students.add("大翔"); // 不小心放入了一个 String!编译器毫无反应
// 遍历时需要强制类型转换
for (int i = 0; i < students.size(); i++) {
Student s = (Student) students.get(i); // 运行到第三个元素时崩溃!
System.out.println(s.getName());
}
运行结果:
小崔
白歌
Exception in thread "main" java.lang.ClassCastException: java.lang.String
cannot be cast to com.feixiang.model.Student
核心问题:
- 编译器不检查集合中元素的类型,任何对象都能放入
- 取出时必须强制类型转换,代码冗长且不安全
- 类型错误在运行时才暴露,线上事故风险极高
2. 泛型:编译期类型安全
JDK 5引入泛型后,同样的代码变为:
// JDK 5+ 泛型集合 —— 类型安全
List<Student> students = new ArrayList<>();
students.add(new Student("小崔", 22));
students.add(new Student("白歌", 28));
// students.add("大翔"); // ❌ 编译错误!Type mismatch
// 遍历时无需强制转换
for (Student s : students) {
System.out.println(s.getName()); // 类型安全,不会ClassCastException
}
泛型的核心价值:将类型检查从运行时提前到编译期,让Bug在编码阶段就被发现。
3. 泛型类
泛型类(Generic Class)是具有一个或多个类型参数的类。
定义和语法
| 语法元素 | 说明 | 示例 |
|---|---|---|
class ClassName<T> | 声明泛型类,T为类型参数 | class Box<T> |
T field | 声明泛型字段 | private T value |
T method() | 返回泛型类型的方法 | public T get() |
void method(T t) | 接受泛型参数的方法 | public void set(T t) |
完整示例:飞翔科技的数据容器
/**
* 飞翔科技的通用数据包装器
* <T> 类型参数:表示该容器可以盛放任意类型的对象
*/
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
/**
* 检查值是否为null
*/
public boolean isPresent() {
return value != null;
}
/**
* 类型安全的操作:泛型类型在方法内部可以正常使用
*/
public String describe() {
if (value == null) {
return "Box[empty]";
}
return "Box[" + value.getClass().getSimpleName() + ": " + value + "]";
}
}
// 测试代码
public class BoxTest {
public static void main(String[] args) {
// 放入学生
Box<Student> studentBox = new Box<>(new Student("小崔", 22));
System.out.println(studentBox.describe());
Student s = studentBox.getValue(); // 无需强制转换
System.out.println("取出学生:" + s.getName());
// 放入字符串
Box<String> stringBox = new Box<>("飞翔科技");
System.out.println(stringBox.describe());
String name = stringBox.getValue();
// 放入整数(自动装箱为Integer)
Box<Integer> intBox = new Box<>(1024);
System.out.println(intBox.describe());
// 编译期类型检查 —— 下面这行无法通过编译
// Student wrong = stringBox.getValue(); // ❌ 编译错误!
}
}
运行输出:
Box[Student: Student{name='小崔', age=22}]
取出学生:小崔
Box[String: 飞翔科技]
Box[Integer: 1024]
类型参数命名约定
| 字母 | 含义 | 典型使用场景 |
|---|---|---|
| E | Element | 集合元素(如 List<E>、Set<E>) |
| K | Key | 键(如 Map<K,V>) |
| V | Value | 值(如 Map<K,V>) |
| T | Type | 通用类型(最常用,如 Box<T>、Repository<T>) |
| S/U | Second/Third Type | 多参数场景的第二、第三个类型 |
| R | Return | 方法返回类型(如 Function<T,R>) |
| N | Number | 数字类型 |
这些命名是约定俗成的,不是语法强制要求。你可以用任何合法标识符(如
Box<A>),但遵循约定能提升代码可读性。
4. 泛型接口
泛型接口广泛应用于集合框架和DAO/Repository设计模式中。
飞翔科技场景:泛型Repository接口
/**
* 飞翔科技通用数据访问接口
* <T> 实体类型
* <ID> 主键类型
*/
public interface Repository<T, ID> {
T findById(ID id);
List<T> findAll();
int save(T entity);
int update(T entity);
int deleteById(ID id);
}
/**
* Student实体的Repository实现
*/
public class StudentRepository implements Repository<Student, Long> {
// 模拟数据库
private Map<Long, Student> database = new HashMap<>();
@Override
public Student findById(Long id) {
return database.get(id);
}
@Override
public List<Student> findAll() {
return new ArrayList<>(database.values());
}
@Override
public int save(Student entity) {
database.put(entity.getId(), entity);
return 1;
}
@Override
public int update(Student entity) {
database.put(entity.getId(), entity);
return 1;
}
@Override
public int deleteById(Long id) {
database.remove(id);
return 1;
}
}
// 测试代码
public class RepositoryTest {
public static void main(String[] args) {
StudentRepository repo = new StudentRepository();
repo.save(new Student(1001L, "小崔", 22, "计算机科学与技术"));
repo.save(new Student(1002L, "白歌", 28, "软件工程"));
repo.save(new Student(1003L, "大翔", 35, "人工智能"));
System.out.println("=== 所有学生 ===");
for (Student s : repo.findAll()) {
System.out.println(s);
}
System.out.println("=== 按ID查找 ===");
Student found = repo.findById(1002L);
System.out.println("ID=1002 -> " + found.getName());
System.out.println("=== 删除后 ===");
repo.deleteById(1003L);
System.out.println("剩余数量:" + repo.findAll().size());
}
}
运行输出:
=== 所有学生 ===
Student{id=1001, name='小崔', age=22, major='计算机科学与技术'}
Student{id=1002, name='白歌', age=28, major='软件工程'}
Student{id=1003, name='大翔', age=35, major='人工智能'}
=== 按ID查找 ===
ID=1002 -> 白歌
=== 删除后 ===
剩余数量:2
5. 泛型方法
泛型方法(Generic Method)是在方法声明中引入自己的类型参数,独立于类是否泛型。
定义和语法
| 语法形式 | 说明 |
|---|---|
<T> T method(T t) | 返回值与参数类型相同 |
<T> void method(T t) | 无返回值的泛型方法 |
<T> List<T> method() | 返回泛型集合 |
<T extends Number> T method(T t) | 有界类型参数的泛型方法 |
static <T> T method(T t) | 静态泛型方法(不能使用类的类型参数) |
完整示例:飞翔科技工具类
/**
* 飞翔科技通用工具类
* 泛型方法可以定义在普通类中
*/
public class FeiXiangUtils {
/**
* 交换数组中两个位置的元素
* <T> 在返回值前声明,表示这是一个泛型方法
*/
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
/**
* 查找元素在数组中的索引(泛型比较)
*/
public static <T> int indexOf(T[] array, T target) {
for (int i = 0; i < array.length; i++) {
if (array[i].equals(target)) {
return i;
}
}
return -1;
}
/**
* 将单个元素包装为List
* 静态方法可以使用自己的类型参数,不能使用类的类型参数
*/
public static <T> List<T> singletonList(T item) {
List<T> list = new ArrayList<>();
list.add(item);
return list;
}
/**
* 合并两个List(泛型方法配合通配符)
*/
public static <T> List<T> merge(List<T> list1, List<T> list2) {
List<T> result = new ArrayList<>(list1);
result.addAll(list2);
return result;
}
}
// 测试代码
public class UtilsTest {
public static void main(String[] args) {
// 测试swap —— 字符串数组
String[] names = {"大翔", "白歌", "小崔"};
System.out.println("交换前:" + Arrays.toString(names));
FeiXiangUtils.swap(names, 0, 2);
System.out.println("交换后:" + Arrays.toString(names));
// 测试swap —— 整数数组
Integer[] nums = {10, 20, 30, 40};
FeiXiangUtils.swap(nums, 1, 2);
System.out.println("整数交换:" + Arrays.toString(nums));
// 测试indexOf
int idx = FeiXiangUtils.indexOf(names, "白歌");
System.out.println("白歌在索引:" + idx);
// 测试singletonList —— 自动推断泛型类型
List<Student> single = FeiXiangUtils.singletonList(
new Student(2001L, "测试员", 20, "测试专业")
);
System.out.println("单元素列表:" + single);
// 测试merge
List<String> list1 = Arrays.asList("A", "B", "C");
List<String> list2 = Arrays.asList("D", "E");
List<String> merged = FeiXiangUtils.merge(list1, list2);
System.out.println("合并结果:" + merged);
}
}
运行输出:
交换前:[大翔, 白歌, 小崔]
交换后:[小崔, 白歌, 大翔]
整数交换:[10, 30, 20, 40]
白歌在索引:1
单元素列表:[Student{id=2001, name='测试员', age=20, major='测试专业'}]
合并结果:[A, B, C, D, E]
类型推断
在JDK 7及之后,Java编译器可以通过上下文自动推断泛型类型,这称为类型推断(Type Inference):
// JDK 6 及之前:必须显式指定泛型类型
List<String> list6 = new ArrayList<String>();
// JDK 7+:钻石操作符(Diamond Operator)
List<String> list7 = new ArrayList<>(); // 编译器推断出<String>
// 泛型方法的类型推断
List<Student> single = FeiXiangUtils.singletonList(new Student(...));
// 编译器自动推断 <T> = Student,无需写 FeiXiangUtils.<Student>singletonList(...)
6. 原始类型
原始类型(Raw Type) 是泛型类的非泛型版本——即不带任何类型参数的泛型类。
// Box<T> 的原始类型
Box rawBox = new Box("hello"); // 编译警告:unchecked
// 等价于(经过类型擦除后)
Box rawBox = new Box(new Object()); // T 被替换为 Object
为什么存在原始类型?
向后兼容。JDK 5引入了泛型,但JDK 1.4的代码没有泛型也能运行。Java允许泛型代码与非泛型的遗留代码共存:
// 遗留代码(没有泛型)
public class LegacyLibrary {
public static List getStudents() {
List list = new ArrayList();
list.add(new Student("小崔", 22));
return list;
}
}
// 新代码(使用泛型)
@SuppressWarnings("unchecked")
public class NewCode {
public static void main(String[] args) {
List<Student> students = LegacyLibrary.getStudents(); // 警告:unchecked conversion
for (Student s : students) {
System.out.println(s.getName());
}
}
}
原始类型的危害
// ⚠️ 原始类型绕过编译检查 —— 运行时灾难!
Box<String> stringBox = new Box<>("安全数据");
Box rawBox = stringBox; // 赋值给原始类型,编译通过
rawBox.setValue(12345); // 放入 Integer!编译通过,仅警告
// 此时 stringBox 内部已经是 Integer 了
String s = stringBox.getValue(); // ❌ ClassCastException: Integer cannot be cast to String
运行时输出:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer
cannot be cast to java.lang.String
准则:永远不要在新代码中使用原始类型。如果遇到遗留代码的原始类型,使用
@SuppressWarnings("unchecked")抑制警告,并确保类型安全。
7. 泛型的编译期类型检查
泛型的核心优势在于编译期类型安全。下面通过对比来说明:
// ======== 无泛型:运行时才发现错误 ========
List legacy = new ArrayList();
legacy.add("飞翔科技");
legacy.add(2024); // 可以放入任何类型
legacy.add(new Date());
for (Object obj : legacy) {
String s = (String) obj; // ❌ 运行到第二个元素 ClassCastException
System.out.println(s.length());
}
// ======== 有泛型:编译期直接报错 ========
List<String> generic = new ArrayList<>();
generic.add("飞翔科技");
// generic.add(2024); // ❌ 编译错误:incompatible types
// generic.add(new Date()); // ❌ 编译错误:incompatible types
for (String s : generic) {
System.out.println(s.length()); // 无需强制转换,类型已被保证
}
泛型类型安全的多层次保障
编译器在编译泛型代码时,会自动在必要处插入checkcast字节码指令。这意味着即使运行时没有泛型信息,JVM仍能保证类型安全:
// 源代码
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);
// 编译后的等效字节码(反编译后)
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 编译器自动插入强制转换
可以用javap -c验证这一点(详见下一章《类型擦除》)。
8. 易错场景
反例一:泛型与基本类型不兼容
// ❌ 错误:泛型类型参数不能是基本类型
List<int> intList = new ArrayList<>(); // 编译错误!
Map<String, double> map = new HashMap<>(); // 编译错误!
Box<char> charBox = new Box<>('A'); // 编译错误!
原因:泛型经过类型擦除后变为Object,基本类型不能赋值给Object。必须使用包装类:
// ✅ 正确:使用包装类
List<Integer> intList = new ArrayList<>(); // 基本类型int -> 包装类Integer
Map<String, Double> map = new HashMap<>(); // double -> Double
Box<Character> charBox = new Box<>('A'); // char -> Character
反例二:混淆泛型类与泛型方法
public class ConfusedExample {
// ❌ 错误:静态方法不能使用类的类型参数
// 编译错误:non-static type variable T cannot be referenced from a static context
public static T processData(T data) {
return data;
}
}
原因:类的类型参数T在实例化时才确定,静态方法属于类级别,此时类型参数尚无确定值。
// ✅ 正确:静态方法必须声明自己的类型参数
public class CorrectExample {
public static <T> T processData(T data) { // <T> 是方法自己的类型参数
return data;
}
}
反例三:用instanceof检查泛型类型
// ❌ 错误:不能对泛型类型使用instanceof
if (obj instanceof List<String>) { // 编译错误:illegal generic type for instanceof
// ...
}
// 原因:类型擦除后,运行时只有List,没有List<String>
// ✅ 正确:使用无界instanceof或通配符
if (obj instanceof List) { // 使用原始类型(不推荐但合法)
// ...
}
if (obj instanceof List<?>) { // 使用无界通配符(推荐)
// ...
}
9. 面试考点
Q1:什么是泛型?为什么要使用泛型?
泛型(Generics)是JDK 5引入的语言特性,允许在定义类、接口和方法时使用类型参数。它在编译期提供类型安全检查,消除强制类型转换,使代码更加健壮和可读。核心优势:① 编译期类型检查,将运行时ClassCastException消灭在编码阶段;② 消除强制类型转换,代码更简洁;③ 支持通用算法,一份代码适配多种类型。
Q2:泛型类、泛型接口、泛型方法的区别?
泛型类/接口在类名/接口名后声明类型参数(如
class Box<T>),作用于整个类/接口的所有实例方法。泛型方法在方法返回值前声明独立的类型参数(如<T> T method(T t)),可以在普通类中定义。静态方法只能使用泛型方法形式(因为静态上下文获取不到类的类型参数)。
Q3:为什么泛型类型参数不能是基本类型?
泛型在编译后会进行类型擦除(Type Erasure),类型参数被替换为
Object或其边界类型。基本类型(int、double等)不是Object的子类,无法赋值给Object引用。因此只能使用对应的包装类(Integer、Double等)。幸运的是,JDK 5引入的自动装箱/拆箱让这个过程对开发者几乎透明。
Q4:原始类型是什么?可以安全使用吗?
原始类型是泛型类不带类型参数的版本(如
List而非List<String>),只是为了向后兼容JDK 1.4代码。在新代码中使用原始类型会绕过编译期类型检查,导致运行时ClassCastException。唯一"安全"使用场景是与遗留接口交互,且必须使用@SuppressWarnings("unchecked")并确保类型安全。
Q5:List<Object>和原始类型List有什么区别?
List<Object>明确告诉编译器"这个List可以放任何Object",编译器会检查你放入的对象类型,取出时返回Object。原始类型List则不进行任何泛型类型检查——你可以将其赋值给List<String>而不报错(仅警告),编译器会放弃对该引用的泛型检查。简言之:List<Object>参与了泛型类型系统,List完全退出了泛型检测。