Java实践(三)---泛型程序设计

使用泛型机制编写的程序代码要比那些杂乱地使用Object变量,然后进行强制类型转换的代码具有更好的安全性和可读性,泛型对于集合尤其有用

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

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

在Java中增加泛型类之前,泛型程序设计是用继承实现的,如ArrayList类:

public class ArrayList
{
    private Object[] elementData;
    ...
    public Object get(int i){...}
    public void add(Object o){...}
}

这样实现由2个问题:

1.当获取一个值时,必须进行强制类型转换
2.没有错误检查,可以向数组列表中添加任何类对象

泛型提供了一个更好的解决方案:类型参数(type parameters)ArrayList类有一个类型参数来指示元素的类型:ArrayList<String> files = new ArrayList<String>();,这使得代码具有更好的可读性

构造函数中可以省略泛型类型ArrayList<String> files = new ArrayList<>();,省略的类型可以从变量的类型推断得出

类型参数使得程序具有更好的可读性和安全性

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

Pair类引入一个类型变量T,用尖括号<>括起来,放在类名的后面,泛型类可以有多个类型变量如:public class Pair<T,U>{...},类定义中的类型变量指定方法的返回类型以及域和局部变量的类型如:private T first;

类型变量使用大写形式,使用变量E表示集合的元素类型,K和V表示关键字和值的类型,T,U,S表示任意类型
用具体的类型替换类型变量就可以实例化泛型类型,如:Pair<String>,可以将结果详细成带有构造器的普通类;泛型可以看做是普通的工厂

3.泛型方法

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

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

这是定义在普通类中的一个泛型方法,类型变量放在修饰符(public static)的后面,返回类型的前面

泛型方法可以定义普通类中,也可以定义在泛型类中
当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型

4.类型变量的限定

类或者方法需要对类型变量加以限定
例如

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

这样泛型的min方法只能被实现了Comparable接口的类(如Sting、Data等)的数组调用

<T extends BoundingType>

表示T应该是绑定类型的子类型,T和绑定类型可以是类,也可以是接口

一个类型变量或通配符可以有多个限定,例如:T extends Comparable & Serializable,限定类型用“&”分隔,逗号用来分隔类型变量

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

5.泛型代码和虚拟机

虚拟机没有泛型类型对象—–所有对象都属于普通类;无论何时定义一个泛型类,都自动提供一个相应的原始类型(raw type),原始类型的名字就是删去类型参数后的泛型类型名;擦除(erased)类型变量,并替换为限定类型(无限定的变量用Object)

在程序中可以包含不同类型的Pair,例如:Pair<String>或Pair<GregorianCalendar>擦除类型后就变成原始的Pair类型了
原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换

1.翻译泛型表达式

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

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

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

1.对原始方法Pair.getFirst的调用
2.将返回的Object类型强制转换为Employee类型

当存取一个泛型域时,也要强制类型转换

2.翻译泛型方法

类型擦除也会出现在泛型方法中
一个完整的方法族如下:

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

擦除类型之后,只剩下一个方法:

public static Comparable min(Comparable[] a)

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

方法的擦除带来2个复杂的问题:

class DataInterval extends Pair<Date>
{
    public void setSecond(Data second)
    {
        if(second.compareTo(getFirst()) >= 0 )
        {
            super.setSecond(second);
        }
    }
    ...
}   

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

class DataInterval extends Pair
{
    public void setSecond(Data second){...}
    ...
}

令人感到奇怪的是,存在另一个从Pair继承的setSecond方法,即:

public void setSecond(Object second)

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

DateInterval interval = new DateInterval(...);
Pair<Date> pair = interval ;// ok  assignment to superclass
pair.setSecond(aDate);

希望对setSecond的调用具有多态性,并调用最合适的那个方法,由于pair引用DateInterval对象,所以应该调用DateInterval.setSecond,问题在于类型擦除与多态发生了冲突,要解决这个问题,就需要编译器在DateInterval类中生成一个桥方法(bridge method):public void setSecond(Object second){setSecond(Date) second},需要了解它的工作过程,要跟踪下列语句:

pair.setSecond(aDate)

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

在擦除类型后会得到2个方法,但是在虚拟机中,用参数类型和返回类型确定一个方法,因此,编译器可能产生2个仅返回类型不同的方法字节码,虚拟机能够正确的处理这一情况

关于Java泛型转换的事实:

1.虚拟机中没有泛型,只有普通的类和方法
2.所有的类型参数都用他们的限定类型替换
3.桥方法被合成来保持多态
4.为保持类型安全性,必须插入强制类型转换

3.调用遗留代码

设计Java泛型类型时,主要目标是允许泛型代码和遗留代码之间能够互操作

6.约束与局限性

使用Java泛型时需要考虑的一些限制,大多数限制都是由类型擦除引起的

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

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

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

虚拟机中的对象总有一个特定的非泛型类型,因此,所有的类型查询只产生原始类型

无论何时使用instanceof或涉及泛型类型的强制类型转换表达式都会看到一个编译器警告,同样的道理,getClass方法总是返回原始类型

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

不能实例化参数化类型的数组,例如:

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

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

Object[] objarray = table;

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

 objarray[0] = "Hello";//ERROR---component type is Pair

不过对于泛型类型,,擦除会使这种机制无效,以下赋值:

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

能够通过数组存储检测,不过仍然会导致一个类型错误

只是不允许创建这些数组,而声明类型为Pair[]的变量仍是合法的,不过不能用new Pair[10]初始化这个变量

4.Varrags警告

Java不支持泛型类型的数组;
如何向参数个数可变的方法传递一个泛型类型的实例:

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);

为了调用这个方法,Java虚拟机必须建立一个Pair<String>数组,这就违反了前面的规则,不过对于这种情况,规则有所放松,你只会得到一个警告, 而不是错误
可以采用2种方法来抑制这个警告:

1.为包含addAll调用的方法增加标注@SuppressWarning(“Unchecked”)
2.用@SafeVarargs直接标注addAll方法

@SafeVarargs
public static <T> void addAll(Collection<T> coll, T... ts)

现在就可以提供泛型类型来调用这个方法了,对于只需要读取参数数组元素的所有方法,都可以使用这个标注,这仅限于最常见的用例
可以使用@SafeVarargs标注来消除创建泛型数组的有关限制

5.不能实例化类型变量

不能使用像new T(…),new T[…]或T.class这样的表达式中的类型变量,类型擦除将T改变成Object,本意肯定不希望调用new Object()

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

不能再静态域或方法中引用类型变量

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

既不能抛出也不能捕获泛型类对象,甚至泛型类扩展Throwable都是不合法的,并且在catch子句中不能用类型变量

但是在异常规范中使用类型变量是允许的:public static <T extends Throwable> void doWork(T t) throws T

Java异常处理的一个基本原则是,必须为所以已检查异常提供一个处理器,不过可以利用泛型消除这个限制,关键在于以下方法:

@SuppressWarning("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T
{
    throw(T) e;
}

8.注意擦除后的冲突

当泛型类型被擦除时,无法创建引发冲突的条件,如下:

public class Pair<T>
{
    boolean equals(T value)
    {
        return first.equals(value) && second.equals(value);
        ...
    }

考虑一个Pair<String>从概念上讲,它有2个equals方法:

1.boolean equals(String)
2.boolean equals(Object)

但是直觉把我们引入歧途,方法擦除 boolean equals(T)就是boolean equals(Object)与Object.equals方法发生了冲突,当然补救的办法是重命名引发错误的方法
泛型规范说明还提到另外一个原则,要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为2个接口类型的子类,而这2个接口是同一接口的不同参数化

7.泛型类型的继承规则

无论S与T是什么联系,通常,Pair<S>Pair<T>没有什么联系;这一限制看起来过于严格,但对于类型安全非常必要

注意泛型和Java数组之间的重要区别
可以将一个Manager[]数组赋给一个类型为Employee[]的变量:

Manager[] managerBuddies = {ceo,cfo};
Employee[] employeeBuddies = managerBuddies;

数组带有特别的保护,如果试图将一个低级别的雇员存储到employeeBuddies[0],虚拟机会抛出ArrayStoreException

永远可以将参数化类型转换为一个原始类型,例如:Pair<Employee>是Pair的一个子类,在与遗留代码衔接时,这个转换非常重要,但是转换成原始类型之后会产生类型错误

泛型类可以扩展或实现其他的泛型类,如下图所示:

这里写图片描述

                           泛型列表类型中子类型间的联系

8.通配符类型

Pair<? extends Employee>

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

1.通配符的超类限定

通配符限定与类型变量限定十分相似,但是,还有一个附加的能力,即可以指定一个超类型限定(supertype bound),如下所示:? super Manager;这个通配符限制为Manager的所有超类型

带有超类型通配符的行为可以为方法提供参数,但是不能使用返回值,如:
Pair<? super Manager>有方法:
void setFirst(? super Manager)
? super Manager getFirst()

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

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

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

无论如何传递一个T类型的对象给compareTo方法都是安全的

2.无限定通配符

无限定的通配符,如:

Pair<?>

有如下方法:

getFirst()
void setFirst(?)

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

3.通配符捕获

编写一个交换一个pair元素的方法:public static void swap(Pair<?> p),通配符不是类型变量,因此,不能再编写代码时使用“?”作为一种类型

可以编写一个辅助方法swapHelper来解决这个问题,如下:

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

swapHelper是一个泛型方法,而swap不是,它具有固定的Pair<?>类型的参数,现在可以由swap调用swapHelper:public static void swap(Pair<?> p){swapHelper(p)},在这种情况下,swapHelper方法的参数T捕获通配符

通配符捕获只有在有许多限制的情况下才是合法的,编译器必须能够确信通配符表达的是单个、确定的类型,例如:ArrayList<Pair<T>>中的T用于不能捕获ArrayList<Pair<?>>中的通配符,数组列表可以保存2个Pair<?>分别针对?的不同类型

9.反射和泛型

现在,Class类是泛型的,例如,String.class实际上是一个Class<String>类对象(事实上是唯一的对象)
类型参数十分有用,这是因为它允许Class<T>方法返回类型更加具有针对性

1.使用Class<T>参数进行类型匹配

有时,匹配泛型方法中的Class<T>参数的类型变量很有实用价值,例如:

public static <T> Pair<T> makePair(Class<T> c)throws InstantiationException,IllegalAccessException
{
    return new Pair<>(c.newInstance(),c.newInstance());
}

如果调用makePair(Employee.class),Employee.class是类型Class<Employee>的一个对象,makePair方法的类型参数T同Employee匹配,并且编译器可以推断出这个方法将返回一个Pair<Employee>

2.虚拟机中的泛型类型信息

Java泛型的卓越特性之一是在虚拟机中泛型类型的擦除,例如:

public static Comparable min(Comparable[] e)

这是一个泛型方法的擦除

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

可以使用反射API来确定:

1.这个泛型方法有一个叫做T的类型参数
2.这个泛型参数有一个子类型限定,其自身又是一个泛型类型
3.这个限定类型有一个通配符参数
4.这个通配符参数有一个超类型限定
5.这个泛型方法有一个泛型数组参数

需要重新构造实现者声明的泛型类型以及方法中的所有内容

为了表达泛型类型声明,Java SE 5.0 在java.lang.reflect包中提供了一个新的接口Type,这个接口包含下列子类型:

1.Class类:描述具体类型
2.TypeVariable接口:描述类型变量(如T extends Comparable<? super T>)
3.WildcardType接口:描述通配符(如?super T)
4.ParamenterizedType接口:描述泛型类型或接口类型(如 Comparable<? super T>)
5.GenericArrayType接口:描述泛型数组(如T[])

最后4个是接口,虚拟机将实例化实现这些接口的适当的类

下图是继承层次:
这里写图片描述
Type类和它的后代

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值