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

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

一 函数的定义和调用

主要是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()
}
复制代码

使用:json

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

1 命名参数

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

注: 为避免混淆,当指明了一个参数名称后,那它以后的全部参数都要显式指明名称app

2 默认参数值

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

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做为后缀的工具类。post

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?) {
                ...
            }
        }
复制代码
相关文章
相关标签/搜索