1.泛型类
使用场景:当你需要创建一个通用的数据结构,例如列表、栈、队列、字典等,这些结构可以处理各种类型的数据时,可以使用泛型类。
优势:泛型类提高了代码的可重用性,因为你可以使用一个类定义来处理不同类型的数据。此外,泛型类还可以提高类型安全性,因为它们在编译时检查类型,从而减少了运行时类型转换错误的可能性。
示例:一个通用的列表类:
public class GenericList<T> {
private List<T> items = new ArrayList<>();
public void add(T item) {
items.add(item);
}
public T get(int index) {
return items.get(index);
}
// 其他方法
}
2.泛型接口
使用场景:当你需要设计一组具有通用操作的接口时,例如比较器、转换器、工厂等,这些接口可以应用于不同类型的对象,可以使用泛型接口。
优势:泛型接口提高了代码的可重用性,因为你可以使用一个接口定义来描述不同类型对象之间的通用操作。此外,泛型接口还可以提高类型安全性,因为它们在编译时检查类型,从而减少了运行时类型转换错误的可能性。
示例:一个通用的比较器接口:
public interface Comparator<T> {
int compare(T o1, T o2);
}
3.泛型方法
使用场景:当你需要设计一个通用的方法,它可以处理不同类型的参数时,可以使用泛型方法。泛型方法在普通类和泛型类中都可以使用。
优势:泛型方法提高了代码的可重用性,因为你可以使用一个方法定义来处理不同类型的参数。此外,泛型方法还可以提高类型安全性,因为它们在编译时检查类型,从而减少了运行时类型转换错误的可能性。
示例:一个通用的交换数组元素的方法:
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
4.类型限定
使用场景:当你需要限制泛型类型参数的范围时,例如,只允许实现了某个接口或继承自某个类的类型作为参数,可以使用类型限定。
优势:类型限定可以增加代码的灵活性,使其适应更多的类型。同时,类型限定还可以确保泛型代码的正确性,因为它限制了可以作为类型参数的范围。
示例:一个只接受某个接口的类型参数的泛型方法:
public interface Named {
String getName();
}
public static <T extends Named> void printNames(List<T> items) {
for (T item : items) {
System.out.println(item.getName());
}
}
5.通配符
使用场景:当你需要编写可以处理各种类型的泛型类或泛型方法的代码时,可以使用通配符。通配符可以用于泛型类和泛型方法的参数,增加了代码的灵活性。特别是在处理泛型集合时,通配符的使用可以让代码更加通用和灵活。
优势:通配符提高了代码的灵活性,使得代码可以适应更广泛的类型。此外,通配符有助于降低类型转换错误的风险。
示例:一个处理各种类型的泛型集合的方法:
public static void printCollection(Collection<?> collection) {
for (Object item : collection) {
System.out.println(item);
}
}
总结:Java 中的泛型在不同的场景下都有其优势,包括提高代码的可重用性、灵活性和类型安全性。泛型类、泛型接口、泛型方法、类型限定和通配符等特性都可以在适当的场景下提高代码的质量。在设计和实现 Java 代码时,了解泛型的这些特性以及如何在不同场景下应用它们是非常重要的。
使用泛型注意事项
1.类型擦除
在 Java 中,泛型的实现是基于类型擦除的。这意味着在运行时,泛型信息会被擦除,泛型类型参数会被替换为它们的限定类型(无限定类型时则为 Object)。这可能导致以下问题:
- 无法创建泛型数组:由于类型擦除,泛型数组在运行时无法确定其实际类型,因此不能创建泛型数组。例如,以下代码是非法的:
List<String>[] stringLists = new List<String>[10]; // 编译错误
作为替代,可以创建一个未泛型化的数组,并在运行时进行类型转换:
List<String>[] stringLists = (List<String>[]) new List[10];
- 无法实例化泛型类型参数:由于类型擦除,无法直接实例化泛型类型参数。例如,以下代码是非法的:
public class Box<T> {
T createInstance() {
return new T(); // 编译错误
}
}
解决方案之一是通过反射来创建实例,但需要传递一个 Class 对象:
public class Box<T> {
private Class<T> clazz;
public Box(Class<T> clazz) {
this.clazz = clazz;
}
T createInstance() throws IllegalAccessException, InstantiationException {
return clazz.newInstance();
}
}
2.堆污染
堆污染是指泛型对象被分配了一个错误类型的引用。这通常发生在使用原始类型、混合泛型和非泛型代码时。堆污染可能导致运行时的 ClassCastException。
例如:
List<Integer> integers = new ArrayList<>();
List rawList = integers;
rawList.add("Hello"); // 堆污染
int firstInteger = integers.get(0); // 运行时抛出 ClassCastException
为了避免堆污染,请尽量避免使用原始类型,并确保泛型和非泛型代码正确地交互。
3.通配符的限制
使用通配符可以使泛型代码更灵活,但也会带来一些限制:
- 使用通配符的集合通常只能用作消费者或生产者,而不能同时用作两者。例如,不能将元素添加到
List<?>
中(除了 null)。 - 使用通配符时,需要注意协变和逆变。
List<? extends T>
可以作为生产者(可以获取 T 类型的元素),但不能向其中添加元素。List<? super T>
可以 - 作为消费者(可以添加 T 类型的元素),但不能保证从中获取的元素具有 T 类型。为了避免类型错误,需要根据实际情况使用适当的通配符。
4.使用泛型时的类型推断问题
Java 编译器在泛型方法调用和泛型类型实例化时会尝试推断类型参数。但在某些情况下,编译器可能无法推断出正确的类型,导致编译错误。在这种情况下,可以使用显式类型参数来解决问题。
例如:
public static <T> T defaultValue() {
return null;
}
public static void main(String[] args) {
// 编译错误,因为编译器无法推断出类型参数
// String defaultString = defaultValue();
// 使用显式类型参数来解决问题
String defaultString = Utils.<String>defaultValue();
}
5.在泛型代码中使用反射
由于类型擦除,泛型类型参数在运行时不可用。这意味着在泛型代码中使用反射时可能遇到问题。例如,不能直接查询泛型类型参数的类对象:
public class Box<T> {
public void doSomething() {
// 编译错误,因为 T 在运行时不可用
// Class<T> clazz = T.class;
}
}
解决方案之一是在创建泛型对象时显式传递 Class 对象,如前文所述。
总之,在使用 Java 泛型时,需要注意类型擦除、堆污染、通配符限制、类型推断问题以及在泛型代码中使用反射等潜在问题。了解这些问题及其解决方案可以帮助您编写更安全、更可靠的泛型代码。