第2章Kotlin基础

323 阅读13分钟

第2章Kotlin基础.png

2.1、基本要素:函数和变量

1、函数

  • 举例说明

    • HelloWrold.kt

      • fun main(args: Array<String>) {
        	println("hello world!")
        }
        
    • 分析

      • 1、关键字 fun 用来声明 一个函数
      • 2、参数的类型写在它的名称后面
      • 3、函数可以定义在文件的最外层,不需要把它放在类中
      • 4、数组就是类(和 Java不同,Kotlin没有声明数组类型的特殊语法)
      • 5、使用 println代替了 System.out.println。Kotlin标准库给 Java标 准库函数提供了许多语法更简洁的包装,而 println 就是其中 一个
      • 6、和许多其他现代语言一样,可以省略每行代码结尾的分号
  • 函数结构

    1函数声明

    • 1、函数的声明以fun关键字开始;
    • 2、函数名称紧随其后;
    • 3、括号里面是参数列表(先写参数变量名,接着写后参数所属类型,用冒号分开);
    • 4、参数列表后面是返回类型,用冒号分开
  • 表达式函数体

    • 可以让前面的函数变得更简单,因为它的函数体是由单个表达式构成的,可以用这个表达式作为完整的函数体,具体操作:

      • 1、去掉花括号和 return 语句;
      • 2、改为等号;
      • 3、去掉返回类型​​

      HTML学习的本质就是该是什么就用什么

    • 代码

        fun max2(a: Int, b: Int): Int = if (a > b) a else b
      
    • 如果函数体写在花括号中,我们说这个函数有代码块体,如果它直接返回了 一个表达式,它就有表达式体

    • 进一步简化,去掉返回类型

       fun max3(a: Int, b: Int) = if (a > b) a else b
      

      为什么可以去掉返回类型?

      1. 每一个表达式和变量都有类型

      2. 每个函数都有返回类型

      3. 对于表达式体函数来说,编译器会分析作为函数体的表达式,并把它的类型作为函数的返回类型,即使没有显示的写出来,这种分析通常被称作类型推导

      • 表达式函数体小结

        • 使用范围:单个表达式
        • 使用场景
          1. 简单的单行函数中
          2. 对更复杂的单个表达式求值的函数中

2、变量

  • Kotlin鼓励你使用不可变的数据,而不是可变的数据

  • java中声明变量是以类型开始,但是kotlin中许多变量声明的类型可以省略,所以:

    • 1、kotlin中变量的声明是从关键字开始,
    • 2、然后是变量名称,
    • 3、最后加上类型(如果变量没有初始化,需要显示地指定它的类型),
    • 4、再加上变量的初始值(val变量要初始化)
  • 不可变变量val

    • 只能初始化一次

      •  val answer: Int = 42
        
      • 如果编译器能确保只有唯一一条初始化语句会被执行,可以根据条件使用不同的值来初始化它

        val message: String
        if(canPerformOperation()){
            message = "Success"
        }else{
            message = "Failed"
        }
        
      • 尽管 val 引用自身是不可变的,但是它指向的对象可能是可变的。例如,下面这段代码是完全有效的

         //声明不可变引用
         val languages= arrayListOf("Java")
         //改变引用指向的对象
         languages.add("Kotlin")
        
  • 可变变量var

    • 变量值可以多次改变

      • 即使var关键字允许变量改变自己的值,但它的类型却是改变不了的

3、更简单的字符串格式化:字符串模板

  • 代码

    •    val name = if (args.size > 0) args[0] else "Kotlin"
         println("hello $name")
      
         //打印$,对$进行转义
         println("hello \$name")
      
         //复杂表达式
         println("hello ,${args[0]}")
      
         //嵌套双引号
         println("hello,${if(args.size>0) args[0] else "Love Kotlin"}")
      

4、语句和表达式

  • 在Kotlin中,if是表达式,而不是语句

  • 表达式和语句的区别

    • 表达式有值,能作为另一个表达式的一部分使用
    • 语句是包围着它的代码块中的顶层元素,没有自己的值
  • java中

    • 所有的控制结构都是语句
    • 赋值操作是表达式
  • kotlin中

    • 除了for/do/do while以外,大多数控制结构都是表达式
    • 赋值操作是语句

2.2、类和属性

1、属性

  • 在Kotlin中,public是默认的可见性,所以可以省略

  • 在Kotlin中,属性是头等的语言特性,完全替代了字段和访问器方法

  • 在类中声明一个属性和声明一个变量一样:使用val和var关键字

  • 例子

    • class Person(name:String,isMarried:Boolean) {
      	val name:String="Bob"
      	//为什么一定要初始化呢?
      	var isMarried:Boolean=false
      }
      
    • val person=Person("Jon",true)
      println(person.name)
      println(person.isMarried)
      
  • 调用构造方法不需要关键字new

  • 可以直接访问属性,但调用的是getter

2、自定义访问器

  • 属性访问器的自定义实现

  • 假如你声明这样一个矩形,它能判断自己是否是正方形。不需要一个单独的字段来存储这个信息,因为可以随时通过检查矩形的长宽是否相等来判断:

    • 代码

      • class Rectangle(val height: Int, val width: Int) {
        //它这里为什么可以直接写一个get()方法呢,Boolean后面直接跟着get,没有什么衔接的吗
        val isSquare: Boolean
          get() {
              return height == width
          }
        }
        
      • 属性isSquare不需要字段来保存它的值,它只有一个自定义实现的getter。它的值是每次访问属性的时候计算出来的

      • 注意,不需要使用带花括号的完整语法,也可以这样写get()=height==width,对这个属性的调用依然不变

      • val rectangle=Rectangle(41,43)
        println(rectangle.isSquare)
        

3、kotlin源码布局:目录和包

  • java把所有的类组织成包。Kotlin和java有类似的包的概念,每一个Kotlin文件都能以一条package语句开头

  • 而文件中定义的所有声明(类、函数及属性)都会被放到这个包中

  • Kotlin不区分导入的是类还是函数,它允许使用import关键字导入任何种类的声明。可以导入顶层函数的名称

    • 例子

      • Rectangle.kt

        package 二.Kotlin基础
        
        import java.util.*
        
        class Rectangle(val height: Int, val width: Int) {
            //它这里为什么可以直接写一个get()方法呢,Boolean后面直接跟着get,没有什么衔接的吗
            val isSquare: Boolean
                get() = height == width
        }
        
        fun createRandomRectangle():Rectangle{
            val random= Random()
            return Rectangle(random.nextInt(),random.nextInt())
        }
        
        • Test.kt
        package 二.Kotlin基础
        import 二.Kotlin基础.createRandomRectangle
        fun main(args: Array<String>) {
        	//如果是同一个包名下的kt文件,可以直接使用其他类的顶层函数
        	println(createRandomRectangle().isSquare)
        }
        
        >>> false
        

2.3、表示和处理选择:枚举和“when”

1、枚举

  • 声明一个简单的枚举类

    • enum class Color {
      	RED,ORANGE,YELLOW,GREEN,BLUE
      }
      
  • 这是极少数Kotlin声明比Java使用了更多关键字的例子:Kotlin用了 enum class两个关键字,而Java只有enum一个关键字

  • Kotlin中,enum是一个所谓的软关键字:只有当它出现在class前面时才有特殊的意义,在其他地方可以把它当做普通的名称使用

  • 可以给枚举类声明属性和方法

    • 代码

      //声明枚举常量的属性
      enum class Color(val r: Int, val g: Int, val b: Int) {
      
          //在每个常量创建的时候指定属性值
          RED(255, 0, 0),
          ORANGE(255, 165, 0),
          YELLOW(255, 255, 0),
          GREEN(0, 255, 0),
          BLUE(0, 0, 255);//这里必须要有分号
      
          //给枚举类定义一个方法
          fun rgb() = (r * 256 + g) * 256 + b
      }
      
      >>> println(Color.BLUE.rgb())
      255
      
  • Kotlin语法中唯一必须使用分号的地方:如果要在枚举类中定义任何方法,就要使用分号把枚举常量列表和方法定义分开

2、使用“when”处理枚举类

  • when是一个有返回值的表达式,因此可以写一个直接返回when表达式的表达式体函数

  • 代码

    • /**
       * 使用when来选择正确的枚举值
       * (直接返回一个“when”表达式)
       */
      fun getMnemonic(color: Color) =
          //如果颜色和枚举常量相等就返回对应的字符串
          when (color) {
              Color.RED -> "Richard"
              Color.ORANGE -> "Of"
              Color.YELLOW -> "York"
              Color.GREEN -> "Gave"
              Color.BLUE -> "Battle"
          }
      
  • 和Java不一样,不需要在每个分支都写上break语句(在Java中遗漏break通常会导致bug)

  • 也可以把多个值合并到同一个分支,用逗号隔开

    • /**
       * 在一个when分支上合并多个选项
       */
      fun getWarmth(color: Color) =
          when (color) {
              Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
              Color.GREEN -> "neutral"
              //Color.BLUE->"cold"
              //常量如果没有全部列出来,就需要用else语句统一处理下
              else -> "unKnow"
          }
      

3、在“when”结构中使用任意对象

  • when允许使用任何对象

    • 代码

      • fun mix(c1: Color, c2: Color) =
          //setOf是Kotlin集合类中的一个方法
          //“when”表达式的实参可以是任何对象,它被检查是否与分支条件相等
          when (setOf(c1, c2)) {
              //列举出能够混合的颜色对
              setOf(Color.RED, Color.YELLOW) -> Color.ORANGE
              setOf(Color.YELLOW, Color.BLUE) -> Color.GREEN
              //如果没有任何其他分支支配这里就会执行
              else -> throw Exception("Dirty color")
            }
        
    • Kotlin标准库中有一个setOf函数可以创建出一个Set,它会包含所有指定为函数实参的对象,set这种集合的条目顺序并不重要,只要两个set中包含一样的条目,它们就是相等的

4、使用不带参数的“when”

  • 代码

    • /**
       * 不带参数的when
       */
      fun mixOptimized(c1: Color, c2: Color) =
          //没有实参传给"when"
          when {
              (c1 == Color.RED && c2 == Color.YELLOW) ||
                      (c1 == Color.YELLOW && c2 == Color.RED) -> Color.ORANGE
      
              (c1 == Color.YELLOW && c2 == Color.BLUE) ||
                      (c1 == Color.BLUE && c2 == Color.YELLOW) -> Color.GREEN
      
              else -> throw Exception("Dirty color")
          }
      
  • 如果没有给 when 表达式提供参数,分支条件就是任意的布尔表达式

  • 这种写法的优点是不会创 建额外的对象,但代价是它更难理解

5、智能转换:合并类型检查和转换

  • 例子

    • 加法算术表达式求值,类似(1+2)+4,这个表达式只包含一种运算,对两个数字求和

    • 如何编码呢?把它们存储在一个树状结构中,结构中的每个节点要么是一个求和(sum)要么是一个数字。Num永远都是叶子节点,而Sum节点有两个子节点:它们是求和运算的两个参数

    • 下面的代码清单展示了一种简单的类结构来表示:一个叫作Expr的接口和它的两个实现类Num和Sum。注意Expr接口没有声明任何方法,它只是一个标记接口,用来给不同种类的表达式提供一个公共的类型

    • 声明类的时候,使用一个冒号(:)后面跟上接口名称,来标记这个类实现了这个接口

    • //计算两个数的和
      interface Expr
      
      class Num(val value: Int) : Expr
      class Sum(val left: Expr, val right: Expr) : Expr
      
  • Sum存储了Expr类型的实参left和right的引用,在这个例子中,它们要么是Num要么是Sum。为了存储前面提到的表达式(1+2)+4,需要创建这样一个对象Sum(Sum(Num(1),Num(2)),Num(4)),树状图如下

    image-20210813151418608

  • 计算表达式的值

    /**
     * 计算两个数和,这里有递归计算
       */
       fun eval(e: Expr): Int {
       if (e is Num) {
           return e.value
       }
       if (e is Sum) {
           return eval(e.left) + eval(e.right)
       }
       throw IllegalAccessException("unKnow expression")
       }
    
      //计算两个数的和:(1+2)+4
      println(eval(Sum(Sum(Num(1), Num(2)), Num(4))));
    
  • Expr接口有两种实现,所以为了计算出表达式的结果值,得尝试两种选项

    • 1、如果表达式是一个数字,直接返回它的值
    • 2、如果是一次求和,得先计算左右两个表达式的值,再返回它们的和
  • 在Kotlin中,使用is检查来判断一个变量是否是某种类型,is检查和Java中的instanceOf相似

  • 如果你检查过一个变量是某种类型,后面就不需要再转换它,可以就把它当当作你检查过的类型使用。事实上编译器为你执行了类型转换,我们把这种行为成为智能转换

  • 前提条件

    • 1、智能转换只在变量经过is检查之后不再发生变化的情况下有效
    • 2、当对类的属性进行智能转换的时候,属性必须是val,而且不能有自定义的访问器
  • 使用as关键字表示到特定类型的显示转换

    • val n=e as Num
      

6、重构:用“when”代替“if”

  • 源码

    •  fun eval(e:Expr):Int{
          if(e is Num){
              return e.value
          }
          if(e is Sum){
              return eval(e.left)+ eval(e.right)
          }
          throw IllegalAccessException("unKnow expression")
          }
      
      fun eval2(e:Expr):Int{
          if(e is Num){
              e.value
          }
          if(e is Sum){
              eval(e.left)+ eval(e.right)
          }
          throw IllegalAccessException("unKnow expression")
      }
      
      • 因为if语句有返回值,这意味着你可以用表达式语法,去掉return语句和花括号,使用if表达式作为函数体
  • 使用“when”表达式

    • fun eval3(e:Expr):Int=
      when(e){
          is Num -> e.value
          is Sum -> eval3(e.left)+ eval3(e.right)
          else -> throw IllegalAccessException("unKnow expression")
      }
      
    • fun eval3(e:Expr):Int {
          return when (e) {
              is Num -> e.value
              is Sum -> eval3(e.left) + eval3(e.right)
              else -> throw IllegalAccessException("unKnow expression")
          }
      }
      
    • when语句是表达式,表达式,有返回值

  • When表达式并不仅限于检查值是否相等,它允许你检查when实参值的类型

7、代码块作为“if”和“when”的分支

  • if和when都可以使用代码块作为分支体,这种情况下,代码块中的最后一个表达式就是结果

  • 源码

    • /**
       * 使用分支中含有混合操作的when
       */
      fun evalWithLogging(e: Expr): Int =
          when (e) {
              is Num -> {
                  println("num:${e.value}")
                  e.value
              }
              is Sum -> {
                  val left = evalWithLogging(e.left)
                  val right = evalWithLogging(e.right)
                  println("sum:$left+$right")
                  left + right
              }
              else -> throw IllegalArgumentException("Unknown expression")
          }
      
    •  >>> println(evalWithLogging(Sum(Sum(Num(1), Num(2)), Num(4))));
           
        num:1
        num:2
        sum:1+2
        num:4
        sum:3+4
        7
      
  • 规则

    • 代码块中最后的表达式就是结果,在所有使用代码块并期望得到一个结果的地方成立。同样的规则对try主体和catch子句也有效
    • 这个规则对常规函数不成立,一个函数要么具有不是代码块的表达式函数体,要么具有包含显式return语句的代码块函数体

2.4、迭代事物:“while”循环和“for”循环

1、while循环

  • kotlin中有while循环和do-while循环,语法和java语法没什么区别

    image-20210813152910509

2、迭代数字:区间和数列

  • Kotlin中没有常规的Java for循环,在这种循环中,先初始化变量,在循环的每一步更新它的值,并在值满足某个限制条件时退出循环。为了替代这种最常见的循环用法,Kotlin使用了区间的概念

  • 区间

    • 区间本质上就是两个值之间的间隔,这两个值通常是数字:一个起始值,一个结束值。使用..运算符表示区间
    • val oneToTen=1..10
    • 注意Kotlin的区间是包含的或者闭合的,意味着第二个值始终是区间的一部分
  • 数列

    • 你能用整数区间做的最基本的事情就是循环迭代其中所有的值。如果你能迭代区间中所有的值,这样的区间被称作数列
  • 例子

    • //区间遍历1..100区间
      for(i in 1..100){
          println(fizzBuzz(i))
      }
      
      fun fizzBuzz(i: Int) =
          when {
              i % 15 == 0 -> "FizzBuzz"
              i % 3 == 0 -> "Fizz"
              i % 5 == 0 -> "Buzz"
              else -> "$i"
          }
      
    • when可以不带参数,不带参数,直接根据表达式的结果来判断

  • 例子2

    • 迭代一个带步长的数列它允许跳过一些数字,步长可以是附属,这种情况下数列是递减而不是递增的

    • //只遍历100以内的偶数
      for(i in 100 downTo 1 step 2){
          println(fizzBuzz(i))
      }
      
    • 100 downTo 1是递减的数列(步长为-1),然后setp把步长的绝对值变成了2,但方向保持不变(事实上步长被设置成了-2)

  • 语法

    • 写法 for in
    • 区间 1..10
    • downTo
    • step
    • until
    • withIndex()
  • ...语法始终创建的是包含结束值的区间,假如不想包含结束值,使用until函数可以创建这样的区间

    • 例子

    • val size=5
      for(x in 0 until size){
      	print("$x,")
          }
          
          >>> 0,1,2,3,4,
      

3、迭代map

  • 例子

    • //迭代map
      val binaryReps= TreeMap <Char,String>()
      
      fun binaryFor() {
          for (char in 'A'..'F') {
            //把ASCII码转换成二进制
              val binary=Integer.toBinaryString(char.toInt())
            //存储到map中
              binaryReps[char]=binary
          }
      
          for((key,value) in binaryReps){
              println("$key=$value")
          }
      }
      
      • 使用TreeMap让键排序
      • ..语法不仅可以创建数字区间,还可以创建字符区间,这里使用它迭代从A到F所有字符,包括F
  • for循环允许展开迭代中的集合元素,(这里展开的是map的键值对元素),把展开的元素存储到了两个独立的变量中,key是键,value是值

  • 根据键来访问和更新 map 的简明语法,可以使用 map[key]读取值,并使用 map[key] = value 设置它们,而不需要调用 get 和 put

    • binaryReps[c]=binary 等价于它的Java版本 binaryReps.put(c,binary)
  • 迭代集合时使用当前下标,不需要创建一个单独的变量来存储下标,使用withIndex()语法

    •  //withIndex集合下标
        val list= arrayListOf("10","11","1001")
        for((index,element) in list.withIndex()){
            println("$index:$element")
        }
      
      >>>
      0:10
      1:11
      2:1001
      

4、使用“in”检查集合和区间的成员

  • 使用 in 运算符来检查 一个值是否在区间中,或者它的逆运算,! n,来检查这 个值是否不在区间中

  • 例子1

    •  fun isLetter(c:Char)=c in 'a'..'z'|| c  in 'A'..'Z'
      
       println(isLetter('&'))
      
       >>> false
      
  • 例子2

    •  fun isNotDigit(c:Char)=c !in '0'..'9'
      
       println(isNotDigit('9'))
      
       >>> false
      
  • in和! in运算符也适用于when表达式

    •  fun recognize(c:Char)= when(c){
        in '0'..'9' ->"It's a digit!"
        //可以组合多种区间
        in 'a'..'z',in 'A'..'Z' ->"It's a letter!"
        else ->"I don't know"
        }
      
       println(recognize('√'))
      
       >>> I don't know
      

5、区间也不仅限于字符

  • 假如有一个支持实例比较操作的任意类(实现了 java. lang . Comparable 接口),就能创建这种类型的对象的区间。如果是这样的区间, 并不能列举出这个区间中的所有对象

  • 例子

    • 是否可以列举出“ Java” 和“ Kotlin”之间所有的字符串?答案是不能。 但是仍然可以使用in运算符检查一个其他的对象是否属于这个区间 :

      •  println(”Kotlin” in "Java” .. "Scala")
        
         >>> true
        
      • 注意,这里字符串是按照字母表顺序进行比较的,因为String就是这样实现Comparable接口的

6、in检查也同样适用于集合

  •    //这个集合不包含字符串“Kotlin”
       println("Kotlin" in setOf("Java","Scala"))
    
       >>> false
    

2.5、kotlin中的异常

1、Kotiin 中异常处理语句

​ Kotiin 中异常处理语句的基本形式和 Java 类似,抛出异常的方式也不例外

  •  val percentage = -1
     if (percentage !in 0..100) {
          throw IllegalArgumentException("A percentage value must be between 0 and 100:$percentage")
      }
     
     >>> Exception in thread "main" java.lang.IllegalArgumentException: A percentage value must be between 0 and 100:-1
     	at 二.Kotlin基础.HelloWorldKt.main(HelloWorld.kt:58)
    

    和其他所有类一样,不必使用new关键字来创建异常实例

2、throw结构是一个表达式

​ throw能作为另一个表达式的一部分使用

  • fun exceptionFun2(number: Int) {
      val percentage =
          if (number in 0..100)
              number
          else
              throw  IllegalAccessException("A percentage value must be between 0 and 100: $number")
    }
    

3、try、catch、finally

  • 和Java一样,使用带有catch和finally子句的try结构来处理异常

  • 例子

    • 从给定的文件中读取一行,尝试把它解析成一个数字,返回这个数字,或者当这一行不是一个有效数字时,返回null

    • //try catch finally
      fun readNumber(reader:BufferedReader):Int?{//不必显式地指定这个函数可能抛出的异常
            try{
                val line=reader.readLine()
            return Integer.parseInt(line)
        }
        catch (e:NumberFormatException){
            return null
        }
        finally {
            //finally的作用和java中的一样
              reader.close()
          }
      }
      
      val reader=BufferedReader(StringReader("239"))
      println(readNumber(reader))
      
      >>> 239
      
      val reader=BufferedReader(StringReader("not a number"))
      println(readNumber(reader))
      
      >>> null
      
  • 和java最大的区别就是throws子句没有出现在代码中:如果用Java来写这个函数,你会显式地在函数声明后写上throws IOException。你需要这样做的原因是IOException是一个受检异常。在Java中,这种异常必须显式地处理。必须声明你的函数能抛出的所有受检异常。如果调用另外一个函数,需要处理这个函数的受检异常,或者声明你的函数也能抛出这些异常

  • kotlin不区分受检异常和未受检异常,不用指定函数抛出的异常,而且可以处理也可以不处理异常,这种设计是基于Java中使用的异常的实践做出的决定

4、try作为表达式

  • Kotlin中的try关键字就像if和when一样,引入了一个表达式,可以把它的值赋值给一个变量。不同于if,你总是需要用花括号把语句主体括起来,和其他语句一样,如果其主体包含多个表达式,那么整个try表达式的值就是最后一个表达式的值

  • 例子

    • //try作为表达式
      fun readNumber2(reader:BufferedReader){
          val number=try{
              Integer.parseInt(reader.readLine())
          }catch (e:NumberFormatException){
              return
          }
          println("readNumber2,$number")
      }
      
    • val reader2 = BufferedReader(StringReader("not a number"))
      readNumber2(reader2)
      

      结果没有打印,因为走到catch语句的return了,println代码没有走

    • val reader3 = BufferedReader(StringReader("999"))
      readNumber2(reader3)
      >>> readNumber2,999
      
    • 将try作为一个表达式,表达式有值,赋值给number

    • 将return语句放在catch代码块中,函数在执行catch代码块后不再执行,若想代码可以继续执行,在catch子句返回一个值,见例子2

  • 例子2

    • //try作为表达式
      fun readNumber2(reader:BufferedReader){
        val number=try{
            Integer.parseInt(reader.readLine())
        }catch (e:NumberFormatException){
            999999
        }
          println("readNumber2,$number")
      }
      

      这里竟然直接写9999就好

2.6、小结

1、fun关键字用来声明函数。val关键字和var关键字用来声明只读变量和可变变量

2、字符串模板帮助你避免烦琐的字符串连接。在变量名称前加前缀或者用前缀或者用​{}包围一个表达式,来把值注入到字符串中

3、值对象类在Kotlin中以简洁的方式表示

4、熟悉的if现在是带返回值的表达式

5、when表达式类似于Java中的switch但功能更强大

6、在检查过变量具有某种类型之后不必显式的转换它的类型:编译器使用智能转换自动帮你完成

7、for、while和do-while循环与java类似,但是for循环现在更加方便,特别是当你需要迭代map的时候,又或是迭代集合需要下标的时候。

8、简洁的语法1..5会创建一个区间。区间和数列允许Kotlin在for循环中使用同一的语法和同一套抽象机制,并且还可以使用in运算符和!in运算符来检查值是否属于某个区间

9、Kotlin中的异常处理和Java非常类似,除了Kotlin不要求你声明函数可以抛出的异常。

附件:

第2章Kotlin基础.svg

Kotlin学习之旅开始啦

第1章Kotlin:定义与目的