Kotlin基础

基本要素:函数和变量

打印经典的 "Hello, world!"Kotlin 代码如下:

fun main(args: Array<String>) {
    println("Hello,world!")
}

相应的 Java 代码实现如下:

public static void main(String[] args) {
    System.out.println("Hello,world!");
}

对比可观察到 Kotlin 的特性如下:

  • fun 来声明一个函数。
  • 参数的类型写在参数名称后面,用冒号分隔。
  • 数组是一个类,没有 Java 中声明数组类型的语法。
  • 使用 println 代替了 System.out.println ,这是 Kotlin 标准库给 Java 标准库提供了许多语法更简洁的包装,println 方法是其中之一。
  • 代码结尾省略分号。

函数

带返回值的函数示例如下:

fun max(a: Int, b: Int): Int {
    return if (a > b) a else b
}

相应的 Java 代码实现如下:

public static final int max(int a, int b) {
      return a > b ? a : b;
}

函数声明总结:

fun —— 函数名 —— (参数列表) —— 冒号 —— 返回类型

对应上述方法如下:

fun —— max —— (a: Int, b: Int) —— : —— Int

上述函数说明了在 kotlin 中, if 是有结果值的表达式,类似于 Java 中的三元运算符(kotlin 里没有三元运算符)。

语句和表达式的区别

kotlin 中 , if 是表达式而不是语句,表达式有值,并且能作为另一个表达式的一部分使用;而语句总是包围它的代码块中的顶层元素,并且没有自己的值。在 Java 中,所有的控制结构都是语句,而在 kotlin 中,除了循环(for、 while、 do/while)以外大多数控制结构都是表达式。

注意: Java 中赋值操作是表达式,而 Kotlin 中反而变成了语句,这有助于避免比较和赋值操作之间的混淆。

代码示例如下:

//kotlin,此方法不能编译,赋值是语句,没有值,不能直接返回
fun assignment(a: Int): Int {
    return  a = 100
}

//java ,正常编译,赋值是表达式,有值,能直接返回
public static int assignment(int a) {
    return a = 100;
}

表达式函数体

如果函数体是由单个表达式构成,可以去掉花括号和 return 语句,并且可以省略返回类型,称之为表达式函数体,否则称之为代码块函数体。把上述带返回值的函数变成表达式体后的代码如下:

// 表达式体的方式展示
fun maxExpression(a:Int,b: Int) = if (a > b) a else b

类型推导

事实上,每个变量和表达式都有类型,每个函数都有返回类型,但是对表达式体函数来说,编译器会分析该表达式并把它的类型作为函数的返回类型,即使没有显式地写出来,这被称作类型推导

注意:只有表达式体函数可以省略返回类型和 return 语句,代码块体函数不能省略返回类型和 return 语句。

变量

Kotlin 中变量声明以关键字开始,然后是变量名称,最后加上类型(类型也可以省略)。

kotlin 代码:

// 显式指定变量类型
val question: String = "Are you Ok ?"
// 通过类型推导
val answer = "yes"

// val 是关键字 ,answer 是变量名称,String 是类型

java 代码:

final String question = "Are you Ok ?";
final String answer = "yes";

注意: 如果不能提供赋值给这个变量的信息,编译器则无法推导出它的类型,需要显式地指定它的类型。

可变变量和不可变变量

声明变量的关键字有两个:

  • val (来自 value),不可变引用,在初始化之后不能再次赋值,对应 Java 中的 final 变量。

  • var (来自 variable),可变引用,可以再次赋值,对应 Java 中非 final 变量。

如果编译器能确保只有唯一一条初始化语句会被执行,可以根据条件使用不同的值来初始化它。示例如下:

class Variable {
    // 需要显式指定类型
    val otherAnswer: String

    constructor(cotent: String) {
        // 在构造方法中赋值
        otherAnswer = if (cotent.isEmpty()) "no" else "yes"
    }
}

注意:尽管 val 引用自身是不可变的,但是它指向的对象可能是可变的,例如:

// val 引用的对象可能是可变的
val languages = arrayListOf("kotlin", "java")
languages.add("python")

其实和 Java 一致, final 定义一个集合,集合中的数据是可以改变的,引用不变即可。

注意: var 声明的变量允许改变值,但不能改变类型,例如:

var answer = "yes"
// 不能编译 The integer literal does not conform to the expected type String
answer = 100

answer 通过类型推导已经知道是 String 类型了,后续的赋值操作不能改变类型。

字符串模板

Kotlin 允许在字符串字面值中引用局部变量,只需要在变量名称前面加上字符 $,当然表达式会进行静态检查,引用不存在的变量时代码根本不能编译,示例如下:

val content = "test"
val age = 18

println("打印的内容如下: $content")
println("年纪为:${age}岁")

Java 代码如下:

String var = "打印的内容如下: " + content;
String var2 = "年纪为:" + age + '岁';

注意: 引用字符串变量时,可省略 {} ,其它变量不能省略。如果要在字符串中使用 $ 符号,需要对它进行转义(在 $ 前加上 ‘\’ 即可),示例如下:

// 输出 转义:$test
print("转义:\$$content")

通过使用 ${} 的方式可以引用更复杂的表达式:

// 输出 1 + 2 = 3
println("1 + 2 = ${1 + 2}")

还可以在双引号内嵌套双引号,只要它们在 {} 内:

// 输出 age大于10 
println("age${if (age > 10) "大于10" else "小于10"}")

类和属性

先来看一个简单的 JavaBeanPerson ,它只有一个属性 name

public class Person {
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

Java 中,构造方法的方法体常常包含完全重复的代码:它把参数赋值给有着相同名称的字段。在 Kotlin 中,这种逻辑不用这么多的样板代码就可以表达。 使用 Convert Java File To Kotlin File 将这个对象转换成 Kotlin

class Person(val name: String)

没错,就是只有一行代码。这种只有数据没有其他代码的类通常被叫做值对象

注意: 在 kotlin 中 public 是默认的可见性,所以可以省略它。

属性

Java 中 ,字段和其访问器(setter/getter) 的组合被叫作属性。在 Kotlin 中,属性是头等语言特性,完全代替了字段和访问器方法,声明一个属性和声明一个变量一样,用 valvar 关键字,声明成 val 的属性是只读的,而 var 属性是可变的。

class Person{
    // 只读属性,生成一个字段和一个简单的 getter
    val name : String    
    // 可变属性,生成一个字段、一个简单的 getter 和一个简单的 setter
    var isMarried : Boolean

    // 构造方法
    constructor(_name: String , _isMarried : Boolean){
        name = _name
        isMarried = _isMarried
    }

}

将上述的 Person 转换成 Java 实现如下:

public final class Person {
   @NotNull
   private final String name;
   private boolean isMarried;

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final boolean isMarried() {
      return this.isMarried;
   }

   public final void setMarried(boolean var1) {
      this.isMarried = var1;
   }

   public Person(@NotNull String _name, boolean _isMarried) {
      Intrinsics.checkParameterIsNotNull(_name, "_name");
      super();
      this.name = _name;
      this.isMarried = _isMarried;
   }
}

简单的说就是平时我们用代码模板生成的 bean ,在 Kotlin 中连模板都不需要使用了,编译时会自动生成对应的代码。生成的 gettersetter 方法都是在属性名称前加上 getset 前缀作为方法名,但是有一种例外,如果属性时以 is 开头,getter 不会增加前缀,而它的 setter 名称中 is 会被替换成 set 。所以你调用的将是 isMarried()

Java 中使用是这样的:

// 跟用 Java 生成的类的使用方式一样
Person person = new Person("fanda", true);
System.out.println(person.getName());
System.out.println(person.isMarried());

kotlin 中使用是这样的:

// 不需要关键字 new 
val person = Person("fanda", true)
// 可以直接访问属性,底层调用还是访问器
println(person.name)
println(person.isMarried)

Java 类在 kotlin 中也可以使用这样简洁的语法来使用,会相应地转化为对应的 val 属性和 var 属性。

自定义访问器

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

class Rectangle(val height: Int, val width: Int) {

    val isSquare: Boolean
        // 自定义 getter 访问器,这里使用了表达体函数
        get() = height == width
}

相应的 Java 代码如下:

public final class Rectangle {
   private final int height;
   private final int width;

   public final boolean isSquare() {
      return this.height == this.width;
   }

   public final int getHeight() {
      return this.height;
   }

   public final int getWidth() {
      return this.width;
   }

   public Rectangle(int height, int width) {
      this.height = height;
      this.width = width;
   }
}

注意: 这里的 isSquare 在转化为 Java 代码时,就是一个无参的函数,其实也可以直接在 kotlin 中定义同样的函数,没有任何差别。通常来说,如果描述的是类的特征(属性),应该把它声明成属性。

Kotlin 源码布局:目录和包

Java 类似,每一个 Kotlin 文件都能以一条 package 语句开头,而文件中定义的所有声明(类、函数及属性)都会被放到这个包中。如果其他文件中定义的声明也有相同的包,这个文件可以直接使用它们;如果包不相同,则需要导入它们。和 Java 一样,导入语句放在文件的最前面并使用关键字 import ,仅仅省略了分号。

package zeng.fanda.com.kotlinpratice.base

import kotlin.random.Random

注意: Kotlin 不区分导入的是类还是函数,可以直接导入顶层函数的名称,在 Kotlin 中的函数可以单独存在,不一定要在类中声明,可以直接声明在文件中,这种函数叫做顶层函数,如果声明的是属性,就叫做顶层属性,无论是顶层函数还是属性,都可以直接导入使用。

Java 中,要把类放在和包结构相匹配的文件与目录结构中。而在 Kotlin 中包层级机构不需要遵循目录层级结构,可以把多个类放进同一个文件中。但是不管怎样,遵循 Java 的目录布局更根据包结构把源码文件放到对应的目录中是个更好的选择,避免一些不期而遇的错误。

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

声明枚举类

Kotlin 中声明枚举类如下:

enum class Color {
    RED, GREEN, BLUE
}

通过 enumclass 两个关键字来声明一个枚举类(在 Java 中只需要 enum),enum 是一个软关键字,只有和 class 连用才有效,在其他地方可当普通名称使用,和 Java 一样,枚举不是值的列表,可以给枚举类声明属性和方法:

// 声明带属性和方法的枚举类
enum class Color(val r: Int, val g: Int, val b: Int) {
    RED(255, 0, 0), GREEN(0, 255, 0), BLUE(0, 0, 255) ;// 这里必须要有分号,用于分隔枚举常量和方法

    // 给枚举类定义一个方法
    fun rgb() = (r * 256 + g) * 256 + b

}

相应的 Java 代码如下:

public enum Color {
   RED,
   GREEN,
   BLUE;

   private final int r;
   private final int g;
   private final int b;

   public final int rgb() {
      return (this.r * 256 + this.g) * 256 + this.b;
   }

   public final int getR() {
      return this.r;
   }

   public final int getG() {
      return this.g;
   }

   public final int getB() {
      return this.b;
   }

   private Color(int r, int g, int b) {
      this.r = r;
      this.g = g;
      this.b = b;
   }
}

枚举常量用的声明构造方法和属性的语法一样,而必须提供该常量的属性值,这里展示了 Kotlin 中唯一使用分号的情况,如果要在枚举类中定义任何方法,必须用分号将常量列表和方法分开。

使用 “when” 处理枚举类

对于 Java ,通常使用 switch 来匹配枚举,如下:

public static String getColorStr(Color color) {
    String colorStr;
    switch (color) {
        case RED:
            colorStr = "red";
            break;
        case GREEN:
            colorStr = "green";
            break;
        case BLUE:
            colorStr = "blue";
            break;
        default:
            colorStr = "nothing";
            break;
    }
    return colorStr;
}

kotlin 中是没有 switch 语句的,而是用 when 表达式来处理,注意这是一个有返回值的表达式,因此可以用表达式体函数来直接返回一个 when 表达式:

fun getColorStr(color: Color) =
    when (color) {
        Color.RED -> "red"
        Color.GREEN -> "green"
        Color.BLUE -> "blue"
        else -> "nothing"
    }

相比来说,kotlin 的代码相当简洁,不需要在每个分支都写 break 语句,如果匹配成功,只有对应的分支会执行,也可以把多个值合并到同一个分支,只需要用逗号隔开,如下:

fun getColor(color: Color) = when (color) {
    Color.BLUE, Color.GREEN -> "bule and green"
    Color.RED -> "red"
}

还可以通过导入这些枚举常量来简化代码:

import zeng.fanda.com.kotlinpratice.base.Color.*

fun getColorStr(color: Color) =
    when (color) {
        RED -> "red"
        GREEN -> "green"
        BLUE -> "blue"
        else -> "nothing"
    }

fun getColor(color: Color) = when (color) {
    BLUE, GREEN -> "bule and green"
    RED -> "red"
}

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

whenswitch 强大的多,switch 要求必须使用常量(枚举常量、字符串或数字字面值)作为分支条件,when 允许使用任何对象,接下来写一个混合两种颜色的函数来体验一下这种特性:

fun mix(c1: Color, c2: Color) =
    when (setOf(c1, c2)) {
        setOf(RED,GREEN) -> BLUE
        setOf(RED,BLUE) -> GREEN
        else -> throw Exception("nothing")
    }

setOfkotlin 标准库提供的一个方法,用于创建 Set (无序)集合,如果没有其他的分支满足条件,else 分支会被执行。

使用不带参数的 “when”

上面的函数每次调用时都会创建一些 Set 实例,如果该函数调用很频繁,为了避免创建额外的垃圾对象,可以用如下方法来重构,虽然可读性会变差:

fun mixOptimized(c1: Color, c2: Color) =
    when {
        (c1 == RED && c2 == GREEN) || (c1 == GREEN && c2 == RED) -> BLUE
        (c1 == RED && c2 == BLUE) || (c1 == BLUE && c2 == RED) -> GREEN
        else -> throw Exception("nothing")
    }

如果没有给 when 表达式提供参数,分支条件就是任意的布尔表达式。

智能转换

Java 中存在多态的概念,当用父类和接口类声明一个对象时,可以创建其子类或实现类,当需要用到子类或实现类时,需要对其进行实例判断并显式地对其进行类型强转,而在 Kotlin 中,当已经进行实例判断之后,不再需要显式强转操作了,编译器会帮助执行类型转换,这种行为称为智能转换,下面看一下示例:

Kotlin 代码如下:

// 一个基类接口
interface Expr

// 简单的值对象,实现 Expr 接口
class Num(val value: Int) : Expr

// Sum 运算,需要左右两个简单值对象,运用多态,声明成基类
class Sum(val left: Expr, val right: Expr) : Expr

// 如果表达式是 Num 类型,直接返回值,如果是 Sum 类型,先计算左右表达式的值再求和
fun eval(e: Expr): Int =
    when (e) {
        // 用 is 判断实例,不需要再强转
        is Num -> e.value
        is Sum -> eval(e.left)+ eval(e.right)
        else -> throw Exception("error")
    }

Java 代码如下(只显示 eval 方法):

public static final int eval(Expr e) throws Exception {
    int result;
    if (e instanceof Num) {
        result = ((Num) e).getValue();
    } else {
        // 用 instanceof 判断实例
        if (!(e instanceof Sum)) {
            throw new Exception("error");
        }
        // 需要强制转换
        result = eval(((Sum) e).getLeft()) + eval(((Sum) e).getRight());
    }
    return result;
}

对比两者代码,用 kotlin 的方式实现非常的简洁,is 检查类似于 instanceof ,而且检查过之后不再需要强转指定类型,这里再次展示了 when 作为有返回值的表达式的好处,不需要用 result 来做临时的变量来做返回。同时这里也展示了用 when 来代替 if 做多个条件判断的简洁性,如果分支条件较少,可以用 if ,否则用 when 更有优势。

注意:如果需要显式的强转操作,需要用到 as 关键字,如下:

// 把 e 强转为 Num 类型赋值给 n
val n = e as Num

代码块作为 “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)
            left + right
        }
        else -> throw IllegalArgumentException("Unkonw expression")
    }

循环和迭代

“while” 循环

Kotlinwhiledo-while 循环与 Java 完全一致,这里不再过多叙述。

迭代数字:区间和数列

Kotlin 里不再使用 Java 中的常规 for 循环了(初始化变量——循环更新值——值满足某个条件后退出循环),而是使用区间,区间本质上就是两个值之间的间隔,这两个值通常是数字:一个起始值和一个结束值,使用 .. 运算符来表示区间:

val oneToTen = 1..10

注意:区间默认是闭合的,即包含最后一个值,如果不想包含最后一个值,可以用 until 函数来实现,比如: 1 until 10 ,等价于 1..9until 函数非常实用,假如有一个数量为 10 的列表 list 需要遍历所有的索引,那么索引就是从 0 until list.size()

接下来,我们用整数迭代来玩 Fizz-Buzz 游戏。游戏玩家轮流递增计数,遇到能被 3 整除的数字就用单词 fizz 代替,遇到能被 5 整除的数字则用单词 buzz 代替,如果一个数字是 35 的公倍数,你得说 FizzBuzz

fun game(value: Int) =
    when {
        value % 15 == 0 -> "FizzBuzz "
        value % 5 == 0 -> "Buzz "
        value % 3 == 0 -> "Fizz "
        else -> "$value "
    }

fun playGame() {
    for (value in 1..50) {
        println(game(value))
    }
}

把游戏变得复杂一点,只计算奇数的数值,即 1、3、5..49 ,可以用 setp 2 来实现,如下:

fun playGame() {
    for (value in 1..50 step 2) {
        println(game(value))
    }
}

再把游戏变得复杂一点,从 50 开始倒着计算到 0 ,并且只计算偶数,实现如下:

fun playGameDown() {
    for (value in 50 downTo 0 step 2) {
        println(game(value))
    }
}

迭代 map

我们用一个打印字符二进制表示的程序作为例子:

fun printCharBinary() {
    // 使用 TreeMap 让键来排序
    val binaryReps = TreeMap<Char, String>()
    // 使用字符区间从 A 到 F 之间的字符
    for (c in 'A'..'F') {
        // 把 ASCII 码转换成二进制
        val binary = Integer.toBinaryString(c.toInt())
        // 根据键把值存储到 map 中 ,等价于 binaryReps.put(c,binary)
        binaryReps[c] = binary
    }

    // 迭代 map ,把键和值赋给两个变量
    for ((letter, binary) in binaryReps) {
        println("$letter == $binary")
    }
}

.. 语法不仅可以创建数字区间,还可以创建字符区间。这里展示了 for 循环的展开语法,把展开的结果存到了两个独立的变量中。 map 中提供根据键来访问和更新 map 的简明语法。使用 map[key] 读取值,并使用 map[key] = value 设置它们,而不需要调用 getput

你还可以使用展开语法来迭代集合的同时跟踪当前项的下标,不再需要创建一个单独的变量来存储下标并手动增加它:

// 打印带索引下标的集合
fun printList() {
    val list = arrayListOf("a", "b", "c")
    for ((index, element) in list.withIndex()) {
        println("$index == $element")
    }
}

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

使用 in 运算符来检查一个值是否在区间中,或者它的逆运算 !in 来检查这个值是否不在区间中。下面展示了如何用 in 来检查一个字符是否属于一个字符区间:

// 判断字符是否是字母
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'

// 判断字符是否不是数字
fun isNotDigit(c: Char) = c !in '0'..'9'

when 中也可以用 in!in 运算符,示例如下:

// 用 when 的方式来处理
fun recognize(c: Char) = when (c) {
    in 'a'..'z', in 'A'..'Z' -> "It is a letter!"
    in '0'..'9' -> "It is a digit!"
    else -> "I don't know !"
}

在集合中也可以用,示例如下:

println("Kotlin" in setOf("Java", "Scala", "Kotlin"))
println("Kotlin" in "Java".."Scala")

Kotlin 中的异常

Kotlin 的异常处理和 Java 类似,但不必使用 new 关键字来创建异常实例,并且 throw 结构也是一个有返回值的表达式,比如:

fun throwExpection(value: Int) =
    if (value > 0) value else throw IllegalArgumentException("value == $value")

try、 catch、 finally

Java 一样,使用带有 catchfinally 子句的 try 结构来处理异常,try 也是一个有返回值的表达式,而不是语句,所以也可以在表达式体函数中直接返回,下面这个例子从给定的文件中读取一行,尝试把它解析成一个数字,返回这个数字;或者当这一行不是一个有效数字时返回 null

fun readNumber(reader: BufferedReader): Int? =
    try {
        val line = reader.readLine()
        Integer.parseInt(line)
    } catch (e: NumberFormatException) { // 异常类型在右边
        null
    } finally {
        reader.close()
    }

Java 最大的区别就是 throws 子句没有出现在代码中:如果用 Java 来写这个函数,你会显示地在函数声明的后写上 throws IOException 。你需要这样做的原因是 IOException 是一个受检异常。在 Java 中,这种异常必须显示地处理。必须申明你的函数能抛出的所有受检异常。如果调用另外一个函数,需要处理这个函数的受检异常,或者声明你的函数也能抛出这些异常。和其他许多现在 JVM 语言一样,Kotlin 并不区分受检异常和未受检异常。不用指定函数抛出的异常,而且可以处理也可以不处理异常。

注意: 如果一个 try 代码块执行一切正常,代码块中最后一个表达式就是结果,如果捕获到了异常,相应 catch 代码中最后一个表达式就是结果。

小结

  • fun 关键字用来声明函数,valvar 关键字分别用来声明只读变量和可变变量。
  • 字符串模板帮助你避免烦琐的字符串拼接,在变量名称前加上 $ 前缀或者用 ${} 包围一个表达式,来把值注入到字符串中。
  • 值对象类在 kotlin 中以简洁的方式表示。
  • 熟悉的 if 现在是带返回值的表达式。
  • when 表达式类似于 Java 中的 switch ,但功能更强大。
  • 在检查过变量具有某种类型之后不必再显式地转换它的类型。
  • for 循环迭代更加方便了,特别是当你需要迭代 map 的时候,又或是迭代集合需要下标的时候。
  • 用简洁的 .. 语法创建区间,还可以使用 in!in 来判断值是否属于某个区间。
  • 不再要求声明函数可以抛出的异常。