Java基础-泛型的使用和原理

一 泛型概述

        在Java SE5.0中增加泛型机制的主要原因是为了满足在1999年制定的最早的Java规范需求之一(JSR 14)。专家组花费了5年左右的时间用来定义规范和测试实现。

        泛型正是我们需要的程序设计手段。使用泛型机制编写的程序代码要比那些杂乱的使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。泛型对于集合类尤其有用,例如,ArrayList就是一个无处不在的集合类。

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

        泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用。例如,我们并不希望为聚集String和File对象分别设计不同的类。实际上,也不需要这么做,因为一个ArrayList类可以聚集任何类型的对象。

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

public class ArrayList
{
    private Object[] elementData;
    
    ....

    public Object get(int i){}

    public void add(Object o){}
}

        这样的实现有两个问题。当获取一个值时必须进行强制类型转换。

ArrayList files = new ArrayList();

String filename = (String)files.get(0);

        此外,这里没有错误检查。可以向数组列表中添加任何类的对象。

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

        对于这个调用,编译和运行都不会出错。然而在其他地方,如果将get的结果强制转换为String类型,就会产生一个错误。

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

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

        这使得代码具有更好的可读性。人们一看就知道这个数组列表包含的是String对象。

【注意】在Java SE7及以后的版本中,构造函数中可以省略泛型类型:

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

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

        编译器也可以很好的利用这个信息,当调用get的时候,不需要进行强制类型转换,编译器就知道返回值类型为String,而不是Object:

String filename = files.get(0);

        编译器还知道ArrayList<String>中的add方法有一个类型为String的参数。这将比使用Object类型的参数安全一下,现在,编译器可以进行检查,避免插入错误类型的对象。

        使用像ArrayList的泛型类很容易。大多数Java程序员都使用ArrayList<String>这样的类型,就好像它们已经构建在语言之中,像String[]数组一样。

        但是,实现一个泛型类并没有那么容易。对于类型参数,使用这段代码的程序员可能想要内置所有的类。他们希望在没有过多的限制以及混乱的错误消息的状态下,做所有的事情。因此,一个泛型程序员的任务就是预测出所有类的未来可能有的所有用途。

        这一任务难到什么程度呢?下面是标准类库的设计者们肯定产生争议的一个典型问题。ArrayList类有一个方法addAll()用来添加另一个集合的全部元素。程序员可能想要将ArrayList<Manager>中的所有元素添加到ArrayList<Employee>中去。然而,反过来就不行了。如果只能允许前一个调用,而不允许后一个调用呢?Java语言的设计者发明了一个具有独创性的新概念,通配符类型,它解决了这个问题。通配符类型非常抽象,然后它们能让库的构建者编写出尽可能灵活的方法。

        泛型程序设计分为3个能力级别。基本级别是,仅仅使用泛型类-典型的是像ArrayList这样的集合-不必考虑他们工作方法与原因。大多数应用程序员将会停留在这一级别上,直到出现了什么问题。当把不同的泛型类混合在一起时,或是在与对类型参数一无所知的遗留的代码进行衔接时,可能会看到含糊不清的错误消息。如果这样的话,就需要学习Java泛型来系统的解决这些问题,而不要胡乱地猜测。

三 定义简单泛型类

        一个泛型类就是具有一个或多个类型变量的类。本节使用一个简单的Pair类作为例子。对于这个类来说,我们只关注泛型,而不会为数据存储的细节烦恼。下面是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 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>{...}

        类定义中的变量类型制定方法的返回类型以及域和局部变量的类型。例如:

private T first;

【注意】:类型变量使用大写形式,且比较短,这是很常见的。在Java库中,使用变量E表示集合元素类型,K和V分别表示类的关键字与值的类型。T(需要时还可以用临近的字母U和S)表示“任意类型”。

        用具体的类型替换类型变量就可以实例化泛型类型,例如:

Pair<String>

可以将结果想象成带有构造器的普通类:

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

和方法:

String getFirst()
String getSecond()
void setFirst(String param)
void setSecond(String param)

换句话说,泛型类可以看作普通类的工厂。

【例3.1】

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

class ArrayA1g{
    public static Pair<String> minmax(String[] a){
        if(a == null || a.length ==0) return null;
        String min = a[0];
        String max = a[0];
        for (int i = 0; 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);
    }
}

        代码解析:静态的minmax()方法遍历了数组并同时计算出最小值和最大值。它用一个Pair对象返回了两个结果。回想一个compareTo()方法比较两个字符串,如果字符串相同则返回0;如果按照字母顺序,第一个字符串比第二个字符串靠前,就返回负值,否则返回正值。

四 泛型方法

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

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

        这个方法是在普通类中定义的,而不是在泛型类中定义的。然而,这是一个泛型方法,可以从尖括号和类型变量看出这一点。注意,类型变量放在修饰符(这里是public static)的后面,返回类型的前面。

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

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

public static void main(String[] args) {
        String middle = ArrayA2g.<String>getMiddle("John","Q.","Public");
        System.out.println(middle);
    }

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

    public static void main(String[] args) {
        String middle = ArrayA2g.getMiddle("John","Q.","Public");
        System.out.println(middle);
    }

        几乎在大多数情况下,对于泛型方法的类型引用没有问题。偶尔,编译器也会提示错误,此时需要解译报错信息。

五 类型变量的限定

        有时,类或方法需要对类型变量加以约束。下面是一个典型的例子。我们要计算数组中的最小元素:

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

        注意看,if(minValue.compareTo(a[i]) > 0) minValue = a[i];这行代码有一个问题。请看一下min方法内部,变量minValue类型为T,这意味着它可以是任何一个类的对象。怎么才能确信T所属的类有compareTo方法呢?

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

class ArrayA3g{
    public static<T extends Comparable> T min(T[] a){
        if(a == null || a.length == 0) return null;
        T minValue = a[0];
        for (int i = 0; i < a.length; i++) {
            if(minValue.compareTo(a[i]) > 0) minValue = a[i];

        }
        return minValue;
    }
}

        实际上Comparable接口本身就是一个泛型类型。目前,我们忽略其复杂性以及编译器产生的警告,后续讨论了如何Comparable接口中适当地使用类型参数。

        现在,泛型的min方法只能被实现了Comparable接口的类(如String,Date等)的数组调用。有人或许会感到奇怪,为什么使用关键词extends而不是implements?毕竟Comparable是一个接口。下面的符号 <T extends BoundingtType> 表示T应该是绑定类型的子类型。T和绑定类型可以是类,可以是接口。选择关键字extends的原因是更接近子类的概念,并且Java的设计者也不打算在语言中再添加一个新的关键词。一个类型变量或通配符可以有多个限定,例如:

T extends Comparable & Serializable,限定类型用“&”分隔,而逗号用来分隔类型变量。

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

        【例3.2】重新编写了一个泛型方法minmax,计算泛型数组的最大值和最小值,并返回Pair<T>

public class PairTest2 {
    public static void main(String[] args) {
        GregorianCalendar[] birthdays = {
                new GregorianCalendar(1906, Calendar.DECEMBER,9),
                new GregorianCalendar(1815,Calendar.DECEMBER,10),
                new GregorianCalendar(1903,Calendar.DECEMBER,3),
                new GregorianCalendar(1910,Calendar.JUNE,22),
        };
        Pair<GregorianCalendar> minmax = ArrayB1g.minmax(birthdays);
        System.out.println("min = " + minmax.getFirst().getTime());
        System.out.println("max = " + minmax.getSecond().getTime());
    }
}

class ArrayB1g{

    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 = 0; 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);
    }

}

五 泛型代码和虚拟机

        虚拟机没有泛型类型对象——所有对象都属于普通类。在泛型实现的早起版本中,甚至能够将使用泛型的程序编译为在1.0虚拟机上运行的类文件。这个向后兼容性在Java泛型开发的后期就被放弃了。

        无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删除类型参数后的泛型类型类型名。擦除类型变量,并替换为限定类型(无限定的变量用Object)。

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

public class PairOld {
    private Object first;
    private Object second;

    public PairOld(Object first, Object second) {
        this.first = first;
        this.second = second;
    }

    public Object getFirst() {
        return first;
    }

    public void setFirst(Object first) {
        this.first = first;
    }

    public Object getSecond() {
        return second;
    }

    public void setSecond(Object second) {
        this.second = second;
    }
}

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

        在程序中可以包含不同类型的Pair,例如 Pair<String>或者Pair<GregorianCalendar>。而擦除类型后就变成原始的Pair类型了。

        原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换。例如,类Pair<T>中的类型变量没有显示的限定,因此,原始类型就用Object替换T。假定声明了一个不同的类型,代码如下:

public class Interval <T extends Comparable & Serializable> implements Serializable{
    private T lower;
    private T upper;

    public Interval(T lower, T upper) {
        if(lower.compareTo(upper) <= 0){
            this.lower = lower;
            this.upper = upper;
        }else{
            this.lower = upper;
            this.upper = lower;
        }
    }

    public T getLower() {
        return lower;
    }

    public void setLower(T lower) {
        this.lower = lower;
    }

    public T getUpper() {
        return upper;
    }

    public void setUpper(T upper) {
        this.upper = upper;
    }
}

原始类型Interval如下所示:

public class Interval  implements Serializable{
    private Comparable lower;
    private Comparable upper;

    public Interval(Comparable lower, Comparable upper) {
        if(lower.compareTo(upper) <= 0){
            this.lower = lower;
            this.upper = upper;
        }else{
            this.lower = upper;
            this.upper = lower;
        }
    }

    public Comparable getLower() {
        return lower;
    }

    public void setLower(Comparable lower) {
        this.lower = lower;
    }

    public Comparable getUpper() {
        return upper;
    }

    public void setUpper(Comparable upper) {
        this.upper = upper;
    }
}

        【注意】:读者可能想知道切换限定:class Interval <T extends Comparable & Serializable>

会发生什么。如果这样做,原始类型用Serializable替换,而编译器在必要时要想Comparable插入强制类型转换。为了提高效率,应该将标签接口放在边界列表的末尾。

       

六 泛型的约束与局限性

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

        不能用类型参数代替基本类型。因此,没有Pair<double>,只有Pair<Double>。其原因是类型擦除。擦除之后,Pair类含有的Object类型的域,而Object不能存储double的值。只有8种基本类型,当包装器类型不能接受替换时,可以使用独立的类和方法处理它们。

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

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

if(a instanceof Pair<String>)  //ERROR

          实际上仅仅测试a是否是任意类型的一个Pair。下面的测试同样如此:

if(a instanceof Pair<T>)  //ERROR

        或强制类型转换:

Pair<String> p = (Pair<String>) a; //WARNING--can only use that a is Pair

        要记住这一风险,无论何时使用instanceof或涉及泛型类型的强制类型转换表达式都会看到一个编译器警告。

        同样的道理,getClass方法总会返回原始类型。例如:

Pair<String> stringPair = ...;

Pair<Employee> employeePair = ...;

if(stringPair.getcClass() == employeePair.getClass)//true

        比较结果为true,这是因为两个调用getClass方法都将返回Pair.class。

6.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<String>[]的变量仍是合法的。不过不能用new Pair<String>[10] 初始化这个变量。

        可以声明通配符类型的数组,然后进行类型转换:

Pair<String>[] table = (Pair<String>[]) new Pair<?>[10];

        结果将是不安全的。如果在table[0]中存储一个Pair<Employee,然后对table[0].getFirst()调用一个String方法,会得到一个ClassCaseException异常。

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

geminigoth

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值