最近在学习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