一、泛型基础回顾
1.1 什么是泛型?
泛型是JDK 1.5引入的新语法,用通俗的话来说,泛型就是适用于许多许多类型 。从代码实现的角度看,泛型是对类型实现了参数化。
核心价值:传统的类和方法只能使用具体的类型(基本类型或自定义类),而泛型使得我们可以编写可以应用于多种类型的代码,极大地提高了代码的复用性。
1.2 为什么需要泛型?
一个示例说明了泛型的重要性:
// 没有泛型的实现
class MyArray {
public Object[] array = new Object[10];
public Object getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos, Object val) {
this.array[pos] = val;
}
}
这种实现方式存在两个问题:
-
任何类型的数据都可以存放
-
获取数据时需要强制类型转换,容易出错
泛型的解决方案:
class MyArray<T> {
public T[] array = (T[]) new Object[10];
public T getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos, T val) {
this.array[pos] = val;
}
}
使用泛型后,代码变得更加类型安全:
-
只能存储指定类型的数据
-
不需要强制类型转换
-
编译时进行类型检查
二、泛型类与泛型方法
2.1 泛型类的定义
语法:
class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
示例:
class MyArray<T> {
// 使用类型参数T
}
类型形参的命名规范(文档中提到的约定):
-
E 表示 Element
-
K 表示 Key
-
V 表示 Value
-
N 表示 Number
-
T 表示 Type
-
S, U, V 等表示第二、第三、第四个类型
2.2 泛型类的使用
基本使用:
MyArray<Integer> list = new MyArray<Integer>();
类型推导:
MyArray<Integer> list = new MyArray<>(); // 编译器可以推导出类型为Integer
重要限制:泛型只能接受类,所有基本数据类型必须使用包装类。
2.3 裸类型(了解概念)
裸类型是指泛型类没有指定类型实参的情况:
MyArray list = new MyArray(); // 裸类型
注意:不应该自己使用裸类型,它只是为了兼容老版本的API而保留的机制。
2.4 泛型方法
语法:
方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { ... }
示例:
public class Util {
// 静态的泛型方法需要在static后用<>声明泛型类型参数
public static <E> void swap(E[] array, int i, int j) {
E t = array[i];
array[i] = array[j];
array[j] = t;
}
}
使用方式:
// 使用类型推导
Integer[] a = {...};
swap(a, 0, 9);
// 不使用类型推导
Util.<Integer>swap(a, 0, 9);
三、泛型的实现原理
3.1 类型擦除机制
Java泛型是通过类型擦除机制在编译级别实现的。这是泛型实现中最核心、也最容易被误解的概念。
关键点:
-
在编译过程中,所有的泛型类型参数T都会被替换为Object
-
编译器生成的字节码在运行期间并不包含泛型的类型信息
-
这是Java泛型与C++模板的最大区别
示例:
// 源代码
class MyArray<T> {
public T[] array = (T[]) new Object[10];
}
// 编译后(概念上)
class MyArray {
public Object[] array = new Object[10];
}
3.2 为什么不能实例化泛型类型数组?
问题 :为什么T[] ts = new T[5];是不对的?
原因 :类型擦除后,这相当于Object[] ts = new Object[5],然后尝试赋值给T[]引用,但数组是协变的,可能会导致类型安全问题。
以下示例说明了问题:
class MyArray<T> {
public T[] array = (T[]) new Object[10];
public T[] getArray() {
return array;
}
}
// 使用时可能出现问题
MyArray<String> myArray = new MyArray<>();
// ...
Object[] objects = myArray.getArray(); // 返回的Object数组可能包含任何类型
正确的方式(了解即可):
class MyArray<T> {
public T[] array;
public MyArray() {
// 通过反射创建数组
}
}
四、泛型的高级特性
4.1 泛型的上界
泛型上界用于对类型参数进行约束,指定类型参数必须是某个类或接口的子类。
语法:
class 泛型类名称<类型形参 extends 类型边界> { ... }
示例1:限制为Number的子类
public class MyArray<E extends Number> { ... }
// 正确使用
MyArray<Integer> l1; // Integer是Number的子类
// 编译错误
MyArray<String> l2; // String不是Number的子类
示例2:限制为实现Comparable接口
public class MyArray<E extends Comparable<E>> { ... }
注意 :如果没有指定类型边界E,可以视为E extends Object。
4.2 通配符
通配符?用于在泛型的使用中表示未知类型,主要用于增加API的灵活性。
4.2.1 为什么需要通配符?
考虑以下场景:
class Message<T> {
private T message;
public T getMessage() { return message; }
public void setMessage(T message) { this.message = message; }
}
public class TestDemo {
public static void main(String[] args) {
Message<String> message = new Message<>();
message.setMessage("比特就业课欢迎您");
fun(message);
}
public static void fun(Message<String> temp) {
System.out.println(temp.getMessage());
}
}
如果现在有Message<Integer>类型,fun()方法将无法处理。通配符?解决了这个问题。
4.2.2 通配符上界
语法 :<? extends 上界>
示例:
class Food {}
class Fruit extends Food {}
class Apple extends Fruit {}
class Banana extends Fruit {}
public static void fun(Message<? extends Fruit> temp) {
// temp.setMessage(new Banana()); // 无法修改!
// temp.setMessage(new Apple()); // 无法修改!
Fruit b = temp.getMessage(); // 可以获取元素
System.out.println(b);
}
重要特性:
-
通配符上界不能进行写入数据,只能进行读取数据
-
因为编译器无法确定具体是哪个子类
4.2.3 通配符下界
语法 :<? super 下界>
示例:
class Plate<T> {
private T plate;
public T getPlate() { return plate; }
public void setPlate(T plate) { this.plate = plate; }
}
public static void fun(Plate<? super Fruit> temp) {
// 可以修改!添加的是Fruit或者Fruit的子类
temp.setPlate(new Apple()); // Fruit的子类
temp.setPlate(new Fruit()); // Fruit本身
// Fruit fruit = temp.getPlate(); // 不能接收,无法确定是哪个父类
System.out.println(temp.getPlate()); // 只能直接输出
}
重要特性:
-
通配符下界不能进行读取数据,只能写入数据
-
可以写入指定类型或其子类的对象
五、PECS原则
我们可以总结出这个重要原则:
PECS:Producer Extends, Consumer Super
-
如果需要一个提供(生产) 数据的泛型容器,使用
<? extends T> -
如果需要一个消费 数据的泛型容器,使用
<? super T>
六、泛型的最佳实践
6.1 优先使用泛型
在编写可重用代码时,优先考虑使用泛型,这可以提高代码的类型安全性和复用性。
6.2 合理使用通配符
在API设计中,合理使用通配符可以提高API的灵活性,但要注意PECS原则。
6.3 避免泛型数组
由于Java泛型的擦除机制,尽量避免创建泛型数组,如果必须使用,要特别注意类型安全。
6.4 注意类型擦除的影响
由于类型擦除,以下操作是不允许的:
-
不能使用
instanceof检查泛型类型 -
不能创建泛型类的数组
-
不能抛出或捕获泛型类的异常
-
不能重载参数类型擦除后相同的方法
七、总结
Java泛型是提高代码类型安全性和复用性的重要特性。通过本文的学习,我们应该掌握:
-
泛型的基本概念和使用:如何定义和使用泛型类、泛型方法
-
类型擦除原理:理解Java泛型的实现机制
-
通配符的使用 :掌握
?、? extends T、? super T的区别和适用场景 -
泛型上界:如何约束类型参数的范围
泛型的主要优点:
-
类型安全:编译时进行类型检查
-
消除强制类型转换
-
提高代码复用性
-
提高代码可读性
泛型的局限性(由于类型擦除):
-
不能使用基本类型作为类型参数
-
不能创建泛型数组
-
不能使用instanceof检查泛型类型
-
不能创建具体类型的泛型实例
掌握泛型及其高级特性,是成为Java高级开发者的重要一步。合理使用泛型可以使代码更加健壮、灵活,同时提高开发效率。希望本博客能帮助你深入理解Java泛型的各个方面。