第二章 Kotlin入门

262 阅读7分钟

第二章 Kotlin入门

  1. 工作原理

    编程语言大致分为两类:编译型语言和解释型语言。Java属于解释性语言,Java代码编译生成class文件,这种class文件只有Java虚拟机能识别。Java虚拟机担当解释器角色,在程序运行时将变异后的class文件解释称计算机可识别的二进制数据后再执行。

    Kotlin的编译器能将Kotlin代码编译成class文件。Java虚拟机不会关心class文件是从Java编译来的,还是从Kotlin编译来的,只要是符合规格的class文件,它都能识别。这就是Kotlin的工作原理。

  2. 变量和函数

    1. 变量

      只允许变量前声明两种关键字:val和var

      val(value简写)声明一个不可变的变量,永远优先使用val来声明变量

      var(variable简写)声明一个可变的变量

      val a: Int = 10
      

      Kotlin完全抛弃了Java中的基本数据类型,全部使用了对象数据类型。

      shift+ctrl+p 显示变量类型

    2. 基本类型

      Kotlin无包装类型(Byte、Integer...)

      字节(Byte)、整型(Short&Int&Long)、浮点型(Float&Long)、字符(Char)、字符串(String

      //    val c = 12345678910l // compile error.
      // L需要大写
      val c = 12345678910L // ok
      
      val e: Int = 10
      //val f: Long = e // implicitness not allowed
      // 显式转换,隐式转换编译不过
      val f: Long = e.toLong() // implicitness not allowed
      
      val k = "Today is a sunny day."
      val m = String("Today is a sunny day.".toCharArray())
      
      // 比较地址
      println(k === m) // compare references.
      // 值是否相等,Java则比较地址,Java比较内容使用的是equals
      println(k == m) // compare values.
      
    3. 无符号类型

      字节(UByte)、整型(UShort&UInt&ULong

      val g: UInt = 10u
      val h: ULong = 100000000000000000u
      
    4. 字符串模板

      val slogan = "To be No.1"
      println("Value of String 'j' is: $slogan") // no need brackets
      println("Length of String 'j' is: ${slogan.length}") // need brackets
      
      val sample = """
                  <!doctype html>
                  <html>
                  <head>
                      <meta charset="UTF-8"/>
                      <title>Hello World</title>
                  </head>
                  <body>
                      <div id="container">
                          <H1>Hello World</H1>
                          <p>This is a demo page.</p>
                      </div>
                  </body>
                  </html>
                  """.trimIndent()
      println(sample)
      
  3. 数组

    整型数组:IntArray,整型装箱数组:Array<Int>

    Integerint的包装类型,比 int更浪费资源,最为一个优秀的 coder,肯定希望在范围允许内使用int而不是Integer

    我们来看看2个方法

    fun myList(vararg inner: Int) {
        println(inner.size)
    }
    
    fun myList2(vararg inner2: Int?) {
        println(inner2.size)
    }
    

    上述2个方法的区别其实只在于 Int后面有没有?号。 我们要不先看看反编译的代码中重要的部分

    Tools->Kotlin->Show Kotlin Bytecode->Decompile

    public static final void myList(@NotNull int... inner) {
       Intrinsics.checkNotNullParameter(inner, "inner");
       int var1 = inner.length;
       boolean var2 = false;
       System.out.println(var1);
    }
    
    public static final void myList2(@NotNull Integer... inner2) {
       Intrinsics.checkNotNullParameter(inner2, "inner2");
       int var1 = inner2.length;
       boolean var2 = false;
       System.out.println(var1);
    }
    

    大家仔细看形参的签名

    • myList是int... inner
    • myList2是Integer ...inner2

    大家可以思考kotlin编译器为何如此编译了? 其实根本的原因就是?,即可为空。 当Kotlin编译器认为形参不可为空的时候,完全可以用非包装的类型来接受参数,更合理的利用资源。

    val intArrayOf = intArrayOf(1, 3, 4)
    myList(*intArrayOf)
    myList(1, 2, 3)
    
    //注意这里的 null
    val array = arrayOf(1, 3, 4, null)
    myList2(*array)
    myList2(1, 2, 3)
    

    上述代码需要注意的是:

    • 申明 Int类型的数组,Kotlin提供了2个方法,一个是intArrayOf(),一个是arrayOf();
    • intArrayOf强调的是int,类似 Java中的int[],所以其中的参数不可为null
    • arrayOf类似 Java 中的Integer[],所以可以在其中赋值null
    • kotlin中数组不能直接赋值给可变形参vararg,需要通过关键字*
    • 反思下,当我们创建array的时候,如果不需要 null,是不是用intArray更优秀了?
    val array = arrayOf(1, 3, 4, null)
    // 打印数组内容
    println(array.contentToString())
    // 获取长度
    println(array.size)
    // 重新赋值    
    array[1] = 4
    // 取值
    println(array[0])
    // 遍历
    for (i in array) {      
    }
    // 判断值是否在数组中
    if (1 in array) {
      println("1 exists in variable 'array'")
    }
    
  4. 区间

    // [0,10] 离散值
    val intRange: IntRange = 1..10
    // [0,10)
    val intRange1: IntRange = 1 until 10
    // [10,9,...,1]
    val intProgression = 10 downTo 1
    
    // 步长
    val intProgression1 = 1..10 step 2
    for (i in intProgression1) {
      // 1  3  5  7  9  
      print("$i  ")
    }
    // 1, 3, 5, 7, 9
    println(intProgression1.joinToString())
    
    // [1,2] 连续值
    val closedFloatingPointRange: ClosedFloatingPointRange<Float> = 1f..2f
    
    val intArrayOf = intArrayOf(1, 2, 3, 4, 5)
    for (i in 0 until intArrayOf.size) {
        print("${intArrayOf[i]} ")
    }
    
    // intArrayOf.indices [0,intArrayOf.size)
    for (i in intArrayOf.indices) {
        print("${intArrayOf[i]} ")
    }
    
  5. 集合

    • 增加了不可变集合

      不可变ListList<T>,可变List MutableList<T>

      Map...

      Set...

      // 不能添加或删除元素
      val listOf = listOf(1, 2, 3)
      // 可以添加或删除元素
      val mutableListOf = mutableListOf(1, 2, 3)
      
      val mutableMapOf = mutableMapOf("1" to "first", "2" to "Second")
      for ((x, y) in mutableMapOf) {
              
      }
      val pair = "Hello" to "Kotlin"
      val pair1 = Pair("Hello", "Kotlin")
      
      val first = pair.first
      val second = pair.second
      val (x, y) = pair
      
      // Java实现
      List<Integer> integers = Arrays.asList(1, 2, 3);
      // 运行时报错
      integers.add(1);
      // 内容可变       
      integers.set(1, 3);
      
      // 内容不可变
      Collections.unmodifiableList(integers)
      
      ArrayList arrayList = new ArrayList();
      arrayList.add(1);
      
    • 提供了丰富易用方法

    • 运算符级别支持,简化访问集合框架

  6. 函数

  7. 函数定义

    fun(function简写)定义函数的关键字。fun后跟着是函数名。接着是一对括号,括号内是参数。括号后是返回值,无返回值时直接不写或者写为UnitUnit等价于Java的void函数返回值。

    fun functionName(arg1: Int, arg2: Int): Int {
        return 1
    }
    

    当函数只有一行代码时,Kotlin允许不写函数体,直接将唯一的一行代码写在函数定义的尾部,中间用等号连接即可。

    fun largeNumber(arg1: Int, arg2: Int) = max(arg1, arg2)
    
  8. 函数 vs 方法

    • 方法可以认为是函数的一种特殊类型
    • 从形式上,有receiver(Java中对象,句柄)的函数即为方法
  9. 函数的类型

    fun foo() {}  // 类型为: () -> Unit
    val a: () -> Unit = {}
    
    // Foo就是bar方法的receiver,Java中使用this,其实是通过函数第一个参数传递了进来没有显示调用
    // 函数的引用类似C语言中的函数指针,用于函数传递
    //    val kFunction: (Foo, String, Long) -> Long = Foo::bar
    //    val kFunction2: Foo.(String, Long) -> Long = Foo::bar
    //    val kFunction3: Function3<Foo, String, Long, Long> = Foo::bar
    class Foo{
        fun bar(p1:String, p2:Long) : Long {
            return p1.length + p2
        }
    }
    
    val x: (Foo, String, Long) -> Any = Foo::bar
    yy(x)
    fun yy(p: (Foo, String, Long) -> Any) {
        p(Foo(), "Hello", 3L)
        p.invoke(Foo(), "Hello", 3L)
    }
    
  10. 程序的逻辑控制

    程序的执行语句分为顺序语句、条件语句和循环语句。

    1. 条件语句

      1. if条件语句

        if条件语句与Java无异

        Kotlin中if语句比Java中多了一个额外功能,if语句可以有返回值

        fun largeNumber(num1: Int, num2: Int): Int {
            val largeInt = if (num1 > num2) {
                num1
            } else {
                num2
            }
            return largeInt
        }
        
      2. when条件语句

        when类似Java中的switch

        switch缺陷:只能传入char, byte, short, int, Character, Byte, Short, Integer, String, or an enum变量作为条件。并且每个case需要break,否则执行完当前case后依次执行下面case。

        基本示例:

        fun getScore(name: String) = when (name) {
            "Tom" -> 85
            "Jim" -> 77
            else -> 0
        }
        

        类型匹配示例:

        fun checkNumber(num: Number) = when (num) {
            is Int -> "Int"
            is Double -> "Double"
            else -> "not support type"
        }
        

        is是类型匹配的关键字,相当于java中的instanceof关键字。

        when不带参数的示例,Kotlin中判断字串或对象使用**==关键字,不用像Java使用equals()**方法

        fun getScoreNoParam(name: String) = when {
            name.startsWith("lisa") -> 99
            name == "Tom" -> 85
            name == "Jim" -> 77
            else -> 0
        }
        
    2. 循环语句

      1. for循环语句

        Java中for-i循环已被舍弃,Java中for-each循环被加强变成了for-in循环

        Kotlin中区间的写法:

        val range = 0..10 // [0,10]    
        val range2 = 0 until 10 // [0,10)
        
        for (i in 0..10) {
            print("$i  ")
            // 输出 0  1  2  3  4  5  6  7  8  9  10  
        }
        

        使用step关键字跳过奇数元素,相当于Java中的i = i + 2操作

        for (i in 0 until 10 step 2) {
            print("$i  ")
            // 输出 0  2  4  6  8  
        }
        

        downTo定义降序区间

        for (i in 10 downTo 0) {
            print("$i  ")
            // 输出 10  9  8  7  6  5  4  3  2  1  0  
        }
        
      2. while循环不管是在语法还是使用技巧上都和Java中的while循环没有任何区别

  11. 面向对象编程

    1. File通常是用于编写Kotlin顶层函数和扩展函数的

      Class是用于编写Kotlin的类

      类默认是用public修饰的

      类示例:

      class Person {
          var name = "Tony"
          var age = 10
      
          fun introduceSelf() {
              println("My name is $name. I am $age years old.")
          }
      }
      

      类示例化:

      val person = Person()
      person.introduceSelf()
      
    2. 继承和构造函数

      1. 定义Student类继承自Person:

        1. 使Person可继承,给Person加上open关键字。在Kotlin中任何一个非抽象类默认都是不可以被继承的,相当于Java中给类声明了final关键字。

          open class Person {
          	......
          }
          
        2. 使Student继承Person

          class Student : Person() {
            init {
            	...      
            }
          	...
          }
          

          Person后面为何加括号:子类构造函数必须调用父类构造函数。

          Kotlin中构造函数有:主构造函数和次构造函数。主构造函数没有函数体,Kotlin提供了init结构体。子类的主构造函数调用父类的哪个构造函数,在继承的时候通过括号来指定。

      2. 改造Person

        Person改造如下:

        open class Person(val name:String, val age:Int) {
            fun introduceSelf() {
                println("My name is $name. I am $age years old.")
            }
        }
        

        Student作出相应调整:

        class Student(val number: Int, val grade : Int, name:String, age:Int) : Person(name, age) {
            init {
        
            }
        }
        

        Student构造函数中的name和age不能声明成var或者val,因为声明为val或者val会成为该类的字段。

      3. 次构造函数

        类只能有一个主构造函数,但是可以有多个次构造函数。所有次构造函数都必须调用主构造函数。

        示例如下:

        class Student(val number: Int, val grade: Int, name: String, age: Int) : Person(name, age) {
            constructor(name: String, age: Int) : this(0, 0, name, age) {
            }
        
            constructor() : this("Tom", 0) {
            }
        }
        

        次构造函数是用constructor定义的。实例化代码如下:

        val student1 = Student()
        val student2 = Student("Jack", 10)
        val student3 = Student(1527, 2, "Jerry", 21)
        

        类只有次构造函数,没有主构造函数:

        class Student : Person {
        
            constructor(number: Int, grade: Int, name: String, age: Int) : super(name, age) {
        
            }
        
            constructor(name: String, age: Int) : this(0, 0, name, age) {
        
            }
        
            constructor() : this("Tom", 0) {
        
            }
        }
        

        在次构造函数中参数不能用var或val声明,因为var或者val声明的参数会成为类的字段。次构造函数不是必须调用的,所以存在未调用导致未初始化,所以禁止使用var或者val声明。

    3. 抽象类

      abstract class AbsClass {
          abstract fun absMethod()
          // 添加open之后才可复写
          open fun overridable(){}
          fun nonOverridable(){}
      }
      

      方法前加open关键字,方可复写

      如果继承的父类有个方法可复写,但是不想子类复写该方法,可添加final关键字

      open class SimpleClass(var x: Int, val y: String)
          : AbsClass() {
      
          override fun absMethod() {}
      
          // 继承SimpleClass的字类不可复写该方法
          final override fun overridable(){
          }
      }
      
    4. 接口

      Kotlin中接口和Java中接口几乎一致。

      1. 接口使用:

        1. 创建Study接口

          interface Study {
              fun readBooks()
              fun doHomework()
          }
          
        2. 实现接口

          class Student(name: String, age: Int) : Person(name, age), Study {
              override fun readBooks() {
              }
          
              override fun doHomework() {
              }
          }
          

          熟悉Java的人一定知道,Java中继承使用的关键字是extends,实现接口使用的关键字是implements,而Kotlin中统一使用冒号,中间用逗号进行分隔。

        3. 示例:

          fun main() {
              val student = Student("Jack", 10)
              doStudy(student)
          }
          
          fun doStudy(study: Study) {
              study.doHomework()
              study.readBooks()
          }
          

          因为Student实现了Study接口,所以Student类可以传递给doStudy函数,接着调用Study接口的方法。这种叫做面向接口编程,也就是多态。

      2. 默认实现

        interface Study {
            fun readBooks()
            fun doHomework() {
                println("doHomework default implementation")
            }
        }
        

        默认实现的函数可自由选择实现或不实现。不实现会使用默认实现逻辑。

      3. 可见性修饰符

        修饰符JavaKotlin
        public所有类可见所有类可见(默认)
        private当前类可见当前类可见
        protected当前类、子类、同一包下类可见当前类、子类可见
        default同一包下类可见(默认)
        internal同一模块中类可见
      4. property(Kotlin) vs field(Java)

        Kotlin 的 property = Java 的(field + get + set

        如果val修饰,只有get

        如果var修饰,get + set

        class Person(age: Int, name: String) {
            var age: Int = age //property
                get() {
                    return field
                }
                set(value) {
                    println("setAge: $value")
                    field = value
                }
            var name: String = name
                get() {
                    return field // backing field
                }
                set(value) {
        
                }
        }
        

        属性引用

        // 未绑定receiver    
        val ageRef = Person::age
        val person = Person(18, "Bennyhuo")
        // 绑定receiver
        val nameRef = person::name
        ageRef.set(person, 20)
        nameRef.set("Andyhuo")
        
    5. 数据类和单例类

      1. 数据类

        数据类通常需要重写equals()、hashCode()、toString()方法

        equals() 判断数据是否相等

        hashCode() HashMap、HashSet等hash相关类正常工作

        toString() 清晰的日志输出

        示例

        data class Cellphone(val brand: String, val price: Double)
        

        在类前面声明data表示这个类是数据类。Kotlin会自动实现equals()、hashCode()、toString()等固定无实际逻辑意义的方法。

      2. 单例类

        全局只有一个实例

        object Singleton {
            fun singletonTest() {
                println("SingletonTest is called")
            }
        }
        

        使用

        Singleton.singletonTest()
        
  12. Lambda编程

    1. 集合创建和遍历

      1. 创建List

        listOf<String>("Hello", "world", "this", "is", "kotlin")
        
      2. 遍历List

        for (s in listOf) {
            println(s)
        }
        
      3. listOf()函数创建的是一个不可变的集合,mutableListOf()函数为可变集合

        val mutableList = mutableListOf<String>("Hello", "world", "this", "is", "kotlin")
        
      4. 创建Map

        var fruitAndPrice :HashMap<String, Int> = HashMap()
        fruitAndPrice["Apple"] = 3
        
        val fruitAndIndex = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "pear" to 4, "Grape" to 5)
        
      5. 遍历Map

        for ((fruit, number) in fruitAndIndex) {
            println("fruit is $fruit, index is $number")
        }
        
    2. 集合的函数式API

      1. 取长度最长的单词

        val wordList = listOf<String>("Hello", "world", "this", "is", "kotlin")
        // 当集合中无元素时,结果会为空
        val maxByOrNull = wordList.maxByOrNull { it.length }
        // 当结果为空时,使用空字串
        val result: String = maxByOrNull ?: ""
        println(result)
        
      2. Lambda是一段可以作为参数传递的代码,Lambda语法结构

        {参数名1 : 参数类型, 参数名2 : 参数类型 -> 函数体}
        
      3. 简化推导过程

        上述maxByOrNull只是一个简单的函数,此函数的内部实现会遍历集合,并且调用lambda表达式(集合元素传入表达式)获取元素的长度并对比,最终返回最长的集合元素。

        1. lambda表达式作为变量

          val wordList = listOf<String>("Hello", "world", "this", "is", "kotlin")
          val function = { fruit: String -> fruit.length }
          val maxByOrNull = wordList.maxByOrNull(function)
          
        2. 直接作为参数传递

          val maxByOrNull = wordList.maxByOrNull({ fruit: String -> fruit.length })
          
        3. 当lambda是函数的最后一个参数时,可以将lambda表达式移到括号外

          val maxByOrNull = wordList.maxByOrNull() { fruit: String -> fruit.length }
          
        4. 如果lambda是函数的唯一参数时,可以将括号省略

          val maxByOrNull = wordList.maxByOrNull { fruit: String -> fruit.length }
          
        5. Kotlin拥有出色的类型推导机制,Lambda表达式中的参数列表其实在大多数情况下不必声明参数类型

          val maxByOrNull = wordList.maxByOrNull { fruit -> fruit.length }
          
        6. 当Lambda表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用it关键字来代替

          val maxByOrNull = wordList.maxByOrNull { it.length }
          
      4. Java函数式API使用

        限制:在Kotlin代码中调用了一个Java方法,并且该方法接收一个Java单抽象方法接口参数,就可以使用函数式API。Java单抽象方法接口指的是接口中只有一个待实现方法,如果接口中有多个待实现方法,则无法使用函数式API。

        以Runnable举例:

        1. Java实现:

          new Thread(new Runnable() {
              @Override
              public void run() {
                  TODO("Not yet implemented")
              }
          });
          
        2. Kotlin实现:

          Thread(object : Runnable {
              override fun run() {
                  TODO("Not yet implemented")
              }
          })
          

          Kotlin舍弃了new关键字,改用object关键字

          再次精简,因为Runnable只有一个待实现方法,无需显式地重写run()方法

          Thread(Runnable {
              TODO("Not yet implemented")
          })
          

          如果一个Java方法的参数列表中有且仅有一个Java单抽象方法接口参数,我们还可以将接口名进行省略

          Thread({
              TODO("Not yet implemented")
          })
          

          如果Lambda表达式还是方法的唯一一个参数,还可以将方法的括号省略

          Thread {
              TODO("Not yet implemented")
          }
          
  13. 空指针检查

  14. 可空类型

    fun doStudy(study: Study) {
        study.doHomework()
        study.readBooks()
    }
    

    上述代码没有空指针风险,因为Kotlin默认所有的参数和变量都不可为空,所以这里传入的Study参数也一定不会为空。

    Kotlin将空指针异常的检查提前到了编译时期,如果我们的程序存在空指针异常的风险,那么在编译的时候会直接报错。

    允许为空示例:

    fun doStudy(study: Study?) {
        if (study != null) {
            study.doHomework()
            study.readBooks()
        }
    }
    
  15. 判空辅助工具

    1. 借助?.操作符将if判断语句去掉

      fun doStudy(study: Study?) {
          study?.doHomework()
          study?.readBooks()
      }
      
    2. ?:操作符,这个操作符的左右两边都接收一个表达式,如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果

      一般写法:

      val a:String = "a"
      val c = if (a == null) a else ""
      

      ?:操作符写法:

      val c = a ?: ""
      

      示例获取字串的长度

      fun getWordLength(word : String) : Int {
          if (word == null) {
              return 0
          } else {
              return word.length
          }
      }
      

      简化后:

      fun getWordLength(word : String?) : Int {
          return word?.length ?: 0
      }
      
    3. *非空断言工具,不推荐,这是有风险的写法

      fun getWordLength(word : String?) : Int {
          return word!!.length
      }
      
    4. 辅助工具——let

      fun doStudy(study: Study?) {
          study?.let { stu ->
              stu.readBooks()
              stu.doHomework()
          }
      }
      

      let函数可以处理全局判空问题,而if判断语句无法做到这点。

      我们将doStudy()函数中的参数变成一个全局变量,使用let函数仍然可以正常工作,但使用if判断语句则会提示错误。

      之所以这里会报错,是因为全局变量的值随时都有可能被其他线程所修改,即使做了判空处理,仍然无法保证if语句中的study变量没有空指针风险。

      var student: Student? = Student("Lucy", 19)
      
      fun study() {
          if (student != null) {
              student.doHomework()
              student.readBooks()
          }
      }
      
  16. Kotlin中小魔术

  17. 字符串内嵌表达式

    class Student(name: String, age: Int) : Person(name, age), Study {
        override fun readBooks() {
            println("My name is $name, I'm ${age} years old. I'm reading!")
        }
    }
    
  18. 函数默认参数

    fun sing(songName:String = "Pretty Girl", artist:String) {
        
    }
    

    调用:

    sing("hello", "jams")
    // 通过键值对赋值
    sing(artist = "lucy")
    

    主构造函数中参数添加默认值,避免多个构造函数

    class Student(val number: Int = 0, val grade: Int = 0, name: String="Tom", age: Int=10) : Person(name, age), Study {
    
    }