当数组遇到泛型

引言

数组是一种常见的数据结构,可以把逻辑上连续的数据,在物理上也连续地存储,而且数组存储的数据,是指定类型的。那么我们该如何用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。阅读上面代码,请读者思考下面的问题:

  1. get方法中有强制类型转换的写法,能否可以将Object[]改为T[],从而在方法中不用写强转?
  2. 编译时泛型会被擦除,为什么获取数据时我们没有感觉?

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的精简版。并分析了下面两个问题:

  1. get方法中有强制类型转换的写法,能否可以将Object[]改为T[],从而在方法中不用写强转?

    写法上可以将Object[]改为T[],实际上编译后仍然是Object[]。

  2. 编译时泛型会被擦除,为什么获取数据时我们没有感觉?

    赋值时会根据需要自动强转,不需要手动编码去强转。


欢迎关注公众号,获取推送更方便,遇到问题来交流!

技术长跑

猜你喜欢

转载自blog.csdn.net/CanvaChen/article/details/104831379