kotlin程序员我爱编程

Kotlin之类、对象和接口

2017-12-24  本文已影响234人  程自舟

Kotlin的类和接口与Java的类和接口是有一定的区别的。
Kotlin的接口是可以包含属性声明。
Kotlin默认的声明是fianl 和public的。
Kotlin里嵌套的类默认并不是内部内,不包含对器外部类的隐式调用。

构造方法简短,同时也有完整的语法可以让你声明带有重要的初始化逻辑。属性声明简短也可以方便的定义自己的访问器实现。
Kotlin编译器能够生成有用的方法来避免冗余。比如将一个类声明为data类可以让编译器生成若干标准方法,同时也可以避免书写委托方法(委托模式kotlin原生支持)。

定义类继承结构

Kotlin的类继承结构定义与Java类似但是有一些不一样的默认行为。同时可以了解用于限制一个类可能存在的子类sealed修饰符。

kotlin的接口

Kotlin的接口和Java8类似,可以包含抽象方法定义和非抽象方法实现(Java8默认方法类似)。但Kotlin接口不能包含任何状态。

/**
 * 使用interface来声明一个接口
 */
interface Clickable {
    fun click();
}


/**
 * 实现接口
 *kotlin在类名后面使用:来代替Java中的extends和implements关键字
 *和Java一样,一个类可以实现多个接口,但是只能继承一个类。
 */
class Button :Clickable {
    override fun click()= println("this is clicked")
}


//调用
fun main(args: Array<String>) {
    val button:Button = Button();
    println(button.click()); //this is clicked
}

Kotlin的override修饰符和Java的@Override注解类似,用来标注被重写的父类或者接口的方法和属性。不同的是,Kotlin里的override修饰符是强制要求的。这会避免先写出实现方法再添加抽象方法造成意外重写(此时你的 代码将不能编译)。除非显式标注override或重命名。

接口的方法可以有一个默认实现。对此Java8需要你标注default关键字,kotlin却并不需要,只需要提供一个方法体。

/**
 * 使用interface来声明一个接口
 *
 */
interface Clickable {
    //普通方法声明
    fun click()
    //带默认的实现方法
    fun showoff() = println("this is clickable showoff")
}

实现了Clickable接口仍需要为click提供一个实现,但是showoff方法你可以重新定义(如果你对默认showoff感到满意可以直接忽略)。

class Button :Clickable {
 //  抽象方法的实现
   override fun click()= println("this is clicked")
    //重新定义默认实现
    override fun showoff() {
        super.showoff()   //这里会先执行接口Clickable的showoff(不需要可以去掉)
        println("this is Button showoff") //这里是重新定义接口Clickable的showoff
    }
}

//调用
fun main(args: Array<String>) {
    val button: Button = Button()
  //保留super.showoff()输出  this is clickable showoff,this is Button showoff
  //删除super.showoff()输出 this is Button showoff
    println(button.showoff())
}

定义另一个实现同样方法的接口

/**
 * 定义另一个实现同样方法的接口
 */
interface Focusable {
    fun setFocus(b: Boolean) =
            //根据b来输出不同的字段
               println("this is ${if (b) "got" else "lost"} focus.")
    
    fun showoff() =  println("this is focusable showoff")
}

如果一个类实现了这两个都包含了带默认的实现方法showoff(),这个类不会调用任何一个接口的showoff实现,同时如果你没有显示的声明showoff,你会得到一个编译错误。

class Button :Clickable ,Focusable{
  //显示调用showoff,不能有接口默认实现  
  //不调用showoff,抛出编译异常:
//Class 'Button' must override public open fun showoff(): Unit defined in //src.iface.Clickable because it inherits multiple interface methods of   it.
   override fun showoff() {
  }
  
    override fun click()= println("this is clicked")

    override fun setFocus(b: Boolean) {
        super.setFocus(b)
    }
}

显示声明并调用showoff

/**
 * 调用继承自接口的方法实现‘,与Java使用相同关键字super
 */
class Button :Clickable ,Focusable{
    //如果同样的继承成员有不止一个实现,必须提供一个显示实现
    override fun showoff() {
        //super<父类名>表明你想调用哪个父类方法
        super<Clickable>.showoff() 
        super<Focusable>.showoff()
    }

    override fun click()= println("this is clicked")

}

//调用
fun main(args: Array<String>) {
    val button: Button = Button()

    println(button.showoff())//this is clickable showoff, this is focusable showoff
    //setFocus在Focusable接口声明中实现被Button自动继承
    println(button.setFocus(true))//this is got focus.
    println(button.click())// this is clicked
}

如果只需要调用一个实现,Button类里需要这么声明

/**
 * 调用继承自接口的方法实现
 */
class Button :Clickable ,Focusable{

    //只需要调用一个继承的实现
    override fun showoff()=super<Focusable>.showoff()

    override fun click()= println("this is clicked")

}

注:Kotlin1.0是以Java6为目标设计的,Java6并不支持接口中的默认方法。因此编译器会把kotlin带默认方法的接口编译成一个普通接口和一个将方法体作为静态函数的类的结合体。接口只包含声明,类包含静态所有实现。所以如果在Java中实现这样一个接口,必须为所有方法,包括kotlin中方法体方法定义自己实现。

open、fianl和abstract修饰符:默认为final

Java允许你创建任意类的子类并重写任意方法,除非显式用final修饰。这在便利的同时也造成一些问题。
对基类进行修改会导致子类不正确的行为,即所谓的脆弱的基类问题,因为基类的修改不再符合在其子类中的假设。如果基类没有提供子类具体实现的明确规则(哪些方法需要被重写及如何重写),会带来子类以预期之外的方式重写方法的风险。而又因为不可能分析所有子类,基类因此变的"脆弱"。所以在Java中如果没有特别需要在子类中被重写的类或方法应该被显示标注final。

Kotlin采用了同样的哲学思想。不同的是Java的类和方法默认是open的,而Kotlin中默认是final的。
如果想让kotlin允许创建一个类的子类,需要使用open修饰符来标示,此外,每一个可以被重写的属性或方法也应该添加open修饰符。


/**
 * 声明一个带open方法的open类
 */
class RichButton :Clickable {  //这个类是open的,其它类可以继承

    fun disable(){} //final修饰,子类不能重写

    open fun animate(){} //open修饰,可以在子类重写它

    override fun click() {  // 重写了open函数,并且本身也是open的
    }

}

如果重写了基类或接口的成员,重写的成员同样默认是open的,如果想阻止这一情况,只需显示的将重写成员标注为final。

/**
 * 声明一个带open方法的open类
 */
class RichButton :Clickable {  
// 显示的添加final修饰(不添加默认为open),阻止子类重写
  final  override fun click() {  
    }

}

类默认为final带来一个重要好处就是使智能转换在大量场景下变为可能。因为智能转换只能在进行类型检查后并没有改过的变量上起作用,即val。

abstract

与Java相同,abstract将类声明为抽象,不能被实例化。Kotlin中类的抽象成员始终是open的,所以不需要显示的使用open修饰。

/**
 * 抽象类,不能创建实例
 */
abstract class Animated  {
    //抽象函数,必须被子类重写
    abstract fun animate()


    open fun stopAnimating(){ //非抽象函数,并不是默认是open的,但可以显式标注

    }

    fun animateTwice(){ //省略open

    }

}
类中的访问修饰符

可见性修饰符

kotlin中的可见性修饰符与java总体类似,同样使用public,protected,和private修饰符。区别在于默认的可见性不一样,kotlin如果省略了修饰符,默认声明是public的。
kotlin并不支持Java的默认可见性—包私有。kotlin只把包作为命名空间里组织代码的一种方式。
作为替代方案,kotlin采用*internal表示"只在模块内部中可见(模块可以看作一个module,一个项目等)。
*internal可见性的优势在于它提供了对模块实现细节的真正封装。

kotlin 在顶层声明中允许使用private,包括类,函数,属性。这些声明只会在文件中可见。

可见性修饰符的简洁说明

错误代码示例

/**
 * 正确解决方法:将函数声明为internal或把类声明为public
 */
 internal  open  class RichButton :Focusable{

  private fun  yell() = println("yell")

  protected fun whisper() = println("why?")

}

fun RichButton.giveSpeech(){ //默认public暴露internal接收者类型,错误
  yell()  //声明为private,不能访问yell
  whisper() //只能子类使用,不能声明whiisper
}

可见修饰符在kotlin中有一个通用的规则:类的基础的类型和类型的参数列表中用到的所有的类或者函数签名都有与这个类或者函数本身相同的可见性。(此规则能确保你在调用一个函数或者继承一个类时候能始终访问到所有类型)。 Kotlin禁止public修饰去引用低可见的修饰类型。

:* kotlin的protected修饰符与Java不同(Java中同包可以访问),在kotlin中只允许在类和它的子类成员中可见。同时,kotlin的类的扩展函数因为不能访问类本身private和protected成员。* Kotlin中的可见修饰符在编译成Java后会把保留,唯一不同的是private会在Java中变成包私有声明。internal会变为public。

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

Kotlin中一个外部类不能看到其内部(或者嵌套)类中的private成员。嵌套类也不能访问外部类的实例,除非你特别做出要求(默认不可以)。

/**
 * 辅助接口,空实现可序列化
 */
interface State:Serializable {

}

/**
 * 定义View,状态可序列化
 */
interface View{
    fun  getCurrentState():State //保持视图状态
    fun restoreState(state: State){} //保存视图状态
}
/**
 * Java的实(伪代码)
 */
class Button implements View {
    @NotNull
    @Override
    public State getCurrentState() {
        return new ButtonState(); //创建一个新的实类
    }

    @Override
    public void restoreState(@NotNull State state) {

    }

    /**
     * Java内部内实现,正确姿势应该声明为static
     */
    public class ButtonState implements State{

    }
}

注明是伪代码是因为它会抛出一个java.io.NotSerializable Exception:Button异常。为什么序列化会指向Butoon而不是ButtonState?因为在Java中,在一个类中声明另一个类,它会默认变为内部类。内部类会隐式地存储外部引用,所以ButtonState持有Button的引用,而Button不是可被序列化的,并且这个引用也同时会破坏掉ButtonState的序列化。
解决这个问题,只需要将ButtonState声明为static。嵌套内声明为static会从这个类中删除包围它的类的隐式作用。

kotlin中使用嵌套类

在kotlin中,内部类默认行为与上述行为恰恰相反。

/**
 * 辅助接口,空实现可序列化
 */
interface State : Serializable {

}

/**
 * 定义View,状态可序列化
 */
interface View {
    fun getCurrentState(): State //保持视图状态
    fun restoreState(state: State) {} //保存视图状态
}

/*
 *  这个类与java中的静态嵌套类相似。
 */
class ButtonState : State {}

kotlin中没有显示修饰符嵌套类与Java的static嵌套类是一样。如果内部内需要持有一个外部类的引用需要用inner修饰符。

  /**
   * 使用inner修饰符,持有外部内引用
   */
  inner class ButtonState : State {}
嵌套类和内部类在java与kotlin中对应的关系
类A在另一个类B中声明 在Java中 在Kotlin中
嵌套类(不存储外部类的引用) static class A class A
内部类(存储外部内的引用) class A inner class A

在kotlin中引用外部类实例的语法与Java也不同。需要使用this@外部类名

  class RichButton :View{
  override fun getCurrentState(): State {
    TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
  }
  

  /**
   * 使用inner修饰符,持有外界引用
   */
  inner class ButtonState : State {
  //需要使用*this@外部类名*
    fun getOutCls():RichButton = this@RichButton
  }

}

kotlin嵌套类的另一个可能使用场景在于创建一个包含有限数量的类的继承结构。

密封类:定义受限的类继承结构

使用接口实现表达式求和

class Num(val value: Int) : Expr

class Sum(val left: Expr, val right: Expr) : Expr

fun eval(e:Expr):Int=
        when(e){
            is Num -> e.value
            is Sum -> eval(e.right)+ eval(e.left)
            else -> throw IllegalArgumentException("throw") //必须检查else分支
        }

//调用
fun main(args: Array<String>) {
        println(eval(Sum(Num(1),Num(2)))) //3
}

使用when结构来执行表达式的时候,kotlin编译器会强制检查默认选项(没有选择返回有意义的值时候,直接抛出异常)。这在有时候显得很不方便,总是不得不添加一个默认分支,会给代码带来潜在的bug。

kotlin使用sealed类来提供解决方案。**直接为父类添加一个sealed修饰符,对可能创建的子类做出严格的限制。所有直接的子类必须嵌套在父类中。


/**
 * 作为密封类的表达式
 * sealed修饰符默认这个类是open的,不需要显式添加open
 */
sealed class Expr{ //将基类作为密封的...
    //将所有可能的类作为嵌套类列出
    class Num(val value: Int) : Expr()
    class Sum(val left: Expr, val right: Expr) : Expr()
}


fun eval(e:Expr):Int=
        when(e){
            is Expr.Num -> e.value
            is Expr.Sum -> eval(e.right)+ eval(e.left)
//            else -> throw IllegalArgumentException("throw") 省略默认分支
        }


//调用
fun main(args: Array<String>) {
        println(eval(Expr.Sum(Expr.Num(1), Expr.Num(2)))) //3
}

在when中使用sealed类并且添加一个新的子类的时候,有返回值的when表达式会导致编译失败,并明确告诉你哪里的代码必须要修改。所以Expr类有一个只能在类内部调用的private构造方法。同时不允许也不能声明一个sealed接口,因为kotlin编译器不能确保任何人能在Java代码中实现这个接口。
在kotlin1.0中,sealed功能限制非常严格。所有子类必须是嵌套且不能为data类。1.1版本解除限制并允许在同一文件中任何位置定义sealed类的子类。

声明一个带非默认构造方法或属性的类

Java中一个类可以声明一个或多个构造方法。kotlin与其类似,不同的是区分了主构造方法(通常为主要而简洁的初始化类的方法,并在类体外部声明)和从构造方法(在类体内部声明)。同时,kotlin也允许在初始化语句块中添加额外的初始化逻辑。

初始化类:主构造方法和初始化语句块
class Person(val name:String) 

通常来讲,类的所有声明都应该在{}中,而上述代码却只包含了声明在()中。实际上()里的语句块就叫主构造方法。它主要目的有两个:表明构造方法的参数,以及定义使用这些参数的初始化属性。

class Person constructor(_name:String){ // 带一个参数的主构造方法
    val mName:String

    //初始化语句块
    init {
        mName=_name
    }
}

上述代码使用到constructor(用来开始一个主构造方法或从构造方法的声明)和init(用来引入一个初始化语句块,该语句块包含了在类被创建的时候执行的代码,并会与主构造方法一起使用)。主构造方法语法有限制,不能包含初始化代码,所以需要初始化语句块。初始化语句块也可以在一个类里有多个声明。
构造方法参数_name中下划线用来区分属性的名字和构造方法参数的名字。也可以使用同样的名字,通过this来消除歧义。

  val name:String

    //初始化语句块
    init {
        this.name=name
    }

上述代码也可以不需要把初始化代码放在初始化语句块中,因为它是可以与name属性的声明结合。如果主参构造方法没有注解或可见性修饰符,同样可以去掉constructor关键字。

class Person(_name: String) { // 带一个参数的主构造方法
    //用参数初始化属性
    val name = _name

}

如果属性用相应的构造方法参数来初始化,代码可以进一步简化。

class Person(val name: String) { // 带一个参数的主构造方法
    
}

所有 Person类的声明都是等价的。同时也可以像函数参数一样为构造方法参数声明一个默认值。

class Person(val name: String,
             val isSubscribed: Boolean = true //为主构造方法参数提供一个默认值
) { 

}

//调用
fun main(args: Array<String>) {
    //isSubscribed使用默认值
    val alice = Person("Alice")
    println(alice.isSubscribed) //true
    //可以按照声明顺序写明所有函数
    val bob = Person("Bob", false)
    println(alice.isSubscribed) //false
    //也可以显示地为某些构造方法参数表明名称
    val carol = Person("Carol", isSubscribed = false)
    println(carol.isSubscribed) //false

}

注:如果所有的构造方法参数都有默认值,编译器会生成一个额外的不带参数的构造方法来使用所有的默认值。这可以让kotlin使用库时通过无参构造方法来实例化类,从而变得更简单。

如果类具有一个父类,主构造方法同样需要初始化父类。可以通过在基类列表的父类引用中提供父类的构造方法参数方式做到这一点。


class Man( val manName:String, val isMan:Boolean=false):Person(manName,isMan) {
}

如果没有给一个类声明任何构造方法,将会生成一个不做任何事情的默认构造方法。

/**
 * 无参会生成一个不带任何参数的默认构造方法
 */

open class Person{

}

如果继承Person类并且没有提供任何构造方法,必须显示调用父类构造方法(即使没有任何参数)。

/**
 * 与接口不同(接口无构造方法),必须显示调用父类构造方法
 * 
 */
class Man:Person() {
}

如果想确保类不被其它代码实例化,必须把构造方法声明为private。

/**
 * 构造声明为private,外部代码不能实例化
 */
class Secretive private constructor(){
}

在Java中,通过private构造方法禁止实例化表示为:静态实用工具成员容器或者是单例。kotlin可以使用顶层函数作为静态实用工具,单例可以使用对象声明(后文)。

构造方法:用不同的方式初始化父类

大多数场景中类的构造方法是非常简单明了:它要么没有参数或者直接把参数与对应的属性关联。所以kotlin的主构造方法设计语法简短整洁。
然而理想很丰满,现实很骨感。所以kotlin也允许类定义足够多的构造方法。

但是通常来讲,kotlin使用多个构造方法并不常见,kotlin支持参数默认值和参数命命名语法涵盖了java的方法重载(不要声明多个构造方法用来重载和提供参数默认值,而应该直接表明默认值)。

最常见的使用场景来自于扩展一个框架类来提供很懂构造方法,以便于在不同的方式来初始化,这在Android开发里极其常见。

/**
 * 无主构造方法,只有两个(可以声明任意多个)从构造方法
 */

 open class View {
    //从构造方法
    constructor(ctx:Context){
        
    }
  //  从构造方法
    constructor(ctx: Context,attr:Attributes){
        
    }
}

从构造方法使用 constructor关键字引出。如果扩展该类,也可以声明同样的构造方法。


/**
 * 定义两个从构造方法使用super()关键字调用对应父类从构造
 */
class MyButton: View{
    constructor(ctx:Context):super(ctx){

    }
    constructor(ctx: Context,attributes: Attributes):super(ctx,attributes){

    }
}

也可以像Java一样,使用this()关键字,从一个构造方法调用类的另一个构造方法。

/**
 * 定义两个从构造方法使用super()关键字调用对应父类从构造
 */
class MyButton: View{
  //委托给这个类的另一个构造方法,为参数传入默认值
    constructor(ctx:Context):this(ctx,null){

    }
  //?表示可为空(父类AttributeSet也要声明可为空)
    constructor(ctx: Context, attributes: AttributeSet?):super(ctx,attributes){

    }
}

如果类没有主构造方法,那么每个从构造方法必须初始化基类或者委托给另一个这样做了的构造方法。

实现接口声明中的属性

在kotlin中,接口可以包含抽象属性声明。

interface Person {
    val name:String
}

这意味着实现Person接口的类需要提供一个取得name值的方式。接口并没有说明这个值应该存储到一个支持字段还是通过getter来获取。接口本身并不包含任何状态,因此只有实现这个接口的类在需要的情况下存储这个值。

/**
 * 主构造方法属性
 */
class PrivatePerson(override val name:String):Person {
}


class SubscribingPerson(val email:String):Person{
    //自定义getter
    override val name: String
    get() = email.substringBefore('@')
}

class FacebookPerson(val accountId:Int):Person{
    //属性初始化(此处省略getFacebookName函数)
    override val name: String=getFacebookName(accountId)
}

//调用
fun main(args: Array<String>) {
    println(PrivatePerson("czz@kotlin.com").name)//czz@kotlin.com
    println(SubscribingPerson("czz@kotlin.com").name)//czz
}

对于PrivatePerson来说,直接使用了简洁语法在主构造方法中声明了一个属性。这个属性来自Person接口抽象属性,所以应该标注为 override。
对于SubscribingPerson来说,name属性通过自定getter实现,这个属性没有一个支持字段来存储它的值,它只有一个getter在每次调用时得到值。
对于 FacebookPerson来说,初始化时候name属性与值关联。getFacebookName函数在其它位置定义(假设),这个函数开销巨大,所以应该只在初始化阶段只调用一次。

以上实现方式尽管看起来很相似,但是仍有差异。SubscribingPerson有一个自定义getter在每次访问时候计算substringBefore,而FacebookPerson中属性却是有一个支持字段来存储在类初始化时候计算得到的数据。

接口也可以包含具有getter和setter属性,前提是它们没有引用一个支持字段(支持字段需要在接口中存储状态,而接口不允许存储状态)。

interface Person {
  //email属性必须在子类重写
    val email: String
    //name属性可以被继承
    val name: String
  //属性没有支持字段,结果值是通过每次访问时候通过计算得到
        get() = email.substringBefore('@')
}

通过getter和setter访问支持字段

既可以存储值又可以在值被访问和修改时提供额外逻辑的属性。

class User(val name:String) {
    var address:String="china"
    set(value) {
        //读取支持的字段值
        println("""Address was changed for $name:"$field" ->"$value". """.trimIndent())
        field=value //更新支持的字段值
    }
}

//调用
fun main(args: Array<String>) {

    val user = User("Alice")
    //常规使用修改属性语法,但是底层调用了setter,这里setter被重新定义了
    user.address = "China"
}

setter函数体中,使用了特殊的标识符field来访问支持字段值,getter中只能读取值,setter中既能读取也能修改它。
当然,也可以只重定义可变属性的一个访问器(上述代码getter默认的并且只有返回字段值,没必要重定义)。

有支持字段的属性和没有的区别在于:访问属性的方式并不依赖于它是否含有支持字段。如果你显式地引用或者使用默认的访问器实现,编译器会为属性生成支持字段。如果提供一个自定义访问器实现并没有使用field(val为getter,可变为两个访问器),支持字段不会被呈现。

修改访问器的可见性

有时候不需要修改访问器的默认实现,但是需要修改它的可见性。访问器的可见性默认与可见性相同。但是需要在get活set关键字前添加可见性修饰符。

/**
 * 计算单词加在一起总长度
 */
class LengthCounter {
    var counter:Int=0
    private set  //不能在类外修改这个属性

    fun addWOrd(word:String){
        counter +=word.length
    }
}

//调用
fun main(args: Array<String>) {

val lengthCounter = LengthCounter()
    lengthCounter.addWOrd("hello!")

    println(lengthCounter.counter) //6
}

编译器生成方法:数据类和类委托

Java平台定义了在许多类中呈现的方法,这些代码都带有样板,自动化生成容易导致源代码文件混乱。kotlin编译器把这些代码自动生成放到幕后。

通用对象方法

kotlin帮助自动生成toSting,equals,hashCode。

/**
 * 简单的用来存储客户名字和邮编的类
 * Client类的最初声明
 */
class Client (val name: String,val postalCode:Int){
}

kotlin中toSting的实现

class Client (val name: String,val postalCode:Int){
    
    //重写toSting
    override fun toString(): String = "Client(name=$name,postalCode=$postalCode)"
}

//调用
fun main(args: Array<String>) {

val lengthCounter = LengthCounter()

    val client = Client("Alice",342562)
    println(client.toString())//Client(name=Alice,postalCode=342562)
}
对象想等性:equals()

在kotlin中,"=="仅代表检查对象是否相等,而不是比较引用。kotlin编译器会编译成"equals"。

val lengthCounter = LengthCounter()

    val client = Client("Alice",342562)
    val client1 = Client("Alice",342562)

    //==比较的是对象是否相等,而不是比较引用,编译器会编译为调用"equals"
    println(client==client1) //false

}

在Java中,"=="可用来比较基本数据类型(比较值)和引用类型(比较引用)。
在kotlin中,"=="运算符比较的是两个对象默认方式,本质上也是通过equals来比较两个值。所以,如果在kotlin类中重写equals,能够安全使用"=="来比较引用。

/**
 * 简单的用来存储客户名字和邮编的类
 * Client类的最初声明
 */
class Client(val name: String, val postalCode: Int) {
    //重写equals
    //"Any"是Java的Object的模拟,kotlin中所有类的父类,前端很容易理解
    //Any?表示为可空类型,即"other"可以为空
    override fun equals(other: Any?): Boolean {
        //检查other是不是一个Client
        //is 检查是Java中instanceof的模拟,检查一个值是否一个指定类型
        if (other == null || other !is Client){
            return false
        }
        //检查对应属性是否相等
        return name==other.name&&postalCode==other.postalCode
    }

//调用
fun main(args: Array<String>) {

val lengthCounter = LengthCounter()

    val client = Client("Alice",342562)
    val client1 = Client("Alice",342562)

    //重写"equals"后
    println(client==client1) //true

}

}

kotlin中 override修饰符是强制使用的,所以意外编写 fun equals(other: Client)时候会得到保护,kotlin认为是新增了一个方法而不是重写了 equals。

Hash容器:hashCode()

然而,重写了equals后并不能让相同的属性值也是相等的(hashCode的缺失)。

fun main(args: Array<String>) {

    val processed = hashSetOf(Client("Alice",342562))
  //缺少hashCode方法
    println(processed.contains(Client("Alice",342562))) //false
}

hashCode有一个通用契约:如果两个对象相等,它们必须有着相同hash值。而在kotlin的HashSet中是以一种优化方式来比较的:先比较hash值,相等才会再去比较真正值。
所以即使拥有equals但是hashCode的缺失也不会让HashSet去正常的工作。这也是为什么hashCode通常一起与equals一起被重写。

class Client(val name: String, val postalCode: Int) {
            ...
    //重写hashCode
    override fun hashCode(): Int =
            name.hashCode() * 31 + postalCode


}

fun main(args: Array<String>) {

    val processed = hashSetOf(Client("Alice", 342562))
    println(processed.contains(Client("Alice", 342562)))// true
}

重写的这些方法,Kotlin编译器会帮助自动生成。

数据类:自动生成通用方法实现

在kotlin 中 data修饰符会让类自动生成 equals, hashCode,toString这些标注方法。

/**
 * data修饰的数据类
 * 
 */
 data class Client(val name: String, val postalCode: Int) {


}

equals和hashCode方法会将所有在主构造方法中声明的属性纳入考虑。没有在主构造方法声明的属性将不会加入到相等检查和哈希值计算中去(equals和hashCode无效)。

数据类和不可变性:copy()

数据类属性虽然没有要求是val,也可以声明var,但是kotlin仍强烈推荐为val,让数据类实例不可变。如果使用hashMap或类似的容器键,这是必须的要求。否则用作容器的对象键被修改会导致容器无效化。同时在多线程中对象会保持初始值,也不会担心其它线程修改对象值。

创建副本通常是修改实例的好选择:副本有着单独的生命周期而又不会影响代码中引用原始实例位置。所以kotlin编译器会为数据类多生成一个copy方法:允许copy实例,并在copy时候允许修改某些属性值。

class Client(val name: String, val postalCode: Int) {
                    ...
    //copy方法实现
    fun copy(name: String=this.name,postalCode: Int=this.postalCode)=Client(name,postalCode)

}

//调用
fun main(args: Array<String>) {

    val bob = Client("Bob", 9723293)
    //调用数据类copy方法修改属性值
    println(bob.copy(postalCode = 123456))//Client(name=Bob,postalCode=123456)
}

类委托:使用"by"关键字

设计大型面向对象系统一个常见问题就是由继承的实现导致的脆弱性。当系统不断演进并且基类的实现被不断修改或新方法添加进去,会让类的行为假设失效,从而偏离正确的行为。

kotlin的设计识别了这样的问题,并将类默认视作final的。这确保只有设计成扩展类才可以被继承。当使用这些类时候,就会知道它是开放的,就会容易注意这些修改和派生类的兼容。

但是骨干的现实通常需要像并没有设计成可扩展的类添加一些行为。这通常以装饰器模式实现。装饰器模式本质是创建一个新类实现与原始类一样的接口并将原类作为一个字段保存。这样就不需要修改原始类,只需要直接转发给原始类的实例。

/**
 * 实现Collection的接口装饰器
 */
class DelegatingCollection<T> : Collection<T> {

    private val innerList = arrayListOf<T>()

    override val size: Int get() = innerList.size
    override fun isEmpty(): Boolean = innerList.isEmpty()
    override fun contains(element: T): Boolean = innerList.contains(element)
    override fun iterator(): Iterator<T> = innerList.iterator()
    override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)


}

Kotlin将类委托作为一个语言级别的功能做了头等支持。无论什么时候实现一个接口,都可以使用by关键字将接口实现委托到另一个对象。

/**
 * 使用关键字by委托对象实现
 */
class DelegatingCollection<T>(
        innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList{


}

使用关键字by会使类中所有的方法实现都消失了。编译器会自动生成,并且实现与上述装饰器实现是相似的,从而避免重写。

使用by来实现一个集合,计算向它添加元素的尝试次数。执行去重操作可以使用这样的集合,通过比较元素的尝试次数和集合的最终大小来评判这种处理效率。

class CountingSet<T>(
        val innerSet: MutableCollection<T> = HashSet<T>()
) : MutableCollection<T> by innerSet { // 将MutableCollection<T>的实现用by委托给innerSet
    var objectsAdded = 0



    //不使用委托,提供一个不同的实现
    override fun add(element: T): Boolean {
        objectsAdded++
        return innerSet.add(element)
    }

    //不使用委托,提供一个不同的实现
    override fun addAll(elements: Collection<T>): Boolean {
        println(elements.size)
        objectsAdded += elements.size
        return innerSet.addAll(elements)
    }
}

//调用
fun main(args: Array<String>) {

    val cset = CountingSet<Int>()
    cset.addAll(listOf(1,1,2))
    //3 obj were added,1 remain
    println("${cset.objectsAdded} obj were added,${cset.size} remain")
}

重写add 和addAll方法来计数,并将MutableCollection接口剩下的实现委托给你包装的容器。重要的部分并不需要对底层集合的实现引入任何依赖,而只需依赖底层集合文档列出的API来实现。

obeject关键字:将声明一个类与创建一个实例结合起来(重点)

kotlin中object关键字在多种情况下出现,但是它们都遵循同样的核心理念:这个关键字定义一个类并同时创建一个实例(对象)。
常见使用场景包括
对象声明是定义单例的一种方式
伴生对象可以持有工厂方法和其它与这个类相关,但在调用时并不依赖类实例的方法。它们的成员可以通过类名来访问。
对象表达式用来替代Java的匿名内部类。

对象声明:创建单例

kotlin通过使用对象声明将对象声明与类声明与该类单一实例结合到一起。

/**
 * obejct关键字引入对象声明
 */
object Payroll {
    val allEmployees= arrayListOf<User>()
    fun calculateSalary(){
        for (
            user in allEmployees
        ){
            //do something
        }
    }
}

对象声明通过关键字object引入。一个对象声明可以非常高效地以一句话来定义一个类和一个该类的变量。
与类相同,对象声明也可以包含属性,方法,初始化语句块等声明。唯一不允许就是构造方法(主构造和从构造都不允许)。因为对象声明在定义的时候就立即创建了,所以它定义构造方法无意义。

对象声明调用方法与访问属性与变量方式相同。

   //对象声明访问属性
    Payroll.allEmployees.add(User("Jack"))
    
    //对象声明访问方法
    Payroll.calculateSalary()

对象声明同样可以继承自类和接口。通常在使用框架时候需要去实现一个接口,但实现并不包含任何状态的时候很有用。


/**
 * kotlin对象声明实现java Comparator接口比较文件路径(忽略大小写)
 */
object CaseInsensitiveFileComparator : Comparator<File> {
    override fun compare(file1 : File, file2: File): Int {
       
        return file1.path.compareTo(file2.path,ignoreCase = true)
    }
}

fun main(args: Array<String>) {

   println(CaseInsensitiveFileComparator.compare(File("/User"),
           File("/user"))) // 0
}

可以在任何使用普通对象的地方使用单例对象。例如也可以将这个对象传入一个接收Comparator的函数。

fun main(args: Array<String>) {
    
    //list实现了Comparator接口
   val fiels = listOf(File("/User"), File("/Person"))

    //sortedWith函数会返回一个根据特定的比较器排序过的列表。
    println(fiels.sortedWith(CaseInsensitiveFileComparator))//[\Person, \User]
}

注:Kotlin对象声明的单例模式和Java一样,对对象实例化没有任何控制,且不能通过构造方法指定特定的参数。这就表示无法在单元测试或系统的不同配置中替换掉对象自身的实现,或对象依赖的其它类。如有此需要,需要依赖注入框架Guice。

kotlin同样也可以在类中声明对象。这样的对象同样只有一个单一实例:它们在每个容器类的实例中并不具有不同实例。比如,在类中放置比较特定对象的比较器比较符合逻辑。

/**
 *嵌套类实现Comparator
 */
data class Person(val name: String) {

    object NameComparator : Comparator<Person> {
        override fun compare(person1: Person, person2: Person): Int =
                person1.name.compareTo(person2.name)

    }
}

fun main(args: Array<String>) {

    val persons= listOf(Person("Bob"),Person("Alice"))

    println(persons.sortedWith(Person.NameComparator))//[Person(name=Alice), Person(name=Bob)]
}

Kotlin中对象的声明被编译成通过静态字段来持有它的单一实例类,这个字段名字始终都是INSTANCE。因此,从Java代码中使用Kotlin对象,可以直接访问静态的INSTANCE。

 //Java中使用Kotlin对象,INSTANCE即CaseInsensitiveFileComparator
    CaseInsensitiveFileComparator.INSTANCE.compare(file1,file2)

伴生对象:工厂方法和静态成员的领域(重点)

kotlin类中不能拥有静态成员,java的static并不是kotlin语言的一部分。作为替代,kotlin依赖包级别的函数(大多数情况下替代java的静态方法)和对象声明(其它情况下替代java的静态方法,同时包括静态字段)。

kotlin推荐尽量使用顶层函数,但是顶层函数不能访问private成员。因此如果需要写一个可以在没有类实例的情况下调用但是需要访问类内部的函数,可以写成此类中对象声明的成员。此函数即称为工厂方法。

在类中定义对象之一可以使用一个特殊的关键字来标记:companion。这样就获得了直接通过容器类名称来访问这个对象的方法和属性的能力,不再需要显示的指明对象名称(此用法看起来非常像java的静态方法调用)。

class A {

    //使用companion object可直接通过容器访问方法和属性
    companion object {
        fun bar(){
            println("this is companion object")
        }
    }
}

fun main(args: Array<String>) {

    A.bar() //this is companion object
}

伴生对象可以访问类中所有的private成员,包括private构造方法,它是实现工厂模式的理想选择。


fun getFaceBookName(accountId: Int): String =
    when (accountId) {
        1 -> "jk"
        2 -> "cl"
        3 -> "wk"
        4 -> "er"
        else -> {
            "no name"
        }
    }

/**
 * 定义一个拥有多个构造方法的类
 */
class User {
    val nickname: String

    //从构造方法
    constructor(email: String) {
        nickname = email.substringBefore('@')
    }

    //从构造方法
    constructor(facebookAccountId: Int) {
        nickname =getFaceBookName(facebookAccountId)
    }


}

表示相同逻辑发另一种方法,就是使用工厂方法来创建实例,而不是通过多个构造方法。
使用工厂方法来代替构造方法。

class User private constructor(val nickName:String){    //主构造方法私有化
    companion object {
        fun newSubscribingUser(emil:String)=User(emil.substringBefore('@'))
        fun newFaceBookUser(accountId: Int)=User(getFaceBookName(accountId))
    }


}

fun main(args: Array<String>) {

    val subscribingUser = User.newSubscribingUser("jk@git.com")
    val faceBookUser = User.newFaceBookUser(3)
    println("subscribingUser:${subscribingUser.nickName}")//subscribingUser:jk
    println("faceBookUser:${faceBookUser.nickName}")//faceBookUser:wk
}

工厂方法极其有用。它们可以根据它们用途命名,也能返回声明这个方法的类的子类,还能避免不需要的时候创建新的对象。但是如果需要扩展,使用多个构造方法会是更好的选择,因为伴生对象成员在子类中不能被重写。

普通对象使用伴生对象

伴生对象是一个声明在类中的普通对象。它可以有名字,实现一个接口或者扩展函数或属性。

/**
 *嵌套类实现Comparator
 */
data class Person(val name: String){

   companion object Loader{
       fun getName(name:String):Person=Person(name)

   }
}

fun main(args: Array<String>) {
    //通过嵌套类名调用方法
    val person = Person.Loader.getName("jack")
    println(person)//Person(name=jack)

  //直接通过类名调用方法
    val person1:Person= Person.getName("Nick")
    println(person1)//Person(name=Nick)
}

大多数情况下,通过包含伴生对象的类的名字来引用伴生对象,所以不必关心它的名字,但是如果需要也可以指明。如果省略了伴生对象名字,默认的名字的将会分配为Companion。

伴生对象中实现接口

与其它对象声明一样,伴生对象也可以实现接口。可以直接将包含它的类的名字当作实现该接口的对象实例来使用。

interface nameFactory<T>{
    fun getName(name:String):T
}

 class Person(val name: String){
     //实现接口的伴生对象
    companion object : nameFactory<Person> {
        override fun getName(name: String): Person =Person(name)

    }
}

kotlin类的伴生对象会同样被编译成常规对象:类中的一个引用了它的实例的静态字段。如果伴生对象没有命名,在Java代码中它可以通过ComPanion引用。如果伴生对象有名字,这个名字取代ComPanion。

伴生对象扩展

伴生对象也可以定义扩展函数。
如果类C有一个伴生对象,并且在C.ComPanion上定义了一个扩展函数func,直接C.func调用即可。

/**
 * 为伴生对象定义一个扩展函数
 */
 class Person(val firstName: String,val lastName: String){
     //声明一个空的伴生对象
   companion object {
       
   }
     
}

//伴生对象的扩展函数
fun Person.Companion.getName(firstName: String,lastName: String):Person=Person("jack","nick")

//调用
fun main(args: Array<String>) {

    val person = Person.getName("jack","nick")
    println("${person.firstName} and ${person.lastName}")
    //this is jack and this is nick
}

如同之前扩展函数一样,看起来像是一个成员,但实际上并不是,请注意,为了类的定义扩展,必须在其中声明一个伴生对象,即使是空的。

对象表达式:改变写法的匿名内部类

object关键字不仅仅能用来声明单例对象,还能用来声明匿名对象。匿名对象替代Java中的匿名内部类。

  val window = JLabel()
    window.addMouseListener(
            //声明一个继承MouseAdapter的匿名对象
            object :MouseAdapter(){
                //重写MouseAdapter方法
                override fun mouseClicked(e: MouseEvent?) {
                    super.mouseClicked(e)
                }

                override fun mouseEntered(e: MouseEvent?) {
                    super.mouseEntered(e)
                }
            }
    )

除了去掉对象名字外,语法与对象声明是与java相同的。对象表达式声明一个类并创建了该类的一个实例,但是并没有给这个类或是实例分配一个名字。通常它们也并不需要名字,如果需要,可以分配名字给对象,存储到一个变量中。

    //分配给对象名字并存储
    val listener = object : MouseAdapter() {
        //重写MouseAdapter方法
        override fun mouseClicked(e: MouseEvent?) {
            super.mouseClicked(e)
        }

        override fun mouseEntered(e: MouseEvent?) {
            super.mouseEntered(e)
        }
    }
    window.addMouseListener(
            listener
    )

与Java匿名内部类只能扩展一个类或实现一个接口不同,kotlin的匿名对象可以实现多个接口或者不实现接口。

注:与对象声明不同,匿名对象不是单例的。每次对象表达式执行都会产生一个新的实例。

kotlin与java的匿名类一样,在对象表达式中的代码可以访问创建它的函数中的变量。不同的是,访问并没有限制在final变量。甚至可以在对象表达式中修改变量的值。

    val window = JLabel()
    //声明局部变量
    var clickCount=0
    window.addMouseListener(
           object :MouseAdapter(){
                //重写MouseAdapter方法
                override fun mouseClicked(e: MouseEvent?) {
                    //对象表达式中无需final,直接更新变量值
                   clickCount++
                }

                override fun mouseEntered(e: MouseEvent?) {
                    
                }
            }
    )

对象表达式在需要的匿名对象中重写多个方法是最有用的。如果只需要实现一个单方法的接口,可以用lambda并依靠kotlin的SAM转换。kotlin的lambda函数式编程极其趣味,下篇纪录。

上一篇下一篇

猜你喜欢

热点阅读