java的协变和逆变

一、协变和逆变的概念

协变:模板中赋值给A的是A或者A的子类。比如:List<? extends A> listA = List<ChildA>() 即:ChildA 可能是A或者A的子类

逆变:模板中赋值给A的是A或者A的父类。比如:List<? super A> listA = List<ParentA>() 即: ParentA可能是A或者A的父类

二、为何会有协变和逆变

1、关于协变

  咱们在没有模板的时候,把ChildA赋值给A是很正常的情况。

  比如 ChildA ca = new ChildA(); A a = ca; 然后对a 调用各种a中的函数

  但是咱们使用模板进行上述的操作的时候:List<ChildA> lca = new List<ChildA>(); List<A> la = lca; 看起来貌似是可以的,但是编译器却不会通过;因为List<ChildA>和List<A>是两个不同的对象,虽然ChildA是A的子类

  所以就需要协变这个特殊的概念来进行操作

  然后我们则可以A a = la.get(0); a可以操作A中的各个公共方法等,如果有ChildA对应的override函数,则会调用对应的ChildA中的函数

2、关于逆变

  逆变则跟协变相反,在模板中List<ParentA> lpa = new List<ParentA>(); List<? super A> lsa = lpa; 这里限定了赋值给lsa的只能是A或者A的父类;

  咱们这里执行一个操作A a = lsa.get(0); 发现编译不通过,因为get出来的是Object类型。也就是说java这里可以有返回类型,但是类型会是Object

  既然返回的是Object对象,那咱们用原生的List也是可以的, 即:List l = lpa; Object o = l.get(0); 也是能正常取值

  但是java增加了这个逆变的概念是为了为了提高泛型编程的灵活性、类型安全性以及代码的可读性和可维护性。具体说明如下所示

1. 提高代码的灵活性:逆变允许向泛型方法或泛型集合传递更广泛的类型参数。这样可以在泛型代码实现过程中处理更多不同场景的需求,同时确保类型安全。
2. 保证类型安全:使用通配符和逆变,您可以在编译时检查类型。这样做可以确保在运行时不会出现意外的类型转换错误。逆变通过限制泛型参数的范围,使编译器能够在编译期间发现潜在的类型不匹配问题,从而提高代码的类型安全性。
3. 提高代码的可读性和维护性:逆变和协变共同促使程序员编写更通用且具有良好层次结构的代码。这种代码为其他开发者提供了更清晰的约束和边界,有助于提高项目整体的可读性和可维护性。

比如:

        List<ParentA> lpa = new ArrayList<>();
        lpa.add(new ParentA());
        List<? super A> lsa = lpa;
        // 如下两个的add都是报错的,因为都不是A或者A的子类
        // 如果使用原生的List l = lpa; 那么如下两个都是能够正常add
        lsa.add(new ParentA());
        lsa.add(new B());

 

三、协变和逆变的标记记住技巧

协变:? extends T

逆变:? super T

因为协变能够返回T类型,并且对T类型的对象进行操作所以对应的类型肯定是T或者T的子类,咱们java中,使用A extends T 表示A是T的子类;所以协变使用的是? extends T

因为逆变是跟协变相反,协变是T或者T的子类,那么逆变就是T或者T的父类;在java中,要调用父类的函数用的是super,所以逆变使用? super T

咱们在别的地方也有出现类似? extends T的格式,比如T extends B ,但是这两个是不同的概念,T extends B 是限定当前的T模板必须有继承B

 

四、具体的代码

复制代码
class ParentA {
    void printPA() {System.out.println("ParentA");}
    void print() {System.out.println("print ParentA");}
}

class A extends ParentA {
    void printA() {System.out.println("A");}
    void print() {System.out.println("print A");}
}

class ChildA extends A {
    void printCA() {System.out.println("ChildA");}
    void print() {System.out.println("print ChildA");}
}

class B {}

class ChildB {}

public class HelloWorld {

    public static void processElements(List<? super A> list) {
        for (Object elem : list) {
            // Process the element
        }
    }

    public static void main(String[] args) {

        List<ChildA> lca = new ArrayList<>();
        lca.add(new ChildA());
        List<? extends A> la = lca;
        la.get(0).printA(); // A
        la.get(0).print(); // print ChildA

        List<ParentA> lpa = new ArrayList<>();
        lpa.add(new ParentA());
        List<? super A> lsa = lpa;
        // 如下两个的add都是报错的,因为都不是A或者A的子类
        // 如果使用原生的List l = lpa; 那么如下两个都是能够正常add
//        lsa.add(new ParentA());
//        lsa.add(new B());

        List lo = lpa;

//        lsa.get(0).printA(); // error: Cannot resolve method 'printA' in 'Object'
    }

}
复制代码

 

posted @   LCAC  阅读(1162)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
历史上的今天:
2022-04-15 swift 属性和方法
2020-04-15 Lua字符串匹配
2020-04-15 Lua面向对象
2020-04-15 Lua杂项
点击右上角即可分享
微信分享提示