什么是Kotlin?
Kotlin 是 JVM 和 Android 的实用编程语言,结合了OO和功能特性,专注于互操作性
,安全性
,清晰度
和工具
支持。
作为通用语言,Kotlin 可以在Java工作的地方工作:服务器端应用程序
,移动应用程序(Android)
,桌面应用程序
。它适用于所有主要的工具和服务,如
- IntelliJ IDEA,Android Studio 和 Eclipse
- Maven,Gradle 和 Ant
- Spring Boot
- GitHub,Slack 甚至 Minecraft
Kotlin的关键重点之一是混合 Java + Kotlin 项目的互操作性和无缝支持,使采用更容易,从而减少了样板代码和更多的类型安全性。此外,Kotlin有一个广泛的标准库,使日常任务轻松流畅,同时保持字节码足迹低。当然,也可以在Kotlin中使用任何Java库。反之亦然。
基本语法
定义包名
在源文件的开头定义包名
package my.demo
import java.util.*
//...
复制代码
包名不必和文件夹路径一致:源文件可以放在任意位置。
定义函数
定义一个函数接受两个 int 型参数,返回值为 int:
fun sum(a: Int , b: Int): Int {
return a + b
}
fun main(args: Array<String>) {
print("sum of 3 and 5 is ")
println(sum(3, 5))
}
复制代码
该函数只有一个表达式函数体以及一个自推导型的返回值:
fun sum(a: Int, b: Int) = a + b
fun main(args: Array<String>) {
println("sum of 19 and 23 is ${sum(19, 23)}")
}
复制代码
返回一个没有意义的值:
fun printSum(a: Int, b: Int): Unit {
println("sum of $a and $b is ${a + b}")
}
fun main(args: Array<String>) {
printSum(-1, 8)
}
复制代码
Unit 的返回类型可以省略:
fun printSum(a: Int, b: Int) {
println("sum of $a and $b is ${a + b}")
}
fun main(args: Array<String>) {
printSum(-1, 8)
}
复制代码
定义局部变量
声明常量:
fun main(args: Array<String>) {
val a: Int = 1 // 立即初始化
val b = 2 // 推导出Int型
val c: Int // 当没有初始化值时必须声明类型
c = 3 // 赋值
println("a = $a, b = $b, c = $c")
}
复制代码
变量:
fun main(args: Array<String>) {
var x = 5 // 推导出Int类型
x += 1
println("x = $x")
}
复制代码
注释
与 java 和 javaScript 一样,Kotlin 支持单行注释和块注释。
// 单行注释
/* 哈哈哈哈
这是块注释 */
复制代码
与 java 不同的是 Kotlin 的 块注释可以级联。
使用字符串模板
fun main(args: Array<String>) {
var a = 1
// 使用变量名作为模板:
val s1 = "a is $a"
a = 2
// 使用表达式作为模板:
val s2 = "${s1.replace("is", "was")}, but now is $a"
println(s2)
}
复制代码
使用条件表达式
fun maxOf(a: Int, b: Int): Int {
if (a > b) {
return a
} else {
return b
}
}
fun main(args: Array<String>) {
println("max of 0 and 42 is ${maxOf(0, 42)}")
}
复制代码
把if当表达式:
fun maxOf(a: Int, b: Int) = if (a > b) a else b
fun main(args: Array<String>) {
println("max of 0 and 42 is ${maxOf(0, 42)}")
}
复制代码
使用可空变量以及空值检查
当空值可能出现时应该明确指出该引用可空。
下面的函数是当 str 中不包含整数时返回空:
fun parseInt(str : String): Int?{
//...
}
复制代码
使用一个返回可空值的函数:
fun parseInt(str: String): Int? {
return str.toIntOrNull()
}
fun printProduct(arg1: String, arg2: String) {
val x = parseInt(arg1)
val y = parseInt(arg2)
// 直接使用 x*y 会产生错误因为它们中有可能会有空值
if (x != null && y != null) {
// x 和 y 将会在空值检测后自动转换为非空值
println(x * y)
}
else {
println("either '$arg1' or '$arg2' is not a number")
}
}
fun main(args: Array<String>) {
printProduct("6", "7")
printProduct("a", "7")
printProduct("a", "b")
}
复制代码
或者这样
fun parseInt(str: String): Int? {
return str.toIntOrNull()
}
fun printProduct(arg1: String, arg2: String) {
val x = parseInt(arg1)
val y = parseInt(arg2)
// ...
if (x == null) {
println("Wrong number format in arg1: '${arg1}'")
return
}
if (y == null) {
println("Wrong number format in arg2: '${arg2}'")
return
}
// x 和 y 将会在空值检测后自动转换为非空值
println(x * y)
}
复制代码
使用值检查并自动转换
使用 is
操作符检查一个表达式是否是某个类型的实例。如果对不可变的局部变量或属性进行过了类型检查,就没有必要明确转换:
fun getStringLength(obj: Any): Int? {
if (obj is String) {
// obj 将会在这个分支中自动转换为 String 类型
return obj.length
}
// obj 在种类检查外仍然是 Any 类型
return null
}
fun main(args: Array<String>) {
fun printLength(obj: Any) {
println("'$obj' string length is ${getStringLength(obj) ?: "... err, not a string"} ")
}
printLength("Incomprehensibilities")
printLength(1000)
printLength(listOf(Any()))
}
复制代码
或者这样
fun getStringLength(obj: Any): Int? {
if (obj !is String) return null
// obj 将会在这个分支中自动转换为 String 类型
return obj.length
}
fun main(args: Array<String>) {
fun printLength(obj: Any) {
println("'$obj' string length is ${getStringLength(obj) ?: "... err, not a string"} ")
}
printLength("Incomprehensibilities")
printLength(1000)
printLength(listOf(Any()))
}
复制代码
甚至可以这样
fun getStringLength(obj: Any): Int? {
// obj 将会在&&右边自动转换为 String 类型
if (obj is String && obj.length >= 0) {
return obj.length
}
return null
}
fun main(args: Array<String>) {
fun printLength(obj: Any) {
println("'$obj' string length is ${getStringLength(obj) ?: "... err, is empty or not a string at all"} ")
}
printLength("Incomprehensibilities")
printLength("")
printLength(1000)
}
复制代码
使用循环
fun main(args: Array<String>) {
val items = listOf("apple", "banana", "kiwi")
for (item in items) {
println(item)
}
}
复制代码
或者
fun main(args: Array<String>) {
val items = listOf("apple", "banana", "kiwi")
for (index in items.indices) {
println("item at $index is ${items[index]}")
}
}
复制代码
使用 while 循环
fun main(args: Array<String>) {
val items = listOf("apple", "banana", "kiwi")
var index = 0
while (index < items.size) {
println("item at $index is ${items[index]}")
index++
}
}
复制代码
使用 when 表达式
fun describe(obj: Any): String =
when (obj) {
1 -> "One"
"Hello" -> "Greeting"
is Long -> "Long"
!is String -> "Not a string"
else -> "Unknown"
}
fun main(args: Array<String>) {
println(describe(1))
println(describe("Hello"))
println(describe(1000L))
println(describe(2))
println(describe("other"))
}
复制代码
使用 ranges
检查 in
操作符检查数值是否在某个范围内:
fun main(args: Array<String>) {
val x = 10
val y = 9
if (x in 1..y+1) {
println("fits in range")
}
}
复制代码
检查数值是否在范围外:
fun main(args: Array<String>) {
val list = listOf("a", "b", "c")
if (-1 !in 0..list.lastIndex) {
println("-1 is out of range")
}
if (list.size !in list.indices) {
println("list size is out of valid list indices range too")
}
}
复制代码
在范围内迭代:
fun main(args: Array<String>) {
for (x in 1..5) {
print(x)
}
}
复制代码
或者使用步进:
fun main(args: Array<String>) {
for (x in 1..10 step 2) {
print(x)
}
// 类似于java i--
for (x in 9 downTo 0 step 3) {
print(x)
}
}
复制代码
使用集合
对一个集合进行迭代:
fun main(args: Array<String>) {
val items = listOf("apple", "banana", "kiwi")
for (item in items) {
println(item)
}
}
复制代码
使用 in
操作符检查集合中是否包含某个对象
fun main(args: Array<String>) {
val items = setOf("apple", "banana", "kiwi")
when {
"orange" in items -> println("juicy")
"apple" in items -> println("apple is fine too")
}
}
复制代码
使用lambda表达式过滤和映射集合:
fun main(args: Array<String>) {
val fruits = listOf("banana", "avocado", "apple", "kiwi")
fruits
.filter { it.startsWith("a") }
.sortedBy { it }
.map { it.toUpperCase() }
.forEach { println(it) }
}
复制代码
习惯用语
创建DTOs(POJOs/POCOs) 数据类
data class Customer(val name: String, val email: String)
复制代码
函数默认值
fun foo(a: Int = 0, b: String = "") {...}
复制代码
过滤 list
val positives = list.filter { x -> x > 0 }
复制代码
或者更短:
val positives = list.filter { it > 0 }
复制代码
字符串插值
println("Name $name")
复制代码
实例检查
when (x) {
is Foo -> ...
is Bar -> ...
else -> ...
}
复制代码
遍历 map/list
for ((k, v) in map) {
print("$k -> $v")
}
复制代码
k,v 可以随便命名
使用 ranges
for (i in 1..100) { ... } // 闭区间: 包括100
for (i in 1 until 100) { ... } // 半开区间: 不包括100
for (x in 2..10 step 2) { ... }
for (x in 10 downTo 1) { ... }
if (x in 1..10) { ... }
for (i in 1..100) { ... }
for (i in 2..10) { ... }
复制代码
只读 list
val list = listOf("a", "b", "c")
复制代码
只读map
val map = mapOf("a" to 1, "b" to 2, "c" to 3)
复制代码
访问 map
println(map["key"])
map["key"] = value
复制代码
懒属性(延迟加载)
val p: String by lazy {
// 生成string的值
}
复制代码
扩展函数
fun String.spcaceToCamelCase() { ... }
"Convert this to camelcase".spcaceToCamelCase()
复制代码
创建单例模式
object Resource {
val name = "Name"
}
复制代码
如果不为空则... 的简写
val files = File("Test").listFiles()
println(files?.size)
复制代码
如果不为空...否则... 的简写
val files = File("test").listFiles()
println(files?.size ?: "empty")
复制代码
如果声明为空执行某操作
val data = ...
val email = data["email"] ?: throw IllegalStateException("Email is missing!")
复制代码
如果不为空执行某操作
val date = ...
data?.let{
...//如果不为空执行该语句块
}
复制代码
返回 when 判断
fun transform(color: String): Int {
return when(color) {
"Red" -> 0
"Green" -> 1
"Blue" -> 2
else -> throw IllegalArgumentException("Invalid color param value")
}
}
复制代码
try-catch 表达式
fun test() {
val result = try {
count()
}catch (e: ArithmeticException) {
throw IllegaStateException(e)
}
//处理 result
}
复制代码
if 表达式
fun foo(param: Int){
val result = if (param == 1) {
"one"
} else if (param == 2) {
"two"
} else {
"three"
}
}
复制代码
方法使用生成器模式返回 Unit
fun arrOfMinusOnes(size: Int): IntArray{
return IntArray(size).apply{ fill(-1) }
}
复制代码
只有一个表达式的函数
fun theAnswer() = 42
复制代码
与下面的语句是等效的
fun theAnswer(): Int {
return 42
}
复制代码
这个可以和其它习惯用语组合成高效简洁的代码。譬如说 when 表达式:
fun transform(color: String): Int = when (color) {
"Red" -> 0
"Green" -> 1
"Blue" -> 2
else -> throw IllegalArgumentException("Invalid color param value")
}
复制代码
利用 with 调用一个对象实例的多个方法
class Turtle {
fun penDown()
fun penUp()
fun turn(degrees: Double)
fun forward(pixels: Double)
}
val myTurtle = Turtle()
with(myTurtle) { // 画一个100像素的正方形
penDown()
for(i in 1..4) {
forward(100.0)
turn(90.0)
}
penUp()
}
复制代码
Java 7’s try with resources
val stream = Files.newInputStream(Paths.get("/some/file.txt"))
stream.buffered().reader().use { reader ->
println(reader.readText())
}
复制代码
需要泛型信息的泛型函数的方便形式
// public final class Gson {
// ...
// public <T> T fromJson(JsonElement json, Class<T> classOfT) throws JsonSyntaxException {
// ...
inline fun <reified T: Any> Gson.fromJson(json): T = this.fromJson(json, T::class.java)
复制代码
产生一个可能为空的布尔值
val b: Boolean? = ...
if (b == true) {
...
} else {
// `b` 是false或者null
}
复制代码
代码风格
命名风格
如有疑惑,默认为Java编码约定,比如:
-- 使用骆驼命名法(在命名中避免下划线)
-- 类型名称首字母大写
-- 方法和属性首字母小写
-- 缩进用四个空格
-- public 方法要写说明文档,这样它就可以出现在 Kotllin Doc 中
冒号
在冒号区分类型和父类型中要有空格,在实例和类型之间是没有空格的:
interface Foo : Bar { fun foo(a: Int): T }
Lambdas
在 Lambdas 表达式中,大括号与表达式间要有空格,箭头与参数和函数体间要有空格。尽可能的把 lambda 放在括号外面传入
list.filter { it > 10 }.map { element -> element * 2 }
复制代码
在使用简短而非嵌套的lambda中,建议使用it
而不是显式地声明参数。在使用参数的嵌套lambda中,参数应该总是显式声明
类声明格式
参数比较少的类可以用一行表示:
class Person(id: Int, name: String)
复制代码
具有较多的参数的类应该格式化成每个构造函数的参数都位于与缩进的单独行中。此外,结束括号应该在新行上。如果我们使用继承,那么超类构造函数调用或实现的接口列表应该位于与括号相同的行中
class Person(
id: Int,
name: String,
surname: String
) : Human(id, name) {
// ...
}
复制代码
对于多个接口,应该首先定位超类构造函数调用,然后每个接口应该位于不同的行中
class Person(
id: Int,
name: String,
surname: String
) : Human(id, name),
KotlinMaker {
// ...
}
复制代码
构造函数参数可以使用常规缩进或连续缩进(双倍正常缩进)。
Unit
如果函数返回 Unit ,返回类型应该省略:
fun foo() { // ": Unit"被省略了
}
复制代码
函数 vs 属性
在某些情况下,没有参数的函数可以与只读属性互换。尽管语义是相似的,但是有一些风格上的约定在什么时候更偏向于另一个。
在下面的情况下,更偏向于属性而不是一个函数:
-- 不需要抛出异常
-- 拥有O(1)复杂度
-- 低消耗的计算(或首次运行结果会被缓存)
-- 返回与调用相同的结果
基本类型
在 Kotlin 中,所有变量的成员方法和属性都是一个对象。一些类型是内建的,它们的实现是优化过的,但对用户来说它们就像普通的类一样。在这节中,我们将会讲到大多数的类型:数值,字符,布尔,以及数组。
数值
Kotlin 处理数值的方法和 java 很相似,但不是完全一样。比如,不存在隐式转换数值的精度,并且在字面上有一些小小的不同。
Kotlin 提供了如下内建数值类型(和 java 很相似):
类型 | 字宽 |
Double | 64 |
Float | 32 |
Long | 64 |
Int | 32 |
Short | 16 |
Byte | 8 |
注意字符在 Kotlin 中不是数值类型:
字面值常量
主要是以下几种字面值常量:
-- 数型:
123
-- 长整型要加大写L
:123L
-- 16进制:0x0f
-- 二进制:0b00001011
注意不支持8进制
Kotlin 也支持传统的浮点数表示:
-- 默认 Doubles :
123.5
,123.5e10
-- Floats 要添加f
或F
:123.5f
表示
在 java 平台上,数值被 JVM 虚拟机以字节码的方式物理存储的,除非我们需要做可空标识(比如说 Int?) 或者涉及泛型。在后者中数值是装箱的。
注意装箱过的数值是不保留特征的:
val a: Int = 10000
print (a === a ) //打印 'true'
val boxedA: Int? =a
val anotherBoxedA: Int? = a
print (boxedA === anotherBoxedA ) //注意这里打印的是 'false'
复制代码
另一方面,它们是值相等的:
val a: Int = 10000
print(a == a) // Prints 'true'
val boxedA: Int? = a
val anotherBoxedA: Int? = a
print(boxedA == anotherBoxedA) // Prints 'true'
复制代码
显式转换
由于不同的表示,短类型不是长类型的子类型。如果是的话我们就会碰到下面这样的麻烦了
//这是些伪代码,不能编译的
val a: Int? =1 //一个装箱过的 Int (java.lang.Integer)
val b: Long? = a // 一个隐式装箱的 Long (java.lang.Long)
print( a == b )// 很惊讶吧 这次打印出的是 'false' 这是由于 Long 类型的 equals() 只有和 Long 比较才会相同
复制代码
因此不止是恒等,有时候连等于都会悄悄丢失。
所以,短类型是不会隐式转换为长类型的。这意味着我们必须显式转换才能把 Byte 赋值给 Int
val b: Byte = 1 // OK, literals are checked statically
val i: Int = b //ERROR
复制代码
我们可以通过显式转换把数值类型提升
val i: Int = b.toInt() // 显式转换
复制代码
每个数值类型都支持下面的转换:
toByte(): Byte
toShort(): Short
toInt(): Int
toLong(): Long
toFloat(): Float
toDouble(): Double
toChar(): Char
隐式转换一般情况下是不容易被发觉的,因为我们可以使用上下文推断出类型,并且算术运算会为合适的转换进行重载,比如
val l = 1.toLong + 1 //Long + Int => Long
复制代码
运算符
Kotlin支持标准的算术运算表达式,这些运算符被声明为相应类的成员。参看运算符重载。
至于位运算,Kotlin 并没有提供特殊的操作符,只是提供了可以叫中缀形式的方法,比如:
val x = (1 shl 2) and 0x000FF000
复制代码
下面是全部的位运算操作符(只可以用在 Int
和 Long
类型):
shl(bits)
– 带符号左移 (相当于 Java's<<
)shr(bits)
– 带符号右移 (相当于 Java's'>>
)ushr(bits)
– 无符号右移 (相当于 Java's>>>
)and(bits)
– 按位与or(bits)
– 按位或xor(bits)
– 按位异或inv(bits)
– 按位翻转
字符
字符类型用 Char
表示。不能直接当做数值来使用
fun check(c: Char) {
if (c == 1) { //ERROR: 类型不匹配
//...
}
}
复制代码
字符是由单引号包裹的 '1',特殊的字符通过反斜杠\
转义,下面的字符序列支持转义:\t
,\b
,\n
,\r
,'
,"
,\
和$
。编码任何其他字符,使用 Unicode 转义语法:\uFF00
。
我们可以将字符显示的转义为Int数字:
fun decimalDigitValue(c: Char): Int {
if (c !in '0'..'9')
throw IllegalArgumentException("Out of range")
return c.toInt() - '0'.toInt() //显示转换为数值类型
}
复制代码
和数值类型一样,需要一个可空引用时,字符会被装箱。特性不会被装箱保留。
布尔值
布尔值只有 true
或者 false
布尔值的内建操作包括
|| – lazy disjunction && – lazy conjunction
Array
Arrays在 Kotlin 中由 Array
类表示,有 get
和 set
方法(通过运算符重载可以由[]调用),以及 size 方法,以及一些常用的函数:
class Array<T> private () {
fun size(): Int
fun get(index: Int): T
fun set(Index: Int, value: T): Unit
fun iterator(): Iterator<T>
//...
}
复制代码
我们可以给库函数 arrayOf()
传递每一项的值来创建Array,arrayOf(1, 2, 3)
创建了一个[1, 2, 3] 这样的数组。也可以使用库函数 arrayOfNulls()
创建一个指定大小的空Array。
或者通过指定Array大小并提供一个迭代器
(原文Another option is to use a factory function that takes the array size and the function that can return the initial value of each array element given its index):
// 创建一个 Array<String> 内容为 ["0", "1", "4", "9", "16"]
val asc = Array(5, {i -> (i * i).toString() })
复制代码
像我们上面提到的,[]
操作符表示调用 get()
set()
函数
注意:和 java 不一样,arrays 在 kotlin 中是不可变的。这意味这 kotlin 不允许我们把 Array<String>
转为 Array<Any>
,这样就阻止了可能的运行时错误(但你可以使用 Array<outAny>
, 参看 Type Projections)
Kotlin 有专门的类来表示原始类型从而避免过度装箱: ByteArray
, ShortArray
, IntArray
等等。这些类与 Array 没有继承关系,但它们有一样的方法与属性。每个都有对应的库函数:
val x: IntArray = intArrayOf(1, 2, 3)
x[0] = x[1] + x[2]
复制代码
字符串
字符串是由 String
表示的。字符串是不变的。字符串的元素可以通过索引操作读取: s[i]
。字符串可以用 for 循环迭代:
for (c in str) {
println(c)
}
复制代码
Kotlin 有两种类型的 String :一种是可以带分割符的,一种是可以包含新行以及任意文本的。带分割符的 String 很像 java 的 String:
val s = "Hello World!\n"
复制代码
整行String 是由三个引号包裹的("""
),不可以包含分割符但可以包含其它字符:
val text = """
for (c in "foo")
print(c)
"""
复制代码
注意:字符串比较
==
表示比较内容,相当于java的equals
===
表示比较对象是否相同
模板
字符串可以包含模板表达式。一个模板表达式由一个 $
开始并包含另一个简单的名称:
val i = 10
val s = "i = $i" // 识别为 "i = 10"
复制代码
或者是一个带大括号
的表达式:
val s = "abc"
val str = "$s.length is ${s.length}" //识别为 "abc.length is 3"
复制代码
Java Style 类型转换(as)
var sub: SubClass = parent as SubClass
复制代码
类似于 java 的类型转换,失败则抛异常。
安全类型转换
var sub: SubClass? = parent as? SubClass
复制代码
如果转换失败返回null,不抛异常。
智能类型转换
if(parent is SubClass) parent.<子类的成员>
- 编译器尽可能的推导类型,远离无用的类型转换
if(nullable != null) nullable.length
包
一个源文件以包声明开始:
package foo.bar
fun bza() {}
class Goo {}
//...
复制代码
源文件的所有内容(比如类和函数)都被包声明包括。因此在上面的例子中, bza()
的全名应该是 foo.bar.bza
,Goo
的全名是 foo.bar.Goo
。
如果没有指定包名,那这个文件的内容就从属于没有名字的 default
包。
Imports
除了模块中默认导入的包,每个文件都可以有它自己的导入指令。导入语法的声明在grammar中描述。
我们可以导入一个单独的名字,比如下面这样:
import foo.Bar //Bar 现在可以不用条件就可以使用
复制代码
或者范围内的所有可用的内容 (包,类,对象,等等):
import foo.*/ /foo 中的所有都可以使用
复制代码
如果命名有冲突,我们可以使用 as
关键字局部重命名解决冲突
import foo.Bar // Bar 可以使用
import bar.Bar as bBar // bBar 代表 'bar.Bar'
复制代码
可见性和包嵌套
如果最顶的声明标注为 private
, 那么它是自己对应包私有。如果包内有私有的属性或方法,那它对所有的子包是可见的。
注意包外的的成员是默认不导入的,比如在导入 foo.bar 后我们不能获得 foo 的成员
流程控制
if 表达式
在 Kotlin 中,if 是表达式,比如它可以返回一个值。是除了condition ? then : else)之外的唯一一个三元表达式
//传统用法
var max = a
if (a < b)
max = b
//带 else
var max: Int
if (a > b)
max = a
else
max = b
//作为表达式
val max = if (a > b) a else b
复制代码
if 分支可以作为块,最后一个表达是是该块的值:
val max = if (a > b){
print("Choose a")
a
}
else{
print("Choose b")
b
}
复制代码
如果 if 表达式只有一个分支,或者分支的结果是 Unit , 它的值就是 Unit 。
When 表达式
when 取代了 C 风格语言的 switch 。最简单的用法像下面这样
when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> { //Note the block
print("x is neither 1 nor 2")
}
}
复制代码
when会对所有的分支进行检查直到有一个条件满足。when 可以用做表达式或声明。如果用作表达式的话,那么满足条件的分支就是总表达式。如果用做声明,那么分支的值会被忽略。(像 if 表达式一样,每个分支是一个语句块,而且它的值就是最后一个表达式的值)
在其它分支都不匹配的时候默认匹配 else 分支。如果把 when 做为表达式的话 else 分支是强制的,除非编译器可以提供所有覆盖所有可能的分支条件。
如果有分支可以用同样的方式处理的话,分支条件可以连在一起:
when (x) {
0,1 -> print("x == 0 or x == 1")
else -> print("otherwise")
}
复制代码
可以用任意表达式作为分支的条件
when (x) {
parseInt(s) -> print("s encode x")
else -> print("s does not encode x")
}
复制代码
甚至可以用 in
或者 !in
检查值是否值在一个集合中:
when (x) {
in 1..10 -> print("x is in the range")
in validNumbers -> print("x is valid")
!in 10..20 -> print("x is outside the range")
else -> print("none of the above")
}
复制代码
也可以用 is
或者 !is
来判断值是否是某个类型。注意,由于 smart casts ,你可以不用另外的检查就可以使用相应的属性或方法。
val hasPrefix = when (x) {
is String -> x.startsWith("prefix")
else -> false
}
复制代码
when 也可以用来代替 if-else if
。如果没有任何参数提供,那么分支的条件就是简单的布尔表达式,当条件为真时执行相应的分支:
when {
x.isOdd() -> print("x is odd")
x.isEven() -> print("x is even")
else -> print("x is funny")
}
复制代码
for 循环
for 循环通过任何提供的迭代器进行迭代。语法是下面这样的:
for (item in collection)
print(item)
复制代码
内容可以是一个语句块
for (item: Int in ints){
//...
}
复制代码
像之前提到的, for 可以对任何提供的迭代器进行迭代,比如:
has an instance- or extension-function iterator(), whose return type
has an instance- or extension-function next(), and
has an instance- or extension-function hasNext() that returns Boolean.
如果你想通过 list 或者 array 的索引进行迭代,你可以这样做:
for (i in array.indices)
print(array[i])
复制代码
在没有其它对象创建的时候 "iteration through a range " 会被自动编译成最优的实现。
while 循环
while 和 do...while 像往常那样
while (x > 0) {
x--
}
do {
val y = retrieveData()
} while (y != null) // y 在这是可见的
复制代码
参看[while 语法](has an instance- or extension-function hasNext() that returns Boolean.)
在循环中使用 break 和 continue
kotlin 支持传统的 break 和 continue 操作符。参看返回和跳转
返回与跳转
Kotlin 有三种机构跳转操作符
return
break
结束最近的闭合循环continue
跳到最近的闭合循环的下一次循环
break 和 continue 标签
在 Kotlin 中表达式可以添加标签。标签通过 @
结尾来表示,比如:abc@
,fooBar@
都是有效的(参看语法)。使用标签语法只需像这样:
loop@ for (i in 1..100){
//...
}
复制代码
现在我们可以用标签实现 break
或者 continue
的快速跳转:
loop@ for (i in 1..100) {
for (j in i..100) {
if (...)
break@loop
}
}
复制代码
break
是跳转标签后面的表达式,continue
是跳转到循环的下一次迭代。
返回到标签处在字面函数,局部函数,以及对象表达式中,函数可以在 Kotlin 中被包裹。return 允许我们返回到外层函数。最重要的例子就是从字面函数中返回,还记得我们之前的写法吗:
fun foo() {
ints.forEach {
if (it == 0) return
print(it)
}
}
复制代码
return 表达式返回到最近的闭合函数,比如 foo
(注意这样非局部返回仅仅可以在内联函数中使用)。如果我们需要从一个字面函数返回可以使用标签修饰 return :
fun foo() {
ints.forEach lit@ {
if (it ==0) return@lit
print(it)
}
}
复制代码
现在它仅仅从字面函数中返回。经常用一种更方便的含蓄的标签:比如用和传入的 lambda 表达式名字相同的标签。
fun foo() {
ints.forEach {
if (it == 0) return@forEach
print(it)
}
}
复制代码
另外,我们可以用函数表达式替代字面函数。在函数表达式中使用 return 语句可以从函数表达式中返回。
fun foo() {
ints.forEach(fun(value: Int){
if (value == 0) return
print(value)
})
}
复制代码
当返回一个值时,解析器给了一个参考,比如(原文When returning a value, the parser gives preference to the qualified return, i.e.):
return@a 1
复制代码
命名函数自动定义标签:
foo outer() {
foo inner() {
return@outer
}
}
复制代码
类和继承
类
在 Kotlin 中类用 class
声明:
class Invoice {
}
复制代码
类的声明包含类名,类头(指定类型参数,主构造函数等等),以及类主体,用大括号包裹。类头和类体是可选的;如果没有类体可以省略大括号。
class Empty
复制代码
构造函数
在 Kotlin 中类可以有一个主构造函数以及多个二级构造函数。主构造函数是类头的一部分:跟在类名后面(可以有可选的类型参数)。
class Person constructor(firstName: String) {
}
复制代码
如果主构造函数没有注解或可见性说明,则 constructor
关键字是可以省略:
class Person(firstName: String){
}
复制代码
主构造函数不能包含任意代码。初始化代码可以放在以 init
做前缀的初始化块内
class Customer(name: String) {
init {
logger,info("Customer initialized with value ${name}")
}
}
复制代码
注意主构造函数的参数可以用在初始化块内,也可以用在类的属性初始化声明处:
class Customer(name: String) {
val customerKry = name.toUpperCase()
}
复制代码
事实上,声明属性并在主构造函数中初始化,在 Kotlin 中有更简单的语法:
class Person(val firstName: String, val lastName: String, var age: Int) {
}
复制代码
就像普通的属性,在主构造函数中的属性可以是可变的(var
)或只读的(val
)。
如果构造函数有注解或可见性声明,则 constructor 关键字是不可少的,并且可见性应该在前:
class Customer public @inject constructor (name: String) {...}
复制代码
二级构造函数
类也可以有二级构造函数,需要加前缀 constructor:
class Person {
constructor(parent: Person) {
parent.children.add(this)
}
}
复制代码
如果类有主构造函数,每个二级构造函数都要直接或间接通过另一个二级构造函数代理主构造函数。在同一个类中代理另一个构造函数使用 this
关键字:
class Person(val name: String) {
constructor (name: String, paret: Person) : this(name) {
parent.children.add(this)
}
}
复制代码
如果一个非抽象类没有声明构造函数(主构造函数或二级构造函数),它会产生一个没有参数的构造函数。该构造函数的可见性是 public 。如果你不想你的类有公共的构造函数,你就得声明一个拥有非默认可见性的空主构造函数:
class DontCreateMe private constructor () {
}
复制代码
注意:在 JVM 虚拟机中,如果主构造函数的所有参数都有默认值,编译器会生成一个附加的无参的构造函数,这个构造函数会直接使用默认值。这使得 Kotlin 可以更简单的使用像 Jackson 或者 JPA 这样使用无参构造函数来创建类实例的库。
class Customer(val customerName: String = "")
复制代码
创建类的实例
我们可以像使用普通函数那样使用构造函数创建类实例:
val invoice = Invoice()
val customer = Customer("Joe Smith")
复制代码
注意 Kotlin 没有 new
关键字。
创建嵌套类、内部类或匿名类的实例参见嵌套类
类成员
类可以包含:
-- 构造函数和初始化代码块
-- 函数
-- 属性
-- 内部类
-- 对象声明
继承
Kotlin 中所有的类都有共同的父类 Any
,它是一个没有父类声明的类的默认父类:
class Example // 隐式继承于 Any
复制代码
Any
不是 java.lang.Object
;事实上它除了 equals()
,hashCode()
以及toString()
外没有任何成员了。参看[Java interoperability]( Java interoperability)了解更多详情。
声明一个明确的父类,需要在类头后加冒号再加父类:
open class Base(p: Int)
class Derived(p: Int) : Base(p)
复制代码
如果类有主构造函数,则基类可以而且是必须在主构造函数中使用参数立即初始化。
如果类没有主构造函数,则必须在每一个构造函数中用 super
关键字初始化基类,或者在代理另一个构造函数做这件事。注意在这种情形中不同的二级构造函数可以调用基类不同的构造方法:
class MyView : View {
constructor(ctx: Context) : super(ctx) {
}
constructor(ctx: Context, attrs: AttributeSet) : super(ctx,attrs) {
}
}
复制代码
open
注解与java中的final
相反:它允许别的类继承这个类。默认情形下,kotlin 中所有的类都是 final ,对应 Effective Java :Design and document for inheritance or else prohibit it.
复写方法
像之前提到的,我们在 kotlin 中坚持做明确的事。不像 java ,kotlin 需要把可以复写的成员都明确注解出来,并且重写它们:
open class Base {
open fun v() {}
fun nv() {}
}
class Derived() : Base() {
override fun v() {}
}
复制代码
对于 Derived.v()
来说override
注解是必须的。如果没有加的话,编译器会提示。如果没有open
注解,像 Base.nv()
,在子类中声明一个同样的函数是不合法的,要么加override
要么不要复写。在 final 类(就是没有open注解的类)中,open
类型的成员是不允许的。
标记为override
的成员是open的,它可以在子类中被复写。如果你不想被重写就要加 final
:
open class AnotherDerived() : Base() {
final override fun v() {}
}
复制代码
等等!我现在怎么hack我的库?!
有个问题就是如何复写子类中那些作者不想被重写的类,下面介绍一些令人讨厌的方案。
我们认为这是不好的,原因如下:
最好的实践建议你不应给做这些 hack
人们可以用其他的语言成功做到类似的事情
如果你真的想 hack 那么你可以在 java 中写好 hack 方案,然后在 kotlin 中调用 (参看java调用),专业的构架可以很好的做到这一点
复写属性
复写属性与复写方法类似,在一个父类上声明的属性在子类上被重新声明,必须添加override
,并且它们必须具有兼容的类型。每个被声明的属性都可以被一个带有初始化器的属性或带有getter方法的属性覆盖
open class Foo {
open val x: Int get { ... }
}
class Bar1 : Foo() {
override val x: Int = ...
}
复制代码
您还可以使用var
属性覆盖一个val
属性,但反之则不允许。这是允许的,因为val属性本质上声明了一个getter方法,并将其重写为var,另外在派生类中声明了setter方法。
注意,可以在主构造函数中使用override关键字作为属性声明的一部分。
interface Foo {
val count: Int
}
class Bar1(override val count: Int) : Foo
class Bar2 : Foo {
override var count: Int = 0
}
复制代码
复写规则
在 kotlin 中,实现继承通常遵循如下规则:如果一个类从它的直接父类继承了同一个成员的多个实现,那么它必须复写这个成员并且提供自己的实现(或许只是直接用了继承来的实现)。为表示使用父类中提供的方法我们用 super<Base>
表示:
open class A {
open fun f () { print("A") }
fun a() { print("a") }
}
interface B {
fun f() { print("B") } // 接口的成员变量默认是 open 的
fun b() { print("b") }
}
class C() : A() , B {
// 编译器会要求复写f()
override fun f() {
super<A>.f() // 调用 A.f()
super<B>.f() // 调用 B.f()
}
}
复制代码
可以同时从 A 和 B 中继承方法,而且 C 继承 a() 或 b() 的实现没有任何问题,因为它们都只有一个实现。但是 f() 有俩个实现,因此我们在 C 中必须复写 f() 并且提供自己的实现来消除歧义。
抽象类
一个类或一些成员可能被声明成 abstract 。一个抽象方法在它的类中没有实现方法。记住我们不用给一个抽象类或函数添加 open 注解,它默认是带着的。
我们可以用一个抽象成员去复写一个带 open 注解的非抽象方法。
open class Base {
open fun f() {}
}
abstract class Derived : Base() {
override abstract fun f()
}
复制代码
伴随对象
在 kotlin 中不像 java 或者 C# 它没有静态方法。在大多数情形下,我们建议只用包级别的函数。
如果你要写一个没有实例类就可以调用的方法,但需要访问到类内部(比如说一个工厂方法),你可以把它写成它所在类的一个成员(you can write it as a member of an object declaration inside that class)
更高效的方法是,你可以在你的类中声明一个伴随对象,这样你就可以像 java/c# 那样把它当做静态方法调用,只需要它的类名做一个识别就好了
密封类
密封类用于代表严格的类结构,值只能是有限集合中的某中类型,不可以是任何其它类型。这就相当于一个枚举类的扩展:枚举值集合的类型是严格限制的,但每个枚举常量只有一个实例,而密封类的子类可以有包含不同状态的多个实例。
声明密封类需要在 class 前加一个 sealed
修饰符。密封类可以有子类但必须全部嵌套在密封类声明内部、
sealed class Expr {
class Const(val number: Double) : Expr()
class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
}
复制代码
注意密封类子类的扩展可以在任何地方,不必在密封类声明内部进行。
使用密封类的最主要的的好处体现在你使用 when 表达式。可以确保声明可以覆盖到所有的情形,不需要再使用 else 情形。
fun eval(expr: Expr): Double = when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
// the `else` clause is not required because we've covered all the cases
}
复制代码
属性和字段
声明属性
在类中,使用关键字 var
或 val
声明属性。
只需要将成员变量定义成一个变量,默认是 public
的。编译器会自动生成 getter
和 setter
方法。下面的属性编译器默认添加了 getter
和 setter
方法。
public class Address {
public var name: String = ...
public var street: String = ...
public var city: String = ...
public var state: String? = ...
public var zip: String = ...
}
复制代码
属性的使用
要使用一个属性,只需要使用名称引用即可:
funcopyAddress(address: Address): Address {
val result = Address() // there's no 'new'keyword in Kotlin
result.name = address.name // accessors arecalled
result.street = address.street
// ...
return result
}
复制代码
注:上面对属性的访问,并不是像Java里面一样,直接访问属性的本身,而是默认调用了
getter
和setter
方法。
Getters and Setters
完整的属性声明如下:
var<propertyName>: <PropertyType> [= <property_initializer>]
[<getter>]
[<setter>]
复制代码
初始器(initializer
)、getter
和 setter
都是可选的。 属性类型(PropertyType
)如果可以从初始器或者父类中推导出来,也可以省略。
var
是允许有 getter
和 setter
方法,如果变量是val
声明的,它类似于Java中的final
,所以如果以val
声明就不允许有setter
方法。
val isEmpty:Boolean
get() = this.size == 0
var stringRepresentation: String
get() = this.toString()
set(value) {
setDataFromString(value) // parses thestring and assigns values to other properties
}
复制代码
实例:
class Person (name: String) {
var name: String = name
get() = field.toUpperCase()
set(value) {
field = value
}
}
//
fun main(args:Array<String>) {
var customer: Person = Person("xiaomin")
println(customer.name) // XIAOMIN
customer.name = "lei"
println(customer.name) // LEI
}
复制代码
另外,对于属性,如果你想改变访问的可见性或者是对其进行注解,但是又不想改变它的默认实现,那么你就可以定义set和get但不进行实现。
var setterVisibility: String = "abc" // Initializer required, not a nullable type
private set // the setter is private and hasthe default implementation
var setterWithAnnotation: Any?
@Injectset // annotate the setter with Inject
复制代码
支持(反向)域(Backing Fields)
如在上面例子中定义的Person类里面,属性的get
和set
方法里面使用了一个field
,它是一个自动的返回字段,代表的就是该属性。
field
只有在访问属性的时候才会产生,其他时候是不会产生的。
var name: String = name
get() = field.toUpperCase()
set(value) {
field = value
}
复制代码
支持(反向)属性(Backing Properties)
如果Backing Fields不适用的话,其实可以直接使用返回属性就可以了。
private var _table:Map<String, Int>? = null
public val table: Map<String, Int>
get() {
if (_table == null)
_table = HashMap() // Type parameters areinferred
return _table ?: throwAssertionError("Set to null by another thread")
}
复制代码
延迟初始化属性
对于非空类型的属性是必须初始化的。如果我们希望延迟进行初始化,就可以使用lateinit
关键字了。
lateinit
只能在不可null的对象上使用,必须为var
,不能为primitives
(Int、Float之类)。
public class MyTest{
lateinit var subject: TestSubject
@SetUp fun setup() {
subject = TestSubject()
}
@Test fun test() {
subject.method() // dereference directly
}
}
复制代码
接口
Kotlin 的接口很像 java 8。它们都可以包含抽象方法,以及方法的实现。和抽象类不同的是,接口不能保存状态。可以有属性但必须是抽象的,或者提供访问器的实现。
接口用关键字 interface 来定义:
interface MyInterface {
fun bar()
fun foo() {
//函数体是可选的
}
}
复制代码
接口的实现
一个类或对象可以实现一个或多个接口
class Child : MyInterface {
fun bar () {
//函数体
}
}
复制代码
接口中的属性
可以在接口中申明属性。接口中的属性要么是抽象的,要么提供访问器的实现。接口属性不可以有后备字段。而且访问器不可以引用它们。
interface MyInterface {
val property: Int // abstract
val propertyWithImplementation: String
get() = "foo"
fun foo() {
print(property)
}
}
class Child : MyInterface {
override val property: Int = 29
}
复制代码
解决重写冲突
当我们在父类中声明了许多类型,有可能出现一个方法的多种实现。比如:
interface A {
fun foo() { print("A") }
fun bar()
}
interface B {
fun foo() { print("B") }
fun bar() { print("bar") }
}
class C : A {
override fun bar() { print("bar") }
}
class D : A, B {
override fun foo() {
super<A>.foo()
super<B>.foo()
}
}
复制代码
A B 接口都有声明了 foo()
bar()
函数。它们都实现了 foo()
方法,但只有 B 实现了 bar()
,bar()
在 A 中并没有声明它是抽象的,这是因为在接口中如果函数没有函数体,那么默认是抽像的。现在,如果我们从 A 中派生一个 C 实体类,显然我们需要重写 bar()
,并实现它。而我们从 A 和 B 派生一个 D ,我们不用重写 bar()
方法,因为我们的一个继承中有一个已经实现了它。但我们继承了两个 foo() 的实现,因此编译器不知道应该选哪个,并强制我们重写 foo()
并且明确指出我们想怎么实现。
可见性修饰词
类,对象,接口,构造函数,属性以及它们的 setter 方法都可以有可见性修饰词。( getter 方法作为属性时都是可见性)。在 Kotlin 中有四种修饰词:private
,protected
,internal
,以及 public
。默认的修饰符是 public
。 下面将解释不同类型的声明作用域。
包
函数,属性和类,对象和接口可以在 "top-level" 声明:
package foo
fun baz() {}
class bar {}
复制代码
如果没有指明任何可见性修饰词,默认使用
public
,这意味着你的声明在任何地方都可见;如果你声明为
private
,则只在包含声明的文件中可见;如果用
internal
声明,则在同一模块中的任何地方可见;
protected
在 "top-level" 中不可以使用
例子:
package foo
private fun foo() {} // visible inside example.kt
public var bar: Int = 5 // property is visible everywhere
private set // setter is visible only in example.kt
internal val baz = 6 // visible inside the same module
复制代码
类和接口
当在类中声明时:
private
只在该类(以及它的成员)中可见
protected
和private
一样,但在子类中也可见
internal
在本模块的所有可以访问到声明区域的均可以访问该类的所有internal
成员 ( internal — any client inside this module who sees the declaring class sees its internal members;)
public
任何地方可见 (public — any client who sees the declaring class sees its public members.)
java 使用者注意:外部类不可以访问内部类的 private 成员。
例子:
open class Outer {
private val a = 1
protected val b = 2
internal val c = 3
val d = 4 // public by default
protected class Nested {
public val e: Int = 5
}
}
class Subclass : Outer() {
// a is not visible
// b, c and d are visible // Nested and e are visible
}
class Unrelated(o: Outer) {
// o.a, o.b are not visible
// o.c and o.d are visible (same module)
// Outer.Nested is not visible, and Nested::e is not visible either
}
复制代码
构造函数
通过下面的语法来指定主构造函数(必须显示的使用 constructor
关键字)的可见性:
class C private constructor(a: Int) { ... }
复制代码
这里构造函数是 private 。所有的构造函数默认是 public ,实际上只要类是可见的它们就是可见的 (注意 internal
类型的类中的 public
属性只能在同一个模块内才可以访问)
局部声明
局部变量,函数和类是不允许使用修饰词的
模块
internal
修饰符是指成员的可见性是只在同一个模块中才可见的。模块在 Kotlin 中就是一系列的 Kotlin 文件编译在一起:
— an IntelliJ IDEA module;
— a Maven or Gradle project;
— a set of files compiled with one invocation of the Ant task.
扩展
与 C# 和 Gosu 类似, Kotlin 也提供了一种,可以在不继承父类,也不使用类似装饰器这样的设计模式的情况下对指定类进行扩展。我们可以通过一种叫做扩展的特殊声明来实现他。Kotlin 支持函数扩展和属性扩展。
函数扩展
为了声明一个函数扩展,我们需要在函数前加一个接收者类型作为前缀。下面我们会为 MutableList 添加一个 swap
函数:
fun MutableList<Int>.swap(x: Int, y: Int) {
val temp = this[x] // this 对应 list
this[x] = this[y]
this[y] = tmp
}
复制代码
在扩展函数中的 this 关键字对应接收者对象。现在我们可以在任何 MutableList 实例中使用这个函数了:
val l = mutableListOf(1, 2, 3)
l.swap(0, 2)// 在 `swap()` 函数中 `this` 持有的值是 `l`
复制代码
当然,这个函数对任意的 MutableList<T>
都是适用的,而且我们可以把它变的通用:
fun <T> MutableList<T>.swap(x: Int, y: Int) {
val tmp = this[x] // 'this' corresponds to the list
this[x] = this[y]
this[y] = tmp
}
复制代码
我们在函数名前声明了通用类型,从而使它可以接受任何参数。参看泛型函数。
扩展是被静态解析的
扩展实际上并没有修改它所扩展的类。定义一个扩展,你并没有在类中插入一个新的成员,只是让这个类的实例对象能够通过.调用新的函数。
需要强调的是扩展函数是静态分发的,举个例子,它们并不是接受者类型的虚拟方法。这意味着扩展函数的调用时由发起函数调用的表达式的类型决定的,而不是在运行时动态获得的表达式的类型决定。比如
open class C
class D: C()
fun C.foo() = "c"
fun D.foo() = "d"
fun printFoo(c: C) {
println(c.foo())
}
printFoo(D())
复制代码
这个例子会输出 c,因为这里扩展函数的调用决定于声明的参数 c 的类型,也就是 C。
如果有同名同参数的成员函数和扩展函数,调用的时候必然会使用成员函数,比如:
class C {
fun foo() { println("member") }
}
fun C.foo() { println("extension") }
复制代码
当我们对C的实例c调用c.foo()
的时候,他会输出"member",而不是"extension"
但你可以用不同的函数签名通过扩展函数的方式重载函数的成员函数,比如下面这样:
class C {
fun foo() { println("number") }
}
fun C.foo(i:Int) { println("extention") }
复制代码
C().foo(1) 的调用会打印 “extentions”。
可空的接收者
注意扩展可以使用空接收者类型进行定义。这样的扩展使得,即使是一个空对象仍然可以调用该扩展,然后在扩展的内部进行 this == null
的判断。这样你就可以在 Kotlin 中任意调用 toString() 方法而不进行空指针检查:空指针检查延后到扩展函数中完成。
fun Any?.toString(): String {
if (this == null) return "null"
// 在空检查之后,`this` 被自动转为非空类型,因此 toString() 可以被解析到任何类的成员函数中
return toString()
}
复制代码
属性扩展
和函数类似, Kotlin 也支持属性扩展:
val <T> List<T>.lastIndex: Int
get() = size-1
复制代码
注意,由于扩展并不会真正给类添加了成员属性,因此也没有办法让扩展属性拥有一个备份字段。这也是为什么初始化函数不允许有扩展属性。扩展属性只能够通过明确提供 getter 和 setter方法来进行定义。
例子:
val Foo.bar = 1 //error: initializers are not allowed for extension properties
复制代码
伴随对象扩展
如果一个对象定义了伴随对象,你也可以给伴随对象添加扩展函数或扩展属性:
class MyClass {
companion object {}
}
fun MyClass.Companion.foo(){
}
复制代码
和普通伴随对象的成员一样,它们可以只用类的名字就调用:
MyClass.foo()
复制代码
扩展的域
大多数时候我们在 top level 定义扩展,就在包下面直接定义:
package foo.bar
fun Baz.goo() { ... }
复制代码
为了在除声明的包外使用这个扩展,我们需要在 import 时导入:
package com.example,usage
import foo.bar.goo // 导入所有名字叫 "goo" 的扩展
// 或者
import foo.bar.* // 导入foo.bar包下得所有数据
fun usage(baz: Baz) {
baz.goo()
}
复制代码
动机
在 java 中,我们通常使用一系列名字为 "*Utils" 的类: FileUtils
,StringUtils
等等。很有名的 java.util.Collections
也是其中一员的,但我们不得不像下面这样使用他们:
//java
Collections.swap(list, Collections.binarySearch(list, Collections.max(otherList)), Collections.max(list))
复制代码
由于这些类名总是不变的。我们可以使用静态导入并这样使用:
swap(list, binarySearch(list, max(otherList)), max(list))
复制代码
这样就好很多了,但这样我们就只能从 IDE 自动完成代码那里获得很少或得不到帮助信息。如果我们可以像下面这样那么就好多了
list.swap(list.binarySearch(otherList.max()), list.max())
复制代码
但我们又不想在 List 类中实现所有可能的方法。这就是扩展带来的好处。
数据类
我们经常创建一个只保存数据的类。在这样的类中一些函数只是机械的对它们持有的数据进行一些推导。在 kotlin 中这样的类称之为 data 类,用 data
标注:
data class User(val name: String, val age: Int)
复制代码
编译器会自动根据主构造函数中声明的所有属性添加如下方法:
equals()
/hashCode
函数
toString
格式是 "User(name=john, age=42)"[compontN()functions] (kotlinlang.org/docs/refere…) 对应按声明顺序出现的所有属性
copy() 函数
如果在类中明确声明或从基类继承了这些方法,编译器不会自动生成。
为确保这些生成代码的一致性,并实现有意义的行为,数据类要满足下面的要求:
注意如果构造函数参数中没有 val
或者 var
,就不会在这些函数中出现;
主构造函数应该至少有一个参数;
主构造函数的所有参数必须标注为
val
或者var
;数据类不能是
abstract
,open
,sealed
,或者inner
;数据类不能继承其它的类(但可以实现接口)。
在 JVM 中如果构造函数是无参的,则所有的属性必须有默认的值,(参看Constructors);
data class User(val name: String = "", val age: Int = 0)
复制
我们经常会对一些属性做修改但想要其他部分不变。这就是 copy() 函数的由来。在上面的 User 类中,实现起来应该是这样:
fun copy(name: String = this.name, age: Int = this.age) = User(name, age)
复制代码
有了 copy 我们就可以像下面这样写了:
val jack = User(name = "jack", age = 1)
val olderJack = jack.copy(age = 2)
复制代码
数据类和多重声明
组件函数允许数据类在多重声明中使用:
val jane = User("jane", 35)
val (name, age) = jane
println("$name, $age years of age") //打印出 "Jane, 35 years of age"
复制代码
标准数据类
标准库提供了 Pair
和 Triple
。在大多数情形中,命名数据类是更好的设计选择,因为这样代码可读性更强而且提供了有意义的名字和属性。
泛型
像 java 一样,Kotlin 中可以拥有类型参数:
class Box<T>(t: T){
var value = t
}
复制代码
通常来说,创建一个这样类的实例,我们需要提供类型参数:
val box: Box<Int> = Box<Int>(1)
复制代码
但如果类型有可能是推断的,比如来自构造函数的参数或者通过其它的一些方式,一个可以忽略类型的参数:
val box = Box(1)//1是 Int 型,因此编译器会推导出我们调用的是 Box<Int>
复制代码
变化
java 类型系统最棘手的一部分就是通配符类型。但 kotlin 没有,代替它的是两种其它的东西:声明变化和类型投影(declaration-site variance and type projections)。
首先,我们想想为什么 java 需要这些神秘的通配符。这个问题在Effective Java,条目18中是这样解释的:使用界限通配符增加 API 的灵活性。首先 java 中的泛型是不变的,这就意味着 List<String>
不是 List<Object>
的子类型。为什么呢,如果 List 不是不变的,就会引发下面的问题:
// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!! The cause of the upcoming problem sits here. Java prohibits this!
objs.add(1); // Here we put an Integer into a list of Strings
String s = strs.get(0); // !!! ClassCastException: Cannot cast Integer to String
复制代码
因此 java 禁止了这样的事情来保证运行时安全。但这有些其它影响。比如,Collection 接口的 addAll() 方法。这个方法的签名在哪呢?直观来讲是这样的:
//java
interface Collection<E> ... {
void addAdd(Collection<E> items);
}
复制代码
但接下来我们就不能做下面这些简单事情了:
//java
void copyAll(Collection<Object> to, Collection<String> from){
to.addAll(from);
}
复制代码
这就是为什么 addAll()
的签名是下面这样的:
//java
interface Collection<E> ... {
void addAll(Colletion<? extend E> items);
}
复制代码
这个通配符参数 ? extends T
意味着这个方法接受一些 T 类型的子类而非 T 类型本身。这就是说我们可以安全的读 T's
(这里表示 T 子类元素的集合),但不能写,因为我们不知道 T 的子类究竟是什么样的,针对这样的限制,我们很想要这样的行为:Collection<String>
是 Collection<? extens Object>
的子类。In “clever words”, the wildcard with an extends-bound (upper bound) makes the type covariant.
The key to understanding why this trick works is rather simple: if you can only take items from a collection, then using a collection of Strings and reading Objects from it is fine. Conversely, if you can only put items into the collection, it’s OK to take a collection of Objects and put Strings into it: in Java we have List<? super String> a supertype of List.
嵌套类
类可以嵌套在其他类中
class Outer {
private val bar: Int = 1
class Nested {
fun foo() = 2
}
}
val demo = Outer.Nested().foo() //==2
复制代码
内部类
类可以标记为 inner 这样就可以访问外部类的成员。内部类拥有外部类的一个对象引用:
class Outer {
private val bar: Int = 1
inner class Inner {
fun foo() = bar
}
}
val demo = Outer().Inner().foo() //==1
复制代码
参看这里了解更多 this 在内部类的用法
匿名内部类
匿名内部类的实例是通过 对象表达式 创建的:
window.addMouseListener(object: MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
// ...
}
override fun mouseEntered(e: MouseEvent) {
// ...
}
})
复制代码
如果对象是函数式的 java 接口的实例(比如只有一个抽象方法的 java 接口),你可以用一个带接口类型的 lambda 表达式创建它。
val listener = ActionListener { println("clicked") }
复制代码
枚举类
枚举类最基本的用法就是实现类型安全的枚举
enum class Direction {
NORTH,SOUTH,WEST
}
复制代码
每个自举常量都是一个对象。枚举常量通过逗号分开。
初始化
因为每个枚举都是枚举类的一个实例,它们是可以初始化的。
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
复制代码
匿名类
枚举实例也可以声明它们自己的匿名类
enum class ProtocolState {
WAITING {
override fun signal() = Taking
},
Taking{
override fun signal() = WAITING
};
abstract fun signal(): ProtocolState
}
复制代码
可以有对应的方法,以及复写基本方法。注意如果枚举定义了任何成员,你需要像在 java 中那样用分号 ; 把枚举常量定义和成员定义分开。
使用枚举常量
像 java 一样,Kotlin 中的枚举类有合成方法允许列出枚举常量的定义并且通过名字获得枚举常量。这些方法的签名就在下面列了出来(假设枚举类名字是 EnumClass):
EnumClass.valueOf(value: String): EnumClass
EnumClass.values(): Array<EnumClass>
复制代码
如果指定的名字在枚举类中没有任何匹配,那么valueOf()
方法将会抛出参数异常。
每个枚举常量都有获取在枚举类中声明的名字和位置的方法:
name(): Sting
ordinal(): Int
复制代码
枚举类也实现了 Comparable 接口,比较时使用的是它们在枚举类定义的自然顺序。
对象表达式和声明
有时候我们想要创建一个对当前类有一点小修改的对象,但不想重新声明一个子类。java 用匿名内部类的概念解决这个问题。Kotlin 用对象表达式和对象声明巧妙的实现了这一概念。
对象表达式
通过下面的方式可以创建继承自某种(或某些)匿名类的对象:
window.addMouseListener(object: MouseAdapter () {
override fun mouseClicked(e: MouseEvent) {
//...
}
})
复制代码
如果父类有构造函数,则必须传递相应的构造参数。多个父类可以用逗号隔开,跟在冒号后面:
open class A(x: Int) {
public open val y: Int = x
}
interface B { ... }
val ab = object : A(1), B {
override val y = 14
}
复制代码
有时候我们只是需要一个没有父类的对象,我们可以这样写:
val adHoc = object {
var x: Int = 0
var y: Int = 0
}
print(adHoc.x + adHoc.y)
复制代码
像 java 的匿名内部类一样,对象表达式可以访问闭合范围内的变量 (和 java 不一样的是,这些变量不用是 final 修饰的)
fun countClicks(windows: JComponent) {
var clickCount = 0
var enterCount = 0
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
clickCount++
}
override fun mouseEntered(e: MouseEvent){
enterCount++
}
})
}
复制代码
对象声明
单例模式是一种很有用的模式,Kotln 中声明它很方便:
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) {
// ...
}
val allDataProviders: Collection<DataProvider>
get() = // ...
}
复制代码
这叫做对象声明,跟在 object 关键字后面是对象名。和变量声明一样,对象声明并不是表达式,而且不能作为右值用在赋值语句。
想要访问这个类,直接通过名字来使用这个类:
DataProviderManager.registerDataProvider(...)
复制代码
这样类型的对象可以有父类型:
object DefaultListener : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
// ...
}
override fun mouseEntered(e: MouseEvent) {
// ...
}
}
复制代码
注意:对象声明不可以是局部的(比如不可以直接在函数内部声明),但可以在其它对象的声明或非内部类中进行内嵌入
伴随对象
在类声明内部可以用 companion 关键字标记对象声明:
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}
复制代码
伴随对象的成员可以通过类名做限定词直接使用:
val instance = MyClass.create()
复制代码
在使用了 companion 关键字时,伴随对象的名字可以省略:
class MyClass {
companion object {
}
}
复制代码
注意,尽管伴随对象的成员很像其它语言中的静态成员,但在运行时它们仍然是真正对象的成员实例,比如可以实现接口:
inerface Factory<T> {
fun create(): T
}
class MyClass {
companion object : Factory<MyClass> {
override fun create(): MyClass = MyClass()
}
}
复制代码
如果你在 JVM 上使用 @JvmStatic
注解,你可以有多个伴随对象生成为真实的静态方法和属性。参看 java interoperabillity。
对象表达式和声明的区别
他俩之间只有一个特别重要的区别:
对象表达式在我们使用的地方立即初始化并执行的
对象声明是懒加载的,是在我们第一次访问时初始化的。
伴随对象是在对应的类加载时初始化的,和 Java 的静态初始是对应的。
代理
类代理
代理模式 给实现继承提供了很好的代替方式, Kotlin 在语法上支持这一点,所以并不需要什么样板代码。Derived
类可以继承 Base
接口并且指定一个对象代理它全部的公共方法:
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { printz(x) }
}
class Derived(b: Base) : Base by b
fun main() {
val b = BaseImpl(10)
Derived(b).print()
}
复制代码
在 Derived
的父类列表中的 by 从句会将 b
存储在 Derived
内部对象,并且编译器会生成 Base
的所有方法并转给 b
。
代理属性
很多常用属性,虽然我们可以在需要的时候手动实现它们,但更好的办法是一次实现多次使用,并放到库。比如:
延迟属性:只在第一次访问是计算它的值 观察属性:监听者从这获取这个属性更新的通知 在 map 中存储的属性,而不是单独存在分开的字段
为了满足这些情形,Kotllin 支持代理属性:
class Example {
var p: String by Delegate()
}
复制代码
语法结构是: val/var <property name>: <Type> by <expression>
在 by 后面的属性就是代理,这样这个属性的 get() 和 set() 方法就代理给了它。
属性代理不需要任何接口的实现,但必须要提供 get()
方法(如果是变量还需要 set()
方法)。像这样:
class Delegate {
fun get(thisRef: Any?, prop: PropertyMetadata): String {
return "$thisRef, thank you for delegating '${prop.name}' to me !"
}
fun set(thisRef: Any?, prop: PropertyMatada, value: String) {
println("$value has been assigned to '${prop.name} in $thisRef.'")
}
}
复制代码
当我们从 p
也就是 Delegate
的代理,中读东西时,会调用 Delegate
的 get()
函数,因此第一个参数是我们从 p
中读取的,第二个参数是 p
自己的一个描述。比如:
val e = Example()
pintln(e.p)
复制代码
打印结果:
Example@33a17727, thank you for delegating ‘p’ to me!
同样当我们分配 p
时 set()
函数就会调动。前俩个参数所以一样的,第三个持有分配的值:
e.p = "NEW"
复制代码
打印结果:
NEW has been assigned to ‘p’ in Example@33a17727.
代理属性的要求
这里总结一些代理对象的要求。
只读属性 (val),代理必须提供一个名字叫 get
的方法并接受如下参数:
接收者--必须是相同的,或者是属性拥有者的子类型
元数据--必须是 PropertyMetadata 或这它的子类型
这个函数必须返回同样的类型作为属性。
可变属性 (var),代理必须添加一个叫 set
的函数并接受如下参数:
接受者--与
get()
一样
元数据--与get()
一样
新值--必须和属性类型一致或是它的字类型
标准代理
kotlin.properties.Delegates
对象是标准库提供的一个工厂方法并提供了很多有用的代理
延迟
Delegate.lazy()
是一个接受 lamdba 并返回一个实现延迟属性的代理:第一次调用 get()
执行 lamdba 并传递 lazy()
并记下结果,随后调用 get()
并简单返回之前记下的值。
import kotlin.properties.Delegates
val lazy: String by Delegates.lazy {
println("computed!")
"Hello"
}
fun main(args: Array<String>) {
println(lazy)
println(lazy)
}
复制代码
如果你想要线程安全,使用 blockingLazy()
: 它还是按照同样的方式工作,但保证了它的值只会在一个线程中计算,并且所有的线程都获取的同一个值。
观察者
Delegates.observable()
需要俩个参数:一个初始值和一个修改者的 handler 。每次我们分配属性时都会调用handler (在分配前执行)。它有三个参数:一个分配的属性,旧值,新值:
class User {
var name: String by Delegates.observable("<no name>") {
d.old,new -> println("$old -> $new")
}
}
fun main(args: Array<String>) {
val user = User()
user.name = "first"
user.name = "second"
}
复制代码
打印结果
-> first first -> second
如果你想能够截取它的分配并取消它,用 vetoable()
代替 observable()
非空
有时我们有一个非空的 var ,但我们在构造函数中没有一个合适的值,比如它必须稍后再分配。问题是你不能持有一个未初始化并且是非抽象的属性:
class Foo {
var bar: Bat //错误必须初始化
}
复制代码
我们可以用 null 初始化它,但我们不用每次访问时都检查它。
Delegates.notNull()
可以解决这个问题
class Foo {
var bar: Bar by Delegates.notNull()
}
复制代码
如果这个属性在第一次写之前读,它就会抛出一个异常,只有分配之后才会正常。
在 Map 中存储属性
Delegates.mapVal()
拥有一个 map 实例并返回一个可以从 map 中读其中属性的代理。在应用中有很多这样的例子,比如解析 JSON 或者做其它的一些 "动态"的事情:
class User(val map: Map<String, Any?>) {
val name: String by Delegates.mapVal(map)
val age: Int by Delegates.mapVal(map)
}
复制代码
在这个例子中,构造函数持有一个 map :
val user = User(mapOf (
"name" to "John Doe",
"age" to 25
))
复制代码
代理从这个 map 中取指(通过属性的名字):
println(user.name) // Prints "John Doe"
println(user.age) // Prints 25
复制代码
var 可以用 mapVar
函数
函数声明
在 kotlin 中用关键字 fun
声明函数:
fun double(x: Int): Int {
}
复制代码
函数用法
通过传统的方法调用函数
val result = double(2)
复制代码
通过.
调用成员函数
Sample().foo() // 创建Sample类的实例,调用foo方法
复制代码
中缀符号
在满足以下条件时,函数也可以通过中缀符号进行调用:
它们是成员函数或者是扩展函数 只有一个参数 使用
infix
关键词进行标记
//给 Int 定义一个扩展方法
infix fun Int.shl(x: Int): Int {
...
}
1 shl 2 //用中缀注解调用扩展函数
1.shl(2)
复制代码
参数
函数参数是用 Pascal 符号定义的 name:type
。参数之间用逗号隔开,每个参数必须指明类型。
fun powerOf(number: Int, exponent: Int) {
...
}
复制代码
默认参数
函数参数可以设置默认值,当参数被忽略时会使用默认值。这样相比其他语言可以减少重载。
fun read(b: Array<Byte>, off: Int = 0, len: Int = b.size() ) {
...
}
复制代码
默认值可以通过在type类型后使用=
号进行赋值
命名参数
在调用函数时参数可以命名。这对于那种有大量参数的函数是很方便的.
下面是一个例子:
fun reformat(str: String, normalizeCase: Boolean = true,upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ' ') {
...
}
复制代码
我们可以使用默认参数
reformat(str)
然而当调用非默认参数是就需要像下面这样:
reformat(str, true, true, false, '_')
复制代码
使用命名参数我们可以让代码可读性更强:
reformat(str,
normalizeCase = true,
uppercaseFirstLetter = true,
divideByCamelHumps = false,
wordSeparator = '_'
)
复制代码
如果不需要全部参数的话可以这样:
reformat(str, wordSeparator = '_')
复制代码
注意,命名参数语法不能够被用于调用Java函数中,因为Java的字节码不能确保方法参数命名的不变性
不带返回值的参数
如果函数不会返回任何有用值,那么他的返回类型就是 Unit
.Unit
是一个只有唯一值Unit
的类型.这个值并不需要被直接返回:
fun printHello(name: String?): Unit {
if (name != null)
println("Hello ${name}")
else
println("Hi there!")
// `return Unit` or `return` is optional
}
复制代码
Unit
返回值也可以省略,比如下面这样:
fun printHello(name: String?) {
...
}
复制代码
单表达式函数
当函数只返回单个表达式时,大括号可以省略并在 =
后面定义函数体
fun double(x: Int): Int = x*2
复制代码
在编译器可以推断出返回值类型的时候,返回值的类型可以省略:
fun double(x: Int) = x * 2
复制代码
明确返回类型
下面的例子中必须有明确返回类型,除非他是返回 Unit
类型的值,Kotlin 并不会对函数体重的返回类型进行推断,因为函数体中可能有复杂的控制流,他的返回类型未必对读者可见(甚至对编译器而言也有可能是不可见的):
变长参数
函数的参数(通常是最后一个参数)可以用 vararg
修饰符进行标记:
fun asList<T>(vararg ts: T): List<T> {
val result = ArrayList<T>()
for (t in ts)
result.add(t)
return result
}
复制代码
标记后,允许给函数传递可变长度的参数:
val list = asList(1, 2, 3)
复制代码
只有一个参数可以被标注为 vararg
。加入vararg
并不是列表中的最后一个参数,那么后面的参数需要通过命名参数语法进行传值,再或者如果这个参数是函数类型,就需要通过lambda法则.
当调用变长参数的函数时,我们可以一个一个的传递参数,比如 asList(1, 2, 3)
,或者我们要传递一个 array 的内容给函数,我们就可以使用 * 前缀操作符:
val a = array(1, 2, 3)
val list = asList(-1, 0, *a, 4)
复制代码
函数范围
Kotlin 中可以在文件顶级声明函数,这就意味者你不用像在Java,C#或是Scala一样创建一个类来持有函数。除了顶级函数,Kotlin 函数可以声明为局部的,作为成员函数或扩展函数。
局部函数
Kotlin 支持局部函数,比如在一个函数包含另一函数。
fun dfs(graph: Graph) {
fun dfs(current: Vertex, visited: Set<Vertex>) {
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v, visited)
}
dfs(graph.vertices[0], HashSet())
}
复制代码
局部函数可以访问外部函数的局部变量(比如闭包)
fun dfs(graph: Graph) {
val visited = HashSet<Vertex>()
fun dfs(current: Vertex) {
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v)
}
dfs(graph.vertices[0])
}
复制代码
局部函数甚至可以返回到外部函数 qualified return expressions
fun reachable(from: Vertex, to: Vertex): Boolean {
val visited = HashSet<Vertex>()
fun dfs(current: Vertex) {
if (current == to) return@reachable true
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v)
}
dfs(from)
return false
}
复制代码
成员函数
成员函数是定义在一个类或对象里边的
class Sample() {
fun foo() { print("Foo") }
}
复制代码
成员函数可以用 .
的方式调用
Sample.foo()
复制代码
更多请参看类和继承
泛型函数
函数可以有泛型参数,样式是在函数后跟上尖括号。
fun sigletonArray<T>(item: T): Array<T> {
return Array<T>(1, {item})
}
复制代码
内联函数
扩展函数
高阶函数和 lambda 表达式
尾递归函数
Kotlin 支持函数式编程的尾递归。这个允许一些算法可以通过循环而不是递归解决问题,从而避免了栈溢出。当函数被标记为 tailrec
时,编译器会优化递归,并用高效迅速的循环代替它。
tailrec fun findFixPoint(x: Double = 1.0): Double
= if (x == Math.cos(x)) x else findFixPoint(Math.cos(x))
复制代码
这段代码计算的是数学上的余弦不动点。Math.cos 从 1.0 开始不断重复,直到值不变为止,结果是 0.7390851332151607 这段代码和下面的是等效的:
private fun findFixPoint(): Double {
var x = 1.0
while (true) {
val y = Math.cos(x)
if ( x == y ) return y
x = y
}
}
复制代码
使用 tailrec
修饰符必须在最后一个操作中调用自己。在递归调用代码后面是不允许有其它代码的,并且也不可以在 try/catch/finall 块中进行使用。当前的尾递归只在 JVM 的后端中可以用
高阶函数与 lambda 表达式
高阶函数
高阶函数就是可以接受函数作为参数并返回一个函数的函数。比如 lock()
就是一个很好的例子,它接收一个 lock 对象和一个函数,运行函数并释放 lock;
fun lock<T>(lock: Lock, body: () -> T ) : T {
lock.lock()
try {
return body()
}
finally {
lock.unlock()
}
}
复制代码
现在解释一下上面的代码吧:body
有一个函数类型 () -> T
,把它设想为没有参数并返回 T 类型的函数。它引发了内部的 try 函数块,并被 lock
保护,结果是通过 lock()
函数返回的。
如果我们想调用 lock()
,函数,我们可以传给它另一个函数做参数:
fun toBeSynchroized() = sharedResource.operation()
val result = lock(lock, ::toBeSynchroized)
复制代码
其实最方便的办法是传递一个字面函数(通常是 lambda 表达式):
val result = lock(lock, {
sharedResource.operation() })
复制代码
字面函数经常描述有更多细节,但为了继续本节,我们看一下更简单的预览吧:
字面函数被包在大括号里
参数在
->
前面声明(参数类型可以省略)函数体在
->
之后
在 kotlin 中有一个约定,如果最后一个参数是函数,可以省略括号:
lock (lock) {
sharedResource.operation()
}
复制代码
最后一个高阶函数的例子是 map()
(of MapReduce):
fun <T, R> List<T>.map(transform: (T) -> R):
List<R> {
val result = arrayListOf<R>()
for (item in this)
result.add(transform(item))
return result
}
复制代码
函数可以通过下面的方式调用
val doubled = ints.map {it -> it * 2}
复制代码
如果字面函数只有一个参数,则声明可以省略,名字就是 it
:
ints map {it * 2}
复制代码
这样就可以写LINQ-风格的代码了:
strings filter {it.length == 5} sortBy {it} map {it.toUpperCase()}
复制代码
内联函数
有些时候可以用 内联函数 提高高阶函数的性能。
字面函数和函数表达式
字面函数或函数表达式就是一个 "匿名函数",也就是没有声明的函数,但立即作为表达式传递下去。想想下面的例子:
max(strings, {a, b -> a.length < b.length })
复制代码
max
函数就是一个高阶函数,它接受函数作为第二个参数。第二个参数是一个表达式所以本生就是一个函数,即字面函数。作为一个函数,相当于:
fun compare(a: String, b: String) : Boolean = a.length < b.length
复制代码
函数类型
一个函数要接受另一个函数作为参数,我们得给它指定一个类型。比如上面的 max
定义是这样的:
fun max<T>(collection: Collection<out T>, less: (T, T) -> Boolean): T? {
var max: T? = null
for (it in collection)
if (max == null || less(max!!, it))
max = it
return max
}
复制代码
参数 less
是 (T, T) -> Boolean
类型,也就是接受俩个 T
类型参数返回一个 Boolean
:如果第一个参数小于第二个则返回真。
在函数体第四行, less
是用作函数
一个函数类型可以像上面那样写,也可有命名参数,
val compare: (x: T,y: T) -> Int = ...
复制代码
函数文本语法
函数文本的完全写法是下面这样的:
val sum = {x: Int,y: Int -> x + y}
复制代码
函数文本总是在大括号里包裹着,在完全语法中参数声明是在括号内,类型注解是可选的,函数体是在 ->
之后,像下面这样:
val sum: (Int, Int) -> Int = {x, y -> x+y }
复制代码
函数文本有时只有一个参数。如果 kotlin 可以从它本生计算出签名,那么可以省略这个唯一的参数,并会通过 it
隐式的声明它:
ints.filter {it > 0}//这是 (it: Int) -> Boolean 的字面意思 注意如果一个函数接受另一个函数做为最后一个参数,该函数文本参数可以在括号内的参数列表外的传递。参看 callSuffix
函数表达式
上面没有讲到可以指定返回值的函数。在大多数情形中,这是不必要的,因为返回值是可以自动推断的。然而,如果你需要自己指定,可以用函数表达式来做:
fun(x: Int, y: Int ): Int = x + y
复制代码
函数表达式很像普通的函数声明,除了省略了函数名。它的函数体可以是一个表达式(像上面那样)或者是一个块:
fun(x: Int, y: Int): Int {
return x + y
}
复制代码
参数以及返回值和普通函数是一样的,如果它们可以从上下文推断出参数类型,则参数可以省略:
ints.filter(fun(item) = item > 0)
复制代码
返回值类型的推导和普通函数一样:函数返回值是通过表达式自动推断并被明确声明
注意函数表达式的参数总是在括号里传递的。 The shorthand syntax allowing to leave the function outside the parentheses works only for function literals.
字面函数和表达式函数的另一个区别是没有本地返回。没有 lable 的返回总是返回到 fun 关键字所声明的地方。这意味着字面函数内的返回会返回到一个闭合函数,而表达式函数会返回到函数表达式自身。
闭包
一个字面函数或者表达式函数可以访问闭包,即访问自身范围外的声明的变量。不像 java 那样在闭包中的变量可以被捕获修改:
var sum = 0
ins filter {it > 0} forEach {
sum += it
}
print(sum)
复制代码
函数表达式扩展
除了普通的功能,kotlin 支持扩展函数。这种方式对于字面函数和表达式函数都是适用的。它们最重要的使用是在 Type-safe Groovy-style builders。
表达式函数的扩展和普通的区别是它有接收类型的规范。
val sum = fun Int.(other: Int): Int = this + other
复制代码
接收类型必须在表达式函数中明确指定,但字面函数不用。字面函数可以作为扩展函数表达式,但只有接收类型可以通过上下文推断出来。
表达式函数的扩展类型是一个带接收者的函数:
sum : Int.(other: Int) -> Int
复制代码
可以用 .
或前缀来使用这样的函数:
1.sum(2)
1 sum 2
复制代码
内联函数
使用高阶函数带来了相应的运行时麻烦:每个函数都是一个对象,它捕获闭包,即这些变量可以在函数体内被访问。内存的分配,虚拟调用的运行都会带来开销
但在大多数这种开销是可以通过内联文本函数避免。下面就是一个很好的例子。lock()
函数可以很容易的在内联点调用。思考一下下面的例子:
lock(i) { foo() }
复制代码
(Instead of creating a function object for the parameter and generating a call),编译器可以忽略下面的代码:
lock.lock()
try {
foo()
}
finally {
lock.lock()
}
复制代码
这好像不是我们开始想要的
想要让编译器不这样做的话,我们需要用 inline
标记 lock()
函数:
inline fun lock<T>(lock: Lock,body: ()-> T): T {
//...
}
复制代码
inline
标记即影响函数本身也影响传递进来的 lambda 函数:所有的这些都将被关联到调用点。
内联可能会引起生成代码增长,但我们可以合理的解决它(不要内联太大的函数)
@noinline
为了你想要一些 lambda 表达式传递给内联函数时是内联的,你可以给你的一些函数参数标记 @noinline
注解:
inline fun foo(inlined: () -> Uint, @noinline notInlined: () -> Unit) {
//...
}
复制代码
内联的 lambda 只能在内联函数中调用,或者作为内联参数,但 @noinline
标记的可以通过任何我们喜欢的方式操控:存储在字段,( passed around etc)
注意如果内联函数没有内联的函数参数并且没有具体类型的参数,编译器会报警告,这样内联函数就没有什么优点的(如果你认为内联是必须的你可以忽略警告)
返回到非局部
在 kotlin 中,我们可以不加条件的使用 return
去退出一个命名函数或表达式函数。这意味这退出一个 lambda 函数,我们不得不使用标签,而且空白的 return
在 lambda 函数中是禁止的,因为 lambda 函数不可以造一个闭合函数返回:
fun foo() {
ordinaryFunction {
return // 错误 不可以在这返回
}
}
复制代码
但如果 lambda 函数是内联传递的,则返回也是可以内联的,因此允许下面这样:
fun foo() {
inlineFunction {
return //
]
}
复制代码
注意有些内联函数可以调用传递进来的 lambda 函数,但不是在函数体,而是在另一个执行的上下文中,比如局部对象或者一个嵌套函数。在这样的情形中,非局部的控制流也不允许在lambda 函数中。为了表明,lambda 参数需要有 InlineOptions.ONLY_LOCAL_RETURN
注解:
inline fun f(inlineOptions(InlineOption.ONLY_LOCAL_RETURN) body: () -> Unit) {
val f = object: Runnable {
override fun run() = body()
}
// ...
}
复制代码
内联 lambda 不允许用 break 或 continue ,但在以后的版本可能会支持。
实例化参数类型
有时候我们需要访问传递过来的类型作为参数:
fun <T> TreeNode.findParentOfType(clazz: Class<T>): T? {
var p = parent
while (p != null && !clazz.isInstance(p)) {
p = p?.parent
}
@suppress("UNCHECKED_CAST")
return p as T
}
复制代码
现在,我们创立了一颗树,并用反射检查它是否是某个特定类型。一切看起来很好,但调用点就很繁琐了:
myTree.findParentOfType(javaClass<MyTreeNodeType>() )
复制代码
我们想要的仅仅是给这个函数传递一个类型,即像下面这样:
myTree.findParentOfType<MyTreeNodeType>()
复制代码
为了达到这个目的,内联函数支持具体化的类型参数,因此我们可以写成这样:
inline fun <reified T> TreeNode.findParentOfType(): T? {
var p = parent
while (p != null && p !is T) {
p = p?.parent
}
return p as T
}
复制代码
我们用 refied 修饰符检查类型参数,既然它可以在函数内部访问了,也就基本上接近普通函数了。因为函数是内联的,所以不许要反射,像 !is
as
这样的操作都可以使用。同时,我们也可以像上面那样调用它了 myTree.findParentOfType<MyTreeNodeType>()
尽管在很多情况下会使用反射,我们仍然可以使用实例化的类型参数 javaClass()
来访问它:
inline fun methodsOf<reified T>() = javaClass<T>().getMethods()
fun main(s: Array<String>) {
println(methodsOf<String>().joinToString('\n'))
}
复制代码
普通的函数(没有标记为内联的)不能有实例化参数。
非空类型
在Kotlin中定义一个变量时,默认就是非空类型。当你将一个非空类型置空时,编译器会告诉你这不可行。
因此,如果你后面任何时候使用该变量时,都可以放心的使用而不用担心会发生NPE
。所以要想远离NPE,尽可能的使用非空类型的定义
。
例如:
var a: String = "abc"
a = null // compilation error
复制代码
可选型(可空类型)
虽然非空类型
能够有效避免 NPE 的问题,但是有时候我们总不可避免的需要使用可选类型
。在定义可选型的时候,我们只要在非空类型的后面添加一个 ?
就可以了。
var b: String? = "abc"
b = null // ok
复制代码
在使用可选型变量的时候,这个变量就有可能为空,所以在使用前我们应该对其进行空判断(在 Java 中我们经常这样做),这样往往带来带来大量的工作,这些空判断代码本身没有什么实际意义,并且让代码的可读性和简洁性带来了巨大的挑战。
Kotlin 为了解决这个问题,它并不允许我们直接使用一个可选型的变量去调用方法或者属性。
var b: String? = null
val l = b.length // compilation error
复制代码
如果使用这种方法,那么空判断是必须的。
val l = if (b != null) b.length else -1
复制代码
注意:如果你定义的变量是全局变量,即使你做了空判断,依然不能使用变量去调用方法或者属性。 这个时候你需要考虑使用下面的方法。
Kotlin 为可选型提供了一个安全调用操作符 ?.
,使用该操作符可以方便调用可选型的方法或者属性。
val l = b?.length
复制代码
这里 l
得到的返回依然是一个可选型 Int?
。
Kotlin 还提供了一个强转的操作符!!
,这个操作符能够强行调用变量的方法或者属性,而不管这个变量是否为空,如果这个时候该变量为空时,那么就会发生 NPE。所以如果不想继续陷入 NPE 的困境无法自拔,请不要该操作符走的太近。
Elvis 操作符
上面有提到一种情况,当 b
为空时,返回它的长度值给一个默认值 -1
。要实现这样的逻辑当然可以用 if else
的逻辑判断实现,但 Kotlin 提供了一个更优雅的书写方式 ?:
。
val l = b?.length ?: -1
复制代码
b?.length ?: -1
和 if (b != null) b.length else -1
完全等价的。
其实你还可以在 ?:
后面添加任何表达式,比如你可以在后面会用 return
和 throw
(在 Kotlin 中它们都是表达式)。
fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ...
}
复制代码
let 函数
let
是官方 stdlib
提供的标准函数库里面的函数,这个函数巧妙的利用的Kotlin语言的特性让let
接受的表达式参数中的调用方是非空的。
val listWithNulls: List<String?> = listOf("A", null)
for (item in listWithNulls) {
item?.let { println(it) } // prints A and ignores null
}
复制代码
上面代码的只会输出 A
,而不会输出 null
。
需要注意的是,这个方法调用的时候必须要使用?.
操作符调用才能生效哦。如果你的部分代码依赖于一个可选型变量为非空的时候,就可以使用 let 函数。
未完待续~