Kotlin学习日志(五)类与对象

时间:2022-07-25
本文章向大家介绍Kotlin学习日志(五)类与对象,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

一、类的构造

1.1 类的简单定义

首先来看看在Android中Java的MainActivity

public class MainActivity extends AppCompatActivity {
	...
}

再看看Kotlin中的MainActivity

class MainActivity : AppCompatActivity() {
	...
}

通过上述的代码比较,Kotlin对类的写法与Java之间有以下几点区别: (1)Kotlin省略了关键字public,因为Kotlin默认类是开放的,所以不需要这个关键字。 (2)Kotlin用冒号“:”代替extends,也就是通过冒号表示继承关系。 (3)Kotlin进行继承时,父类后面多了括号“()”。

然后我们自己新建名为Animal的Kotlin类 步骤: 鼠标右键你的包名→New→Kotlin File/Class→创建的文件类型选择Class→OK(创建完成)

然后你就会看到这样的一个图

现在开始编写代码:

package com.llw.kotlinstart

class Animal {
    
    //类的初始化函数
    init {
        //Kotlin的println替换Java的System.out.println
        println("Animal:这是个动物类")
    }
    
}

现在这个类已经创建好了,并且有了初始化函数,我们在MainActivity.kt中来实例化这个类,代码如下: activity_main.xml代码

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:gravity="center_horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <TextView
        android:textColor="#000"
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <TextView
        android:textColor="#000"
        android:id="@+id/tv_result"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <LinearLayout
        android:gravity="center"
        android:layout_marginTop="20dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <Button
            android:id="@+id/btn_test"
            android:text="Test"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </LinearLayout>


</LinearLayout>

MainActivity.kt代码

package com.llw.kotlinstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.*

class MainActivity : AppCompatActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btn_test.setOnClickListener {
            //因为根据等号后面的构造函数已经明确知道这是个Animal的实例
            //所以声明对象时可以不用指定它的类型
            var animal = Animal()
            tv_result.text = "简单类的初始化结果见日志"
        }

    }



}

运行效果图如下:

经过这一番操作,我们再与Java对比一下区别: (1)Kotlin对类进行初始化的函数名称叫init,不像Java那样把雷鸣作为构造函数的名称。 (2)Kotlin打印日志使用类似C语言的println方法,而非Java的System.out.println (3)Kotlin创建实例时省略了关键字new。 这里面,初始化函数init看似是Kotlin对类的构造函数,但它只是构造函数的一部分,并不完整,因为没有定义输入参数,那么怎么定义呢?谁来定义呢?

1.2 类的构造函数

入参的类定义代码如下:

//如果主构造函数没有带@符号的注解说明,类名后面的constructor就可以省略
    class AnimalMain constructor(context:Context,name:String){
    //class AnimalMain (context:Context,name:String){
        init {
            context.toast("这是头$name")
        }
    }

一个类可能有多个构造函数,Java可以通过覆写带不同参数的构造函数来实现,那么Kotlin已经在类名后面指明了固定数量的入参,又该如何表示拥有其他参数的构造函数呢?针对这个问题,Kotlin引入了主构造函数与二级构造函数的概念,之前的代码演示的是主构造函数,分为两部分,跟在类名后面的参数是主构造函数的入参,同时init方法是主构造函数的内部代码,至于二级构造函数,则可以在类内部直接书写完整的函数表示式,新建一个名为AnimalMain的类,代码如下:

class AnimalMain constructor(context:Context,name:String){
        init {
            context.toast("这是头$name")
        }

        constructor(context: Context,name: String,sex:Int) : this(context,name){
            var sexName:String = if(sex ==0 ) "公" else "母"
            context.toast("这头${name}是${sexName}的")
        }
    }

从以上代码可以看出,二级构造函数和普通函数相比有以下两个区别: (1)二级构造函数没有函数名称,只用关键字constructor表示这是一个构造函数。 (2)二级构造函数需要调用主构造函数。“this(context,name)”这句代码在Java中要以“super(context,name)”的形式写在函数体内部,在Kotlin中则以冒号开头补充到输入参数后面,这意味着二级构造函数实际上是从主构造函数派生出来的,也可以看作二级函数的返回值是主构造函数。 由此看来,二级构造函数从属于主构造函数,如果使用二级构造函数声明该类的实例,系统就会先调用主构造函数的init代码,再调用二级构造函数的自身代码,现在若想声明AnimalMain类的实例,既可通过主构造函数,也可通过二级构造函数,代码如下:

package com.llw.kotlinstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.*

class MainActivity : AppCompatActivity() {


    var animalName:String = ""
    var animalSex:Int = 0
    var count:Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)


        btn_test.setOnClickListener {
            setAnimalInfo()
            when(count%2){
                0 -> { var animal = AnimalMain(this,animalName) }
                else -> { var animal = AnimalMain(this,animalName,animalSex)  }
            }
            count++
        }

    }

    fun setAnimalInfo() {
        animalName = "牛"
        animalSex = 0
    }


}

上面代码在运作过程中,通过二级构造函数声明实例有一个问题,就是toast会弹窗两次,因为主构造函数的init方法已经弹窗,然后二级构造函数自身再次弹窗,那能不能不调用主构造函数呢?为了解决该问题,Kotlin设定了主构造函数时不是必需的,也就是说类可以把几个构造函数都放在类内部定义,从而都变成二级构造函数,如此就去掉了主构造函数,为了直观,重新建名为一个AnimalSeparate的类,代码如下

package com.llw.kotlinstart

import android.content.Context
import org.jetbrains.anko.toast

class AnimalSeparate {
    constructor(context: Context,name:String){
        context.toast("这是头$name")
    }
    constructor(context: Context,name: String,sex:Int){
        var sexName:String = if(sex ==0 ) "公" else "母"
        context.toast("这头${name}是${sexName}的")
    }
}

这样写就没有主构造函数了,都是二级构造函数,直接使用即可,函数之间没有从属关系,不存在重复调用。

1.3 带默认参数的构造函数

说到默认参数,不知道你有没有想起之前的带默认参数的函数呢?上面的代码中,两个构造函数之间只有一个输入参数的区别,所以完全可以把二者合二为一,变成一个带默认参数的主构造函数,新的主构造函数既能输入两个参数,又能输入三个参数,新创建一个类AnimalDefault,代码如下:

package com.llw.kotlinstart

import android.content.Context
import org.jetbrains.anko.toast

class AnimalDefault (context: Context,name:String,sex:Int = 0){
    init {
        var sexName:String = if(sex == 0) "公" else "母"
        context.toast("这只${name}是${sexName}的")
    }
}

运行效果类似,但是代码更加的简洁了。

二、类的成员

2.1成员属性

创建一个新的类WildAnimal,然后在构造函数中放两个参数,代码如下:

class WildAnimal(name:String,sex:Int = 0) {
    
}

然后我们再声明对应的属性字段,用于保存入参的数值,加入按照Java的编码思路,下面的代码应该是这样的。

class WildAnimal(name: String, sex: Int = 0) {
    var name: String // 表示动物名称可以修改
    val sex: Int //表示动物性别不能修改

    init {
        this.name = name
        this.sex = sex
    }
}

这上面的写法从Java的角度来看倒是没有问题,但如果时Kotlin呢,代码冗余了, (1)属性字段跟构造函数的入参,二者名称一样,变量类型也一样。 (2)初始化函数中的属性字段赋值,为了区别同名的属性和入参,特意给属性字段添加了this。

那么Kotlin如何精简这个类的代码呢?代码如下:

class WildAnimal(var name: String,val sex: Int = 0) {
    
}

你没有看错,就是这样,接下来使用一下吧。

package com.llw.kotlinstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.*

class MainActivity : AppCompatActivity() {


    var animalName: String = ""
    var animalSex: Int = 0
    var count: Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btn_test.setOnClickListener {
            setAnimalInfo()
            var animal = when (count % 2) {
                0 -> {
                    WildAnimal(animalName)
                }
                else -> {
                    WildAnimal(animalName, animalSex)
                }
            }
            count++

            tv_result.text = "这头${animal.name}是${if (animal.sex == 0) "公" else "母"}的"
        }

    }

    fun setAnimalInfo() {
        animalName = "牛"
        animalSex = 1
    }


}

再看看Java代码中怎么做的

package com.llw.kotlinstart;

public class WildAnimal {
    private String name;
    private String sex;

    public WildAnimal(String name, String sex) {
        this.name = name;
        this.sex = sex;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
}

很熟悉吧,因为基本上每一个实体都离不开这一步, 对比一下: (1)冗余的同名属性声明语句。 (2)冗余的同名属性赋值语句。 (3)冗余的属性获取方法与设置方法。 Kotlin的代码真的精简了很多,鸟枪换炮, 如果某个字段并非入参的同名属性,就需要在类内部显示声明该属性字段,例如,前面WildAnimal类的性别只是一个整型的类型字段,而界面上展示的是性别的中文名称,所以应当给该类补充一个性别名称的属性字段,这样每次访问sexName字段即可获得该动物的性别名称,新建一个名为WildAnimalMember的类,代码如下:

package com.llw.kotlinstart

class WildAnimalMember (val name:String,val sex:Int = 0) {
    //非空的成员属性必须在声明时赋值或者在构造函数中赋值,否则编译器会报错
    var sexName:String
    init {
        sexName = if(sex == 0) "公" else "母"
    }
}

然后再看一下怎么调用这个类:

package com.llw.kotlinstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.*

class MainActivity : AppCompatActivity() {


    var animalName: String = ""
    var animalSex: Int = 0
    var count: Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btn_test.setOnClickListener {
            setAnimalInfo()
            var animal = when (count % 2) {
                0 -> {
                    WildAnimalMember(animalName)
                }
                else -> {
                    WildAnimalMember(animalName, animalSex)
                }
            }
            count++

            tv_result.text = "这头${animal.name}是${animal.sexName}的"
        }

    }

    fun setAnimalInfo() {
        animalName = "牛"
        animalSex = 1
    }


}

2.2 成员方法

类的成员除了成员属性还有成员方法,在类内部定义成员方法的过程和普通函数定义比较类似。下面增加一个获取动物描述信息的成员方法getDesc(),新创建一个名为WildAnimalFunction的类

package com.llw.kotlinstart

class WildAnimalFunction(var name: String, val sex: Int = 0) {
    var sexName: String

    init {
        sexName = if (sex == 0) "公" else "母"
    }

    fun getDesc(tag: String): String {
        return "欢迎来到$tag:这头${name}是${sexName}的"
    }
}

然后我们在MainActivity.kt中调用这个类的方法

package com.llw.kotlinstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.llw.kotlinstart.custom_class.WildAnimalFunction
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {


    var animalName: String = ""
    var animalSex: Int = 0
    var count: Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btn_test.setOnClickListener {
            setAnimalInfo()
            var animal = when(count%2){
                0 -> WildAnimalFunction(
                    animalName
                )
                else -> WildAnimalFunction(
                    animalName,
                    animalSex
                )
            }
            tv_result.text = animal.getDesc("动物园")
            count++
        }

    }

    fun setAnimalInfo() {
        animalName = "牛"
        animalSex = 1
    }


}

2.3 伴生对象

伴生对象这个是在Kotlin中有的,Java中没有,什么是伴生对象呢,你可以把它理解为“影子”,把类当做一个人,这个人可以有很多房子,但是人只有一个,影子也只有一个。你也可以把伴生对象替换掉静态成员的作用,但它比静态成员的功能要强大。我们之前通过性别类型来获得性别名称,那么反推呢,我们使用伴生对象来实现这一功能,新创建一个名为WildAnimalCompanion的类

package com.llw.kotlinstart.custom_class

class WildAnimalCompanion (var name: String,val sex:Int = 0) {
    var sexName:String
    init {
        sexName = if(sex == 0) "公" else "母"
    }

    fun getDesc(tag: String): String {
        return "欢迎来到$tag:这头${name}是${sexName}的"
    }

    //关键字companion表示伴随,object表示对象,WildAnimal表示伴生对象的名称
    companion object WildAnimal{
        fun judgeSex(sexName:String):Int{
            var sex:Int = when (sexName){
                "公","雄" -> 0
                "母","雌" -> 1
                else -> -1
            }
            return sex
        }
    }
}

代码应该没有什么好说的,一目了然,关键定义这个伴生对象和使用它,接下来看怎么使用

代码如下:

package com.llw.kotlinstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.llw.kotlinstart.custom_class.WildAnimalCompanion
import com.llw.kotlinstart.custom_class.WildAnimalFunction
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    var count: Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val sexArray:Array<String> = arrayOf("公","母","雄","雌")

        btn_test.setOnClickListener {
            var sexName:String = sexArray[count++%4]
            //伴生对象的WildAnimal名称可以省略掉
            //tv_result.text = ""$sexName"对应的类型是${WildAnimalCompanion.WildAnimal.judgeSex(sexName)}"
            tv_result.text = ""$sexName"对应的类型是${WildAnimalCompanion.judgeSex(sexName)}"//双引号 要加反斜杠  如同这样 ""
        }


    }


}

这个是灰色的,我们省略掉也是可以的

有四种结果,我只放两个图

2.4 静态属性

之前我们的伴生对象可以实现静态函数,同样也能实现静态属性,只要在伴生对象内部增加几个字段定义就行了,之前是用0和1表示动物的雄雌,接下来用整型常量MALE表示雄性的0,整型常量FEMALE表示雌性的1,创建一个名为WildAnimalConstant的类,代码如下,

package com.llw.kotlinstart.custom_class

class WildAnimalConstant(var name: String, val sex: Int = MALE) {
    var sexName: String

    init {
        sexName = if (sex == MALE) "公" else "母"

    }

    fun getDesc(tag: String): String {
        return "欢迎来到$tag: 这只${name}是${sexName}的。"
    }

    companion object WildAnimal {
        //静态常量的值是不可变得,所以要使用关键字val修饰
        val MALE = 0
        val FEMALE = 1
        val UNKOWN = -1
        fun judgeSex(sexName:String):Int{
            var sex:Int = when(sexName){
                "公","雄" -> MALE
                "母","雌" -> FEMALE
                else -> UNKOWN
            }
            return sex
        }
    }
}

然后再进行调用

val sexArray:Array<String> = arrayOf("公","母","雄","雌")

        btn_test.setOnClickListener {
            var sexName:String = sexArray[count++%4]
            //伴生对象的WildAnimal名称可以省略掉
            //tv_result.text = ""$sexName"对应的类型是${WildAnimalCompanion.WildAnimal.judgeSex(sexName)}"
            tv_result.text = ""$sexName"对应的类型是${WildAnimalConstant.judgeSex(sexName)}"//双引号 要加反斜杠  如同这样 ""
        }

改一下类名就可以了,运行效果和之前的是一样的,只不过程序里面就可以通过WildAnimalConstant.MALE和WildAnimalConstant.FEMALE来判断公母了,不像之前通过0和1这种毫无意义的值来判断。 Kotlin的类成员分为实例成员与静态成员,实例成员包括成员属性和成员方法,其中与入参同名的成员属性可以在构造函数中直接声明,外部必须通过类的实例才能访问类的成员属性和成员方法,类的静态成员包括静态属性与静态方法,它们都在类的伴生对象中定义,外部可以通过类名直接访问该类的静态成员。

三、类的继承

我们一开始就提到了类的继承,如class MainActivity : AppCompatActivity(),这和Java是不一样的,那么Kotlin怎么定义基类并由基类派生出子类呢?

3.1 开放性修饰符

之前我们写了好多个WildAnimal类,Java和Kotlin关于类的继承还有区别,比如Java中默认每个类都能被继承,除非加了final关键字,而Kotlin刚好相反,它默认每个类都不能被继承(PS:这不是搞我心态吗!!!),这个时候要想让一个类成为基类,就要把该类开放出来,于是就用到了开放性修饰符open(PS:敲黑板,重点来了,哪个),演示代码如下:

open class Animal(var name:String,val sex:Int = 0){

    }

在Java中有几个熟悉的关键字,public、protected、private,分别表示公开、只对子类开放、私有。那么在Kotlin中也给出了4个开放性修饰符。

开放性修饰符

说明

public

对所有人开放。Kotlin的类、函数、变量不加开放性修饰符的话,默认就是public类型

internal

只对本模块内部开放,这是Kotlin新增的关键字。

protected

只对自己和子类开放

private

只对自己开放、即私有

注意到这几个修饰符与open一样都加在类和函数前面,并且都包含“开放”的意思,乍看起来还真有点迷,到底open跟这4个开放性修饰符是什么关系呢?其实很简单,open不控制某个对象的访问权限,只决定该对象能否繁衍开来,说白了,就是公告这个叼毛有没有资格繁衍下一代,只有头戴open帽子的类,才允许作为基类派生出子类来,而头戴open帽子的函数,表示它允许在子类中进行重写,如果没戴open帽子,该类就只好打光棍了,函数没戴open帽子的话,类的孩子就没有办法修改它。 至于那4个开放性修饰符,则是用来限定允许访问某对象的外部范围,通俗地说,就是哪里的帅哥可以跟这个美女搞对象,头戴public的,表示全世界的帅哥都能跟她处对象,头戴internal的,表示只有本国的帅哥可以,头戴protected的,表示自由本单位以及下属单位的可以,头戴private,表示自己本单位可以。

3.2 普通类继承

创建一个Poultry类,代码如下:

package com.llw.kotlinstart.custom_class

//Kotlin的类型默认是不能继承的(即 final类型),如果需要继承某类,该父类就应当声明open类型
open class Poultry (var name:String,val sex:Int = MALE){
    //变量、方法、类默认都是public,所以一般都把public省略掉了
    var sexName:String
    init {
        sexName = getSexName(sex)
    }

    //私有的方法既不能被外部访问,也不能被子类继承,因此open与private不能共存,否则编译器会报错
    open protected fun getSexName(sex:Int):String{
        return if(sex == MALE) "公" else "母"
    }

    fun getDesc(tag:String):String{
        return "欢迎来到$tag: 这只${name}是${sexName}的。"
    }

    companion object BirdStatic{
        val MALE = 0
        val FEMALE = 1
        val UNKOWN = -1
        fun judgeSex(sexName:String):Int {
            var sex:Int = when (sexName){
                "公","雄" -> MALE
                "母","雌" -> FEMALE
                else -> UNKOWN
            }
            return sex
        }
    }
}

然后我们再创建一个名为Pig的子类,继承Poultry,代码如下:

package com.llw.kotlinstart.custom_class

//注意父类Bird已经在构造函数声明了属性,故而子类Pig无须重复声明属性
//也就是说,子类的构造函数在输入参数前面不需要再加val和var
class Pig(name:String="猪",sex: Int= MALE) : Poultry(name, sex){

}

然后在Activity中调用:

package com.llw.kotlinstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.llw.kotlinstart.custom_class.*
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    var count: Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var sex:Int
        btn_test.setOnClickListener {
            var sexPoultry = if(count++%3==0) Poultry.MALE else Poultry.FEMALE
            var pig = Pig(sex = sexPoultry)
            tv_result.text = pig.getDesc("高老庄")
        }


    }


}

运行效果图如下:

然后再来定义一个小狗类 Dog的代码

package com.llw.kotlinstart.custom_class

class Dog (name:String = "哈士奇",sex:Int = MALE):Poultry(name, sex){
    override public fun getSexName(sex: Int): String {
        return if(sex == MALE) "雄" else "雌"
    }

}

然后在Activity中调用

package com.llw.kotlinstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.llw.kotlinstart.custom_class.*
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    var count: Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var sex:Int
        btn_test.setOnClickListener {
            var sexPoultry = if(count++%3==0) Poultry.MALE else Poultry.FEMALE
            var dog = Dog(sex = sexPoultry)
            tv_result.text = dog.getDesc("狗狗流浪记")
        }


    }


}

运行效果图如下:

3.3 抽象类

Kotlin中也存在与Java类似的抽象类,抽象类之所以存在,是因为其内部拥有被关键字abstract修饰的抽象方法。抽象方法没有具体的函数体,故而外部无法直接声明抽象类的实例,只有在子类继承时重写方法,方可使用该子类正常声明对象实例。 For Example ,鸡属于鸟类,可是公鸡和母鸡的叫声是不一样的,所以鸡这个类的叫唤方法“callOut”发出什么声音并不确定,只能先声明为抽象方法,连带着鸡类“Chicken”也变成抽象类了。 现在定义一个抽象的Chicken类,代码如下:

package com.llw.kotlinstart.custom_class
//子类的构造函数,原来的输入参数不用加var和val,新增的输入参数必须加var或者val
//因为抽象类不能直接使用,所以构造函数不必默认参数赋值
abstract class Chicken (name:String,sex:Int,var voice:String):Poultry(name, sex){
    val numberArray:Array<String> = arrayOf("一","二","三","四","五","六","七")
    //抽象方法必须在子类进行重写,所以可以省略关键字open,因为abstract方法默认就是open类型
    //open abstract fun callOut(times:Int):String
    abstract fun callOut(times:Int):String
}

然后我们从Chicken类派生出公鸡类Cock,指定攻击的叫声为“喔喔喔”,同时还要重写callOut方法,明确公鸡的叫唤行为。具体代码如下:

package com.llw.kotlinstart.custom_class

class Cock(name:String="鸡",sex:Int = Poultry.MALE,voice:String="喔喔喔"): Chicken(name, sex, voice) {
    override fun callOut(times: Int): String {
        var count = when {
            //when语句判断大于和小于时,要把完整的判断条件写到每个分支中
            times <=0 ->0
            times >=7 -> 6
            else -> times
        }
        return "$sexName$name${voice}叫了${numberArray[count]}声,这是在报晓"
    }
}

再派生出公鸡类Hen,指定攻击的叫声为“咯咯咯”,再重写callOut方法

package com.llw.kotlinstart.custom_class

class Hen(name:String="鸡",sex:Int = Poultry.FEMALE,voice:String="咯咯咯"): Chicken(name, sex, voice) {
    override fun callOut(times: Int): String {
        var count = when {
            //when语句判断大于和小于时,要把完整的判断条件写到每个分支中
            times <=0 ->0
            times >=7 -> 6
            else -> times
        }
        return "$sexName$name${voice}叫了${numberArray[count]}声,这是在下蛋"
    }
}

然后在Activity中调用不同的类

package com.llw.kotlinstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.llw.kotlinstart.custom_class.*
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    var count: Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btn_test.setOnClickListener {
            //调用公鸡类
            tv_result.text = Cock().callOut(count++ % 7)
        }

    }

}
package com.llw.kotlinstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.llw.kotlinstart.custom_class.*
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    var count: Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        btn_test.setOnClickListener {
            //调用母鸡类
            tv_result.text = Hen().callOut(count++%7)
        }
        
    }

}

运行结果

3.4 接口

Kotlin的接口与Java一样是为了间接实现多重继承,由于直接继承多个类可能存在方法冲突等问题,因此Kotlin在编译阶段就不允许某个类同 时继承多个基类,否则会报错,于是,只能通过接口定义几个抽象方法,然后在实现该接口的具体类中重写这几个方法,从而间接实现类似C++多重继承的功能。 在Kotlin中定义接口需要注意以下几点: (1)接口不能定义构造函数,否则编译器会报错"An interface may not have a constructor"。 (2)接口的内部方法通常要被实现它的类进行重写,所以这些方法默认为抽象类型。 (3)与Java不同的是,Kotlin允许在接口内部实现某个方法,而Java接口的所有内部方法都必须是抽象方法。

Android开发中最常见的接口是控件的点击监听器View.OnClickListener,它内部的点击动作onClick,类似的还有长按监听器、选中监听器等等,它们无一例外都定义了某种行为的事件处理过程。我们可以用一个列子来表达这些,比如鸟儿的飞翔、游泳、奔跑等,下面定义一个行为接口 Behavior。

package com.llw.kotlinstart.custom_class

//Kotlin与Java一样不允许多重继承,即不能同时继承两个及两个以上类
//否则编译器报错"Only one class may appear in a supertype list"
//所以仍然需要接口interface 来间接实现多重继承的功能
//接口不能带构造函数(那样就变成一个类了),否则编译器报错"An interface may not have a constructor"
interface Behavior {
    //接口内部的方法默认就是抽象的,所以不加abstract 也可以,当然open也可以不加
    open abstract fun fly():String

    //比如下面这个swim方法就没有加关键字abstract ,也无须在此实现方法
    fun swim():String

    //Kotlin的接口与Java的区别在于,Kotlin接口内部允许实现方法
    //此时该方法不是抽象方法,就不能加上abstract
    //不过该方法依然是open类型,接口内部的所有方法都默认是open类型
    fun run():String{
        return "大多数鸟儿跑得并不像样,只有鸵鸟、鸸鹋等少数鸟类才擅长奔跑。"

    }

    //Kotlin的接口允许声明抽象属性,实现该接口的类必须重载该属性
    //与接口内部方法一样,抽象属性前面的open和abstract 也可以省略掉
    //open abstract var skiledSporte:String
    var skiledSporte:String

}

在其他类实现这个接口时,跟类继承一样把接口名称放在冒号后面,也就是说,Java的extends和implement这两个关键字在Kotlin中都被冒号取代了。然后就想重写抽象类的抽象方法一样重写接口的抽象方法,创建一个名为Goose的类,代码如下:

package com.llw.kotlinstart.custom_class

class Goose(name: String = "鹅", sex: Int = Poultry.MALE) : Poultry(name, sex), Behavior {

    override fun fly(): String {
        return "鹅能飞一点点,但是飞不高,也飞不远"
    }

    override fun swim(): String {
        return "鹅是会游泳的"
    }

    //因为接口已经实现了run方法,所以此处可以不用实现该方法,当你也可以实现它
    override fun run(): String {
        //super用来调用父类的属性或方法,由于Kotlin的接口允许实现方法,因此super所指的对象也可以是interface
        return super.run()
    }

    //重载了来自接口的抽象属性
    override var skiledSporte: String = "游泳"

}

然后在Activity中使用

package com.llw.kotlinstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.llw.kotlinstart.custom_class.*
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    var count: Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btn_test.setOnClickListener {
            tv_result.text = when(count++%3){
                0 -> Goose().fly()
                1 -> Goose().swim()
                else -> Goose().run()
            }
        }

    }

}

运行效果如下图: 调用fly方法

调用swim方法

调用run方法

3.5 接口代理

通过接口固然完成了相应行为,但是鸟类这个家族非常庞大,如果每种鸟都实现Behavior接口,工作量是非常大的,其实鸟类的行为并不多,可以分类为飞禽、水禽、走禽三个行为类 下面是飞禽的行为类代码示例:

package com.llw.kotlinstart.custom_class

class BehaviorFly : Behavior {
    override fun fly(): String {
        return "翱翔天空"
    }

    override fun swim(): String {
        return "落水凤凰不如鸡"
    }

    override fun run(): String {
        return "能飞干嘛还要走"
    }

    override var skiledSporte: String = "飞翔"

}

下面是水禽

package com.llw.kotlinstart.custom_class

class BehaviorSwim : Behavior {
    override fun fly(): String {
        return "看情况,大雁能展翅高飞,企鹅却欲飞还休"
    }

    override fun swim(): String {
        return "怡然戏水"
    }

    override fun run(): String {
        return "赶鸭子上树"
    }

    override var skiledSporte: String = "游泳"

}

下面是走禽

package com.llw.kotlinstart.custom_class

class BehaviorRun : Behavior {
    override fun fly(): String {
        return "飞不起来"
    }

    override fun swim(): String {
        return "望洋兴叹"
    }

    override fun run(): String {
        return super.run()
    }

    override var skiledSporte: String = "奔跑"

}

然后定义一个引用代理类的野禽基类,通过关键字by表示接口将由入参中的代理类实现,野禽基类WildFowl代码如下:

package com.llw.kotlinstart.custom_class

//只有接口才能使用关键字by进行代理操作
class WildFowl (name:String,sex:Int=MALE,behavior: Behavior):Poultry(name,sex),Behavior by behavior{
    
}

然后在Activity中使用

package com.llw.kotlinstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.llw.kotlinstart.custom_class.*
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    var count: Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btn_test.setOnClickListener {
            var fowl = when (count++ % 6) {
                //把代理类作为输入参数来创建实例
                0 -> WildFowl("老鹰", Poultry.MALE, BehaviorFly())
                //由于sex字段是个默认参数,因此可通过命名参数给behavior赋值
                1 -> WildFowl("凤凰", behavior = BehaviorFly())
                2 -> WildFowl("大雁", Poultry.FEMALE, BehaviorSwim())
                3 -> WildFowl("企鹅", behavior = BehaviorSwim())
                4 -> WildFowl("鸵鸟", Poultry.MALE, BehaviorRun())
                else -> WildFowl("鹂鹃", behavior = BehaviorRun())
            }

            var action = when (count % 11) {
                in 0..3 -> fowl.fly()
                4, 7, 10 -> fowl.swim()
                else -> fowl.run()
            }
            tv_result.text = "${fowl.name}: $action"
        }

    }

}

运行效果如下: 老鹰的飞翔行为:

凤凰的游泳行为:

大雁的飞翔行为:

企鹅的游泳行为:

鸵鸟的飞翔行为:

鹂鹃的奔跑行为

通过一顿操作之后,总结出Kotlin的类继承与Java相比有所不同,主要体现在以下几点: (1)Kotlin的类默认不可被继承,若需继承,则要添加open声明,而Java的类默认是允许被继承的,只有添加final声明才表示不能为继承。 (2)Kotlin除了常规的三个开放性修饰符public、protected、private外,另外增加了修饰符internal,表示只对本模块开放。 (3)Java的类继承关键字extends以及接口实现关键字implement在Kotlin中都被冒号所取代。 (4)Kotlin允许在接口内部实现某个方法,而Java接口的内部方法只能是抽象方法。 (5)Kotlin引入了接口代理(类代理)的概念,而Java不存在代理的写法。

四、特殊类

4.1 嵌套类

一个类可以在单独的代码文件中定义,也可以在另一个类内部定义,后一种情况叫作嵌套类,即A类嵌套在B类之中,听起来和Java的嵌套类是一样的,但其实有所差别,Java的嵌套类允许访问外部类的成员,而Kotlin的嵌套类不允许访问外部类的成员,强行访问则会报错。 下面是示例代码:

package com.llw.kotlinstart.custom_class

class Tree(var treeName: String) {
    //在类内部再定义一个类,这个新类称作嵌套类
    class Flower(var flowerName: String) {
        fun getName(): String {
            return "这是一朵$flowerName"
            //普通的嵌套类不能访问外部类的成员,如treeName
            //否则编译器会报错:" Unresolved reference: *** "
            //return "这是${treeName}上的一朵$flowerName"
        }


    }
}

调用嵌套类时,得在嵌套类的类名前面添加外部类的类名,相当于把这个嵌套类作为外部类的静态对象使用,在Activity中调用代码如下:

package com.llw.kotlinstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.llw.kotlinstart.custom_class.*
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btn_test.setOnClickListener {
            //使用嵌套类时,只能引用外部类的类名,不能调用外部类的构造函数
            val peachBlossom = Tree.Flower("桃花")
            tv_result.text = peachBlossom.getName()
        }

    }

}

因为嵌套类无法访问外部类的成员,所以其方法只能返回自身的信息,运行效果图如下:

4.2 内部类

Kotlin限制了嵌套类不能访问外部类的成员,那还有什么方法可以实现此功能呢?针对该问题,Kotlin另外增加了关键字inner表示内部,把inner加在嵌套类的class前面,然后嵌套类就变成了内部类,所以Kotlin的内部类就相当于Java的嵌套类,而Kotlin的嵌套类则是加了访问限制的内部类。还是在之前的嵌套类Tree中,加一个内部类Fruit ,示例代码如下,

package com.llw.kotlinstart.custom_class

class Tree(var treeName: String) {
    //在类内部再定义一个类,这个新类称作嵌套类
    class Flower(var flowerName: String) {
        fun getName(): String {
            return "这是一朵$flowerName"
            //普通的嵌套类不能访问外部类的成员,如treeName
            //否则编译器会报错:" Unresolved reference: *** "
            //return "这是${treeName}上的一朵$flowerName"
        }
        
    }
    
    //嵌套类加上inner前缀,就变成内部类
    inner class Fruit(var fruitName:String){
        fun getName():String{
            //只有声明为内部类(添加了关键字inner,才能访问内外部类的成员)
            return  "这是${treeName}长出来的$fruitName"
        }
    }
}

然后在Activity中调用

package com.llw.kotlinstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.llw.kotlinstart.custom_class.*
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btn_test.setOnClickListener {
            //使用嵌套类时,只能引用外部类的类名,不能调用外部类的构造函数
            val peach = Tree("桃树").Fruit("桃子")
            tv_result.text = peach.getName()
        }

    }

}

运行效果图:

4.3 枚举类

Java有一种枚举类型,它采用关键字enum来表达,其内部定义了一系列名称,通过有意义的名字比0、1、2这些数字能够更有效地表达语义,下面是一个Java定义枚举类型的代码示例:

package com.llw.kotlinstart.custom_class;

enum Season {SPRING, SUMMER, AUTUMN, WINTER}

再来看Kotlin的枚举类

package com.llw.kotlinstart.custom_class

enum class SeasonType {
    SPRING, SUMMER, AUTUMN, WINTER
}

虽然看上去只比Java的枚举类型多了一个class,但是Kotlin中枚举类内部的枚举变量除了可以直接拿来赋值之外,还可以通过枚举值的几个属性获得对应的信息,例如ordinal属性用于获取该枚举值的序号,name属性用于获取该枚举值的名称。枚举变量本质上还是该类的一个实例,所以如果枚举类存在构造函数,枚举变量也必须调用对应的构造函数,这样做的好处是,每一个枚举值不但携带唯一的名称,还可以拥有更加个性化的特征描述。下面创建一个枚举类来说明,代码如下:

package com.llw.kotlinstart.custom_class

enum class SeasonName(val seasonName: String) {
    SPRING("春天"),
    SUMMER("夏天"),
    AUTUMN("秋天"),
    WINTER("冬天")
}

然后在Activity中使用枚举类SeasonType和SeasonName:

package com.llw.kotlinstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.llw.kotlinstart.custom_class.*
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var count: Int = 0
        btn_test.setOnClickListener {
            if (count % 2 == 0) {
                //ordinal表示枚举类型的序号,name表示枚举类型的名称
                tv_result.text = when (count++ % 4) {
                    SeasonType.SPRING.ordinal -> SeasonType.SPRING.name
                    SeasonType.SUMMER.ordinal -> SeasonType.SUMMER.name
                    SeasonType.AUTUMN.ordinal -> SeasonType.AUTUMN.name
                    SeasonType.WINTER.ordinal -> SeasonType.WINTER.name
                    else -> "未知"
                }
            } else {
                tv_result.text = when(count++ % 4){
                    //使用自定义属性seasonName表示个性化的描述
                    SeasonName.SPRING.ordinal -> SeasonName.SPRING.seasonName
                    SeasonName.SUMMER.ordinal -> SeasonName.SUMMER.seasonName
                    SeasonName.AUTUMN.ordinal -> SeasonName.AUTUMN.seasonName
                    SeasonName.WINTER.ordinal -> SeasonName.WINTER.seasonName
                    else -> "未知"
                    //枚举类的构造函数是给枚举类型使用的,外部不能直接调用枚举类的构造函数
                }
            }
        }

    }

}

4.4 密封类

为了解决枚举值判断的多余分支问题,Kotlin提出了“密封类”得概念,密封类就像是一种更加严格的枚举类,它内部有且仅有自身的实例对象,所以是一个有限的自身实例集合,或者说,密封类采用了嵌套类的手段,它的嵌套类全部由自身派生而来,定义密封类的时候,需要在该类的class前面加上关键字sealed作为标记。定义一个密封类,代码如下:

package com.llw.kotlinstart.custom_class

sealed class SeasonSealed {
    //密封类内部的每个嵌套类都必须继承该类
    class Spring (var name:String) : SeasonSealed()
    class Summer (var name:String) : SeasonSealed()
    class Autumn (var name:String) : SeasonSealed()
    class Winter (var name:String) : SeasonSealed()

}

这样外部使用when语句便无须指定else分支了。下面是Activity中使用代码

package com.llw.kotlinstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.llw.kotlinstart.custom_class.*
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var count: Int = 0
        btn_test.setOnClickListener {
            var season = when(count++%4){
                0 -> SeasonSealed.Spring("春天")
                1 -> SeasonSealed.Summer("夏天")
                2 -> SeasonSealed.Autumn("秋天")
                else -> SeasonSealed.Winter("冬天")
            }
            //密封类是一种严格的枚举类,它的值是一个有限的集合
            //密封类确保条件分支覆盖了所有的枚举类型,因此不再需要else分支
            tv_result.text = when(season){
                is SeasonSealed.Spring -> season.name
                is SeasonSealed.Summer -> season.name
                is SeasonSealed.Autumn -> season.name
                is SeasonSealed.Winter -> season.name
            }



        }

    }

}

4.5 数据类

在Android实际开发中,我们经常需要定义一些实体类来存放返回的数据,在Java中一个数据类的通常我完成以下工作: (1)定义实体类的每个字段,以及对字段进行初始赋值的构造函数。 (2)定义每个字段的get/set方法 (3)再判断两个数据对象是都相等时,通常每个字段都要比较一遍。 (4)在复制数据对象时,如果想另外修改某几个字段值,得再补充对应数量的赋值语句。 (5)在调试程序时,为获知数据对象里保存的字段值,得手工把每个字段值都打印出来。 这对于开发者来说无疑于一个繁琐的工作,而Kotlin鉴于此,推出了名为“数据类”这样的骚操作,其实说起来也比较简单,数据类的定义仅仅只要在class前面增加关键字data,并声明拥有完整输入参数的构造函数,即可无缝实现以下功能: (1)自动声明与构造函数入参同名的属性字段。 (2)自动实现每个属性字段的get/set方法。 (3)自动提供equals方法,用于比较两个数据对象是否相等。 (4)自动提供copy方法,允许完整赋值某个数据对象,也可在复制后单独修改某几个字段的值。 (5)自动提供toString方法,用于打印数据对象中保存的所有字段值。 说的这么叼,也不知道是不是真的,来定义一个试一下吧。 代码如下:

package com.llw.kotlinstart.custom_class
//数据类必须有主构造函数,且至少有一个输入参数
//并且要声明与输入参数同名的属性,即输入参数前面添加关键字val或者var
//数据类不能是基类也不能是子类,不能是抽象类,也不能是内部类,更不能密封类
//我就是我,是颜色不一样的烟火
data class Plant(
    var name: String,
    var stem: String,
    var leaf: String,
    var flower: String,
    var fruit: String,
    var seed: String
) {
}

然后在Activity中调用一下吧

package com.llw.kotlinstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.llw.kotlinstart.custom_class.*
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var count: Int = 0
        
        var lotus = Plant("莲","莲藕","莲叶","莲花","莲蓬","莲子")
        //数据类的copy方法不带参数,表示复制一模一样的的对象
        var lotus2 = lotus.copy()
        btn_test.setOnClickListener {
            lotus2 = when(count++%2){
                //copy方法带参数,表示指定参数另外赋值
                0 -> lotus.copy(flower = "荷花")
                else -> lotus.copy(flower = "莲花")
            }
            //数据类自带equals方法,用于判断两个对象是否一样
            var result = if(lotus2.equals(lotus)) "相等" else "不等"
            tv_result.text = "两个植物的比较结果是${result}n"+
                    "第一个植物的描述是${lotus.toString()}n"+
                    "第二个植物的描述是${lotus2.toString()}"
        }

    }

}

上述代码调用了Plant对象的copy、equals、toString等方法,然后看一下运行的效果

4.6 模板类

模板类的应用相当广泛,Kotlin中保留了它,而且写法与Java类似,一样在类名后面补充形如“”或者“<A,B>”这样的表达式,表示此处的参数类型待定,要等创建类实例时再确定具体的参数类型,举个例子,森林里有一条小河,小河的长度可能以数字形式输入(包括Int、Long、Float、Double),还有可能以字符串形式输入。针对于这个需求编写名为River的模板类,代码如下:

package com.llw.kotlinstart.custom_class
//在类名后面添加"<T>",表示这是一个模板类
class River<T> (var name:String,var length:T) {
    fun getInfo():String{
        var unit:String = when(length){
            is String -> "米"
            //Int、Long、Float、Double都是数字类型Number
            is Number -> "m"
            else -> ""
        }
        return "${name}的长度是$length$unit"
    }
}

然后在Activity中调用和这个模板类

package com.llw.kotlinstart

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.llw.kotlinstart.custom_class.*
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var count: Int = 0

        btn_test.setOnClickListener {
            var river = when(count++%4){
                //模板类(泛型类)声明对象时,要在模板类的类名后面加上"<参数类型>"
                0 -> River<Int>("小溪",100)
                //如果编译器根据输入参数就能知晓参数类型,也可直接省略"<参数类型>"
                1 -> River("瀑布",99.9f)
                2 -> River<Double>("山涧",50.5)
                //如果你已经是老手了,那么怎么舒服怎么来,Kotlin的设计初衷就是偷懒
                else -> River("大河","一千")
            }
            tv_result.text = river.getInfo()
        }

    }

}

运行效果图如下:

小溪的长度

瀑布的长度:

山涧的长度:

大河的长度:

学习过程也是断断续续的,碎片时间,能写完这篇博客实属不易啊,希望能帮到您,山高水长,后会有期~

Kotlin学习日志(一)TextView、Button、Toast的使用 Kotlin学习日志(二)数据类型 Kotlin学习日志(三)控制语句 Kotlin学习日志(四)函数