Java泛型

Java泛型

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

泛型程序设计(Generic programming)意味着编写的代码可以被很多不同类型的对象所重用。举个例子:利用泛型,我们可以不用为了聚集StringFile对象分别设计不同的类。

下面从类型参数的好处的角度来简单谈谈泛型程序设计机制的演化,这能帮助我们更好的理解泛型。

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

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

这种方法有两个问题:

  1. 当获取一个值时必须进行强制类型转换

    ArrayList files = new ArrayList();
    String fileName = (String) files.get(0);
    
  2. 这里没有错误检查,可以向数组列表中添加任何类的对象,对于这个调用,编译和运行都不会出错,但在其他地方,如果将get的结果强制类型转换为String类型,就会产生一个错误,因为添加的是个File对象,File对象无法转换为String对象:

    file.add(new File("..."));
    

于是泛型提供了一个更好的解决方案:类型参数(type parameters)ArrayList类有一个类型参数用来指示元素的类型,这使得代码具有更好的可读性,人们一看就知道这个数组列表中包含的是String对象:

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

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

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

2. 定义简单泛型类

一个泛型类(generic class)就是具有一个或多个类型变量的类。下面使用一个简单的Pair类作为例子:

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 this.first;
    }
    
    public T getSecond(){
        return this.second;
    }
    
    public void setFirst(T newValue){
        this.first = newValue;
    }
    
    public void setSecond(T newValue){
        this.second = newValue
    }
}

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

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

用具体的类型替换类型变量就可以实例化泛型类型,可以把结果想象成带有构造器的普通类:

Pair<String>();
Pair<String>(String, String);

程序示例:

public class PairTest1 {
    public static void main(String[] args) {
        String[] words = {"Marry", "had", "a", "little", "lamb"};
        Pair<String> mm = ArrayAlg.minmax(words);
        System.out.println("min = " + mm.getFirst());
        System.out.println("max = " + mm.getSecond());
    }
}

class ArrayAlg{
    /**
     *Gets the minimum and maximum of an array of Strings
     */
    public static Pair<String> minmax(String[] a){
        if(a == null || a.length == 0) return null;
        String min = a[0], max = a[0];
        for(int i = 1; i < a.length; i++){
            if(min.compareTo(a[i]) > 0) min = a[i];
            if(max.compareTo(a[i]) < 0) max = a[i];
        }
        return new Pair<>(min, max);
    }
}

3. 泛型方法

前面介绍了如何定义一个泛型类。实际上,还可以定义一个带有类型参数的简单方法:

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

泛型方法可以定义在普通类中,也可以定义在泛型类中。注意,类型变量放在修饰符(这里是public static)的后面, 返回类型的前面。

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

String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public");

实际上,方法调用中可以省略<String>类型参数,编译器有足够的信息能够推断出所调用的方法,也就是说,可以调用:

String middle = ArrayAlg.getMiddle("John", "Q.", "Public");

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

事实上,这个程序有个问题。在min方法的代码内部,变量smallest类型为T,这意味着它可以是任何一个类的对象,那么我们怎么确保T所属的类有compareTo方法呢?

解决这个问题的方案是将T限制为实现了Comparable接口(只含一个方法compareTo的标准接口)的类。可以通过对类型变量T设置限定(bound)实现这一点:

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

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

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

T extends Comparable & Serializable

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

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

程序示例:

import java.time.LocalDate;

public class PairTest2 {
    public static void main(String[] args) {
        LocalDate[] birthDays = {
                LocalDate.of(1906, 12,9),
                LocalDate.of(1815,12,10),
                LocalDate.of(1903, 12, 3),
                LocalDate.of(1910, 6, 22)
        };

        Pari<LocalDate> mm = ArrayAlg.minmax(birthDays);
        System.out.println("min = " + mm.getFirst());
        System.out.println("max = " + mm.getSecond());
    }
}

class ArrayAlg{
    /**
     * Gets the minimum and maximum of an array of objects of type T.
     */
    public static <T extends Comparable> Pair<T> minmax(T[] a){
        if(a == null || a.length == 0) return null;
        T min = a[0];
        T max = a[0];
        for(int i = 1; i < a.length; i++){
            if(min.compareTo(a[i]) > 0) min = a[i];
            if(max.compareTo(a[i]) < 0) max = a[i];
        }
        return new Pair<>(min, max);
    }
}

5. 泛型代码和虚拟机

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

(1)类型擦除

无论何时定义一个泛型类型,都自动提供一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型(无限定的变量用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 this.first;
    }
    
    public Object getSecond(){
        return this.second;
    }
    
    public void setFirst(Object newValue){
        this.first = newValue;
    }
    
    public void setSecond(Object newValue){
        this.second = newValue;
    }
}

这是因为T是一个无限定的变量,所以直接用Object替换。替换的结果是一个普通的类,就好像泛型引入Java语言之前已经实现的那样。

原始类型要用第一个限定的类型变量来替换,如果没有给定限定就要用Object替换。如上例所示。现在假定有一个不同的类型:

public class Interval <T extends Comparable & Serializable> implements Serializeble{
    private T lower;
    private T upper;
    ...
    private 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;
    ...
    private Interval(Comparable first, Comparable second){...}
}

注: 如果切换限定:class Interval<T extends Serializable & Comparable>, 那么原始类型用Serializable替换T,而编译器在必要时要向Comparable 插入强制类型转换。为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界列表的末尾。

(2)翻译泛型表达式

当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。例如下面这个语句序列:

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

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

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

当存取一个泛型域是也要插入强制类型转换。假设Pair类的first域和second域都是公有的,表达式Employee buddy = buddies.first也会在结果字节码中插入强制类型转换。

(3)翻译泛型方法

类型擦除也会出现在泛型方法中。程序员通常认为public static <T extends Comparable> T min(T[] a)是一个完整的方法族,而擦除类型之后,只剩下一个方法:public static Comparable min(Comparable[] a),此时类型参数T已经被擦除了, 只留下了限定类型Comparable

方法的擦除带来了两个复杂问题,看一看下面的示例:

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

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

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

这就很奇怪了,因为存在一个从Pair继承的setSecond方法,即:

public void setSecond(Object second)

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

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

在这里,希望对setSecond的调用具有多态性,并调用最合适的那个方法。由于pair引用DateInterval对象,所以应该调用DateInterval.setSeconde。问题在于类型擦除与 多态发生了冲突。要解决这个问题,就需要编译器在DateInterval类中生成一个桥方法(bridge method):

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

要想了解它的工作原理,要仔细跟踪下列语句的执行pair.setSecond(aDate)

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

DateInterval类中,有两个getSecond方法:

LocalDate getSecond() //define in DateInterval
Object getSecond() //override the method defined in Pair to call the first method

我们在程序中不能这样编写java代码,因为具有相同参数类型的两个方法是不合法的。它们都没有参数。但是在虚拟机中可以用参数类型和返回类型确定一个方法。因此编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确的处理这一情况。

总之,需要记住有关Java泛型转换的事实:

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

6. 约束和局限性

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

例如,没有Pair<double>,只有Pair<Double>。原因就在于类型擦除。擦除之后,Pair类含有Object类型的域,而Object不能存储double值。

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

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

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

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

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

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

(4) Varargs 警告

这里讨论一个向参数个数可变的方法传递一个泛型类型的实例

考虑下面这个简单的方法,它的参数个数是可变的:

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

现在考虑以下调用:

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

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

可以采用两种方法来抑制这个警告。一种方法是为包含addAll调用的方法增加注解@SuppressWarnings("unchecked")。或者在Java SE 7中,还可以用@SafeVarargs直接标注addAll方法:

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

这样就可以提供泛型类型来调用这个方法了。

(5) 不能实例化类型变量

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

(6) 不能构造泛型数组

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

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

(9) 可以消除对受查异常的检查

(10) 注意擦除后的冲突

7. 泛型类型的继承规则

在使用泛型类时,需要了解一些有关继承和子类型的准则。考虑一个类和一个子类,如EmployeeManagerPair<Manager>Pair<Employee>的一个子类吗?不是!

事实上,无论ST 有什么联系,Pair<S>Pair<T>没有任何联系。

需要说明的是,泛型类可以扩展或者实现其他的泛型类。例如ArrayList<T>类实现List<T>接口。

8. 通配符类型

(1) 通配符概念

通配符类型中,允许类型参数变化,例如通配符类型Pair<? extends Employee>表示任何泛型Pair类型, 它的类型参数是Employee 的子类, 如Pair<Manager>,但不是Pair<String>

假设要编写一个打印雇员对的方法:

public static void printBuddies(Pair<Employee> p){
    Employee first = p.getFirst();
    Employee second = p.getSecond();
    System.out.println(first.getName() + "and" + second.getName() + " are buddies");
}

不能将Pair<Manager>传递给这个方法,这一点很受限制。解决的办法也很简单,使用通配符类型:

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

(2) 通配符的超类型限定

通配符限定与类型变量限定十分类时,但还有一个附加的能力,即可以指定一个超类型限定(supertype bound),例如? super Manager

这个通配符限制为Manager的所有超类型。带有超类型限定的通配符可以为方法提供参数,但不能使用返回值。

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

(3) 无限定通配符

还可以使用无限定的通配符,例如Pair<?>。初看起来,这好像与原始的Pair类型一样。实际上,有很大的不同。类型Pair<?>有一下方法:

? getFirst()
void setFirst(?)

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

这样脆弱的类型对于许多简单的操作非常有用。例如下面这个方法将用来测试一个Pair是否包含一个null引用,它不需要实际的类型。

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

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

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

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

(4) 通配符捕获

编写一个交换成对元素的方法:

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调用swapHelper

public static void swap(Pair<?> p){
    swapHelper(p);
}

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

通配符捕获只有在有许多限制的情况下才是合法的。编译器必须能够确信通配符表达的是单个、 确定的类型。 例如, ArrayList<Pair<T>>中的T 永远不能捕获ArrayList<Pair<?>>中的通配符。

9. 反射和泛型

(1)泛型Class类

Class类是泛型的,例如,String.class实际上是一个Class<String>类的对象(事实上,是唯一的对象)。

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

T newInstance()
T cast(Object obj)
T[] getEnumConstants()
Class<? super T> getSuperclass()
Constructor<T> getConstructor(Class... parameterTypes)
Constructor<T> getDeclaredConstructor(Class... parameterTypes)

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

(2) 使用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),makePair方法的类型参数TEmployee匹配,并且编译器可以推断出这个方法将返回一个Pair<Employee>

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

推荐阅读更多精彩内容