您好,欢迎访问一九零五行业门户网

Java泛型编程最全总结

1介绍
java泛型编程是jdk1.5版本后引入的。泛型让编程人员能够使用类型抽象,通常用于集合里面。下面是一个不用泛型例子:
list myintlist=new linkedlist(); //1   myintlist.add(newinteger(0)); //2   integer x=(integer)myintlist.iterator().next(); //3
注意第3行代码,但这是让人很不爽的一点,因为程序员肯定知道自己存储在list里面的对象类型是integer,但是在返回列表中元素时,还是必须强制转换类型,这是为什么呢?原因在于,编译器只能保证迭代器的next()方法返回的是object类型的对象,为保证integer变量的类型安全,所以必须强制转换。
这种转换不仅显得混乱,更可能导致类型转换异常classcastexception,运行时异常往往让人难以检测到。保证列表中的元素为一个特定的数据类型,这样就可以取消类型转换,减少发生错误的机会, 这也是泛型设计的初衷。下面是一个使用了泛型的例子:
listc0f559cc8d56b43654fcbe4aa9df7b4a myintlist=newlinkedlistc0f559cc8d56b43654fcbe4aa9df7b4a(); //1’   myintlist.add(newinteger(0)); //2’   integerx=myintlist.iterator().next(); //3’
在第1行代码中指定list中存储的对象类型为integer,这样在获取列表中的对象时,不必强制转换类型了。
2定义简单的泛型
下面是一个引用自java.util包中的接口list和iterator的定义,其中用到了泛型技术。
public interface list1a4db2c2c2313771e5742b6debf617a1 {   5144dad4cd9c9023410d94caa0f34ebc    54bdf357c58b8a65c66d7c19c8e4d114void add(e x);   5144dad4cd9c9023410d94caa0f34ebc    54bdf357c58b8a65c66d7c19c8e4d114iterator1a4db2c2c2313771e5742b6debf617a1 iterator();   }   public interface iterator1a4db2c2c2313771e5742b6debf617a1 {   5144dad4cd9c9023410d94caa0f34ebc    54bdf357c58b8a65c66d7c19c8e4d114e next();   5144dad4cd9c9023410d94caa0f34ebc    54bdf357c58b8a65c66d7c19c8e4d114boolean hasnext();   }
这跟原生类型没有什么区别,只是在接口后面加入了一个尖括号,尖括号里面是一个类型参数(定义时就是一个格式化的类型参数,在调用时会使用一个具体的类型来替换该类型)。
也许可以这样认为,listc0f559cc8d56b43654fcbe4aa9df7b4a表示list中的类型参数e会被替换成integer。
public interface integerlist { <span style="white-space: pre;"> </span>void add(integer x) <span style="white-space: pre;"> </span>iterator<integer> iterator(); }
类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上,因此泛型类型中的静态变量是所有实例共享的。此外,需要注意的是,一个static方法,无法访问泛型类的类型参数,因为类还没有实例化,所以,若static方法需要使用泛型能力,必须使其成为泛型方法。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。在使用泛型时,任何具体的类型都被擦除,唯一知道的是你在使用一个对象。比如:list<string>和list<integer>在运行事实上是相同的类型。他们都被擦除成他们的原生类型,即list。因为编译的时候会有类型擦除,所以不能通过同一个泛型类的实例来区分方法,如下面的例子编译时会出错,因为类型擦除后,两个方法都是list类型的参数,因此并不能根据泛型类的类型来区分方法。
public class erasure{ public void test(list<string> ls){ system.out.println("sting"); } public void test(list<integer> li){ system.out.println("integer"); } }
那么这就有个问题了,既然在编译的时候会在方法和类中擦除实际类型的信息,那么在返回对象时又是如何知道其具体类型的呢?如list<string>编译后会擦除掉string信息,那么在运行时通过迭代器返回list中的对象时,又是如何知道list中存储的是string类型对象呢?
擦除在方法体中移除了类型信息,所以在运行时的问题就是边界:即对象进入和离开方法的地点,这正是编译器在编译期执行类型检查并插入转型代码的地点。泛型中的所有动作都发生在边界处:对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型。
3.泛型和子类型
为了彻底理解泛型,这里看个例子:(apple为fruit的子类
list<apple> apples = new arraylist<apple>(); //1 list<fruit> fruits = apples; //2
第1行代码显然是对的,但是第2行是否对呢?我们知道fruit fruit = new apple(),这样肯定是对的,即苹果肯定是水果,但是第2行在编译的时候会出错。这会让人比较纳闷的是一个苹果是水果,为什么一箱苹果就不是一箱水果了呢?可以这样考虑,我们假定第2行代码没有问题,那么我们可以使用语句fruits.add(new strawberry())(strawberry为fruit的子类)在fruits中加入草莓了,但是这样的话,一个list中装入了各种不同类型的子类水果,这显然是不可以的,因为我们在取出list中的水果对象时,就分不清楚到底该转型为苹果还是草莓了。
通常来说,如果foo是bar的子类型,g是一种带泛型的类型,则g<foo>不是g<bar>的子类型。这也许是泛型学习里面最让人容易混淆的一点。
4.通配符
4.1通配符?
先看一个打印集合中所有元素的代码。
void printcollection(collection c) { <span style="white-space: pre;"> </span>iterator i=c.iterator(); <span style="white-space: pre;"> </span>for (k=0;k < c.size();k++) { <span style="white-space: pre;"> </span>system.out.println(i.next()); <span style="white-space: pre;"> </span>} }
void printcollection(collection<object> c) { for (object e:c) { system.out.println(e); } }
很容易发现,使用泛型的版本只能接受元素类型为object类型的集合如arraylist<object>();如果是arraylist<string>,则会编译时出错。因为我们前面说过,collection<object>并不是所有集合的超类。而老版本可以打印任何类型的集合,那么如何改造新版本以便它能接受所有类型的集合呢?这个问题可以通过使用通配符来解决。修改后的代码如下所示:
//使用通配符?,表示可以接收任何元素类型的集合作为参数 void printcollection(collection<?> c) { <span style="white-space: pre;"> </span>for (object e:c) { <span style="white-space: pre;"> </span>system.out.println(e); <span style="white-space: pre;"> </span>} }
这里使用了通配符?指定可以使用任何类型的集合作为参数。读取的元素使用了object类型来表示,这是安全的,因为所有的类都是object的子类。这里就又出现了另外一个问题,如下代码所示,如果试图往使用通配符?的集合中加入对象,就会在编译时出现错误。需要注意的是,这里不管加入什么类型的对象都会出错。这是因为通配符?表示该集合存储的元素类型未知,可以是任何类型。往集合中加入元素需要是一个未知元素类型的子类型,正因为该集合存储的元素类型未知,所以我们没法向该集合中添加任何元素。唯一的例外是null,因为null是所有类型的子类型,所以尽管元素类型不知道,但是null一定是它的子类型。
collection<?> c=new arraylist<string>(); c.add(newobject()); //compile time error,不管加入什么对象都出错,除了null外。 c.add(null); //ok
另一方面,我们可以从list<?> lists中获取对象,虽然不知道list中存储的是什么类型,但是可以肯定的是存储的类型一定是object的子类型,所以可以用object类型来获取值。如for(object obj: lists),这是合法的。
4.2边界通配符
1)?extends通配符
假定有一个画图的应用,可以画各种形状的图形,如矩形和圆形等。为了在程序里面表示,定义如下的类层次:
public abstract class shape { <span style="white-space: pre;"> </span>public abstract void draw(canvas c); } public class circle extends shape { <span style="white-space: pre;"> </span>private int x,y,radius; <span style="white-space: pre;"> </span>public void draw(canvas c) { ... } } public class rectangle extends shape <span style="white-space: pre;"> </span>private int x,y,width,height; <span style="white-space: pre;"> </span>public void draw(canvasc) { ... } }
为了画出集合中所有的形状,我们可以定义一个函数,该函数接受带有泛型的集合类对象作为参数。但是不幸的是,我们只能接收元素类型为shape的list对象,而不能接收类型为list<cycle>的对象,这在前面已经说过。为了解决这个问题,所以有了边界通配符的概念。这里可以采用public void drawall(list<? extends shape>shapes)来满足条件,这样就可以接收元素类型为shape子类型的列表作为参数了。
//原始版本 public void drawall(list<shape> shapes) { <span style="white-space: pre;"> </span>for (shapes:shapes) { <span style="white-space: pre;"> </span>s.draw(this); <span style="white-space: pre;"> </span>} }
//使用边界通配符的版本 public void drawall(list<?exends shape> shapes) { <span style="white-space: pre;"> </span>for (shapes:shapes) { <span style="white-space: pre;"> </span>s.draw(this); <span style="white-space: pre;"> </span>} }
这里就又有个问题要注意了,如果我们希望在list<?exends shape> shapes中加入一个矩形对象,如下所示:
shapes.add(0, new rectangle()); //compile-time error
那么这时会出现一个编译时错误,原因在于:我们只知道shapes中的元素时shape类型的子类型,具体是什么子类型我们并不清楚,所以我们不能往shapes中加入任何类型的对象。不过我们在取出其中对象时,可以使用shape类型来取值,因为虽然我们不知道列表中的元素类型具体是什么类型,但是我们肯定的是它一定是shape类的子类型。
2)?super通配符
这里还有一种边界通配符为?super。比如下面的代码:
list<shape> shapes = new arraylist<shape>(); list<? super cicle> ciclesupers = shapes; ciclesupers.add(new cicle()); //ok, subclass of cicle also ok ciclesupers.add(new shape()); //error
这表示ciclesupers列表存储的元素为cicle的超类,因此我们可以往其中加入cicle对象或者cicle的子类对象,但是不能加入shape对象。这里的原因在于列表ciclesupers存储的元素类型为cicle的超类,但是具体是cicle的什么超类并不清楚。但是我们可以确定的是只要是cicle或者circle的子类,则一定是与该元素类别兼容。
3)边界通配符总结
<!--[if !supportlists]-->l <!--[endif]-->如果你想从一个数据类型里获取数据,使用 ? extends 通配符
<!--[if !supportlists]-->l <!--[endif]-->如果你想把对象写入一个数据结构里,使用 ? super 通配符
<!--[if !supportlists]-->l <!--[endif]-->如果你既想存,又想取,那就别用通配符。
5.泛型方法
考虑实现一个方法,该方法拷贝一个数组中的所有对象到集合中。下面是初始的版本:
static void fromarraytocollection(object[]a, collection<?> c) { <span style="white-space: pre;"> </span>for (object o:a) { <span style="white-space: pre;"> </span>c.add(o); //compile time error <span style="white-space: pre;"> </span>} }
可以看到显然会出现编译错误,原因在之前有讲过,因为集合c中的类型未知,所以不能往其中加入任何的对象(当然,null除外)。解决该问题的一种比较好的办法是使用泛型方法,如下所示:
static <t> void fromarraytocollection(t[] a, collection<t>c){ <span style="white-space: pre;"> </span>for(t o : a) { <span style="white-space: pre;"> </span>c.add(o);// correct <span style="white-space: pre;"> </span>} }
注意泛型方法的格式,类型参数<t>需要放在函数返回值之前。然后在参数和返回值中就可以使用泛型参数了。具体一些调用方法的实例如下:
object[] oa = new object[100]; collection<object>co = new arraylist<object>(); fromarraytocollection(oa, co);// t inferred to be object string[] sa = new string[100]; collection<string>cs = new arraylist<string>(); fromarraytocollection(sa, cs);// t inferred to be string fromarraytocollection(sa, co);// t inferred to be object integer[] ia = new integer[100]; float[] fa = new float[100]; number[] na = new number[100]; collection<number>cn = new arraylist<number>(); fromarraytocollection(ia, cn);// t inferred to be number fromarraytocollection(fa, cn);// t inferred to be number fromarraytocollection(na, cn);// t inferred to be number fromarraytocollection(na, co);// t inferred to be object fromarraytocollection(na, cs);// compile-time error
注意到我们调用方法时并不需要传递类型参数,系统会自动判断类型参数并调用合适的方法。当然在某些情况下需要指定传递类型参数,比如当存在与泛型方法相同的方法的时候(方法参数类型不一致),如下面的一个例子:
public <t> void go(t t) { system.out.println("generic function"); } public void go(string str) { system.out.println("normal function"); } public static void main(string[] args) { funcgenric fg = new funcgenric(); fg.go("haha");//打印normal function fg.<string>go("haha");//打印generic function fg.go(new object());//打印generic function fg.<object>go(new object());//打印generic function }
如例子中所示,当不指定类型参数时,调用的是普通的方法,如果指定了类型参数,则调用泛型方法。可以这样理解,因为泛型方法编译后类型擦除,如果不指定类型参数,则泛型方法此时相当于是public void go(object t)。而普通的方法接收参数为string类型,因此以string类型的实参调用函数,肯定会调用形参为string的普通方法了。如果是以object类型的实参调用函数,则会调用泛型方法。
6.其他需要注意的小点
1)方法重载
在java里面方法重载是不能通过返回值类型来区分的,比如代码一中一个类中定义两个如下的方法是不容许的。但是当参数为泛型类型时,却是可以的。如下面代码二中所示,虽然形参经过类型擦除后都为list类型,但是返回类型不同,这是可以的。
/*代码一:编译时错误*/ public class erasure{ public void test(int i){ system.out.println("sting"); } public int test(int i){ system.out.println("integer"); } }
/*代码二:正确 */ public class erasure{ public void test(list<string> ls){ system.out.println("sting"); } public int test(list<integer> li){ system.out.println("integer"); } }
2)泛型类型是被所有调用共享的
所有泛型类的实例都共享同一个运行时类,类型参数信息会在编译时被擦除。因此考虑如下代码,虽然arraylist<string>和arraylist<integer>类型参数不同,但是他们都共享arraylist类,所以结果会是true。
list<string>l1 = new arraylist<string>(); list<integer>l2 = new arraylist<integer>(); system.out.println(l1.getclass() == l2.getclass()); //true
3)instanceof
不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。
collection cs = new arraylist<string>(); if (cs instanceof collection<string>){…}// compile error.如果改成instanceof collection<?>则不会出错。
4)泛型数组问题
不能创建一个确切泛型类型的数组。如下面代码会出错。
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>类型。最后一行代码是正确的,类型匹配,不会抛出异常。
ist<?>[] 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
更多java泛型编程最全总结。
其它类似信息

推荐信息