当前位置:   article > 正文

这一文,关于 Java 泛型的点点滴滴 二 (extends、super、<?> 通配符、泛型与反射)

这一文,关于 Java 泛型的点点滴滴 二 (extends、super、<?> 通配符、泛型与反射)

本文是《这一文,关于Java泛型的点点滴滴》的第二篇,也是最后一篇。在上一篇文章中我们介绍了关于 Java 泛型的基础知识,而在本文中,我们将深入 Java 泛型,介绍了 extendssuper<?> 通配符,并在最后介绍了使用反射获取泛型信息。

在阅读本文之前,请先阅读上一篇文章:这一文,关于Java泛型的点点滴滴 一(泛型基础、类型擦除)

另外这里插播一下我的微信公众号,希望大家能够多多关注,我会不定期更新优秀的技术文章:

接下来,开始我们的正文吧。

extends 通配符,确定上界

在 Java 中,可以通过使用边界类型来限制泛型参数的类型范围。边界类型分为上界(upper bounds)和下界(lower bounds)。上界类型限制了泛型类型参数必须是某个特定类型的子类型,通常用 <T extends SomeType> 来表示。

前面我们写了一个很有用的函数,用于打印任意类型的数组,现在我们把它改造一下,用于打印上面的 Pair 类:

public void printPair(Pair<Number, Number> pair) {
    System.out.println(pair);
    System.out.println("\tfirst = "+pair.first);
    System.out.println("\tsecond = "+pair.second);
}
  • 1
  • 2
  • 3
  • 4
  • 5

这个方法能够正常编译,其接收的参数是 Pair<Number, Number>,既然是能够接受数字类型的,那我们给它传入一个 Pair<Integer, Integer> 试试:

Pair<Integer, Integer> intPair = Pair.create(111, 222);
printPair(intPair);        //编译报错
  • 1
  • 2

原因很明显,Pair<Integer, Integer> 并不是 Pair<Number, Number> 的子类,因此 printPair 方法不接受 Pair<Integer, Integer> 类型的参数。但事实上这个这个参数是可以传递给 printPair 方法的,方法的内部代码也不会有任何问题,只是参数限制死了只能传入 Pair<Number, Number>

那有没有方法能够限制接受的参数为 Pair 的泛型为 Number 的子类呢?这就是 <? extends Number>

public void printPair(Pair<? extends Number, ? extends Number> pair) {
    System.out.println(pair);
    System.out.println("\tfirst = "+pair.first);
    System.out.println("\tsecond = "+pair.second);
}
  • 1
  • 2
  • 3
  • 4
  • 5

在看一下之前的报错,消失了,代码正常运行。因为 Pair<Integer, Integer> 类型是符合 Pair<? extends Number, ? extends Number> 限制的,这种使用 <? extends Number> 的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型 T 的上界限定在 Number 了。

在这种上界限制下,传入的参数 pairfirstsecond 都被限制在了 Number 和其子类,但具体是什么类型,是不能确定的。

现在我们掌握了 <T extends SomeType> 作为函数参数的第一种作用:用于限定传入的参数的类型。但 <T extends SomeType> 作为函数参数还有另外一个作用,标识方法内部不会对参数进行参数设置。这一点有点不好理解,我们来看下面的例子:

public class Pair<F, S> {
    public F first;
    public S second;

    public Pair(F first, S second) {
        this.first = first;
        this.second = second;
    }

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

    public void setSecond(S second) {
        this.second = second;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

我们在 Pair 对象中加入了 set 方法,然后我们再添加一个 modify 方法,这个方法只是修改传入的 pair 的内容:

public void modifyPair(Pair<? extends Number, ? extends Number> pair) {
    System.out.println(pair);
    pair.setFirst(new Integer(100));    //编译错误
}
  • 1
  • 2
  • 3
  • 4

在这个方法中,当我们调用设置参数 pairfirst 参数会发生编译错误。那么参数中的 pairPair<? extends Number, ? extends Number> 类型的,也就是 first 就是个 <? extends Number> 类型的,我给它设置个 Integer 应该是合理的啊,为什么会编译错误呢?

原因还是在 Java 泛型的实现方式:类型擦除。在这里我们这么理解,modifyPair 方法接受的 pair 参数只要求 pair 的泛型类型继承于 Number 类,也就是说我可以传入 Pair<Integer, Integer> 也可以传入 Pair<Float, Float>,其具体类型是不知道的,那你在方法中设置 firstInteger,是不是就不太对了。

如果传入的参数是 Pair<Integer, Integer> 那还好说,如果传入的是 Pair<Float, Float>,你设置一个 Integer 肯定是有问题的。

于是这里就是 <T extends SomeType> 在作为方法的参数的另一个作用了,简单说来,就是说明这个方法对参数只能进行读的操作,而不不能进行写的操作。

但是这里有一个例外,那就是你可以设置 null 值,但这种情况也是很少见,基本就属于恶意操作了。我想着也没有人会那么无聊,所以这里就不赘述了。

下面我们举一个例子:

int sumOfList(List<? extends Integer> list) {
    int sum = 0;
    for (int i=0; i<list.size(); i++) {
        Integer n = list.get(i);
        sum = sum + n;
    }
    return sum;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这个方法用来计算 Integer 列表中所有元素的和,在这个代码作用使用了 <? extends Integer> 而不是是直接使用 Integer,这就表示了在这个方法中对参数 list 只进行读操作,而不进行写操作。如果只使用了 Integer,那么在方法内部就可以对 list 进行写入的操作了。

总结一下,<? extends SomeType> 这种泛型限定用在方法的参数上有两个作用:

  1. 标识传入参数的泛型上限
  2. 标识方法中只对参数进行读操作,而没有写操作

extends 通配符不仅可以用在方法的参数上,也可以用在类的定义上,用于标识泛型参数的上限。

例如上面我们定义的 Pair<F, S>,其实我们也可以做如下定义:

public static class Pair<F extends Number, S extends Number> {
    //......
}
  • 1
  • 2
  • 3

在这里,泛型参数 FS 被限制为 Number 及其子类。

super 通配符,确定下界

上面说到边界类型分为上界(upper bounds)和下界(lower bounds)。上界类型限制了泛型类型参数必须是某个特定类型的子类型,通常用 <T extends SomeType> 来表示。那么下界自然就是限制了泛型类型参数必须是某个特定类型的超类型,通常用 <? super SomeType> 来表示。

我们先回到上一节中的 modifyPair 方法:

public void modifyPair(Pair<? extends Number, ? extends Number> pair) {
    System.out.println(pair);
    pair.setFirst(new Integer(100));
}
  • 1
  • 2
  • 3
  • 4

我们已经明白了由于使用了 extends 类型限制,在这个方法中为 pair 设置值值错误的行为。那怎么样能够为 pair 设置新的值呢?

先这么想,我们要设置进去的是一个 Integer 对象,那么哪种类型的变量能够接受这个 Integer 类型呢?显然,只要是 Integer 的父类的变量都是能够接收 Integer 类型的。那我们如何表示 Integer 的父类呢?那就是 <T extends SomeType> 泛型限定了。

现在我们修改一下代码:

public void modifyPair(Pair<? super Integer, ? super Integer> pair) {
    System.out.println(pair);
    pair.setFirst(Integer.valueOf(100));
    pair.setSecond(Integer.valueOf(200));
}
  • 1
  • 2
  • 3
  • 4
  • 5

这时候编译器不报错了,这段代码可以正常编译。因为参数 pairfirstsecond 都被限制为了 Integer 或其父类,这个类型的变量显然是可以接收 Integer 对象的,因此代码可以通过编译。

这是 super 通配符的第一个作用。那有人就问了,参照 extends,这个 super 通配符是不是只能写,不能读了?

答案是对的,但是不完全对,听我一一道来。

首先,<T super SomeType> 通配符是允许写入的,这个上面说的,但是作为读来说,你就首先考虑用什么类型的变量来接收,在使用 super 通配符后,你只能用 Object 类型来接收,无法使用其他类型接收,这也是我为什么说答案是对的,但也不完全对。如果你再想一下,你接收到的对象你无法知道其具体的类型的,也只能用 Object 来接收,但是用 Object 类型能做的事情实在太少了。因此咱们就认为其不可读好了。

那现在咱们总结一下 <T super SomeType>

  1. 标识传入参数的泛型类型的下界
  2. 标识方法内可以对参数进行写,而不能读

再明白了这两个通配符后,我们再把 extendssuper 放到一起做一下比较:

  • <T extends SomeType> 标识上界,可读不可写
  • <T super SomeType> 标识下界,可写不可读

那结合这两个统配符号我们来看一下 Collections 类中的 copy() 方法的定义:

//将 src 列表中的元素复制到 dest 中
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    //......
}
  • 1
  • 2
  • 3
  • 4

这个方法体咱们不关注,咱们就关注参数。srcList<? extends T> 的类型,列表中的元素是 T 或其子类,由于是 extends 限定,因此方法中可以读 src 列表;destList<? super T> 类型,列表中的元素是 T 或其父类,由于是 super 限定,因此方法中只能写不能读。这完美地展示了这两个限定符的用途。

<?> 无限定通配符

上面我们讨论了 <? extends T><? super T> 作为方法参数的作用。实际上,Java 的泛型还允许使用无限定通配符(Unbounded Wildcard Type),即只定义一个 <?>。看下面这个方法:

public static boolean isNull(Pair<?, ?> pair) {
    return pair.first == null && pair.second == null;
}
  • 1
  • 2
  • 3

这个方法中使用了 Pair<?, ?>,那这个无限定通配符有啥用呢?结合上面的 extendssuper,这个 <?> 总结一下就是:既不能读也不能写。

这玩意集 extendssuper 的限制于一身,既不能读也不能写,那能做的事情就很少了,基本只能做一些 null 判断,如上面的方法一样。也正是因为能做的事情不多,所以我们见得比较少。要说 <?> 的好处,也就是我们不用特意声明泛型参数了吧。

大多数情况下,可以使用泛型参数 <T> 消除 <?> 统配符:

public static <T> boolean isNull(Pair<T, T> pair) {
    return pair.first == null && pair.second == null;
}
  • 1
  • 2
  • 3

<?> 通配符有一个独特的特点,那就是 Pair<?> 是所有 Pair<T> 的超类:

Pair<String, Integer> pair = Pair.create("aaa", 123);
Pair<?, ?> p = pair;            //编译通过
  • 1
  • 2

上面由于 Pair<?, ?>Pair<String, Integer> 的超类,因此可以直接将 pair 转换为 Pair<?, ?>

简单总结:无限定通配符 <?> 很少使用,可以用 <T> 替换,同时它是所有 <T> 类型的超类。

泛型与反射

在说明泛型的反射之前,我们先要知道泛型中有个关于反射的局限:那就是无法从泛型类的对象的 Class 信息中获取到泛型信息。例如下面的代码,从 intPairstrPair 获取到的 Class 都是同一个 Pair.class,我们无法从这个 Class 中获取到有关这两个对象的泛型信息。

Pair<Integer, Integer> intPair = Pair.create(111, 222);
Pair<String, String> strPair = Pair.create("aaa", "bbb");
Class intPairClass = intPair.getClass();
Class strPairClass = strPair.getClass();
System.out.println("是否是相同的 Class 对象:" + (intPairClass == strPairClass));    // true
  • 1
  • 2
  • 3
  • 4
  • 5

在代码中虽然 Pair 的参数类型不一样,但是在虚拟机那里,都是 Object,因此对于代码中两个不同类型的 Pair 获取 Class 时,获取到的是同一个 Class,也就是 PairClass。通过这个 Class,是无法反射拿到泛型信息的。

那么在泛型时,我们可以做哪些反射操作呢?其实跟我们正常使用泛型一样,我们能够拿到在编译时已经确定好的代码结构的各种信息。这里我们用一段代码来做示例:

public static class Pair<F, S> implements Comparable<F> {
    public F first;
    public S second;

    public Pair(F first, S second) {
        this.first = first;
        this.second = second;
    }

    @Override
    public int compareTo(F o) {
        return this.first.hashCode() - o.hashCode();
    }

    public static <A, B> Pair<A, B> create(A a, B b) {
        return new Pair<>(a, b);
    }
}

public static class Example<T> {
    private Pair<String, Integer> pair1;
    private Pair<? extends Number, ? super Integer> pair2;
    private T[] array;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

这两个类包含了很多泛型参数,那现在,咱们就看看如何通过反射来拿到这里面的与泛型有关的信息。

首先我们可以通过下面的代码获取 Pair 的泛型参数类型:

Class<Pair> pairClass = Pair.class;
System.out.println("pairClass = "+pairClass);
TypeVariable<?>[] typeParameters = pairClass.getTypeParameters();
// 输出泛型参数信息
for (TypeVariable<?> typeParameter : typeParameters) {
    System.out.println("Generic parameter: " + typeParameter.getName());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

输出,就可以看我们定义的 FS 这两个泛型参数

System.out                  I  Generic parameter: F
System.out                  I  Generic parameter: S
  • 1
  • 2

我们还可以通过这两个泛型参数去看它们的上界:

// 输出泛型参数信息
for (TypeVariable<?> typeParameter : typeParameters) {
    System.out.println("Generic parameter: " + typeParameter.getName());

    // 输出泛型参数的上界
    for (Type bound : typeParameter.getBounds()) {
        System.out.println("  Bound: " + bound.getTypeName());       //输出 Object
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

接下来我们再看成员变量的泛型:

Field[] fields = pairClass.getDeclaredFields();
for(Field field : fields) {
    System.out.println("成员变量: " + field);
}
  • 1
  • 2
  • 3
  • 4

输出:

System.out                  I  成员变量: public java.lang.Object lic.swift.demo.java.Pair.first
System.out                  I  成员变量: public java.lang.Object lic.swift.demo.java.Pair.second
  • 1
  • 2

可以看到,反射得到的,这两个成员变量都已经变成 Object 类型了。

那如果我想拿到泛型类型呢?可以通过 field.getGenericType() 方法获取:

Field[] fields = pairClass.getDeclaredFields();
for(Field field : fields) {

    System.out.println("成员变量: " + field);
    System.out.println("\t泛型类型:"+field.getGenericType());    //返回 F 和 S
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这里我直接把反射的代码,贴出来,这段代码通过反射拿到了泛型中的所有信息:

Class<?> pairClass = Pair.class;
System.out.println("Pair 类的 Class 对象:" + pairClass);
TypeVariable<?>[] typeParameters = pairClass.getTypeParameters();
// 输出泛型参数信息
for (TypeVariable<?> typeParameter : typeParameters) {
    System.out.println("Pair 类的泛型类型: " + typeParameter.getName());

    // 输出泛型参数的上界
    for (Type bound : typeParameter.getBounds()) {
        System.out.println("\t此泛型上界: " + bound.getTypeName());
    }
}

Field[] fields = pairClass.getDeclaredFields();
for (Field field : fields) {
    System.out.println("成员变量: " + field);
    System.out.println("\t泛型类型:" + field.getGenericType());
}

Constructor<?>[] constructors = pairClass.getDeclaredConstructors();
for (Constructor<?> constructor : constructors) {
    System.out.println("构造函数: " + constructor);
    Parameter[] parameters = constructor.getParameters();
    for (Parameter parameter : parameters) {
        System.out.println("\t此构造方法的参数:" + parameter + ", 参数的泛型类型:" + parameter.getParameterizedType());
    }
}

Method[] methods = pairClass.getDeclaredMethods();
for (Method method : methods) {
    if (Modifier.isStatic(method.getModifiers())) {
        System.out.println("静态方法:" + method);
    } else {
        System.out.println("成员方法:" + method);
    }
    Parameter[] parameters = method.getParameters();
    for (Parameter parameter : parameters) {
        System.out.println("\t此方法的参数:" + parameter + ", 参数的泛型类型:" + parameter.getParameterizedType());
    }
}

Type[] types = pairClass.getGenericInterfaces();
for (Type type : types) {
    System.out.println("此类实现的接口:" + type);
}

Class<?> exampleClass = Example.class;
System.out.println("Example 类的 Class 对象:" + exampleClass);

Field[] exampleFields = exampleClass.getDeclaredFields();
for (Field field : exampleFields) {
    System.out.println("Example 成员变量:" + field.getName());
    System.out.println("\t此变量类型:" + field.getGenericType());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

这里注意代码中的输出,打印了 PairExample 这两个类的泛型相关信息。

关于反射的更多内容,大家可以看这篇文章,写得更详细一些:Java Reflection 反射使用 完全指南

总结

好了,这里关于 Java 泛型中的所有信息基本都已经说完了。我们知道为什么需要泛型,怎么用泛型,泛型中的三个通配符,相信通过这篇文章,诸位能对 Java 的泛型有一个更深的认识。

最后,祝各位衣食无忧,一夜暴富。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/在线问答5/article/detail/1012906
推荐阅读
相关标签
  

闽ICP备14008679号