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

泛型程序设计

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

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

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

ArrayList<T> list = new ArrayList<T>();

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

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

ArrayList<T> list = new ArrayList<>();

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

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

定义简单泛型类

​ 一个泛型类(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 newValue){first =newValue;}
   public void setSecond(T newValue){second = newValue;}
}

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

public class Pair <T,U> {...}

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

泛型方法

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

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

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

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

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

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

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

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

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

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

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

类型变量的限定

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

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设置限定实现这一点

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

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

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

<T extends BoundingType>

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

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

T extends Comparable & Serializable

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

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

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

约束与局限性

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

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

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

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

    例如

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

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

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

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

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

  3. 不能创建参数化的数组

    例如

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

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

    Object[] objarray = table;

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

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

  4. Varargs警告

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

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

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

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

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

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

  5. 不能实例化类型变量

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

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

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

    first = T.class.newInstance();       //ERROR

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

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

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

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

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

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

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

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

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

    public class Pair<T>{
        private T pair1;    //ERROR
    
       public T getPair1(){ //ERROR
            return pair1; 
    }
    }

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

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

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

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

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

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

  8. 擦除后的冲突

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

泛型类型的继承规则

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

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

通配符类型

Pair<? extends Employee>

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

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

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

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

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

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

通配符的超类型限定

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

? super Manager

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

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

void setFirst(? super Manager)
? super Manager getFirst()

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

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

无限定通配符

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

? getFirst()
void setFirst(?)

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

可以调用setFirst(null)

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

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

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

public static <T> boolean hasNulls(Pair<T> p)

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

通配符捕获

一个交换pair元素的方法

public static void swap(Pair<?> P)

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

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

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

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

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

现在可以用swap调用swapHepler

public static void swap{swapHepler(p);}

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

反射和泛型

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

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

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

最后,getConstructorgetdeclaredConstructor方法返回一个Constructor<T>对象。Constructor类也已经变成泛型,以便newInstance方法有一个正确的返回类型

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值