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

Java自定义泛型类的实现

2022-02-041.6k 阅读

一、泛型类的基本概念

在Java中,泛型(Generics)是一种强大的特性,它允许我们在定义类、接口和方法时使用类型参数。泛型类是一种参数化类型的类,它可以接受一个或多个类型参数,使得类的行为可以适应不同的数据类型,而无需为每种类型都编写重复的代码。

举个简单的例子,如果我们要实现一个可以存储任意类型数据的容器类。在没有泛型之前,我们可能会使用Object类型来实现,如下:

public class NonGenericContainer {
    private Object data;

    public NonGenericContainer(Object data) {
        this.data = data;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

使用这个类时,需要进行强制类型转换:

NonGenericContainer container = new NonGenericContainer("Hello");
String str = (String) container.getData();

这种方式存在两个问题:一是类型不安全,如果我们错误地将一个非String类型的数据存入容器,运行时才会抛出ClassCastException;二是代码不够简洁,每次获取数据都需要进行强制类型转换。

而使用泛型类,我们可以这样定义:

public class GenericContainer<T> {
    private T data;

    public GenericContainer(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

使用时:

GenericContainer<String> container = new GenericContainer<>("Hello");
String str = container.getData();

这里<T>中的T就是类型参数,它可以代表任何引用数据类型。在实例化GenericContainer时,指定TString类型,这样编译器就能确保容器中存储的数据类型是String,并且在获取数据时无需进行强制类型转换。

二、自定义泛型类的语法

2.1 定义泛型类

定义一个泛型类的基本语法如下:

class ClassName<T1, T2, ..., Tn> {
    // 类的成员变量、方法等
}

其中,<T1, T2, ..., Tn>是类型参数列表,T1T2等是类型参数的名称,通常使用单个大写字母表示,常见的有T(Type)、E(Element)、K(Key)、V(Value)等。这些类型参数在类中可以像普通类型一样使用,用于声明成员变量、方法参数和返回值等。

例如,定义一个简单的二元组泛型类:

public 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,分别用于表示键和值的类型。

2.2 实例化泛型类

实例化泛型类时,需要为类型参数指定具体的类型。例如:

Pair<String, Integer> pair = new Pair<>("score", 95);

在Java 7及以上版本,可以使用菱形语法<>,让编译器根据上下文推断出类型参数的具体类型,这样代码更加简洁。如果在Java 7之前,需要完整地写出类型参数:

Pair<String, Integer> pair = new Pair<String, Integer>("score", 95);

三、泛型类的类型擦除

3.1 什么是类型擦除

Java的泛型是在编译期实现的一种语法糖,其本质是类型擦除(Type Erasure)。在编译过程中,所有的泛型信息都会被擦除,只保留原始类型(Raw Type)。例如,对于GenericContainer<String>,编译后实际上存储的是GenericContainerString类型信息被擦除了。

编译器会在适当的地方插入类型转换代码,以保证类型安全。例如,对于以下代码:

GenericContainer<String> container = new GenericContainer<>("Hello");
String str = container.getData();

编译后的字节码大致相当于:

GenericContainer container = new GenericContainer("Hello");
String str = (String) container.getData();

3.2 类型擦除的规则

  1. 替换类型参数:编译器会用类型参数的限定类型(如果有)或Object类型替换类型参数。例如,对于class GenericClass<T extends Number>,在类型擦除后,T会被替换为Number;如果没有限定类型,如class GenericClass<T>T会被替换为Object
  2. 桥接方法:在泛型类继承或实现泛型接口时,为了保证多态性,编译器会生成桥接方法(Bridge Method)。例如,有如下代码:
class SubGenericClass extends GenericClass<String> {
    @Override
    public String getData() {
        return "subclass data";
    }
}

编译后,为了保证GenericClass的泛型方法在多态调用时的正确性,编译器会生成一个桥接方法:

class SubGenericClass extends GenericClass<String> {
    @Override
    public String getData() {
        return "subclass data";
    }

    // 桥接方法
    @Override
    public Object getData() {
        return getData();
    }
}

桥接方法的存在使得在运行时,基于泛型的多态调用能够正确执行。

3.3 类型擦除带来的限制

  1. 不能使用基本类型作为类型参数:由于类型擦除后,泛型类型会被替换为Object,而Object不能存储基本类型数据,所以不能使用基本类型如intdouble等作为泛型类型参数。例如,GenericContainer<int>是不允许的,需要使用对应的包装类型GenericContainer<Integer>
  2. 运行时无法获取泛型类型信息:因为类型擦除,在运行时无法获取泛型类型参数的具体类型。例如:
GenericContainer<String> container = new GenericContainer<>("Hello");
Class<?> clazz = container.getClass();
// 无法获取到具体的泛型类型String
  1. 静态成员不能使用泛型类的类型参数:静态成员属于类,而不是类的实例,在类加载时就已经确定,此时泛型类型参数还未确定,所以静态成员不能使用泛型类的类型参数。例如:
public class GenericClass<T> {
    // 错误,静态成员不能使用泛型类型参数T
    private static T staticField;

    public static void staticMethod(T param) {
        // 错误
    }
}

四、泛型类的继承与多态

4.1 泛型类的继承

泛型类可以继承其他泛型类或普通类。例如:

class ParentGenericClass<T> {
    private T data;

    public ParentGenericClass(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }
}

class ChildGenericClass<T> extends ParentGenericClass<T> {
    public ChildGenericClass(T data) {
        super(data);
    }
}

这里ChildGenericClass继承自ParentGenericClass,并且使用相同的类型参数T。在这种情况下,ChildGenericClass具有ParentGenericClass的所有特性,并且可以根据需要扩展功能。

也可以在继承时指定不同的类型参数:

class AnotherChildGenericClass extends ParentGenericClass<String> {
    public AnotherChildGenericClass(String data) {
        super(data);
    }
}

AnotherChildGenericClass继承自ParentGenericClass<String>,它固定了父类的类型参数为String

4.2 泛型类的多态

泛型类同样支持多态。例如:

ParentGenericClass<Integer> parent1 = new ParentGenericClass<>(10);
ChildGenericClass<Integer> child1 = new ChildGenericClass<>(20);

ParentGenericClass<Integer> parent2 = child1;

这里child1可以赋值给parent2,因为ChildGenericClassParentGenericClass的子类,这体现了泛型类的多态性。

但是,需要注意的是,泛型类型的协变和逆变问题。例如:

// 以下代码会编译错误
ParentGenericClass<Object> parentObj = new ParentGenericClass<String>("Hello");

虽然StringObject的子类,但ParentGenericClass<String>并不是ParentGenericClass<Object>的子类,这就是泛型类型不支持协变的体现。为了解决这个问题,Java引入了通配符(Wildcards)。

五、通配符在泛型类中的应用

5.1 上界通配符(? extends Type)

上界通配符? extends Type表示类型参数是TypeType的子类。例如:

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

public class GenericUtils {
    public static void printAnimals(GenericContainer<? extends Animal> container) {
        Animal animal = container.getData();
        System.out.println(animal);
    }
}

这里GenericUtils.printAnimals方法接受一个GenericContainer<? extends Animal>类型的参数,它可以是GenericContainer<Dog>GenericContainer<Cat>等,因为DogCat都是Animal的子类。

5.2 下界通配符(? super Type)

下界通配符? super Type表示类型参数是TypeType的父类。例如:

public class GenericUtils {
    public static void addDog(GenericContainer<? super Dog> container) {
        container.setData(new Dog());
    }
}

GenericUtils.addDog方法接受一个GenericContainer<? super Dog>类型的参数,它可以是GenericContainer<Dog>GenericContainer<Animal>甚至GenericContainer<Object>,因为DogDog本身、AnimalObject的子类,这样就可以安全地向容器中添加Dog对象。

5.3 无界通配符(?)

无界通配符?表示类型参数可以是任何类型。例如:

public class GenericUtils {
    public static void printAny(GenericContainer<?> container) {
        Object obj = container.getData();
        System.out.println(obj);
    }
}

GenericUtils.printAny方法接受一个GenericContainer<?>类型的参数,它可以是任何类型的GenericContainer。但是,使用无界通配符时,只能调用返回值类型为Object的方法,或者不依赖于类型参数的方法,因为无法确定具体的类型。

六、自定义泛型类的高级应用

6.1 泛型类与集合框架

Java集合框架广泛使用了泛型。例如,ArrayList就是一个泛型类:

ArrayList<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");

我们也可以自定义泛型类来与集合框架结合使用。比如,实现一个泛型的栈:

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

public class GenericStack<T> {
    private List<T> elements;

    public GenericStack() {
        elements = new ArrayList<>();
    }

    public void push(T element) {
        elements.add(element);
    }

    public T pop() {
        if (elements.isEmpty()) {
            throw new RuntimeException("Stack is empty");
        }
        return elements.remove(elements.size() - 1);
    }

    public boolean isEmpty() {
        return elements.isEmpty();
    }
}

这样就可以创建不同类型的栈:

GenericStack<Integer> intStack = new GenericStack<>();
intStack.push(10);
intStack.push(20);
intStack.pop();

6.2 泛型类与反射

虽然在运行时无法直接获取泛型类型信息,但通过反射可以在一定程度上实现与泛型相关的操作。例如,获取泛型类的类型参数:

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

class GenericClass<T> {}

public class ReflectionUtils {
    public static <T> Class<T> getGenericClassType(GenericClass<T> instance) {
        Type type = instance.getClass().getGenericSuperclass();
        if (type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) type;
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            if (actualTypeArguments.length > 0) {
                return (Class<T>) actualTypeArguments[0];
            }
        }
        return null;
    }
}

使用时:

GenericClass<String> genericInstance = newGenericClass<>();
Class<String> type = ReflectionUtils.getGenericClassType(genericInstance);

这种方式可以在运行时获取泛型类的部分类型信息,尽管有一定的局限性,但在某些场景下还是很有用的。

6.3 泛型类与函数式编程

在Java 8引入函数式编程特性后,泛型类也可以与函数式接口很好地结合。例如,Function接口就是一个泛型接口:

import java.util.function.Function;

public class GenericTransformer<T, R> {
    private Function<T, R> function;

    public GenericTransformer(Function<T, R> function) {
        this.function = function;
    }

    public R transform(T input) {
        return function.apply(input);
    }
}

可以这样使用:

Function<Integer, String> intToStringFunction = num -> String.valueOf(num);
GenericTransformer<Integer, String> transformer = new GenericTransformer<>(intToStringFunction);
String result = transformer.transform(10);

通过这种方式,可以利用泛型类和函数式接口实现更加灵活和可复用的功能。

七、自定义泛型类的最佳实践

  1. 合理选择类型参数名称:使用有意义的类型参数名称,如K表示键,V表示值,E表示元素等,这样可以提高代码的可读性。
  2. 考虑类型安全:在设计泛型类时,要充分利用编译器的类型检查机制,确保类型安全。避免在泛型类中进行不安全的类型转换。
  3. 遵循Liskov替换原则:在泛型类的继承和多态使用中,要遵循Liskov替换原则,确保子类可以安全地替换父类,不会破坏程序的正确性。
  4. 谨慎使用通配符:通配符可以增加代码的灵活性,但也可能降低类型安全性。在使用通配符时,要明确其作用和影响,确保代码的正确性。
  5. 结合其他Java特性:将泛型类与Java的其他特性,如集合框架、反射、函数式编程等结合使用,可以发挥更大的威力,提高代码的复用性和可维护性。

通过深入理解和掌握Java自定义泛型类的实现和应用,可以编写出更加通用、类型安全和高效的Java代码。在实际开发中,根据具体的需求和场景,灵活运用泛型类的各种特性,能够提升软件的质量和开发效率。