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的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。