##### 为什么需要泛型?或者说泛型是用来解决什么问题的?
* 单一类型容器需求 && 编译期安全检查 && 改善代码冗余
(1)拿容器来说,实际中的需求,大部分容纳的都是同一个类型的,而不是通用类型的。如果需要通用类型的,那就直接使用List<Object>就可以了。
(2)既然需要单一类型容器,就需要编译器检查,否则放入了不合适的类型,只能在运行期出错了
(3)既然是单一类型容器,就希望取出来的时候不用再进行显性的类型转换了
看看实际的例子,体现了上面的3点:
List myIntList=new LinkedList(); //1 myIntList.add(newInteger(0)); //2 Integer x=(Integer)myIntList.iterator().next(); //3
List<Integer> myIntList=newLinkedList<Integer>(); //1’
myIntList.add(newInteger(0)); //2’
Integerx=myIntList.iterator().next(); //3’
在第1行代码中指定List中存储的对象类型为Integer,这样在获取列表中的对象时,不必强制转换类型了。
* 通用程序需求(类似c++模版一样)
设计底层结构或者通用容器的时候,需要一定的通用性,类似c++模版一样。
否则就得用抽象接口,但实际中怎么能麻烦的去要求不可控的实际情况都实现某些接口呢?
看下面的代码,不可能期望容器接纳的类都去实现某个接口。(继承就更是不能奢望了)
class Box<T> { private T data; public Box() { } public Box(T data) { this.data = data; } public T getData() { return data; } public void setData(T data) { this.data = data; } }
明白设计的目标,是理解的重要部分,否则后面会出现很多疑问。
##### Java设计泛型来达到上面的目标,但又有哪些限制和需要注意的地方呢?
//基类 class Plant {} class Fruit extends Plant{} class Apple extends Fruit{} class RedApple extends Apple{} class Orange extends Fruit{}
1. 分清主要矛盾和次要矛盾:容器是主,容器的泛型类型的关系为辅。
List<Fruit> fruitList = new ArrayList<Fruit>(); List<Apple> appleList = new ArrayList<Apple>();
fruitList和appleList是不同的容器对象,即两个盛不同东西的容器。类型都是List,没有继承关系。
//fruitList = appleList;//compile error: Required FruitList but Found AppleList
通常来说,如果Apple是Fruit的子类型,G是一种带泛型的类型,则G<Fruit>不是G<Apple>的子类型。这也许是泛型学习里面最让人容易混淆的一点。
2. 通配符<?>,通用程序设计
//使用通配符?,表示可以接收任何元素类型的集合作为参数 void printCollection3(Collection<?> c) { for (Object e:c) { System.out.println(e); } }
这样就实现了不同泛型限制的容器,可以更通用的调用。
3. 通配符<?>的缺陷:使用了泛型限制的容器,只能是一种类型确切的类型,不能杂乱添加,否则就违背了泛型设计的初衷了。
static void generalInsertAndGet() { List<?> c = new ArrayList<String>(); //c.add(new Object()); //compile time error,不管加入什么对象都出错, //除了null外,这是非常严重的!!!。 c.add(null); //OK //String s = c.get(0);//compile time error,凡是通配符的地方,都以向上边界得到对象, //也只能这样,否则编译器能怎样呢,他也不知道确切的类型。 }
如果试图往使用通配符?的集合中加入对象,就会在编译时出现错误。需要注意的是,这里不管加入什么类型的对象都会出错。这是因为通配符?表示该集合存储的元素类型未知,可以是任何类型。往集合中加入元素需要是一个未知元素类型的子类型,正因为该集合存储的元素类型未知,所以我们没法向该集合中添加任何元素。唯一的例外是null,因为null是所有类型的子类型,所以尽管元素类型不知道,但是null一定是它的子类型。
另一方面,我们可以从List<?> lists中获取对象,虽然不知道List中存储的是什么类型,但是可以肯定的是存储的类型一定是Object的子类型,所以可以用Object类型来获取值。如for(Object obj: lists),这是合法的。
4. 边界通配符的上限:<? extends XXXClass>,取值的上边界为XXXClass,但没法向容器add值。
表示这样的容器:泛型类型为XXXClass或其任一的子类。比如参数为List<? extends Fruit>,表示可以接收这样的参数传递进来调用:泛型为Fruit或任一Fruit子类的容器。
static void generalCall(List<? extends Fruit> list) { Fruit fruit = list.get(0); //Apple apple = list.get(0);//compile error,这是合理的,因为这里通配符的上边界 //就是Fruit,编译器只能知道这些,并不能知道到底是边界的哪个子类。 //list.add(new Object());//compile error,这是合理的,因为是个上届为Fruit的容器, //不能添加其他的东西 //list.add(new Fruit());//compile error,这看着有点儿不合理的!!! //这是最容易出现问题的地方!!!一个Fruit的容器居然不能添加Fruit!!! /** * 真实情况是任何时候,都只能调用一种泛型限制了的容器参数, * 参数可以是new ArrayList<Fruit>(),new ArrayList<Apple>(),new ArrayList<RedApple>() * 但到底是哪一个,运行的时候是不知道的。所以这里的<? extends Fruit>泛型和<?>泛型 * 在添加上没有区别,都不能添加任何的东西,null除外。 * 那问题就来了,这个扩展了上限限制的<? extends Fruit>有什么好处呢? * 好处是可以直接得到泛型上限的值,而不用使用转型。如下代码所示。 * 这总比让容器泛型的边界直接抵达顶层类Object要好很多,对程序来说, * 也更精确了一些,这就是改进了的地方。 */ Fruit fruit1 = list.get(10000); //list.add(new Apple());//compile error,如果明白了上面的理由,边界为Fruit的容器参数, //连Fruit的都不能添加,Apple就更不能添加了。 /** * 要知道,真实情况的调用下,传递给框架的容器参数,是单一类型的容器且不能 * 确定是Fruit的哪个类型。所以这里不能确定添加泛型边界的哪个子类。 */ } public static void main(String[] args) { generalCall(new ArrayList<Fruit>()); generalCall(new ArrayList<Apple>()); generalCall(new ArrayList<RedApple>()); //generalCall(new ArrayList<Plant>());//compile error }
5. 边界通配符下限:<? super XXXClass>,可以向容器add泛型下边界的值,但取值的上边界是Object。
表示:此容器的泛型为XXXClass或此容器的泛型为XXXClass的任一父类。
这总比单一的<?>多了一些约束,这就够了,很不错了。
static void generalCall2(List<? super Fruit> list){ //Fruit fruit = list.get(10000);//compile error. Required Fruit Found Object. //如果明白了extends那里的关键点,这里就很容易明白了。 //关键点:1.运行时某个时刻调用的容器参数的泛型一定是固定的一种,而不是多种。 //2. 想想编译器是否可以知道这一点,若不知道或不能确定,那不让编译通过。 //这里不能通过的关键地方在于super限制了参数容器的下限边界为Fruit, //这就是说泛型为Fruit是可以传递进来的,Fruit的子类也是Fruit,所以也可以添加进来。 //但是某个时刻,从参数容器中取值值,编译器就不能确定取出的值的类型 是边界之上的哪一种 //了。这种情况下,不能让编译通过。 //Apple apple = list.get(10000);//compile error. } static void call101(){ generalCall2(new ArrayList<Fruit>()); generalCall2(new ArrayList<Plant>()); generalCall2(new ArrayList<Object>()); } public static void main(String[] args) { call100();call101(); }
边界通配符总结:
如果你想从一个数据类型里获取数据,使用 ? extends 通配符
如果你想把对象写入一个数据结构里,使用 ? super 通配符
如果你既想存,又想取,那就别用通配符。
注意:边界通配符的上述限制,仅仅是因为其要作为参数传递。否则不存在上述问题。请想想为什么~
既然是作为参数传递过来,那么如果必须做相应的操作,就copy一份,就可绕过编译不能通过的问题。
6. 关于泛型数组:数组没有必要使用泛型,因为数组本身就是强类型的,不存在容器的两个问题。
规则:不能创建一个确切泛型类型的数组,只能创建带通配符的泛型数组。
不能创建一个确切泛型类型的数组。如下面代码会出错。
List<String>[] lsa = new ArrayList<String>[10]; //compile error.
因为如果可以这样,那么考虑如下代码,会导致运行时错误。
//List<String>[] lsa = new ArrayList<String>[10]; // 实际上并不允许这样创建数组,因为是精确类型的泛型数组 Object o = lsa; Object[] oa = (Object[]) o; List<Integer>li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li;// unsound, but passes run time store check String s = lsa[1].get(0); //run-time error - ClassCastException
因此只能创建带通配符的泛型数组,如下面例子所示,这回可以通过编译,但是在倒数第二行代码中必须显式的转型才行,即便如此,最后还是会抛出类型转换异常,因为存储在lsa中的是List<Integer>类型的对象,而不是List<String>类型。最后一行代码是正确的,类型匹配,不会抛出异常。
List<?>[] lsa = new List<?>[10]; // ok, array of unbounded wildcard type Object o = lsa; Object[] oa = (Object[]) o; List<Integer>li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; //correct String s = (String) lsa[1].get(0);// run time error, but cast is explicit Integer it = (Integer)lsa[1].get(0); // OK
Prefer:
http://qiemengdao.iteye.com/blog/1525624 本篇文章总结的非常好!向作者表示感谢(文中有错误,看的时候注意)。
http://www.cnblogs.com/lwbqqyumidi/p/3837629.html
http://blog.csdn.net/daniel_h1986/article/details/5708605
http://developer.51cto.com/art/200909/153983.htm
http://www.liutime.com/java_circlescontentinfo/id=488
http://blog.csdn.net/jiafu1115/article/details/6624254 #很不错的总结
0
0
0
8
0
0
0