浅谈Java中的桥接方法与泛型的逆变和协变

目录
  • 1. 泛型的协变
    • 1.1 泛型协变的使用
    • 1.2 泛型协变存在的问题
      • 1.2.1 Java当中桥接方法的来由
      • 1.2.2 为什么泛型协变时,不允许添加元素呢
      • 1.2.3 从Java字节码的角度去看桥接方法
  • 2. 泛型逆变
    • 2.1 泛型逆变的使用
    • 2.2 泛型逆变会有什么问题
  • 3.协变与逆变-PECS原则

泛型的协变和逆变是什么?对应于Java当中,协变对应的就是<? extends XXX>,而逆变对应的就是<? super XXX>

1. 泛型的协变

1.1 泛型协变的使用

当我们有一个有方法,方法的签名定义成为如下的方式

public static void test(List<Number> list)

这时,如果我们想要给test方法传入一个List<Double>或者是List<Integer>可以吗?很显然不行,因为传递参数,肯定是要传递它的子类才行,但是List<Double>或者是List<Integer>是它的子类吗?很明显不是,这时我们就需要用到泛型的协变。

我们将方法的参数变成如下的这种形式

public static void test(List<? extends Number> list)

这时,我们的泛型,就只需要传入一个是Number的子类型的泛型即可。因为Integer和Double,它们都是Number的子类,因此很明显是合法的。

test(new ArrayList<Integer>());
test(new ArrayList<Double>());

在test方法中:

  • 1.如果我们想要去获取集合当中的某个元素时,因为约定了元素的所有类型都得是Number类型极其子类的,因此我们获取的元素一定可以用它们的共同父类Number去进行接收。
  • 2.但是当我们想要往集合当中添加元素时,竟然无法往list当中添加元素?奇奇怪怪的!而且关键我们的list,只要求元素的类型是Number或者它的子类类型。但是我们加入的是1,是个Intger类型,很明显是符合规范的呀!
    public static void test(List<? extends Number> list) {
        Number number = list.get(0);  // right
        list.add(1); // error
    }

1.2 泛型协变存在的问题

泛型的协变,不能让我们往集合当中添加元素。那么为什么不能添加呢?

要知道为什么,我们首先需要了解Java当中桥接方法的来由。

1.2.1 Java当中桥接方法的来由

我们首先定义如下的自定义ArrayList类,并重写了它的add方法,

public class MyArrayList extends ArrayList<Double> {

    @Override
    public boolean add(Double e) {
        return super.add(e);
    }
}

首先,我们肯定知道ArrayList类中的add方法的原型是下面这样的

public boolean add(E e)

在Java当中,是在编译时去进行类型擦除的,在运行时并无泛型类型一说。也就是说,该原型方法,会被抹掉成为

public boolean add(Object e)

但是,我们定义了自己的ArrayList,我们自己的add方法的原型为

public boolean add(Double e)

这个两个方法的签名并不相同,但是当使用下的代码创建一个ArrayList时:

ArrayList<Double> list = new MyArrayList();
list.add(1.0);

它实际调用的方法的原型是public boolean add(Object e),但是我们子类中的重写的方法的原型时什么?public booleab add(Double e)

也就是说,通过父类的方法调用的和子类重写的方法,并不是同一个方法,因为它们连方法签名都不同。这时候,就需要要一个方式,将public booleab add(Object e)转到public booleab add(Double e)当中去执行。这时候,就会涉及到桥接方法的存在了。

Java的实现方式是:通过在Javac编译器编译时,为我们生成一个public boolean add(Object e)这样的方法,而这个方法当中,要做的实际上就是调用public booleab add(Double e)这个方法。

    public boolean add(Object o) {
        return add((Double) o);
    }

通过桥接方法的方式,就可以让我们能在针对泛型方法进行重写时,可以被JVM执行到。

1.2.2 为什么泛型协变时,不允许添加元素呢

当我们使用下面的代码创建了一个我们自定义的MyArrayList对象。

ArrayList<Double> list = new MyArrayList();

这时,我们调用test方法

test(list)

test方法对于list的泛型定义为<? entends Number>,理论上应该是可以往里面放入任何Number子类类型的元素的。但是别忘了,我们MyArrayList中对于方法的定义,是下面这样子的!

public boolean add(Object e) {
    return add((Double)e);
}

public boolean add(Double e)  {
    // ......
}

如果我们往集合当中添加一个Integer类型的1,走到桥接方法当中时会有(Double)e这样的强制类型转换,这不就是抛出了ClassCastException异常了吗?很明显,是不允许我们这样干的。因此Java的做法就是,在编译期就去禁止这种做法,避免产生运行时的ClassCastException

有的人也许会说

ArrayList<Double> list = new MyArrayList();

我们创建list时,不是约束了泛型类型为Double了吗,为什么test方法内就不能默认它是Double的泛型呢?问题就是:我写test方法时,我怎么知道你传递的是Double类型的泛型,玩意别人传递的是Integer的泛型呢?所以很明显是行不通的。

1.2.3 从Java字节码的角度去看桥接方法

我们可以看到,Javac编译器,在对Java代码进行编译时,其实针对add方法去生成了两个方法,而它们的访问标识符并不相同。我们自己的方法的访问标识符为0x0001[public],而Javac编译器为我们生成的桥接方法的返回值,为0x1041[pubic synthetic bridge],多了两个访问标识符syntheticbridge

我们打开桥接方法的code字节码

我们来分析下字节码

  • 1.aload_0,众所周知,就是从LocalVariableTable(局部变量表)获取this对象的引用,并压栈。
  • 2.aload_1,自然就是将传入的元素e的引用压栈。
  • 3.checkcast #3 <java/lang/Double>,自然是检查能否执行强制类型转换。
  • 4.invokevirtual #4 <com/wanna/generics/java/MyArrayList.add : (Ljava/lang/Double;)Z>,做到实际上就是从常量池的4号元素当中拿到要执行的方法,也就是我们自己实现的方法。invokevirtual就是执行目标方法,没毛病。
  • 5.ireturn,自然就是返回一个int类型的值,为什么是int类型?而不是boolean类型?因为Java当中,在存放到局部变量表和栈中的情况下,int/byte/boolean/char,都是使用的int的形式存放的,占用一个局部变量表的槽位。

我们通过分析得到的信息和我们之前的分析一致,就是通过桥接方法桥接一下,去调用我们自己实现的方法。我们接下来,尝试使用反射的方式去获取到add方法有几个,方法信息是什么。

        Arrays.stream(MyArrayList.class.getMethods()).filter(method -> method.getName().equals("add") && method.getParameterCount() == 1).forEach(method -> {
            System.out.printf("方法名为:%s,方法的返回值类型为:%s,方法的参数列表为:%s%n",
                    method.getName(), method.getReturnType(), Arrays.toString(method.getParameterTypes()));
        });

代码的最终执行结果为

方法名为:add,方法的返回值类型为:boolean,方法的参数列表为:[class java.lang.Double]
方法名为:add,方法的返回值类型为:boolean,方法的参数列表为:[class java.lang.Object]

也就是说,生成的桥接方法,是我们可以通过反射拿到的,它是一个真实的方法。

通过反射拿到Method之后,我们还可以通过访问标识符判断该方法是否是桥接方法。

method.isBridge()
method.isSynthetic()

判断桥接方法,实际上,在Spring框架当中的反射工具类(ReflectionUtils)当中就有用到,用来判断一个方法是否是用户定义的方法。

2. 泛型逆变

2.1 泛型逆变的使用

泛型逆变的泛型形式是:<? super XXX>,它的作用是赋值给它的约束容器的泛型类型,只能是XXX以及它的父类。

那么我们可以往容器里放入它的子类吗?也许会说,上面不是都说了需要放入的是XXX以及它的父类吗,那肯定是不能放入它的子类的呀!但是我们需要想到一个问题,那就是XXX的所有子类,其实都是可以隐式转换为XXX类型,或者可以直接说,它的子类就是XXX类型。

我们依次定义三个类

    static class Person {

    }

    static class User extends Person {

    }

    static class Student extends User {

    }

接着,定义一个使用逆变的泛型参数的方法

public static void test(List<? super User> list)

上面我们说了,可以接收的容器泛型类型是User以及它的父类,也就是说,容器的泛型可以是User也基于是Person。因此,我们可以传入下面这样的容器给test方法。

 test(new ArrayList<Person>());

在test方法当中,我们可以执行下面的才做

list.add(new User()); // 放入User
list.add(new Student());  // 放入User的子类

2.2 泛型逆变会有什么问题

我们需要想想一个问题:我们使用了逆变约定了,接收的容器的泛型类型是User以及User的父类。我们往容器当中放入的元素,可以是User以及User的子类。也就是说,我们获取容器中的元素时,根本不知道是什么类型,只能用Object去接收从容器中获取的元素类型,因为只是约定了容器的泛型为User和User的父类,而Object也是它的父类,因此我们甚至可以传入一个容器类型为ArrayList<Object>,我们根本无法决定元素类型的上限,只能用Object去进行接收。

final Object object = list.get(0);

现在又有一个问题:之前协变时,会出现因为执行桥接方法时,发生类型转换异常,在逆变当中会出现这种情况吗?

我们仔细想想,接收的容器泛型类型为User以及User的父类,而可以往容器里存放的是User以及User的子类,也就是说,我们放入到容器中的元素类型,比你原来约束的类型还严格,因为:"User以及User的子类"一定是"User以及User的父类"的子类。也就是说,逆变当中,并不会因为桥接方法中进行的类型导致ClassCastException,所以允许add。

3.协变与逆变-PECS原则

对于协变和逆变,有这样的一个原则:称为PECS(Producer Extends Consumer Super)。也就是说:

  • 1.Extends应该用在生产者的情况,也就是要根据泛型类型去返回对象的形式。
  • 2.Super应该用在消费者的情况,应该传入一个泛型类型的容器,应该利用该容器对数据进行处理,但是不能根据泛型去进行返回,如果要进行返回,只能返回Object,但是这就失去了泛型的意义。
    public static <T> void testCS(List<? super T> list) {  // Consumer Super
        list.add(...);
    }

    public static <T> T testPE(List<? extends T> list) {  // Producer Extends
        return list.get(0);
    }

到此这篇关于浅谈Java中的桥接方法与泛型的逆变和协变的文章就介绍到这了,更多相关Java桥接方法与泛型逆变协变内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解Java的桥接方法

    什么是桥接方法? Java中的桥接方法(Bridge Method)是一种为了实现某些Java语言特性而由编译器自动生成的方法. 我们可以通过Method类的isBridge方法来判断一个方法是否是桥接方法. 在字节码文件中,桥接方法会被标记为ACC_BRIDGE和ACC_SYNTHETIC,其中ACC_BRIDGE用于表示该方法是由编译器产生的桥接方法,ACC_SYNTHETIC用于表示该方法是由编译器自动生成. 什么时候生成桥接方法? 为了实现哪些Java语言特性会生成桥接方法?最常见的两种

  • 3分钟快速搞懂Java的桥接方法示例

    什么是桥接方法? Java中的桥接方法(Bridge Method)是一种为了实现某些Java语言特性而由编译器自动生成的方法. 我们可以通过Method类的isBridge方法来判断一个方法是否是桥接方法. 在字节码文件中,桥接方法会被标记为ACC_BRIDGE和ACC_SYNTHETIC,其中ACC_BRIDGE用于表示该方法是由编译器产生的桥接方法,ACC_SYNTHETIC用于表示该方法是由编译器自动生成. 什么时候生成桥接方法? 为了实现哪些Java语言特性会生成桥接方法?最常见的两种

  • 浅谈Java中的桥接方法与泛型的逆变和协变

    目录 1. 泛型的协变 1.1 泛型协变的使用 1.2 泛型协变存在的问题 1.2.1 Java当中桥接方法的来由 1.2.2 为什么泛型协变时,不允许添加元素呢 1.2.3 从Java字节码的角度去看桥接方法 2. 泛型逆变 2.1 泛型逆变的使用 2.2 泛型逆变会有什么问题 3.协变与逆变-PECS原则 泛型的协变和逆变是什么?对应于Java当中,协变对应的就是<? extends XXX>,而逆变对应的就是<? super XXX>. 1. 泛型的协变 1.1 泛型协变的使

  • 浅谈java中==以及equals方法的用法

    equals 方法是 java.lang.Object 类的方法. 有两种用法说明: (1)对于字符串变量来说,使用"=="和"equals()"方法比较字符串时,其比较方法不同. "=="比较两个变量本身的值,即两个对象在内存中的首地址. "equals()"比较字符串中所包含的内容是否相同. 比如: String s1,s2,s3 = "abc", s4 ="abc" ; s1 =

  • 浅谈Java中的hashcode方法(推荐)

    哈希表这个数据结构想必大多数人都不陌生,而且在很多地方都会利用到hash表来提高查找效率.在Java的Object类中有一个方法: public native int hashCode(); 根据这个方法的声明可知,该方法返回一个int类型的数值,并且是本地方法,因此在Object类中并没有给出具体的实现. 为何Object类需要这样一个方法?它有什么作用呢?今天我们就来具体探讨一下hashCode方法. 一.hashCode方法的作用 对于包含容器类型的程序设计语言来说,基本上都会涉及到has

  • 浅谈Java中几种常见的比较器的实现方法

    在Java中经常会涉及到对象数组的排序问题,那么就涉及到对象之间的比较问题. 通常对象之间的比较可以从两个方面去看: 第一个方面:对象的地址是否一样,也就是是否引用自同一个对象.这种方式可以直接使用"=="来完成. 第二个方面:以对象的某一个属性的角度去比较. 从最新的JDK8而言,有三种实现对象比较的方法: 一.覆写Object类的equals()方法: 二.继承Comparable接口,并实现compareTo()方法: 三.定义一个单独的对象比较器,继承自Comparator接口

  • 浅谈Java中方法参数传递的问题

    可以理解当我们要调用一个方法时,我们会把指定的数值,传递给方法中的参数,这样方法中的参数就拥有了这个指定的值,可以使用该值,在方法中运算了.这种传递方式,我们称为参数传递.在这里,定义方法时,参数列表中的变量,我们称为形式参数. 调用方法时,传入给方法的数值,我们称为实际参数 在Java中调用方法时,如果参数是基本类型(byte/short/int/long/float/double/char/boolean)以及String类型时,形式参数的改变不影响实际参数. 以下代码在内存中发生的动作:

  • 浅谈java中unmodifiableList方法的应用场景

    java对象中primitive类型变量可以通过不提供set方法保证不被修改,但对象的List成员在提供get方法后,就可以随意add.remove改变其结构,这不是希望的结果.网上看了下,发现Collections的静态方法unmodifiableList可以达到目的.方法原型为:public static <T> List<T> unmodifiableList(List<? extends T> list);用法也很简单,传入一个List实例la,返回这个list

  • 浅谈Java中hashCode的正确求值方法

    本文研究的主要是Java中hashCode的正确求值方法的相关内容,具体如下. 散列表有一项优化,可以将对象的散列码(hashCode)缓存起来,如果散列码不匹配,就不会检查对象的等同性而直接认为成不同的对象.如果散列码(hashCode)相等,才会检测对象是否相等(equals). 如果对象具有相同的散列码(hashCode),他们会被映射到同一个散列桶中.如果散列表中所有对象的散列码(hashCode)都一样,那么该散列表就会退化为链表(linked list),从而大大降低其查询效率. 一

  • 浅谈Java中浮点型数据保留两位小数的四种方法

    目录 一.String类的方式 二.DecimalFormat类 三.BigDecimal类进行数据处理 四.NumberFormat类进行数据处理 总结一下 今天在进行开发的过程中遇到了一个小问题,是关于如何将double类型的数据保留两位小数.突然发现这方面有一点欠缺,就来总结一下. 一.String类的方式 该方式是是使用String的format()方法来实现的,该方法的作用就是规范数据的格式,第一个参数传入一个字符串来表示输出的数据格式,如保留两位小数就使用"%.2f",第二

  • 浅谈Java中Collections.sort对List排序的两种方法

    目录 一.Collections.sort的简单使用 二.问题提出 三.Comparable实现排序 四.Comparator实现排序 五.Comparable 与Comparator区别 一.Collections.sort的简单使用 说到List的排序,第一反应当然是使用Collections.sort,方便简单.下面实现一下~~ private void sortStrings() { List<String> list = new ArrayList<String>();

  • 浅谈Java中Collection和Collections的区别

    1.java.util.Collection 是一个集合接口.它提供了对集合对象进行基本操作的通用接口方法.Collection接口在Java 类库中有很多具体的实现.Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式. Collection ├List │├LinkedList │├ArrayList │└Vector │ └Stack └Set 2.java.util.Collections 是一个包装类.它包含有各种有关集合操作的静态多态方法.此类不能实例化,就像一

随机推荐