Java泛型方法

1.* 泛型方法

前面介绍了在定义类、接口时可以使用类型形参,在该类的方法定义和Field定义、接口的方法定义中,这些类型形参可被当成普通类型来用。在另外一些情况下,我们定义类、接口时没有使用类型形参,但定义方法时想自己定义类型形参,这也是可以的,Java 5还提供了对泛型方法的支持。

1.*.& 定义泛型方法

假设需要实现这样一个方法,该方法负责将一个Object数组的所有元素添加到一个Collection集合中。考虑采用如下代码来实现该方法:

点击查看代码
static void fromArrayToCollection(Object[] a, Collection<Object> c) {
    for (Object o : a) {
        c.add(o);
    }
}

上面定义的方法没有任何问题,关键在于方法中的c形参,它的数据类型是Collection<Object>。正如前面所介绍的,Collection<String>不是Collection<Object>的子类型,所以这个方法的功能非常有限,它只能将Object数组的元素复制到Object(Object的子类不行)Collection集合中,即下面代码将引起编译错误:

点击查看代码
String[] strArr = {"a", "b"};
List<String> strList = new ArrayList<>();
//Collection<String>对象不能当成Collection<Object>使用,下面代码出现编译错误
fromArrayToCollection(strArr, strList);

可见上面方法的参数类型不可以使用Collection<String>,那使用通配符Collection<?>是否可行呢?显然也不行,我们不能把对象放进一个未知类型的集合中。

为了解决这个问题,可以使用Java 5提供的泛型方法(Generic Method)。所谓泛型方法,就是在声明方法时定义一个或多个类型形参。泛型方法的用法格式如下:

点击查看代码
修饰符 <T, S> 返回值类型 方法名(形参列表) {
    //方法体...
}

把上面方法的格式和普通方法的格式进行对比,不难发现泛型方法的方法签名比普通方法的方法签名多了类型形参声明,类型形参声明以尖括号括起来,多个类型形参之间以逗号,隔开,所有的类型形参声明放在方法修饰符和方法返回值类型之间。

采用支持泛型的方法,就可以将上面的fromArrayToCollection方法改为如下形式:

点击查看代码
static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
    for (T o : a) {
        c.add(o);
    }
}

下面程序示范了完整的用法:

查看代码

上面程序定义了一个泛型方法,该泛型方法中定义了一个T类型形参,这个T类型形参就可以在该方法内当成普通类型使用。与接口、类声明中定义的类型形参不同的是,方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。

与类、接口中使用泛型参数不同的是,方法中的泛型参数无须显式传入实际类型参数,如上面程序所示,当程序调用fromArrayToCollection()方法时,无须在调用该方法前传入String、Object等类型,但系统依然可以知道类型形参的数据类型,因为编译器根据实参推断类型形参的值,它通常推断出最直接的类型参数。例如,下面调用代码:

点击查看代码
fromArrayToCollection(sa, cs);

上面代码中cs是一个Collection<String>类型,与方法定义时的fromArrayToCollection(T[] a, Collection<T> c)进行比较,只比较泛型参数,不难发现该T类型形参代表的实际类型是String类型。

对于如下调用代码:

点击查看代码
fromArrayToCollection(ia, cn);

上面的cn是Collection<Number>类型,与此方法的方法签名进行比较,只比较泛型参数,不难发现该T类型形参代表了Number类型。

为了让编译器能准确地推断出泛型方法中类型形参的类型,不要制造迷惑!系统一旦迷惑了,就是你错了!看如下程序:

查看代码

上面程序中定义了test方法,该方法用于将前一个集合里的元素复制到下一个集合中,该方法中的两个形参from、to的类型都是Collection<T>,这要求调用该方法时的两个集合实参中的泛型类型相同,否则编译器无法准确地推断出泛型方法中类型形参的类型。

上面程序中调用test方法传入了两个实际参数,其中as的数据类型是List<String>,而ao的数据类型是List<Object>,与泛型方法签名进行对比:test(Collection<T> a, Collection<T> c),编译器无法正确识别T所代表的实际类型。为了避免这种错误,可以将该方法改为如下形式:

查看代码

上面代码改变了test方法签名,将该方法的前一个形参类型改为Collection<? extends T>,这种采用类型通配符的表示方式,只要test方法的前一个Collection集合里的元素类型是后一个Collection集合里元素类型的子类即可。

那么这里产生了一个问题:到底何时使用泛型方法?何时使用类型通配符呢?

1.*.& 泛型方法和类型通配符的区别

大多数时候都可以使用泛型方法来代替类型通配符。例如,对于Java的Collection接口中两个方法定义:

点击查看代码
public interface Collection<E> {
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E> c);
    ...
}

上面集合中两个方法的形参都采用了类型通配符的形式,也可以采用泛型方法的形式,如下所示:

点击查看代码
public interface Collection<E> {
    boolean <T> containsAll(Collection<T> c);
    boolean <T extends E> addAll(Collection<T> c);
    ...
}

上面方法使用了<T extends E>泛型形式,这时定义类型形参时设定上限(其中E是Collection接口里定义的类型形参,在该接口里E可当成普通类型使用)。

上面两个方法中类型形参T只使用了一次,类型形参T产生的唯一效果是可以在不同的调用点传入不同的实际类型。对于这种情况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的。

泛型方法允许类型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。

提示:如果某个方法中一个形参a的类型或返回值的类型依赖于另一个形参b的类型,则形参b的类型声明不应该使用通配符,因为形参a或返回值的类型依赖于该形参b的类型,如果形参b的类型无法确定,程序就无法定义形参a的类型。在这种情况下,只能考虑使用在方法签名中声明类型形参,也就是泛型方法。

如果有需要,我们可以同时使用泛型方法和通配符,如Java的Collections.copy()方法:

点击查看代码
public class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {...}
    ...
}

上面copy方法中的dest和src存在明显的依赖关系,从源List中复制出来的元素,必须可以“丢进”目标List中,所以源List集合元素的类型只能是目标集合元素的类型的子类型或者它本身。但JDK定义src形参类型时使用的是类型通配符,而不是泛型方法。这是因为:该方法无须向src集合中添加元素,也无须修改src集合里的元素,所以可以使用类型通配符,无须使用泛型方法。

当然,也可以将上面的方法签名改为使用泛型方法,不使用类型通配符,如下所示:

点击查看代码
class Collections {
    public static <T , S extends T> void copy(List<T> dest, List<S> src) {
        ...
    }
    ...
}

这个方法签名可以代替前面的方法签名。但注意上面的类型形参S,它仅使用了一次,没有其他参数的类型、方法返回值的类型依赖于它,那类型形参S就没有存在的必要,即可以用通配符来代替S。使用通配符比使用泛型方法(在方法签名中显式声明类型形参)更加清晰和准确,因此Java设计该方法时采用了通配符,而不是泛型方法。

类型通配符与泛型方法(在方法签名中显式声明类型形参)还有一个显著的区别:类型通配符既可以在方法签名中定义形参的类型,也可以用于定义变量的类型;但泛型方法中的类型形参必须在对应方法中显式声明。

1.*.& Java 7的“菱形”语法与泛型构造器

正如泛型方法允许在方法签名中声明类型形参一样,Java也允许在构造器签名中声明类型形参,这样就产生了所谓的泛型构造器。

一旦定义了泛型构造器,接下来在调用构造器时,就不仅可以让Java根据数据参数的类型来“推断”类型形参的类型,而且程序员也可以显式地为构造器中的类型形参指定实际的类型。如下程序所示:

查看代码

上面程序中new <String>Foo(“疯狂Android讲义”)不仅显式指定了泛型构造器中的类型形参T的类型应该是String,而且程序传给该构造器的参数值也是String类型,因此程序完全正常。但在new <String>Foo(12.3)处,程序显式指定了泛型构造器中的类型形参T的类型应该是String,但实际传给该构造器的参数值是Double类型,因此这行代码将会出现错误。

前面介绍过Java 7新增的“菱形”语法,它允许调用构造器时在构造器后使用一对尖括号来代表泛型信息。但如果程序显式指定了泛型构造器中声明的类型形参的实际类型,则不可以使用“菱形”语法。如下程序所示:

查看代码

上面程序中MyClass<String> mc3 = new <Integer> MyClass<>(5)既指定了泛型构造器中的类型形参是Integer类型,又想使用“菱形”语法,所以这行代码无法通过编译。

1.*.& 设定通配符下限

假设自己实现一个工具方法:实现将src集合里的元素复制到dest集合里的功能,因为dest集合可以保存src集合里的所有元素,所以dest集合元素的类型应该是src集合元素类型的父类。为了表示两个参数之间的类型依赖,考虑同时使用通配符、泛型参数来实现该方法。代码如下:

点击查看代码
public static <T> void copy(Collection<T> dest , Collection<? extends T> src) {
    for (T ele : src) {
        dest.add(ele);
    }
}

上面方法实现了前面的功能。现在假设该方法需要一个返回值,返回最后一个被复制的元素,则可以把上面方法改为如下形式:

点击查看代码
public static <T> T copy(Collection<T> dest , Collection<? extends T> src) {
    T last = null;
    for (T ele : src) {
        last = ele;
        dest.add(ele);
    }
    return last;
}

表面上看起来,上面方法实现了这个功能,实际上有一个问题:当遍历src集合的元素时,src元素的类型是不确定的(但可以肯定它是T的子类),程序只能用T来笼统地表示各种src集合的元素类型。例如如下代码:

点击查看代码
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
//下面代码引起编译错误
Integer last = copy(ln , li);

上面代码中ln的类型是List<Number>,与copy方法签名的形参类型进行对比即得到T的实际类型是Number,而不是Integer类型,即copy方法的返回值也是Number类型,而不是Integer类型,但实际上最后一个复制元素的元素类型一定是Integer。也就是说,程序在复制集合元素的过程中,丢失了src集合元素的类型。

对于上面的copy方法,可以这样理解两个集合参数之间的依赖关系:不管src集合元素的类型是什么,只要dest集合元素的类型与前者相同或是前者的父类即可。为了表达这种约束关系,Java允许设定通配符的下限:<? super Type>,这个通配符表示它必须是Type本身,或是Type的父类。下面程序采用设定通配符下限的方式改写了前面的copy方法:

查看代码

使用这种语句,就可以保证程序的Integer last = copy(ln, li)代码调用后推断出最后一个被复制的元素类型是Integer,而不是笼统的Number类型。

实际上,Java集合框架中的TreeSet<E>有一个构造器也用到了这种设定通配符下限的语法,如下所示:

点击查看代码
//下面的E是定义TreeSet类时的类型形参
TreeSet(Comparator<? super E> c)

正如前面所介绍的,TreeSet会对集合中的元素按自然顺序或定制顺序进行排序。如果需要TreeSet对集合中的所有元素进行定制排序,则要求TreeSet对象有一个与之关联的Comparator对象。上面构造器中的参数c就是进行定制排序的Comparator对象。

Comparator接口也是一个带泛型声明的接口:

点击查看代码
public interface Comparator<T> {
    int compare(T fst, T snd);
}

通过这种带下限的通配符的语法,可以在创建TreeSet对象时灵活地选择合适的Comparator。假定需要创建一个TreeSet<String>集合,并传入一个可以比较String大小的Comparator,这个Comparator既可以是Comparator<String>,也可以是Comparator<Object>,只要尖括号里传入的类型是String的父类型(或它本身)即可。如下程序所示:

查看代码

通过使用这种通配符下限的方式来定义TreeSet构造器的参数,就可以将所有可用的Comparator作为参数传入,从而增加了程序的灵活性。当然,不仅TreeSet有这种用法,TreeMap也有类似的用法,具体请查阅Java的API文档。

1.*.& 泛型方法与方法重载

因为泛型既允许设定通配符的上限,也允许设定通配符的下限,从而允许在一个类里包含如下两个方法定义:

点击查看代码
public class MyUtils {
    public static <T> void copy(Collection<T> dest , Collection<? extends T> src) {...}
    public static <T> T copy(Collection<? super T> dest , Collection<T> src) {...}
}

上面的MyUtils类中包含两个copy方法,这两个方法的参数列表存在一定的区别,但这种区别不是很明确:这两个方法的两个参数都是Collection对象,前一个集合里的集合元素类型是后一个集合里集合元素类型的父类。如果这个类仅包含这两个方法不会有任何错误,但只要调用这个方法就会引起编译错误。例如,对于如下代码:

点击查看代码
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
//引起编译错误
copy(ln, li);

上面程序块中调用copy方法,但这个copy方法此时两个copy方法都能匹配,编译器无法确定这行代码想调用哪个copy方法,所以这行代码将引起编译错误。

原文地址:http://www.cnblogs.com/hzhiping/p/16906232.html

1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长! 2. 分享目的仅供大家学习和交流,请务用于商业用途! 3. 如果你也有好源码或者教程,可以到用户中心发布,分享有积分奖励和额外收入! 4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解! 5. 如有链接无法下载、失效或广告,请联系管理员处理! 6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需! 7. 如遇到加密压缩包,默认解压密码为"gltf",如遇到无法解压的请联系管理员! 8. 因为资源和程序源码均为可复制品,所以不支持任何理由的退款兑现,请斟酌后支付下载 声明:如果标题没有注明"已测试"或者"测试可用"等字样的资源源码均未经过站长测试.特别注意没有标注的源码不保证任何可用性