黑马程序员_泛型程序设计

来源:互联网 发布:耐克抢鞋软件 编辑:程序博客网 时间:2024/04/27 15:20

-------android培训、java培训、期待与您交流! ----------

      泛型程序设计(Generic programming)意味着编写的代码可以被很多不同类型的对象所重用。例如,一个ArrayList类可以聚集任何类型的对象,这是一个泛型程序设计的实例。

      在Java SE 5.0之前,Java 泛型程序设计是用继承实现的。ArrayList类只维护一个Object引用的数组:

publicclass ArrayList    //before Java SE 5.0{public Object get(int i){...}public void add(Object o){...}//省略部分代码private Object[] elementDate;}

这样的实现有两个问题,当捕获一个值时,必须进行强制类型转换。

ArrayListfiles = new ArrayList();//省略部分代码String filename = (String)files.get(0);

此外,这里没有错误检查。可以向数组列表中添加任何类的对象。

files.add(newFile(“…”));

      对于这个调用,编译和运行都不会出错,然而在其他地方,如果将get的结果强制类型转换为String类型,就会产生一个错误。

      泛型提供了一个更好的解决方法:类型参数(typeparameters)。ArrayList类有一个类型参数用来指示元素的类型:

ArrayList<String>files = new ArrayList<String>();

      这使得代码具有更好的可读性,人们一看就知道这个数组列表中包含的是String对象。

      编译器也可以很好的利用这个信息。到调用get的时候,不需要进行强制类型转换,编译器就知道返回值类型是String,而不是Object。

String filename = files.get(0);

      编译器还知道ArrayList<String>中的add方法有一个类型为String的参数,这将比使用Object类型的参数安全一些。现在,编译器可以进行检查,避免插入错误类型的对象。例如:

files.add(newFile("..."));       //ERROR!canonly add String objects to an //ArrayList<String>

是无法通过编译的。出现编译错误比类在运行时出现类的强制类型转换异常要好得多。类型参数的魅力在于,是的程序具有更好的可读性和安全性。

 

简单泛型类的定义

      一个泛型类(generic class)就是具有一个或多个类型变量的类。使用一个简单的Pair类作为例子。对于这个类来说,我们只关注泛型,而不会为数据存储的细节烦恼。下面是Pair的代码:

public class Pair<T>{public Pair(){first = null; secone =null;}public Pair(T first,T second){this.first= first;this.second = second;)public T getFirst(){return first;}public T getSecond(){return second;}public void setFirst(T newValue){first =newValue;}public void setSecond(T newValue){second= newValue;}private T first;private T second;}

      Pair类引入一个类型变量T,用尖括号(<>)括起来,并放在类名的后面,泛型类可以有多个类型变量。例如,可以定义Pair类,其中第一个域和第二个域使用不同的类型变量:

public class Pair<T,U>{…}

      类定义中的类型变量指定①方法的返回类型②域的类型③局部变量的类型,例如:

private T first     //uses type variable

      PS:类型变量使用大写形式,且比较短,这是很常见的。在Java库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字与值的类型。T(需要时还可以用临近的字幕U和S)表示“任意类型”。

 

      用具体的类型替换类型变量就可以实例化泛型类型。

Pair<String>

      可以将结果向下成带有构造器的普通类和方法。

泛型类可以看作普通类的工厂。

 

泛型方法

      可以定义一个带有类型参数的简单方法

classArrayAlg{public static <T> T getMiddle(T[]a){returna[a.length / 2];}}

      这个方法是在普通类中定义的,而不是在泛型类中定义的。然而,这是一个泛型方法,可以从尖括号和类型变量看出这一点。注意,类型变量放在修饰符(这是是public static)的后面,返回类型的前面。

      泛型方法可以定义在普通类中,也可以定义在泛型类中。

      当调用一个泛型方法时,在方法名的尖括号放入具体的类型:

String[] names = {"John","Q.","Public"};String middle = ArrayAlg.<String>getMiddle(names);

      在这种情况下(实际也是大多数情况),方法调用中可以省略<String>类型参数。编译器有足够的信息能够推断出所调用的方法。它用names的类型(即String[])与泛型类型T[]进行匹配并推断出一定是String。也就是说,可以调用:

String middle = ArrayAlg.getMiddle(names);

      在大多数情况下,对于泛型方法的类型引用没有问题。偶尔,编译器也会提示错误,此时需要解译错误报告。看看下面这个示例:

double middle = ArrayAlg.getMiddle(3.14,1729,0);

      错误消息是:“found :java.lang.Number & java.lang.Comparable < ? extends java.lang.Numbers& java.lang.Comparable<?>>, required : double”。简单的说,编译器将会自动打包参数为1个Double和2个Integer对象,而后需要这些类的共同超类型。事实上,找到2个这样的超类型:Number和Comparable接口,其本身也是一个泛型类型。在这种情况下,可以采取的补救措施是将所有的参数写为double值。

 

类型变量的限定

      有时,类或方法需要对类型变量加以约束,下面是一个典型例子。我们要计算数组中的最小元素:

class ArrayAlg{public static <T> T min(T[] a)      //almost correct{if(a == null || a.length == 0)return null;T smallest = a[0];for(int i = 1;i < a.length;i++)if(smalest.compareTo(a[i])> 0) smallest = a[i];return smallest;}}

      这里有一个问题,在min方法的内部代码中,变量smallest类型为T,这意味着它可以是任意一个类的对象,怎样才能确信T所属的类有compareTo方法呢?

      解决这个问题的方法是将T限制为实现Comparable接口,可以通过对类型变量T设定限定(bound)实现这一点。

public static <T extends Comparable> T min(T[] a)…

      实际上Comparable接口本身就是一个泛型类型,目前,我们忽略其复杂性以及编译器产生的警告。

      现在,泛型的min方法只能被实现Comparable接口的类(如String、Date等)的数组调用;没有实现Comparable接口的方法,调用min方法会产生一个编译错误。

      为什么使用关键字extends而不是implements?下面的符号

<T extends Bounding Type>

      表示T应该是绑定类型的子类型(subtype)。T和绑定类型可以是类,也可以是接口。选择关键字extends的原因是更接近子类的概念。

      一个类型变量或通配符可以有多个限定,例如:

T extends Comparable & Serializable

      限定类型用“&”分隔,而逗号用来分隔类型变量。

      在Java 的继承中,可以根据需要拥有多个接口类型,但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。

泛型代码和虚拟机

      虚拟机没有泛型类型对象——所有对象都属于普通类。

      无论何时定义一个泛型类型,都自动提供一个相应的原始类型(raw type)。原始类型的名字就是山区类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型(无限定的变量用Object)。

      例如,Pair<T>的原始类型如下所示:

public class Pair{public Pair(Object first,Object second){ this.first = first; this.second = second;}public Object getFirst{return first;}public Object getSecond{return second;}public void setFirst(ObjectnewValue){first = newValue;}public void setSecond(ObjectnewValue{second = newValue;}private Object first;private Object second;}

      因为T是一个无限定的变量,所以直接用Object替换。

      结果是一个普通的类,就好像泛型引入Java语言之前已经实现的那样。

      在程序中可以包含不同类型的Pair,例如,Pair<String>或Pair<GregorianCalendar>,而擦除类型之后就变成原始的Pair类型了。

      原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换。例如,类Pair<T>中的类型变量,没有显示的限定,因此,原始类型用Object替换T。假定声明了一个不同类型:

public class Interval<T extends Comparable & Serializable> implementsSerializable{public Interval(T first,T second){ if(first.compareTo(second) <=0){lower = first;upper = second;} else{lower = second;upper = first}}//省略部分代码private T lower;private T upper;}

原始类型Interval如下所示:

public class Interval implements Serializable{public Interval(Comparable first , Comparablesecond){...}private Comparable lower;private Comparable upper;}

PS:如果写成class Interval<T extends Serializable & Comparable>,原始类型用Serializable替换T,而编译器在必要时向Comparable插入强制类型转换。为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界列表的末尾。

 

翻译泛型表达式

      当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换,例如,下面这个语句语序:

Pair<Employee>buddies = ...;Employeebuddy = buddies.getFirst();

      擦除getFirst的返回类型后将返回Object类型,编译器自动插入Employee的强制类型转换,也就是说,编译器把这个方法调用翻译为两条虚拟机指令:

1)     对原始方法Pair.getFirst的调用

2)     将返回的Object类型强制转换为Employee类型

 

翻译泛型方法

      类型参数也会出现在泛型方法中,程序员通常认为下述的泛型方法:

public static <T extends Comparable> T min(T[] a)

是一个完整的方法族,而参数类型之后,只剩下一个方法:

public static Comparable min( Comparable[] a)


注意,类型参数T已经被擦除了,只留下限定类型Comparable。

 

方法的擦除带来了两个复杂问题,看下下面这个示例:

classDateInterval extends Pair<Date>{public void setSecond(Date second){if(second.compareTo(getFirst())>= 0)super.setSecond(second);}//省略部分代码}

      一个日期区间是一对Date对象,并且需要覆盖这个方法来确保第二个值永远不小于第一个值。这个类擦除后变成:

class DateInterval extends Pair //after erasure{public void setSecond(Date second){...}//省略部分代码}

      注意,这里还存在一个从Pair继承的setSecond方法,即:

public void setSecond(Object second)

      这显然是一个i额不同的方法,因为它有一个不同类型的参数——Object,而不是Date。然而,不应该不一样,考虑下面的语句序列。

DateInterval interval = new DateInterval(...);Pair<Date> pair = interval;   //OK--assignment tosuperclasspair.setSecond(aDate);

      这里,希望对setSecond的调用具有多态性,并调用最合适的那个方法,由于pair引用DateInterval对象,所以应该调用DateInterval.setSecond。问题在于类型擦除与多态发生了冲突,要解决这个问题,就需要在DateInterval类中生成一个桥方法(bridge method):

public void setSecond( Object second){ setSecond ((Date)second);}

      要想了解它的工作过程,跟踪下列语句的执行:

pair.setSecond(aDate);

      变量pair已经声明为类型Pair<Date>,并且这个类型只有一个简单的方法setSecond,即setSecond(Object),虚拟机用pair引用的对象调用这个方法,这个对象是DateInterval类型的,因而会调用DateInterval.setSecond(Object),这个方法是合成的桥方法,他调用DateInterval.setSecond(Date),这正是我们所期望的操作效果。

 

需要记住Java泛型转换的事实:

1)     虚拟机中没有泛型,只有普通的类和方法。

2)     所有的类型参数都用它们的限定类型替换

3)     桥方法被合成来保持泛型

4)     为保持类型安全性,必要时插入强制类型转换

 

约束与局限性

1)     不能用基本类型实例化类型参数

不能用类型参数代替基本类型,因此,没有Pair<double>,只有Pair<Double>,当然,其原因是类型擦除,擦除之后,Pair类含有Object类型的域,而Object不能存储double值。

 

2)     运行虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。例如:

if(a instanceof Pair<String>)       //sameas a instanceof Pair

实际上仅仅测试a是否是任意类型的一个Pair。下面的测试同样为真:

if(a instanceof Pair<T>) //T is ingored

或强制类型转换

Pair<String> p = (Pair<String>)a;     //WARNING--canonly test that is a Pair

      要记住这一风险,无论何时使用instanceof或涉及泛型类型的强制类型转换都会看到一个编译器警告。

      同样的道理,getClass方法总是返回原始类型。例如:

Pair<String> stringPair = ...;Pair<Employee> employeePair = ...;if(stringPair.getClass()== employeePair.getClass())//they are equal

其比较结果是true,这是因为两次调用getClass都将返回Pair.class。

 

3)     不能抛出也不能捕获泛型类实例

事实上,泛型类扩展与Throwable都不合法。例如,下面的定义不会通过编译:

public class Problem<T> extends Exception{/*...*/}  //ERROR--can'textends Throwable

      不能在catch子句中使用类型变量,例如,下面的方法不能通过编译:

public static <T extends Throwable> void doWork(Class<T> t){try{/*...*/}catch(T e)   //ERROR--can't catch type variable{Logger.global.info(...);}}

但是,在异常声明中可以使用类型变量。下面这个方法是合法的:

public static <T extends Throwable> void doWork(T t> throws T //OK{try{/*...*/}catch (Throwable realCause){t.initCause(realCause);throw t;}}

 

4)     参数化类型的数组不合法

不能声明参数化类型的数组,如:

Pair<String>[] table = new Pair<String>[10]; //ERROR!

      因为参数之后,table的类型是Pair[],可以将其转换为Object[]

Object[] objarray = table;

数组能够记住它的原始类型(component type),如果试图存入一个错误类型的元素,就会抛出一个ArrayStoreException异常:

objarray[0]= “Hello”;      //ERROR—component type is Pair

但是,对于泛型而言,擦除将会降低这一机制的效率。赋值:

objarray[0]= new Pair<Employee>();

可以通过数组存储的检测,但仍然会导致类型错误。因此,禁止使用参数化类型的数组。

PS:如果需要收集参数化类型的对象,最好直接使用ArrayList,ArrayList<Pair<String>>,这样既安全又有效。

 

5)     不能实例化类型变量

不能使用像new T(…),newT[…]或T.class这样的表达式中的类型变量。但是,可以通过反射调用Class.newInstance方法构造泛型对象。

public static <T> Pair<T> makePair(Class<T> cl){try{return newPair<T>(cl.newInstance(),cl.newInstance());}catch (Exception e){return null;}}

这个方法可以按照下列方式调用:

Pair<String>p = Pair.makePair(String.class);

      注意,Class类本身是泛型。例如,String.class是一个Class<String>的一个实例(事实上,它是唯一的实例)。因此,makePair方法能够推断出Pair的类型。

      不能构造一个泛型数组:

public static <T extends Comparable> T[] minmax(T[] a){T[] mm = new T[2];   //ERROR/*...*/}

类型擦除会让这个方法永远构造Object[2]数组。

如果数组仅仅作为一个类的私有实例域,就可以将这个数组声明为Object[],并且在获取元素的时候进行类型转换。例如,ArrayList类可以这样实现:

public class ArrayList<E>{private Object[] elements;@SuppressWarnings("unchecked")public E get(int n){return (E) elements[n];}public void set(int n,E e){elements[n] =e;}    //no cast needed}

实际的实现没有这么清晰:

public class ArrayList<E>{private E[] elements;public ArrayList(){elements = (E[]) newObject[10];}}

这里,强制类型转换E[]是一个假象,而类型擦除使其无法察觉。

 

6)     泛型类的静态域和静态方法中类型变量无效

不能在静态域或方法中引入类型变量。

 

7)     注意擦除后的冲突

下面是一个示例,假定像下面这样将equals方法添加到Pair类中:

publicclass Pair<T>{public boolean equals(T value){returnfirst.equals(value)&& second.equals(value);       /*....*/}

考虑一个Pair<String>,方法擦除后boolean equals(T)就是boolean equals (Object),这与Object.equals方法发生冲突。

补救的方法是重新命名引发错误的方法。

      泛型规范说明还提到另外一个原则:“支持擦除的转换,就需要强制一个类或类型变量不能同时成为两个接口是同一个接口的不同参数化的子类。”

0 0
原创粉丝点击