Java核心技术-泛型程序设计

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

泛型对于集合类尤其有用

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

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

1.1 类型参数的好处

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

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

这种方法有两个问题:

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

泛型提供了一个更好的解决方案:类型参数,ArrayList类有一个类型参数用来指示元素的类型:

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

这使得代码具有更好的可读性

Java SE 7及以后的版本中可以在构造函数中省略泛型类型:

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

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

编译器也可以很好的利用这个信息,当获取一个值时不需要进行强制类型转换,编译器就知道返回值类型为String。

编译器还知道ArrayList<String>中add方法有一个类型为String的参数,并且编译器会进行检查,避免插入错误类型的对象

类型参数的魅力在于:使得程序具有更好的可读性和安全性。

1.2 谁想称为泛型程序员

使用一个像ArrayList的泛型类很容易,但是实现一个泛型类并没有那么容易。

例如,ArrayList类有一个addAll方法用来添加另一个集合的全部元素。可以将ArrayList<Manage>中的所有元素添加到ArrayList<Employee>中去,但是反过来就不行。如果只能允许前一个调用,而不能允许后一个调用呢?(通配符类型)

2 定义简单泛型类

一个泛型类就是具有一个或多个类型变量的类。

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

这里引入了一个类型变量T,用尖括号(<>)括起来,并放在类名的后面。

泛型类可以有多个类型变量,例如:

public class Pair<T,U>

类定义中的类型变量指定方法的返回类型以及域和局部变量的类型。

用具体的类型替换类型变量就可以实例化泛型类型,可以将结果想象成带有构造器的普通类,换句话说,泛型类可看作普通类的工厂

3 泛型方法

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

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

类型变量放在修饰符的后面,返回类型的前面。

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

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=1;i<a.length;i++)
            if(smallest.compareTo(a[i])>0) smallest=a[i];
        return smallest;
    }
}

这里需要将T限制为实现了Comparable接口的类。可以通过类型变量T设置限定实现这点:

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

一个类型变量或通配符可以有多个限定(可以有多个接口超类型,但至多有一个类):

T extends Comparable & Serializable

限定类型用“&”分隔,用逗号来分隔类型变量,如果用一个类作为限定,它必须是限定列表中的第一个。

5 泛型代码和虚拟机

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

5.1 类型擦除

无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。

擦除类型变量,并替换为限定类型(无限定的变量用Object)

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

public class Pair
{
    private Object first;
    private Object second;
    
    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(Object newValue) {first=newValue;}
    public void setSecond(Object newValue) {second=newValue;}
}

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

假定有一个泛型类Interval:

public class Interval<T extends Compareble & Serializable> implements Serializable
{
    private T lower;
    private T upper;
    ...
    public Interval(T first,T second)
    {
        if(first.compareTo(second)<=0){lower=first;upper=second;}
        else{lower=second;upper=first;}
     }
}

原始类型Interval如下:

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

注:如果切换限定:class Interval<T extends Serializable & Comparable>,则原始类型用Serializable替换T,而编译器在必要时要插入Comparable强制类型转换。

为了提高效率,应该将标签接口(没有方法的接口)放在边界列表的末尾。

5.2 翻译泛型表达式

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

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

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

5.3 翻译泛型方法

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

有一个日期区间是一对LocalDate对象,并且需要覆盖Pair中的setSecond这个方法来确保第二个值永远不小于第一个值。

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

但是类型擦除后变成:

class DateInterval extends Pair
{
    public void setSecond(LocalDate second){...}
    ...
}

发现除了原先覆盖超类中的setSecond(LocalDate second),此时又多了一个setSecond(Object second)方法,由于方法参数不同,所以这两个显然是不同的方法。

考虑下面的调用:

DateInterval interval=new DateInterval(...);
Pair<LocalDate> pair=interval;
pair.setSecond(aDate);

这里,希望对setSecond的调用具有多态性,并调用最合适的方法。但由于类型擦除与多态发生了冲突。要解决这个问题需要在DateInterval中生成一个桥方法(再覆盖超类的setSecond(Object second)方法):

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

假设DateInterval也覆盖了超类的getSecond方法:

class DateInterval extends Pair<LocalDate>
{
    public LocalDate getSecond(){
        return (Date)super.getSecond().clone(); 
    }
}

由于类型擦除,此时DateInterval中有两个getSecond方法:

LocalDate getSecond()
Object getSecond()

显然,在Java中,具有相同参数类型的两个同名方法是不合法的,但是,在虚拟机中,用参数类型和返回类型确定一个方法。因此,编译器可能产生两个仅返回类型不同的方法字节码,但虚拟机能够正确处理这一情况。我们称这种情况为这两种方法具有协变的返回类型。

Java泛型转换的事实:

*虚拟机中没有泛型,只有普通的类和方法
*所有的类型参数都用它们的限定类型替换
*桥方法被用来保持多态
*为保持类型安全性,必要时插入强制类型转换

5.4 调用遗留代码

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

可以使用@SuppressWarnings("unchecked")消除警告

6 约束与局限性

下面将讨论使用Java泛型时的一些限制,大多数限制是由类型擦除引起的。

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

可以使用基本类型的包装类,如Pair<Double>,擦除之后Pair类可以含Object类型的域,而Object不能存储double。

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

例如:

if(a instanceof Pair<String>

事实上仅仅测试a是否是任意类型的一个Pair

同样:

Pair<String> stringPair=...;
Pair<Employee> employeePair=...;
if(stringPair.getClass()==employee.getClass())  //they are equal
两次调用getClass(),返回的都是Pair.class

6.3 不能创建参数化类型的数组

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

因为类型擦除后允许:

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

如果需要收集参数化类型对象,只有一种安全而有效的方法:使用ArrayList:

ArrayList<Pair<String>>

6.4 Varargs警告

向参数个数可变的方法传递一个泛型类型的实例:

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

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

可以使用注解@SuppressWarnings("unchecked")或@SafeVarargs抑制这个警告。

6.5 不能实例化类型变量

不能使用像new T(...)、 new T[...]或T.class这样的表达式中的类型变量:

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

最好的解决办法是让调用者提供一个构造器表达式:

Pair<String> p=Pair.makePair(String::new);

makePair方法接收一个Supplier<T>,这是一个函数式接口,表示一个无参数而且返回类型为T的函数:

public static <T> Pair<T> makePair(Supplier<T> constr)
{
    return new Pair<>(constr.get(),constr.get());
}

constr.get()方法是Supplier<T>函数执行构造器引用的方法。

比较传统的方法是使用反射来构造泛型对象,不能调用:

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

T.class会被擦除成Object.class

必须像下面这样设计API以便得到一个Class对象:

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

然后这样调用:

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

6.6 不能构造泛型数组

如同不能构造泛型实例一样,也不能实例化数组。

最好让用户提供一个数组构造器表达式:

String[] ss=ArrayAlg.minmax(String[]::new,"Tom","Dick","Harry");

利用数组构造器表达式实现:

public static <T extends Comparable> T[] 
    minmax(IntFunction<T[]> constr,T... a)
{
    T[] mm=constr.apply(2);
}

利用反射实现:

public static <T extends Comparable> T[]  minmax(T... a)
{
    T[] mm=Array.newInstance(a.getClass().getComponentType(),2);
}

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

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

private static T singleInstance;

public static T getSingleInstance(){...}

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

泛型类扩展Throwable都是不合法的:

public class Problem<T> extends Exception{...] //Error

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

catch(T e) //Error

6.9 可以消除对受查异常的检查

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

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

6.10 注意擦除后的冲突

有这样一个泛型类:

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

考虑这样一个问题:

Pair<T>泛型擦除后有两个equals方法:

boolean equals(Object) //define in Pair<T>

boolean equals(Object)//inherited from Object

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

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

例如,下述代码是非法的:

class Employee implements Comparable<Employee>{...}

class Manager extends Employee implements Comparable<Manager>{...}

Manager会实现Comparable<Employee>Comparable<Manager>,这是同一接口的不同参数化。

其原因非常微妙,有可能与合成桥方法产生冲突。

7 泛型类型的继承规则

在使用泛型时,需要了解一些有关继承和子类型的准则。

如Manager是Employee的子类,那么Pair<Manager>Pair<Employee>的一个子类吗? 答案是:“不是”

规则:无论S与T有什么联系。通常,Pair<S>与Pair<T>没有什么联系。

在这里插入图片描述

8 通配符类型

固定的泛型类型系统使用起来并没有那么令人愉快,Java设计者发明了一种巧妙的“解决方案”:通配符类型。

泛型通配符(?)和类型变量(T)的区别——任何泛型类型(泛型变量声明中)vs某一种泛型类型(泛型类实现中)

?可以在声明变量时使用,例如:

Pair<Manager> managerBuddies=new Pair<>(ceo,cfo);

Pair<? extends Employee> wildcardBuddies=managerBuddies;

8.1 通配符概念

通配符类型中,允许类型参数变化。

例如,通配符类型:

Pair<? extends Employee>

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

正如前面说到的问题:不能将Pair<Manager>传递给Pair<Employee>,解决方法是:使用通配符类型。

在这里插入图片描述

Pair<? extends Employee>中的方法似乎是这样:

? extends Employee getFirst()

void setFirst(? extends Employee)

这样将不能调用setFirst方法,编译器只知道需要某个Employee的子类型,但不知道具体是什么类型,所以拒绝传递任何特定的类型。

使用getFirst就不存在这个问题:将getFirst的返回值赋给一个Employee的引用完全合法

这就是引入有限定的通配符的关键之处。

8.2 通配符的超类型限定

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

? super Manager

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

引入super的原因:

带有超类型限定的通配符的行为之前介绍的的Pair<? extends Employee>行为恰恰相反,可以为方法提供参数,但不能使用返回值

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


在这里插入图片描述

8.3 无限定通配符

还可以使用无限定的通配符,如:Pair<?>,它有以下方法:

? getFirst()
void setFirst()

getFirst的返回值只能赋给一个Object。setFirst方法不能被调用

8.4 通配符捕获

通配符不是类型变量,因此不能在编写代码时使用“?”作为一种类型,如:

? t=p.getFirst();

如何临时保存一个通配符类型变量,需要一个辅助方法,如下:

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

这里使用swapHelper方法的参数T捕获通配符。

通配符捕获只有在有许多限制的情况下是合法的,编译器必须确定通配符表达的是单个、确定的类型。

如:ArrayList<Pair<?>>中的通配符就不能通过ArrayList<Pair<T>>捕获。数组列表可以保存两个Pair<?>,分别针对?的不同类型。

9 反射和泛型

反射允许你在运行时分析任意对象。但如果对象是泛型类的实例,关于泛型类型参数则得不到太多信息,因为它们会被擦除。

下面将了解利用反射可以获得泛型类的什么信息。

9.1 泛型Class类

现在,Class类是泛型的,类型参数十分有用,这是因为它允许Class<T>方法的返回类型更加具有针对性

Class<T>中的方法就使用了类型参数:

T newInstance()——返回一个实例,这个实例所属的类由默认的构造器获得。
T cast(Object obj)——如果给定的类型确实是T的一个子类型,cast方法就会返回一个现在声明为类型T的对象

T[] getEnumConstants()——如果这个类不是enum类或类型T的枚举值的数组,返回null

Class<? super T>get SuperClass()

Constructor<T> getConstructor(Class... parameterTypes)

Constructor<T> getDeclaredConstructor(Class... parameterTypes)

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

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

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

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

9.3 虚拟机中的泛型类型信息

Java泛型的卓越特性之一是在虚拟机中泛型类型的擦除。令人感到奇怪的是,擦除的类仍然保留一些泛型祖先的微弱记忆。

例如,原始的Pair类知道源于泛型类Pair<T>,但不知道具体的类型变量。类似地,方法:

public static Comparable min(Comparable[] a)

这是一个泛型方法的擦除:

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

可以使用反射API来确定:

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

可以重新构造实现者声明的泛型类以及方法中的所有内容。但是,不会知道对于特定的对象或方法调用,如果解释类型参数。

为了表达泛型类型声明,使用java.lang.reflect包中提供的接口Type,这个接口包含下列子类型:

*Class类,描述具体类型
*TypeVariable接口,描述类型变量(如T extends Comparable<? super T>)
*WildcardType接口,描述通配符(如? super T)
*ParameterizedType接口,描述泛型类或接口类型(如Comparable<? super T>)
*GenericArrayType接口,描述泛型数组(如T[])

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容