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' } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需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杂项