第4章类、对象和接口

本文详细介绍了Kotlin中的类、对象和接口的使用,包括接口的默认实现、抽象类、访问修饰符、内部类、密封类,以及类的构造方法、初始化语句块、数据类和类委托。同时讲解了对象声明、伴生对象的工厂方法和静态成员的替代,以及对象表达式的使用,展示了Kotlin在面向对象编程上的灵活性和实用性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第4章类、对象和接口.png

4.1、定义类继承结构

Kotlin中的接口

  • Kotlin的接口与Java8相似:它们可以包含抽象方法的定义以及非抽象方法的实现,但它们不能包含任何状态

  • 使用interface关键字而不是class来声明一个Kotlin的接口

  • 声明一个简单的接口(注意:这里的override修饰符是强制要求显式写出来

    interface Clickable {
        fun click()
    }
    
  • 接口实现

     class Button:Clickable {
         override fun click() {
             println("i was clicked")
         }
     }
    
  • 测试

    fun main(args: Array<String>) {
        val btn=Button()
        btn.click()
    }
    
  • Kotlin在类名后面使用冒号来代替了Java中的extends和implements关键字

  • 和java一样,一个类可以实现多个接口,但是只能继承一个类

  • 与Java中的@Override注解类似,override修饰符用来标注被重写的父类或者接口的方法和属性。在Kotlin中使用override修饰符是强制要求的

  • 接口的方法可以有一个默认实现

    • 与Java8不同,Java8中需要你在这样的实现上标注default关键字,对于这样的方法,Kotlin没有特殊的注解

    • 只需要提供一个方法体,也就是按照正常的方法写就可以

    • 例子

      • 给Clickable接口添加一个带默认实现的方法

         interface Clickable {
             fun click()
             //接口默认实现方法,和正常的实现方法写法一样
             fun showOff()= println("i'am clickable!")
         }
        
         class Button:Clickable {
             override fun click() {
                 println("i was clicked")
             }
         
              //该showOff方法可以重写,也可以不重写
              override fun showOff() {
                super.showOff()
           }
         }
        
    • 假如有两个接口都有showOff的方法实现,Button类继承了这两个类,那会怎样

      • 例子

         interface Clickable {
             fun click()
             fun showOff()= println("i'am clickable!")
         }
        
         interface Focusable {
             fun setFocus(b:Boolean)=println("I ${if(b) "got" else "lost"} focus.")
             fun showOff()=println("I'm focusable!")
         }
        
         class Button:Clickable,Focusable {
             override fun click() {
             println("i was clicked")
             }
         }
        
         fun main(args: Array<String>) {
             val btn=Button()
             btn.click()
         }
        
        • IDE报红

          • Kotlin: Class ‘Button’ must override public open fun showOff(): Unit defined in 四.类对象和接口.Clickable because it inherits multiple interface methods of it

            image.png

        • Kotlin编译器强制要求你提供自己的实现,使用尖括号加上父类的名字的super表明了你想要调用哪一个父类的方法

           class Button:Clickable,Focusable {
             override fun click() {
               println("i was clicked")
             }
           
             override fun showOff() {
               //使用尖括号加上父类的名字的super表明了你想要调用哪一个父类的方法
               super<Clickable>.showOff()
               super<Focusable>.showOff()
             }
           }
          

2、Kotlin中的抽象类

  • 可以将一个类声明为abstract,这种类不能被实例化,一个抽象类通常包含一些没有实现并且必须在子类重写的抽象成员

  • 抽象成员始终是open的,不需要显式的使用open修饰符,抽象类中的非抽象函数并不是默认open的,但是可以标注为open的

  • 示例

     abstract class Animated {
         abstract fun animate()
         //抽象类中的非抽象函数并不是默认open的,但是可以标注为open的
         open fun stopAnimating(){
         }
     
         fun animateTwice(){
         }
     }
    

3、类中的访问修饰符:默认为final

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

    • Java的类和方法默认是open的,而Kotlin中默认都是final的

    • 如果你想允许创建一个类的子类,需要使用open修饰符来标示这个类。此外,需要给每一个可以被重写的属性或方法添加open修饰符

    • 例子

      • 声明一个带一个open方法的RichButton类

        class RichButton:Clickable {
         //这个函数是final的,不能在子类中重写它
         fun disable(){}
         //这个函数是open的,可以再子类中重写它
         open fun animate(){}
         //重写了一个基类或者接口中的成员或者函数,重写了的成员同样默认是open的
         //如果想要防止子类重写你的实现,可以显式的将重写的成员标注为final
         override fun click() {
         }
        }
        
      • 禁止click函数被重写

         final override fun click() {
         }
        
  • 访问修饰符的意义

    • 修饰符的意义

      image.png

5、可见性修饰符:默认为public

  • 可见性修饰符帮助控制对代码库中声明的访问。通过限制类中实现细节的可见性,可以确保在修改它们时避免破坏依赖这个类的代码的风险

  • 与Java类似,同样可以使用public、protected和private修饰符。但是默认的可见性是不一样的:如果省略了修饰符,声明是public

  • java中的默认可见性-包私有,在Kotlin中并没有使用。Kotlin只把包作为在命名空间里组织代码的一种方式使用,并没有将其用作可见性控制

  • 作为替代方案,Kotlin提供了一个新的修饰符:internal,表示“只在模块内部可见”。一个模块就是一组一起编译的Kotlin文件。这有可能是一个IntelliJ IDEA模块,一个Eclipse项目、一个Moven或者Grade了项目或者一组使用调用Ant任务编译的文件

  • Kotlin允许在顶层声明中使用private可见性,包括类、函数和属性。这些声明就会只在声明它们的文件中可见,这就是另外一种隐藏子系统实现细节的非常有用的方式

  • Kotlin的可见性修饰符

    image.png

  • 例子

    image.png

    • Kotlin禁止从public函数giveSpeech去引用低可见的类型TalktiveButton(这个例子中是internal)
    • 一个通用的规则是:类的基础类型和类型参数列表中用到的所有类,或者函数的签名都有与这个类或者函数本身相同的可见性。
    • 这个规则可以确保你在需要调用函数或者继承一个类时能够始终访问到所有的类型。
    • 要解决上面例子中的问题,既可以把函数改为internal的,也可以把类改成public的
    • 注意,protected修饰符在Kotlin和Java中不同的行为,在Java中可以从同一个包中访问一个protected的成员,但是Kotlin不允许这样做
    • Kotlin中的可见性规则非常简单,protected成员只在类和它的子类中可见
    • 同时还要注意的是,类的扩展函数不能访问他的private和protected成员

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

  • Kotlin可以在另一个类中声明一个类。这样做在封装一个辅助类或者把一些代码放到靠近它被使用的地方时非常有用

  • Kotlin的嵌套类不能访问外部类的实例,除非你特别的做出了要求

  • 例子

    • 定义一个View元素,它的状态是可以序列化的。想要序列化一个视图可能并不容易,但是可以把所有需要的数据复制到另一个辅助类中

    • 声明一个State接口去实现Serializable

    • View接口声明了可以用来保存视图状态的getCurrentState和restoreState方法

      interface State :Serializable {
      }
      
      interface View{
       fun getCurrentState():State
       fun restoreState(state:State){}
      }
      
  • Java中的Button类的实现过程

     public class Button2 implements View{
    
      @Override
      public State getCurrentState() {
          return new ButtonState();
      }
    
      @Override
      public void restoreState(State state) {
          //。。。。
      }
    
      //内部类
      public class ButtonState implements State{
          //。。。。
      }
    }
    
    • java代码会有什么问题,得到一个java.io.NotSerializableException:Button异常,因为ButtonState类隐式的存储了它的外部Button类的引用,Button不是可序列化的,所以会报错,修复这个问题,需要声明ButtonState类是static。将一个嵌套类声明为static会从这个类中删除包围它的类的隐式引用

    • Kotlin实现过程

       class Button3:View {
           override fun getCurrentState()=ButtonState()
           override fun restoreState(state: State) {
           //具体代码
           }
       
           //这个类与Java中的静态嵌套类类似
           class ButtonState:State{
           
           }
       }
      
    • Kotlin中没有显式修饰符的嵌套类与Java中的static嵌套类是一样的

    • 要把它变成一个内部类来持有一个外部类的引用的话,需要使用inner修饰符

    • java和Kotlin行为不同之处

      嵌套类和内部类在Java和Kotlin中的对应关系.png
      嵌套类不持有外部类引用,而内部类持有.png

    • 在Kotlin中引用外部类实例的语法与Java不同,需要使用 this@Outer (Outer类为外部类类名) 从Inner类去访问Outer类

      • 例子

         class Button3:View {
         
           override fun getCurrentState()=ButtonState()
         
           override fun restoreState(state: State) {
             //具体代码
           }
         
           //这个类与Java中的静态嵌套类类似
           class ButtonState:State{
           }
         
           //引用外部类的实例
           //类inner修饰符修饰
           inner class Inner{
            //使用this@“外部类名称”来获取
            fun getOuterReference():Button3=this@Button3
           }
         }
        

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

  • 2.3.5节中关于继承结构表达式的例子,父类Expr有两个子类:表示数字的Num,以及表示两个表达式之和的Sum,When表达式中处理所有可能的子类固然方便,但是必须提供一个else分支来处理没有任其他分支能支配的情况:

     //计算两个数的和
     
     interface Expr
     
     class Num(val value: Int) : Expr
     class Sum(val left: Expr, val right: Expr) : Expr
     
      fun eval3(e:Expr):Int=
         when(e){
             is Num -> e.value
             is Sum -> eval3(e.left)+ eval3(e.right)
             else -> throw IllegalAccessException("unKnow expression")
         }
    

    when结构来执行表达式的时候,Kotlin编译器会强制检查默认选项,在这个例子中,不能返回一个有意义的值,所以直接抛出一个异常

  • 总是不得不添加一个默认分支很不方便,重要的是,假如添加了一个新的子类,编译器不能发现有地方改变了。如果忘记添加新分支,就会选择默认的选项,这有可能导致潜在的bug

  • Kotlin为这个问题提供了一个解决方案:sealed类,为父类添加一个sealed修饰符,对可能创建的子类做出严格的限制,所有的直接子类必须嵌套在父类中。

    • 例子

      • image.png

      • //将基类标记位密封类
        sealed class Expr {
           class Num(val value: Int) : Expr()
           class Sum(val left: Expr, val right: Expr) : Expr()
        }
        
        fun eval(e: Expr): Int =
            //when表达式涵盖了所有可能的情况,所以不再需要else分支
            when (e) {
                is Expr.Num -> e.value
                is Expr.Sum -> eval(e.left) + eval(e.right)
            }
        
      • 如果在when表达式中处理所有sealed类的子类,就不需要提供默认分支
        意,sealed修饰符隐含的这个类是一个open类,不需要显式添加open修饰符,密封类的行为如图

        密封类不能在类外部拥有子类.png

      • 当你在when中使用sealed类并且添加一个新的子类的时候,有返回值的when表达式会导致编译失败,它会告诉你哪里的代码必须要修改

      • 在这种情况下,Expr类有一个只能在类内部调用的private构造方法

      • 也不能声明一个sealed接口,如果这样做,Kotlin编译器不能保证任何人都不能在Java代码中实现这个接口

      • 注意

        1. Kotlin1.0中sealed功能是相当严格的,例如所有子类必须是嵌套的,并且子类不能创建为data类
        2. Kotlin1.1解除了这些限制并允许在同一文件的任何位置定义sealed类的子类

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

Java中一个类可以声明一个或多个构造方法,Kotlin也类似,只是做了点修改:区分了主构造方法和从构造方法

  • 主构造方法

    • 通常是主要而简洁的初始化类的方法,并且在类体外外部声明
  • 从构造方法

    • 在类体内部声明

1、初始化类:主构造方法和初始化语句块

  • 声明一个简单的类

    •  class User(val nickname:String) 
      
    • 为什么这个类没有花括号而只包含了声明在括号中。

    • 这段被括号围起来的语句块叫做主构造方法

    • 主要有两个目的:表明构造方法的参数,以及定义使用这些参数初始化的属性

    • 完成同样事情的最明确的代码是什么样子的

      • class User2 constructor(_nickname:String) {//带一个参数的主构造方法
           val nickname:String
           init {//初始化语句块
             nickname=_nickname
           }
         }
        
      • constructor关键字用来开始一个主构造方法或从构造方法的声明,如果主构造方法没有注解或可见性修饰符,同样可以去掉constructor关键字

      • class User3(_nickname:String) {//带一个参数的主构造方法
          val nickname=_nickname//用参数来初始化属性
         }
        
      • init关键字用来引入一个初始化语句块,这种语句块包含了在类被创建时执行的代码,并会与主构造方法一起使用

      • 前面两个例子在类体中使用val关键字声明了属性,如果属性用相应的构造方法参数来初始化,代码可以通过把val关键字加在参数前的方式来进行简化,这样可以替换类中的属性定义:

         //“val”意味着相应的属性会用构造方法的参数来初始化
         class User(val nickName:String)
        
      • 可以像函数参数一样为构造方法参数声明一个默认值:

         //为构造方法参数提供一个默认值
         class User4(val nickname:String ="默认",val isSubscribed:Boolean = false)
        
      • 创建实例:只需要直接调用构造方法,不需要new关键字

        image.png

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

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

        image.png

        image.png

      • 如果一个类没有声明任何构造方法,会生成一个默认构造方法

        • //将会生成一个不带任何参数的默认构造方法
          open class Button
          
        • 如果继承了Button类并且没有提供任何的构造方法,必须显式的调用父类的构造方法,即使它没有任何的参数

          image.png

          image.png

          • 这就是为什么在父类名称后面还需要一个空的括号
          • 注意与接口的区别:接口没有构造方法,所以在实现一个接口的时候,不需要在父类类型列表中它的名称后面再加上括号
      • 如果想要确保类不被其他代码实例化,必须把构造方法标记位private

        image.png

2、构造方法:用不同的方式来初始化类

  • 使用场景

    • 当你需要扩展一个框架类来提供多个构造方法,以便于通过不同的方式来初始化类的时候

      • 示例
        image.png

        这个类没有声明一个主构造方法(因为在类头部的类名后面并没有括号),但是它声明了两个从构造方法。

      • 从构造方法使用constructor关键字引出,只要需要,可以声明任意多个从构造方法

      • 扩展这个类,可以声明同样的构造方法

        • 示例
          image.png

        • 这里定义了两个构造方法,都使用super()关键字调用了对应的父类构造方法,如图,箭头显示了构造方法委托的目标

          使用不同的父类构造方法.png

          • 也可以使用this关键字,从一个构造方法中调用自己类的另一个构造方法

          委托给同一个类的构造方法.png

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

3、实现在接口中声明的属性

  • 接口可以包含抽象属性声明

    • 例子

      image.png

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

      • 例子

        • PrivateUser,只填写了昵称的用户

        • SubscribingUser,被迫提供email进行注册的用户

        • FacebookUser,轻率的共享了Facebook账号的用户

          image.png

          • 直接在主构造方法中声明了一个属性
          • 这个属性实现了来自User6的抽象属性
          • 所以将其标记位override

          image.png

          • nickname属性通过一个自定义getter实现,这个属性没有一个支持字段来存储它的值,它只有一个getter在每次调用时从email中得到昵称

          image.png

          • 在初始化时将nickname属性与值关联
          • 使用被认为可以通过账号ID返回Facebook用户名称的getFacebookName函数(假设这个函数是在别的地方定义的)
  • 接口还可以包含具有getter和setter的属性

    • 只要它们没有引用一个支持字段(支持字段需要在接口中存储状态,而这是不允许的)

    • 例子

      image.png

      • 这个接口包含属性email,同时nickname属性有一个自定义的getter
      • 第一个属性必须在子类重写,第二个是可以被继承的

4、通过getter和setter访问支持字段field

  • 假设,在任何对存储在属性中的数据进行修改时输出日志

    • 声明了一个可变属性,并且在每次setter访问时执行额外的代码

    • 例子

      image.png

      image.png

      image.png

      • 其实就是重写了下set方法
      • 在setter的函数体中,使用了特殊的标识符field来访问支持字段的值,在getter中,只能读取值
    • 有支持字段的属性和没有的有什么区别?

      • 访问属性的方式不依赖于它是否含有支持字段
      • 如果显式的引用或者使用默认的访问器实现,编译器会为属性生成支持字段
      • 如果提供了一个自定义的访问器实现并且没有使用field
        (如果属性是val类型,就是getter;如果是可变属性,则是两个访问器),
        支持字段将不会被呈现出来
    • 有时候不需要修改访问器的默认实现,但是需要修改它的可见性

5、修改访问器的可见性

  • 访问器的可见性默认与属性的可见性相同

  • 如果需要可通过在get和set关键字前放置可见性修饰符的方式来修改它

  • 例子

    image.png

    image.png

    外界访问会报错

属性更多话题

  • 在非空属性上使用的lateinit修饰符表明这个属性会将初始化推迟到构造方法被调用过后,这是一些框架的常用用法
  • 惰性初始化属性,作为更通用的委托属性的一部分,将会在第7章中涵盖到
  • const修饰符使得使用注解更加方便,并且允许使用基本数据类型或者String的属性作为注解参数

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

1、通用对象方法

  • Kotlin类也有许多重写的方法toString、equals和hashCode

  • 例子

    • 一个简单的用来存储客户名字和邮编的Client类
     class Client(val name:String,val postalCode:Int) {
     }
    
    • 字符串表示:toString()

      • 和java类似

      • 重写toString方法

         class Client(val name:String,val postalCode:Int) {
           override fun toString()="Client(name=$name,postalCode=$postalCode)"
         }
        
      • 对象相等性:equals()

        • image.png

        • image.png

        • 在Kotlin中,==检查对象是否相等,而不是比较引用,这里编译的时候会调用“equals”方法

        • 对象并不相等,这意味着需要重写equals

          image.png

          • 运行结果

            image.png

          • 重写equals方法后,==比较返回值就为true了

        • Hash容器:hashCode()

          • hashCode方法通常与equals方法一起被重写

          • 例子

            image.png

            image.png

            • 返回false原因就是Client类缺少了hashCode方法,因为它违反了通用的hashCode契约:如果两个对象相等,它们必须有相同的hash值

            • processed set是一个HashSet,在HashSet中值是以一种优化过的方式来比较的:首先比较它们的hash值,然后才会比较它们真正的值

            • 要返回true,需要重写hashCode方法

              image.png

              image.png

2、数据类型:自动生成的通用方法的实现

  • 如果想要你的类是一个方便的数据容器,需要重写这些方法:toString、equals和hashCode

  • 通常来讲,idea可以自动生成

  • kotlin中,如果为你的类添加data修饰符,必要的方法将会自动生成

  • 例子

    image.png

    image.png

    image.png

  • 用data修饰的类,重写了所有标准Java方法的类

    • equals用来比较实例
    • hashCode用来作为例如HashMap这种哈希容器的键
    • toString用来为类生成按声明顺序排列的所有字段的字符串表达形式
  • 注意

    • equals和hashCode方法会将所有在 主构造方法 中声明的属性纳入考虑
    • 生成的equals方法会检测所有的属性值是否相等
    • hashCode方法会返回一个根据所有属性生成的哈希值
    • 请注意没有在主构造方法中声明的属性将不会加入到相等性检查和哈希值计算中去

3、数据类型和不可变性:copy()方法

  • 虽然数据类的属性并没有要求是val,同样可以是var,但还是强烈推荐只使用只读属性,让数据的实例不可变

  • 如果想使用这样的实例作为HashMap或者类似容器的键,这会是必需的要求,因为如果不这样,被用作键的对象在加入了容器后被修改了,容器可能会进入一种无效的状态

  • 为了让使用不可变对象的数据类型变得容易,Kotlin编译器为它们多生了一个方法:一个允许copy类的实例方法,并在copy的同时修改某些属性的值

  • 创建副本通常是修改实例的好选择:副本有着单独的生命周期而且不会影响代码中引用原始实例的位置

  • 例子

    image.png

    • 这个是手动copy的方法
    • 假如是用data修饰符修饰的类,Kotlin编译器自动生成copy方法,可以直接使用

3、类委托:使用“by”关键字

  • 避免IDE生成样板代码:类委托

  • 你常常需要向其他类添加一些行为,即使它并没有设计为可扩展的,一个常用的方式以装饰器闻名

  • 这种模式的本质就是创建一个新类,实现与原始类一样的接口并将原来的类的实例作为一个字段保存。与原始类拥有同样行为的方法不会被修改,只需要直接转发到原始类的实例

  • 这种方式的一个缺点是需要相当多的样板代码

  • 例子

    • 实现一个简单的如Collection的接口的装饰器

      image.png

      image.png

    • 使用装饰器模式,需要重写很多的样板代码

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

    • 例子

      image.png

      • 类中所有的方法实现消失了,编译器会生成它们,并且实现与DelegatingCollection的例子是相似的

      • 例子

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

          image.png

          image.png

          image.png

        • 这个集合貌似和set集合似的,不可重复的

4.4、“object”关键字:将声明一个类与创建一个实例结合起来

object关键字用来定义一个类并同时创建一个实例(换句话说就是一个对象)

  • 使用场景

    • 1、对象声明是定义单例的一种方式
    • 2、伴生对象可以持有工厂方法和其他与这个类相关,但在调用时并不依赖类实例的方法。他们的成员可以通过类名来访问
    • 3、对象表达式用来替代java的匿名内部类

1、对象声明

  • 1、对象声明通过object关键字引入,将类声明与类的单一实例结合声明到了一起

  • 2、对象声明不允许有构造方法(包括主构造方法和从构造方法)

  • 3、对象声明在定义的时候就立即创建了实例

  • 4、对象声明使用对象名.字符的方式来调用方法和访问属性

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

  • 例子1

    • 使用一个对象声明来表示一个组织的工资单

      image.png

      image.png

  • 例子2

    • Comparator的实现接收两个对象并返回一个整数来表示哪个对象更大。比较器通常来说都不存储任何数据,所以通常只需要一个单独的Comparator实例来以特定的方式比较对象

    • 这是一个非常完美的对象声明使用场景

    • 忽略大小写比较文件路径的比较器

      image.png

      image.png
      image.png

      • 这里使用了sortedWith函数,它返回一个根据特定的比较器排序过的列表
  • 例子3

    • 可以在类中声明对象,这样的对象同样只有一个单一的实例,它们在每个容器类的实例中并不具有不同的实例

    • 在类中放置一个用来比较特定对象的比较器

      image.png

      image.png

      image.png

  • 在Java中使用Kotlin对象

    • Kotlin中的对象声明被编译成了通过静态字段来持有它的单一实例的类,这个字段名字始终都是INSTANCE

    • 如果要从java代码使用Kotlin对象,可以通过访问静态的INSTANCE字段

    • 例子

      image.png

2、伴生对象:工厂方法和静态成员的地盘

  • Kotlin中的类不能拥有静态成员,Java的static关键字并不是Kotlin语言的一部分

  • 作为替代,Kotlin依赖包级别函数和对象声明

    • 包级别函数(在大多数情形下能够替代Java的静态方法)

      • 在大多数情况下,推荐使用顶层函数,但是顶层函数不能访问类的private成员
        image.png
    • 对象声明(在其他情况下替代Java的静态方法,同时还包括静态字段)

  • 在类中定义的对象之一可使用一个特殊的关键字来标记:companion。这样可以直接通过类名称来访问这个对象的方法和属性

    image.png

    • 例子

      image.png

      image.png

      image.png

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

    • 有一个声明了两个构造方法的例子,将其改成使用在伴生对象中声明的工厂方法

    • 例子

      • 多个从构造方法的类

        image.png

      • 改为用工厂方法来替代构造方法

        image.png

      • 通过类名来调用伴生对象的方法

        image.png

        image.png

    • 工厂方法是非常有用的,它们可以根据它们的用途来命名,此外,工厂方法能够返回声明这个方法的类的子类,就像例子中的SubscribingUser和FacebookUser类。你还可以在不需要的时候避免创建新的对象。例如,你可以确保每一个email都与一个唯一的User实例对应,并且如果email在缓存中已经存在,那么在调用工厂方法时就返回这个存在的实例而不是创建一个新的。但是如果你需要扩展这样的类,使用多个构造方法也许是一个更好的方案,因为伴生对象成员在子类中不能被重写

3、作为普通对象使用的伴生对象

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

  • 例子

    • 假设你正工作在一个公司工资单的网站服务上,并且需要在对象和JSON之间序列化和反序列化,可以将序列化的逻辑放在伴生对象中

      image.png

      image.png

  • 在大多数情况下,通过包含伴生对象的类的名字来引用伴生对象,所以不必关心它的名字

  • 如果省略了伴生对象的名字,默认的名字将会分配为Companion

4、在伴生对象中实现接口

  • 伴生对象也可以实现接口,可以直接将包含它的类的名字当做实现了该接口对象实例来使用

  • 例子

    • 假定你的系统中有许多对象,包括Person,你想要通过一个通用的方式来创建所有类型的对象。假设你有一个JSONFactory接口可以从JSON反序列化对象,并且你的系统中的所有对象都通过这个工厂来创建。你可以为你的Person类提供一个这种接口的实现

      image.png

    • 这时,如果你有一个函数使用抽象方法来加载试题,可以传给它Person3对象

      image.png

  • Kotlin的伴生对象和静态成员

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

      image.png

    • 但是你也许要和这样的Java代码一起工作,它需要类中的成员是静态的,可以在对应的成员上使用@JVMStatic注解来达到这个目的。如果想声明一个static字段,可以再在一个顶层属性或者声明在object的属性上使用@JvmField注解

    • Kotlin可以访问在Java类中声明的静态方法和字段,使用与Java相同的语法

  • 伴生对象扩展

    • 扩展函数允许你定义 可以通过代码库中 其他地方定义的类实例调用 的方法

    • 如果需要定义可以通过类自身调用的方法,就像伴生对象方法或者是Java静态方法该怎么办呢?如果类有一个伴生对象,可以通过在其上定义扩展函数来做到这一点

    • 具体来说,如果类C有一个伴生对象,并且在C.Companion上定义了一个扩展函数func,可以通过C.func()来调用它

    • 为了能够为你的类定义扩展,必须在其中声明一个伴生对象,即使是一个空的

    • 例子

      • 假设你希望的Person类有一个清晰的关注点分离,这个类本身会是核心业务逻辑模块的一部分,但是你并不想将这个模块与任何特定的数据格式耦合起来

      • 正因为如此,反序列化函数需要定义在模块中用来负责客户端/服务端通信,可以使用扩展函数来做到这一点

      • 注意,该怎样使用默认名字(Companion)来引用没有显式地定义名字的伴生对象

        image.png

      • 调用fromJSON好像它是一个伴生对象定义的方法一样,但是它实际上是作为扩展函数在外部定义的,扩展函数看起来是一个成员,但实际并不是

5、对象表达式

  • 1、object关键字还可以用来声明匿名对象,匿名对象替代了Java中匿名内部类的用法

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

  • 3、与对象声明不同,匿名对象不是单例的,每次对象表达式被执行都会创建一个新的对象的实例

  • 4、在对象表达式中的代码可以访问创建它的函数中的变量

  • 5、对象表达是可以修改变量的值

  • 例子

    • 将一个典型的Java匿名内部类用法-事件监听器-转换成Kotlin

      image.png

    • 除了去掉对象的名字外,语法与对象声明相同,对象表达式声明了一个类并创建了该类的一个实例,但是并没有给这个类或是实例分配一个名字

    • 通常来说,它们都是不需要名字的,因为你会将这个对象用作一个函数调用的参数

    • 如果你需要给对象分配一个名字,可以将其存储到一个变量中

      image.png

  • 与Java不同,访问并没有被限制在final变量,还可以在对象表达式中修改变量的值

    • 例子

      • 使用监听器对窗口点击计数

      image.png

    • 注意

      • 对象表达式在需要在匿名对象中重写多个方法时是最有用的
      • 如果只需要实现一个单方法的接口(就像Runnable),可以将你的实现写作函数字面值(lambda)并依靠Kotlin的SAM转换(把函数字面值转换成单抽象接口的实现)

4.5、小结

Kotlin的接口与Java的类似,但是可以包含默认实现(Java从第8版才开始支持)和属性

所有的声明默认都会final和public的

要想声明不是final的,将其标记为open

internal声明在同一模块中可见

嵌套类默认不是内部类。使用inner关键字来存储外部类的引用

sealed类的子类只能嵌套在自身的声明中(Kotlin1.1允许将子类放置在同一文件的任意地方)

初始化语句块和从构造方法为初始化实例提供了灵活性

数据类提供了编译器生成的equals、hashCode、toString、copy和其他方法

类委托帮助避免在代码中出现许多相似的委托方法

对象声明是Kotlin中定义单例的方法

伴生对象(与包级别函数和属性一起)替代了Java静态方法和字段定义

伴生对象和其他对象一样,可以实现接口,也可以拥有扩展函数和属性

对象表达式是Kotlin中针对Java匿名内部类的替代品,并增加了诸如实现多个接口的能力和修改在创建对象的作用域中定义变量的能力等功能

附件:

第4章类、对象和接口.svg

Kotlin学习之旅开始啦

第1章Kotlin:定义与目的

第2章Kotlin基础

第3章函数的定义和调用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值