目录

null

 可空性

 安全调用操作符"?."

安全操作符“let”

安全操作调用符"!!."

安全调用操作符 VS if

空合并操作符

异常

先决条件函数

字符串操作

substring

 split

replace

字符串比较

foreach遍历字符

数字类型 

 安全转换函数

Double类型格式化

标准库函数

apply

let

run

with

also

takeIf

takeUnless


null

在Java中 NullPointerException对于我们开发者已经司空见惯,带给我们太多不必要的麻烦,Kotlin对此做了改良,Kotlin更多地把运行时可能会出现 null问题,以编译时报错的方式,提前在编译期强迫我们重视起来,而不是等到运行时报错,防患于未然,提高了程序的健壮性。

对于 null值问题,Kotlin反其道而行之,除非另有规定,否则不允许变量为 null,这样一来,因为 null问题导致的运行时崩溃就从根源上得到了解决。

 如果我们给变量赋值为 null,编译器会报红提示我们修改代码。Kotlin学习笔记(三)_kotlin

 可空性

为了避免空指针异常,Kotlin的做法是不让我们给非空类型变量赋空值,但 null在Kotlin中依然存在着。

fun main() {
    // ? 在这里的意义就是表明声明的 str是可空字符串
    var str: String? = "honey"
    str = null
    println("input:$str")
}

Kotlin区分可空类型和非可空类型,所以要运行可空类型变量,而它又可能为 null值,对于这种潜在风险,编译器时刻警惕着。为了应对这种风险,Kotlin不允许我们使用可空类型变量调用函数,除非我们主动接手安全管理。
Kotlin学习笔记(三)_android_02

 安全调用操作符"?."

"?."会告诉编译器,如果是 null值,就跳过函数调用,而不是返回 null。

    str = null
    val newStr = str?.capitalize()
    println(newStr)

从下面的示例中,我们确实看到了当是null值是,会跳过函数调用:

fun main() {
    val str = null
    println(str?.capitalized())
}

private fun String.capitalized(): String {
    println("我被调用了")
    return if (isNotEmpty() && this[0].isLowerCase()) substring(0, 1).toUpperCase() + substring(1) else "String is null"
}

安全操作符“let”

安全调用允许我们使用可空类型值调用函数,但是我们还想做点额外的事情,比如变量为空白字符串时赋新值,或者不为 空白字符串时调用其他函数,此时 let就该出场表演了。

fun main() {
    var str: String? = "honey"
    //var str: String? = null  //为 null时安全调用let函数会直接跳过
    //str = ""

    str = str?.let {
        if (it.isNotBlank()) {
            it.capitalize()
        } else {
            "str is blank"
        }
    }

    println("str = $str")
}

安全操作调用符"!!."

"!!."是非空断言操作符,也叫感叹号操作符,当调用者为 null时,会抛出 KotlinNullPointerException。

fun main() {
    var str: String? = ""
    str = null
    str!!.capitalize()
}

上面的代码会跑出空指针异常: 

Kotlin学习笔记(三)_kotlin_03

安全调用操作符 VS if

我们也可以跟Java中一样,使用 if判断 null值情况,但相比之下,安全调用操作符使用更灵活,代码更简洁,我们可以使用安全操作符进行多个函数的链式调用。

fun main() {
    var str: String? = "luffy"
    //str = null

    //使用 if判断
    if(str  != null) str = str.capitalize() else str = "str is null"
    println("str = $str")
    //使用安全调用操作符链式调用
    println(str?.capitalize().plus(" is great."))
}

空合并操作符

"?:"操作符的作用是,如果它左边的表达式求值结果为 null ,就使用右边的结果值,否则用左边的结果值。

fun main() {
    var str: String? = "luffy"
    str = null
    println(str ?: "honey")
}

空合并操作符也可以和 let函数结合使用,代替if/else:

fun main() {
    var str: String? = "luffy"
    str = null

    str = str?.let { it.capitalize() } ?: "Honey"
    println(str)
}
异常
import kotlin.IllegalArgumentException

fun main() {
    var str: String? = null
    try {
        checkOperation(str)
        str!!.capitalize()
    } catch (e: KotlinNullPointerException) {
        println(e.cause?.message)
    }
}

fun checkOperation(str: String?) {
    str ?: throw UnskilledException()
}

/**
 * 自定义异常
 * PS:Kotlin中的 IllegalArgumentException是 java.lang包下的 IllegalArgumentException的别名
 * @SinceKotlin("1.1") public actual typealias IllegalArgumentException = java.lang.IllegalArgumentException
 */
class UnskilledException() : IllegalArgumentException("Improper operation")

先决条件函数

Kotlin标准库提供了一些便利函数,使用这些内置函数,可以抛出带自定义信息的异常,这些便利函数 叫做先决条件函数,我们可以用它定义先决条件,条件必须满足,目标代码才能执行。

Kotlin学习笔记(三)_kotlin_04

fun main() {
    var num: Int? = null
    try {
        checkNotNull(num){"Improper operation"}
        num!!.plus(1)
    } catch (e: KotlinNullPointerException) {
        println(e.cause?.message)
    }
}
字符串操作

substring

字符串截取,substring函数支持IntRang类型(表示一个整数范围的类型)的参数,until创建的范围不包括上限值

const val WORDS = "Luffy's friend"
fun main() {
    val index = WORDS.indexOf('\'')
    var str1 = WORDS.substring(0, index)
    //Kotlin中,substring函数支持IntRang类型
    var str2 = WORDS.substring(0 until index)

    println("str1 = $str1")
    println("str2 = $str2")
}

 split

split函数返回的是List集合数据,List集合有支持解构语法特性,它允许我们在一个表达式里给多个变量赋值,解构常用来简化变量的赋值。
 

const val NAMES = "Luffy,Honey,Kitty"
fun main() {
    val names: List<String> = NAMES.split(',')
    println(names[0])
    //解构语法特性
    val (origin, dest, proxy) = NAMES.split(',')
    println("$origin,$dest,$proxy")
}

replace

fun main() {
    //加密替换一个字符串
    val str1 = "The people's Republic of China."
    //第一个参数是正则表达式,用来指定要替换哪些字符
    //第二个参数是匿名函数,用来决定当匹配到正则中的字符时,该怎么处理
    val str2 = str1.replace(Regex("[aoeiu]")) {
        when (it.value) {
            "a" -> "8"
            "o" -> "6"
            "e" -> "5"
            "i" -> "2"
            "u" -> "0"
            else -> it.value
        }
    }
    println("str1 = $str1")
    println("str2 = $str2")
}

字符串比较

在Kotlin中,用"=="检查两个字符串中的字符是否匹配,用"==="检查两个变量是否指向内存堆上同一对象,而在java中,"=="用作引用的比较
,做内容比较时用equals方法。 

fun main() {
    val name1 = "Honey"
    val name2 = "Honey"
    println("name1 == name2 : ${name1 == name2}")
    println("name1 === name2 : ${name1 === name2}")

    val name3 = "Luffy"
    val name4 = "luffy".capitalize()
    println("name3 == name4 : ${name3 == name4}")
    println("name3 === name4 : ${name3 === name4}")
}

执行结果:
Kotlin学习笔记(三)_kotlin_05

 上面示例代码执行结果有没有疑问,这个就涉及到Java的内存模型了,字符串是放在常量池中的,因为字符串也是常量,所以示例代码中的name1和name2都指向了同一字符串对象“Honey”,所以两个输出都是true;而“Luffy”和“luffy”是两个不同的字符串常量,虽然"luffy".capitalize()的执行结果也是“Luffy”,但是这期间生成了新的字符串对象:
Kotlin学习笔记(三)_java_06

 看下 capitalize()的源码就明白了,这里进行了字符串的拼接,所以生成了新的字符串对象,所以name3和name4虽然内容一样,但是指向了不同的对象。

foreach遍历字符

fun main() {
    val saying = "For you, a thousand times over."
    println("* * *")
    saying.forEach { println("* $it *") }
    println("* * *")
}
数字类型 

和java一样,Kotlin的所有数字类型都是有符号的,即可以表示正数,也可以表示负数。

Kotlin学习笔记(三)_java_07

 安全转换函数

Kotlin提供了 toDoubleOrNull和 toIntOrNull这样的安全转换函数,如果数值不能正确转换,与其触发异常不如干脆返回null值。

import kotlin.math.roundToInt

/**
 * Double 转 Int
 */
fun main() {
    //val result = "66.88".toInt()//抛出 java.lang.NumberFormatException
    val result1 = "66.88".toIntOrNull()
    println("result1:$result1")

    val result2 = "66.88".toDouble()
    println("result2:$result2")

    //精度损失
    val result4 = 98.66.toInt()
    println("result4:$result4")

    //四舍五入
    val result5 = 98.66.roundToInt()
    println("result5:$result5")
}

Double类型格式化

格式化字符串是一串特殊的字符,它绝对如何格式化数据。

fun main() {
    val result = "%.2f".format(5.20522)
    println("result:$result")
}

注意到上述代码输出结果是:5.21,说明格式化之后的Double数据会四舍五入 。

标准库函数

apply

apply函数可以看作是一个配置函数,我们可以传入一个接收者,然后调用一系列函数来配置它,如果提供lambda给apply执行,它会返回配置好的接收者。

import java.io.File

fun main() {
    val file = File("D://honey.txt")
    file.setReadable(true)
    file.setWritable(true)
    file.setExecutable(true)
    println(file)

    //使用apply
    val file2 = File("D://honey.txt").apply {
        setWritable(true)
        setExecutable(true)
        setReadable(true)
    }
    println(file)
}

从上面的示例代码中,我们可以看到调用一个个函数配置接收者时,变量名就省掉了,这是因为在lambda表达式中,apply能让每个配置函数都作用于接收者,这种行为有时又叫做相关作用域,因为lambda表达式中的所有函数调用都是针对接收者的,或者说他们是针对接收者的隐式调用。

let

let函数可以把变量或表达式传给lambda,用 it关键字可以指代变量或表达式,与 apply相比,let函数返回的是最后一行代码的执行结果,而apply返回的是当前接收者。

fun main() {
    //案例一:求集合中第一个元素的平方
    //使用 let函数
    val result = listOf(5, 2, 0).first().let { it * it }
    println("result = $result")

    //如果不用 let函数
    val firstElement = listOf(5, 2, 0).first()
    val result2 = firstElement * firstElement
    println("result2 = $result2")

    println(formatGreeting("Luffy"))
    println(formatGreeting2(null))

}

//案例二:欢迎或问候
//使用 let函数
fun formatGreeting(guestName: String?): String {
    return guestName?.let { "Welcome to Shanghai,$guestName" } ?: "What's your name?"
}

//不使用 let函数
fun formatGreeting2(guestName: String?): String {
    return if (guestName != null && guestName.isNotBlank()) "Welcome to Shanghai,$guestName" else "What's your name?"
}

run

光看作用域行为,run和apply差不错,但与apply不同的是,run返回的是lambda结果,而apply是返回配置好的接收者。

import java.io.File

fun main() {
    val result = "I have a dream.".run {
        length > 10
    }
    println("result:$result")
    //查看某个文件是否包含某一个字符串
    val contained = File("D:" + File.separator + "honey.txt").run {
        readText().contains("honey")
    }
    println("contained:$contained")
}

run函数也支持函数引用

/**
 * run 也可以执行函数引用
 */
fun main() {
    val result = "We laughed and kept saying \"see u soon\", but inside we both knew we'd never see each other again.".run(::isLong)
    println("result = $result")

    //当有多个函数调用时,run的优势就显而易见了
    "We laughed and kept saying \"see u soon\", but inside we both knew we'd never see each other again."
            .run(::isLong)//会把isLong的执行结果传递给下面的showMsg函数调用
            .run(::showMsg)//会把showMsg的执行结果传递给下面的println函数调用
            .run(::println)
}

fun isLong(str: String) = str.length > 100

fun showMsg(isLong: Boolean): String {
    return if (isLong) "Words is too lang"
    else "We laughed and kept saying \"see u soon\", but inside we both knew we'd never see each other again."
}

with

with函数是run的变体,他们的功能行为是一样,但with的调用方式不同,调用with时需要值参作为其第一个参数传入

fun main() {
    val isLong = with("The course of true love never did run smooth.") {
        length > 100
    }
    println(isLong)
}

also

also函数和let函数功能相似,also也是把接收者作为值参传给lambda,但有一点不同,also返回接收者对象,而let是返回lambda结果。因为这个差异,also尤其适合针对同一原始对象,利用副作用做事,既然also返回的是接收者对象,我们就可以基于原始接收者对象执行额外的链式调用。

import java.io.File

fun main() {
    var fileContents: List<String>
    File("D:" + File.separator + "honey.txt")
        .also { println(it.name) }
        .also { fileContents = it.readLines() }
    println("fileContents == $fileContents")
}

takeIf

takeIf函数需要判断lambda中提供的条件表达式,给出true或false的结果,如果是true,从takeIf函数返回接收者对象,如果是false则返回null。当我们需要判断某个条件是否满足,再决定是否可以赋值变量或执行某项任务,takeIf就非常有用,概念上讲,takeIf函数类似于if,但它的优势是可以直接在对象实例上调用,避免临时变量赋值的麻烦。

import java.io.File

fun main() {
    val words = File("D:" + File.separator + "honey.txt")
            .takeIf { it.exists() && it.canRead() }
            ?.readText()
    println("words === $words")

    //不用takeIf函数
    val file = File("D:" + File.separator + "honey.txt")
    val words2 = if (file.exists() && file.canRead()) {
        file.readText()
    } else {
        null
    }
    println("words2 === $words2")
}

takeUnless

takeUnless是takeIf的辅助函数,与takeIf功能正好相反,只有判断给定的条件是false时,才会返回原始的接收者对象。

import java.io.File

fun main() {
    val words = File("D:" + File.separator + "honey.txt")
            .takeUnless { !it.exists() && it.isHidden }
            ?.readText()
    println("words == $words")
}