kotlin 逆向,kotlin型变

  kotlin 逆向,kotlin型变

  

java基础教程栏目今天介绍kotlin的协变与逆变。

  如何解决写爬虫IP受阻的问题?立即使用。

  

前言

  为了更好的理解kotlin和Java中的协变和逆变,先看一些基础知识。

  

普通赋值

  在Java中,常见的赋值语句如下:

  a a=b;复制代码赋值语句必须满足的条件是左侧要么是右侧的父类,要么是与右侧相同的类型。即a的类型要“大于”b的类型,如Object o=new String( s );为方便起见,以下简称为b。

  除了上面提到的最常见的赋值语句之外,还有另外两种赋值语句:

  

函数参数的赋值

   public void fun(A a) {}//在调用处赋值B B=new B();

  乐趣(b);复制代码调用fun(b)方法时,会将传递的B b实参赋给形参A a,即A A=B的形式,同样,形参类型必须大于实参,即A B。

  

函数返回值的赋值

  公共A fun() {

  B B=new B();返回b;

  }

  复制的代码函数的返回值类型接收实际返回类型的值,实际返回类型B b相当于赋给返回值类型A a,即B b赋给A a,即A a=b,所以A B的类型关系必须满足。

  所以无论哪种赋值,都必须满足左边的类型和右边的类型,也就是A B。

  

Java 中的协变与逆变

  有了前面的基础知识,你就很容易解释协变和倒置了。

  如果一个反式变换后得到的反式(A)和反式(B)仍然满足反式(A)反式(B),则称为协变

  倒置正好相反。如果A类和B类,变换trans后得到的trans(A)和trans(B)满足trans(B) trans(A),称为逆变

  比如大家都知道Java数组是协变的。如果一个B,那么就有一个[] B[],所以B[]可以赋给A[]。例如:

  Integer[]nums=new Integer[]{ };

  object[]o=nums;//可以赋值。由于数组的协变性质,Object Integer得到Object[] Integer[]复制代码,但是Java的泛型对这种改变并不满意,如下:

  list integer l=new ArrayList();

  ListObject o=l;//这里会报错,复制的代码无法编译。上面的代码报错是因为,虽然改变了Object Integer,但是ListObject ListInteger不满足,因为泛型不满意足协的改变。既然不满足左侧大于右侧的条件,从前面的话我们知道,自然ListObject就不能赋ListObject。一般来说,Java泛型不支持类型化。

  

Java 中泛型如何实现协变与逆变

  从前面我们知道了Java中泛型不支持类型化,但是这样会导致一种奇怪的混乱,很多关于泛型的文章中也提到了这一点:

  如果B是A的子类,那么ListB应该是ListA的子类!这是很自然的想法!

  但是很抱歉,Java因为各种原因不支持。但是,Java并没有完全抹杀泛型的变体特征。Java提供了什么?延伸T和?t使得泛型具有协变和倒置的特性。

  

? extends T 与 ? super T

   ?Endst称为上限通配符,t称为下界通配符。上限通配符可以协变泛型,下限通配符可以反转泛型。

  例如,前面的例子

  list integer l=new ArrayList();

  ListObject o=l;//这里会报错,复制的代码无法编译。如果使用上限通配符,

  list integer l=new ArrayList();

  列表?扩展对象o=l;//可以这样编译复制代码,List?extends对象的类型大于ListInteger的类型,实现了协变。这就是所谓的“泛型的子类就是泛型的子类”。

  同样,下界通配符?t可以实现倒置,例如:

  公共列表?超级整数fun(){

  ListObject l=new ArrayList();返回l;

  }复制代码。上面的代码怎么倒过来?首先,对象整数;另外,从前面的话我们知道,函数的返回值类型必须大于实际返回值类型,这里是List?超级整数ListObject,正好和Object Integer相反。也就是说,泛型改变后,Object和Integer的类型关系反转,称为反转,用下界通配符实现反转?超级T .

  从上面可以看出,extends T中的上界是T,也就是说?extends T引用的类型都是T或者T本身的子类,所以T大于?扩展T .超中的下界是T,也就是说?“super”所指的类型是T的父类或者T本身,那么?T大于T.

  虽然Java用通配符解决了泛型的共变和倒置问题,但是很多关于泛型的文章都晦涩难懂,一度让我觉得tm是什么。直到我在stackoverflow上找到了一个通俗易懂的解释(没错,前面大部分内容都来自stackoverflow里大神的解释),我才终于明白了。其实只要抓住赋值语句左边类型必须大于右边类型这个关键点,一切就好理解了。

  

PECS

   PECS标准是生产者扩展消费者超级。生产者使用上限通配符,消费者使用下限通配符。直接看这句话可能会比较混乱,我们追根溯源,看看为什么会有这句话。

  首先,我们编写一个简单的泛型类:

  public class container T { private T item;公共无效集(T t) {

  item=t;

  } public T get(){ return item;

  }

  }复制代码,然后编写以下代码:

  container object c=new container string();//(1)编译错误容器?扩展对象c=new container string();//(2)通过c.set(sss )编译;//(3)编译错误对象o=c . get();//(4)通过复制代码code进行编译(1),container object c=newcontainerstring();编译错误,因为泛型是不可变的,ContainerString不是ContainerObject的子类型,所以不能赋值。

  代码(2),添加上限通配符后,支持泛型协变,ContainerString变成容器?EndsObject的子类型,所以它是编译过的,可以赋值。

  既然代码(2)编译了,为什么代码(3)报错?因为代码(3)试图将字符串类型赋给?Endsobject类型。很明显,编译器只知道?Endsobject是Obejct的一个子类型,但具体是哪一个未知。它可能不是字符串类型,所以不能直接给它赋字符串类型。

  从上面可以看出,对于使用?extends T的类型不能写入元素,否则它将编译并报告一个错误,如代码(3)所示。

  但是可以读取元素,例如代码(4)。而这种类型只能读取元素。这就是所谓的“生产者”,也就是只能从中读取元素的就是生产者,生产者用?Endst通配符。

  对于消费者,代码如下:

  container string c=new container object();//(1)编译错误容器?超级字符串c=new container object();//(2)编译通过

  c . set( SSS );//(3)编译通过

  string s=c . get();//(4)编译时复制出错的代码(1)编译时复制出错的代码,因为泛型不支持反转。而且就算你不懂泛型,这段代码的形式乍一看也不对。

  代码(2)编译通过是因为加了?超级通配符后,泛型反转。

  代码(3)被编译,它将字符串类型赋给?超弦,超级字符串一般指String或String的父类,所以这个可以编译。

  代码(4)编译了一个错误,因为它试图将?超级字符串被赋给字符串,而?超级字符串比字符串大,所以不能赋值。其实编译器根本不知道用什么类型来接受c.get()的返回值,因为在编译器眼里?Super String是一个泛型类型,String和String本身的所有父类都是可能的。

  从上面的代码可以看出,对于使用?类型为super时,不能读取元素,否则它会像代码(4)中那样编译并报告错误。但是您可以编写元素,比如代码(3)。这种类型只能写元素,也就是所谓的“消费者”,也就是只能写入元素的就是消费者,那么消费者用吗?t通配符。

  总结一下,这就是胸肌原理。

  

kotlin 中的协变与逆变

   kotlin放弃了Java中的通配符,转而使用声明处型变类型投影

  

声明处型变

  首先让我们回顾一下容器的定义:

  public class container T { private T item;公共无效集(T t) {

  item=t;

  } public T get(){ return item;

  }

  }复制代码在某些情况下,我们只会使用容器?Endst还是Container?t表示我们只把容器作为生产者,或者把容器作为消费者。

  既然如此,为什么在定义类容器的时候要同时定义get和set呢?试想一下,如果一个类只充当消费者,那么完全没有必要定义get方法。

  相反,如果一个泛型类只有生产者方法,比如下面的例子(来自kotlin官方文档):

  //Javainterface SourceT {

  t nextT();//仅生产者方法}//Java VoidDemo(源字符串strs) {

  SourceObject objects=strs//!Java中不允许。是否要使用上限通配符?扩展对象

  //…}复制代码以将SourceString实例的引用存储在SourceObject 3354类型的变量中是极其安全的,因为没有要调用的消费者方法。但是Java还是不让我们直接赋值,所以需要使用上限通配符。

  但这毫无意义。使用通配符只是让类型变得更复杂,并没有带来额外的价值,因为唯一可以调用的方法是producer方法。但是Java编译器只识别死亡原因。

  那么,如果我们能在使用一个类之前确定它是生产者还是消费者,那么在定义一个类的时候直接声明它的角色岂不是很美?

  这是声明科特林的地方。当一个类被声明时,它的变形行为被直接定义。

  例如:

  类容器out T { //(1)

  私有var项目:T?=空

  fun get(): T?=项目

  }

  val:container any=container string()/(2)被编译,因为t是一个out参数。复制代码(1)时,直接用out T指定T类型只能出现在生产者的位置。虽然有一些限制,但是kotlin编译器知道T的作用后,可以像(2)中那样直接把ContainerString赋给ContainerAny,像泛型直接可以协变了一样,不需要使用Java中的通配符?扩展字符串.

  同样,对于消费者来说,

  T { //(1)中的类容器

  私有var项目:T?=空

  趣味套装(t: T) {

  item=t

  }

  } Val C:container string=container any()/(2)编译,因为T是参数内复制代码。在代码(1)中使用in T来指定T类型只能出现在消费者的位置。代码(2)可以编译传递,任意字符串,但是ContainerString可以用ContainerAny赋值,也就是说ContainerString大于ContainerAny,也就是看起来像T 直接实现了泛型逆变没有任何帮助?超级字符串通配符实现反转。如果是Java代码,需要写成容器?超级字符串c=new container object();

  这是声明处型变。当声明一个类时,使用out和in关键字。使用时,可以直接编写泛型类型的代码。

  但是使用Java时,必须使用通配符来实现泛型变体,也就是使用处型变

  

类型投影

  有时一个类既可以是生产者,也可以是消费者。在这种情况下,我们不能在t之前直接添加in或out关键字。例如:

  class ContainerT { private var item:T?=空

  趣味套装(t: T?) {

  item=t

  } fun get(): T?=项目

  }复制代码考虑这个函数:

  有趣的副本(从:ContainerAny,到:ContainerAny) {

  to.set(from.get())

  }在我们实际使用这个函数的时候复制代码:

  val from=container int()val to=container any()

  Copy(from,to) //报告错误,from是ContainerInt类型,to是ContainerAny类型的复制代码。

  在这种情况下,编译器会报告一个错误,因为我们将值赋给了两种不同的类型。用科特林公文的话说,复制功能就是“做坏事”。它试图从给出一个任意类型的值,我们使用Int类型来接收这个值。如果编译器没有报告错误,那么运行时将抛出一个ClassCastException异常。

  那我该怎么办呢?只是防止被直接写!

  将复印功能更改如下:

  Funcopy (from: container out any,to:container any){//添加到from的类型

  to.set(from.get())

  } val from=container int()val to=container any()

  Copy(from,to) //它不会再报错复制代码了。这是类型投影:from是一个类限制的(投影的)容器类。我们只能用它做生产者,它只能调用get()方法。

  同样,如果from的泛型用in修饰,from只能用作使用者。它只能调用set()方法,上面的代码会报错:

  Funcopy (from: container in any,to:container any){//添加到from的类型中

  to.set(from.get())

  } val from=container int()val to=container any()

  Copy(from,to) //复制有错误的代码

  其实从上面可以看到,类型投影类似于Java的通配符,也是使用时型变的一种。

  

为什么要这么设计?

  为什么Java的数组默认是类型化的,而泛型默认是不可变的?实际上,kolin的泛型在默认情况下是不可变的,只是使用了out和in关键字,使其看起来像泛型变体。

  为什么这么设计?为什么不默认泛型类型?

  我在stackoverflow上找到了答案,参考:stackoverflow.com/questions/1…

  :

总结

   1.Java泛型默认是不可变的,所以ListString不是ListObject的子类。如果你想实现通用变量,你需要吗?延伸T和?t通配符,这是一种使用本地变量的方法。使用?extends T通配符意味着该类是一个生产者,只能调用get (): t .和use?T通配符表示该类是消费者,只能调用set(T t)和add(T t)等方法。

  2.Kotlin泛型实际上是默认不可变的,只是out和in关键字是用来在类声明时进行类型化的,使用时可以达到看起来像直接类型化的效果。但是,这将限制该类被声明为生产者或消费者。

  使用类型投影可以避免类声明的限制,但是在使用的时候要用out和in关键字来表示此时类的角色是消费者还是生产者。文字投影也是一种利用局部变形的方法。

  以上是kotlin从Java协变和反演的细节。请多关注我们的其他相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: