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

 

 简介:

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

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

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

     3.数组可以持有原始类型 ( 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[]

     Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
	at com.internet.vemall.GenericArray.main(GenericArray.java:27)

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

   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);
		}
	}
}
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 中不允许直接创建泛型数组。本文分析了其中原因并且总结了一些创建泛型数组的方式。其中有部分个人的理解,如果错误希望大家指正。下一篇会总结通配符的使用。

        原文链接:https://segmentfault.com/a/1190000005179147

发表评论