Java泛型核心知识总结
# Java 泛型核心知识总结
# 泛型
# 什么是泛型?有什么用?
Java 泛型(Generics)是 JDK 5 中引入的一个新特性,它提供了一种类型安全的编程机制,可以在编译时检查类型错误,避免了在运行时出现类型转换异常的情况。它可以使程序员在编写代码时指定类型参数,从而使得代码更加灵活和可重用。
比如 ArrayList<Persion> persons = new ArrayListPersion>()
这行代码就指明了该 ArrayList
对象只能传入 Persion
对象,如果传入其他类型的对象就会报错。
类型参数 T
是一种占位符类型,用于表示实际的类型。
一般在哪定义?
Java 泛型可以用于类、接口和方法的定义中。
三种使用方式:泛型类、泛型接口、泛型方法。
使用泛型有什么好处?
简单来说,使用泛型参数,可以增强代码的可读性以及稳定性。
使用泛型可以带来许多好处,比如:
- 类型安全:Java 泛型可以在编译时检查类型错误,避免了在运行时出现类型转换异常的情况。
- 代码重用:泛型可以使代码更加通用和模块化,可以重用在不同的场景中。
- 更好的性能:泛型可以避免不必要的类型转换和装箱操作,从而提高代码的性能。
泛型的实现方式?
泛型主要通过以下两种方式来实现:
- 参数化类型和通配符类型。
# 泛型有哪些限制?为什么?
泛型参数
<T>
不能是基本类型,例如int
,因为实际类型是Object
,Object
类型无法持有基本类型。- Java 的泛型是基于类型擦除的,编译后泛型信息会被擦除,T 变成
Object
。 - 但
Object
不能直接存储int
,只能存储Integer
。 - 解决方案:使用包装类,如
Integer
而不是int
。
- Java 的泛型是基于类型擦除的,编译后泛型信息会被擦除,T 变成
无法取得带泛型的
Class
。(getClass()
)- 泛型类型在运行时会被擦除,比如
List<String>
和List<Integer>
运行时都变成List
,所以getClass()
无法区分不同泛型类型。
- 泛型类型在运行时会被擦除,比如
无法判断带泛型的类型(
instanceof
判断会出错)。- 由于泛型擦除,所有泛型类型在运行时都被视为
Object
,无法进行精确的instanceof
判断。 - 可以判断是否为
List
,但无法判断T
的类型,例如list instanceof List<String>
不能这样判断
- 由于泛型擦除,所有泛型类型在运行时都被视为
不能实例化泛型参数的数组。因为擦除后为
object
后无法进行类型判断。T[] arr = new T[10]; // ❌ 无法创建泛型数组
1由于类型擦除,JVM 无法知道数组的具体类型,导致无法创建安全的泛型数组。
Java 数组是协变的,意味着
String[]
是Object[]
的子类,而泛型是不协变的(List<String>
不是List<Object>
)。
不能实现两个不同泛型参数的同一接口,擦除后多个父类的桥方法将冲突。
- 泛型擦除后,多个接口的方法签名可能冲突,导致无法区分。
不能使用
static
修饰泛型变量。泛型是属于实例的,而
static
变量属于类,所有实例共享,如果static
变量使用泛型,会导致类型不安全。class MyClass<T> { private static T value; // ❌ 编译错误 } // 如果 static 需要泛型,应该独立声明 class MyClass { private static <T> void print(T value) { // ✅ 静态方法可以使用独立泛型 System.out.println(value); } }
1
2
3
4
5
6
7
8
9
10
泛型数组的限制:Java 泛型数组的创建和使用受到一些限制,例如无法创建泛型数组、无法向泛型数组中添加元素等。
- 由于类型擦除,泛型数组不能保证类型安全,无法创建泛型数组。
- 无法安全地向泛型数组添加元素,因为 Java 运行时不会记录泛型类型。
总结:
限制 | 原因 | 解决方案 |
---|---|---|
不能使用基本类型(如 int ) | 类型擦除后变成 Object ,Object 不能存储 int | 用包装类 Integer 代替 |
无法获取带泛型的 Class | 类型擦除后 List<String> 和 List<Integer> 运行时相同 | 不能区分泛型类型 |
不能用 instanceof 判断泛型类型 | 类型擦除后泛型类型变成 Object | 只能判断是否是 List ,不能判断 List<String> |
不能创建泛型数组 | JVM 无法检查泛型数组类型 | 用 List<T> 代替数组 |
不能实现多个不同泛型参数的同一接口 | 类型擦除后方法签名冲突 | 避免同时实现多个不同泛型的同一接口 |
不能在 static 方法或变量中使用泛型参数 | static 变量是共享的,而泛型是实例级的 | 让 static 方法自己定义泛型 |
# 项目中哪里用到了泛型?
比如:
- 自定义接口通用返回结果类
CommonResult<T>
通过参数T
可根据具体的返回类型动态指定结果的数据类型。 - 定义
Excel
处理类ExcelUtil<T>
用于动态指定Excel
导出的数据类型。 - 构建集合工具类(参考
Collections
中的sort
,binarySearch
方法)。
# 什么是类型擦除?
类型擦除是指在 Java 编译器将泛型代码编译成字节码时,会将泛型类型擦除,替换为实际的类型或者 Object
类型,从而使得泛型类型在运行时不存在。
注意:是【实际的类型】或者 【Object
】类型。
为什么要擦除?
这是因为 Java 虚拟机并不支持泛型,所以需要在编译期对泛型进行擦除,将泛型代码转换为普通的 Java 类型。
比如:
泛型类型没有定义类型参数的限定类型的情况:
class MyList<T> { ... }
对于上面的 MyList<T>
类,实际上编译器会将其擦除成如下形式:
class MyList {
...
}
2
3
在运行时,我们无法获取泛型类型的类型参数,例如 无法获取 MyList<String>
和 MyList<Integer>
的类型参数,它们都被擦除为 MyList 类型。
# 什么是桥方法?
桥方法(Bridge Method)是 Java 泛型类型擦除机制的一种补偿措施。它是指在泛型类或泛型接口中,由编译器自动生成的一个方法,用于在类型擦除后保持多态性。
具体说明:
在 Java 泛型中,由于类型擦除机制的存在,导致在某些情况下,泛型类型的继承关系会被破坏。
例如下面的代码:
public class MyList<T> {
...
public void add(T element) {
...
}
}
2
3
4
5
6
假设我们定义了一个子类:
public class MyStringList extends MyList<String> {
...
}
2
3
由于类型擦除机制的存在,MyStringList
类实际上是继承自 MyList
类的原始类型,而不是继承自 MyList<String>
类型。
因此,如果我们在 MyStringList
类中定义一个重写 add()
方法的话,会出现编译错误:
public class MyStringList extends MyList<String> {
...
@Override
public void add(String element) {
...
}
}
2
3
4
5
6
7
这是因为 Java 编译器会将 MyStringList
类中的 add()
方法擦除成如下形式:
public void add(Object element) {
...
}
2
3
这个方法的参数类型是 Object
,与 MyList<String>
中的 add
方法的参数类型不同,因此编译器会报错。
为了解决这个问题,Java 编译器会在 MyStringList 类中自动生成一个桥方法,用于在类型擦除后保持多态性:
public class MyStringList extends MyList<String> {
...
@Override
public void add(Object element) {
add((String) element);
}
public void add(String element) {
...
}
}
2
3
4
5
6
7
8
9
10
在上面的代码中,编译器会自动生成一个桥方法 add(Object element)
,它会调用原始方法 add(String element)
,从而保持多态性。
# 通配符
# 什么是通配符?有什么作用?
泛型类型是固定的,某些场景下使用起来不太灵活,于是,通配符就来了!
通配符可以允许类型参数变化,用来解决泛型无法协变的问题。
举个例子:
// 限制类型为 Person 的子类
<? extends Person>
// 限制类型为 Manager 的父类
<? super Manager>
2
3
4
# 通配符 ? 和常用的泛型 T 之间有什么区别?
T
可以用于声明变量或常量,而?
不行。T
一般用于声明泛型类或方法,通配符?
一般用于泛型方法的调用代码和形参。T
在编译期会被擦除为限定(实际)类型或object
,通配符用于捕获具体类型。
擦除为限定(实际)类型是什么意思?
1、如果泛型类型定义了类型参数的限定类型,例如:
class MyList<T extends Number> {
private T[] elements;
public void add(T element) { ... }
public T get(int index) { ... }
}
2
3
4
5
那么在编译时,编译器会将类型参数 T
擦除为其限定类型 Number
,例如:
class MyList {
private Number[] elements;
public void add(Number element) { ... }
public Number get(int index) { ... }
}
2
3
4
5
因此,在运行时,无法获取泛型类型的类型参数 T
,而只能获取其限定类型 Number
。
2、如果泛型类型没有定义类型参数的限定类型,例如:
class MyList<T> {
private T[] elements;
public void add(T element) { ... }
public T get(int index) { ... }
}
2
3
4
5
那么在编译时,编译器会将类型参数 T
擦除为 Object
类型,例如:
class MyList {
private Object[] elements;
public void add(Object element) { ... }
public Object get(int index) { ... }
}
2
3
4
5
因此,在运行时,无法获取泛型类型的类型参数 T
,而只能获取其擦除后的类型 Object
。
# 介绍一下常用的通配符?
常用的通配符有三种:
<? extends T>
:上边界通配符extends
,表示该泛型必须是 T 的子类(包括 T 本身),用于限定泛型的上界。<? super Integer>
:下边界通配符super
,表示该泛型必须是 T 的父类(包括 T 本身),用于限定泛型的下界。<?>
:无限定通配符?
,表示任意类型,用于表示不确定的类型参数。无限定通配符<?>
很少使用,可以用<T>
替换,同时它是所有<T>
类型的父类。