Kotlin——类的继承、解构声明和泛型

引言

通过前面几篇文章大致把Kotlin 语言的基本体系结构浏览了一遍

一、类的继承

与Java普通类 类似万物基于Object,Kotlin普通类也基于Any,所有的类都默认继承Any。Any默认也提供了三个函数:equals()、toString()、hashCode(),但Any 不是Object,Any类中只有这三个成员。与Java 不同,Kotlin的类默认是不可被继承的final 类型,如果想要作为基类被继承必须使用open 关键字修饰(抽象类也可被继承)。Kotlin中实现接口和继承都是使用关键字符冒号 :(子类 : 基类1,基类2)

1、子类对于构造函数的处理

1.1、当子类有主构造函数

如果子类有主构造函数, 则基类必须在主构造函数中立即初始化。

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

1.2、当子类没有主构造函数

如果子类没有主构造函数,则必须在每一个次要构造函数或代理另一个构造函数中用 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}")

    }
}

2、方法的覆盖

基类中,使用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<A>.f()//调用 A.f()
        super<B>.f()//调用 B.f()
    }
}

fun main(args: Array<String>) {
    val c =  C()
    c.f();
}

3、属性的覆盖

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
}

4、继承需遵守的规则

  • 如果一个类从其直接的超类继承同一个成员的许多实现,它必须覆盖该成员并提供自己的实现
  • 要表示从其继承的实现的超类型,我们在尖括号中使用超类型名称的超级限定 如 super<Base>
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<A>.f() // call to A.f()
        super<B>.f() // call to B.f()
    }
}

二、解构声明Destructuring Declarations

所谓解构声明就是把对象属性分解并映射到对应的变量中,具体语法形式就像是把对象赋值到一组自定义的变量集中(变量名称不一定要对应对象的属性名、数量只要不大于原对象属性的个数且自Kotlin1.1起还可以使用下划线⽤于未使⽤的变量即可),Kotlin自动会根据变量定义的顺序一一映射。

1、对象的解构声明

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() 的返回值,为使其能⽤解构声明,事实上标准库也是提供了这样的扩展

  • 通过提供⼀个 iterator() 函数将映射表⽰为⼀个值的序列
  • 通过提供函数 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-循环中对映射(以及数据类实例的集合等)⾃由使⽤解构声明。

2、Lambda表达式的解构声明

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!" }

三、泛型Generics

所谓泛型,即 “参数化类型”,将类型参数化,可以用在类,接口,方法上为类型安全提供保证,消除类型强转的烦恼。

1、泛型的定义

声明一个泛型

class Box<T>(t: T) {
    val value = t
}

创建泛型类,通常需要提供具体的类型,但是如果类型参数可以推断出来,则类型参数可省(比如从构造函数的参数或者从其他途径)

val box: Box<Int> = Box<Int>(10)

val box2 = Box(10) // 10 具有类型 Int,所以编译器知道我们说的是 Box<Int>。

val box3= Box<String?>(null)//第三个对象接收一个null引用,那仍然还是需要指定它的类型,因为它不能去推断出来

当我们想限制上一个类中为非null类型,我们只需要这样定义泛型类T: Any

//你将看到t3现在会抛出一个错误。可null类型不再被允许了
class Box<T: Any>(t: T) {
    val value = t
}

同理我们严格限制到泛型类只能是某一类的子类,可以这样定义T: Xxxx,以只能是View 的子类为例

//只有是View 的子类才能使用这个泛型类
class MyView<T: View>(t: T) {
    val value = t
}

函数中使用泛型

fun <T> genericFun(item: T): List<T> {
    ...
}

2、型变Variance

我们都知道Java普通泛型是不型变的(即List<String> 并不是 List<Object>的子类),所以Java 会通过通配符来保证类型安全。而Kotlin中是没有通配符的,它是通过声明处型变(declaration-site variance)类型投影(type projections)两种机制来实现。(无论是Java还是Kotlin 这部分都是有点难理解的,作为初级阶段先会使用泛型,再跟着官方的引导思路来学习下,而且型变是主要用于提升API的灵活性)。首先,Effect Java中介绍Java 中引入通配符的作用在于——使用有界通配符来提高api 的灵活性,比如以下代码

// 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<E> items);
}

那就意味着以下的代码可以成功运行,但是却不能

// Java
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from); //对于这种简单声明的 addAll 将不能编译,因为Collection<String> 不是 Collection<Object> 的⼦类型
}

在Effective Java第25条中总结了一个教训——列表优先于数组,而实际上addAll() 的实际签名是有限制的泛型

// Java
interface Collection<E> …… {
void addAll(Collection<? extends E> items);
}

Java中 通配符 ? extends E表示此方法接受E的对象及其某些子类型的集合,即所谓的协变(covariant),协变则意味着我们可以安全地从其中读取 E(该集合中的元素是 E的⼦类的实例) ,但不能写⼊(因为我们不知道写入的对象是否符合那个未知的 E 的⼦类型)。 反之,该限制可以让Collection<String> 表⽰为Collection< ? extends Object > 的⼦类型;而如果只能从集合中获取项⽬,那么使⽤ String 类型即可, 而且从其中读取 Object 也没问题 。反之,如果只能向集合中写入项⽬,就可以⽤ Object 集合并向其中放⼊ String,因为在 Java 中有 List< ? super String> 是 List<Object> 的⼀个父类。? super E表示此接受E的对象及其父类,即所谓的逆变(contravariance)。因而Joshua Bloch 称那些你只能从中读取的对象为⽣产者,并称那些你只能写⼊的对象为消费者。他建议:“为了灵活性最⼤化,在表⽰⽣产者或消费者的输⼊参数上使⽤通配符类型。当然以上是Java 的泛型知识,下文才是Kotlin解决以上Java问题的机制。

2.1、 声明处型变Declaration-site variance

所谓声明处的型变就是在声明时候使用协变注解修饰符修饰参数in消费者 和 生产者 out
使用 out 使得一个类型参数协变,协变类型参数只能用作输出,可以作为返回值类型但是无法作为入参的类型。接下来看一个通过使用out 修饰符来确保Source 的类型参数 T 来确保仅从 Source 成员中返回(即⽣产),并从不被消费

abstract class Source<out T> {
abstract fun nextT(): T
}
fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // 编译通过,因为 T 是⼀个 out参数
    // ……
}

通常,当⼀个类 C 的类型参数 T 被 out修饰时,它就只能出现在 C 的成员的输出-位置,但是 C<Base> 可以安全地作为C<Derived> 的超类返回(but in return C<Base> can safely be a supertype of C<Derived>,其中Base 是基类的意思,Derivd是派生类,即代表Derived类型是Base 的子类的意思)。以生产者和消费者角度来说,当类 C 是在参数 T 上是协变的或者说 T 是⼀个协变的类型参数时,C是T的生产者而非T的消费者。再看一个协变的例子

// 定义一个支持协变的类
class GenericDemo<out T>(val t: T) {
    fun show(): T {
        return t
    }
}

  var str: GenericDemo<String> = GenericDemo("String 类型")
  var any: GenericDemo<Any> = GenericDemo<Any>("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<Number>) {
x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的⼦类型
// 因此,我们可以将 x 赋给类型为 Comparable <Double> 的变量
val y: Comparable<Double> = 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>("Any 类型")
    strDCo = anyDCo
}

2.2、使用处型变:类型投影Use-site variance: Type projections

前面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<Any>, to: Array<Any>) {
    assert(from.size == to.size)
    for (i in from.indices)
    to[i] = from[i]
}

val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" }
copy(ints, any) // 错误:期望 (Array<Any>, Array<Any>)

因为Array <T> 在 T 上是不型变的,因此 Array <Int> 和 Array <Any> 都不是另⼀个的⼦类型,为了避免这种种错误Kotlin 提供了在使用处使用in 和out 修饰类型参数的语法即类型投影

fun copy(from: Array<out Any>, to: Array<Any>) {
// ……
}

此处的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() 函数。

2.3、星投影Star-projection

有些时候我们可能想表示你并不知道类型参数的任何信息, 但是仍然希望能够安全地使用它。所谓”安全地使用”是指:对泛型类型定义一个类型投射, 要求这个泛型类型的所有的实体实例都是这个投射的子类型。对于这种情况, 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> , 那么可以出现以下几种星号投射:

  • Function< *, String> , 代表 Function< in Nothing, String> ;
  • Function< Int, *> , 代表 Function< Int, out Any?> ;
  • Function< , > , 代表 Function< in Nothing, out Any?> .

其实星型投影与 Java 的原生类型(raw type)非常类似, 但星型投影安全更。

3、泛型函数

Kotlin和Java一样不仅类可以有泛型类型参数。函数也可以有,类型参数要放在函数名称之前:

fun <T> singletonList(item: T): List<T> {
// ……
}
fun <T> T.basicToString() : String { // 扩展函数
// ……
}

要调⽤泛型函数,在调⽤处函数名之后指定类型参数即可:

val sing = singletonList<Int>(10)

4、泛型约束

Kotlin也可以使用泛型约束来限制一个给定参数允许使用的类型集合。

4.1、 对泛型的的类型上限进行约束。

上限约束对应 Java中 的 extends 关键字对应的 上界:

fun <T : Comparable<T>> sort(list: List<T>) {
    // ……
}

Comparable 的子类型可以替代 T。 例如:

sort(listOf(1, 2, 3)) // OK。Int 是 Comparable<Int> 的子类型
sort(listOf(HashMap<Int, String>())) // 错误:HashMap<Int, String> 不是 Comparable<HashMap<Int, String>> 的子类型

默认的上界是 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等,概念也大同小异参照对比着学习或许更能理解

猜你喜欢

转载自blog.csdn.net/crazymo_/article/details/79500094