引言
数组是一种常见的数据结构,可以把逻辑上连续的数据,在物理上也连续地存储,而且数组存储的数据,是指定类型的。那么我们该如何用Java写一个通用的数组呢?
正文
对于如何写一个通用的数组,我们很容易想到用Object[],这样我们想放字符串时可以用它,想放数值时也可以用它。但是如果我们字符串和数值都放进去,很难保证运行时不会出错。所以我们需要加入一些约束,增强代码的安全性。Java中的泛型可以让我们写出多类型通用的代码,并且编译器能帮我们检查类型使用是否得当,可以满足我们的需求。
于是我们就通过对Object[]进行封装,并结合泛型来实现。考虑到我们并不知道数组中实际存储了多少个有效的数据,所以再加个size字段,用来记录数组实际元素的个数。
public class ObjectArray<T> {
private Object[] data;
private int size;
public ObjectArray(int capacity) {
this.data = new Object[capacity];
this.size = 0;
}
public T get(int index) {
checkIndex(size);
return (T) data[index];
}
public void add(T e) {
checkIndex(size);
data[size++] = e;
}
private void checkIndex(int index) {
if (index < 0 || index >= data.length) {
throw new IllegalArgumentException("invalid index");
}
}
}
上面实现了对数组的封装,并加入了获取数组元素和添加数组元素两个功能。通过参数化类型T,保证了我们加入数组的类型是T,获取到的类型也是T。阅读上面代码,请读者思考下面的问题:
- get方法中有强制类型转换的写法,能否可以将Object[]改为T[],从而在方法中不用写强转?
- 编译时泛型会被擦除,为什么获取数据时我们没有感觉?
data = (T[]) new Object[capacity]
对于第1个问题,我们可以将Object[]改为T[],这样get方法里没了强转的写法,但构造方法里创建数组需要强转。
public class TArray<T> {
private T[] data;
private int size;
public TArray(int capacity) {
this.data = (T[]) new Object[capacity];
this.size = 0;
}
public T get(int index) {
checkIndex(size);
return data[index];
}
public void add(T e) {
checkIndex(size);
data[size++] = e;
}
private void checkIndex(int index) {
if (index < 0 || index >= data.length) {
throw new IllegalArgumentException("invalid index");
}
}
}
阅读上面代码,细心的你可能会疑惑Object[]为什么可以强转为T[],而如果我们将Object[]强转为Integer[],则会抛出异常呢?
java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
checkcast
要搞明白这个问题,我们得看编译后的字节码。
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iload_1
6: anewarray #2 // class java/lang/Object
9: checkcast #3 // class "[Ljava/lang/Object;"
12: putfield #4 // Field data:[Ljava/lang/Object;
15: aload_0
16: iconst_0
17: putfield #5 // Field size:I
20: return
Code第9行checkcast是校验强制类型转换,从备注中可以看出强转为Object[]。所以以下两句编译完是等效的:
this.data = (T[]) new Object[capacity];
this.data = (Object[]) new Object[capacity];
实际上data编译后也仍然是Object[]类型,从而强转不会有ClassCastException的异常。
接下来第2个问题,我们看get方法的字节码。
descriptor: (I)Ljava/lang/Object;
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_0
2: getfield #5 // Field size:I
5: invokespecial #6 // Method checkIndex:(I)V
8: aload_0
9: getfield #4 // Field data:[Ljava/lang/Object;
12: iload_1
13: aaload
14: areturn
从descriptor可以看出,返回值是Object类型。那为什么拿到返回值后,我们不需要自己再强转一次?我们写个例子试下。
public static void main(String[] args) {
TArray<Integer> array = new TArray<>(10);
array.add(1);
array.get(0);
Integer i = array.get(0);
Number n = array.get(0);
}
注意上面代码省略了类,实际运行要加上。下面再看字节码。
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
0: new #10 // class com/canva/demo/java/java8/test/TArray
3: dup
4: bipush 10
6: invokespecial #11 // Method "<init>":(I)V
9: astore_1
10: aload_1
11: iconst_1
12: invokestatic #12 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
15: invokevirtual #13 // Method add:(Ljava/lang/Object;)V
18: aload_1
19: iconst_0
20: invokevirtual #14 // Method get:(I)Ljava/lang/Object;
23: pop
24: aload_1
25: iconst_0
26: invokevirtual #14 // Method get:(I)Ljava/lang/Object;
29: checkcast #15 // class java/lang/Integer
32: astore_2
33: aload_1
34: iconst_0
35: invokevirtual #14 // Method get:(I)Ljava/lang/Object;
38: checkcast #16 // class java/lang/Number
41: astore_3
42: return
从上面可以看出:当运行array.get(0)时,并没有校验类型转换;当运行Integer i = array.get(0)时,校验转换Integer类型;当运行Number n = array.get(0)时,校验转换Number类型。也就是说,强转类型取决于我们需要的是什么类型,如果不需要引用,并不会马上强转。
总结
本文对数组进行简单的封装,并结合泛型,做成兼顾通用性和安全性的数组,相当于List的精简版。并分析了下面两个问题:
-
get方法中有强制类型转换的写法,能否可以将Object[]改为T[],从而在方法中不用写强转?
写法上可以将Object[]改为T[],实际上编译后仍然是Object[]。
-
编译时泛型会被擦除,为什么获取数据时我们没有感觉?
赋值时会根据需要自动强转,不需要手动编码去强转。