Kotlin(五)之面向对象(一)

类和对象

  1. 定义类的语法

    [修饰符] class 类名 [constructor 主构造器]{
      	0...n 个次构造器
        0...n 个属性
        0...n 个方法  
    }
    
    修饰符: public internal private protected 只能出现一个
    final open abstract  
    
  2. 修饰符

    • 类声明默认是public final的
    • open是final的反义词,用于修饰类、方法或属性,表示类可派生子类,方法或属性可被重写

包和导入

  1. 一旦在Kotlin程序中使用了package关键字,就意味着该源程序中定义的所有类、函数都属于这个包。位于包中的每个类的完整类名都应该是包名和类名的组合。包中的类在文件系统中也必须有与包名相同的目录结构(这点与java相同),Java 和 Kotlin 的 package 语句指定源文件编译之后的路径,与源文件的路径无关,一般情况下我们最好把 package 语句与源文件路径写成一致

  2. 如果没有指明包,该文件的内容属于无名字的默认包

  3. Kotlin中的import相当于java中的import和import static(静态导入)的合并。不仅可以导入类,还可以导入

    • 顶层函数以及属性
    • 在对象声明中声明的函数和属性
    • 枚举常量
  4. 如果在同一源文件中导入不同包中的类,在java中很难处理,必须有一个类使用全限定名(即放弃导入包)。而Kotlin使用更简单的处理方式。即import语句后面增加as关键字,指定导入类的别名

    
    import java.util.*
    import java.sql.Date as SqlDate
    
    var d = Date()
    var s = SqlDate(System.currentTimeMillis())
    println(d)
    println(s)
    
    
  5. Kotlin的默认导入

    kotlin.*
    kotlin.annotation.*
    kotlin.collections.*
    kotlin.comparisons.*
    kotlin.io.*
    kotlin.ranges.*
    kotlin.sequence.*
    kotlin.text.*
    
  6. 对于JVM平台,还会自动导入

    java.lang.*
    kotlin.jvm.*
    
  7. 对于JavaScript平台,额外导入

    kotlin.js.*
    

访问控制

  1. Kotlin提供四种访问控制符

    • private: 与java的private类似,private成员只能在该类的内部或文件的内部被访问,如果顶层声明是 private 的,它是声明它的文件所私有的
    • internal:internal成员可以在该类的内部或文件的内部或同一模块内访问
    • protected:protected成员可以在该类的内部或文件的内部或者子类中被访问
    • public:public成员可以在任意地方被访问
  2. Kotlin与java访问控制符的区别

    • Kotlin默认的访问控制符是public。java默认访问权限是包访问权限
    • Kotlin取消了protected的包访问权限
  3. Kotlin使用internal代替java的包访问权限控制符是很好的改进。java的包访问控制符有时不方便。比如在同一个项目组开发,同一个模块内不同类型的应用组件肯定放在不同的子包中。此时如果同一模块中的不同成员使用的是包访问控制符,那么他们之间就不能互相访问,只能改为使用public修饰符。

构造方法

  1. Kotlin通过构造器返回类的对象(不用使用new)

  2. 一个Kotlin类可以有0~1个主构造器,0~n个次构造器。主构造器是类头的一部分,如果主构造器没有任何注解或修饰符,则可以省略constructor关键字,如果构造函数有注解或可见性修饰符,constructor不可省略。如果没有显示的为非抽象类定义任何构造器,系统会自动提供一个无参主构造器(public修饰),一旦为一个类提供了构造器,系统将不再为该类提供构造器

    class User constructor(username: String) {
    
    }
    等价
    class User(username: String) {
    
    }
    
    
  3. 主构造器作为类头的一部分,只有形参,没有执行体(初始化代码可以放到初始化块中)。主构造器形参的作用

    • 初始化块(使用关键字init)可以使用主构造器定义的形参。
    • 在声明属性时可以使用主构造器定义的形参
    • 从以上两点可以看出,Kotlin的主构造器与Java的构造器不相同,它更像java初始化块的增强(java的初始化块不能传入参数)。
  4. 案例:从以下输出可以看出,当程序通过主构造器创建对象时,系统其实就是调用该类里定义的初始化块。如果一个类中有多个普通初始化块,则按照定义的顺序依次执行(一般情况下,会把多个初始化块合并为一个,可读性更强)。如果在Kotlin中想为对象的属性显示指定初始化值,则可以通过初始化块来指定

    fun main() {
        /**
         * 局部变量a>3
         * 初始化(1)块执行,jannal...
         * 初始化(2)块执行,jannal...
         * jannal
         */
        var h = HelloWorld("jannal", 1)
        println(h.name)
    }
    
    class HelloWorld(name: String, count: Int) {
        var name: String
        var count: Int
    
        init {
            var a = 6
            if (a > 3) {
                println("局部变量a>3")
            }
            println("初始化(1)块执行,${name}...")
        }
    
        init {
            println("初始化(2)块执行,${name}...")
        }
    
        init {
            this.name = name
            this.count = count
        }
    }
    
  5. Kotlin要求所有的次构造器都必须先调用主构造器(执行初始化块中的代码),即每个次构造器函数需要委托主构造函数。这种设计与java类似,java会让每个构造器(java的构造器相当于Kotlin中的次构造器)先调用初始化块(java的初始化块相当于Kotlin的主构造器)。Kotlin使用:this(参数)来委托另一个构造器,到底委托哪个构造器取决于传入的参数。

    class Man(name: String) {
        var name: String
        var age: Int
        var info: String? = null
    
        init {
            println("Man init")
            this.name = name
            this.age = 0
        }
    
        //次构造器委托主构造器,java中委托this(name),kotlin中使用:this(name)
        constructor(name: String, age: Int) : this(name) {
            this.age = age
        }
        //次构造器委托次构造器。系统会根据传入的参数推断出委托了哪个构造器
        constructor(name: String, age: Int, info: String) : this(name, age) {
            this.info = info
        }
    
    }
    
    fun main() {
        /**
         * Man init
         * jack,0,null
         */
        var man = Man("jack")
        println("${man.name},${man.age},${man.info}")
    
        /**
         * Man init
         * jack,18,null
         */
        man = Man("jack", 18)
        println("${man.name},${man.age},${man.info}")
        /**
         * Man init
         * jack,18,hello world!
         */
        man = Man("jack", 18, "hello world!")
        println("${man.name},${man.age},${man.info}")
    
    }
    
    
  6. Kotlin允许主构造器上声明属性。直接在参数之前使用var或者val即可声明属性(读写或者只读属性)。

    • 当程序调用这种方式声明的主构造器创建对象时,传给构造器的参数将会赋值给对象的属性
    • 如果主构造的所有参数都有默认值,程序能以构造参数的默认值来调用该构造器(即不需要为构造参数传入值),此时看上去像调用无参的构造器。
    class Customer(val name: String = "jannal") {
        var city: String = "天津"
    }
    fun main() {
        //天津,jack
        var cus = Customer("jack")
        println("${cus.city},${cus.name}")
        //天津,jannal
        cus = Customer()
        println("${cus.city},${cus.name}")
    
    }
    

类中的方法

  1. 分离类中的方法单独使用

    class Cat {
        fun run() {
            println("run...")
        }
    
        fun eat(food: String) {
            print("eat ")
        }
    }
    
    fun main(args: Array<String>) {
        //将Cat的run方法赋值给变量,获取类中方法的引用,需要在方法前加类的名字.
        var runFunction: (Cat) -> Unit = Cat::run
        val cat = Cat()
        //当程序将run方法独立成函数时,调用run方法的调用者(Cat对象)将作为第一个参数传入
        runFunction(cat)
    
        //将Car变量的类型赋值给et变量,没有指定,系统自动推断出et的类型是(Cat,String)->Unit
        var et = Cat::eat
        et(cat, "老鼠")
    
    }
    
  2. 方法调用中缀表示法:使用infix修饰,这样该方法就可以通过中缀表示法调用,就像此方法是双目运算符一样。需要指出的是,infix修饰的方法只能有一个参数(因为双目运算符后面只能有一个参数)

    class Cat {
        fun run() {
            println("run...")
        }
    
        fun eat(food: String) {
            println("eat ")
        }
    }
    
    
    class Calc(value: Int) {
        var value = value
        infix fun add(calc: Calc): Int {
            return this.value + calc.value
        }
    
    }
    
    fun main(args: Array<String>) {
    
        var left = Calc(3)
        var right = Calc(5)
        //普通方式调用,输出8
        println(left.add(right))
        //中缀表达式方式调用,输出8
        println(left add right)
    
    }
    

componetN

  1. Kotlin允许将一个对象的N个属性解构给多个变量,通过componentN方法,程序需要解构几个变量,就必须为该对象的类定义几个componentN方法,并且该方法需要使用operator修饰

    //将一个对象的N个属性解构给多个变量,这里是将user的component1和component2解构
    var (name, pass) = user
    
    class User constructor(username: String, password: String) {
        var username: String = username
        var password: String = password
        /**
         * componentN方法与解构
         */
        operator fun component1(): String {
            return this.username
        }
    
        operator fun component2(): String {
            return this.password
        }
    }
    
    fun main(args: Array<String>) {
        var user = User("jannal", "123456")
        //将一个对象的N个属性解构给多个变量,这里是将user的component1和component2解构
        var (name, pass) = user
        //使用_来占位,忽略相关的componentN返回值
        var (name2, _) = user
        var (_, pass2) = user
        //jannal
        println(name)
        //jannal
        println(name2)
        //123456
        println(pass)
        //123456
        println(pass2)
    }    
    
  2. 遍历Map的语法解析

        //遍历map
        val mapp = mutableMapOf<String, Any>("key" to 24, "name" to "zhangsan", "age" to 25)
        // 普通方式
        for (entry in mapp){
            println("${entry.key}:${entry.value}")
        }
        //因为Map.Entry提供operator修饰的componentN方法,所以程序可执行如下解构
        // var (key,value) = Map.Entry
        for ((key, value) in mapp) {
            println("$key:$value")
        }
    	  
    	  //对于具有多个参数的 lambda 表达式,可以使用_字符替换不使用的参数的名称
        mapp.mapValues { (_, value) -> "${value}!" }
    

数据类与多返回值

  1. Kotlin本身并不支持定义返回多个值的函数或方法。可以通过让Kotlin返回一个支持解构的对象来达到返回多个值的目的。为了简化解构的实现,Kotlin提供了专门用于封装数据的类(java中的POJO DTO VO等)——数据类

  2. 数据类需要满足的要求

    • data修饰,参数必须有var或者val
    • 主构造器至少需要有一个参数
    • 数据类不能用abstract、open 、sealed、inner修饰,也不能定义成内部类
    • kotlin1.1之前,数据类只能实现接口,之后数据类可继承其他类
  3. 定义数据类,系统会自动给数据类生成如下内容:

    • 生成equals()/hashcode()
    • 自动重写toString()
    • 为每个属性自动生成operator修饰的componentN()方法
    • 生成copy()方法,用于完成对象复制
  4. 案例

    //定义一个数据类
    data class Result(val status: Int, val message: String)
    
    fun status(): Result {
        return Result(200, "success")
    }
    
    //通过结构获取函数多返回值
    var (status, message) = status()
    
  5. 通过IDEA查看Tools->Kotlin->Show Kotlin Bytecode

    image-20191018112937128

    可以看到数据类反编译为java之后
    @Metadata(
       mv = {1, 1, 15},
       bv = {1, 0, 3},
       k = 1,
       d1 = {"\u0000 \n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\b\n\u0000\n\u0002\u0010\u000e\n\u0002\b\t\n\u0002\u0010\u000b\n\u0002\b\u0004\b\u0086\b\u0018\u00002\u00020\u0001B\u0015\u0012\u0006\u0010\u0002\u001a\u00020\u0003\u0012\u0006\u0010\u0004\u001a\u00020\u0005¢\u0006\u0002\u0010\u0006J\t\u0010\u000b\u001a\u00020\u0003HÆ\u0003J\t\u0010\f\u001a\u00020\u0005HÆ\u0003J\u001d\u0010\r\u001a\u00020\u00002\b\b\u0002\u0010\u0002\u001a\u00020\u00032\b\b\u0002\u0010\u0004\u001a\u00020\u0005HÆ\u0001J\u0013\u0010\u000e\u001a\u00020\u000f2\b\u0010\u0010\u001a\u0004\u0018\u00010\u0001HÖ\u0003J\t\u0010\u0011\u001a\u00020\u0003HÖ\u0001J\t\u0010\u0012\u001a\u00020\u0005HÖ\u0001R\u0011\u0010\u0004\u001a\u00020\u0005¢\u0006\b\n\u0000\u001a\u0004\b\u0007\u0010\bR\u0011\u0010\u0002\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\t\u0010\n¨\u0006\u0013"},
       d2 = {"Lcn/jannal/kotlin/Result;", "", "status", "", "message", "", "(ILjava/lang/String;)V", "getMessage", "()Ljava/lang/String;", "getStatus", "()I", "component1", "component2", "copy", "equals", "", "other", "hashCode", "toString", "kotlin-object"}
    )
    public final class Result {
       private final int status;
       @NotNull
       private final String message;
    
       public final int getStatus() {
          return this.status;
       }
    
       @NotNull
       public final String getMessage() {
          return this.message;
       }
    
       public Result(int status, @NotNull String message) {
          Intrinsics.checkParameterIsNotNull(message, "message");
          super();
          this.status = status;
          this.message = message;
       }
    
       public final int component1() {
          return this.status;
       }
    
       @NotNull
       public final String component2() {
          return this.message;
       }
    
       @NotNull
       public final Result copy(int status, @NotNull String message) {
          Intrinsics.checkParameterIsNotNull(message, "message");
          return new Result(status, message);
       }
    
       // $FF: synthetic method
       public static Result copy$default(Result var0, int var1, String var2, int var3, Object var4) {
          if ((var3 & 1) != 0) {
             var1 = var0.status;
          }
    
          if ((var3 & 2) != 0) {
             var2 = var0.message;
          }
    
          return var0.copy(var1, var2);
       }
    
       @NotNull
       public String toString() {
          return "Result(status=" + this.status + ", message=" + this.message + ")";
       }
    
       public int hashCode() {
          int var10000 = this.status * 31;
          String var10001 = this.message;
          return var10000 + (var10001 != null ? var10001.hashCode() : 0);
       }
    
       public boolean equals(@Nullable Object var1) {
          if (this != var1) {
             if (var1 instanceof Result) {
                Result var2 = (Result)var1;
                if (this.status == var2.status && Intrinsics.areEqual(this.message, var2.message)) {
                   return true;
                }
             }
    
             return false;
          } else {
             return true;
          }
       }
    }
    
    
  6. @Metadata 信息存在于由 Kotlin 编译器生成的所有类文件中 , 并由编译器和反射读取

  7. 对Lambda表达式的解构: Kotlin允许对Lambda表达式使用解构,如果Lambda表达式的参数是支持解构的类型(如Pair、Map.Entry等),它们都具有operator修饰的componentN()方法,那么即可通过将它们放在括号中引入多个新参数来代替带个参数

    mapp.mapValues { entry -> "${entry.value}!" }
    mapp.mapValues { (key,value) -> "${key},${value}!" }
    mapp.mapValues { (_,value) -> "${value}!" }
    
  8. Lambda表达式包含两个参数和使用解构的区别: Kotlin中Lambda表达式多个参数是不需要括号的,如果使用括号就是使用解构

    {a -> ...} 一个参数
    {a,b -> ...} 两个参数
    {(a,b)->...} 一个解构对
    {(a,b),c -> ...} 一个解构和第三个参数
    

Kotlin提供的数据类

  1. Pair(二元组)与Triple(三元组)

    public data class Pair<out A, out B>(
        public val first: A,
        public val second: B
    ) : Serializable {
        public override fun toString(): String = "($first, $second)"
    }
    //二元组的中缀表达式
    public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
    public fun <T> Pair<T, T>.toList(): List<T> = listOf(first, second)
    
      
    public data class Triple<out A, out B, out C>(
        public val first: A,
        public val second: B,
        public val third: C
    ) : Serializable {
    
        public override fun toString(): String = "($first, $second, $third)"
    }
    public fun <T> Triple<T, T, T>.toList(): List<T> = listOf(first, second, third)
      
    
  2. 使用Pair来初始化一个Map

    val map= mapOf(1 to "A", 2 to "B", 3 to "C")
    println(map)
    

属性和字段

  1. Kotlin中的属性相当于java的字段(field)再加上该字段的getter和setter方法(如果是只读字段,则没有)

  2. Kotlin中var定义读写属性,val定义只读属性,系统会为只读属性生成getter方法,为读写属性生成getter和setter方法。在定义普通属性时,需要显示指定初始化值,在定义时指定或者在构造器中指定。Kotlin虽然确实会为属性生成getter和setter方法,但是由于源程序中并未真正定义这些getter和setter方法,所以Kotlin程序不允许直接调用Address对象的getter和setter方法。但是如果Java程序来调用Address类,只能通过getter和setter方法访问属性

    /**
     * 在kotlin类中定义属性后,被Kotlin程序使用时只能使用点语法访问属性
     * 被java程序使用时只能通过getter和setter方法访问属性
     */
    class Address {
        var street: String = ""
        var provice = ""
        var postCode: String? = null
        var city = ""
    }
    
     var address = Address()
     address.city = "信阳"
     address.provice = "河南"
     println(address.city)
    
  3. 自定义getter和setter方法

    • 定义getter或setter方法无需使用fun关键字

      class Person(first: String, last: String) {
        var first: String = first
        var last: String = last
        /**1. 自定义getter和setter方法不需要fun关键字
         * 2. fullName是只读属性,因此只能重写get方法不能重写setter方法
         * 3. 对于fullName,Kotlin不需要为该属性生成对应的field(字段)
         */
        val fullName: String
            get() {
                println("执行fullName的getter方法")
                return "${first}.${last}"
            }
      }      
      
  4. 如果仅仅是改变getter或者setter方法的可见性或者对其添加注解,但不需要修改默认的行为,可以只定义getter或setter方法名,不需要重新定义代码实现

     var city: String = "北京"
         private set
     var food: String = "苹果"
         @Inject set 
    

幕后字段

  1. 在Kotlin中定义一个普通属性时,Kotlin会为属性生成一个field(字段)、getter和setter方法。Kotlin为属性生成的field被称为幕后字段(backing field)。

  2. 如果Kotlin类的属性有幕后字段,则Kotlin要求为该属性显示指定初始化(定义时或者构造器中指定)。如果Kotlin类的属性没有幕后字段,则Kotlin不允许为该属性值指定初始化值(因为没有field)

  3. 满足以下条件为属性生成幕后字段

    • 该属性使用Kotlin自动生成的getter或setter方法

    • 重写getter或setter方法时,使用field关键字显示引用了幕后字段

      class Person(first: String, last: String) {
          /**
           * 1. kotlin中定义一个普通属性时,kotlin会为该属性生成一个field(字段)、getter、setter(只读属性没有)
           * kotlin为该属性所生成的field就被称为幕后字段(backing field).
           * 2. kotlin要求幕后字段必须显示指定初始值,定义的时候或者构造器中指定
           * 3. 对于只读属性,如果重写getter方法,对于读写属性,如果不重写getter、setter方法
           * kotlin总会为该属性生成幕后字段
           * 4. 重写getter、setter方法时,使用field关键字显示引用了幕后字段
           */
          var first: String = first
          var last: String = last
      
          /**1. 自定义getter和setter方法不需要fun关键字
           * 2. fullName是只读属性,因此只能重写get方法不能重写setter方法
           * 3. 对于fullName,Kotlin不需要为该属性生成对应的field(字段)
           */
          val fullName: String
              get() {
                  println("执行fullName的getter方法")
                  return "${first}.${last}"
              }
      
      
          var fullName2: String
              get() {
                  return "${first}.${last}"
              }
              set(value) {
                  val split = value.split(".")
                  this.first = split[0]
                  this.last = split[1]
      
              }
      
          /**
           * 可以省略{}
           */
          val fullName3: String
              get() = this.first + this.last
      
      
          /**
           * 1. 重写getter setter方法,使用field显示引用幕后字段
           * 2. 当程序重写getter、setter方法时,不能通过点语法来对password赋值,因为点语法
           * 本质是调用getter和setter方法,这样就形成了无限递归
           * 3. 通过field引用幕后字段,从而实现对幕后字段赋值
           */
      
          var password: String = "123456"
              set(newPassword) {
                  field = newPassword
              }
      
          var city: String = "北京"
              private set
         /* var food: String = "苹果"
              @Inject set*/
      }
      
      

幕后属性

  1. 幕后属性就是使用private修饰的属性。此种方式比较繁琐,通常没必要采用这种方式

    fun main() {
    
        var bp = BackProperty("jannal")
        //jannal
        println(bp.name)
        bp.name = "孙悟空"
        //孙悟空
        println(bp.name)
        //无法访问
        //println(bp.username)
    }
    
    class BackProperty(name: String) {
        //幕后属性
        private var username: String = name
        var name
            get() = username
            set(newName) {
                if (newName.length > 8) {
                    println("用户名必须大于8位")
                } else {
                    username = newName
                }
            }
    }
    

延迟初始化属性

  1. Kotlin提供了lateinit修饰符来解决属性的延迟初始化,使用lateinit修饰的属性,可以在定义该属性时和在构造器中都不指定初始值

  2. lateinit的限制

    • lateinit只能修饰在类体中声明的可变属性(使用val声明的属性不行,主构造器中声明的属性也不行)
    • lateinit修饰的属性不能有自定义getter或setter方法
    • lateinit修饰的属性必须是非空类型
    • lateinit修饰的属性不能是原生类型
  3. 与java不同的是,kotlin不会为属性执行默认初始化。如果在lateinit属性赋值初始化之前,程序将引发lateinit property name has not been initialized异常

    fun main(args: Array<String>) {
        var user = UserInfo()
        //由于name和password两个属性都没有初始化值,因此在创建User
        //对象后不能立即访问它的name和password属性,否则将会引发异常
        // 以下程序运行时会报错lateinit property name has not been initialized
        //println(user.name)
        
        //初始化后就不会抛异常
        user.name = "jannal"
        println(user.name)
    }
    
    class UserInfo {
        lateinit var name: String
        lateinit var password: String
    
    }
    

内联属性

  1. 从Kotlin1.1开始,inline修饰符可以修饰没有幕后字段的属性的getter或setter方法。inline如果直接修饰属性本身,相当于同时修饰属性的getter和setter方法。inline修饰属性与内联函数一样,程序在调用getter或setter方法时也会执行内联化
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值