资讯专栏INFORMATION COLUMN

Java 泛型总结(二):泛型与数组

Vultr / 2064人阅读

摘要:总结数组与泛型的关系还是有点复杂的,中不允许直接创建泛型数组。本文分析了其中原因并且总结了一些创建泛型数组的方式。

简介

上一篇文章介绍了泛型的基本用法以及类型擦除的问题,现在来看看泛型和数组的关系。数组相比于Java 类库中的容器类是比较特殊的,主要体现在三个方面:

数组创建后大小便固定,但效率更高

数组能追踪它内部保存的元素的具体类型,插入的元素类型会在编译期得到检查

数组可以持有原始类型 ( int,float等 ),不过有了自动装箱,容器类看上去也能持有原始类型了

那么当数组遇到泛型会怎样? 能否创建泛型数组呢?这是这篇文章的主要内容。

这个系列的另外两篇文章:

Java 泛型总结(一):基本用法与类型擦除

Java 泛型总结(三):通配符的使用

泛型数组 如何创建泛型数组

如果有一个类如下:

 class Generic {
    
}

如果要创建一个泛型数组,应该是这样: Generic ga = new Generic[]。不过行代码会报错,也就是说不能直接创建泛型数组。

那么如果要使用泛型数组怎么办?一种方案是使用 ArrayList,比如下面的例子:

public class ListOfGenerics {
    private List array = new ArrayList();
    public void add(T item) { array.add(item); }
    public T get(int index) { return array.get(index); }
}

如何创建真正的泛型数组呢?我们不能直接创建,但可以定义泛型数组的引用。比如:

public class ArrayOfGenericReference {
    static Generic[] gia;
}

gia 是一个指向泛型数组的引用,这段代码可以通过编译。但是,我们并不能创建这个确切类型的数组,也就是不能使用 new Generic[]。具体参见下面的例子:

public class ArrayOfGeneric {
    static final int SIZE = 100;
    static Generic[] gia;
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        // Compiles; produces ClassCastException:
        //! gia = (Generic[])new Object[SIZE];
        // Runtime type is the raw (erased) type:
        gia = (Generic[])new Generic[SIZE];
        System.out.println(gia.getClass().getSimpleName());
        gia[0] = new Generic();
        //! gia[1] = new Object(); // Compile-time error
        // Discovers type mismatch at compile time:
        //! gia[2] = new Generic();
        Generic g = gia[0];
    }
} /*输出:
Generic[]
*///:~

数组能追踪元素的实际类型,这个类型是在数组创建的时候建立的。上面被注释掉的一行代码: gia = (Generic[])new Object[SIZE],数组在创建的时候是一个 Object 数组,如果转型便会报错。成功创建泛型数组的唯一方式是创建一个类型擦除的数组,然后转型,如代码: gia = (Generic[])new Generic[SIZE]giaClass 对象输出的名字是 Generic[]

我个人的理解是:由于类型擦除,所以 Generic 相当于初始类型 Generic,那么 gia = (Generic[])new Generic[SIZE] 中的转型其实还是转型为 Generic[],看上去像没转,但是多了编译器对参数的检查和自动转型,向数组插入 new Object()new Generic() 均会报错,而 gia[0] 取出给 Generic 也不需要我们手动转型。

使用 T[] array

上面的例子中,元素的类型是泛型类。下面看一个元素本身类型是泛型参数的例子:

public class GenericArray {
    private T[] array;
    @SuppressWarnings("unchecked")
    public GenericArray(int sz) {
        array = (T[])new Object[sz];   // 创建泛型数组
    }
    public void put(int index, T item) {
        array[index] = item;
    }
    public T get(int index) { return array[index]; }
    // Method that exposes the underlying representation:
    public T[] rep() { return array; }     //返回数组 会报错
    public static void main(String[] args) {
        GenericArray gai =
        new GenericArray(10);
        // This causes a ClassCastException:
        //! Integer[] ia = gai.rep();
        // This is OK:
        Object[] oa = gai.rep();
    }
}

在上面的代码中,泛型数组的创建是创建一个 Object 数组,然后转型为 T[]。但数组实际的类型还是 Object[]。在调用 rep()方法的时候,就报 ClassCastException 异常了,因为 Object[] 无法转型为 Integer[]

那创建泛型数组的代码 array = (T[])new Object[sz] 为什么不会报错呢?我的理解和前面介绍的类似,由于类型擦除,相当于转型为 Object[],看上去就是没转,但是多了编译器的参数检查和自动转型。而如果把泛型参数改成 ,那么因为类型是擦除到第一个边界,所以 array = (T[])new Object[sz] 中相当于转型为 Integer[],这应该会报错。下面是实验的代码:

public class GenericArray {
    private T[] array;
    @SuppressWarnings("unchecked")
    public GenericArray(int sz) {
        array = (T[])new Object[sz];   // 创建泛型数组
    }
    public void put(int index, T item) {
        array[index] = item;
    }
    public T get(int index) { return array[index]; }
    // Method that exposes the underlying representation:
    public T[] rep() { return array; }     //返回数组 会报错
    public static void main(String[] args) {
        GenericArray gai =
        new GenericArray(10);
        // This causes a ClassCastException:
        //! Integer[] ia = gai.rep();
        // This is OK:
        Object[] oa = gai.rep();
    }
}

相比于原始的版本,上面的代码只修改了第一行,把 改成了 ,那么不用调用 rep(),在创建泛型数组的时候就会报错。下面是运行结果:

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
at GenericArray.(GenericArray.java:15)
使用 Object[] array

由于擦除,运行期的数组类型只能是 Object[],如果我们立即把它转型为 T[],那么在编译期就失去了数组的实际类型,编译器也许无法发现潜在的错误。因此,更好的办法是在内部最好使用 Object[] 数组,在取出元素的时候再转型。看下面的例子:

public class GenericArray2 {
    private Object[] array;
    public GenericArray2(int sz) {
        array = new Object[sz];
    }
    public void put(int index, T item) {
        array[index] = item;
    }
    @SuppressWarnings("unchecked")
    public T get(int index) { return (T)array[index]; }
    @SuppressWarnings("unchecked")
    public T[] rep() {
        return (T[])array; // Warning: unchecked cast
    }
    public static void main(String[] args) {
        GenericArray2 gai =
        new GenericArray2(10);
        for(int i = 0; i < 10; i ++)
        gai.put(i, i);
        for(int i = 0; i < 10; i ++)
        System.out.print(gai.get(i) + " ");
        System.out.println();
        try {
            Integer[] ia = gai.rep();
        } catch(Exception e) { System.out.println(e); }
    }
} /* Output: (Sample)
0 1 2 3 4 5 6 7 8 9
java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
*///:~

现在内部数组的呈现不是 T[] 而是 Object[],当 get() 被调用的时候数组的元素被转型为 T,这正是元素的实际类型。不过调用 rep() 还是会报错, 因为数组的实际类型依然是Object[],终究不能转换为其它类型。使用 Object[] 代替 T[] 的好处是让我们不会忘记数组运行期的实际类型,以至于不小心引入错误。

使用类型标识

其实使用 Class 对象作为类型标识是更好的设计:

public class GenericArrayWithTypeToken {
    private T[] array;
    @SuppressWarnings("unchecked")
    public GenericArrayWithTypeToken(Class type, int sz) {
        array = (T[])Array.newInstance(type, sz);
    }
    public void put(int index, T item) {
        array[index] = item;
    }
    public T get(int index) { return array[index]; }
    // Expose the underlying representation:
    public T[] rep() { return array; }
    public static void main(String[] args) {
        GenericArrayWithTypeToken gai =
        new GenericArrayWithTypeToken(
        Integer.class, 10);
        // This now works:
        Integer[] ia = gai.rep();
    }
}

在构造器中传入了 Class 对象,通过 Array.newInstance(type, sz) 创建一个数组,这个方法会用参数中的 Class 对象作为数组元素的组件类型。这样创建出的数组的元素类型便不再是 Object,而是 T。这个方法返回 Object 对象,需要把它转型为数组。不过其他操作都不需要转型了,包括 rep() 方法,因为数组的实际类型与 T[] 是一致的。这是比较推荐的创建泛型数组的方法。

总结

数组与泛型的关系还是有点复杂的,Java 中不允许直接创建泛型数组。本文分析了其中原因并且总结了一些创建泛型数组的方式。其中有部分个人的理解,如果错误希望大家指正。下一篇会总结通配符的使用,有兴趣的读者可进入下一篇:Java 泛型总结(三):通配符的使用。

参考

Java 编程思想

如果我的文章对您有帮助,不妨点个赞支持一下(^_^)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/65967.html

相关文章

  • Java 泛型总结(一):基本用法与类型擦除

    摘要:然而中的泛型使用了类型擦除,所以只是伪泛型。总结本文介绍了泛型的使用,以及类型擦除相关的问题。一般情况下泛型的使用比较简单,但是某些情况下,尤其是自己编写使用泛型的类或者方法时要注意类型擦除的问题。 简介 Java 在 1.5 引入了泛型机制,泛型本质是参数化类型,也就是说变量的类型是一个参数,在使用时再指定为具体类型。泛型可以用于类、接口、方法,通过使用泛型可以使代码更简单、安全。然...

    Java_oldboy 评论0 收藏0
  • Java 泛型总结(三):通配符的使用

    简介 前两篇文章介绍了泛型的基本用法、类型擦除以及泛型数组。在泛型的使用中,还有个重要的东西叫通配符,本文介绍通配符的使用。 这个系列的另外两篇文章: Java 泛型总结(一):基本用法与类型擦除 Java 泛型总结(二):泛型与数组 数组的协变 在了解通配符之前,先来了解一下数组。Java 中的数组是协变的,什么意思?看下面的例子: class Fruit {} class Apple ex...

    itvincent 评论0 收藏0
  • JAVA语法糖和语法糖编译

    摘要:提供给了用户大量的语法糖,比如泛型自动装箱拆箱循环变长参数内部类枚举类断言新特性方法引用等解语法糖语法糖的存在主要是方便开发人员使用。 首先,部分总结文字引用 简书作者:Eric新之助 。链接:https://www.jianshu.com/p/4de08deb6ba4 已获得授权 showImg(https://segmentfault.com/img/bVbfuX9?w=646&...

    weakish 评论0 收藏0
  • Java™ 教程(类型推断)

    类型推断 类型推断是Java编译器查看每个方法调用和相应声明的能力,以确定使调用适用的类型参数,推理算法确定参数的类型,如果可用,还确定分配或返回结果的类型,最后,推理算法尝试查找适用于所有参数的最具体类型。 为了说明最后一点,在下面的示例中,推断确定传递给pick方法的第二个参数是Serializable类型: static T pick(T a1, T a2) { return a2; } ...

    JerryC 评论0 收藏0
  • Java 虚拟机总结给面试的你(中)

    摘要:验证过程验证过程的目的是为了确保文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。二虚拟机字节码执行引擎虚拟机的执行引擎自行实现,可以自行制定指令集与执行引擎的结构体系。 本篇博客主要针对Java虚拟机的类加载机制,虚拟机字节码执行引擎,早期编译优化进行总结,其余部分总结请点击Java虚拟总结上篇 。 一.虚拟机类加载机制 概述 虚拟机把描述类的数据从Clas...

    MRZYD 评论0 收藏0

发表评论

0条评论

Vultr

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<