`
足至迹留
  • 浏览: 485385 次
  • 性别: Icon_minigender_1
  • 来自: OnePiece
社区版块
存档分类
最新评论

泛型编程

阅读更多
参考资料:《java核心技术 卷1》 网络

Java中的泛型只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。

1.为什么要使用泛型程序设计
泛型程序设计(generic programming)意味着编写的代码可以被很多不同类型的对象所重用。比如常用的ArrayList就是泛型设计,可以存储String,File等不同类型的实例(当然,不是混合存储不同类型)。
在jdk 5.0之前,java泛型程序设计是用继承实现的,比如ArrayList内部只维护一个Object类型的数组。这样就面临两个问题,一是取值时必须进行强制类型转换;二是没有类型检查,存值时可以添加任何类型的对象。这样也会导致取值强制转换时造成运行时错误。泛型提供了一个更好的解决方案:类型参数(type parameters)。还用ArrayList举例,ArrayList类有一个类型参数来指示元素的类型:
ArrayList<String> files = new ArrayList<String>();
这样一眼就能看出这个数组列表包含的是String对象。不仅如此,编译器也能进行类型检查,编译期错误总比运行时发现更好。解决了上面提到的两个问题,提高了程序的可读性和安全性 。

2.简单泛型类的定义
一个泛型类(generic class)就是具有一个或多个类型变量的类。
示例:
public class Pair<T>
{
    private T first;
    private T second;

    public Pair() {first = null; second = 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 first) {this.first = first;}
    public void setSecond(T second) {this.second = second;}
}

此类引入了一个类型变量T,用尖括号(<>)括起来并放在类名的后面泛型类可以有多个类型变量。例如,可以定义Pair类两个成员具有不同类型。

Public class Pair<T, U>
{
    …
}

注意:一般类型变量使用大写形式且比较短,可以是java合法的任意字符。一般用E表示集合的元素类型,K和V分别表示关键字和值类型。T表示任意类型。

使用泛型类时,用具体的类型(不能用基本类型,比如int, long,后面会解释原因)替换类型变量既可以实例化泛型类型。比如:
Pair<String>
Pair<String>()
Pair<String>(String, String)

3.泛型方法
前面介绍了泛型类,实际上还可以只定义带有类型参数的简单方法,泛型方法可以放在普通类里也可以放在泛型类里。
示例:
public class ArrayAlg
{
    public static <T> T getMiddle(T[] a)
    {
        return a[a.length / 2];
    }

    public <T> T getValue(){…};
}

这里泛型方法是放在普通类里的。注意这里类型变量”<T>”放在修饰符(这里是public static)的后面,返回类型的前面,无论是static方法还是非static方法都要这样。

调用泛型方法时,两种方式,拿上面的泛型方法举例:
1)在方法名前的尖括号放入具体的类型:
String[] names={“jhon”, “sd”, “ddd”};
String middle = ArrayAlg.<String>getMiddle(names);

2)也是大多数情况下,省略尖括号和里面的类型参数
String middle = ArrayAlg.getMiddle(names);

4.类型变量的限定
有时,类或方法需要对类型变量加以约束。
示例:
class ArrayAlg
{
    public static <T> T min(T[] a)
    {
        if (a == null || a.length == 0)
        {
            return null;
        }

        T smallest = a[0];

        for (int i=0; I < a.length; i++)
        {
            if (smallest.compareTo(a[i] > 0)
            {
                smallest= a[i];
            }
        }

        return smallest;
    }
}


这里就有一个问题。内部使用了compareTo()方法,怎样确定类型T一定有这个方法呢?解决办法就是将T设置限定(bound)
public static<T extends Comparable> T min(T[] a);
<T extends BoundingType>表示T应该是绑定类型的子类型。T和绑定类型可以是类也可以是接口。一个类型变量或通配符(后面会讲)可以有多个限定,多个限定类型之间用“&”分隔,多个类型变量之间用逗号“,”分隔:
T extends Comparable & Serializable

如果是多个绑定类型,最多只能有一个是类且必须放在第一个,其他只能是接口类型。

5.泛型代码和虚拟机
虚拟机没有泛型类型对象,所有对象都属于普通类。
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变量并替换为限定类型(用&连接的多个限定类型会选用第一个限定的类型来替换,无限定的变量用Object替换)。

5.1翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换,如:
Pair<Employee> buddies = …
Employee buddy = buddies.getFirst();

擦除getFirst()的返回类型后将返回Object类型。编译器自动插入Employee的强制类型转换。也就是说,编译器会把上面的第二句调用翻译为两条虚拟机指令:
1)对原始方法Pair.getFirst的调用。
2)将上面调用返回的Object类型强制转为Employee类型。

5.2翻译泛型方法
类型擦除也会出现在泛型方法中。如:
Public static <T extends Comparable> T min(T[] a); 擦除后剩下一个方法:
Public static Comparable min(Comparable[] a);
注意,类型参数T已经被擦除了,只留下了限定类型Comparable.

6.约束与局限性
下面将说明使用java泛型时需要考虑的一些限制。大多数限制都是由类型擦除引起的。

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

6.2运行时类型查询(instanceof)只适用于原始类型
虚拟机中的对象总是一个特定的非泛型类型。因此,所有的类型查询只产生原始类型,比如:
List list = new ArrayList<String>();

		if (list instanceof ArrayList<String>)// 编译错误
		{
			//
		} 

会提示错误:
Cannot perform instanceof check against parameterized type ArrayList<String>. Use the form ArrayList<?> instead since further generic type information will be erased at runtime
改为:
List list = new ArrayList<String>();
		if (list instanceof ArrayList<?>) // 正确
		{
			//
		}

实际上仅仅测试list是否是任意类型的一个ArrayList,因为ArrayList<String>的<String>会被擦除。同样的道理,getClass方法总是返回原始类型,如:
Pair<String> stringPair = …
Pair<Employee> employeePair = …
If (stringPair.getClass()  ==  employeePair.getClass()) //true
注意这里比较是用的“==”而不是equals, 其比较结果仍然是true,因为所有泛型类的实例都共享同一个运行时类,类型参数信息会在编译时被擦除,两次调用getClass()返回的都是Pair.class。
也就是:不能对确切的泛型类型使用instanceOf操作。

6.3不能抛出也不能捕获泛型类实例
不能捕获也不能抛出泛型类的对象。事实上,泛型类扩展Throwable都不合法。如下代码不会通过编译:
1.
public class Problem<T> extends Exception {…} // 不能extends Throwable

2.
public  void doSomething(T oa)
{
    try
    {
        throw  a; //错误
    }
    catch(T el) //错误
    {
        ……
    }
}
先来看第一个错误,抛出一个T类型的对象oa作为异常,这是不允许的。因为在没指定上界的情况下,T会被擦除成Object类,而Object类显然不会是Throwable的子类,因此它不符合异常的有关规定。第二个错误的原因也是一样的。
改掉第一个错误:
public static<T extends Throwable> void doWork(Class<T> t)
{
    try
    {
       …
    }
    catch (T e)  //错误,这里不能捕获泛型变量
    {
        …
    }
}
改正第一个问题的办法是在类的头部加上限界:<T extends Throwable>。但上面这段代码仍然会有一个编译错误,再次修改,不捕获泛型异常,在异常声明中可以使用类型变量,下面是合法的:
Public static <T extends Throwable> void doWork(T t) throws t //正确
{
    try
    {
        …
    }
    catch (Throwable realCause)
    {
        t.initCause(realCause);
        throw t;
    }
}

参考:http://book.51cto.com/art/200903/114785.htm

6.4参数化类型的数组不合法
不能声明参数化类型的数组,如:
Pair<String>[] table = new Pair<String>[10];  // 错误
问题在于擦除之后,table的类型是Pair[], 可以将其转换为Object[]:
Object[] objArray = table;
数组能记住它的元素类型,如果试图存入一个错误类型的元素将会抛出ArrayStoreException异常。但是对于泛型而言,擦除将会降低这一机制的效率。赋值:
objArray[0] = new Pair<Employee>();
可以通过数组存储的检测,,但仍然会导致类型错误。因此,禁止使用参数化类型的数组。

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

6.5不能实例化类型变量
不能使用像new T(), new T[…]或T.class这样的表达式中的类型变量,如:
public Pair() {first = new T(); second = new T();} // 错误。
类型擦除将T改变成Object,而且,本意肯定不希望调用new Object()。但是,可以通过反射机制调用Class.newInstance方法来构造泛型对象。不能调用:
First = T.class.newInstance(); //错误
表达式T.class是不合法的。必须像下面这样设计api以便可以支配Class对象:
public static <T> Pair<T> makePair([color=red]Class<T> cl[/color])
{
    try
    {
        return new Pair<T>([color=red]cl.newInstance(), cl.newInstance()[/color]);
    }
    catch (Exception ex)
    {
        …
    }
}

这个方法可以按照下面方式调用:
Pair<String> p = Pair.makePair(String.class);

强调:
1.如果需要对泛型实例化,需要使用Class<T>参数明确传递到需要实例化的地方,而不能像后面语句那样直接实例化:T theT=new T();
2.还有泛型类型不能作为泛型类型,比如<T<U>>不是合法的。


6.6泛型类的静态上下文中类型变量无效
不能在静态域或方法中引用类型变量,这一点上面讲到过。如:
Public class Singleton<T>
{
    public static T getSingleInstance() // 错误,必须public static <T> T getSingleInstance()
    {
        …
    }

    private static T singleInstance; // 错误
}


7.泛型类型的继承规则
在使用泛型类时,需要了解一些关于继承和子类型的准则。如,Manager类继承Employee类,那么Pair<Manager>是Pair<Employee>的一个子类吗? 答案:不是。并且两者无任何联系。


8.通配符类型
固定的泛型类型系统使用起来会有一些局限,有时无法约束泛型类型必须满足某种条件。通配符类型可以解决这个问题。如:
Pair< ? extends Employee>
表示任何泛型Pair类型,它的类型参数是Employee的子类。如Pair<Manager>,但不能使Pair<String>。
使用通配符会通过Pair<? Extends Employee>的引用破坏Pair<Manager>吗?
Pair<Manager> managerPair = new Pair<Manager>(a, b);
Pair<? Extends Employee> pair = managerPair; // 正确
Pair.setFirst(c); // 其中c是Employee的其他子类型。编译错误。
我们看到通配符不会导致类型破坏不符合条件的写操作(set方法)会引入类型错误。事实上set拒绝一切类型的写操作,因为它无法根据通配符匹配类型。但是get方法完全可以使用。从方法上大概能看出:
? extends Employee getFirst()
Void setFirst(? Extends Employee)

8.1通配符的超类型限定
通配符限定可以指定一个超类型限定(supertype bound),如:
? super Manager
这个通配符限制为Manager的所有超类型。(注意,前面的是extends限定类型为子类型,这里super限定为父类型, 两者都包含本身类型。
这样,Pair<? Super Manager>有方法:
Void setFirst(? Super Manager)
? super Manager getFirst()
编译器不知道setFirst方法的确切类型,但是可以用任意Manager对象(或子类型,因为子类型可以向上转型, 但父类型向下转型是不安全的。)调用它,而不能用Employee对象调用(因为Employee是Manager的父类型)。

对比发现,带有超类型限定的通配符可以向泛型对象写入不能读,带有子类型限定的通配符可以从泛型对象读取不能写入。如果既想读又想写就不能使用通配符了。

8.2无限定通配符
还可以使用无限定通配符,如,Pair<?> 。但这个跟原始的Pair类型是不一样的。类型Pair<?> 有方法(只是理论上的方法,并不能像下面这样定义方法):
? getFirst() // 实际中不能直接用"?"当返回类型,或参数类型,要放在<?>中
Void setFirst(?) // 实际中不能直接用"?"当返回类型,或参数类型,要放在<?>中
getFirst返回值只能赋给一个Object。setFirst方法不能被调用。使用这个通配符可以限制写操作,又可以泛型化读操作。

8.3 通配符使用注意
1)泛型通配符只能用于引用的声明中,不可以在创建对象时使用。
List<?> list = new ArrayList<String>(); // 编译正确
List<?> list = new ArrayList<?>(); // 编译失败

2)不可以使用采用了泛型通配符的引用调用使用了泛型参数的方法
   
package net.oseye;
     
    public class FanXing {
    public static void main(String[] args) {
    Fruit<?> fruit=new Fruit<String>();
    fruit.setColor("red"); // [color=darkred]编译失败[/color]
    }
    }
     
    class Fruit<T>{
    private T color;
    public void setColor(T color){
    this.color=color;	
    }
    public String getColor(){
    return this.color.toString();	
    }
    }

   
其实,List<?> ls = new ArrayList<Integer>();
    ls.add(1); // 编译失败:The method add(int, capture#4-of ?) in the type List<capture#4-of ?> is not applicable for
the arguments (int)
改成:
List ls = new ArrayList<Integer>();
    ls.add(1); // 编译通过

3)无限定通配符不能当做类型参数定义类
public class Pair<?> // 编译失败:Syntax error on token "?", invalid TypeParameter
{
...
}


4)无限定通配符只能出现在尖括号里指明泛型的类型,而且作为整体此时不受读写的限制(也就是上面第2点的限制)。
正确的例子:
public class Pair
{
    private [color=red]List<?>[/color] list = new ArrayList<[color=red]String[/color]>();

    public void setList([color=red]List<?>[/color] list)
    {
    	this.list = list;
    }
    public [color=red]List<?>[/color] getList()
    {
    	return this.list;
    }
}


记住一个原则:无限定通配符只用在与具体泛型值操作无关的地方,比如作为List元素的删除(跟类型无关),移位等。

9.反射和泛型
现在,Class类是泛型的。如String.class实际是一个Class<String>类的对象。
Class<T>中的方法就使用了类型参数:
T newInstance()
T cast(Object obj)
T[] getEnumConstants()
Class<? Super T> getSuperclass()
Constructor<T> getConstructor(Class… parametrTypes)
Constructor<T> getDeclaredConstructor(Class… parameterTypes)
newInstance方法返回一个实例,这个实例由所属的类的默认构造函数获得。如果给定的类型确实是T的一个子类型,cast方法就返回一个现在声明为类型T的对象,否则抛出BadCastException异常。

9.1使用Class<T> 参数进行类型匹配
有时,匹配类型方法中的Class<T>参数的类型变量很有实用价值,下面是一个具有一定权威的实例:
Public static <T> Pair<T> makePair(Class<T> c) throws InstantiationException, IllegalAccessException
{
    Return new Pair<T>(c.newInstance(), c.newInstance());
}

9.2虚拟机中的泛型类型信息
Java泛型的特性之一是在虚拟机中泛型类型的擦除。令人奇怪的是,擦除的类仍然保留一些泛型祖先的的微弱记忆。如,原始的Pair类知道源于泛型类Pair<T>,即使一个Pair类型的对象无法区分是又Pair<String>构造的还是由Pair<Employee>构造的。
类似的,public static Comparable min(Comparable[] a)
是一个泛型方法的擦除:
Public static<T extends Comparable<? Super T>> T min(T[] a)
可以使用jdk 5.0增加的反射api来确定:
1)这个泛型方法有一个叫做T的类型参数,
2)这个类型参数有一个子类型限定,其自身又是一个泛型类型。
   3)这个限定类型有一个通配符参数
   4)这个通配符参数有一个超类型限定。
   5)这个泛型方法有一个泛型数组参数

为了表达泛型类型声明,jdk5.0在java.lang.reflect包中提供了一个新的接口Type。这个接口包含下列子类型:
1) Class类,描述具体类型。
2) TypeVariable接口,描述类型变量(如T extends Comparable<? Super T>)
3) WildcardType接口,描述通配符(如? Super T)
4) ParameterizedType接口,描述泛型类或接口类型(如Comparable<? Super T>)
5) GenericArrayType接口,描述泛型数组(如T[])
最后4个子类型是接口,虚拟机将实例化实现这些接口的适当的类。

10.类型推导
参考:
http://wwwiteye.iteye.com/blog/1849917
http://blog.sina.com.cn/s/blog_5d660384010182jw.html
(关于类型推导网上的资料大部分都是重复的这篇内容,这里只是简单拼凑多篇内容,详细请查看原文)

编译器判断范型方法的实际类型参数的过程称为类型推断,类型推断是相对于知觉推断的,其实现方法是一种非常复杂的过程。
根据调用泛型方法时实际传递的参数类型或返回值的类型来推断,具体规则如下:
1)当某个类型变量只在整个参数列表中的所有参数和返回值中的一处被应用了,那么根据调用方法时该处的实际应用类型来确定,这很容易凭着感觉推断出来,即直接根据调用方法时传递的参数类型或返回值来决定泛型参数的类型,例如:
  swap(new String[3],3,4)  -->    static <E> void swap(E[] a, int i, int j)
2)当某个类型变量在整个参数列表中的所有参数和返回值中的多处被应用了,如果调用方法时这多处的实际应用类型都对应同一种类型来确定,这很容易凭着感觉推断出来,例如:
  add(3,5)  -->  static <T> T add(T a, T b)
3)当某个类型变量在整个参数列表中的所有参数和返回值中的多处被应用了,如果调用方法时这多处的实际应用类型对应到了不同的类型且没有使用返回值,这时候取多个参数中的最大交集类型,例如,下面语句实际对应的类型就是Number了,编译没问题,只是运行时出问题:
  fill(new Integer[3],3.5f)  -->  static <T> void fill(T[] a, T v)
4)当某个类型变量在整个参数列表中的所有参数和返回值中的多处被应用了,如果调用方法时这多处的实际应用类型对应到了不同的类型, 并且使用返回值,这时候优先考虑返回值的类型,例如,下面语句实际对应的类型就是Integer了,编译将报告错误,将变量x的类型改为float,对比eclipse报告的错误提示,接着再将变量x类型改为Number,则没有了错误:
  int x =(3,3.5f)  -->  static <T> T add(T a, T b)
5)参数类型的类型推断具有传递性,下面第一种情况推断实际参数类型为Object,编译没有问题,而第二种情况则根据参数化的Vector类实例将类型变量直接确定为String类型,编译将出现问题:
  copy(new Integer[5],new String[5]) --> static <T> void copy(T[] a,T[] b);
  copy(new Vector<String>(), new Integer[5]) -->  static <T> void copy(Collection<T> a , T[] b);

泛型推导在java7中已经实现了。
  
1.List<String> list = new ArrayList<>(); 
     因为编译器可以从前面(List)推断出推断出类型参数,所以后面的ArrayList之后可以不用写泛型参数了,只用一对空着的尖括号就行。当然,你必须带着”菱形”<>,否则会有警告的。
     Java SE7 只支持有限的类型推断:只有构造器的参数化类型在上下文中被显著的声明了,你才可以使用类型推断,否则不行。 看代码:

List<String> list = new ArrayList<>(); 
list.add("A"); 
 
//这个不行 
list.addAll(new ArrayList<>()); 
 
// 这个可以 
List<? extends String> list2 = new ArrayList<>(); 
list.addAll(list2); 

0
0
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics