今天我们一起学习一个Java中非常核心且强大的特性--泛型。
泛型是Java SE 5.0引入的,它的出现极大地增强了代码的可读性和安全性。
一、为什么需要泛型
在泛型出现之前,我们一般使用Object类来实现通用代码。
Object是所有Java类的根类,所以它可以引用任何类型的对象。
这样做虽然灵活,但也带来了两个显而易见的麻烦。
1、强制类型转换
当你从一个集合(比如ArrayList)中取出一个元素时,它的类型是Object。
你必须手动把它强制转换成你所期望的真实类型,才能调用它特有的方法。
没有泛型的时候,都是这样写代码:
java
package com.lazy.snail.day23;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName Day23Demo
* @Description TODO
* @Author lazysnail
* @Date 2025/6/11 9:35
* @Version 1.0
*/
public class Day23Demo {
public static void main(String[] args) {
List list = new ArrayList();
list.add("Hello");
list.add(123);
String a1 = (String) list.get(0);
Integer a2 = (Integer) list.get(1);
String a3 = (String) list.get(1);
}
}
集合里面装的都是Object(什么都能装),但是你取的时候就得知道你每个位置装的是什么东西。
如果你类型转换错误了,程序直接就崩溃了。
在编译期是没有任何问题,运行时问题才暴露出来。
你想想看,假设出现了这种问题,代码都在线上运行了才暴露问题,谁受得了。
2、缺乏类型安全
上面的例子里,你可以往ArrayList放任何东西。编译器表示很无奈,它没办法在编译阶段检查出你是不是添加了错误类型的对象。
所有的风险都被推迟到了运行时,你就说这种代码脆不脆弱吧。
泛型就是为了解决两个痛点:
泛型允许你在编译时就指定集合(或类、方法)里能存放的对象类型。
编译器会帮你检查,如果放了错误的类型,会直接报错。
然后类型一旦明确,你从泛型集合里获取元素时,就不再需要进行强制类型转换了。
把上面的例子改成这样:
java
package com.lazy.snail.day23;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName Day23Demo
* @Description TODO
* @Author lazysnail
* @Date 2025/6/11 9:35
* @Version 1.0
*/
public class Day23Demo {
public static void main(String[] args) {
List<String> list = new ArrayList();
list.add("Hello");
list.add("lazysnail");
list.add(123);
String a1 = list.get(0);
String a2 = list.get(1);
}
}
你限定了集合只能放String类型,当你放123进去,直接就编译错误了。

然后你在获取集合元素的时候,也不再需要强制类型转换了。
二、泛型怎么用
主要有三种应用形式:
2.1 泛型类
定义一个类的时候,可以在类名后面加上,T是一个类型参数。
这个T可以理解成一个占位符,表示将来使用这个类时会传入的实际类型。
java
package com.lazy.snail.day23;
/**
* @ClassName Box
* @Description TODO
* @Author lazysnail
* @Date 2025/6/11 15:39
* @Version 1.0
*/
public class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<>();
integerBox.setItem(2025);
Integer year = integerBox.getItem();
System.out.println("年份: " + year);
Box<String> stringBox = new Box<>();
stringBox.setItem("懒惰蜗牛");
String nickName = stringBox.getItem();
System.out.println("昵称: " + nickName);
}
}
代码里的T只是一个约定俗成的名称,代表"Type"。你可以换成其他字母,效果都一样。
2.2 泛型接口
泛型接口跟泛型类定义很像,也是在接口名后添加类型参数。
java
package com.lazy.snail.day23;
import java.util.Random;
/**
* @ClassName Generator
* @Description TODO
* @Author lazysnail
* @Date 2025/6/11 15:47
* @Version 1.0
*/
public interface Generator<T> {
T generate();
}
class StringGenerator implements Generator<String> {
@Override
public String generate() {
return "字符串生成器";
}
}
class RandomNumberGenerator<T extends Number> implements Generator<T> {
private T[] numbers;
public RandomNumberGenerator(T[] numbers) {
this.numbers = numbers;
}
@Override
public T generate() {
if (numbers == null || numbers.length == 0) {
return null;
}
Random random = new Random();
int index = random.nextInt(numbers.length);
return numbers[index];
}
}
StringGenerator类在实现Generator的时候,限定了类型参数是String。
StringGenerator中实现方法generate()的返回值就确定为String类型。
RandomNumberGenerator类本身也是个泛型类,T extends Number表示只能接受Number及其子类。
它在实现Generator接口的时候,把自身的泛型类型T传递给了接口Generator<T>,还对T增加了extends Number的类型约束。
泛型的上下限,我们会在后文补充讲。
2.3 泛型方法
有的时候,我们只是希望某个方法具有泛型能力,而不是整个类。
这个时候就可以定义泛型方法。
泛型方法可以在普通类、泛型类或者接口里定义。
java
package com.lazy.snail.day23;
/**
* @ClassName UtilDemo
* @Description TODO
* @Author lazysnail
* @Date 2025/6/11 16:09
* @Version 1.0
*/
public class UtilDemo {
public static <T> void printArray(T[] inputArray) {
for(T element : inputArray) {
System.out.printf("%s ", element);
}
}
public static void main(String[] args) {
Integer[] intArray = { 1, 2, 3, 4, 5 };
String[] stringArray = { "懒惰", "蜗牛" };
UtilDemo.printArray(intArray);
System.out.println();
UtilDemo.printArray(stringArray);
}
}
泛型方法的类型参数列表(<T>)要放在返回值类型之前。
这个声明就是告诉编译器,这是一个泛型方法,并且这个方法里使用的T是这个方法的类型参数,而不是类的。
Java编译器可以根据传入的参数类型,自动推断出泛型方法中的T具体是什么类型。
2.4 容易混淆的"泛型方法"
回看2.1节示例代码里的getItem方法,虽然返回值是泛型类,但方法本身并不是泛型方法。
getItem中的T并不是自己声明的,只是借用了Box定义的时候已经声明的类型参数T。
这个方法的类型T在创建Box对象时就已经确定了。
它不具备在调用时根据参数推断类型的泛型方法特性。
还有一种参数是泛型类,但方法本身不是泛型方法,比如:
java
public class BoxProcessor {
public void inspect(Box<Integer> box) {
System.out.println(box.getItem());
}
}
这个方法的参数是Box<Integer>,一个具体化了的泛型类。不是一个可变的类型占位符。
完全不具备泛型的通用性,应该一眼就看出它不是什么泛型方法。
把它改成这样:
java
public class BoxProcessor {
public <T> void inspect(Box<T> box) {
System.out.println(+ box.getItem());
}
}
声明了这是一个泛型方法,T是一个临时的类型参数。可以用它来处理任何类型的Box。
这才成了真正的泛型方法。
其实判断一个方法是不是泛型方法,就看一点就够了。 方法签名里,紧跟在修饰符(public, static等)之后、返回值类型之前,有没有、 这样的类型参数声明? 有,妥妥的泛型方法。 没有,不管它的参数或返回值是不是T,都不是泛型方法。只是在使用它所属的类或接口已经声明好的泛型类型而已。
结语
今天,我们简单探讨了Java的泛型。
大致了解了Java是怎么通过泛型来保证类型安全和简化代码,怎么把运行时的风险提前到了编译期。
通过案例大致的了解了泛型类、泛型接口和泛型方法这三板斧的基本用法。
下一篇预告
Day24 | Java泛型通配符与边界解析
如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!