泛型,好像没那么简单

泛型广泛用于各类框架中,日常开发、或是源码阅读时,泛型的的曝光率都很高,搞懂泛型很有必要。泛型的基础使用场景比较简单,但在复杂场景下往往并不容易理解

本文首先介绍了java泛型基础概念,随后列举了泛型的一些复杂使用场景,最后介绍了spring中强大易用的泛型API,希望帮你更深入的理解泛型,也可以在日常开发工作时更好的利用泛型进行通用设计

java泛型定义

泛型允许用户在定义class、interface、method时,声明一个或多个类型参数(type parameter)。和方法参数一样,定义好类型参数后,就可以在对应作用域中(class中、interface、method中)引用该参数。不同之处是,方法参数的输入是值、而类型参数的输入是type

相关名词解释:

泛型类型(generic type):包括泛型类和泛型接口,定义格式:

public class Generics<T1, T2, ..., Tn> {
    ...
}

泛型方法(generic method):包括普通方法和构造函数,静态和非静态方法都支持泛型,注意泛型参数必须在方法返回类型前,定义格式:

public <T1, T2, ..., Tn> void test(T1 t1,T2 t2...,Tn tn){
    ...
}

类型参数/类型变量(type parameter/type variable):泛型类型或方法声明中的"<>"包含的T1、T2..就是type parameter(也称为type variable)

类型实参(type argument):注意不要和type parameter混淆了,type parameter是形参(声明时定义的参数),type argument是实参(实际传入的参数)。比如class Foo<T>{}中的T是形参(type parameter),而Foo<String>  foo;中的String是实参(type argument)

参数化类型(parameterized type):Foo<String>  foo;中的Foo<String>就是parameterized type

有界泛型(bounded Generics):可以通过限制type parameter的边界,来限制泛型类型、泛型方法接受的type argument的类型范围,比如下面的泛型方法限定只能接受Number及其子孙类作为输入

public <T extends Number> List<T> fromArrayToList(T a) {
    ...
}

泛型通配符(wildcards):使用?表示泛型通配符,用于表示未知类型,下文的场景描述中会作详细说明

有界泛型通配符(bounded wildcards):同样的,通配符也支持边界限制:

public static void paintAllBuildings(List<? extends Building> buildings) {
    ...
}

原始类型(raw type):参数化类型(parameterized type)去掉类型实参(type argument)之后就是raw type,Foo<String>中的Foo就是raw type

泛型复杂使用场景

泛型的基础使用场景我们就不展开了,下面介绍一些稍微复杂点的场景

场景一:

public void someMethod(Number n) { /* ... */ }
someMethod(new Integer(10));   // OK
someMethod(new Double(10.1));   // OK

List<Number> list = new ArrayList<Number>();
list.add(new Integer(10));   // OK
list.add(new Double(10.1));  // OK

毫无疑问,上述两个场景都是OK的,原因是Integer、Double是Number的子类型,那么我们考虑下下面这个方法可以传入什么类型的参数:

public void test(List<Number> n) {}

List<Integer>、List<Double>可以吗?答案是不可以,因为List<Integer>、List<Double>并不是List<Number>的子类ArrayList<Number>、LinkedList<Number>是可以的,它们是List<Number>的子类

场景二:

写一个打印list元素的方法,要求支持任意类型的list

public static void printList(List<Object> list) {
    for (Object elem : list){
        System.out.println(elem + " ");
    }
}

显然这个方法无法满足要求,因为它只能处理List<Object>类型的list,List<String>、List<Integer>都不行,原因在场景一中已经说过

如何实现呢?用泛型通配符

public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}

"?"表示泛型通配符,常用于这两个场景:一种是独立的泛型方法,该方法希望支持任何继承于Object的类型(比如上面的printList方法);二是泛型类中的方法,虽然泛型类已经定义了type parameter,但是方法并不依赖于该type parameter,比如List接口中的removeAll方法

public interface List<E> extends Collection<E>{
    boolean removeAll(Collection<?> c);
    。。。
}

虽然List接口已经定义了type parameter(E),但是removeAll方法的功能是从当前集合中移除在输入集合中存在的元素,输入集合可能包含E类型的元素,但是也可能包含其他类型的元素,因此用Collection<?>而不是Collection<E>

注意,只有方法才会用到<?>,如果你看到某个类上用到了,那一定是作者理解有误

场景三:  

//定义一个泛型接口A
interface A<T>{}

下面这几种实现方式你一定都遇到过:

//方式一
class B implements A<String>{}

//方式二
class B<T> implements A<T>{}

//方式三
class B<T extends User> implements A<T>{}

//方式四
class B<T> implements A<String>{}

//方式五
class B<T,E> implements A<T>{}

那么它们分别代表什么含义呢?

方式一:子类B指定类型实参(type argument)为String,此时B已经不是一个泛型类了,在使用B时,也不再需要指定type argument了

方式二:子类B原封不动的继承了父类的类型参数(type parameter),在使用B时,可以指定任意类型的类型实参(type argument)

方式三:子类B重新定义了从父类继承来的类型参数(type parameter),并通过有界泛型(bounded Generics)将其范围缩小为必须是User及其子孙类,在使用B时,只能指定类型为User及其子孙类的类型实参(type argument)

方式四:子类B指定父类的类型实参(type argument)为String,并定义了自己的类型参数(type parameter)(T),这个T跟父类没有任何关系

方式五:子类B继承了父类的类型参数(type parameter)(T)、并且定义了自己的type parameter(E),在使用B时,需要指定两个类型实参(type argument)

场景四:

抽象模板类A的handle方法首先将输入的json串转为T类型对象,然后交给子类处理

public abstract class A<T>{
    
    public void handle(String json){
        //通过json序列化框架将json转换为T类型的实例,然后交给子类处理
        T t=fromJson(json);
        doHandle(t);
    }

    private T fromJson(String json) {
        //如何实现?
    }

    protected abstract void doHandle(T t);
}

考虑下如何fromJson方法如何实现,json转为对象得先拿到对应的Class,怎么拿呢?T.class你猜猜行不行?。。。正确的解法如下,你可以结合上面的名词解释理解下:

public abstract class A<T>{

    public void handle(String json){
        T t=fromJson(json);
        doHandle(t);
    }

    private T fromJson(String json) {
        //获取父类的参数化类型(parameterized type):A<User>
        ParameterizedType parameterizedType=(ParameterizedType)this.getClass().getGenericSuperclass();
        //获取其类型实参数组(type arguments):new Type[]{User.class}
        Type[] typeArguments=parameterizedType.getActualTypeArguments();
        //获取第一个类型实参(type argument):User.class
        Class<T> clazzT=(Class<T>)typeArguments[0];
        return new Gson().fromJson(json,clazzT);
    }

    protected abstract void doHandle(T t);
}

public class B extends A<User>{
    @Override
    protected void doHandle(User user) {
        //do something
    }
}

场景五:

class C<T>{},如何在C中获取T对应的type argument?答案是没有任何办法获取到,在场景三中能拿到是因为type argument在编译期间就已确定下来了(class B extends A<User>),当前场景中,编译时只有type parameter没有type argument,而针对泛型的处理都是在编译期间完成的,试图从运行时获取泛型信息是行不通的

泛型解析工具ResolvableType:

如果你在项目中实现过通用功能,你应该有过泛型解析的需求(比如场景四),Spring提供了一个强大的泛型解析工具ResolvableType:ResolvableType封装一个java.lang.reflect.Type对象,提供了对Type上各种信息的访问能力,比如对父类(getSuperType())、接口(getInterfaces())、类上泛型参数的访问能力(getGeneric(int...))、以及将泛型参数解析为class的能力(resolve())

可以通过field、method parameter、method return type或者class来构造ResolvableType,分别对应ResolvableType中的静态方法:forField(Field)、forMethodParameter(Method, int)、forMethodReturnType(Method)、forClass(Class)

下面通过几组例子感受下ResolvableType的能力、以及其易用性

//需求一、解析指定field的泛型信息
public class Generics{
   private HashMap<Integer, List<String>> myMap;
}

public void resolveFieldGenerics() throws NoSuchFieldException {
	//通过field构造一个ResolvableType对象
	ResolvableType t = ResolvableType.forField(getClass().getDeclaredField("myMap"));
	//获取父类并解析为class->java.util.AbstractMap
	System.out.println(t.getSuperType().resolve());
	//解析方法第一个type argument的类型,结果为:java.lang.Integer
	System.out.println(t.getGeneric(0).resolve());
	//效果同上,结果为:java.lang.Integer
	System.out.println(t.resolveGeneric(0));

	//解析方法第二个type argument的类型,结果为:java.util.List,注意:如果type argument依然是一个泛型类型,则resolve返回的是原始类型(raw type)
	System.out.println(t.getGeneric(1).resolve());
	//效果同上,结果为:java.util.List
	System.out.println(t.resolveGeneric(1));

	//解析list第一个type argument的类型,结果为:java.lang.String
	System.out.println(t.getGeneric(1).getGeneric(0).resolve());
	//效果同上,结果为:java.lang.String
	System.out.println(t.resolveGeneric(1, 0));
}

//需求二、解析指定方法返回类型的泛型信息
public Map<String, List<Integer>> test(){return null;}

void resolveMethodGenerics() throws NoSuchMethodException {
	//通过方法返回类型构造一个ResolvableType对象
	ResolvableType methodReturnType=ResolvableType.forMethodReturnType(getClass().getMethod("test"));
	//->java.lang.String
	System.out.println(methodReturnType.resolveGeneric(0));
	//->java.util.List
	System.out.println(methodReturnType.resolveGeneric(1));
	//->java.lang.Integer
	System.out.println(methodReturnType.resolveGeneric(1, 0));
}

//需求三、解析类的泛型信息,是前面举过的例子,这里通过ResolvableType再实现一遍,你可以跟场景四中用jdk自带api实现的方式做个对比
public abstract class A<T>{

    public void handle(String json){
        T t=fromJson(json);
        doHandle(t);
    }

    private T fromJson(String json) {
        //通过class构造一个ResolvableType对象,并将其作为父类的ResolvableType返回
        ResolvableType classResolvableType=ResolvableType.forClass(this.getClass()).as(A.class);
        //->User
        Class<T> clazzT=classResolvableType.resolveGeneric(0);
        return new Gson().fromJson(json,clazzT);
    }

    protected abstract void doHandle(T t);
}

public class B extends A<User>{
    @Override
    protected void doHandle(User user) {
        //do something
    }
}

几乎你能想象出来的泛型解析场景,ResolvableType都可以帮你解决,更关键的是解决方式都比较优雅,希望你能掌握

参与评论 您还未登录,请先 登录 后发表或查看评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

wb_snail

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值