泛型
知识点
在Java高级开发中,掌握泛型(Generics)是非常重要的,它是Java语言中的一项重要特性,提供了编译时类型安全检查机制,使得代码更加灵活和可重用。以下是Java高级开发需要掌握的泛型知识点:
-
泛型基础:
- 理解泛型的定义和基本语法,包括泛型类、泛型方法、泛型接口。
- 泛型用法示例:
class MyClass<T> {}
、public <T> void myMethod(T t) {}
。
-
泛型通配符:
- 理解
<?>
、<? extends T>
、<? super T>
等通配符的含义和使用场景。 - 了解上界通配符和下界通配符在泛型方法和泛型类中的应用。
- 理解
-
泛型和继承关系:
- 理解泛型在继承和子类化中的表现,包括泛型类的继承、通配符的上下界限定。
-
类型擦除:
- 理解Java泛型的类型擦除机制,即在运行时泛型类型信息被擦除为原始类型。
- 泛型类和泛型方法在编译后如何处理为非泛型形式的字节码。
-
泛型和集合:
- 熟悉Java集合框架中泛型的应用,如
List<T>
、Map<K, V>
等。 - 掌握使用泛型提高集合类型安全性的方法。
- 熟悉Java集合框架中泛型的应用,如
-
泛型方法:
- 理解泛型方法的定义和使用,以及与泛型类的区别。
- 了解泛型方法如何在静态方法和实例方法中应用。
-
泛型的好处:
- 掌握泛型的优势,如提高代码的类型安全性、避免类型转换错误、增加代码的可读性和重用性等。
-
泛型和反射:
- 理解泛型和反射的结合使用,如何通过反射获取泛型信息。
-
泛型约束:
- 熟悉泛型的类型约束,如泛型的边界限定(extends 和 super 关键字)。
-
泛型和异常:
- 了解泛型和异常处理的结合使用,如何处理泛型异常。
使用
1. 泛型类和泛型接口
限定类型可以在定义泛型类和泛型接口时使用。
// 限定类型 T 必须是 Number 的子类
public class NumberBox<T extends Number> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
// 泛型接口示例
public interface NumberProcessor<T extends Number> {
void process(T number);
}
2. 泛型方法
在定义泛型方法时,可以使用限定类型来约束方法的类型参数。
public class Utility {
// 泛型方法,限定类型 T 必须是 Number 的子类
public static <T extends Number> void printNumber(T number) {
System.out.println("Number: " + number);
}
}
3. 泛型通配符
限定类型也可以在泛型通配符中使用,以表示方法参数或返回值的类型约束。
import java.util.List;
public class WildcardExample {
// 通配符上限,list 中的元素必须是 Number 的子类
public static void printNumbers(List<? extends Number> list) {
for (Number number : list) {
System.out.println(number);
}
}
// 通配符下限,list 中的元素必须是 Integer 的父类
public static void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
}
4. 多重限定
如果类型参数需要同时满足多个接口,可以使用 &
符号进行多重限定。
// T 必须同时实现 Comparable<T> 和 Serializable 接口
public class MultiBound<T extends Comparable<T> & Serializable> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
5. 示例业务场景
下面通过一些具体的业务场景示例来说明限定类型的使用。
示例 1: 数据处理器
假设我们有一个数据处理器,需要处理不同类型的数值数据,我们可以使用泛型类来实现。
public class DataProcessor<T extends Number> {
private T data;
public DataProcessor(T data) {
this.data = data;
}
public void process() {
System.out.println("Processing data: " + data);
}
public T getData() {
return data;
}
}
public class Main {
public static void main(String[] args) {
DataProcessor<Integer> intProcessor = new DataProcessor<>(100);
intProcessor.process();
DataProcessor<Double> doubleProcessor = new DataProcessor<>(99.99);
doubleProcessor.process();
}
}
示例 2: 数字打印工具
我们可以使用泛型方法来创建一个打印数字的工具方法。
public class PrintUtil {
public static <T extends Number> void print(T number) {
System.out.println("Number: " + number);
}
public static void main(String[] args) {
print(123); // Integer
print(45.67); // Double
}
}
示例 3: 集合操作工具
使用泛型通配符来创建一个工具类,用于操作集合中的元素。
import java.util.List;
import java.util.ArrayList;
public class CollectionUtil {
public static void printNumbers(List<? extends Number> list) {
for (Number number : list) {
System.out.println("Number: " + number);
}
}
public static void addNumbers(List<? super Integer> list) {
list.add(10);
list.add(20);
}
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
addNumbers(intList);
printNumbers(intList);
}
}
结合具体场景的最佳实践
当然可以。以下是结合具体业务场景的Java泛型最佳实践讲解:
1. 确保类型安全
业务场景:一个电商系统需要存储和处理不同类型的订单(如书籍订单和电子产品订单)。
示例:
class Order { /*...*/ }
class BookOrder extends Order { /*...*/ }
class ElectronicsOrder extends Order { /*...*/ }
List<Order> orders = new ArrayList<>();
orders.add(new BookOrder());
orders.add(new ElectronicsOrder());
// orders.add(new String()); // 编译时会报错,防止插入非订单类型
通过使用泛型,确保集合中只存储订单对象,防止错误类型的插入。
2. 使用通配符提高代码灵活性
业务场景:需要处理一个包含各种类型商品的库存。
上界通配符(Producer-Extends):
public void printInventory(List<? extends Product> inventory) {
for (Product p : inventory) {
System.out.println(p.getName());
}
}
List<Book> books = new ArrayList<>();
List<Electronic> electronics = new ArrayList<>();
printInventory(books);
printInventory(electronics);
通过上界通配符,可以处理各种类型的商品清单。
下界通配符(Consumer-Super):
public void addElectronics(List<? super Electronics> inventory) {
inventory.add(new Electronics());
inventory.add(new Smartphone()); // Smartphone extends Electronics,注意这里可以添加三种类:父类,自己,子类(多态)
}
List<Product> products = new ArrayList<>();
addElectronics(products);
通过下界通配符,可以向商品列表中添加各种电子产品。
3. 避免使用原始类型
业务场景:一个社交媒体应用需要存储用户评论。
示例:
List<Comment> comments = new ArrayList<>();
comments.add(new Comment("Great post!"));
// comments.add(new User()); // 编译时会报错,防止插入非评论类型
通过使用泛型,确保集合中只包含评论对象。
4. 使用泛型方法提高代码重用性
业务场景:一个文件处理系统,需要打印不同类型的文件内容。
示例:
public <T> void printFiles(T[] files) {
for (T file : files) {
System.out.println(file.toString());
}
}
Document[] documents = {new Document("Doc1"), new Document("Doc2")};
Image[] images = {new Image("Image1"), new Image("Image2")};
printFiles(documents);
printFiles(images);
通过使用泛型方法,可以打印任何类型的文件内容,提高代码重用性。
5. 使用有界类型参数进行约束
业务场景:一个排行榜系统,需要找到分数最高的用户。
示例:
public <T extends Comparable<T>> T findTopScorer(T[] scores) {
T top = scores[0];
for (T score : scores) {
if (score.compareTo(top) > 0) {
top = score;
}
}
return top;
}
Integer[] intScores = {85, 92, 88};
Double[] doubleScores = {85.5, 92.3, 88.9};
System.out.println(findTopScorer(intScores)); // 输出92
System.out.println(findTopScorer(doubleScores)); // 输出92.3
通过使用有界类型参数,可以确保数组中的元素可以比较,从而找到最高分数。
6. 避免使用泛型类型的静态成员
业务场景:一个用户管理系统,需要存储用户信息。
示例:
public class UserManager<T> {
private T user;
// private static T instance; // 编译错误,避免使用泛型类型的静态成员
}
通过避免使用泛型类型的静态成员,防止类型擦除带来的问题。
7. 使用类型令牌解决类型擦除问题
业务场景:一个对象工厂,需要根据类型创建对象实例。
示例:
public <T> T createInstance(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
User user = createInstance(User.class);
通过使用类型令牌,可以在运行时获取泛型类型信息,创建对象实例。
8. 避免在泛型类中使用泛型数组
业务场景:一个订单管理系统,需要存储不同类型的订单。
示例:
public class OrderManager<T> {
private T[] orders; // 编译错误,避免使用泛型数组
private List<T> orderList = new ArrayList<>();
}
通过避免使用泛型数组,防止类型擦除带来的问题。
9. 谨慎使用泛型和异常
业务场景:一个数据处理系统,需要处理不同类型的数据。
示例:
// 错误示例
public <T extends Exception> void processData() throws T {
// 不能抛出或捕获泛型异常类型
}
避免抛出或捕获泛型异常类型。
10. 使用PECS原则
业务场景:一个物流系统,需要处理和添加不同类型的货物。
示例:
// Producer-Extends
public void processCargo(List<? extends Cargo> cargoList) {
for (Cargo cargo : cargoList) {
System.out.println(cargo.getDetails());
}
}
// Consumer-Super
public void addCargo(List<? super PerishableCargo> cargoList) {
cargoList.add(new PerishableCargo());
cargoList.add(new FreshCargo()); // FreshCargo extends PerishableCargo
}
List<Cargo> cargos = new ArrayList<>();
addCargo(cargos);
通过PECS原则,可以处理和添加不同类型的货物。
泛型的实现原理及其在运行时的表现
1. 泛型的本质
Java 泛型在编译时提供类型检查和类型安全,允许开发人员编写更灵活且类型安全的代码。然而,在运行时,Java 泛型会被类型擦除(Type Erasure),这意味着所有的类型参数都会被擦除,并替换为它们的限定类型(如果没有指定,则替换为 Object
)。
2. 类型擦除(Type Erasure)
类型擦除是 Java 泛型的核心机制。在编译时,编译器会移除泛型类型信息并插入必要的类型转换,以确保类型安全。在运行时,泛型类型信息不存在,所有泛型类型都被替换为原始类型。
例如,以下泛型类:
public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
在编译后,类型参数 T
会被替换为 Object
:
public class Box {
private Object content;
public void setContent(Object content) {
this.content = content;
}
public Object getContent() {
return content;
}
}
编译器在插入和取出元素时会生成适当的类型转换代码,以确保类型安全。
3. 限定类型
如果泛型类型参数有上限限制,如 T extends Number
,类型擦除后会替换为限定类型,而不是 Object
。
例如:
public class NumberBox<T extends Number> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
在编译后,类型参数 T
会被替换为 Number
:
public class NumberBox {
private Number content;
public void setContent(Number content) {
this.content = content;
}
public Number getContent() {
return content;
}
}
4. 泛型方法
泛型方法在运行时也会经历类型擦除,其类型参数在运行时被替换为限定类型或 Object
。
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
在编译后,类型参数 T
被替换为 Object
:
public static void printArray(Object[] array) {
for (Object element : array) {
System.out.println(element);
}
}
5. 泛型数组
Java 不允许创建泛型类型的数组,因为在运行时泛型类型被擦除,数组的运行时类型需要具体的类型信息。
以下代码是非法的:
List<String>[] listArray = new List<String>[10]; // 编译错误
解决方法是使用通配符或 Object
:
List<?>[] listArray = new List<?>[10];
6. 泛型与反射
由于类型擦除,在使用反射时,无法获取泛型类型参数的具体类型信息。例如:
List<String> list = new ArrayList<>();
Type type = list.getClass().getGenericSuperclass();
System.out.println(type); // 输出 java.util.AbstractList<E>
上面的代码只能得到泛型类型参数 E
,而不是具体的 String
类型。
为什么输出
java.util.AbstractList<E>
ArrayList
继承自AbstractList
,而AbstractList
又是AbstractCollection
的子类。具体的继承关系如下:
ArrayList<E>
extendsAbstractList<E>
AbstractList<E>
extendsAbstractCollection<E>
当你调用
list.getClass().getGenericSuperclass()
时,getGenericSuperclass()
方法返回的是直接父类的类型,即AbstractList<E>
。这个类型包含泛型信息,但因为泛型类型参数E
是在编译时擦除的,实际运行时显示的是泛型类型E
。
小结
Java 泛型通过类型擦除实现,在编译时确保类型安全,但在运行时移除类型信息。类型擦除机制使得 Java 泛型在运行时没有性能开销,并且与非泛型代码兼容。然而,这也导致在运行时无法获取具体的泛型类型信息,需要通过其他方式(如反射)来处理泛型类型。
类型擦除的例外
在 Java 中,泛型类型信息确实会在编译期间被擦除,这就是所谓的类型擦除(type erasure)。类型擦除的基本概念是:泛型参数类型在编译时被替换为它们的非泛型上界(通常是 Object
,除非有特定的边界),并在必要时插入类型转换。类型擦除的结果是,在运行时,泛型参数的实际类型信息是不可用的。然而,有一些情况下,类型信息是可以保留的,主要依赖于编译器在生成 class 文件时的额外信息。以下是几种情况:
- 字段:类的字段声明为泛型时,泛型信息会存储在 class 文件的字段信息部分。
- 方法参数和返回类型:方法参数或返回类型使用泛型时,泛型信息会存储在 class 文件的方法签名部分。
- 类和接口:类或接口本身声明为泛型时,泛型信息会存储在 class 文件的类型信息部分。
1. 字段的泛型类型
当一个类的字段声明为泛型类型时,编译器会将泛型类型的信息存储在 class 文件的字段信息部分。这使得在运行时,通过反射 API 可以获取到这些泛型类型的信息。比如:
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
public class GenericField {
private List<String> stringList;
public static void main(String[] args) throws NoSuchFieldException {
Field field = GenericField.class.getDeclaredField("stringList");
Type genericFieldType = field.getGenericType();
if (genericFieldType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) genericFieldType;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (Type type : actualTypeArguments) {
System.out.println(type);
}
}
}
}
在这个示例中,stringList
字段的泛型类型信息在编译期被存储在 class 文件中,虽然在运行时无法直接使用泛型类型,但可以通过反射 API 获取到这些信息。输出结果为 class java.lang.String
。
2. 方法的泛型参数类型
类似地,当方法的参数或返回类型使用泛型时,编译器会在 class 文件中存储这些泛型信息。在运行时,可以通过反射获取这些信息。比如:
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
public class GenericMethod {
public <T> void printList(List<T> list) {
// Method body
}
public static void main(String[] args) throws NoSuchMethodException {
Method method = GenericMethod.class.getMethod("printList", List.class);
Type[] genericParameterTypes = method.getGenericParameterTypes();
for (Type type : genericParameterTypes) {
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (Type actualType : actualTypeArguments) {
System.out.println(actualType);
}
}
}
}
}
在这个示例中,printList
方法的参数 list
的泛型类型信息被存储在 class 文件中,可以在运行时通过反射获取。
总结
类型擦除确实会在运行时丢失泛型参数的具体类型信息,但是编译器会在 class 文件中存储一些必要的泛型信息,使得可以在运行时通过反射获取这些信息。因此,通过 ParameterizedType
,可以在运行时获取泛型参数的类型信息,这在某些情况下非常有用。
ParameterizedType
ParameterizedType
是 Java 反射 API 中的一个接口,它表示一个带有实际类型参数的泛型类型。在实际开发中,我们经常会遇到需要在运行时获取泛型类型参数的情况,这时 ParameterizedType
就非常有用。
ParameterizedType
接口用于表示参数化类型。一个参数化类型是指带有实际类型参数的类型,比如 List<String>
、Map<String, Integer>
等。通过反射 API,可以在运行时获取这些参数化类型的实际类型参数。
常用方法
ParameterizedType
接口定义了一些方法,用于获取参数化类型的详细信息:
Type[] getActualTypeArguments()
:返回实际类型参数的数组。Type getRawType()
:返回不带泛型参数的原始类型。Type getOwnerType()
:返回这个类型的所有者类型,如果这个类型是一个内部类的话。
使用示例
下面通过一个具体的例子来说明如何使用 ParameterizedType
获取泛型类型参数。
示例 1:获取类的泛型类型参数
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
public class GenericClass<T> {
public static void main(String[] args) {
// 使用匿名子类来保留泛型类型信息
GenericClass<List<String>> instance = new GenericClass<List<String>>() {};
Type superclass = instance.getClass().getGenericSuperclass();
if (superclass instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) superclass;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (Type type : actualTypeArguments) {
System.out.println(type);
}
}
}
}
在这个示例中,通过创建 GenericClass
的匿名子类,我们保留了泛型类型信息。然后使用反射获取泛型类型参数,输出结果为 java.util.List<java.lang.String>
。
示例 2:获取方法的泛型类型参数
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
public class GenericMethod {
public <T> void printList(List<T> list) {
// Method body
}
public static void main(String[] args) throws NoSuchMethodException {
Method method = GenericMethod.class.getMethod("printList", List.class);
Type[] genericParameterTypes = method.getGenericParameterTypes();
for (Type type : genericParameterTypes) {
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (Type actualType : actualTypeArguments) {
System.out.println(actualType);
}
}
}
}
}
在这个示例中,我们通过反射获取了泛型方法 printList
的类型参数,输出结果为 T
。
示例 3:获取字段的泛型类型参数
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
public class GenericField {
private List<String> stringList;
public static void main(String[] args) throws NoSuchFieldException {
Field field = GenericField.class.getDeclaredField("stringList");
Type genericFieldType = field.getGenericType();
if (genericFieldType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) genericFieldType;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (Type type : actualTypeArguments) {
System.out.println(type);
}
}
}
}
在这个示例中,我们通过反射获取了泛型字段 stringList
的类型参数,输出结果为 java.lang.String
。