Java 泛型详解:泛型类、泛型方法、泛型接口、类型擦除与类型通配符
Java泛型
一、 泛型概述
1.1 泛型的定义
JDK1.5之后,Java引入了“类型参数化”的概念,表面意思是表示广泛的、普通的类型。Java的参数化类型被称为泛型。
1.2 如何使用泛型
泛型使用的是一对尖括号定义的,如:List 指定的就是在List集合中只能放String类型的数据。
接下来我们来一个对比评测,看一看使用泛型和不使用泛型的区别。
不使用泛型:
List list1 = new ArrayList();
list1.add("www.educoder.net");
String str1 = (String)list1.get(0);
使用泛型:
List<String> list2 = new ArrayList<String>();
list2.add("www.educoder.net");
String str2 = list.get(0);
可以发现在这里我们不用强转数据类型了。
1.3 使用泛型的好处
-
类型安全,通过知道使用泛型定义的变量的类型限制,编译器可以更有效地提高Java程序的类型安全;
-
消除强制类型转换,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。所有的强制转换都是自动和隐式的;
-
提高性能。
1.4 格式
<数据类型>
注:此处的数据类型只能是引用类型
例:
List<String> strList = new ArrayList<>();
泛型的字母规范
在定义泛型类时,任意一个大写字母都可以用于指定泛型的变量。
- E–Element,常用在Java Collection里面,如List< E >, Set< E >
- T–Type,类型,如String,Integer
- K–Key,V–Value,代表Map键值对
- N–Number,数字
二、泛型类
2.1 泛型类的定义
(修饰符) Class 类名 <泛型标识>{
...
}
示例:
public class Cat<T> {
private T info;
public Cat(T info) {
this.info = info;
}
public void setInfo(T info){
this.info = info;
}
public T getInfo(){
return this.info;
}
}
2.2 易错点
注意:虚拟机没有泛型类对象——所有对象都属于普通类型 .
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型(raw type)即不会生成Cat< String>.class,也不会生成Cat< Animal>.class,而是只会生成一个原型Cat.class.
注意: 泛型类的静态成员(静态变量、静态方法)不能使用该类的泛型参数
因为静态成员属于类级别,在类加载时初始化,而泛型类的< T>在创建对象时才确定,,两者生命周期不匹配。
- 静态变量不能声明为static T info;
- 静态方法不能直接使用类的< T> 作为返回值或参数,如static T eat(){…}是错误的。但可以自己声明独立的泛型参数,如:static < S> S eat(){…}。这种独立声明的泛型参数与类的泛型参数无关。
注意:编译器不允许创建泛型变量的数组。
即在创建数组实例时,数组的元素不能使用参数化的类型。
例如,下面语句有错误:
Vector< Integer> vectorList[] = new Vector< Integer>[10];
原因:Java 的数组是协变的(子类可以替换父类,比如 String[] 可以赋值给 Object[]),而泛型是不可协变的(List< String> 不能赋值给 List< Object>),两者特性冲突会导致类型安全被破坏。
2.3 泛型类的继承规则
参数化类型不考虑类型参数的继承关系
如:String是Object的子类
Vector< String> v = new Vector< Object>();
// 错误!不写< Object>没错,写了就是明知故犯
Vector< Object> v = new Vector< String>();
// 也错误
再如:Manager是Employee的子类,但是Pair< Manager>不是Pair< Employee>的子类
三、泛型方法
一个方法是不是泛型的,与它所在类是不是泛型没有什么关系。
3.1 定义泛型方法
修饰符 <T,S> 返回值类型 方法名(形参列表){
...//方法体
}
示例:
public <T> void swap(DynamicArray<T>arr,int i,int j){
T tmp = arr.get(i);
arr.set(i,arr.get(j));
arr.set(j,tmp);
}
3.2 泛型参数的类型推断
在调用泛型方法的时候,可以显式地指定类型参数,也可以不指定。
- 与类、接口使用泛型类型不同的是,方法中的的泛型参数无须显示传入实际类型参数,编译器会根据实参推断类型参数的值。
因为编译器根据实参类型推断出泛型所代表的类型,它通常推断出最直接的类型。
示例:
public class testGenericMethod{
static<T> void fromArrayToCollection(T[] a,Collection<T> c){
for(T obj : a){
c.add(obj);
}
}
public static void main(String args[]){
Object[] o = new Object[20];
Collection<Object> co = new ArrayList<Object>();
fromArrayToCollection(o,co);
}
}
编译器自动推断出T是Object类型,Collection< T>是Collection< Object>,无需手动指定泛型类型fromArrayToCollection< Object>(o, co)。
- 在指定了类型参数时,传入的实参数据类型必须是指定数据类型或者其子类,否则编译器无法准确推断出泛型类型。
示例:(反面示例)
public class TestMethod{
static <T> void copy(Collection<T> from, Collection<T> to){
for(T obj : from){
to.add(obj);
}
}
public static void main(String args[]){
Collection<Object> co = new ArrayList<Object>();
Collection<String> cs = new ArrayList<String>();
copy(co,cs);
}
}
copy(co,cs);将产生编译错误
由2.3泛型的继承规则可知,Collection< Object>和Collection< String>无继承关系,不满足 “指定类型参数时,传入的实参数据类型必须是指定数据类型或者其子类” 的条件。
- 若根据实参推断 T=Object,则 to 参数需要 Collection< Object>或者其子类,但实际是 Collection< String>(不匹配);
- 若推断 T=String,则 from 参数需要 Collection< String>或者其子类,但实际是 Collection< Object>(不匹配)。
四、泛型接口
4.1 格式
修饰符 interface 接口名 <类型>{
}
示例:
public interface List< E>{
...
}
4.2 实现方式
- 明确指定具体类型(常用)
实现接口时,直接将泛型参数< T>替换为具体类型(如String,Circle…),接口中的T会被自动替换为该类型。
示例:
class Circle implements Comparable<Circle> {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public int compareTo(Circle c) {
if (radius < c.radius)
return -1;
else if (radius == c.radius)
return 0;
else
return 1;
}
}
- 保留泛型参数,创建对象时再确定
实现接口时不指定具体类型,而是继续使用泛型参数(如< T>)。
public class MyArrayList<E> implements List<E> {
...
}
创建对象时再确定类型:
public class GenericsDemo{
public static void main(String[] args){
MyArrayList<String>list = new MyArrayList<>();
list.add("hello");
}
}
五、类型擦除
5.1 什么是类型擦除
编译器可使用泛型信息,但是这些泛型信息在运行时是不可使用的,这被称为类型擦除。
泛型是使用一种类型擦除的方法来实现的。
5.2 原理
泛型是提供给javac编译器使用的,可以限定集合中的输入类型,让编译器挡住源程序中的非法输入。泛型信息仅在编译期用于类型检查,运行期会被擦除为原始类型(如 List< String> 擦除为 List),以保证与旧版本 Java 的兼容性并避免性能损耗。因此,对于参数化的泛型类型,getClass()方法的返回值和原始类型完全一样。
注意:由于系统中并不会真正生成泛型类,所以instanceof运算符后面不能使用泛型类。
示例:(反面示例)
Collection c=new ArrayList< String>();
if(c instanceof List< String>){
...
}
List< String>不是一个类,所以这是错误的。
六、类型通配符 <?>
当我们为方法中传入一个集合形参,而集合形参的类型不确定,又需要满足“指定类型参数时,传入的实参数据类型必须是指定数据类型或者其子类”的条件时,为了表示各种泛型的父类,我们需要使用类型通配符。
“?”被称为通配符,它的元素类型可以匹配任何类型。
6.1 无界通配符
定义:
? 表示任意引用类型
特点:
- 通用性最强,可以接收任何类型的泛型对象。
- 只能从集合中读取元素,不能喜爱那个集合中添加元素(null除外),即只能add(null)。
原因:编译器无法确定元素类型,避免类型混乱。
示例:
public void printItem(List<?> c){
for(int i = 0; i < c.size(); i++){
System.out.println(c.get(i));
}
}
List<?>和List< Object>的区别
- List<?>: ?代表可以是任意类型。List<?>是List接口各种泛型的父类,任何引用类型的List都可以传入,如List< Object>,List< String>,List< Manager>等。
- List< Object>: 是指明具体元素类型的泛型集合,只可以在其中添加元素为Object的List,不能接收其他类型的List(参数化类型不考虑类型参数的继承关系)
关系图如下:(箭头表示继承关系,箭头所指是父类,raw是原型)

注意:通配符不是类型变量,因此不能在编写代码时使用“?”作为一种类型
例如: ? t = p.get(i); 是错误的。
注意: 可以通过调用get()方法来返回List<?>集合指定索引处的元素,其返回值是一个未知类型,但肯定是一个Object。
6.2 上界通配符
通过extends关键字可以让参数为给定的上界类型或其子类型。这个上界可以是某个具体的类或者某个具体的接口。指定通配符上限就是为了支持类型协变。
? extends 父类代表“父类及其所有子类类型”。如 List<? extends Number> 表示 “元素类型是 Number 或其子类(Integer、Double 等)的 List 集合”。
特点:
- 范围受限:只能接收 “父类及子类” 类型的泛型对象(比无界通配符更具体);
- 只读不写:通配符上限的集合,只能从集合中取元素(取出的元素总是上限的类型或其子类),不能向集合中添加元素(null除外)。
示例:
public static double sum(List<? extends Number> list) {
double total = 0;
for (Number num : list) {
total += num.doubleValue();
}
return total;
}
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.5, 2.5);
System.out.println(sum(intList));
System.out.println(sum(doubleList)); 4.0
List<String> strList = Arrays.asList("1", "2");
// sum(strList); // 编译报错:String 不是 Number 的子类
}
在java继承中,接口是多继承,类是单继承的,则限定列表中可以有多个接口类型,但是至多有一个具体类。
如果用一个具体类作为限定,则这个具体类必须是限定列表中的第一个。
例如:
T extends Comparable&Serializable
T extends Number&ComParable&Serializable
6.3 下界通配符
指定通配符的下限是为了支持类型逆变。逆变用于灵活写入,不能被类型参数代替
? super 子类 代表 “子类及其所有父类类型”,如 List<? super Integer> 表示 “元素类型是 Integer 或其父类(Number、Object 等)的 List 集合”。
示例:
public void copyTo(DynamicArray<? super E> dest){
for(int i =0 ;i<size;i++){
dest.add(get(i));
}
}
public static void main(String[] args) {
DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
ints.copyTo(numbers);
}
6.4 通配符VS类型参数
- 通配符能覆盖大部分泛型类型匹配场景。在方法中采用类型参数是泛型方法,采用通配符依旧是普通方法,更加简洁,可读性更好。
- 如果需要处理类型参数之间的依赖关系、返回值与输入类型关联、自由写操作,或定义泛型接口,类、静态泛型方法时,则只能用类型参数。
- 通配符侧重 “使用泛型的灵活性”,类型参数侧重 “定义泛型的可控性”。
七、总结
7.1 Java 泛型中通配符与具体类型的继承关系示意图

7.2 补充:协变与逆变概念示意图
类型之间的赋值兼容性方向:

本文围绕Java泛型展开,核心讲解了JDK1.5引入的泛型(类型参数化)机制,包括其定义、使用好处(类型安全、消除强转、提升性能);详细介绍了泛型类、泛型方法、泛型接口的定义、语法、实现方式及易错点;阐述了类型擦除的原理与限制;重点解析了无界、上界、下界通配符的特性、用法及与类型参数的区别;还说明了泛型关键规则(如参数化类型无继承关系),帮助开发者全面掌握泛型技术以提升代码质量。
感谢阅读!若文中有错误,欢迎评论区指正,共同进步~
更多推荐



所有评论(0)