目录
null
在Java中 NullPointerException对于我们开发者已经司空见惯,带给我们太多不必要的麻烦,Kotlin对此做了改良,Kotlin更多地把运行时可能会出现 null问题,以编译时报错的方式,提前在编译期强迫我们重视起来,而不是等到运行时报错,防患于未然,提高了程序的健壮性。
对于 null值问题,Kotlin反其道而行之,除非另有规定,否则不允许变量为 null,这样一来,因为 null问题导致的运行时崩溃就从根源上得到了解决。
如果我们给变量赋值为 null,编译器会报红提示我们修改代码。
可空性
为了避免空指针异常,Kotlin的做法是不让我们给非空类型变量赋空值,但 null在Kotlin中依然存在着。
fun main() {
// ? 在这里的意义就是表明声明的 str是可空字符串
var str: String? = "honey"
str = null
println("input:$str")
}
Kotlin区分可空类型和非可空类型,所以要运行可空类型变量,而它又可能为 null值,对于这种潜在风险,编译器时刻警惕着。为了应对这种风险,Kotlin不允许我们使用可空类型变量调用函数,除非我们主动接手安全管理。
安全调用操作符"?."
"?."会告诉编译器,如果是 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()
}
上面的代码会跑出空指针异常:
安全调用操作符 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标准库提供了一些便利函数,使用这些内置函数,可以抛出带自定义信息的异常,这些便利函数 叫做先决条件函数,我们可以用它定义先决条件,条件必须满足,目标代码才能执行。
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}")
}
执行结果:
上面示例代码执行结果有没有疑问,这个就涉及到Java的内存模型了,字符串是放在常量池中的,因为字符串也是常量,所以示例代码中的name1和name2都指向了同一字符串对象“Honey”,所以两个输出都是true;而“Luffy”和“luffy”是两个不同的字符串常量,虽然"luffy".capitalize()的执行结果也是“Luffy”,但是这期间生成了新的字符串对象:
看下 capitalize()的源码就明白了,这里进行了字符串的拼接,所以生成了新的字符串对象,所以name3和name4虽然内容一样,但是指向了不同的对象。
foreach遍历字符
fun main() {
val saying = "For you, a thousand times over."
println("* * *")
saying.forEach { println("* $it *") }
println("* * *")
}
数字类型
和java一样,Kotlin的所有数字类型都是有符号的,即可以表示正数,也可以表示负数。
安全转换函数
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")
}