阅读 271

Kotlin 总结系列(2)——函数和类

kotlin总结系列(1)--基础要素

一 函数的定义和调用

主要是kotlin上函数的特点:命名参数、默认参数值、顶层函数和属性、扩展方法和扩展属性(本质上是静态函数高级语法糖),和能消除重复代码(DRY)的局部函数。

让我们从一个常用例子出发,java集合都有默认的toString()方法,但它的输出是固定格式化的,有时并不是你所需要的([1,2,3]),如要自定义字符串的前缀、后缀,和分隔符时,一般定义个方法,并传入参数:

fun <T> joinToString(collection: Collection<T>,separator:String,prefix:String,postfix:String):String{
    val result = StringBuilder(prefix)
    for ((index,element) in collection.withIndex()){
        if (index>0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}
复制代码

使用:

val list = listOf("kotlin","java")
println(joinToString(list,",","{","}")) // {kotlin,java}
复制代码

1 命名参数

关注的第一个问题就是函数的可读性,如上面的使用joinToString(list,",","{","}"),难以看出这些string对应的是什么参数(虽然可以借助ide)。kotlin可以优雅的调用,即可以显式的标明一些参数的名称,如joinToString(list,",",prefix = "{",postfix = "}"),直接在调用时指明了prefixpostfix,能清晰明了的分辨出来。

注: 为避免混淆,当指明了一个参数名称后,那它之后的所有参数都要显式指明名称

2 默认参数值

java函数的另一个普遍存在的问题是,一些类的重载函数实在太多了(如java.lang.Thread便有8个构造方法),导致参数名和类型被不断重复。

kotlin则使用了默认参数值,可以在声明函数的时候,指定参数的默认值,就可以避免重复创建函数。如上面的joinToString函数,大多数情况下,可以不加前缀或者后缀并用逗号分隔,所以把它们设为默认值:

//只改变了参数声明
fun <T> joinToString(collection: Collection<T>,separator:String=",",prefix:String="",postfix:String=""):String{
    val result = StringBuilder(prefix)
    for ((index,element) in collection.withIndex()){
        if (index>0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}
复制代码

可以像java多重重载函数一样地调用:

println(joinToString(list)) // 打印:kotlin,java
println(joinToString(list,";")) // 打印:kotlin;java
println(joinToString(list,prefix = "{",postfix = "}"))  // 打印:kotlin;java 
复制代码

3 消除静态工具类的顶层函数和属性

java中,所有的代码都必须写成类的函数。有时存在一个基本的对象,但你不想通过实例函数来添加操作,让它的API继续膨胀,结果就是,会把这些函数写成静态,并交由不包含任何状态和实例函数的类保管,如JDK中的Collections,或者自己代码中,一些以Util作为后缀的工具类。

kotlin可以直接把函数或属性放在代码文件顶层,而不用从属于任何的类。(依然是包内成员,如果需要从包外访问它,则需要import(可以使用as更改导入的名字),但不再需要额外包一层。

kotlin属性也可以放在文件顶层,类似java静态字段; 若想像java的public final static一样声明一个常量,kotlin可以使用const val修饰

使用方式在下面给出↓

4 扩展方法和属性

4.1 扩展方法

kotlin的一大特色是,可以平滑地与现有代码集成。扩展函数可以在类的外面定义一个类的成员函数,如我们添加一个方法扩展String类型,计算一个字符串的最后一个字符并返回:(像成员函数一样地调用

import...

fun String.lastChar():Char = this.get(length-1)  //this可以省略

...
fun main(){
    //像成员函数一样地调用
    println("kotlin".lastChar()) // 打印: n
}
复制代码

扩展函数的声明,与普通函数区别就是,把你要扩展的类或者接口名称,反到即将添加的函数名签名,这个类被称为接收者类型,如例子中的String;用来调用这个扩展函数的那个对象,被叫做 接收者对象,如例子中的“kotlin”

在扩展函数中,可以直接访问被扩展类或接口的其他方法和属性(如例子中的String.get方法),就好像是在这个类中定义一样。

对于开始的字符串例子,现在可以这么定义使用:

fun <T> Collection<T>.joinToString(separator:String=",",prefix:String="",postfix:String=""):String{
    val result = StringBuilder(prefix)
    for ((index,element) in withIndex()){
        if (index>0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

...
val list = listOf("kotlin","java")
println(list.joinToString(prefix = "{",postfix = "}"))  //kotlin;java

复制代码

扩展函数本质上,是java的静态函数的高效语法糖,最终是被转为一个接收该对象类型的静态函数,因此是不能被继承重写的。

注: 当类的成员函数和扩展函数有相同签名时,成员函数会优先使用

4.2 扩展属性

和扩展函数一样,kotlin也支持扩展属性,使用类似,如:

var StringBuilder.lastChar:Char
    get() = get(length -1)
    set(value) {
        this.setCharAt(length-1,value)
    }
    
...
val sb = StringBuilder("kotlin?")
println(sb.lastChar) // 打印: ?
sb.lastChar = '!'
println(sb) // 打印: kotlin!
    
复制代码

注: 扩展属性是没有支持字段存储的

5 局部函数

提高代码质量标准之一:不要重复你自己的代码(DRY)。kotlin提供了一个整洁的方案,局部函数:可以在函数中嵌套函数

让我们来看怎么用局部函数解决常见的代码重复问题,例子中,saveUser函数用于将user信息存到数据库中,并确保user对象含有有效数据

class User(val id:Int,val name:String,val address:String)
复制代码
fun saveUser(user:User){
    if (user.name.isEmpty()){
        throw IllegalArgumentException("Can't save user ${user.id} :empty Name")
    }
    if (user.address.isEmpty()){
        throw IllegalArgumentException("Can't save user ${user.id} :empty Address")
    }

    //保存到数据库中
    ...
}
复制代码

函数saveUser中,存在重复的字段检查,当检查字段增多,代码会显得特别臃肿,可利用局部函数提取重复代码:

fun saveUser(user:User){
    //可省略局部函数的user参数
    fun validate(user: User,value:String,fieldName:String){
        if (value.isEmpty()){
            throw IllegalArgumentException("Can't save user ${user.id} :empty $fieldName")
        }
    }
    validate(user,value = user.name,fieldName = "Name")
    validate(user,value = user.address,fieldName = "Address")

    //保存到数据库中
}
复制代码

声明了一个局部函数validate提取重复的检查逻辑,因局部函数可以访问到所在函数的所有参数和变量,所以,可以去掉冗余的User参数:

fun saveUser(user:User){
    //去掉冗余的User参数
    fun validate(value:String,fieldName:String){
        if (value.isEmpty()){
            throw IllegalArgumentException("Can't save user ${user.id} :empty $fieldName")
        }
    }
    validate(value = user.name,fieldName = "Name")
    validate(value = user.address,fieldName = "Address")

    //保存到数据库中
}
复制代码

继续改进,可以将user的验证扩展成扩展函数

fun User.validateBeforeSave(){
    fun validate(value:String,fieldName:String){
        if (value.isEmpty()){
            throw IllegalArgumentException("Can't save user $id :empty $fieldName")
        }
    }
    
    validate(value = name,fieldName = "Name")
    validate(value = address,fieldName = "Address")
}

fun saveUser(user:User){

   user.validateBeforeSave()

    //保存到数据库中
}
复制代码

再次表明,扩展函数可以很大程度优化代码。

注:扩展函数也可以被声明为局部函数,但一般不建议多层嵌套,因深度嵌套的局部函数往往会让人太费解

二 类和接口

kotlin接口和类的实现与java还是有一点区别的,如接口可以包含属性声明;kotlin的声明默认是public final 的;此外,嵌套类默认不是内部类,即静态的,没有包含怼外部类的隐式引用

1 类的继承结构

1.1 kotlin中的接口

kotlin中的接口与java8中的相似,可以包含抽象方法,和非抽象方法的实现(与java8中默认方法类似);同时可以有属性声明,但没有包含任何状态,即没有支持字段来保存(后面有讲)

接口声明:

interface Clickable{
    fun click()  //抽象方法
    fun showOff() = println("I'm clickable") //带默认实现的方法
}
复制代码

kotlin在类名后面用冒号来代替java的extends和implements关键字。和java一样,一个类可以实现任意多个接口,但只能继承一个类

kotlin用override修饰符标注被重写的方法和属性,同时,override修饰符是强制要求

 class Button:Clickable{
    override fun click() {
        println("button clicked")
    }

}
复制代码

若同时实现两个接口,且两个接口含有相同的函数签名,且都有默认实现,则kotlin会强制要求提供你自己的实现

interface Clickable{
    fun click()
    fun showOff() = println("I'm clickable")
}

interface Focusable{
    fun focus()
    fun showOff() = println("I'm focusable")
}

class Button:Clickable,Focusable{

    override fun click() {
        println("button clicked")
    }

    override fun focus() {
        println("button clicked")
    }

    override fun showOff() {
    //使用尖括号加父类名字的“super”表明了你想要调用哪个父类方法
        super<Clickable>.showOff() 
        super<Focusable>.showOff()
    }
}
复制代码

调用父类实现,kotlin也使用了关键字super,使用尖括号加父类名字的“super”表明了你想要调用哪个父类方法

1.2 关于重写基类的修饰符,open、final和abstract:默认是final

kotlin中,类,方法和属性默认是final的,即不可重写。如果要允许创建子类,需要使用open修饰符来标示这个类,属性或方法也要添加。

open class RichButton:Clickable{ //这个类是open的,即可继承的
    fun disable(){}  //这个函数是final的,子类不可重写
    
    open fun animate(){} //这个函数是open的,子类可重写

    override fun click() {} //重写的成员默认是open的,除非显式标注 final
}
复制代码

注: 重写的成员默认是open的,除非显式标注为final

同java一样,可以将一个类声明为abstract,这种类可以有一些没被实现并且需要在子类实现的抽象成员(用abstract修饰),抽象成员不能有实现,与java基本一致。

注: abstract始终是open的,同时,接口也始终是open的,都不能声明为final。

修饰符 相关成员 批注
final 不能被重写 类中成员默认使用
open 可以被重写 需要明确标示
abstract 必须被重写 只能在抽象类中使用;抽象成员不能有实现
override 重写父类或接口中的成员 如果没有使用final明确表明,重写的成员默认是open的

1.3 可见性修饰符:默认是public

与java可见性区别:

  • (1)kotlin中,默认是public
  • (2)Java的默认可见性——包私有,在kotlin中并没有,取而代之的是新的修饰符internal,表示“只在模块内部可见”
  • (3)kotlin允许在顶层声明中使用private可见性,包括类、函数,接口和属性,这种声明就会只在声明它们的文件中可见
  • (4)kotlin禁止去引用低可见的类型
  • (5)kotlin中,protected成员只能在类和它的子类中可见;同时,类的扩展函数或属性不能访问它的private和protected成员(因扩展函数或属性时静态函数高级语法糖)
  • (6)kotlin中,外部类不能访问到其内部(或嵌套)类中的private和protected成员
修饰符 类成员 顶层声明
public(默认) 所有地方可见 所有地方可见
internal 模块中可见 模块中可见
protected 类和子类中可见 ----
private 类中可见 文件中可见

1.4 内部类和嵌套类:默认是嵌套类

kotlin中,默认嵌套类不能访问外部类实例,即相当于静态内部类,没有隐式拥有外部类的实例。如果要把变成一个内部类来持有一个外部类的引用的话,需要使用inner修饰符。在kotlin中引用外部类实例语法也与java不同,需使用this@Outer(java是Outer.this)

class Outer{
    inner class Inner{ //声明为inner
        fun getOuterReference():Outer = this@Outer //引用外部类实例
    }
}
复制代码

1.5 密封类

密封类:包含有限数量的类的继承结构。

在使用when表达式的时候,总是提供一个else分支很不方便,如果处理的是sealed类的子类,则可以不再需要提供默认分支,且当sealed添加一个子类时,有返回值的when表达式会编译失败,并告诉你哪里必须修改

sealed class Expr{
    class Num:Expr() //括号构造方法下面讲解
    class Sum:Expr()
    class Plus:Expr()
}

data class Out(val s:String) :Expr() //后续添加的子类

fun eval(e:Expr):String{
    when(e){
        is Expr.Num -> return "num"
        is Expr.Sum -> return "Sum"
        is Expr.Plus -> return "plus"
        is Out-> return "plus" //必须把后续添加的子类放到分支里,否则报错
    }
}
复制代码

注: sealed修饰的这个类始终是open的。且不能用于声明sealed接口

2 类的构造方法和自定义getter/setter属性

类的构造方法区分了主构造方法从构造方法。同时也允许在初始化语句块中添加额外的初始化逻辑

2.1 主构造方法和初始化语句块

一个简单类的声明:

class User(val nickname:String)
复制代码

这段被括号围起来的语句块就叫做主构造方法,主要两个目的,表明构造方法参数和使用这些参数初始化的属性。完成同样事情最明确代码如下:

class User constructor(_nickname:String){ //带一个参数的主构造方法
    val nickname:String //属性
    
    init {    //初始化代码块
        nickname = _nickname
    }
}
复制代码
  • (1)关键字constructor用来开始一个主构造方法或从构造方法的声明;而init关键字用来引入一个初始化语句块,这种语句块包含了类被创建时执行的代码,可以在一个类中声明多个初始化语句块
  • (2)这个例子可以省略init,直接初始化属性,同时如果主构造方法没有注解或可见性修饰符,可以省略constructor关键字
class User(_nickname:String){
    val nickname = _nickname
}
复制代码
  • (3)可以把val/var关键字放在参数前进行简化,同时可以指定参数默认值
class User(val nickname:String = "John")
复制代码

对于子类,需要初始化父类,可在类声明中使用父类的构造方法:

open class Button

class RadioButton:Button()
复制代码

这就是为什么在父类名称后面还需要一个空的括号;接口没有构造方法,所以不用加括号

2.2 从构造方法:用不同方式来初始化类

 open class View{
    constructor(context:Context?){ //从构造方法
        println("this is view1")
    }

    constructor(context: Context?,attr:AttributeSet?){
        println("this is view2")
    }
}
复制代码
  • 如果没有主构造方法,那么每个构造方法必须初始化基类或委托给另一个这样做了的构造方法
  • 主构造方法优先,如果同时有主从构造方法,从构造方法需显式调用this(...)以满足主构造方法
class MyView:View{

    constructor(context: Context?):this(context,null){ //this委托
        println("this is MyView1")
    }

    constructor(context: Context?,attr: AttributeSet?):super(context,attr){ //初始化基类
        println("this is MyView2")
    }
}
复制代码

2.3 接口或抽象类中的属性

  • 1 kotlin接口是可以含有属性声明的。但不能有状态,即没有支持字段存储
interface User{
    val nickName:String
}
复制代码

重写属性有三种方式

class User1(override val nickName: String) :User //有支持字段存储

class User2(val email:String):User{
    override val nickName: String //没有支持字段存储
        get() = email.substringBefore("@")
}

class User3(val id:Int):User{ 
    override val nickName = "$id" //有支持字段存储
}
复制代码
  • 2 接口除了抽象属性声明外,也可以含有具有getter/setter的属性,只要他们没引用支持字段,如:
interface User{
    val email:String
    val nickName:String //并没有持有支持字段和状态
        get() = email.substringBefore("@")
}
复制代码
  • 3 通过getter或setter访问支持字段

属性可以自定义访问器getter/setter以提供被访问或修改时额外逻辑。假设需要在修改时输出日志:

class User(val name:String){
    var address = "unSpecified"
        set(value) {
            println("address was changed")
            field = value // filed表示支持的字段值
        }
}
复制代码

field标识符在getter/setter表示支持字段的值,在getter中,只能读取值,在setter中,既能读取也能修改。

注: 可以只修改一个访问器,另一个会自动用默认的实现

  • 4 有无支持字段的判断:访问属性的方式不依赖于是否含有支持字段。如果显式引用或者使用默认访问器实现,编译器会为属性生产支持字段。如果提供了一个自定义的访问器并且没有使用field(如果是val,就是getter,如果是var,就是getter和setter),支持字段就不会被呈现出来。

  • 5 访问器的可见性默认与属性的可见性相同,但可以修改,如:

class User(val name:String){
    var address = "unSpecified"
        private set //修改setter的可见性为private
}
复制代码

3 data class 和by委托

4 对象声明

  • 1 object关键字:对象声明,定义一个类,并同时创建一个实例(单例
  • 2 与普通类一样,一个对象可以含有属性,方法,初始化语句块等,唯一不同的是,不允许有构造函数
  • 3 对象也可以继承自类或接口,同样也可以在类中声明
  • 4 本质上被编程成了通过静态字段持有的的单一实例类
data class Person(val name:String){

    object NameComparator:Comparator<Person>{  //单一实例
        override fun compare(p0: Person?, p1: Person?): Int {
            if (p0 ==null || p1 == null)
            return 0
            return p0.name.compareTo(p1.name)
        }
    }
}

...
//使用
Person.NameComparator.compare(Person("John"),Person("Cap"))

复制代码

5 伴生对象:替代工厂方法和静态成员

kotlin中的类不能拥有静态成员,java的static关键字并不是kotlin语言的一部分。作为替代,一般使用顶层函数和对象声明。大多数情况下推荐顶层函数,但顶层函数无法访问private成员,像工厂方法这种例子就得使用对象声明。

  • 1 kotlin中,在类中声明的对象可以使用特殊关键字标记:companion。从而可以直接通过容器名称来访问这个对象的方法和属性,最终看起来就像是java的静态方法调用:
class A{
    companion object{
        fun bar() = println("called")
    }
}

...
A.bar() // 输出 called,像静态方法一样调用

复制代码

:本质上也是被编程成了通过静态字段持有的的单一实例类

  • 2 伴生对象可以访问类中的private成员,包括private构造方法,是实现工厂方法的理想选择
class User private constructor(val name: String){
    companion object{
        fun newFaceBookUser(email:String) = User(email.substringBefore('@'))
        
        fun newMarvelUser() = User("Cap")
    }
}

复制代码

可以通过类名来调用companion object的方法

val facebook = User.newFaceBookUser("xxx@163.com")
val marvel = User.newMarvelUser()
复制代码
  • 3 伴生对象是一个声明在类中的普通对象声明,可以有名字(不指定则默认为 companion
class Person(val name:String){
    companion object Loader{
        fun fromJSON(jsonText:String):Person = ...
    }
}
复制代码

则可以通过两种方式使用

val person = person.Loader.fromJSON(...)
val person2 = person.fromJSON(...)
复制代码
  • 4 伴生对象可以像其他对象声明一样,实现接口,因伴生对象的特性,可以直接将包含它的类的名字作为实现了改接口的对象实例来使用
interface JSONFactory<T>{
    fun fromJSON(json:String):T
}

class Hero private constructor(val heroName:String){
    companion object Loader:JSONFactory<Hero>{
         override fun fromJSON(json: String): Hero {
            return Hero("IronMan")
        }
    }
}

fun <T> loadFromJson(jsonFactory: JSONFactory<T>) = jsonFactory.fromJSON("John")

//使用,直接将包含它的类的名字作为实现了改接口的对象实例来使用
 loadFromJson(Hero)
复制代码
  • 5 伴生对象可以像普通对象一样使用扩展函数或属性,接收者类型显式指出伴生对象名字即可
class Person(val name:String){
    companion object{
    }
}

//扩展函数显式指明默认名字companion
fun Person.companion.fromJSON(json:String):Person{ 
    ...
}
复制代码

使用时,仍可以直接用包含的类名来调用

val p = Person.fromJSON(json)
复制代码

6 对象表达式:kotlin的匿名内部类写法

object关键字不仅能用来声明单例对象,还能用来声明匿名对象

与java匿名对象不同的是:

  • 1 与java匿名对象只能扩展一个类或接口不用,kotlin的匿名对象可以实现多个接口或不实现接口
  • 2 与对象声明不同,匿名对象不是单例的,每次对象表达式被执行,都会创建一个新的对象实例
  • 3 与java匿名对象一样,对象表达式代码可以访问创建它的函数中的变量,但不用的是,访问没有被限制在final变量之中
class MainActivity : AppCompatActivity() {

    val c:String = ""
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        var clickCount = 0
        
        val view :View= findViewById(R.id.action)
        
        //这里的匿名对象表达式可以被lambda代替
        view.setOnClickListener(object :View.OnClickListener{ 
            override fun onClick(p0: View?) {
                clickCount ++ //不需要final
                println(c)
            }
        })
    }
}
复制代码

匿名对象也可以直接存储到一个变量中

val listener = object :View.OnClickListener{ 
            override fun onClick(p0: View?) {
                ...
            }
        }
复制代码
关注下面的标签,发现更多相似文章
评论