通过前面几篇文章大致把Kotlin 语言的基本体系结构浏览了一遍
与Java普通类 类似万物基于Object,Kotlin普通类也基于Any,所有的类都默认继承Any。Any默认也提供了三个函数:equals()、toString()、hashCode(),但Any 不是Object,Any类中只有这三个成员。与Java 不同,Kotlin的类默认是不可被继承的final 类型,如果想要作为基类被继承必须使用open 关键字修饰(抽象类也可被继承)。Kotlin中实现接口和继承都是使用关键字符冒号 :(子类 : 基类1,基类2)
如果子类有主构造函数, 则基类必须在主构造函数中立即初始化。
open class People(var name : String, var age : Int){// 基类
}
class Student(name : String, age : Int, var no : String) : People(name, age) {
}
// 测试
fun inherit() {
val s = Student("CrazyMo_", 2, "200912301052", 89)
println("姓名: ${s.name}")
println("年龄: ${s.age}")
println("学号: ${s.no}")
}
//结果
学生名: CrazyMo_
年龄: 2
学生号: 200912301052
如果子类没有主构造函数,则必须在每一个次要构造函数或代理另一个构造函数中用 super 关键字初始化基类。初始化基类时,可以使用super来调用基类的不同构造函数。
//用户基类
open class People(name:String){
//次要构造函数
constructor(name:String,age:Int):this(name){
println("基类次要构造函数")
}
}
/**子类继承 People类**/
class Student:People{
/**次要构造函数**/
constructor(name:String,age:Int,no:String):super(name,age){
println("继承类次要构造函数")
println("学生名: ${name}")
println("年龄: ${age}")
println("学生号: ${no}")
}
}
基类中,使用fun声明函数时(默认为final修饰)不能被子类重写。如果要允许子类重写该函数,就要使用 open 关键字修饰, 子类重写方法使用 override 关键字。
open class People(var name : String, var age : Int){// 基类
open fun show(){
println("基类中的show函数")
}
}
class Student(name : String, age : Int, var no : String, var score : Int) : People(name, age) {
//子类中的方法签名必须和父类的一样
override fun show(){
println("子类中的show函数")
}
}
若有多个相同的方法(比如同时继承或者实现自其他多个类),则必须要重写该方法并使用super范型去选择性地调用父类的实现。
open class A {
open fun f () { print("A") }
fun a() { print("a") }
}
interface B {
fun f() { print("B") } //接口的成员变量默认是 open 的
fun b() { print("b") }
}
class C() : A() , B{
override fun f() {
super.f()//调用 A.f()
super.f()//调用 B.f()
}
}
fun main(args: Array) {
val c = C()
c.f();
}
Kotlin默认的属性和函数一样也是不可被覆盖的,都需要使用open 来显示注明可被覆盖,属性覆盖使用 override 关键字,属性必须具有兼容类型,每一个声明的属性都可以通过初始化程序或者getter方法被覆盖,可以使用var 属性来覆盖一个 val 属性,但是反过来不行。(因为val属性本身定义了getter方法,重写为var属性会在衍生类中额外声明一个setter方法)
open class Foo {
open val x: Int get { …… }
}
class Bar1 : Foo() {
override val x: Int = ……
}
还可以在主构造函数中使用 override 关键字作为属性声明的一部分
interface Foo {
val count: Int
}
class Bar1(override val count: Int) : Foo
class Bar2 : Foo {
override var count: Int = 0
}
open class A {
open fun f() { print("A") }
fun a() { print("a") }
}
interface B {
fun f() { print("B") } // interface members are 'open' by default
fun b() { print("b") }
}
class C() : A(), B {
// The compiler requires f() to be overridden:
override fun f() {
super.f() // call to A.f()
super.f() // call to B.f()
}
}
所谓解构声明就是把对象属性分解并映射到对应的变量中,具体语法形式就像是把对象赋值到一组自定义的变量集中(变量名称不一定要对应对象的属性名、数量只要不大于原对象属性的个数且自Kotlin1.1起还可以使用下划线⽤于未使⽤的变量即可),Kotlin自动会根据变量定义的顺序一一映射。
data class User(val name: String, val age: Int)
val user = User(name = "CrazyMO_", age = 100)
var (name2,age2) = user//这样的语法就是解构声明
//var (_,age2) = user//下划线来替代不想要的属性
println("解构出来的name:"+name2+"解构出来的age:"+ age2)
对于解构声明会被编译成componentN()
var name2 = user.component1()
val age2= user.component2()
其中的 component1() 和 component2() 函数是在 Kotlin 中⼴泛使⽤的约定原则 的另⼀个例⼦。实际上任何表达式都可以出现在解构声明的右侧,只要可以对它可以调⽤所需数量的 componentN 函数(数据类、for循环系统已经提供了对应的扩展)即可。所以可以有 component3() 和 component4() 等等。请注意componentN() 函数需要⽤ operator 关键字标记,以允许在解构声明中使⽤。这个特性背后的逻辑是非常强大的,比如解构声明也可以⽤在 for-循环中
//解构声明也可以⽤在 for-循环中:当你写
for ((a, b) in collection) { …… }
在很多情况下可以帮助我们简化代码。再比如集合Map类含有一些扩展函数的实现,允许它在迭代时使用key和value
for ((key, value) in map) {
println("map", "key:$key, value:$value")
}
变量 key 和 value的值取⾃对集合中的元素上调⽤ component1() 和 component2() 的返回值,为使其能⽤解构声明,事实上标准库也是提供了这样的扩展
operator fun <K, V> Map<K, V>.iterator(): Iterator<Map.Entry<K, V>> = entrySet().iterator()
operator fun <K, V> Map.Entry<K, V>.component1() = getKey()
operator fun <K, V> Map.Entry<K, V>.component2() = getValue()
因此你可以在 for-循环中对映射(以及数据类实例的集合等)⾃由使⽤解构声明。
Kotlin1.11起,你可以对 lambda 表达式参数使⽤解构声明语法,如果 lambda 表达式具有 Pair 类型(或者 Map.Entry 或任何其他具有相应 componentN 函数的类型)的参数,那么可以通过将它们放在括号中来引⼊多个新参数来取代单个参数:
map.mapValues { entry -> "${entry.value}!" }
map.mapValues { (key, value) -> "$value!" }
声明⼀个解构对来取代单个参数之间的区别:
{ a //-> …… } // ⼀个参数
{ a, b //-> …… } // 两个参数
{ (a, b) //-> …… } // ⼀个解构对
{ (a, b), c //-> …… } // ⼀个解构对以及其他参数
如果解构的参数中的⼀个组件未使⽤,那么可以将其替换为下划线,以避免编造其名称:
map.mapValues { (_, value) -> "$value!" }
你可以指定整个解构的参数的类型或者分别指定特定组件的类型:
map.mapValues { (_, value): Map.Entry<Int, String> -> "$value!" }
map.mapValues { (_, value: String) -> "$value!" }
所谓泛型,即 “参数化类型”,将类型参数化,可以用在类,接口,方法上为类型安全提供保证,消除类型强转的烦恼。
声明一个泛型
class Box<T>(t: T) {
val value = t
}
创建泛型类,通常需要提供具体的类型,但是如果类型参数可以推断出来,则类型参数可省(比如从构造函数的参数或者从其他途径)
val box: Box = Box(10)
val box2 = Box(10) // 10 具有类型 Int,所以编译器知道我们说的是 Box。
val box3= Box(null)//第三个对象接收一个null引用,那仍然还是需要指定它的类型,因为它不能去推断出来
当我们想限制上一个类中为非null类型,我们只需要这样定义泛型类T: Any
//你将看到t3现在会抛出一个错误。可null类型不再被允许了
class Box(t: T) {
val value = t
}
同理我们严格限制到泛型类只能是某一类的子类,可以这样定义T: Xxxx,以只能是View 的子类为例
//只有是View 的子类才能使用这个泛型类
class MyView(t: T) {
val value = t
}
在函数中使用泛型
fun <T> genericFun(item: T): List<T> {
...
}
我们都知道Java普通泛型是不型变的(即List
// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // 语法错误,即将来临的问题的原因就在这⾥。Java 禁⽌这样!
objs.add(1); // 这⾥我们把⼀个整数放⼊⼀个字符串列表
String s = strs.get(0); // ClassCastException:⽆法将整数转换为字符串
因此,Java 机制通过禁⽌这样的事情来保证运⾏时的安全,但会产生影响。比如说 Collection 接⼝中的 addAll() ⽅法,直觉上或许我们会猜想是以下的签名
// Java
interface Collection<E> …… {
void addAll(Collection items);
}
那就意味着以下的代码可以成功运行,但是却不能
// Java
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from); //对于这种简单声明的 addAll 将不能编译,因为Collection 不是 Collection
}
在Effective Java第25条中总结了一个教训——列表优先于数组,而实际上addAll() 的实际签名是有限制的泛型
// Java
interface Collection<E> …… {
void addAll(Collection extends E> items);
}
Java中 通配符 ? extends E表示此方法接受E的对象及其某些子类型的集合,即所谓的协变(covariant),协变则意味着我们可以安全地从其中读取 E(该集合中的元素是 E的⼦类的实例) ,但不能写⼊(因为我们不知道写入的对象是否符合那个未知的 E 的⼦类型)。 反之,该限制可以让Collection
所谓声明处的型变就是在声明时候使用协变注解修饰符修饰参数:in消费者 和 生产者 out。
使用 out 使得一个类型参数协变,协变类型参数只能用作输出,可以作为返回值类型但是无法作为入参的类型。接下来看一个通过使用out 修饰符来确保Source 的类型参数 T 来确保仅从 Source 成员中返回(即⽣产),并从不被消费。
abstract class Source {
abstract fun nextT(): T
}
fun demo(strs: Source) {
val objects: Source = strs // 编译通过,因为 T 是⼀个 out参数
// ……
}
通常,当⼀个类 C 的类型参数 T 被 out修饰时,它就只能出现在 C 的成员的输出-位置,但是 C
// 定义一个支持协变的类
class GenericDemo<out T>(val t: T) {
fun show(): T {
return t
}
}
var str: GenericDemo = GenericDemo("String 类型")
var any: GenericDemo = GenericDemo("Any 类型")
println(any.show())
any = str//但是不能str=any
println(any.show())
而in 使得一个类型参数逆变,逆变类型参数只能用作输入,可以作为入参的类型但是无法作为返回值的类型,in修饰符与out相反(其实in、out、setter、getter都是借鉴C#的思想,如果还不理解的话可以去查查C#的相关资料)
abstract class Comparable<in T> {
abstract fun compareTo(other: T): Int
}
fun demo(x: Comparable) {
x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的⼦类型
// 因此,我们可以将 x 赋给类型为 Comparable 的变量
val y: Comparable = x // OK!
}
再比如
// 定义一个支持逆变的类
class Generic<in T>(t: T) {
fun foo(t: T) {
}
}
fun main(args: Array<String>) {
var strDCo = Generic("String 类型")
var anyDCo = Generic("Any 类型")
strDCo = anyDCo
}
前面in 和out 注解修饰符都是用在声明处使用的,所以这种语法就是所谓的声明处型变。同样的Kotlin还提供了在使用处型变即所谓的类型投影。引入使用处型变的初衷是因为有时候有些泛型类的类型参数T既不能是协变也不能是逆变,如
class Array<T>(val size: Int) {
fun get(index: Int): T { ///* …… */ }
fun set(index: Int, value: T) { ///* …… */ }
}
在使用的时候会有一些类型转换的麻烦,例如将项⽬从⼀个数组复制到另⼀个数组时
fun copy(from: Array, to: Array) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
val ints: Array = arrayOf(1, 2, 3)
val any = Array(3) { "" }
copy(ints, any) // 错误:期望 (Array, Array)
因为Array
fun copy(from: Array<out Any>, to: Array) {
// ……
}
此处的out 修饰的from 是一个受限制的数组(我们只可以调⽤返回类型为类型参数 T 的⽅法即只能调用 get()),这种语法功能相当于是Java中的Array< ? extends Object>;当然也可以使用in来修饰
fun fill(dest: Array<in String>, value: String) {
// ……
}
in 修饰则对应Java 中的 Array< ? super String> ,即你可以传递⼀个 CharSequence 数组或⼀个 Object 数组给fill() 函数。
有些时候我们可能想表示你并不知道类型参数的任何信息, 但是仍然希望能够安全地使用它。所谓”安全地使用”是指:对泛型类型定义一个类型投射, 要求这个泛型类型的所有的实体实例都是这个投射的子类型。对于这种情况, Kotlin 提供了一种语法称为 星型投影(star-projection)。
如果类型定义为 Foo< out T> , 其中 T 是一个协变的类型参数, 上界(upper bound)为 TUpper ,Foo<> 等价于 Foo< out TUpper> . 它表示, 当 T 未知时, 你可以安全地从 Foo<> 中 读取TUpper 类型的值.
如果类型定义为 Foo< in T> , 其中 T 是一个反向协变的类型参数, Foo<> 等价于 Foo< inNothing> . 它表示, 当 T 未知时, 你不能安全地向 Foo<> 写入 任何东西.
如果类型定义为 Foo< T> , 其中 T 是一个协变的类型参数, 上界(upper bound)为 TUpper , 对于读取值的场合, Foo< *> 等价于 Foo< out TUpper> , 对于写入值的场合, 等价于 Foo< in Nothing> .
如果一个泛型类型中存在多个类型参数, 那么每个类型参数都可以单独的投射. 如果类型定义为interface Function< in T, out U> , 那么可以出现以下几种星号投射:
其实星型投影与 Java 的原生类型(raw type)非常类似, 但星型投影安全更。
Kotlin和Java一样不仅类可以有泛型类型参数。函数也可以有,类型参数要放在函数名称之前:
fun singletonList(item: T): List {
// ……
}
fun T.basicToString() : String { // 扩展函数
// ……
}
要调⽤泛型函数,在调⽤处函数名之后指定类型参数即可:
val sing = singletonList(10)
Kotlin也可以使用泛型约束来限制一个给定参数允许使用的类型集合。
上限约束对应 Java中 的 extends 关键字对应的 上界:
fun > sort(list: List) {
// ……
}
Comparable 的子类型可以替代 T。 例如:
sort(listOf(1, 2, 3)) // OK。Int 是 Comparable 的子类型
sort(listOf(HashMap() )) // 错误:HashMap 不是 Comparable> 的子类型
默认的上界是 Any?。
对于多个上界约束条件,可以用 where 子句:
fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>
where T : Comparable, Cloneable {
return list.filter(it > threshold).map(it.clone())
}
由于泛型无论是在Java就还是Kotlin中知识体系都比较复杂,此篇文章只是对于基本的概念和用法进行的总结,要想更好滴掌握泛型可以参考Java、C#其他语言对照着深入学习,其实Kotlin本身就是借鉴了很多其他语言的思想Java、C#、Javascript等,概念也大同小异参照对比着学习或许更能理解