1

博客主页

Kotlin 基础

对比下Java的类Person与Kotlin的类Person区别:

// java
public class Person {
    private final String name;

    public Person(String name) {
        this.name = name;
    }
    // 提供一个getter访问器
    public String getName() {
        return name;
    }
}

// kotlin
// 在Kotlin中,public是默认的可见性,可以省略
class Person(val name: String)

属性

在Kotlin中,在类中声明一个属性和声明一个变量一样,使用 val(只读的) 和 var(可变的) 关键字。

如果属性的名称以 is 开头,getter不会增加任何的前缀,而它的setter名称中的is会被替换成set

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

// kotlin转换为java
public final class Person {
   @NotNull
   private final String name;
   @NotNull
   private String address;
   private boolean isMarried;

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

   @NotNull
   public final String getAddress() {
      return this.address;
   }

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

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

   public final void setAddress(@NotNull String var1) {
      this.address = var1;
   }

   public Person(@NotNull String name, @NotNull String address, boolean isMarried) {
      this.name = name;
      this.address = address;
      this.isMarried = isMarried;
   }
}

在java 和 kotlin 中调用Person类区别:

// java中使用Person
 Person person = new Person("kerwin", "anqing", false);
 person.setMarried(true);
 System.out.println(person.getName() + " : " + person.getAddress() + " : " + person.isMarried());
// kerwin : anqing : true


// kotlin中使用Person
val person = Person("kerwin", "anqing", false)
person.isMarried = true
println("${person.name} : ${person.address} : ${person.isMarried}")
// kerwin : anqing : true

Kotlin 属性访问器自定义

下面自定义一个属性isSquare,实现getter,它的值是每次访问属性的时计算出来的。

// kotlin
class Rectangle(
    val height: Int,
    val width: Int
) {
    // 自定义访问器
    val isSquare: Boolean
        // 声明属性的getter
        // 也可以这样写: get() = width == height 
        get() {
            return width == height
        }
}

// kotlin转java
public final class Rectangle {
   private final int height;
   private final int width;

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

   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;
   }
}

Kotlin 目录和包

每一个kotlin文件都能以一条package语句开头,而文件中定义的所有的声明(类、函数、属性)都会被放到这个包中。如果包不相同,则需要导入它们,使用关键字import

kotlin不区分导入的是类还是函数,且允许使用import关键字导入任何种类的声明。可直接导入顶层函数的名称

// 包声明
package com.kerwin.kotlin.demo

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

    val isSquare: Boolean
        get() = width == height
}

// 在com.kerwin.kotlin.demo包中定义函数
fun createRectangle(): Rectangle {
    return Rectangle(12, 23)
}

导入其他包中的函数

// 包声明
package com.kerwin.kotlin.demo1
// 导入函数名称
import com.kerwin.kotlin.demo.createRectangle

fun main(args: Array<String>) {
    println(createRectangle().isSquare)
}

在kotlin中,可以把多个类放在同一个文件中,文件的名字还可以随意选择。kotlin也没有对磁盘上源文件的布局强加任何限制。包层级结构不需要遵循目录层级结构。

Kotlin 程序结构

1.选择处理

声明枚举类

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
}

使用"when"处理枚举类

when 是一个有返回值的表达式。

// kotlin中不需要在每个分支都写上break语句。如果匹配成功。只有对应的分支会执行
fun getColorString(color: Color) = when(color) {
    Color.RED -> "red"
    Color.BLUE -> "blue"
    Color.ORANGE -> "orange"
    Color.YELLOW -> "yellow"
    Color.GREEN -> "green"
}

可以把多个值合并到同一个分支,只需要用逗号(,)隔开这些值就可以

fun getWarmth(color: Color) = when(color) {
    Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
    Color.BLUE, Color.GREEN -> "cold"
}

在"when"结构中使用任意对象

when 允许使用任何对象作为分支条件。

// 在when分支中使用不同的对象
fun mix(c1: Color, c2: Color) =
    // when 表达式的实参可以是任何对象,它被检查是否与分支条件相等
    when (setOf(c1, c2)) {
        setOf(Color.RED, Color.YELLOW) -> Color.ORANGE
        setOf(Color.YELLOW, Color.BLUE) -> Color.GREEN
        else -> throw Exception("dirty color")
    }

使用不带参数的"when"

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

fun mixOptimized(c1: Color, c2: Color) =
    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")
    }

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

定义一个函数:(1 + 2) + 4 算术表达式求值。智能转换只在变量经过is检查后不再发生变化的情况下有效。属性必须是一个val属性,且不能有自定义的访问器。使用as关键字来表示到特定类型的显示转换。

// 仅作为一个标记接口
interface Expr

// 实现接口使用冒号(:),后面跟上接口名称
class Num(val value: Int) : Expr

class Sum(val left: Expr, val right: Expr) : Expr

// 使用if层叠对表达式求值
fun eval(e: Expr): Int {
    // 使用is检查来判断一个变量是否是某种类型
    if (e is Num) {
        // 显示的转换成类型Num是多余的
        val num = e as Num
        return num.value
    }

    if (e is Sum) {
        // 变量e智能转换了类型
        return eval(e.left) + eval(e.right)
    }

    throw IllegalArgumentException("Unknown")
}

>>>  println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))

重构:用"when"代替"if"

kotlin没有三元运算符,因为if表达式有返回值。如果if分支只有一个表达式,花括号可以省略。如果if分支是一个代码块,代码块中的最后一个表达式会被作为结果返回。

// 使用有返回值的if表达式
fun eval(e: Expr): Int {
    if (e is Num) {
        e.value
    } else if (e is Sum) {
        eval(e.left) + eval(e.right)
    } else {
        throw IllegalArgumentException("Unknown")
    }
}

可以使用when代替if层叠

// when允许检查实参值的类型
fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.left) + eval(e.right)
        else -> throw IllegalArgumentException("Unknown")
    }

代码块作为 "if" 和 "when" 的分支

if 和 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")
    }

2.循环

while循环

和java没有区别,有while循环 和 do-while循环

迭代数字:区间和数列

区间:两个值之间的间隔,这两个值通常是数字,一个起始值,一个结束值。使用 .. 运算符表示区间。
kotlin区间是包含的或者闭合的。是包含结束值的。如果不包含结束值,使用 until 函数

val oneToTen = 1..10

// 不包含size
for (x in 0 until size)
等价于
for (x in 0..size - 1)

迭代1到100之间的所有数字

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

>>>  for (i in 1..100) {
        print(fizzBuzz(i))
    }

迭代带步长的100到1的区间

 // 100 downTo 1 是递减的数列(步长为-1)
 // step 2 把步长的绝对值变成2,但是方向保持不变(步长被设置成了为-2)
 for (i in 100 downTo 1 step 2) {
    print(fizzBuzz(i))
 }

迭代map

初始化map并迭代。..语法可以创建数字区间,也可以创建字符区间。

    // 初始化map,使用TreeMap让键排序
    val binaryMap = TreeMap<Char, String>()

    // 使用字符区间迭代从A到F之间的字符
    for (c in 'A'..'F') {
        // 字符二进制
        val binary = Integer.toBinaryString(c.toInt())
        // 简明语法:map[key] = value 设置值;map[key] 读取值
        // 这行等价于:binaryMap.put(c, binary)
        binaryMap[c] = binary
    }

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

可以在迭代集合时,使用当前下标

    val list = arrayListOf("10", "11", "12")
    // 迭代集合时使用下标
    for ((index, element) in list.withIndex()) {
        println("$index : $element")
    }

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

关键字 in 可以迭代区间或者集合,还可以用来检查区间或者集合是否包含了某个值。!in 检查一个值是否不在区间中。

fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'

// !in 检查是否不在区间中
fun isNotDigit(c: Char) = c !in '0'..'9'

in 运算符 和 !in 检查可以作为 when分支

fun recognize(c: Char) = when (c) {
    // 检查值是否在0到9的区间之内
    in '0'..'9' -> "It's a digit."
    in 'a'..'z', in 'A'..'Z' -> "It's a letter."
    else -> "I don't know."
}

Kotlin 函数

1.在kotlin中创建集合

    // 创建set
    val set = hashSetOf(1, 3, 5)
    // kotlin的javaClass等价于java的getClass
    println(set.javaClass)
    // class java.util.HashSet


   // 创建list
   val list = arrayListOf(1, 3, 5)
   println(list.javaClass)
   // class java.util.ArrayList

   // 创建map
   // to 并不是一个特殊的结构,而是一个普通函数
   val map = hashMapOf(1 to "one", 3 to "three", 5 to "five")
   println(map.javaClass)
  // class java.util.HashMap

kotlin中可以通过下面方式获取一个列表中最后一个元素,或者得到一个数字列表的最大值

    val list = listOf("abc", "def", "ker")
    println(list.last())  // ker

    val set = setOf(1, 23, 4)
    println(set.max()) // 23

2.函数声明

java集合都有一个默认的toString方法实现。假设不想使用默认的实现,需要自己实现或者使用第三方库,如:guava 和Apache Commons。

// 自己实现joinToString函数
fun <T> joinToString(
    collection: Collection<T>,
    prefix: String,
    postfix: String,
    separator: String
): String {

    val result = StringBuilder(prefix)

    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator).append(" ")

        result.append(element)
    }

    result.append(postfix)

    return result.toString()
}

>>> val list = listOf("abc", "def", "ker")
>>> println(joinToString(list, "(", ")", ";"))
(abc; def; ker)

命名参数

// 这种方式不能知道每个参数对应什么含义
joinToString(list, "(", ")", ";")

// kotlin,可以显式的标明一些参数名称
// 如果在调用一个函数时,指明了一个参数名称,为了避免混淆,那它之后所有参数都需要标明名称
joinToString(list, prefix = "(", postfix = ")", separator = ";")

默认参数值

java中存在一个普遍的问题,一些类的重载函数很多,如:Thread类(8个构造方法)。

在kotlin中,可以在声明函数的时候,指定参数的默认值,就可以避免创建重载的函数。

fun <T> joinToString(
    collection: Collection<T>,
    prefix: String = "",   // 有默认的参数
    postfix: String = "",
    separator: String = ", "
): String 

// 调用的时候,可以省略只有排在末尾的参数
>>> joinToString(list)
>>> joinToString(list, "(", ")")
// 如果使用命名参数,可以省略中间的一些参数
>>> joinToString(list, separator = "; ")
// 也可以以任意顺序只给定需要的参数
>>> joinToString(list, separator = "; ", prefix = "[")

消除静态工具类:顶层函数和属性

在kotlin中,不需要创建静态工具类,可以把函数直接放到代码文件的顶层,不用从属任何的类。

声明joinToString作为顶层函数

// join.kt
package strings
fun joinToString(...): String { ... }

// 当编译join.kt这个文件时,会生成一些类,JoinKt.java。因为JVM只能执行类中的代码。
// 且join.kt文件中的所有顶层函数编译为JoinKt.java这个类的静态函数
// 从java中调用这些函数:JoinKt.joinToString(list, "", "", "");
public final class JoinKt {
     @NotNull
     public static final String joinToString(@NotNull Collection collection, @NotNull String prefix, @NotNull String postfix, @NotNull String separator) {
              ...
    }
}

修改文件类名:
要修改包含kotlin顶层函数的生成的类的名称,需要为这个文件添加 @file:JvmName 注解,将其放到这个文件的开头,位于包名的前面

// 注解指定类名
@file:JvmName("StringUtils")

// 包的声明跟在文件注解后
package strings

fun <T> joinToString(
    collection: Collection<T>,
    prefix: String = "",
    postfix: String = "",
    separator: String = ", "
): String { ... }

// 编译后,生成StringUtils.class文件
// 在java代码中调用这个函数:StringUtils.joinToString(list, "", "", "")

顶层属性和函数一样,属性也可以放到文件的顶层。

package strings

val LINE_SEPARATOR = "\n"

// 编译后,私有的静态常量。
private static final String LINE_SEPARATOR = "\n";

// 使用const修饰,const val LINE_SEPARATOR = "\n" 编译后生成public的静态常量 
public static final String LINE_SEPARATOR = "\n";

3.扩展函数和属性

扩展函数就是把要扩展的类或者接口的名称,放到即将添加的函数前面。这个类的名称称为接受者类型,用来调用这个扩展函数的那个对象,叫作接受者对象。但是扩展函数不能访问私有的或者受保护的成员。

package strings

// 为String类添加自己的方法:字符串的最后一个字符
// String:接受者类型   this:接受者对象
fun String.lastChar(): Char = this[this.length - 1]


>>> println("abc".lastChar())

导入和扩展函数

kotlin允许用和导入类一样的语法来导入单个函数。

import strings.lastChar
// 使用 * 来导入也可以: import strings.*
// 还可以使用关键字 as 来修改导入的类或者函数名称
// import strings.lastChar as last
// 调用: val lastChar = "abc".last()

val lastChar = "abc".lastChar()

从java中调用扩展函数

扩展函数就是静态函数,它把调用对象作为了它的第一个参数。

// 把接受者对象作为第一个参数传进去即可
StringUtils.lastChar("abc")

作为扩展函数的工具函数

为元素的集合类Collection添加一个扩展函数,然后给所有的参数添加一个默认值。

@file:JvmName("StringUtils")

package strings

import java.lang.StringBuilder

// Collection<T>: 接受者类型
// 为Collection<T> 声明一个扩展函数
fun <T> Collection<T>.joinToString(
    prefix: String = "",
    postfix: String = "",
    separator: String = ", "
): String {

    val result = StringBuilder(prefix)

    // this: 接受者对象
    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator).append(" ")

        result.append(element)
    }

    result.append(postfix)

    return result.toString()
}

>>> val list = listOf("abc", "def", "ker")
>>> println(list.joinToString(prefix = "{", postfix = "}", separator = "; "))
{abc;  def;  ker}

不可重写的扩展函数

kotlin中不能重写扩展函数,因为kotlin会把它们当作静态函数对待。

扩展属性

扩展属性必须定义getter函数,因为没有支持字段,因此没有默认getter实现,初始化也不可以,因为没有地方存储初始值。

// 声明一个扩展属性
val String.lastChar: Char
     get() = this[this.length - 1]

也可以声明一个可变的扩展属性

var StringBuilder.lastChar: Char
    // getter属性
    get() = this.get(this.length - 1)
    // setter属性
    set(value) {
        this.setCharAt(length - 1, value)
    }


>>> val sb = StringBuilder("ab?")
>>> sb.lastChar = '!'
>>> println(sb) // ab!

// 如果从java中访问扩展属性,显式的调用它的getter函数
StringUtils.getLastChar(new StringBuilder("abc"));

4.处理集合:可变参数、中缀调用和库的支持

  1. 可变参数的关键字 vararg,声明一个函数可以接收任意数量的参数
  2. 一个中缀表达法,当调用一些只有一个参数的函数时,可让代码更简练
  3. 解构声明,用来把一个单独的组合值展开到多个变量中

扩展java集合的API

val list = listOf("abc", "def", "ker")

// last函数被定义为List的扩展函数
println(list.last()) // ker

// public fun <T> List<T>.last(): T { ... }

可变参数:让函数支持任意数量的参数

kotlin中,在参数上使用vararg修饰符;java中使用三个点(...)

val list = listOf("abc", "def", "ker")

// listOf函数在库中声明
public fun <T> listOf(vararg elements: T): List<T>

还有一个区别:当需要传递的参数是已经包装在数组中时,在java中,可以按原样传递数组;kotlin中要求显式的解包数组,以便每个数组元素在函数中能作为单独的参数来调用。

val array = arrayOf(11, 12, 13)
// 使用展开运算符(*)传递数组
val list = listOf("10", *array)

>>> println(list) // [10, 11, 12, 13]

键值对处理:中缀调用和解构声明

val map = mapOf(1 to "one", 2 to "two")
// mapOf函数在库中的声明
public fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V>

// 一般函数调用: 2.to("two"); 中缀符号调用to函数:2 to "two"
// to 不是内置结构,而是一种特殊的函数调用,称为中缀调用

// to 函数在库中的声明
// 中缀调用可以与只有一个参数的函数一起使用,使用中缀符号调用函数,需要使用**infix**修饰符标记。
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

可以直接用Pair的内容来初始化两个变量,称为解构声明。解构声明还可以使用map的key和value内容来初始化两个变量。也适用于循环,如: withIndex

 val (number, name) = 1 to "one"


 val list = listOf("abc", "def", "ker")
 for ((index, element) in list.withIndex()) {
     println("$index : $element")
 }

5.字符串和正则表达式的处理

字符串模板

kotlin可以在字符串字面值中引用局部变量,只需要在变量名称前面加上字符$

   val x = 12
   println("x = $x") // x = 12

   // 如果对它转义,不会把x解析成变量的引用 
   println("x = \$x") //x = $x

   // 如果引用复杂的表达式,需要把表达式用花括号括起来
    val x= 1
    val y = 2
    println("x + y = ${x + y}")

分割字符串

// 在java中,期望得到[12, 345-6, A],但是返回是一个空数组,因为点号(.)表示任何字符的正则表达式
 System.out.println(Arrays.toString("12.345-6.A".split("."))); // []

// 在kotlin中,可以显示的创建一个正则表达式分割字符串
// 需要转义表示字面量,而不是通配符
// 使用扩展函数toRegex将字符串转换为正则表达式
println("12.345-6.A".split("\\.".toRegex()))  // [12, 345-6, A]

 // kotlin中的split扩展函数的其他重载支持任意数量的纯文本字符串分隔符;
 // 指定多个分隔符
 println("12.345-6.A".split(".", "-"))  // [12, 345, 6, A]

正则表达式和三重引号字符串

使用String的扩展函数来解析文件路径。

fun parsePath(path: String) {
    val directory = path.substringBeforeLast("/")

    val fullName = path.substringAfterLast("/")

    val fileName = fullName.substringBeforeLast(".")

    val extension = fullName.substringAfterLast(".")
    println("Dir: $directory, fileName: $fileName, ext: $extension")

    //Dir: /User/kerwin/book, fileName: readme, ext: md
}

>>> parsePath("/User/kerwin/book/readme.md")

使用正则表达式解析文件路径

fun parsePath(path: String) {
    // 正则表达式写在一个 三重引号的字符串中,不需要对任何字符进行转义,包括反斜线,所以可以用\.而不是\\.表示点
    // (.+) 目录,/ 最后一个斜线,(.+) 文件名,\. 最后一个点,(.+) 扩展名
    val regex = """(.+)/(.+)\.(.+)""".toRegex()
    val matchResult = regex.matchEntire(path)
    if (matchResult != null) {
        val (directory, fileName, extension) = matchResult.destructured
        println("Dir: $directory, fileName: $fileName, ext: $extension")
    }
}

多行三重引号的字符串

可以避免转义字符,且可以包含任何字符,包括换行符,用于格式化代码的缩进。

    val string = """| //
                   .|//
                   .|/ \ """
// 可以去掉缩进
// 先向字符串内容添加前缀,标记边距的结尾,然后调用trimMargin来删除每行中的前缀和前面的空格
>>> println(string.trimMargin("."))
| //
|//
|/ \ 

6. 局部函数和扩展

带重复代码的函数

fun saveUser(user: User) {
    if (user.name.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${user.id}, name is empty.")
    }

    if (user.address.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${user.id}, address is empty.")
    }

    // save user ...
}

提取局部函数来避免重复,在布局函数中可以访问外层函数的参数

fun saveUser(user: User) {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user ${user.id}, $fieldName is empty.")
        }
    }

    validate(user.name, "name")
    validate(user.address, "address")

    // save user ...
}

提取逻辑到扩展函数

class User(val id: Int, val name: String, val address: String)

fun User.validateBeforeSave(value: String, fieldName: String) {
    if (value.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${this.id}, $fieldName is empty.")
    }
}

fun saveUser(user: User) {
    user.validateBeforeSave(user.name, "name")
    user.validateBeforeSave(user.address, "address")
    // save user ...
}

如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)


小兵兵同学
56 声望23 粉丝

Android技术分享平台,每个工作日都有优质技术文章分享。从技术角度,分享生活工作的点滴。