05_generic_01_method

第五十七章 Java泛型编程(Java Generics)

1 简介

泛型编程(Generic Programming)是一种允许将类型(Type)作为参数传递的编程方式。泛型编程能帮助开发人员最大程度的复用代码,并增强Java语言的类型系统(Type System),提供编译时的类型检查与类型安全(Compile-time type checking and type safety)。

泛型编程起源于一个非常简单的编程场景。如下面的代码所示,在Example类中定义了两个成员方法max(),返回两个参数a和b中较大的那个。这两个max()方法的实现内容完全相同。唯一不同的是一个方法接受两个Integer对象,返回一个Integer对象;而另一个方法接受两个Double对象,返回一个Double对象。

public class Example {
    public Integer max (Integer a, Integer b) {
        if (a.compareTo(b) >= 0) {
            return a;
        } else {
            return b;
        }
    }

    public Double max (Double a, Double b) {
        if (a.compareTo(b) >= 0) {
            return a;
        } else {
            return b;
        }
    }
}

因为这两个max()方法的内容几乎完全相同,所以,开发人员希望能有一种方法,只需要实现一个max()方法,可以分别应用于Integer和Double两个场景。因此,泛型编程应运而生。

2 泛型方法(Generic Method)

在泛型编程中,成员方法能够接受类型变量,即参数的类型是可变的。如下面的代码所示。GenericMethodExample类中包含了一个泛型成员方法max()。它接受两个参数a和b。a和b的类型是未知的。当max()方法被调用时,Java编译器会推演a和b的类型。因为a和b的类型是未知的,我们可以使用一个变量T来暂时代替a和b的类型。为了告诉Java编译器类型T是一个泛型类型,而不是一个具体的类名或者接口名,开发人员需要在返回参数之前使用<T>。其意义是尖括号内部的是一个泛型类型。常用的泛型类型变量名称有T、E、V等。它们都是由单个大写的英文字母组成。

public class GenericMethodExample {
    public <T> T max(T a, T b) {
        ...
    }
}

泛型静态成员方法也可以使用相似的方法实现。关键字static可以用来声明一个静态的max()方法。

public class GenericMethodExample {
    public static <T> T max(T a, T b) {
        ...
    }
}

当需要使用两个或者多个类型变量时,其使用方法也是十分相似的。如下的代码示例定义了类型变量T, U和R。

public class GenericMethodExample {
    public <T, U, R> R max(T a, U b) {
        ...
    }
}
public class GenericMethodExample {
    public static <T, U, R> R max(T a, U b) {
        ...
    }
}

3 泛型方法的重载(Overloading)与覆盖(Overriding)

当遇到泛型方法重载的问题时,开发人员需要理解Java编译器处理泛型变量的方法:类型擦除(Type Erasure)。换句话说,当一个方法被调用时,Java编译器会寻找一个最合适的方法用于方法调用处。所以,在此时,Java编译器会根据调用的上下文,参数的个数和类型,选择可用于调用的候选方法(Candidate Methods)。在候选方法中,Java编译器会优先选择精确匹配的方法。在精确匹配失败后,Java编译器会选择可应用于调用场景(Method Invocation Context)的方法。如果此时Java编译器未能找到合适的方法,或者Java编译器找到了两个或者两个以上的方法,并且Java编译器无法确定哪个优先选择的话,会报告错误。

在候选方法之间做决定时,Java编译器会先"擦除"泛型方法的类型参数,将其替换为一个更加通用的类型。类型擦除的原理和细节可参考小水滴的文章"类型擦除(Type Erasure)"

所以,在如下代码中,main()方法首先会调用max(Integer, Integer)方法,因为这个方法调用是精确匹配的。第二个方法调用会使用max(T, T),因为此处无法使用max(Integer, Integer)方法。Java编译器会将类型参数T替换成Object,进而满足方法的参数类型。

public class GenericMethodExample {
    public <T> T max(T a, T b) {
        System.out.println("max of T is invoked.");
        ...
    }

    public Integer max(Integer a, Integer b) {
        System.out.println("max of Integer is invoked.");
        ...
    }

    public static void main(String[] args) {
        GenericMethodExample example = new GenericMethodExample();
        example.max(Integer.valueOf(1), Integer.valueOf(2));
        example.max(Double.valueOf(1.0), Double.valueOf(2.0));
    }
}

当需要在子类中覆盖父类的泛型方法时,情况要复杂得多。当需要覆盖父类的泛型方法时,子类也需要定义"一模一样"的方法。因为只有这样,在父类和子类中的方法签名(Method Signature)才会相同。方法覆盖的内容可参考函数签名相等性(Override-Equivalent Signatures)

public class GenericMethodExample {
    public <T> T max(T a, T b) {
        ...
    }
}

public class OverrideGenericMethodExample extends GenericMethodExample {
    @Override
    public <T> T max(T a, T b) {
        ...
    }
}

使用普通方法来覆盖泛型方法是不行的。因为方法签名中包含了泛型信息。所以,普通方法和泛型方法的方法签名是不同的。

public class GenericMethodExample {
    public <T> T max(T a, T b) {
        ...
    }
}

public class OverrideGenericMethodExample extends GenericMethodExample {
    @Override
    public Object max(Object a, Object b) {
        // 普通方法不能覆盖泛型方法
        ...
    }
}

4 类型的上界(Upper-bound)

我们在上面的例子中展示了max()方法可以接受任何类型的对象,因为类型参数T可以表示任何类型。但是,在某些情况下,我们希望类型参数只能表示某一些类型。比如,在max()方法中,我们希望所有的参数对象都实现了接口java.lang.Comparable,表示该对象是可比较的。所以,我们可以为参数类型设置一个上界,如下面的代码所示。当在尖括号中添加了extends Comparable之后,T只能表示实现了Comparable接口的对象。在这里,无论上界是接口还是类,都使用关键字extends。当需要设置多个上界时,每一个上界可由&分隔。例如:<T extends Comparable & Cloneable>。

public class GenericMethodExample {
    public <T extends Comparable> T max(T a, T b) {
        if (a.compareTo(b) >= 0) {
            return a;
        } else {
            return b;
        }
    }
}

5 泛型编程的限制

在Java语言中,泛型编程的类型参数只能用于类或者接口对象,而无法用于基本数据类型(Primitive Types)。所以,在泛型编程中,小水滴建议尽量使用类对象。当无法避免使用基本数据类型时,Java语言为了减轻这个问题,提供了装箱机制(Boxing)。Java语言能够将基本数据类型自动的转换成对应的类类型。如果对装箱机制感兴趣,可参考小水滴的文章"Java装箱转换(Boxing Conversion)和拆箱转换(Unboxing Conversion)"

另外,因为Java语言采用了类型擦除来实现泛型编程,所以,在运行时,程序无法获取泛型参数的类型。例如,在下面的程序中,我们无法获得List对象中元素的类型,因为该类型被擦除了。

List<Integer> listOfInteger = new ArrayList<Integer>();
if (listOfInteger instanceof List<Integer>) { // 这是错误的,因为List<Integer>被擦除成List了,所以listOfInteger的类型是List

}

其三,Java语言不支持在泛型类型上使用new操作符(new operator),创建一个泛型类型的对象。所以,下面的程序是错误的。当需要创建对象时,可使用Class类中的newInstance()方法或者通过反射机制获取类的构造函数对象,并调用其构造函数。

public class GenericFactoryExample {
    public <T> T newInstance(T a) {
        return new T(); // 不能在类型T上使用new操作符
    }
}

其四,Java语言不支持抛出和捕抓泛型类型的异常。所以,下面的程序是错误的。

public class GenericExceptionExample {
    public <T> T throwException(T a) {
        try {
            ...
        } catch (T e) {  // 不能捕抓泛型类型的异常,在这里可以使用Throwable代替
            ...
        }
    }
}

6 小结

本章介绍了Java泛型编程的基本概念、泛型方法的基本使用方法、和Java语言泛型编程的限制。这些限制有些是因为设计的考量,Java设计者没有将其实现在Java语言中。有些是因为Java语言使用了类型擦除来实现泛型编程,引来了诸多问题。我们会在下一章介绍泛型类的使用方法以及类型擦除的一些实现原理和细节。

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.