Java 泛型总结(二):泛型与数组
简介
上一篇文章介绍了的问题,现在来看看泛型和数组的关系。数组相比于java 类库中的容器类是比较特殊的,主要体现在三个方面:
- 数组创建后大小便固定,但效率更高
- 数组能追踪它内部保存的元素的具体类型,插入的元素类型会在编译期得到检查
- 数组可以持有原始类型 ( int,float等 ),不过有了自动装箱,容器类看上去也能持有原始类型了
那么当数组遇到泛型会怎样? 能否创建泛型数组呢?这是这篇文章的主要内容。
这个系列的另外两篇文章:
泛型数组
如何创建泛型数组
如果有一个类如下:
class generic<t> { }
如果要创建一个泛型数组,应该是这样: generic<integer> ga = new generic<integer>[]
不过行代码会报错,也就是说不能直接创建泛型数组。
那么如果要使用泛型数组怎么办?一种方案是使用 arraylist
,比如下面的例子:
public class listofgenerics<t> { private list<t> array = new arraylist<t>(); public void add(t item) { array.add(item); } public t get(int index) { return array.get(index); } }
如何创建真正的泛型数组呢?我们不能直接创建,但可以定义泛型数组的引用。比如:
public class arrayofgenericreference { static generic<integer>[] gia; }
gia
是一个指向泛型数组的引用,这段代码可以通过编译。但是,我们并不能创建这个确切类型的数组,也就是不能使用 new generic<integer>[]
具体参见下面的例子:
public class arrayofgeneric { static final int size = 100; static generic<integer>[] gia; @suppresswarnings("unchecked") public static void main(string[] args) { // compiles; produces classcastexception: //! gia = (generic<integer>[])new object[size]; // runtime type is the raw (erased) type: gia = (generic<integer>[])new generic[size]; system.out.println(gia.getclass().getsimplename()); gia[0] = new generic<integer>(); //! gia[1] = new object(); // compile-time error // discovers type mismatch at compile time: //! gia[2] = new generic<double>(); generic<integer> g = gia[0]; } } /*输出: generic[] *///:~
数组能追踪元素的实际类型,这个类型是在数组创建的时候建立的。上面被注释掉的一行代码: gia = (generic<integer>[])new object[size]
,数组在创建的时候是一个 object 数组,如果转型便会报错。成功创建泛型数组的唯一方式是创建一个类型擦除的数组,然后转型,如代码: gia = (generic<integer>[])new generic[size]
,gia 的 class 对象输出的名字是 generic[]。
我个人的理解是:由于类型擦除,所以 generic<integer> 相当于初始类型 generic,那么 gia = (generic<integer>[])new generic[size]
中的转型其实还是转型为 generic[],看上去像没转,但是多了编译器对参数的检查和自动转型,向数组插入 new object()
和 new generic<double>()
均会报错,而 gia[0] 取出给 generic<integer>
也不需要我们手动转型。
使用 t[] array
上面的例子中,元素的类型是泛型类。下面看一个元素本身类型是泛型参数的例子:
public class genericarray<t> { 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<integer> gai = new genericarray<integer>(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[]
,看上去就是没转,但是多了编译器的参数检查和自动转型。而如果把泛型参数改成 <t extends integer>
,那么因为类型是擦除到第一个边界,所以 array = (t[])new object[sz]
中相当于转型为 integer[]
,这应该会报错。下面是实验的代码:
public class genericarray<t extends integer> { 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<integer> gai = new genericarray<integer>(10); // this causes a classcastexception: //! integer[] ia = gai.rep(); // this is ok: object[] oa = gai.rep(); } }
相比于原始的版本,上面的代码只修改了第一行,把 <t>
改成了 <t extends integer>
那么不用调用 rep(),在创建泛型数组的时候就会报错。下面是运行结果:
exception in thread "main" java.lang.classcastexception: [ljava.lang.object; cannot be cast to [ljava.lang.integer; at genericarray.<init>(genericarray.java:15)
使用 object[] array
由于擦除,运行期的数组类型只能是 object[],如果我们立即把它转型为 t[],那么在编译期就失去了数组的实际类型,编译器也许无法发现潜在的错误。因此,更好的办法是在内部最好使用 object[] 数组,在取出元素的时候再转型。看下面的例子:
public class genericarray2<t> { 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<integer> gai = new genericarray2<integer>(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<t> { private t[] array; @suppresswarnings("unchecked") public genericarraywithtypetoken(class<t> 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<integer> gai = new genericarraywithtypetoken<integer>( integer.class, 10); // this now works: integer[] ia = gai.rep(); } }
在构造器中传入了 class<t>
对象,通过 array.newinstance(type, sz)
创建一个数组,这个方法会用参数中的 class 对象作为数组元素的组件类型。这样创建出的数组的元素类型便不再是 object,而是 t。这个方法返回 object 对象,需要把它转型为数组。不过其他操作都不需要转型了,包括 rep() 方法,因为数组的实际类型与 t[] 是一致的。这是比较推荐的创建泛型数组的方法。
总结
数组与泛型的关系还是有点复杂的,java 中不允许直接创建泛型数组。本文分析了其中原因并且总结了一些创建泛型数组的方式。其中有部分个人的理解,如果错误希望大家指正。下一篇会总结。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持!
推荐阅读