您的位置:首页 > 编程语言 > Java开发

【java核心技术卷一】泛型程序设计

2015-12-03 16:37 267 查看

泛型程序设计

为什么要使用泛型程序设计?

​ 泛型程序设计(Generic programming)意味着编写的代码可以被很多不同类型的对象所重用。

​ 例如
ArrayList<T>
,其中
T
可以为任意对象,这也就是说该
ArrayList
的操作都是以
T
为基本单位的,
T
称为类型参数(type parameters)。
ArrayList
类有一个类型参数用来指示元素的类型

[code]ArrayList<T> list = new ArrayList<T>();


在使用时一看就知道
list
包含的是
T
类型对象

在J***A SE7及以后的版本,构造函数中可以省略泛型类型


[code]ArrayList<T> list = new ArrayList<>();


省略的类型可以从变量的类型推断得出

泛型程序设计使在对
list
中的对象进行使用时,可以避免使用错误的方法或参数,让程序具有更好额可读性和安全性。

定义简单泛型类

​ 一个泛型类(generic class)就是具有一个或者多个类型变量的类。对于这个类来说,我们只关注泛型,而不会为数据储存的细节烦恼。

[code]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 newValue){first =newValue;}
   public void setSecond(T newValue){second = newValue;}
}


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

[code]public class Pair <T,U> {...}


在java中,类型变量使用大写形式且较短为常见。在java库中,使用变量
E
表示集合的元素类型,
K
V
分别表示表的关键字(key)和值(Value)的类型。
T
以及附近
U
S
表示任意类型

泛型方法

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

[code]class ArrayAlg{
    public static <T> T getMiddle(T...a){
        return a[a.length/2];
    }
}


这个方法使在普通类中定义的,而不是在泛型类中定义的,但这是一个泛型方法。注意,类型变量放在修饰符的后面,返回类型的前面。

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

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

[code]String middle = ArrayAlg.<String>getMiddle("aa","bb","cc");


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

[code]String middle = ArrayAlg.getMiddle("aa","bb","cc");


几乎在大多数情况下,对于泛型方法的类型引用没有问题。偶尔,编译器也会给出错误提示:

[code]double middle = ArrayAlg.getMiddle(3.14 , 123 , 0);


错误消息会以晦涩的方法指出:解释这句代码有两种方法,而且这两种方法都是合法的。简单地说,编译器会自动打包参数为一个
Double
和两个
Integer
对象,而后寻找这些类的共同超类型。事实上,找到2个这样的超类:
Number
Comparable
接口,其本身也是一个泛型类型。这种情况下的措施就是将所有参数改为
Double
类型

类型变量的限定

有时候,类或方法需要对类型变量加以束缚。例如求数组中最小元素

[code]class Test{
    public static <T> T min(T[] a){
        if(a ==null||a.length==0){
            return null;
        }else{
            T smallest = a[0];
            for(int i = 1 ; i<a.length ;i++ ){
                if(smallest.compareTo(a[i]>0))smallest = a[i];
                return smallest;            
            }
        }
    }
}


但是变量
smallest
的类型为
T
,这意味着它可以是任何一类的对象,如何确信
T
所属的类有
compareTo
方法?

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

[code]public static <T extends Comparable> T min(T[] a){...}


现在,泛型的
min
方法只能被实现了
Comparable
接口的类的数组调用。

但为什么实现接口的关键字是
extends
而不是
implements
?毕竟
Comparable
是一个接口

[code]<T extends BoundingType>


表示
T
应该是绑定类型的子类型(subType)。
T
和绑定类型可以是累,也可以是接口。选择
extends
是因为更接近子类的概念。

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

[code]T extends Comparable & Serializable


限定类型用
&
分隔,而类型变量用
,
分隔。

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

[code]public class A <T extends List&Comparable&Serializable> implements  Serializable{}


约束与局限性

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

没有
Pair<double>
,只有
Pair<Double>
。原因是类型擦除,擦除之后
Object
类型不能储存
double
的值。而当包装器类型不能接受替换时,可以使用独立的类和方法处理



运行时类型查询只适用于原始类型

所有类型查询只能产生原始类型。

例如

[code]if(a instanceof Pair<String>) //ERROR
if(a instanceof Pair<T> )//ERROR
Pair<String> p = (Pair<String>) a;//WARNING


无论何时使用
instance
或涉及泛型类型的强制类型转换表达式都会看到一个警告。

同样,getClass总是返回原始类型

[code]Pair<String> a = ...; Pair<Double> b = ...;
if(a.getClass()==b.getClass()) //always true


因为
getClass
的返回结果都是
Pair.class




不能创建参数化的数组

例如

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


擦除之后,table的类型是Pair[],可以转换为Object[]:

[code]Object[] objarray = table;


数组会记住它的元素类型,如果试图储存其他类型的元素,就会跑出一个
ArrayStoreException
异常

[code]objarray[0] = "hello" //ERROR component type is Pair




Varargs警告

向参数个数可变的方法传递一个泛型类型的实例。例如下面这个方法

[code]public static <T> void addAll(Collection<T> coll,T ... ts){
 for(t:ts)
 coll.add(t);
}


实际上参数
ts
是一个数组,包含提供的所有实参

[code]Collection <Pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
addAll(table,pair1,pair2);


为了调用这个方法,JVM必须建立一个
Pair<String>
数组,但这违反了前面的规则。但你只会得到一个警告。

可以使用
@SuppressWarnings("unchecked")
或者在Java SE7中使用
@SafeVarargs
来标注
addAll
方法



不能实例化类型变量

不能使用
new T(...)
new T[...]
或者
T.class
这样的表达式中的类型变量。例如

[code]public Pair(){first = new T();}      //ERROR


因为在类型擦除后会使得
first
Object
,而本意肯定并非如此。不过可以使用烦着调用
Class.newInstance
方法来构造泛型对象。但非下面这样

[code]first = T.class.newInstance();       //ERROR


表达式是不合法的,必须像下面这样

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


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

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


注意,
Class
类本身是泛型。例如,
String.class
是一个也是唯一
Class<String>
的实例。因此,
makePair
可以判断
pair
的类型。

不能构造一个泛型数组,类型参数会让这个方法永远构造
Object[]
数组。

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



泛型类的静态上下文中类型变量无效

不能在静态实例域或方法中引用类型变量

[code]public class Pair<T>{
    private T pair1;    //ERROR

   public T getPair1(){ //ERROR
        return pair1; 
}
}




不能抛出或捕获泛型类的实例

不能抛出或捕获泛型类对象,甚至泛型类扩展
Throwable
都是不合法的

catch
子句中不能使用类型变量

[code]public static <T extends Throwable> void do(Class<T > t){
    try{
        ...
}catch(T e){    //ERROR
        ...
}
}


不过,在异常规范中使用类型变量是允许的

[code]public static <T extends Throwable> void do(Class<T > t) throws T{       //OK
    try{
        ...
}catch(Throwable cause){
        t.initCause(cause);
       throw t;
}
}




擦除后的冲突

要想支持擦除的转换,就需要强行限制一个类或者类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化

泛型类型的继承规则

永远可以将一个参数化类型转换为一个原始类型。

例如
Student
Person
的一个子类,但是
Pair<Student>
并不是
Pair<Person>
的子类,他们抽象化后都是
Pair<T>


通配符类型

[code]Pair<? extends Employee>


表示任何泛型
Pair
类型,它的类型参数是
Employee
的子类,如
Pair<Manager>
,但不是
Pair<String>


假如要编写一个打印雇员的方法,例如

[code]public static void printBuddies(Pair<Employee> p){
    System.out.print(p.getFirst.getName());
}


正如泛型类型的继承规则,不能将
Pair<Manager>
传递给这个方法,这一点很受限,但是可以用通配符解决

[code]public static void printBuddies(Pair<? extends Employee> p )


类型
Pair<Manager>
Pair<? extends Employee>
的子类型

通配符的超类型限定

通配符限定与类型变量限定十分类似,但是,还有一个附加的能力,即可以指定一个超类型限定(supertype bound):

[code]? super Manager


这个通配符限制为
Manager
的所有超类型。

带有超类型限定的通配符,可以为方法提供参数,但不能使用返回值。例如,
Pair<? super Manager>
有方法

[code]void setFirst(? super Manager)


[code]? super Manager getFirst()


编译器不知道
setFirst
方法的确切类型,但是可以用任意
Manager
对象调用它,而不能用
Employee
对象调用。然而,如果调用
getFirst
,返回的对象类型就不会得到保证,只能把它赋给一个
Object


直观地讲,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取

无限定通配符

无限制的通配符
Pair<?>
看起来好像与原始的
Pair
类型一样,实际上有很大不同。类型
Pair<?>
有方法如下所示:

[code]? getFirst()
void setFirst(?)


getFirst()
的返回值只能赋给一个
Object
setFirst
方法不能被调用,甚至不能用
Object
调用
Pair<?>
Pair
本质的不同在于:可以用任意
Object
对象调用原始的
Pair
类的
setObject
方法

可以调用
setFirst(null)


为什么要使用这样的类型?例如,下面这个方法用来测试一个
pair
是否包含一个
null
引用,它不需要实际的类型。

[code]public static boolean hasNulls(Pair<?> p){
    return p.getFirst()==null||p.getSecond()==null;
}


通过将
hasNulls
转换成泛型方法,可以避免使用通配符类型

[code]public static <T> boolean hasNulls(Pair<T> p)


但是,带有通配符的版本可读性更强

通配符捕获

一个交换
pair
元素的方法

[code]public static void swap(Pair<?> P)


通配符不是类型变量,因此,不能再编写代码中使用“?”作为一种类型。也就是说下列代码是非法的

[code]? t = p.getFirst()  //ERROR
p.setFirst(p.getSecond())
p.setSecond(t)


在交换的时候必须临时保存第一个元素。不过我们可以写一个辅助方法
swapHelper


[code]public static <T> void swapHelper(Pair<T> p){
    T t = p.getFirst();
    p.setFirst(p.getSecond());
    p.setSecond(t);
}


swapHelper
是一个泛型方法,而
swap
不是,它具有固定的
Pair<?>
类型参数

现在可以用
swap
调用
swapHepler


[code]public static void swap{swapHepler(p);}


这种情况下,
swapHepler
方法的参数
T
捕获通配符。他不知道是哪种类型的通配符,但是,这是明确的类型,而且
<T>swapHepler
的定义只有在
T
指出类型时才有明确的含义

反射和泛型

现在,
Class
类是泛型的。例如,
String.class
实际上是一个也是唯一一个Class`类的对象。

类型参数十分有用,这是因为它允许
Class<T>
方法的返回类型更加具有针对性。下面
Class<T>
中的方法就是用了类型参数

[code]T newInstance()
T cast(Object obj)
T[] getEnumConstants()
Class<? super T> getSuperClass()
Constructor<T> getConstructor(Class ... param)
Constructor<T> getDeclaredConstructor(Class ... param)


newInstance
方法返回一个实例,这个实例所属的类由默认的构造器获得。他的返回类型目前声明为
T
,其类型与
Class<T>
描述的类相同,这样就免除了类型转换。

如果给定的类型确实是
T
的一个子类型,
cast
方法就会返回一个现在声明为类型
T
的对象,否则,抛出一个
BadCastException
异常

如果这个类不是
enum
类或者类型
T
的枚举值的数组,
getEnumConstans
方法将返回
null


最后,
getConstructor
getdeclaredConstructor
方法返回一个
Constructor<T>
对象。
Constructor
类也已经变成泛型,以便
newInstance
方法有一个正确的返回类型
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: