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

打印 上一主题 下一主题

主题 552|帖子 552|积分 1656

本文是《这一文,关于Java泛型的点点滴滴》的第二篇,也是最后一篇。在上一篇文章中我们介绍了关于 Java 泛型的基础知识,而在本文中,我们将深入 Java 泛型,介绍了 extends、super、<?> 通配符,并在最后介绍了使用反射获取泛型信息。
在阅读本文之前,请先阅读上一篇文章:这一文,关于Java泛型的点点滴滴 一(泛型基础、类型擦除)
别的这里插播一下我的微信公众号,希望大家可以或许多多关注,我会不定期更新良好的技术文章:

接下来,开始我们的正文吧。
extends 通配符,确定上界

在 Java 中,可以通过使用边界类型来限定泛型参数的类型范围。边界类型分为上界(upper bounds)和下界(lower bounds)。上界类型限定了泛型类型参数必须是某个特定类型的子类型,通常用 <T extends SomeType> 来表示。
前面我们写了一个很有用的函数,用于打印任意类型的数组,如今我们把它改造一下,用于打印上面的 Pair 类:
  1. public void printPair(Pair<Number, Number> pair) {
  2.     System.out.println(pair);
  3.     System.out.println("\tfirst = "+pair.first);
  4.     System.out.println("\tsecond = "+pair.second);
  5. }
复制代码
这个方法可以或许正常编译,其接收的参数是 Pair<Number, Number>,既然是可以或许接受数字类型的,那我们给它传入一个 Pair<Integer, Integer> 试试:
  1. Pair<Integer, Integer> intPair = Pair.create(111, 222);
  2. printPair(intPair);        //编译报错
复制代码
缘故原由很显着,Pair<Integer, Integer> 并不是 Pair<Number, Number> 的子类,因此 printPair 方法不接受 Pair<Integer, Integer> 类型的参数。但究竟上这个这个参数是可以通报给 printPair 方法的,方法的内部代码也不会有任何问题,只是参数限定死了只能传入 Pair<Number, Number>。
那有没有方法可以或许限定接受的参数为 Pair 的泛型为 Number 的子类呢?这就是 <? extends Number>:
  1. public void printPair(Pair<? extends Number, ? extends Number> pair) {
  2.     System.out.println(pair);
  3.     System.out.println("\tfirst = "+pair.first);
  4.     System.out.println("\tsecond = "+pair.second);
  5. }
复制代码
在看一下之前的报错,消散了,代码正常运行。因为 Pair<Integer, Integer> 类型是符合 Pair<? extends Number, ? extends Number> 限定的,这种使用 <? extends Number> 的泛型界说称之为上界通配符(Upper Bounds Wildcards),即把泛型类型 T 的上界限定在 Number 了。
在这种上界限定下,传入的参数 pair 的 first 和 second 都被限定在了 Number 和其子类,但具体是什么类型,是不能确定的。
如今我们掌握了 <T extends SomeType> 作为函数参数的第一种作用:用于限定传入的参数的类型。但 <T extends SomeType> 作为函数参数还有别的一个作用,标识方法内部不会对参数举行参数设置。这一点有点欠好理解,我们来看下面的例子:
  1. public class Pair<F, S> {
  2.     public F first;
  3.     public S second;
  4.     public Pair(F first, S second) {
  5.         this.first = first;
  6.         this.second = second;
  7.     }
  8.     public void setFirst(F first) {
  9.         this.first = first;
  10.     }
  11.     public void setSecond(S second) {
  12.         this.second = second;
  13.     }
  14. }
复制代码
我们在 Pair 对象中加入了 set 方法,然后我们再添加一个 modify 方法,这个方法只是修改传入的 pair 的内容:
  1. public void modifyPair(Pair<? extends Number, ? extends Number> pair) {
  2.     System.out.println(pair);
  3.     pair.setFirst(new Integer(100));    //编译错误
  4. }
复制代码
在这个方法中,当我们调用设置参数 pair 的 first 参数会发生编译错误。那么参数中的 pair 是 Pair<? extends Number, ? extends Number> 类型的,也就是 first 就是个 <? extends Number> 类型的,我给它设置个 Integer 应该是公道的啊,为什么会编译错误呢?
缘故原由还是在 Java 泛型的实现方式:类型擦除。在这里我们这么理解,modifyPair 方法接受的 pair 参数只要求 pair 的泛型类型继承于 Number 类,也就是说我可以传入 Pair<Integer, Integer> 也可以传入 Pair<Float, Float>,其具体类型是不知道的,那你在方法中设置 first 为 Integer,是不是就不太对了。
如果传入的参数是 Pair<Integer, Integer> 那还好说,如果传入的是 Pair<Float, Float>,你设置一个 Integer 肯定是有问题的。
于是这里就是 <T extends SomeType> 在作为方法的参数的另一个作用了,简单说来,就是阐明这个方法对参数只能举行读的利用,而不不能举行写的利用。
但是这里有一个例外,那就是你可以设置 null 值,但这种情况也是很少见,基本就属于恶意利用了。我想着也没有人会那么无聊,以是这里就不赘述了。
下面我们举一个例子:
  1. int sumOfList(List<? extends Integer> list) {
  2.     int sum = 0;
  3.     for (int i=0; i<list.size(); i++) {
  4.         Integer n = list.get(i);
  5.         sum = sum + n;
  6.     }
  7.     return sum;
  8. }
复制代码
这个方法用来盘算 Integer 列表中所有元素的和,在这个代码作用使用了 <? extends Integer> 而不是是直接使用 Integer,这就表示了在这个方法中对参数 list 只举行读利用,而不举行写利用。如果只使用了 Integer,那么在方法内部就可以对 list 举行写入的利用了。
总结一下,<? extends SomeType> 这种泛型限定用在方法的参数上有两个作用:

  • 标识传入参数的泛型上限
  • 标识方法中只对参数举行读利用,而没有写利用
extends 通配符不仅可以用在方法的参数上,也可以用在类的界说上,用于标识泛型参数的上限。
例如上面我们界说的 Pair<F, S>,其实我们也可以做如下界说:
  1. public static class Pair<F extends Number, S extends Number> {
  2.     //......
  3. }
复制代码
在这里,泛型参数 F 和 S 被限定为 Number 及其子类。

super 通配符,确定下界

上面说到边界类型分为上界(upper bounds)和下界(lower bounds)。上界类型限定了泛型类型参数必须是某个特定类型的子类型,通常用 <T extends SomeType> 来表示。那么下界自然就是限定了泛型类型参数必须是某个特定类型的超类型,通常用 <? super SomeType> 来表示。
我们先回到上一节中的 modifyPair 方法:
  1. public void modifyPair(Pair<? extends Number, ? extends Number> pair) {
  2.     System.out.println(pair);
  3.     pair.setFirst(new Integer(100));
  4. }
复制代码
我们已经明确了由于使用了 extends 类型限定,在这个方法中为 pair 设置值值错误的行为。那怎么样可以或许为 pair 设置新的值呢?
先这么想,我们要设置进去的是一个 Integer 对象,那么哪种类型的变量可以或许接受这个 Integer 类型呢?显然,只要是 Integer 的父类的变量都是可以或许接收 Integer 类型的。那我们如何表示 Integer 的父类呢?那就是 <T extends SomeType> 泛型限定了。
如今我们修改一下代码:
  1. public void modifyPair(Pair<? super Integer, ? super Integer> pair) {
  2.     System.out.println(pair);
  3.     pair.setFirst(Integer.valueOf(100));
  4.     pair.setSecond(Integer.valueOf(200));
  5. }
复制代码
这时候编译器不报错了,这段代码可以正常编译。因为参数 pair 的 first、second 都被限定为了 Integer 或其父类,这个类型的变量显然是可以接收 Integer 对象的,因此代码可以通过编译。
这是 super 通配符的第一个作用。那有人就问了,参照 extends,这个 super 通配符是不是只能写,不能读了?
答案是对的,但是不完全对,听我一一道来。
起首,<T super SomeType> 通配符是允许写入的,这个上面说的,但是作为读来说,你就起首考虑用什么类型的变量来接收,在使用 super 通配符后,你只能用 Object 类型来接收,无法使用其他类型接收,这也是我为什么说答案是对的,但也不完全对。如果你再想一下,你接收到的对象你无法知道其具体的类型的,也只能用 Object 来接收,但是用 Object 类型能做的事情实在太少了。因此咱们就以为其不可读好了。
那如今咱们总结一下 <T super SomeType>:

  • 标识传入参数的泛型类型的下界
  • 标识方法内可以对参数举行写,而不能读
再明确了这两个通配符后,我们再把 extends 和 super 放到一起做一下比较:


  • <T extends SomeType> 标识上界,可读不可写
  • <T super SomeType> 标识下界,可写不可读
那结合这两个统配符号我们来看一下 Collections 类中的 copy() 方法的界说:
  1. //将 src 列表中的元素复制到 dest 中
  2. public static <T> void copy(List<? super T> dest, List<? extends T> src) {
  3.     //......
  4. }
复制代码
这个方法体咱们不关注,咱们就关注参数。src 是 List<? extends T> 的类型,列表中的元素是 T 或其子类,由于是 extends 限定,因此方法中可以读 src 列表;dest 是 List<? super T> 类型,列表中的元素是 T 或其父类,由于是 super 限定,因此方法中只能写不能读。这完美地展示了这两个限定符的用途。
<?> 无限定通配符

上面我们讨论了 <? extends T> 和 <? super T> 作为方法参数的作用。实际上,Java 的泛型还允许使用无限定通配符(Unbounded Wildcard Type),即只界说一个 <?>。看下面这个方法:
  1. public static boolean isNull(Pair<?, ?> pair) {
  2.     return pair.first == null && pair.second == null;
  3. }
复制代码
这个方法中使用了 Pair<?, ?>,那这个无限定通配符有啥用呢?结合上面的 extends 和 super,这个 <?> 总结一下就是:既不能读也不能写。
这玩意集 extends 和 super 的限定于一身,既不能读也不能写,那能做的事情就很少了,基本只能做一些 null 判定,如上面的方法一样。也正是因为能做的事情不多,以是我们见得比较少。要说 <?> 的长处,也就是我们不用特意声明泛型参数了吧。
大多数情况下,可以使用泛型参数 <T> 消除 <?> 统配符:
  1. public static <T> boolean isNull(Pair<T, T> pair) {
  2.     return pair.first == null && pair.second == null;
  3. }
复制代码
<?> 通配符有一个独特的特点,那就是 Pair<?> 是所有 Pair<T> 的超类:
  1. Pair<String, Integer> pair = Pair.create("aaa", 123);
  2. Pair<?, ?> p = pair;            //编译通过
复制代码
上面由于 Pair<?, ?> 是 Pair<String, Integer> 的超类,因此可以直接将 pair 转换为 Pair<?, ?>。
简单总结:无限定通配符 <?> 很少使用,可以用 <T> 替换,同时它是所有 <T> 类型的超类。

泛型与反射

在阐明泛型的反射之前,我们先要知道泛型中有个关于反射的局限:那就是无法从泛型类的对象的 Class 信息中获取到泛型信息。例如下面的代码,从 intPair 和 strPair 获取到的 Class 都是同一个 Pair.class,我们无法从这个 Class 中获取到有关这两个对象的泛型信息。
  1. Pair<Integer, Integer> intPair = Pair.create(111, 222);
  2. Pair<String, String> strPair = Pair.create("aaa", "bbb");
  3. Class intPairClass = intPair.getClass();
  4. Class strPairClass = strPair.getClass();
  5. System.out.println("是否是相同的 Class 对象:" + (intPairClass == strPairClass));    // true
复制代码
在代码中固然 Pair 的参数类型不一样,但是在虚拟机那边,都是 Object,因此对于代码中两个不同类型的 Pair 获取 Class 时,获取到的是同一个 Class,也就是 Pair 的 Class。通过这个 Class,是无法反射拿到泛型信息的。
那么在泛型时,我们可以做哪些反射利用呢?其实跟我们正常使用泛型一样,我们可以或许拿到在编译时已经确定好的代码结构的各种信息。这里我们用一段代码来做示例:
  1. public static class Pair<F, S> implements Comparable<F> {
  2.     public F first;
  3.     public S second;
  4.     public Pair(F first, S second) {
  5.         this.first = first;
  6.         this.second = second;
  7.     }
  8.     @Override
  9.     public int compareTo(F o) {
  10.         return this.first.hashCode() - o.hashCode();
  11.     }
  12.     public static <A, B> Pair<A, B> create(A a, B b) {
  13.         return new Pair<>(a, b);
  14.     }
  15. }
  16. public static class Example<T> {
  17.     private Pair<String, Integer> pair1;
  18.     private Pair<? extends Number, ? super Integer> pair2;
  19.     private T[] array;
  20. }
复制代码
这两个类包含了许多泛型参数,那如今,咱们就看看如何通过反射来拿到这里面的与泛型有关的信息。
起首我们可以通过下面的代码获取 Pair 的泛型参数类型:
  1. Class<Pair> pairClass = Pair.class;
  2. System.out.println("pairClass = "+pairClass);
  3. TypeVariable<?>[] typeParameters = pairClass.getTypeParameters();
  4. // 输出泛型参数信息
  5. for (TypeVariable<?> typeParameter : typeParameters) {
  6.     System.out.println("Generic parameter: " + typeParameter.getName());
  7. }
复制代码
输出,就可以看我们界说的 F 和 S 这两个泛型参数
  1. System.out                  I  Generic parameter: F
  2. System.out                  I  Generic parameter: S
复制代码
我们还可以通过这两个泛型参数去看它们的上界:
  1. // 输出泛型参数信息
  2. for (TypeVariable<?> typeParameter : typeParameters) {
  3.     System.out.println("Generic parameter: " + typeParameter.getName());
  4.     // 输出泛型参数的上界
  5.     for (Type bound : typeParameter.getBounds()) {
  6.         System.out.println("  Bound: " + bound.getTypeName());       //输出 Object
  7.     }
  8. }
复制代码
接下来我们再看成员变量的泛型:
  1. Field[] fields = pairClass.getDeclaredFields();
  2. for(Field field : fields) {
  3.     System.out.println("成员变量: " + field);
  4. }
复制代码
输出:
  1. System.out                  I  成员变量: public java.lang.Object lic.swift.demo.java.Pair.first
  2. System.out                  I  成员变量: public java.lang.Object lic.swift.demo.java.Pair.second
复制代码
可以看到,反射得到的,这两个成员变量都已经变成 Object 类型了。
那如果我想拿到泛型类型呢?可以通过 field.getGenericType() 方法获取:
  1. Field[] fields = pairClass.getDeclaredFields();
  2. for(Field field : fields) {
  3.     System.out.println("成员变量: " + field);
  4.     System.out.println("\t泛型类型:"+field.getGenericType());    //返回 F 和 S
  5. }
复制代码
这里我直接把反射的代码,贴出来,这段代码通过反射拿到了泛型中的所有信息:
  1. Class<?> pairClass = Pair.class;
  2. System.out.println("Pair 类的 Class 对象:" + pairClass);
  3. TypeVariable<?>[] typeParameters = pairClass.getTypeParameters();
  4. // 输出泛型参数信息
  5. for (TypeVariable<?> typeParameter : typeParameters) {
  6.     System.out.println("Pair 类的泛型类型: " + typeParameter.getName());
  7.     // 输出泛型参数的上界
  8.     for (Type bound : typeParameter.getBounds()) {
  9.         System.out.println("\t此泛型上界: " + bound.getTypeName());
  10.     }
  11. }
  12. Field[] fields = pairClass.getDeclaredFields();
  13. for (Field field : fields) {
  14.     System.out.println("成员变量: " + field);
  15.     System.out.println("\t泛型类型:" + field.getGenericType());
  16. }
  17. Constructor<?>[] constructors = pairClass.getDeclaredConstructors();
  18. for (Constructor<?> constructor : constructors) {
  19.     System.out.println("构造函数: " + constructor);
  20.     Parameter[] parameters = constructor.getParameters();
  21.     for (Parameter parameter : parameters) {
  22.         System.out.println("\t此构造方法的参数:" + parameter + ", 参数的泛型类型:" + parameter.getParameterizedType());
  23.     }
  24. }
  25. Method[] methods = pairClass.getDeclaredMethods();
  26. for (Method method : methods) {
  27.     if (Modifier.isStatic(method.getModifiers())) {
  28.         System.out.println("静态方法:" + method);
  29.     } else {
  30.         System.out.println("成员方法:" + method);
  31.     }
  32.     Parameter[] parameters = method.getParameters();
  33.     for (Parameter parameter : parameters) {
  34.         System.out.println("\t此方法的参数:" + parameter + ", 参数的泛型类型:" + parameter.getParameterizedType());
  35.     }
  36. }
  37. Type[] types = pairClass.getGenericInterfaces();
  38. for (Type type : types) {
  39.     System.out.println("此类实现的接口:" + type);
  40. }
  41. Class<?> exampleClass = Example.class;
  42. System.out.println("Example 类的 Class 对象:" + exampleClass);
  43. Field[] exampleFields = exampleClass.getDeclaredFields();
  44. for (Field field : exampleFields) {
  45.     System.out.println("Example 成员变量:" + field.getName());
  46.     System.out.println("\t此变量类型:" + field.getGenericType());
  47. }
复制代码
这里注意代码中的输出,打印了 Pair 和 Example 这两个类的泛型相关信息。
关于反射的更多内容,大家可以看这篇文章,写得更详细一些:Java Reflection 反射使用 完全指南
总结

好了,这里关于 Java 泛型中的所有信息基本都已经说完了。我们知道为什么必要泛型,怎么用泛型,泛型中的三个通配符,信赖通过这篇文章,诸位能对 Java 的泛型有一个更深的熟悉。
最后,祝各位衣食无忧,一夜暴富。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

麻花痒

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表