您的位置:首页 > 编程语言 > Java开发

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

2017-09-21 17:40 441 查看
(转载)https://segmentfault.com/a/1190000005179147


简介

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

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

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

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

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

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

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

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


泛型数组


如何创建泛型数组

如果有一个类如下:
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 中不允许直接创建泛型数组。本文分析了其中原因并且总结了一些创建泛型数组的方式。其中有部分个人的理解,如果错误希望大家指正。下一篇会总结通配符的使用。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: