Kotlin——类的继承、解构声明和泛型(六)_kotlin 继承泛型_CrazyMo_的博客-程序员秘密

技术标签: 类继承  解构  泛型  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等,概念也大同小异参照对比着学习或许更能理解

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/CrazyMo_/article/details/79500094

智能推荐

省份城市的三级联动_da_yu_hai_tang的博客-程序员秘密

省份城市的三级联动效果图:&lt;!DOCTYPE html&gt;&lt;html lang="en"&gt;&lt;head&gt; &lt;meta charset="UTF-8"&gt; &lt;title&gt;Document&lt;/title&gt; &lt;style&gt; select{ width: 80px; margin: 5px; ...

python – 将ConfigParser.items(”)转换为字典_configparser 字典_水木云的博客-程序员秘密

如何将ConfigParser.items(‘section’)的结果转换为字典来格式化一个字符串,如下所示:import ConfigParserconfig = ConfigParser.ConfigParser()config.read('conf.ini')connection_string = ("dbname='%(dbname)s' user='%(dbuser)s' host='%(host)s' " "password=..

android ViewPager + TabLayout 使用,并且解决 子fragmeng页面数据刷新问题_朱玉松的博客-程序员秘密

xml:&amp;lt;android.support.design.widget.TabLayout android:id=&quot;@+id/tabLayout&quot; android:layout_width=&quot;match_parent&quot; android:layout_height=&quot;wrap_content&quot; app:tabIndicatorColor=&quot;#666666&quot;...

SequoiaDB巨杉数据库-配置_engine=sequoiadb_RoyLQ的博客-程序员秘密

本文档将介绍 SequoiaDB 巨杉数据库中 MySQL 实例的相关配置。支持的建表选项选项 默认值 描述 AUTO_INCREMENT 1 自增字段的起始值,SequoiaDB 的自增字段不是严格递增,而是趋势递增,可参考 SequoiaDB自增字段章节 CHARACTER SET utf8mb4 字符数据的字符集 COLLATE utf8mb4_bin 字符数据的比较规则,不支持忽略大小写的字符比较规则,字符比较对大小写敏感 C.

数据结构精录&总结Episode.3 数据结构入门之栈和队列详解(基于Visual C++)_笙歌散尽的博客-程序员秘密

通信作业做了一整个晚上,突然想起自己今天忘记更新博文了。。难顶。。然而做信息论作业的时候,我突然想起之前和小组成员研究香农编码方法的C语言实现时,正好就用到了队列算法的一点点思想解决编码过程中反复迭代对比的工序,这也许是一种巧合,更多的还是证明了最最基本的数据结构算法在工程上的强大应用潜力。之前记得看到过一篇IT杂志上的文章,讲的是拥有十年从业经验的顶级高校毕业的软件工程师,面试新公司时被...

随便推点

java class getfield_java.lang.Class.getField()方法实例_weixin_39965490的博客-程序员秘密

全屏java.lang.Class.getField()返回一个Field对象,它反映此Class对象所表示的类或接口的指定公共成员字段。 name参数是一个字符串,指定所需字段的简单名称。声明以下是java.lang.Class.getField()方法的声明publicFieldgetField(Stringname)throwsNoSuchFieldException,Secu...

中软国际python机试题_中软国际入职机试题[优质资料]_孙硕星的博客-程序员秘密

资料-参考11邮箱地址合法性验证,合法输出字符“1”,不合法输出字符“0”合法条件:A仅包含一个“@”。B最后三位必须是“.com”C字符直接没有空格B有效数组是1~9、a~z、A~Z、“.”、“@”、“_”程序#includeintmain(){charinput[100],out=1;intlen,i,flag=0,flag1=0,flag2=0,flag3=0,ttt=0;scanf("%s...

【python 图像识别】python 身份证号码识别_pytorch 身份证识别_东华果汁哥的博客-程序员秘密

一、需求分析 识别身份证图片上的身份证号码。 如: 二、python实现源代码 # !/usr/bin/python#-*-coding:utf-8-*-import sysreload(sys)sys.setdefaultencoding('utf-8')import timetime1 = time.time()from PIL import Imageimport pyte

Java导出excel基于velocity模板引擎_excel模板引擎_sha1024的博客-程序员秘密

项目地址: GitHub https://github.com/wusas/java-excel-velocity项目结构:├─.idea│ ├─inspectionProfiles│ ├─libraries│ └─sonarlint│ └─issuestore├─.mvn│ └─wrapper├─src│ ├─main│ │ ├─java│ ...

Python Web数据抓取(xpath版)_Yatere的博客-程序员秘密

(1)采用SQLite缓存抓取的HTML页面,大大提高了二次数据处理的效率。第一次运行程序大约耗时6小时,以后只需3分钟左右即可完成。(2)采用xpath替换之前的正则表达式进行HTML解析。xpath定位更加简单、方便,而且能够自动修正html错误语法。xpath真强大!!(3

R包的一些细分_Nicky_1218的博客-程序员秘密

基于R,仅供自食。相信自己,每天多学一点。(本文为转载,地址:https://www.douban.com/note/579491229/)【下面列出每个步骤最有用的一些R包】1.数据导入以下R包主要用于数据导入和保存数据:feather:一种快速,轻量级的文件格式;在R和python上都可使用readr:实现表格数据的快速导入readxl:读取Microsoft Excel电子表格数据openx...

推荐文章

热门文章

相关标签