Java 泛型详解:从入门到真正理解
这是一篇面向初学者的原创学习笔记。很多人第一次接触泛型,只知道在集合里写
<String>、<Integer>,但并不真正明白泛型到底解决了什么问题。本文会从“为什么需要泛型”开始,逐步讲清泛型类、泛型方法、泛型接口、通配符以及extends和super的区别。
一、什么是泛型?
在 Java 里,泛型可以理解成:把类型也当成参数来传递。
以前我们给方法传参数,传的是具体的值;而泛型做的事情,是让类、接口、方法在设计阶段先不把类型写死,而是在使用的时候再决定具体类型。
比如下面这行代码:
ArrayList<String> list = new ArrayList<>();
这里的 String 就是传给集合的“类型参数”。
这意味着:
- 这个集合里准备存放
String - 往里放别的类型会报错
- 取出来的数据也会自动当成
String处理
所以泛型不是语法装饰,而是一种类型约束机制。
二、为什么要有泛型?
如果没有泛型,很多代码虽然也能写,但会有两个很明显的问题:
- 类型不安全
- 取数据时需要强制类型转换
先看一个不用泛型的例子:
import java.util.ArrayList;
public class Test {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("Java");
list.add(100);
String s1 = (String) list.get(0);
String s2 = (String) list.get(1); // 运行时报错
System.out.println(s1);
System.out.println(s2);
}
}
问题在哪?
1. 什么类型都能往里放
因为没有指定集合中元素的类型,所以字符串、整数、对象都能放进去。
2. 取出来必须强转
String s1 = (String) list.get(0);
这种写法很麻烦,而且容易出错。
3. 错误延迟到运行期才暴露
真正危险的是:代码编译时不报错,但运行到这里时才抛出异常。
这类错误排查起来往往比编译错误更麻烦。
三、使用泛型之后有什么变化?
看同样的例子,改成泛型写法:
import java.util.ArrayList;
public class Test {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Java");
// list.add(100); // 编译报错
String s = list.get(0);
System.out.println(s);
}
}
这时好处很明显:
- 集合只能存
String - 不需要强制类型转换
- 类型错误在编译阶段就能发现
所以泛型最大的价值可以总结成一句话:
把原本运行时才会暴露的类型问题,尽量提前到编译时解决。
四、泛型的本质怎么理解?
泛型的本质不是“高级语法”,而是“预留类型位置”。
比如你写一个箱子类:
class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
这里的 T 可以理解为“暂时还不知道的类型”。
等到真正使用时,再告诉它到底是什么:
Box<String> box1 = new Box<>();
Box<Integer> box2 = new Box<>();
这就表示:
box1中的T被替换成Stringbox2中的T被替换成Integer
所以你完全可以把泛型理解成:
写代码时先把类型空出来,等使用时再填进去。
五、泛型中的常见字母含义
泛型参数常常写成单个大写字母,这些字母本身没有强制要求,但通常有约定俗成的命名习惯:
T:Type,类型E:Element,元素K:Key,键V:Value,值
例如:
class Box<T> {
}
interface List<E> {
}
interface Map<K, V> {
}
这些只是命名习惯,不是语法要求。
六、泛型类
1. 定义泛型类
泛型类就是在类定义时,把类型参数也一起定义出来。
class Box<T> {
private T data;
public void setData(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
2. 使用泛型类
public class Test {
public static void main(String[] args) {
Box<String> box1 = new Box<>();
box1.setData("你好,泛型");
System.out.println(box1.getData());
Box<Integer> box2 = new Box<>();
box2.setData(200);
System.out.println(box2.getData());
}
}
输出:
你好,泛型
200
3. 泛型类的意义
同一个类,不需要重复写多个版本:
StringBoxIntegerBoxDoubleBox
只要写一个 Box<T>,就能适配多种数据类型。
这体现的就是代码复用能力。
七、泛型方法
有时候,不是整个类都需要泛型,而只是某个方法需要更灵活的类型处理。
这时候就可以使用泛型方法。
1. 定义方式
class Util {
public <T> void printValue(T value) {
System.out.println(value);
}
}
这里要特别注意:
public <T> void printValue(T value)
<T> 一定写在返回值类型前面,这表示这个方法自己声明了一个泛型参数。
2. 使用示例
public class Test {
public static void main(String[] args) {
Util util = new Util();
util.printValue("hello");
util.printValue(123);
util.printValue(true);
}
}
输出:
hello
123
true
这说明这个方法能够接收多种类型的数据。
3. 带返回值的泛型方法
class Util {
public <T> T getData(T value) {
return value;
}
}
调用:
Util util = new Util();
String s = util.getData("Java");
Integer n = util.getData(10);
八、泛型接口
接口同样可以使用泛型。
1. 定义泛型接口
interface Data<T> {
void add(T t);
T get();
}
2. 实现方式一:实现类直接指定类型
class StringData implements Data<String> {
private String data;
@Override
public void add(String t) {
this.data = t;
}
@Override
public String get() {
return data;
}
}
这种写法表示:这个实现类只处理 String 类型。
3. 实现方式二:实现类继续保留泛型
class DataImpl<T> implements Data<T> {
private T data;
@Override
public void add(T t) {
this.data = t;
}
@Override
public T get() {
return data;
}
}
这种写法表示:实现类本身也保持通用,使用时再决定具体类型。
九、泛型在集合中的典型使用
泛型最常见的落地场景,就是集合框架。
1. List
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
for (String s : list) {
System.out.println(s);
}
}
}
2. Set
import java.util.HashSet;
import java.util.Set;
public class Test {
public static void main(String[] args) {
Set<Integer> set = new HashSet<>();
set.add(10);
set.add(20);
System.out.println(set);
}
}
3. Map
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("语文", 95);
map.put("数学", 100);
for (String key : map.keySet()) {
System.out.println(key + "=" + map.get(key));
}
}
}
这里:
String是键的类型Integer是值的类型
十、泛型和 Object 有什么区别?
很多同学会想:
“泛型不就是为了接收不同类型吗?那我直接用 Object 不也一样吗?”
表面上看有点像,但本质完全不同。
1. Object 是“统一接收”
class Box {
private Object value;
public void setValue(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
这种写法确实什么都能存,但取出来时必须强制类型转换。
String s = (String) box.getValue();
2. 泛型是“编译期约束”
class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
这种写法不仅更通用,而且更安全。
所以:
Object是宽泛接收,缺点是取出时不安全- 泛型是在编译期就把类型限定好
十一、通配符 ? 是什么?
通配符是泛型里一个很重要的知识点。
? 表示:未知类型。
也就是说,我知道这里有个泛型,但我暂时不确定它到底是什么具体类型。
十二、无界通配符 <?>
1. 基本含义
<?> 表示任意类型。
public void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
这个方法既能接收 List<String>,也能接收 List<Integer>。
printList(new ArrayList<String>());
printList(new ArrayList<Integer>());
2. 为什么不能随便添加元素?
List<?> list = new ArrayList<String>();
// list.add("abc"); // 编译错误
list.add(null); // 可以
因为你只知道这是“某种类型的集合”,但不知道它具体是什么类型,所以不能安全地往里面放任意对象。
十三、上界通配符 <? extends T>
<? extends T> 表示:
泛型类型必须是
T或T的子类。
例如:
public void showNumbers(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n);
}
}
这个方法可以接收:
List<Integer>List<Double>List<Float>
因为这些类型都属于 Number 的子类。
适合理解为“可读”
因为不管具体是 Integer 还是 Double,取出来时都可以按 Number 来接收:
Number n = list.get(0);
但一般不能安全添加元素:
// list.add(10); // 编译错误
因为编译器不知道它到底是 List<Integer> 还是 List<Double>。
十四、下界通配符 <? super T>
<? super T> 表示:
泛型类型必须是
T或T的父类。
例如:
public void addIntegers(List<? super Integer> list) {
list.add(10);
list.add(20);
}
这个方法可以接收:
List<Integer>List<Number>List<Object>
适合理解为“可写”
因为无论这个集合具体是 Integer、Number 还是 Object,往里面放 Integer 一定是安全的。
但读取时只能按 Object 看待:
Object obj = list.get(0);
因为你没法确定它实际声明成了哪个父类类型。
十五、怎么记 extends 和 super?
这是泛型里最经典、最容易混淆的一部分。
你可以记住下面这句口诀:
PECS:Producer Extends,Consumer Super
意思是:
- 如果一个参数主要是“提供数据给你读取”,用
extends - 如果一个参数主要是“接收你写进去的数据”,用
super
换成更好理解的话就是:
extends:偏读取super:偏写入
十六、泛型擦除是什么?
Java 泛型还有一个必须知道的概念,叫做类型擦除。
意思是:
泛型主要在编译阶段起作用,到了运行阶段,具体的泛型类型信息会被擦除。
看例子:
ArrayList<String> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass());
输出是:
true
这说明在运行时,它们本质上还是同一个 ArrayList 类。
所以泛型并不是运行时真正保留了完整的类型信息,而是主要帮助编译器做类型检查。
十七、泛型的注意事项
1. 泛型不能使用基本数据类型
错误写法:
ArrayList<int> list = new ArrayList<>();
正确写法:
ArrayList<Integer> list = new ArrayList<>();
因为泛型只能使用引用类型。
所以要学会基本类型和包装类的对应关系:
int->Integerdouble->Doublechar->Character
2. 静态方法中如果要使用泛型,必须单独声明
错误写法:
class Demo<T> {
public static void show(T t) {
}
}
正确写法:
class Demo {
public static <T> void show(T t) {
System.out.println(t);
}
}
3. 泛型不支持直接创建泛型数组
例如下面这种写法通常不允许:
T[] arr = new T[10];
4. List<Object> 不能接收 List<String>
错误写法:
List<Object> list = new ArrayList<String>();
这是很多初学者容易误解的地方。
Java 中泛型之间默认没有这样的继承关系。
如果你想接收任意泛型类型,应该使用:
List<?> list = new ArrayList<String>();
十八、泛型的核心价值总结
如果要把整篇内容浓缩成最重要的几句话,那就是:
1. 泛型的本质
把类型参数化。
2. 泛型的最大作用
提高类型安全,减少强制类型转换。
3. 泛型最常见的场景
集合框架。
4. 通配符的核心理解
<?>:任意类型<? extends T>:上界,偏读<? super T>:下界,偏写
十九、最后总结
泛型是 Java 中非常重要的“进阶基础”。
它表面上看只是尖括号里的一个类型,实际上背后体现的是 Java 对“类型安全”和“代码复用”的支持。
你可以把它记成下面这几句话:
- 泛型不是装饰,而是约束
- 泛型不是为了写起来花哨,而是为了让代码更安全
- 泛型最重要的价值,是把类型错误提前到编译阶段
当你真正理解泛型后,后面再学集合、Stream、Lambda、框架源码时,会轻松很多。
二十、练习题
练习 1
定义一个泛型类 Container<T>,包含一个属性 data,并提供 setData() 和 getData() 方法。
练习 2
定义一个泛型方法,传入任意类型参数并输出。
练习 3
定义一个泛型接口 Message<T>,再写一个实现类来实现它。
练习 4
分别解释下面三种写法的含义:
List<?>
List<? extends Number>
List<? super Integer>
练习 5
思考:为什么 List<Object> 不能直接接收 List<String>?
更多推荐

所有评论(0)