标签:
public class ListErr { public static void main(String[] args) { //创建一个只想保存字符串的List集合 List strList = new ArrayList(); strList.add("Hello World"); strList.add("Good Morning"); strList.add("你好"); //"不小心"把一个Integer对象"丢进"了集合 strList.add(5); for(int i = 0; i < strList.size(); i++){ //因为List里取出的全部是Object,所以必须强制类型转换 //最后一个元素将出现ClassCastException异常 // java.lang.ClassCastException:java.lang.Integer cannot be cast to java.lang.String String str = (String) strList.get(i); } } }
上面的程序创建了一个List集合,并且只是希望该List对象保存字符串对象,但是我们没有办法添加任何的限制,所以程序中“不小心”丢进了一个Integer对象,这将导致程序发生java.lang.ClassCastException异常。因为编译阶段正常,而运行时会出现“java.lang.ClassCastException”异常,因此导致此类错误编码过程中不易发现。通过上面程序,可以使我们了解到Java集合的两个问题:
public class GenericList { public static void main(String[] args) { //创建一个只想保存字符串的List集合 List<String> strList = new ArrayList<String>(); strList.add("Hello World"); strList.add("Good Morning"); strList.add("你好"); //下面代码将引起编译错误 strList.add(5);//01 for(int i = 0; i < strList.size(); i++){ //下面代码无须强制类型转换 String str = strList.get(i);//02 } } }上面程序将在01处引起编译错误,因为strList集合只能添加String对象,所以不能将Integer对象"丢进"该集合。而且程序在02处不需要进行强制类型转换,因为strList对象可以“记住”它的所有集合元素都是String类型。
public interface List<E> extends Collection<E> { int size(); boolean isEmpty(); boolean contains(Object o); Iterator<E> iterator(); Object[] toArray(); <T> T[] toArray(T[] a); boolean add(E e); boolean remove(Object o); boolean containsAll(Collection<?> c); boolean addAll(Collection<? extends E> c); boolean addAll(int index, Collection<? extends E> c); boolean removeAll(Collection<?> c); }可以看到,在List接口中采用泛型化定义之后,<E>中的E表示类型形参,可以接收具体的类型实参,并且此接口定义中,凡是出现E的地方均表示相同的接受自外部的类型实参。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; } public ArrayList() { super(); this.elementData = EMPTY_ELEMENTDATA; } public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); size = elementData.length; // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } //省略 }通过上面的介绍可以发现,我们可以为任何类增加泛型声明(并不是只有集合类才可以使用泛型声明,虽然泛型是集合类的重要使用场所),
public class Apple<T> { private T data; public Apple(T data){ this.data = data; } public void setData(T data){ this.data = data; } public T getData(){ return this.data; } public static void main(String[] args) { //因为传给T形参的是String类型,所以构造函数只接受String类型 Apple<String> a1 = new Apple<String>("苹果"); System.out.println(a1.getData()); //因为传给T形参的是Double实际类型,所以构造器的参数只能是Double或者double Apple<Double> a2 = new Apple<Double>(1.23); System.out.println(a2.getData()); } }上面程序定义了一个带泛型声明的Apple<T>类,实际使用Apple<T>类时会为T形参传入实际类型,这样就可以生成如Apple<String>、Apple<Double>等等形式的多个逻辑子类(物理上并不存在)。
//定义类继承Apple类,Apple类不能跟类型形参 public class A extends Apple<T>{}正确的写法应该是:
public class A extends Apple<String>{}如果从Apple<String>类派生子类,则在Apple类中所有使用T类型形参的地方都将被替换成String类型,即它的子类将会继承到String getData()和void setData(String data)两个方法,如果子类需要重写父类的方法,必须注意这一点。此处不再赘述。
//定义一个如此的方法 public void test(List<Object> ol){ for(int i = 0; i < ol.size(); i++){ System.out.println(ol.get(i)); } }表面上看起来,上面方法声明没有问题,这个方法声明确实没有任何问题。问题是调用该方法传入实际参数值时可能不是我们所期望的,如下调用此方法:
//创建一个List<String>对象 List<String> strList = new ArrayList<String>(); //将strList作为参数来调用前面的test方法 test(strList);//01编译上面程序,将在01处发生如下编译错误:
The method test(List<Object>) in the type Apple<T> is not applicable for the arguments (List<String>)显然得到List<Object>不是List<String>的父类,那么它们的父类是谁呢?为了表示各种泛型的父类,我们需要使用类型通配符,类型通配符是一个问号(?),将问号作为类型实参传给泛型类,例如List<?>。这个问号(?)被称为通配符,它的元素类型可以匹配任何类型。因此可以把List<?>在逻辑上认为是List<Object>、List<String>等所有List<具体类型实参>的父类(类型通配符一般是使用?代替具体的类型实参,注意是类型实参而不是类型形参)。
List<?> c = new ArrayList<String>(); //下面程序引起编译时错误 c.add(new Object());因为我们不知道上面程序中c的集合里元素的类型,所以不能向其中添加对象。根据前面的List<E>接口定义的代码可以发现:add方法有类型参数E作为集合的元素类型,所以我们传给add的参数必须是E类的对象或者其子类的对象。但因为在该例中不知道E是什么类型,所以程序无法将任何对象“丢进”该集合。唯一的例外是null,它是所有引用类型的实例。如何解决这个问题呢,下面介绍的类型通配符的上限和下限将解决这个问题。
public void test(List<?> c){ for(int i = 0; i < c.size(); i++){ System.out.println(c.get(i)); } }现在我们对类型实参进一步限制:只能是Number类及其子类。此时,需要用到类型通配符上限,将上面的代码修改为如下:
public void test(List<? extends Number> c){ for(int i = 0; i < c.size(); i++){ System.out.println(c.get(i)); } }同理,如果只能是Integer类本身及其父类呢?此时,需要用到类型通配符下限,示例代码为如下:
public void test(List<? super Integer> c){ for(int i = 0; i < c.size(); i++){ System.out.println(c.get(i)); } }同样可以使用类型通配符上下限设置类型形参。
public class Apple<T extends Number> { private T data; public Apple(T data){ this.data = data; } public void setData(T data){ this.data = data; } public T getData(){ return this.data; } }
public class TestErasure{ public static void main(String[] args) { Apple<Integer> a = new Apple<Integer>(6); //01 //a的getSize方法返回Integer对象 Integer as = a.getData(); //把a对象赋给Apple变量,会丢失尖括号里的类型信息 Apple b = a; //02 //b只知道sizede类型是Number Number size1 = b.getData(); //下面代码引起编译错误 Integer size2 = b.getData(); //03 } }从上面程序可知,当把a赋给一个不带泛型信息的b变量时,编译器就会丢失a对象的泛型信息,即使所有尖括号里的信息都被丢失。但因为Apple的类型形参的上限是Number类,所有编译器依然知道b的getSize方法返回Number类型,但具体是Number的哪个子类就不清楚了。这就是类型的擦除。
标签:
原文地址:http://blog.csdn.net/jzpkzm/article/details/51718407