浅析Java泛型数组的实现

最近在学习CS61B的时候遇到一个问题,使用数组来实现双向链表的时候要求用泛型数组.

在Java中,如果向创建一个泛型数组首先想到的写法可能是:

T array = new T[1];

但是这时会发现IDEA会报错类型形参 'T' 不能直接实例化,这是由于Java泛型的类型擦除机制导致的.

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

而类型擦除机制是指在Java编译器编译时,会将泛型类型参数如<T>替换为其上界类型或Object类型,从而安全的进行编译,这种替换会将定义的泛型类型擦除,以保证泛型的通用性和兼容性,如:

public class Generics {
    //声明变量
    private T v;
    
    //使用泛型作为传入参数类型的方法
    public void setValue(T value) {
        this.v = value;
    }
    
    //使用泛型作为返回值类型的方法
    public T getValue() {
        return v;
    }
}

编译时会将其中所有的泛型类型T替换为Object

public class Generics {
    private Object v;
    
    public void setValue(Object value) {
        this.v = value;
    }
    
    public Object getValue() {
        return v;
    }
}

但是在运行过程中,仍可以通过类型的强制转换来使用正确的类型进行调用:

//实例化Generics对象
Generics generic = new Generics<>();
//方法调用
generic.setValue("This is a Generic");
//数据类型强制转换
String v = (String) generic.getValue();
System.out.println(v);

上界与下界

上界(Upper Bounds)与下界(Lower Bounds)是用于限制泛型参数的类型的规定

在表明泛型类型的上界时,可以使用extends关键字来进行限定:

public class Generics {...}

这里限定了泛型类型T的上界为Number,也就是只能将Number及其子类作为泛型类型的实际参数传递给Generics,如Integer Double这些类型,这是在运行时如果向Generics传递了一个非Number及其子类的类型的参数, 如尝试传入一个String类型的参数:

Generics generic = new Generics<>();

这时候IDEA会报错类型形参 'java.lang.String' 不在其界限内

extends后接的上界可以是接口,也可以使用分隔符&进行多重限定:

public class Generics> {...}

这意味着定义的泛型类型T必须同时满足类型Number和接口Comparable<T>这两个条件,如果超出上界则会报错.

下界和上界有相近的功能,但是规定二者的关键字并不相同,在规定泛型类型的下界时应使用super关键字来定义.

与上界不同的是,它规定泛型类型必须为下界类型及其父类,如:

public class Generics {...}

类似的,这里限定了泛型类型T的下界为Integer,这意味着只能将Integer Number Object这些类型传递给Generics使用,如果传递了在此范围之外的类型则会报错.

另一方面,泛型不允许对下界进行多重限定,只能够规定单一下界.

泛型数组

由于类型擦除这一机制的存在,导致数组在编译前后的类型发生了改变,而数组是静态的数据结构,其类型一旦在声明时确定后便无法更改.

不过,可以用一个取巧的方式,那就是创建一个Object类型的数组,在将其转换为泛型数组来实现,由于Object是所有类型的父类,因此这样创建的数组能够接收任何类型的实参.

public class GenericArray {
    
    private Object[] array;
    
    public GenericArray(int size) {
        array = new Object[size];
    }
    
    public static void main(String[] args) {
        GenericArray SArray = new GenericArray<>(5);
    }
}

此时如果要强制转换会发现IDEA会警告未检查的转换: 'java.lang.Object[]' 转换为 'T'

这是因为在实例化时限定了参数类型为String,但实际上可以添加任意类型的值,如果这时向该数组中添加一个Integer类型的值,程序就会抛出ClassCastException异常,因此要确保添加值的类型和实例化时规定的类型一致.如果看这个警告很不爽也可以加上@SuppressWarnings("unchecked")注解来忽略它.

完成这些步骤以后会发现此时的泛型数组是无法使用Array的get set等方法的,这是由于数组的类型检查是在编译前完成,而编译时会擦除泛型类型,这是编译器并不知道泛型数组是什么,因此也就无法使用Array类中提供的方法.

由于这种方法得到的泛型数组是由Object类型的数组强制转换而来,因此可以对Object数组调用Array类的方法再转换为泛型类型,例如:

public class GenericArray {
    
    private Object[] array;
    
    public GenericArray(int size) {
        array = new Object[size];
    }
    
    @SuppressWarnings("unchecked")
    public void get(int index) {
        return (T) array[index];
    }
    
    public void set(int index, T value) {
        array[index] = value;
    }
    
    public static void main(String[] args) {
        GenericArray SArray = new GenericArray<>(5);
    }
}

可以发现,在这里我们操作的对象仍然是Object数组,只是在传递值的时候强制转换为了泛型类型,这样操作后操作数组的数据类型都是定义的泛型类型T

当然,也可以使用ArrayList来创建泛型数组,ArrayList原生支持泛型,这样用功能和性能应该都要更胜一筹,不过CS61B的这次Project不允许使用Util包中的内容,这里也就不多研究了.似乎也可以使用反射reflect来实现泛型数组,不过简单看了看似乎比较复杂,先摆了,等要用的时候在研究🤣


参考文章:

https://docs.oracle.com/javase/tutorial/extra/generics/fineprint.html

https://www.runoob.com/java/java-generics.html

https://www.cnblogs.com/minghaiJ/p/11259318.html

文章链接:https://blog.syrizelink.top/index.php/2023/02/239/
🔔本博客文章仅用作个人学习/知识分享使用,不保证其正确性以及时效性
✏️部分素材来源于网络,如有侵权请联系我删除
🌏未经作者同意时,如要转载请务必标明出处
上一篇
下一篇