java泛型总结

1.概念

       泛型实现了参数化类型的概念,其最初的目的是希望类或方法能够具备最广泛的表达能力。通俗来说就是为了避免转换出错,限制数据类型。通过解耦类或者方法与所使用的类型之间的约束。

List theList1 = new ArrayList(Arrays.asList(1,2,3,"string"));
for(Object o : theList1){
	Integer i = (Integer)o;
}
List theList2 = new ArrayList(Arrays.asList(1,2,3,"string"));

theList1是原生态类型的List,theList2是参数化类型(泛型化)的List。theList1遍历到“string”的时候会出现ClassCaseException异常。theList2在编译时就会报错。

不严格的说,前者逃避了编译时检查,后者明确告诉编译器,它应该和能够持有的对象,在编译时检查。

 2.泛型的子类化

泛型有子类型化的规则,List是原生态类型List的一个子类型,而不是参数化类型List的子类型。

考虑如下代码:

                List a;
		List n = a;
		n.add(2.5f);
		Integer num = a.get(0); 

假设List是List的子类型,根据父类的引用可以指向子类的对象。让n指向a,给n添加一个Float类型的元素。接着a.get(0)的类型为Float,与最初定义的Lista相违背。所以泛型是不可变的,即,对于任意两个不同的类型Type1和Type2,List既不是List的子类型,也不是List的超类型。

泛型是通过擦除来实现的。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。

当原始类型一致,类型参数不一致时,其为同一个对象。

如下代码:

List type1 = new ArrayList();
List type2 = new ArrayList();
System.out.println(type1.getClass() == type2.getClass());

结果为true。

创建泛型数组是非法的,即new List()、newList[]和new E[]都是不允许的。因为它们违背了泛型系统提供的基本保证——类型安全。

有一种特殊情况,当一个方法接受泛型的可变类型。这时会出现警告,这是由于 每次调用可变参数方法时,就会创建一个数组来存放可变参数。如果这个数组的元素类型不是可具体会的(比如泛型),就会得到一条警告。除了用SupressWarnings(“unchecked”)消除这些警告,别无他法。

public void theMethod(@SuppressWarnings("unchecked") List ...a){
	List args = a[0];
	}

3.优先考虑泛型

考虑如下代码:

public class Stack {
	private Object[] elements;
	private List theOne = new ArrayList();
	private List theString;
	private int [] a = new int[1];
	private int size = 0;
	
	public Stack(){
		elements = new E(16);
	}
	public void push(E e)
	{
		ensureCapacity();
		elements[size++] = e;
	}
	
	public E pop(){
		if(!isEmpty()){
			E e = (E)elements[--size];
			return e;
		}
		return null;
	}
	
	private boolean isEmpty(){
		return size == 0;
	}
	
	private void ensureCapacity(){
		if(elements.length == size){
			elements = Arrays.copyOf(elements, 2 * size + 1);
		}
	}
}

会出现一条错误。不能创建不可具体化的类型的数据,如E,是什么类型,不知道。解决这个问题有两种方法。

第一种:创建一个Object的数组,并将它转换成泛型数组类型。

	public Stack(){
		elements = (E[])new Object[16];
	}

但会出现一条警告,说明类型是不安全的。你必须确保未受检的转换不会危及到程序的类型安全性。Elements保存在一个私有的域中,永远不会被返回到客户端,或者传给任何其他方法。这个数组中保存的唯一元素,是传给push方法的那些元素,而push方法只能接受E类型的参数,从而保证了未受检的转换不会有任何危害。

第二种:将elements域的类型从E[]改为Object[]。

private Object[] elements;
	public E pop(){
		if(!isEmpty()){
			@SuppressWarnings("unchecked")
			E e = (E)elements[--size];
			return e;
		}
		return null;
	}

有一个错误,需要强制类型转化为E,由push方法能保证elements里的元素类型为E,所以强制转换为E时不会出现ClassCaseException。

4.类型通配符

考虑如下代码:

class Box{
   private Integer data;
   public Box(){
			
 }
public Box(Integer data){
			setData(data);
		}
		public Integer getData() {
			return data;
		}
		public void setData(Integer data) {
			this.data = data;
		}
	}

我们可以很容易得到类型参数为Integer的Box对象,如果类型参数为String、Number呢,是不是重新定义Box类呢。只要使用类型通配符就可重用之前写好的代码。将Integer改为E。如下代码。

class Box{
		private E data;
		public Box(){
			
		}
		public Box(E data){
			setData(data);
		}
		public E getData() {
			return data;
		}
		public void setData(E data) {
			this.data = data;
		}
	}

当我们需要什么类型时,定义实参为所需类型即可。

5.有限通配符

表示E继承某种类型的一种类型

看如下代码:

	Stack numberStack = new Stack();
	Iterable integers = null;
	numberStack.pushAll(integers);

会报错,因为pushAll的输入参数类型不应该为“E的Iterable接口”,而应该为“E继承的Iterable接口”。所以应该把pushAll的方法参数改为

public void pushAll(Iterable src){
		for(E e : src)
			push(e);
	}

表示某种E的超类型

看代码:

Stack numberStack = new Stack();
	Collection objects = null;
	numberStack.popAll(objects); 
  

原因分析:Collection不是Collection的子类型。popAll的输入参数类型不应该为“E的集合”,而应该为“某种以E为超类的集合”。

修改popAll方法

public void popAll(Collection dst){
		while(!isEmpty){
			dst.add(pop());
		}
	}

上面的objects里存放的是Object类型,当传入到popAll时,dst接受的实参类型为Collection,即以Object为超类的集合。所以,dst当然可以add()Number类型的数据啦。

说明:

	public static void test(List list){
	    Object e = list.get(0); // get OK
	    list.set(0, e);         // set 编译报错
	}

set报错的原因是因为此时方法中的类型是不可具体化的(reified),你可以传递一个String,Number,Book,等任何继承自Object的类作为List的参数类型给test方法,而list要求集合中的类型必须是一致的,set的时候没有办法保证set进去的数据类型是否和list中原本的类型一致,比如你传给test方法的是List, 那么在方法中set进去一个Object显然类型就不一致了。这也是通配符带来灵活性的同时所要付出的代价。

结论:使用了 这样的通配符,test方法的参数list变成了只能get不能set(除了null) 或者不严谨的说它变成了只读参数了, 有些类似一个生产者,提供数据。

public static void test(List list){
	    Number n = list.get(0);             // 编译错误
	    Object o = list.get(0);             // OK
	    list.set(0, new Object());          // 编译错误
	    list.set(0, new Long(0));           // OK
	    list.set(0, new Integer(0));        // OK
	}

这时get只能get出最宽泛的父类型,即Object。

这时set的时候,必须是Number或Number的子类。

原因和上面的get类似。

结论: 使用了 这种通配符,test方法的参数list的get受到了很大的制约,只能最宽泛的方式来获取list中的数据,相当于get只提供了数据最小级别的访问权限(想想,你可能原本是放进去了一个Book,却只能当作Object来访问)。它更多适合于set的使用场景,像是一个消费者,主要用来消费数据。

PECS表示producer-extends,consumer-super。来总结什么时候用

在上诉的Stack示例中,pushAll的src参数产生E实例供Stack使用,因此src相应的类型为Iterable;popAll的dst参数通过Stack消费E实例,因此dst相应的类型为Collection

  如果你想从一个数据类型里获取数据,使用

  如果你想把对象写入一个数据结构里,使用< ? super T>

  如果你既想存,又想取,那就别用通配符。

你可能感兴趣的:(java泛型总结)