MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java泛型编程的深入探讨与应用场景

2023-06-165.6k 阅读

Java 泛型编程基础概念

Java 泛型(Generics)是 JDK 5.0 引入的一个重要特性,它提供了编译时类型安全检测机制,允许我们在编译时检测到非法的类型操作。简单来说,泛型让我们可以在定义类、接口和方法的时候使用类型参数,这些类型参数在使用时会被具体的类型所替换。

例如,我们定义一个简单的泛型类 Box

class Box<T> {
    private T t;

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }
}

在这个例子中,T 就是类型参数。我们可以在创建 Box 对象的时候指定 T 的具体类型,比如:

Box<Integer> integerBox = new Box<>();
integerBox.set(10);
Integer value = integerBox.get();

这里我们将 T 替换为 IntegerBox 就成为了一个专门存放 Integer 类型数据的盒子。如果我们尝试放入其他类型的数据,比如 String,编译器就会报错,这就是泛型提供的编译时类型安全检测。

泛型类型参数命名规范

在 Java 中,泛型类型参数通常使用单个大写字母命名,常见的命名约定如下:

  • T(Type):最常用,代表一般的类型。
  • E(Element):通常用于表示集合中的元素类型,比如 List<E>
  • K(Key)和 V(Value):常用于表示键值对中的键和值类型,比如 Map<K, V>
  • N(Number):用于表示数值类型。

泛型类

定义泛型类

泛型类就是在类名后面使用尖括号 <> 声明类型参数的类。除了前面提到的 Box 类,我们再看一个更复杂一点的例子,定义一个可以存储两个不同类型值的 Pair 类:

class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

在这个 Pair 类中,我们使用了两个类型参数 KV,分别代表键和值的类型。使用时可以这样:

Pair<String, Integer> pair = new Pair<>("age", 30);
String key = pair.getKey();
Integer value = pair.getValue();

泛型类的继承和实现

当一个泛型类被继承或者实现接口时,有几种不同的情况。

  1. 子类也是泛型类
class SubPair<K, V> extends Pair<K, V> {
    public SubPair(K key, V value) {
        super(key, value);
    }
}

这里 SubPair 继承自 Pair,并且也使用了相同的类型参数 KV

  1. 子类指定父类的类型参数
class StringIntegerPair extends Pair<String, Integer> {
    public StringIntegerPair(String key, Integer value) {
        super(key, value);
    }
}

在这种情况下,StringIntegerPair 已经明确指定了 Pair 的类型参数为 StringInteger,它不再是泛型类。

泛型接口

定义泛型接口

泛型接口和泛型类类似,在接口名后面声明类型参数。例如,定义一个比较器接口 Comparator

public interface Comparator<T> {
    int compare(T o1, T o2);
}

这个接口定义了一个 compare 方法,用于比较两个类型为 T 的对象。

实现泛型接口

  1. 实现类也是泛型类
class MyComparator<T> implements Comparator<T> {
    @Override
    public int compare(T o1, T o2) {
        // 假设 T 实现了 Comparable 接口
        return ((Comparable<T>) o1).compareTo(o2);
    }
}

这里 MyComparator 是一个泛型类,实现了 Comparator<T> 接口。

  1. 实现类指定接口的类型参数
class IntegerComparator implements Comparator<Integer> {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
}

IntegerComparator 明确指定了实现的是 Comparator<Integer> 接口,不再是泛型类。

泛型方法

定义泛型方法

泛型方法是在方法声明中使用类型参数的方法,它可以在普通类中定义,也可以在泛型类中定义。例如,在一个普通类 Utils 中定义一个泛型方法 printArray

class Utils {
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
}

这里 <T> 表示这是一个泛型方法,T 是类型参数。使用时可以这样:

Integer[] intArray = {1, 2, 3};
Utils.printArray(intArray);

String[] stringArray = {"a", "b", "c"};
Utils.printArray(stringArray);

泛型方法的类型推断

Java 编译器可以根据方法调用时传递的参数类型来推断泛型方法的类型参数。例如,对于上面的 printArray 方法,我们调用 Utils.printArray(intArray) 时,编译器可以推断出 TInteger

但是,在某些情况下,类型推断可能不那么明显,比如当方法有多个参数,并且类型参数在不同参数中使用时。例如:

class GenericMethodExample {
    public static <T> T findFirst(T[] array, T target) {
        for (T element : array) {
            if (element.equals(target)) {
                return element;
            }
        }
        return null;
    }
}

在调用 findFirst 方法时,如果参数类型比较复杂,编译器可能无法正确推断类型参数,这时我们可能需要显式指定类型参数,比如 GenericMethodExample.<String>findFirst(stringArray, "b")

通配符

上界通配符

上界通配符使用 ? extends 语法,它表示类型参数的上界。例如,假设有一个类层次结构:

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

我们定义一个方法,它接受一个 List,但这个 List 中的元素必须是 Animal 或者 Animal 的子类:

import java.util.List;

class WildcardExample {
    public static void printAnimals(List<? extends Animal> list) {
        for (Animal animal : list) {
            System.out.println(animal);
        }
    }
}

这里 List<? extends Animal> 表示 list 可以是 List<Animal>List<Dog> 或者 List<Cat> 等。

下界通配符

下界通配符使用 ? super 语法,它表示类型参数的下界。例如,我们定义一个方法,它接受一个 List,这个 List 中的元素必须是 Dog 或者 Dog 的父类:

import java.util.List;

class WildcardExample {
    public static void addDog(List<? super Dog> list) {
        list.add(new Dog());
    }
}

这里 List<? super Dog> 表示 list 可以是 List<Dog>List<Animal> 等。

泛型擦除

泛型擦除的概念

Java 的泛型是一种编译时特性,在编译之后,所有的泛型类型信息都会被擦除,这就是泛型擦除。例如,对于 Box<Integer>,在编译后,它实际上会变成 BoxInteger 类型信息被擦除了。

泛型擦除的实现原理

编译器在编译泛型代码时,会进行类型检查和类型推断,然后将泛型类型替换为它们的擦除类型。对于有上界的类型参数,擦除类型是它们的上界;对于没有上界的类型参数,擦除类型是 Object

例如,对于泛型类 Box<T>,如果没有指定上界,编译后:

class Box {
    private Object t;

    public void set(Object t) {
        this.t = t;
    }

    public Object get() {
        return t;
    }
}

如果 Box<T> 定义为 Box<T extends Number>,编译后:

class Box {
    private Number t;

    public void set(Number t) {
        this.t = t;
    }

    public Number get() {
        return t;
    }
}

泛型擦除带来的问题和解决方案

  1. 不能创建泛型数组 由于泛型擦除,new T[10] 这样的代码是不允许的,因为在运行时 T 被擦除为 Object,创建的数组实际上是 Object[],这可能导致类型安全问题。解决方案是使用 ArrayList 等集合类来代替数组。

  2. 不能在静态上下文中使用泛型类型参数 因为静态成员属于类,而不是对象,在类加载时,泛型类型参数还没有被具体类型替换,所以不能在静态方法、静态字段中使用泛型类型参数。例如:

class GenericStaticExample<T> {
    // 错误,不能在静态字段中使用泛型类型参数
    private static T staticField;

    // 错误,不能在静态方法中使用泛型类型参数
    public static void staticMethod(T t) {}
}

如果需要在静态方法中使用泛型,可以将静态方法定义为泛型方法:

class GenericStaticExample {
    public static <T> void staticMethod(T t) {}
}

Java 泛型的应用场景

在集合框架中的应用

Java 集合框架广泛使用了泛型,使得集合可以存储特定类型的元素,同时保证类型安全。例如,ArrayList 类:

import java.util.ArrayList;
import java.util.List;

class ArrayListExample {
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        stringList.add("hello");
        // 编译错误,不能放入非 String 类型的元素
        // stringList.add(10);
        String element = stringList.get(0);
    }
}

通过使用泛型,ArrayList 可以明确存储的元素类型,避免了在运行时发生类型转换异常。

自定义数据结构中的应用

在自定义数据结构中,泛型也非常有用。比如我们定义一个简单的栈数据结构:

class Stack<T> {
    private T[] elements;
    private int top;

    public Stack(int capacity) {
        elements = (T[]) new Object[capacity];
        top = -1;
    }

    public void push(T element) {
        elements[++top] = element;
    }

    public T pop() {
        return elements[top--];
    }

    public boolean isEmpty() {
        return top == -1;
    }
}

这个栈可以存储任何类型的数据,通过泛型保证了类型安全。使用时:

Stack<Integer> integerStack = new Stack<>(5);
integerStack.push(10);
Integer value = integerStack.pop();

在算法实现中的应用

泛型在算法实现中也有广泛应用。例如,我们实现一个通用的排序算法,使用 Comparator 接口:

import java.util.Comparator;

class SortingAlgorithm {
    public static <T> void sort(T[] array, Comparator<T> comparator) {
        for (int i = 0; i < array.length - 1; i++) {
            for (int j = i + 1; j < array.length; j++) {
                if (comparator.compare(array[i], array[j]) > 0) {
                    T temp = array[i];
                    array[i] = array[j];
                    array[j] = temp;
                }
            }
        }
    }
}

这里的 sort 方法是一个泛型方法,可以对任何实现了 Comparator 接口的类型数组进行排序。使用时:

Integer[] intArray = {3, 1, 2};
SortingAlgorithm.sort(intArray, (o1, o2) -> o1 - o2);

泛型与反射

泛型在反射中的应用

反射是 Java 提供的一种在运行时获取类的信息、调用类的方法等功能的机制。在反射中使用泛型可以获取更精确的类型信息。

例如,我们获取一个泛型类 Box<Integer> 的泛型类型信息:

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

class Box<T> {
    private T t;

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }
}

public class ReflectionWithGenerics {
    public static void main(String[] args) {
        Box<Integer> box = new Box<>();
        Class<?> boxClass = box.getClass();
        Type genericSuperclass = boxClass.getGenericSuperclass();
        if (genericSuperclass instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
            Type[] typeArguments = parameterizedType.getActualTypeArguments();
            for (Type typeArgument : typeArguments) {
                System.out.println("泛型类型参数: " + typeArgument);
            }
        }
    }
}

在这个例子中,通过反射获取了 Box 类的泛型类型参数 Integer

反射操作泛型类和方法

我们还可以通过反射操作泛型类的方法,并且处理泛型类型参数。例如,假设我们有一个泛型类 GenericMethodClass

class GenericMethodClass<T> {
    public T genericMethod(T t) {
        return t;
    }
}

通过反射调用这个泛型方法:

import java.lang.reflect.Method;

public class ReflectionCallGenericMethod {
    public static void main(String[] args) throws Exception {
        GenericMethodClass<Integer> genericObject = new GenericMethodClass<>();
        Class<?> clazz = genericObject.getClass();
        Method method = clazz.getMethod("genericMethod", clazz.getTypeParameters()[0].getClass());
        Integer result = (Integer) method.invoke(genericObject, 10);
        System.out.println("方法返回值: " + result);
    }
}

在这个例子中,我们通过反射获取了 genericMethod 方法,并调用它,处理了泛型类型参数。

总结

Java 泛型编程是一个强大的特性,它提供了编译时类型安全检测,使代码更加健壮和可读。通过泛型类、接口、方法以及通配符的使用,我们可以编写更加通用和灵活的代码。虽然泛型擦除带来了一些限制,但通过合理的设计和使用集合框架等方式,我们可以充分发挥泛型的优势。在实际开发中,无论是集合框架、自定义数据结构还是算法实现,泛型都有着广泛的应用场景,熟练掌握泛型编程对于编写高质量的 Java 代码至关重要。同时,了解泛型与反射的结合使用,可以在一些需要动态处理类型信息的场景中发挥更大的作用。