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

Java泛型与继承关系

2021-12-028.0k 阅读

Java泛型的基础概念

Java泛型是一种强大的类型参数化机制,它允许我们在定义类、接口或方法时使用类型参数。这些类型参数在使用时会被具体的类型所替代,从而提供了一种类型安全且可复用的编程方式。

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

class Box<T> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

在这个例子中,T就是一个类型参数。我们可以通过以下方式使用这个泛型类:

Box<Integer> intBox = new Box<>();
intBox.setValue(10);
Integer num = intBox.getValue();

这里,T被具体化为Integer类型,使得Box类能够安全地存储和获取Integer类型的数据。

泛型类型擦除

在Java中,泛型主要是在编译期起作用,运行时并不保留泛型的具体类型信息,这就是所谓的类型擦除。

编译器会在编译时根据泛型的使用情况进行类型检查,并将泛型类型替换为它们的擦除类型。对于没有指定边界的类型参数,擦除类型是Object;对于有边界的类型参数,擦除类型是边界类型。

例如,对于Box<T>,在运行时,T会被擦除为Object。这意味着在运行时,Box<Integer>Box<String>实际上具有相同的类型,都是Box(擦除后的类型)。

泛型与继承的基本关系

  1. 泛型类的继承
    • 当一个泛型类继承自另一个泛型类时,子类可以选择保留父类的类型参数,也可以指定具体的类型。
    • 例如,定义一个父类GenericParent和一个子类GenericChild
class GenericParent<T> {
    protected T value;

    public GenericParent(T value) {
        this.value = value;
    }
}

class GenericChild<T> extends GenericParent<T> {
    public GenericChild(T value) {
        super(value);
    }

    public T getValue() {
        return value;
    }
}

在这个例子中,GenericChild保留了父类GenericParent的类型参数T。这样,GenericChild可以处理与GenericParent相同类型参数的情况。

  • 如果我们想要子类处理特定类型,可以这样定义:
class SpecificChild extends GenericParent<String> {
    public SpecificChild(String value) {
        super(value);
    }

    public String getValue() {
        return value;
    }
}

这里,SpecificChild将父类的类型参数指定为String,使得SpecificChild只能处理String类型的数据。

  1. 泛型接口的继承
    • 泛型接口的继承与泛型类类似。一个接口可以继承另一个泛型接口,并保留或指定类型参数。
    • 例如,定义一个泛型接口GenericInterface和一个实现接口ImplementingInterface
interface GenericInterface<T> {
    T getValue();
}

class ImplementingClass<T> implements GenericInterface<T> {
    private T value;

    public ImplementingClass(T value) {
        this.value = value;
    }

    @Override
    public T getValue() {
        return value;
    }
}

这里,ImplementingClass实现了GenericInterface并保留了类型参数T

  • 也可以在实现接口时指定具体类型:
class SpecificImplementingClass implements GenericInterface<Integer> {
    private Integer value;

    public SpecificImplementingClass(Integer value) {
        this.value = value;
    }

    @Override
    public Integer getValue() {
        return value;
    }
}

这里,SpecificImplementingClass将接口的类型参数指定为Integer,只能处理Integer类型的数据。

通配符与继承关系

  1. 上界通配符(? extends
    • 上界通配符用于表示一个未知类型,这个未知类型是某个特定类型的子类型。
    • 例如,假设有一个类层次结构:Animal是父类,DogCatAnimal的子类。
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
  • 我们定义一个方法,该方法接受一个Box,但只关心其中装的是Animal或其子类:
void printAnimals(Box<? extends Animal> animalBox) {
    Animal animal = animalBox.getValue();
    System.out.println(animal);
}
  • 这样,我们可以传递Box<Dog>Box<Cat>给这个方法:
Box<Dog> dogBox = new Box<>();
dogBox.setValue(new Dog());
printAnimals(dogBox);

Box<Cat> catBox = new Box<>();
catBox.setValue(new Cat());
printAnimals(catBox);
  • 上界通配符的作用在于,它允许我们读取Box中的数据,但不允许写入(除了null)。因为编译器无法确定? extends Animal具体是哪种类型,写入可能会导致类型安全问题。
  1. 下界通配符(? super
    • 下界通配符表示一个未知类型,这个未知类型是某个特定类型的超类型。
    • 还是以Animal类层次结构为例,我们定义一个方法,该方法接受一个Box,并且可以向其中写入Dog类型的数据:
void addDog(Box<? super Dog> dogBox) {
    dogBox.setValue(new Dog());
}
  • 这里,? super Dog表示Box中装的类型是DogDog的超类型(如AnimalObject)。这样,我们可以传递Box<Dog>Box<Animal>甚至Box<Object>给这个方法:
Box<Dog> dogBox = new Box<>();
addDog(dogBox);

Box<Animal> animalBox = new Box<>();
addDog(animalBox);

Box<Object> objectBox = new Box<>();
addDog(objectBox);
  • 下界通配符允许我们写入数据,但读取数据时会受到限制,因为我们无法确定具体的类型,只能将读取到的数据当作Object类型处理(除非有进一步的类型检查)。

泛型数组与继承

  1. 泛型数组的创建限制
    • 在Java中,不能直接创建泛型数组。例如,T[] array = new T[10];这样的代码是不允许的,因为类型擦除会导致运行时类型错误。
    • 但是,我们可以通过一些间接的方式创建泛型数组。例如:
class GenericArray<T> {
    private T[] array;

    @SuppressWarnings("unchecked")
    public GenericArray(int size) {
        array = (T[]) new Object[size];
    }

    public void set(int index, T value) {
        array[index] = value;
    }

    public T get(int index) {
        return array[index];
    }
}
  • 在这个例子中,我们使用了类型转换(T[]) new Object[size],这是因为new T[size]不被允许。同时,我们使用了@SuppressWarnings("unchecked")注解来抑制编译器的警告,因为这种类型转换在运行时可能会导致ClassCastException,如果使用不当的话。
  1. 泛型数组与继承关系
    • 虽然不能直接创建泛型数组,但当涉及到继承关系时,泛型数组的行为与普通数组有相似之处。
    • 例如,假设我们有一个Box泛型类和一个Box<Dog>类型的数组:
Box<Dog>[] dogBoxArray = new Box[10]; // 实际上创建的是Box[],但由于类型擦除,编译时不会报错
Box<Animal>[] animalBoxArray = dogBoxArray; // 编译错误,因为泛型数组不允许这样的赋值,即使Dog是Animal的子类
  • 这里,虽然DogAnimal的子类,但Box<Dog>[]Box<Animal>[]之间不存在继承关系。这是为了确保类型安全,防止在运行时将错误类型的数据放入数组中。

泛型方法与继承

  1. 泛型方法的定义
    • 泛型方法是在方法声明中定义类型参数的方法,它可以独立于类的泛型定义。
    • 例如,定义一个泛型方法printArray,可以打印任何类型的数组:
class GenericMethods {
    static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
}
  • 我们可以通过以下方式调用这个泛型方法:
Integer[] intArray = {1, 2, 3};
GenericMethods.printArray(intArray);

String[] stringArray = {"a", "b", "c"};
GenericMethods.printArray(stringArray);
  1. 泛型方法与继承关系
    • 当泛型方法在继承体系中使用时,子类可以重写父类的泛型方法。
    • 例如,定义一个父类Parent和一个子类Child,父类有一个泛型方法:
class Parent {
    <T> void genericMethod(T value) {
        System.out.println("Parent's generic method: " + value);
    }
}

class Child extends Parent {
    @Override
    <T> void genericMethod(T value) {
        System.out.println("Child's generic method: " + value);
    }
}
  • 这里,子类Child重写了父类Parent的泛型方法。注意,在重写时,子类的泛型方法签名必须与父类完全一致(包括类型参数)。

  • 同时,子类也可以定义自己特有的泛型方法,与父类的泛型方法没有直接的重写关系:

class Child {
    @Override
    <T> void genericMethod(T value) {
        System.out.println("Child's generic method: " + value);
    }

    <U> void anotherGenericMethod(U value) {
        System.out.println("Child's another generic method: " + value);
    }
}

泛型的多边界

  1. 多边界的定义
    • 泛型类型参数可以有多个边界,使用&符号连接。这意味着类型参数必须是所有边界类型的子类型。
    • 例如,定义一个泛型类MultiBound,其类型参数T必须是SerializableComparable的子类型:
class MultiBound<T extends Serializable & Comparable<T>> {
    private T value;

    public MultiBound(T value) {
        this.value = value;
    }

    public int compareTo(T other) {
        return value.compareTo(other);
    }
}
  • 在这个例子中,T必须同时实现SerializableComparable接口。这样,我们可以在类中安全地使用T类型对象的compareTo方法,并且可以将T类型对象进行序列化。
  1. 多边界与继承关系
    • 当涉及继承关系时,如果一个类实现了多个接口,并且这些接口作为泛型类型参数的边界,那么该类的子类也必须满足这些边界条件。
    • 例如,假设有一个类MyClass实现了SerializableComparable接口:
class MyClass implements Serializable, Comparable<MyClass> {
    private int id;

    public MyClass(int id) {
        this.id = id;
    }

    @Override
    public int compareTo(MyClass other) {
        return Integer.compare(this.id, other.id);
    }
}
  • 我们可以使用MyClass作为MultiBound的类型参数:
MultiBound<MyClass> multiBound = new MultiBound<>(new MyClass(10));
  • 如果有一个子类SubMyClass继承自MyClass,它也满足MultiBound的类型参数边界条件:
class SubMyClass extends MyClass {
    public SubMyClass(int id) {
        super(id);
    }
}

MultiBound<SubMyClass> subMultiBound = new MultiBound<>(new SubMyClass(20));

泛型与反射

  1. 反射获取泛型信息
    • 在Java中,通过反射可以获取泛型类型信息,尽管类型擦除会丢失一些具体的类型细节,但仍然可以获取部分泛型信息。
    • 例如,获取泛型类的类型参数:
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

class GenericReflection {
    public static void main(String[] args) {
        Box<Integer> intBox = new Box<>();
        Type type = intBox.getClass().getGenericSuperclass();
        if (type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) type;
            Type[] typeArguments = parameterizedType.getActualTypeArguments();
            for (Type typeArgument : typeArguments) {
                System.out.println("Type argument: " + typeArgument);
            }
        }
    }
}
  • 在这个例子中,通过getClass().getGenericSuperclass()获取泛型类的泛型超类信息,然后通过ParameterizedType获取实际的类型参数。
  1. 反射与泛型的继承关系
    • 当涉及继承关系时,反射可以获取到泛型在继承体系中的相关信息。
    • 例如,假设有一个子类SubBox继承自Box,并且在子类中指定了具体的类型参数:
class SubBox extends Box<String> {}
  • 我们可以通过反射获取子类的泛型信息:
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

class GenericReflectionInheritance {
    public static void main(String[] args) {
        SubBox subBox = new SubBox();
        Type type = subBox.getClass().getGenericSuperclass();
        if (type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) type;
            Type[] typeArguments = parameterizedType.getActualTypeArguments();
            for (Type typeArgument : typeArguments) {
                System.out.println("Type argument of SubBox: " + typeArgument);
            }
        }
    }
}
  • 这里,通过反射可以获取到SubBox的父类Box的泛型类型参数为String,这展示了反射在处理泛型与继承关系时的能力。

泛型在集合框架中的应用与继承关系

  1. 集合框架中的泛型
    • Java集合框架广泛使用了泛型,以提供类型安全的集合操作。
    • 例如,List接口是一个泛型接口,我们可以创建List<Integer>List<String>等具体类型的列表:
List<Integer> intList = new ArrayList<>();
intList.add(10);
Integer num = intList.get(0);
  • 这里,List<Integer>表示一个只能存储Integer类型元素的列表,通过泛型确保了类型安全。
  1. 集合框架中泛型与继承关系
    • 在集合框架中,泛型与继承关系有一些特定的规则。
    • 例如,ArrayListList接口的一个实现类。当我们使用泛型时,ArrayList<Integer>List<Integer>的一个实现,但ArrayList<Integer>List<Number>之间不存在继承关系,尽管IntegerNumber的子类。
    • 然而,我们可以使用通配符来处理这种情况。例如,如果我们有一个方法接受List<? extends Number>,那么它可以接受List<Integer>List<Double>等:
void processList(List<? extends Number> list) {
    for (Number number : list) {
        System.out.println(number);
    }
}

List<Integer> intList = new ArrayList<>();
intList.add(10);
processList(intList);

List<Double> doubleList = new ArrayList<>();
doubleList.add(10.5);
processList(doubleList);
  • 这里,? extends Number表示一个未知类型,但这个类型是Number的子类型,使得processList方法可以处理不同具体类型的List,只要这些类型是Number的子类型。

泛型与协变、逆变

  1. 协变
    • 协变是指当一个类型是另一个类型的子类型时,相应的泛型类型也具有子类型关系。在Java中,通过上界通配符(? extends)实现了有限的协变。
    • 例如,DogAnimal的子类,Box<? extends Dog>Box<? extends Animal>的子类型。这意味着我们可以将Box<Dog>赋值给Box<? extends Animal>类型的变量:
Box<Dog> dogBox = new Box<>();
Box<? extends Animal> animalBox = dogBox;
  • 协变允许我们在一定程度上以安全的方式处理具有继承关系的泛型类型,主要用于读取操作。
  1. 逆变
    • 逆变是指当一个类型是另一个类型的子类型时,相应的泛型类型具有相反的子类型关系。在Java中,通过下界通配符(? super)实现了有限的逆变。
    • 例如,Box<? super Dog>Box<? super Animal>的子类型,因为AnimalDog的超类型。这意味着我们可以将Box<Animal>赋值给Box<? super Dog>类型的变量:
Box<Animal> animalBox = new Box<>();
Box<? super Dog> dogSuperBox = animalBox;
  • 逆变主要用于写入操作,允许我们在安全的前提下将数据写入到具有继承关系的泛型类型中。

通过深入理解Java泛型与继承关系的各个方面,我们可以更有效地利用泛型的强大功能,编写出类型安全、可复用且高效的Java代码。无论是在集合框架的使用中,还是在自定义泛型类和方法的设计中,对这些概念的掌握都至关重要。在实际编程中,要根据具体的需求,合理运用泛型的特性,充分发挥Java语言在类型安全和代码复用方面的优势。同时,也要注意类型擦除等潜在问题,避免在运行时出现类型相关的错误。在涉及到复杂的继承体系和泛型组合时,要仔细分析和设计,确保代码的正确性和可读性。通过不断地实践和总结经验,我们能够更好地驾驭Java泛型与继承关系,提升自己的编程能力。