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 泛型时,需要注意类型擦除、堆污染、通配符限制、类型推断问题以及在泛型代码中使用反射等潜在问题。了解这些问题及其解决方案可以帮助您编写更安全、更可靠的泛型代码。