博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
十四,泛型(Generics)
阅读量:7206 次
发布时间:2019-06-29

本文共 10508 字,大约阅读时间需要 35 分钟。

hot3.png

1.泛型介绍

Java泛型编程是JDK1.5版本后引入的.泛型让编程人员能够使用类型抽象,通常用于集合里面.最大的特点是泛型中的属性可以由外部决定.

类的泛型声明格式:

class 类名称<泛型类型, 泛型类型... ...>{}

示例:

List myIntList=new LinkedList(); myIntList.add(new Integer(0));Integer x=(Integer)myIntList.iterator().next();    // next()返回的是Object,所以必须强转
注意第 3行 ,存储在 List里面的对象类型是 Integer,但是在返回列表中元素时 ,还是必须强制转换类型 ,这是为什么呢?原因在于 ,编译器只能保证迭代器的 next()方法返回的是 Object类型的对象 ,为保证 Integer变量的类型安全 ,所以必须强制转换 .

这种转换不仅显得混乱,更可能导致类型转换异常ClassCastException,为保证操作安全,减少转换发生错误, 而泛型使取出变得非常容易,不需要再使用向下转型.这就是泛型设计的初衷.

示例:

List
myIntList=newLinkedList
();myIntList.add(new Integer(0));Integer x=myIntList.iterator().next();
在第 1行代码中 指定List 中存储的对象类型为Integer,这样在获取列表中的对象时 ,不必强制转换类型了 .

2泛型示例

下面是一个引用自java.util包中的接口List和Iterator的定义,其中用到了泛型技术.

示例:

public interface List
{ //类型由外部决定 void add(E x); //add的类型与设置类型保持一致 Iterator
iterator(); } public interface Iterator
{ //类型由外部决定 E next(); //返回的类型与传入类型保持一致 boolean hasNext(); }
这跟原生类型没有什么区别 ,只是在接口后面加入了一个尖括号 ,尖括号里面是一个类型参数 .

List<Integer>表示List中的类型参数E被替换成Integer类型.和如下代码等价:

public interface IntegerList {    void add(Integer x)    Iterator
iterator();}

3.类型擦除

类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上.在使用时如果没有指定泛型类型,则表示擦除泛型类型.擦除后按Object接收.一般不要擦除泛型,因为没有什么实际意义,同时以保证操作的安全性.

编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上,因此泛型类型中的静态变量是所有实例共享的.故一个static方法,无法访问泛型类的类型参数,因为类还没有实例化,所以,若static方法需要使用泛型能力,必须使其成为泛型方法.

类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且在必要的时候添加类型检查和类型转换的方法.在使用泛型时,任何具体的类型都被擦除,唯一知道的是你在使用一个对象.比如:List<String>List<Integer>在运行事实上是相同的类型.他们都被擦除成他们的原生类型,List.

因为编译的时候会有类型擦除,所以不能通过同一个泛型类的实例来区分方法,因为类型擦除后,两个方法都是List类型的参数,因此并不能根据泛型类的类型来区分方法.

示例:

public class Info
{ private T msg; public Info(T msg) { this.msg = msg; } public T getMsg() { return msg; } public void setMsg(T msg) { this.msg = msg; }}public class GenDemo { public static void main(String[] args) { Info info0 = new Info(1) ; // 没有指定泛型类型,但写代码时会有警告 Info
info1 = new Info(1) ; // 没有指定泛型类型 Info info2 = new Info("1") ; // 没有指定泛型类型 //注意的是 info0, info1, info2的类型都是Info,并不是Integer和String型 System.out.println(info0.getClass() == info1.getClass()); //True }}

那么这就有个问题了 ,既然在编译的时候会在方法和类中擦除实际类型的信息 ,那么在返回对象时又是如何知道其具体类型的呢?如 List<String>编译后会擦除掉 String信息 ,那么在运行时通过迭代器返回 List中的对象时 ,又是如何知道 List中存储的是 String类型对象呢?

擦除在方法体中移除了类型信息,所以在运行时的问题就是边界即对象进入和离开方法的地点,这正是编译器在编译期执行类型检查并插入转型代码的地点.泛型中的所有动作都发生在边界处:对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型.

4.泛型和子类型

泛型不支持我们之前接触过的向上转型,为了彻底理解泛型,这里看个例子:(Apple为Fruit的子类)

示例:

List
apples = new ArrayList
(); // 向上转型List
fruits = apples;

第 1行代码显然是对的 ,但是第 2行在编译的时候会出错 .这会让人比较纳闷的是一个苹果是水果 ,为什么一箱苹果就不是一箱水果了呢?可以这样考虑 ,假定第 2行代码没有问题 ,那么我们可以使用语句 fruits.add(new Strawberry())在 fruits中加入草莓了 ,但是这样的话 ,一个 List中装入了各种不同类型的子类水果 ,这显然是不可以的 ,因为我们在取出 List中的水果对象时 ,就分不清楚到底该转型为苹果还是草莓了 .

通常来说,如果FooBar的子类型,G是一种带泛型的类型,G<Foo>不是G<Bar>的子类型.这也是泛型学习里面最让人容易混淆的一点.

5.通配符

4.1通配符?

先看一个打印集合中所有元素的代码.

示例:

//不使用泛型void printCollection(Collection c) {    Iterator i=c.iterator();    for (k=0;k < c.size();k++) {    System.out.println(i.next());    }}

示例:

//使用泛型void printCollection(Collection c) {    for (Object e:c) {    System.out.println(e);    }}
很容易发现 ,使用泛型的版本只能接受元素类型为 Object类型的集合 ,如 ArrayList<Object>();如果是 ArrayList<String>,则会编译时出错 .

Collection<Object>并不是所有集合的超类.而老版本可以打印任何类型的集合,那么如何改造新版本以便它能接受所有类型的集合呢?这个问题可以通过使用通配符来解决.修改后的代码如下所示:

示例:

//使用通配符?表示可以接收任何元素类型的集合作为参数void printCollection(Collection
c) { for (Object e:c) { System.out.println(e); }}
这里使用了通配符?指定可以使用任何类型的集合作为参数 .读取的元素使用了 Object类型来表示 ,这是安全的 ,因为所有的类都是 Object的子类 .

这里就又出现了另外一个问题,如果试图往使用通配符?的集合中加入(add)对象,就会在编译时出现错误.

需要注意的是,这里不管加入什么类型的对象都会出错.这是因为通配符?表示该集合存储的元素类型未知,可以是任何类型.往集合中加入元素需要是一个未知元素类型的子类型,正因为该集合存储的元素类型未知,所以我们没法向该集合中添加任何元素.唯一的例外是null,因为null是所有类型的子类型,所以尽管元素类型不知道,但是null一定是它的子类型.

示例:

Collection
c=new ArrayList
();c.add(new Object()); //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 {           public abstract void draw(Canvas c);      }           public class Circle extends Shape {           private int x,y,radius;           public void draw(Canvas c) { ... }      }            public class Rectangle extends Shape {         private int x,y,width,height;           public void draw(Canvasc) { ... }      }
为了画出集合中所有的形状 ,我们可以定义一个函数 ,该函数接受带有泛型的集合类对象作为参数 .但是不幸的是 ,我们只能接收元素类型为 Shape的 List对象 ,而不能接收类型为 List<Cycle>的对象 ,这在前面已经说过 .为了解决这个问题 ,所以有了 边界通配符的概念 .这里可以采用 public void drawAll(
List<? extends Shape> shapes)来满足条件 ,这样就可以接收元素类型为 Shape子类型的列表作为参数了 .

示例:

//原始版本public void drawAll(List
shapes) { for (Shape s:shapes) { s.draw(this); }}

示例:

//使用边界通配符的版本public void drawAll(List<?exends Shape> shapes) {    for (Shape s:shapes) {    s.draw(this);    }}
这里就又有个问题要注意了 ,如果我们希望在 List<? exends Shape> shapes中加入一个矩形对象 ,如下所示:

shapes.add(0, new Rectangle()); //compile-time error
那么这时会出现一个编译时错误 ,原因在于:我们只知道 shapes中的元素时 Shape类型的子类型 ,具体是什么子类型我们并不清楚 ,所以我们不能往 shapes中加入任何类型的对象 . 不过我们在取出其中对象时, 可以使用Shape类型来取值 ,因为虽然我们不知道列表中的元素类型具体是什么类型 ,但是我们肯定的是它一定是 Shape类的子类型 .

2)?super通配符

即泛型的下限.

示例:

List
shapes = new ArrayList
();List
cicleSupers = shapes;cicleSupers.add(new Cicle()); //OK, subclass of Cicle also OKcicleSupers.add(new Shape()); //ERROR
这表示 cicle Supers列表存储的元素为 Cicle的超类 ,因此我们可以往其中加入 Cicle对象或者 Cicle的子类对象 ,但是不能加入 Shape对象 .这里的原因在于列表 cicle Supers存储的元素类型为 Cicle的超类 ,但是具体是 Cicle的什么超类并不清楚 .但是我们可以确定的是只要是 Cicle或者 Circle的子类 ,则一定是与该元素类别兼容 .

3)边界通配符总结

  • 想从一个数据类型里获取数据,使用 ? extends 通配符

  • 若想把对象写入一个数据结构里,使用 ? super 通配符

  • 若既想存,又想取,那就别用通配符.

6.泛型方法

考虑实现一个方法,该方法拷贝一个数组中的所有对象到集合中.下面是初始的版本:

示例:

static void fromArrayToCollection(Object[]a, Collection
c) { for (Object o:a) { c.add(o); //compile time error }}
可以看到显然会出现编译错误 ,原因在之前有讲过 ,因为集合 c中的类型未知 ,所以不能往其中加入任何的对象(当然 ,null除外) .解决该问题的一种比较好的办法是使用泛型方法 ,如下所示:

示例:

static 
void fromArrayToCollection(T[] a, Collection
c){ for(T o : a) { c.add(o);// correct }}
注意泛型方法的格式 ,类型参数 <T>需要放在函数返回值之前 .然后在参数和返回值中就可以使用泛型参数了 .具体一些调用方法的实例如下:

示例:

Object[] oa = new Object[100];      Collectionco = new ArrayList();      fromArrayToCollection(oa, co);// T inferred to be Object      String[] sa = new String[100];      Collection
cs = new ArrayList
(); 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
cn = new ArrayList
(); 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  
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.
go("haha");//打印generic function fg.go(new Object());//打印generic function fg.
go(new Object());//打印generic function }

如例子中所示 ,当不指定类型参数时 ,调用的是普通的方法 ,如果指定了类型参数 ,则调用泛型方法 .可以这样理解 ,因为泛型方法编译后类型擦除 ,如果不指定类型参数 ,则泛型方法此时相当于是 public void go(Object t).而普通的方法接收参数为 String类型 ,因此以 String类型的实参调用函数 ,肯定会调用形参为 String的普通方法了 .如果是以 Object类型的实参调用函数 ,则会调用泛型方法 .

7.补充说明

1)方法重载

在JAVA里面方法重载是不能通过返回值类型来区分的,比如代码一中一个类中定义两个如下的方法是不容许的.但是当参数为泛型类型时,却是可以的.如下面代码二中所示,虽然形参经过类型擦除后都为List类型,但是返回类型不同,这是可以的.

示例1:

/*代码一:编译时错误*/   public class Erasure{              public void test(int i){                  System.out.println("Sting");              }              public int test(int i){                  System.out.println("Integer");              }    }

示例2:

/*代码二:正确 */       public class Erasure{                  public void test(List
ls){ System.out.println("Sting"); } public int test(List
li){ System.out.println("Integer"); } }

2 )泛型类型是被所有调用共享的

所有泛型类的实例都共享同一个运行时类,类型参数信息会在编译时被擦除.因此考虑如下代码,虽然ArrayList<String>和ArrayList<Integer>类型参数不同,但是他们都共享ArrayList类,所以结果会是true.

示例:

List
l1 = new ArrayList
();List
l2 = new ArrayList
();System.out.println(l1.getClass() == l2.getClass()); //True

3)instanceof

不能对确切的泛型类型使用instanceOf操作.如下面的操作是非法的,编译时会出错.

示例:

Collection cs = new ArrayList
();if (cs instanceof Collection
){…}// compile error.//如果改成instanceof Collection
则不会出错.

4)
泛型数组问题

不能创建一个确切泛型类型的数组.如下面代码会出错.

List
[] lsa = new ArrayList
[10]; //compile error.
因为如果可以这样,
那么考虑如下代码,
会导致运行时错误.

示例:

List
[] lsa = new ArrayList
[10]; // 实际上并不允许这样创建数组 Object o = lsa; Object[] oa = (Object[]) o; List
li = new ArrayList
(); 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
li = new ArrayList
(); 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

参考资料:

20150419

JAVA学习笔记系列

--------------------------------------------

                    联系方式

--------------------------------------------

        Weibo: ARESXIONG

        E-Mail: aresxdy@gmail.com

------------------------------------------------

转载于:https://my.oschina.net/u/2288529/blog/403578

你可能感兴趣的文章
[TC-HouseProtection]House Protection
查看>>
[ONTAK2015]OR-XOR
查看>>
不要把时间浪费在QQ上
查看>>
ntohs, ntohl, htons,htonl的比较和详解
查看>>
Ubuntu12.04 eclipse4.2安装ADT20时报错
查看>>
计算形状Shape(圆Circle,矩形Square ,正方形Rectangle)的面积、周长
查看>>
WP7基础学习---第七讲
查看>>
[摘录]第一部分 掌舵领航(4)
查看>>
50、转自知乎上android开发相见恨晚的接口
查看>>
递归 && 反射
查看>>
android AlertDialog 错误 OnClickListener 报错
查看>>
mysql 随机数字 & 置顶排序
查看>>
javaweb配置连接mysql数据库
查看>>
Android — — —动态添加碎片
查看>>
欧拉函数
查看>>
如何使用系统软件截图
查看>>
【转】 Oracle 中的一些重要V$ 动态性能视图,系统视图和表
查看>>
模板模式 c#
查看>>
由于js词法性质和全局变量被更改,循环绑定的click事件执行时变量和定义时 不一致的bug,各种解决方案。...
查看>>
图片处理--边缘高亮
查看>>