第1章 Kotlin简介
第2章 快速开始:HelloWorld
Kotlin 是 JetBrains 在 2010 年推出的基于 JVM 的新编程语言。开发者称,设计它的目的是避免 Java 语言编程中的一些难题。比如:
作为一个跨平台的语言,Kotlin 可以工作于任何 Java 的工作环境:服务器端的应用,移动应用(Android版),桌面应用程序。Kotlin这个语言从一开始推出到如今,已经有六年了。官方正式发布首个稳定版本的时间相对比较晚(2016.2),这是一门比较新的语言。其大致发展简史如下:
Kotlin 具有很多下一代编程语言静态语言特性:如类型推断、多范式支持、可空性表达、扩展函数、模式匹配等。
Kotlin的编译器kompiler可以被独立出来并嵌入到 Maven、Ant 或 Gradle 工具链中。这使得在 IDE 中开发的代码能够利用已有的机制来构建,可以在新环境中自由使用
目前AndroiStudio 3.0预览版本已自带Kotlin插件,无需做任何的配置即可开始体验。
Android Studio 3.0之前的版本,需要我们自行做些配置:在Android Studio中打开Settings,选择Plugins选项,点击Browse Repositories,在打开的窗口中搜索Kotlin,点击Install。
【图像1】
下载安装完成后会提示你重启Android Studio,重启之后,就可以使用了
【图像2】
【图像3-5】
【图6-7】
安装完插件的AndroidStudio现在已经拥有开发Kotlin的功能。我们先来尝试它的转换功能:Java -> Kotlin,可以把现有的java文件翻译成Kotlin文件。
打开MainActivity文件,在Code菜单下面可以看到一个新的功能:Convert Java File to Kotlin File,点击后进行转换(如果未进行相关配置,会弹出”Configure Kotlin in Project”的Kotlin配置界面)
【图9】
可以看到转换后的Kotlin文件:MainActivity.kt
package com.kotlin.easy.kotlinandroid
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
MainActivity已经被转换成了Kotlin实现,但是项目目前gradle编译、构建、运行还不能执行,还需要进一步配置一下,让项目支持grade的编译、运行。参看《1.2.1.1 Android Studio 3.0以下版本配置》
未来的是云的世界。不需要搭建本地开发运行环境,直接用浏览器打开 https://try.kotlinlang.org/
【图8】
在源文件的开头定义包名:
package my.demo
import java.util.*
//...
包名不必和文件夹路径一致:源文件可以放在任意位置。
如果将两个含有相同名称的的类库以“*”同时导入,将会存在潜在的冲突。例如
import net.mindview.simple.*
import java.until.*
//由于java.util.和net.mindview.simple.都包含Vector类。如果在创建一个Vector类时:
Vecotr v = Vector()//此时,编译器并不知道调用哪个类库的类,从而会报错误信息
可以:
import net.mindview.simple.Vecotr
import java.until.Vecotr as aVector
val v : Vecotr = Vector()
val vA : aVector = aVector()
fun sum(a: Int , b: Int) : Int{
return a + b
}
//fun sum(a: Int, b: Int) = a + b
fun main(args: Array) {
print("sum of 3 and 5 is ")
println(sum(3, 5))
}
//fun main(args: Array) {
// 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 printSum(a: Int, b: Int) {
println("sum of $a and $b is ${a + b}")
}
fun main(args: Array) {
printSum(-1, 8)
}
更多请参看 《4.1 函数》
Kotlin 中使用 var 和 val 来界定 reference 的可变性,这其实是对 final 修饰符的泛化。带来的结果是,它让我更关注 reference 应该是 mutable 还是 immutable 的。我践行的一种思想是 [ 尽量保持 reference 是 immutable 的 ]
fun main(args: Array) {
//立即初始化
val NUM_A: Int = 100
//推导出类型
val NUM_B = 50
//没有初始化的时候,必须声明类型
val NUM_C: Int
NUM_C = 1
// c += 1 因为c是常量,所以这句代码是会报错的
}
fun main(args: Array<String>) {
var x = 5 // 推导出Int类型
x += 1
println("x = $x")
}
更多请参看 《3.2 属性和字段》
与 java 和 javaScript 一样,Kotlin 支持单行注释和块注释。
// 单行注释
/* 哈哈哈哈
这是块注释 */
/*
第一层块注释
/*
第二层块注释
/*
第三层快注释
这种注释方式在java中是不支持的,但是在kotlin中是支持的。算是一个亮点吧(貌似意义不大)。
*/
*/
*/
参看文档化 Kotlin 代码学习更多关于文档化注释的语法
data class Customer(val name: String, val email: String)
《参看 3.6 数据类》
Kotlin对比于Java的一个最大的区别就是它致力于消除空引用所带来的危险。在Java中,如果我们尝试访问一个空引用的成员可能就会导致空指针异常NullPointerException(NPE)的出现。在Kotlin语言中就解决了这个问题,下面来看看它是如何做到的。
Kotlin 的类型系统旨在从我们的代码中消除 NullPointerException。NPE 的唯一可能的原因可能是
对于初始化,有一些数据不一致(如一个未初始化的 this 用于构造函数的某个地方)
在Kotlin中,类型系统将可空类型和不可空类型进行了区分,例如,String为不可空类型,String?为可空类型,如果将不可空类型赋值为null将会编译不通过。
var a: String = "abc"
a = null // compilation error
var b: String? = "abc"
b = null // ok
val l = a.length
val l = b.length // 编译错误:变量“b”可能为空//如果你想访问 b 的同一个属性,那么这是不安全的,并且编译器会报告一个错误
但是我们还是需要访问该属性,对吧?有几种方式可以做到:
首先,你可以显式检查 b 是否为 null,并分别处理两种可能,编译器会跟踪所执行检查的信息,并允许你在 if 内部调用 length
val l = if (b != null) b.length else -1
同时,也支持更复杂(更智能)的条件:
if (b != null && b.length > 0) {
print("String of length ${b.length}")
} else {
print("Empty string")
}
请注意,这只适用于 b 是不可变的情况(即在检查和使用之间没有修改过的局部变量 ,或者不可覆盖并且有幕后字段的 val 成员),因为否则可能会发生在检查之后 b 又变为 null 的情况
对于这种限制,有好也有坏,给人的感觉有些死板,并且有些麻烦,使用非空引用,必须保证它非空,这个可以接受,使用可空引用,每次还有做检查,好麻烦,语言的设计者为了简单使用,可空引用有一种安全的调用方式,使用?.进行调用
b?.length
如果 b 非空,就返回 b.length,否则返回 null,整个表达式的类型是 Int? 可空Int类型
安全调用在链式调用中很有用。例如,如果一个员工 Bob 可能会(或者不会)分配给一个部门, 并且可能有另外一个员工是该部门的负责人,那么获取 Bob 所在部门负责人(如果有的话)的名字,我们写作:
//如果任意一个属性(环节)为空,这个链式调用就会返回 null
bob?.department?.head?.name
// 如果要只对非空值执行某个操作,安全调用操作符可以与 let 一起使用:
val listWithNulls: List?> = listOf("A", null)
for (item in listWithNulls) {
item?.let { println(it) } // 输出 A 并忽略 null
}
非空断言运算符(!!)将任何值转换为非空类型,若该值为空则抛出异常。
我们可以写 b!! ,这会返回一个非空的 b 值 (例如:在我们例子中的 String)或者如果 b 为空,就会抛出一个 NPE 异常:
val l = b!!.length
因此,如果你想要一个 NPE,你可以得到它,但是你必须显式要求它,否则它不会不期而至
如果 ?: 左侧表达式非空,elvis 操作符就返回其左侧表达式,否则返回右侧表达式。 请注意,当且仅当左侧为空时,才会对右侧表达式求值
当我们有一个可空的引用 r 时,我们可以说“如果 r 非空,我使用它;否则使用某个非空的值 x”:
val l: Int = if (b != null) b.length else -1
val l = b?.length ?: -1
请注意,因为 throw 和 return 在 Kotlin 中都是表达式,所以它们也可以用在 elvis 操作符右侧。这可能会非常方便,例如,检查函数参数:
fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ……
}
如果你有一个可空类型元素的集合,并且想要过滤非空元素,你可以使用 filterNotNull 来实现:
val nullableList: List?> = listOf(1, 2, null, 4)
val intList: List = nullableList.filterNotNull()
如果对象不是目标类型,那么常规类型转换可能会导致 ClassCastException。
var a: Long = 1
val aInt: Int? = a as Int // java.lang.ClassCastException
另一个选择是使用安全的类型转换,如果尝试转换不成功则返回 null:
val aInt: Int? = a as? Int
解构:顾名思义,就是将对象的相关属性,按照解构函数的方式,解构到相应的属性上;是从属性变量构建对象的逆向过程
如果想对某个类的对象使用解构,那么这个类中必须定义相关componentN()函数群,否则是不能解构的;数据类会自动生成组建函数(componentN() 函数群)
一个解构声明同时创建多个变量
val jane = Person("Jane", 35)
val (n, a) = jane
//我们已经声明了两个新变量:name 和 age,并且可以独立使用它们:
println("$n, $a years of age") // 打印结果将是 "Jane, 35 years of age"
在数据类data class中, JVM会生成一些列的组件函数(componentN() 函数群),其与这些函数与类的属性对应, 函数名中的数字 1 到 N, 与属性的声明顺序一致,有了这些组件函数, 就可以在解构声明中使用数据
这也就是说,如果想对某个类的对象使用解构,那么这个类中必须定义相关componentN()函数群,否则是不能解构的;数据类会自动生成组建函数(componentN() 函数群)
一个解构声明会被编译成以下代码:
val name = person.component1()
val age = person.component2()
其中的 component1() 和 component2() 函数是在 Kotlin 中广泛使用的 约定原则 的另一个例子。 (参见像 + 和 *、for-循环等操作符)
任何表达式都可以出现在解构声明的右侧,只要可以对它调用所需数量的 component 函数即可。 当然,可以有 component3() 和 component4() 等等
请注意, componentN() 函数需要用 operator 关键字标记,以允许在解构声明中使用它们
对于以这种方式跳过的组件,不会调用相应的 componentN() 操作符函数
val (_, status) = getResult()
你可以对 lambda 表达式参数使用解构声明语法。 如果 lambda 表达式具有 Pair 类型(或者 Map.Entry 或任何其他具有相应 componentN 函数的类型)的参数,那么可以通过将它们放在括号中来引入多个新参数来取代单个新参数:
map.mapValues { entry -> "${entry.value}!" }
map.mapValues { (key, value) -> "$value!" }
//注意声明两个参数和声明一个解构对来取代单个参数之间的区别:
//{ a //-> …… } // 一个参数
//{ a, b //-> …… } // 两个参数
//{ (a, b) //-> …… } // 一个解构对
//{ (a, b), c //-> …… } // 一个解构对以及其他参数
//如果解构的参数中的一个组件未使用,那么可以将其替换为下划线,以避免编造其名称:
//map.mapValues { (_, value) -> "$value!" }
//可以指定整个解构的参数的类型或者分别指定特定组件的类型:
//map.mapValues { (_, value): Map.Entry -> "$value!" }
//map.mapValues { (_, value: String) -> "$value!" }
变量 a 和 b 的值取自对集合中的元素上调用 component1() 和 component2() 的返回值
for ((a, b) in collection) { …… }
让我们假设我们需要从一个函数返回两个东西。例如,一个结果对象和一个某种状态。 在 Kotlin 中一个简洁的实现方式是声明一个数据类并返回其实例(因为数据类自动声明 componentN() 函数,所以这里可以用解构声明)
data class Result(val result: Int, val status: Status)
fun function(……): Result {
// 各种计算
return Result(result, status)
}
// 现在,使用该函数:
val (result, status) = function(……)
注意:我们也可以使用标准类 Pair 并且让 function() 返回 Pair
for ((key, value) in map) {
// 使用该 key、value 做些事情
}
当然事实上,标准库提供了这样的扩展
operator fun <K, V> Map<K, V>.iterator(): Iterator<Map.Entry<K, V>> = entrySet().iterator() operator fun <K, V> Map.Entry<K, V>.component1() = getKey() operator fun <K, V> Map.Entry<K, V>.component2() = getValue()
为了表示当前的 接收者 我们使用 this 表达式:
如果 this 没有限定符,它指的是最内层的包含它的作用域。要引用其他作用域中的 this,请使用 标签限定符:
要访问来自外部作用域的this(一个类 或者扩展函数, 或者带标签的带接收者的函数字面值)我们使用this@label,其中 @label 是一个代指 this 来源的标签:
class A { // 隐式标签 @A
inner class B { // 隐式标签 @B
fun Int.foo() { // 隐式标签 @foo
val a = this@A // A 的 this
val b = this@B // B 的 this
val c = this // foo() 的接收者,一个 Int
val c1 = this@foo // foo() 的接收者,一个 Int
val funLit = lambda@ fun String.() {
val d = this // funLit 的接收者
}
val funLit2 = { s: String ->
// foo() 的接收者,因为它包含的 lambda 表达式
// 没有任何接收者
val d1 = this
}
}
}
}
区间表达式由具有操作符形式 .. 的 rangeTo 函数辅以 in 和 !in 形成。 区间是为任何可比较类型定义的,但对于整型原生类型,它有一个优化的实现
整型区间(IntRange、 LongRange、 CharRange)有一个额外的特性:它们可以迭代。 编译器负责将其转换为类似 Java 的基于索引的 for-循环而无额外开销
for (i in 1..4) print(i) // 输出“1234”
for (i in 4..1) print(i) // 什么都不输出
for (i in 4 downTo 1) print(i) // 输出“4321”
//能否以不等于 1 的任意步长迭代数字? 当然没问题, step() 函数有助于此
for (i in 1..4 step 2) print(i) // 输出“13”
for (i in 4 downTo 1 step 2) print(i) // 输出“42”
//要创建一个不包括其结束元素的区间,可以使用 until 函数:
for (i in 1 until 10) { // i in [1, 10) 排除了 10
println(i)
}
我们可以在运行时通过使用 is 操作符或其否定形式 !is 来检查对象是否符合给定类型:
if (obj is String) {
print(obj.length)
}
if (obj !is String) { // 与 !(obj is String) 相同
print("Not a String")
}
else {
print(obj.length)
}
val x: String = y as String
//请注意,null 不能转换为 String 因该类型不是可空的, 即如果 y 为空,上面的代码会抛出一个异常
//为了匹配 Java 转换语义,我们必须在转换右边有可空类型,就像:
val x: String? = y as String?
val x: String? = y as? String
fun demo(x: Any) {
if (x is String) {
print(x.length) // x 自动转换为字符串
}
}
反射是语言与库中的一组功能, 可以在运行时刻获取程序本身的信息.在Kotlin中,不仅可以通过发射获取类的信息,同时可以获取函数和属性的信息。也就是说,在在运行时刻得到一个函数或属性的名称和数据类型) 可以通过简单的函数式, 或交互式的编程方式实现.
在Java平台上, 使用反射功能所需要的运行时组件是作为一个单独的JAR文件发布的( kotlinreflect.jar). 这是为了对那些不使用反射功能的应用程序, 减少其运行库的大小. 如果你需要使用反射, 请注意将这个.jar文件添加到你的项目的classpath中.
最基本的反射功能就是获取一个 Kotlin 类的运行时引用. 要得到一个静态的已知的 Kotlin 类的引用, 可以使用”类字面值(class literal) 语法(::)”:
val c = MyClass::class
类引用是一个 KClass 类型的值
请注意,Kotlin 类引用与 Java 类引用不同。要获得 Java 类引用, 请在 KClass 实例上使用 .java 属性。
val widget: Widget = ……
assert(widget is GoodWidget) { "Bad widget: ${widget::class.qualifiedName}" }
你可以获取对象的精确类的引用,例如 GoodWidget 或 BadWidget,尽管接收者表达式的类型是 Widget
使用 :: 操作符来实现函数的引用
//当我们有一个命名函数声明如下:
fun isOdd(x: Int) = x % 2 != 0
//我们可以很容易地直接调用它
isOdd(5)
//我们也可以把它作为一个值传递。例如传给另一个函数。 为此,我们使用 :: 操作符
//:isOdd 是函数类型 (Int) -> Boolean 的一个值
val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd)) // 输出 [1, 3]
fun isOdd(x: Int) = x % 2 != 0
fun isOdd(s: String) = s == "brillig" || s == "slithy" || s == "tove"
val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd)) // 引用到 isOdd(x: Int)
val predicate: (String) -> Boolean = ::isOdd // 引用到 isOdd(x: String)
如果我们需要使用类的成员函数或扩展函数,它需要是限定的。 例如 String::toCharArray 为类型 String 提供了一个扩展函数:String.() -> CharArray
函数组合
fun compose(f: (B) -> C, g: (A) -> B): (A) -> C {
return { x -> f(g(x)) }
}
fun length(s: String) = s.length
val oddLength = compose(::isOdd, ::length)
val strings = listOf("a", "ab", "abc")
println(strings.filter(oddLength)) // 输出 "[a, abc]"
在Kotlin中, 对于包级别的属性可以作为对象来访问, 方法是使用 :: 操作符,我们可以获取一个类型为 KProperty对象。
var x = 1
fun main(args: Array<String>) {
println(::x.get()) // 打印结果为: "1"
::x.set(2)
println(x) // 打印结果为: "2"
}
class A(val p: Int)
fun main(args: Array<String>) {
val prop = A::p
println(prop.get(A(1))) // 输出 "1"
}
val strs = listOf("a", "bc", "def")
println(strs.map(String::length)) // 输出 [1, 2, 3]
val String.lastChar: Char
get() = this[length - 1]
fun main(args: Array<String>) {
println(String::lastChar.get("abc")) // 输出 "c"
}
例如,要查找一个用作 Kotlin 属性 getter 的 幕后字段或 Java方法,可以这样写:
import kotlin.reflect.jvm.*
class A(val p: Int)
fun main(args: Array<String>) {
println(A::p.javaGetter) // 输出 "public final int A.getP()"
println(A::p.javaField) // 输出 "private final int A.p"
}
要获得对应于 Java 类的 Kotlin 类,请使用 .kotlin 扩展属性:
fun getKClass(o: Any): KClass = o.javaClass.kotlin
构造器引用可以用于使用函数类型对象的地方, 但这个函数类型接受的参数应该与构造器相同, 返回值应该是构造器所属类的对象实例. 引用构造器使用 :: 操作符, 再加上类名称.
class Foo
fun function(factory: () -> Foo) {
val x: Foo = factory()
}
//调用了
function(::Foo)
你可以引用特定对象的实例方法:
val numberRegex = "\\d+".toRegex()
println(numberRegex.matches("29")) // 输出“true”
val isNumber = numberRegex::matches//【看这里!】
println(isNumber("29")) // 输出“true”
取代直接调用方法 matches 的是我们存储其引用。 这样的引用会绑定到其接收者上。 它可以直接调用(如上例所示)或者用于任何期待一个函数类型表达式的时候:
val strings = listOf("abc", "124", "a70")
println(strings.filter(numberRegex::matches)) // 输出“[124]”
比较绑定的类型和相应的未绑定类型的引用。 绑定的可调用引用有其接收者“附加”到其上,因此接收者的类型不再是参数:
val isNumber: (CharSequence) -> Boolean = numberRegex::matches
val matches: (Regex, CharSequence) -> Boolean = Regex::matches
属性引用也可以绑定:
val prop = "abc"::length
println(prop.get()) // 输出“3”
自 Kotlin 1.2 起,无需显式指定 this 作为接收者:this::foo 与 ::foo 是等价的。
类型别名为现有类型提供替代名称。 如果类型名称太长,你可以另外引入较短的名称,并使用新的名称替代原类型名。
类型别名不会引入新类型。 它们等效于相应的底层类型,它有助于缩短较长的泛型类型
typealias NodeSet = Set
typealias FileTable<K> = MutableMap<K, MutableList<File>>
typealias MyHandler = (Int, String, Any) -> Unit
typealias Predicate = (T) -> Boolean
class A {
inner class Inner
}
class B {
inner class Inner
}
typealias AInner = A.Inner
typealias BInner = B.Inner
Kotlin 允许我们为自己的类型提供预定义的一组操作符的实现。这些操作符具有固定的符号表示 (如 + 或 *)和固定的优先级。为实现这样的操作符,我们为相应的类型(即二元操作符左侧的类型和一元操作符的参数类型)提供了一个固定名字的成员函数或扩展函数。 重载操作符的函数需要用 operator 修饰符标记。
另外,我们描述为不同操作符规范操作符重载的约定
表达式 | 翻译为 |
---|---|
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
这个表是说,当编译器处理例如表达式 +a 时,它执行以下步骤:
注意 这些操作以及所有其他操作都针对基本类型做了优化,不会为它们引入函数调用的开销。
以下是如何重载一元减运算符的示例:
data class Point(val x: Int, val y: Int)
operator fun Point.unaryMinus() = Point(-x, -y)
val point = Point(10, 20)
println(-point) // 输出“(-10, -20)”
表达式 | 翻译为 |
---|---|
a++ | a.inc() + 见下文 |
a– | a.dec() + 见下文 |
inc() 和 dec() 函数必须返回一个值,它用于赋值给使用 ++ 或 – 操作的变量。它们不应该改变在其上调用 inc() 或 dec() 的对象
编译器执行以下步骤来解析后缀形式的操作符,例如 a++:
计算表达式的步骤是:
对于前缀形式 ++a 和 –a 以相同方式解析,其步骤是:
操作符重载
表达式是有返回值的;语句只是一条命令
和Java一样,Kotlin也是基于JVM的,不同的是,后者是静态类型语言,意味着所有变量和表达式类型在编译时已确定。在Java中,通过装箱和拆箱在基本数据类型和包装类型之间相互转换,而,Kotlin中,所有变量的成员方法和属性都是对象
在 Kotlin 中,所有东西都是对象,在这个意义上讲我们可以在任何变量上调用成员函数和属性。
一些类型可以有特殊的内部表示——例如,数字、字符和布尔值可以在运行时表示为原生类型值(int|String|boolean),但是对于用户来说,它们看起来就像普通的类
原始类型:语言自有的基础数据类型,从内存操作方式上可以分为 值类型(value type)和引用类型(reference type)。 其中前者的实现通过值(而不是引用,或者叫指针)直接传递的,后者只是指针引用的修改。
我们一直以为“Java 有值类型,原始类型 int,boolean 等是值类型”,其实是长久以来的一种误解,它混淆了实现和语义的区别(实现上存在值类型,语义上不存在)。
王垠在文章 [《Java 有 value type 吗》?](http://www.yinwang.org/blog-cn/2016/06/08/java-value-type) 提出了一个很有趣的想法:
文中假设将 Java 中的所有原始类型(int boolean long...)都设计为 reference type,你会发现它依然和 value type 表现一致。
一个很重要的原因是 Java 中并不具备 C 语言中的 deref 操作符,无法修改 reference 指向中真实的 value,你在对它进行再赋值时,仅仅是改变了它的指向
就如int和Integer,前者是我们常规认识中的值类型,后者是引用类型,但是两者在使用方式和外在表现上是一样的:因为Integer并不提供 setValue() 这样改变内值的函数。 由此可以看出Java 是在刻意保持 Integer 和 int 表现的一致性(自动装箱也是一方面)
从这个角度来看,Java 在语义上是没有值类型的。值类型和引用类型如果同时并存,程序员必须能够在语义上感觉到它们的不同,然而不管原始类型是值类型还是引用类型,作为程序员,你无法感觉到任何的不同。所以你完全可以认为 Java 只有引用类型,把原始类型全都当成引用类型来用,虽然它们确实是用值实现的
然而在Kotlin中就可以减少该方面的疑惑:在 Kotlin 中只有 Int 而没有 int, Int以像 Integer 一样提供一系列额外的操作函数,然而实际上在 JVM 上却储存的是 int 类型!(int 效率会更高)
也就是说 Kotlin 中 Int 类型编译成字节码后实际上是 int 类型,它利用编译器隐藏了一些实现,把 int 等原子类型看起来更像 reference type,并提供了额外的函数:
10.ushr(10)
这也就符合了 Kotlin 中所有东西都是对象的设计原则
思考下 Kotlin 中的 Int 和 Int?(也就是 nullable 的 Int) 类型:
如果Kotlin 中的 Int 类型实际上是用 value type 的 int 实现的话,怎么可能会存在 nullable 的情况(只有 reference type 才有 null 这个概念)!!!
Kotlin 处理数字在某种程度上接近 Java,但是并不完全相同。例如,对于数字没有隐式拓宽转换(如 Java 中 int 可以隐式转换为long——译者注),另外有些情况的字面值略有不同
Kotlin 提供了如下的内置引用类型来表示数字(与 Java 很相近):
Type | Bit width |
---|---|
Double | 64 |
Float | 32 |
Long | 64 |
Int | 32 |
Short | 16 |
Byte | 8 |
注意在 Kotlin 中字符不是数字
例:
var a: Byte = 2
var b: Short = 2
var c: Int = 2
var d: Long = 2L //长整型由大写字母L标记
var e: Float = 2f //单精度浮点型由小写字母f或大写字符F标记
var f: Double = 2.0
println(" a => $a \n b => $b \n c => $c \n d => $d \n e => $e \n f => $f);
输出结果为:
a => 2
b => 2
c => 2
d => 2
e => 2.0
f => 2.0
注意: 不支持八进制
数值常量字面值有以下几种:
Kotlin 同样支持浮点数的常规表示方法:
数字字面值支持下划线(自 1.1 起),使数字常量更易读:
val oneMillion = 1_000_000
val creditCardNumber = 1234_5678_9012_3456L
val socialSecurityNumber = 999_99_9999L
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_10010010
在Kotlin中,存在数字的装箱,但是不存在拆箱。因为Kotlin是没有基本数据类型的,Kotlin是万般皆对象的原则。故不存在和Java中的类似int是数据类型,Integer是整型的引用类型
在Kotlin中要实现装箱操作。首先要了解可空引用。即类似Int?(只限数值类型)这样的
val numValue: Int = 123
//装箱的过程,其实装箱之后其值是没有变化的
val numValueBox: Int? = numValue
println("装箱后: numValueBox => $numValueBox")
//输出结果为:
装箱后: numValueBox => 123
判断两个数值是否相等(==),判断两个数值在内存中的地址是否相等(===),其实上面的装箱操作之后其内存中的地址根据其数据类型的数值范围而定
”==”与”===”是不同的,一个是判断值是否相等,一个是判断值及类型是否完全相等
- ==
结构相等由 ==(以及其否定形式 !=)操作判断
1. 如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等
2. 如果作用于引用类型的变量,则比较的是所指向的对象的地址
- ===
引用相等由 ===(以及其否定形式 !==)操作判断。a === b 当且仅当 a 和 b 指向同一个对象时求值为 true
1. 对于基本数据类型,如果类型不同,其结果就是不等。如果同类型相比,与“==”一致,直接比较其存储的 “值”是否相等;
2. 对于引用类型,与“==”一致,比较的是所指向的对象的地址
- equals
1. equals方法不能作用于基本数据类型的变量
2. 如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;
3.诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容。
数字装箱不必保留同一性:
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) // 输出“true”
val boxedA: Int? = a
val anotherBoxedA: Int? = a
print(boxedA == anotherBoxedA) // 输出“true”
当需要可空引用时,像数字、字符会被装箱。装箱操作不会保留同一性
较小的类型不能隐式转换为较大的类型,这意味着在不进行显式转换的情况下我们不能把 Byte 型值赋给一个 Int 变量
val b: Byte = 1 // OK, 字面值是静态检测的
val i: Int = b // 错误
val i: Int = b.toInt() // OK: 显式拓宽
这是由于不同的表示方式,较小类型并不是较大类型的子类型。 如果它们是的话,就会出现下述问题:
// 假想的代码,实际上并不能编译:
val a: Int? = 1 // 一个装箱的 Int (java.lang.Integer)
val b: Long? = a // 隐式转换产生一个装箱的 Long (java.lang.Long)
print(a == b) // 惊!这将输出“false”鉴于 Long 的 equals() 检测其他部分也是 Long
每个数字类型支持如下的转换:
缺乏隐式类型转换并不显著,因为类型会从上下文推断出来,而算术运算会有重载做适当转换,例如:
// 30L + 12 -> Long + Int => Long
val num = 30L + 12
print(num)
输出结果为:42
Kotlin支持数字运算的标准集,运算被定义为相应的类成员(但编译器会将函数调用优化为相应的指令)
本节讨论的浮点数操作如下:
当其中的操作数 a 与 b 都是静态已知的 Float 或 Double 或者它们对应的可空类型(声明为该类型,或者推断为该类型,或者智能类型转换的结果是该类型),两数字所形成的操作或者区间遵循 IEEE 754 浮点运算标准。
然而,为了支持泛型场景并提供全序支持,当这些操作符并非静态类型为浮点数(例如是 Any、 Comparable<……>、 类型参数)时,这些操作使用为 Float 与 Double 实现的不符合标准的 equals 与 compareTo,这会出现:
布尔用 Boolean 类型表示,它有两个值:true 和 false。
若需要可空引用布尔会被装箱。
内置的布尔运算有:
数组在 Kotlin 中使用 Array 类来表示,它定义了 get 和 set 函数(按照运算符重载约定这会转变为 [])和 size 属性,以及一些其他有用的成员函数:
- [] 运算符代表调用成员函数 get() 和 set()
class Array private constructor() {
val size: Int
operator fun get(index: Int): T
operator fun set(index: Int, value: T): Unit
operator fun iterator(): Iterator
// ……
}
var arr1 = arrayOf("0","2","3",'a',32.3f)
var arr2 = arrayOfNulls(3)
// 创建一个 Array<String> 初始化为 ["0", "1", "4", "9", "16"]
val asc3 = Array(5, { i -> (i * i).toString() })
注意: 与 Java 不同的是,Kotlin 中数组是不型变的(invariant)。这意味着 Kotlin 不让我们把 Array 赋值给 Array,以防止可能的运行时失败(但是你可以使用 Array, 参见类型投影)
Kotlin 也有无装箱开销的专门的类来表示原生类型数组: ByteArray、 ShortArray、IntArray 等等。这些类和 Array 并没有继承关系,但是它们有同样的方法属性集。它们也都有相应的工厂方法:
val x: IntArray = intArrayOf(1, 2, 3)
x[0] = x[1] + x[2]
Kotlin中不支持字符串类型这种原始类型数组,可以看源码Arrays.kt这个类中并没有字符串数组的声明。而源码中StringArray.kt这个类并不是声明字符串型数组的
字符用 Char 类型表示。字符不能直接当作数字
fun check(c: Char) {
if (c == 1) { // 错误:类型不兼容
// ……
}
}
可以显式把字符转换为 Int 数字:
fun decimalDigitValue(c: Char): Int {
if (c !in '0'..'9')
throw IllegalArgumentException("Out of range")
return c.toInt() - '0'.toInt() // 显式转换为数字
}
当需要可空引用时,像数字、字符会被装箱。装箱操作不会保留同一性
String表示字符串类型。其是不可变的。所以字符串的元素可以通过索引操作的字符:str[index]来访问。可以使用for循环迭代字符串: 其中str[index]中的str为要目标字符串,index为索引
val str: String = "kotlin"
println("str => $str")
//迭代
for (s in str){
print(s)
print("\t")
}
输出结果为:
str => kotlin
k o t l i n
Kotlin 有两种类型的字符串字面值: 转义字符串可以有转义字符,以及原生字符串可以包含换行和任意文本
val s = "Hello, world!\n"
val text = """
for (c in "foo")
print(c)
"""
可以通过 trimMargin() 函数去除前导空格:
val text = """
|Tell me and I forget.
|Teach me and I remember.
|Involve me and I learn.
|(Benjamin Franklin)
""".trimMargin()
默认 | 用作边界前缀,但你可以选择其他字符并作为参数传入,比如 trimMargin(“>”)
字符串可以包含模板表达式 ,即一些小段代码,会求值并把结果合并到字符串中。原生字符串和转义字符串内部都支持模板。
模板表达式以美元符( )开头,在 符号后面加上变量名或大括号中的表达式
val i = 10
val s = "i = $i" // 求值结果为 "i = 10"
val s = "abc"
val str = "$s.length is ${s.length}" // 求值结果为 "abc.length is 3"
如果你需要在原生字符串中表示字面值 $ 字符(它不支持反斜杠转义),你可以用下列语法:
val price = """
${'$'}9.99
"""
在 Kotlin 中,if是一个表达式,即它会返回一个值。 因此就不需要三元运算符(条件 ? 然后 : 否则),因为普通的 if 就能胜任这个角色
- 如果你使用 if 作为表达式而不是语句(例如:返回它的值或者把它赋给变量),该表达式必须需要有 else 分支
// 作为表达式
val max = if (a > b) a else b
// 传统用法
var max = a
if (a < b) max = b
// With else
var max: Int
if (a > b) {
max = a
} else {
max = b
}
if的分支可以是代码块,最后的表达式作为该块的值:
val max = if (a > b) {
print("Choose a")
a
} else {
print("Choose b")
b
}
when 取代了类 C 语言的 switch 操作符。when 将它的参数和所有的分支条件顺序比较,直到某个分支满足条件。其最简单的形式如下:
when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> { // 注意这个块
print("x is neither 1 nor 2")
}
}
when 既可以被当做表达式使用也可以被当做语句使用。如果它被当做表达式, 符合条件的分支的值就是整个表达式的值,如果当做语句使用, 则忽略个别分支的值。(像 if 一样,每一个分支可以是一个代码块,它的值是块中最后的表达式的值。)如果其他分支都不满足条件将会求值 else 分支
- 如果 when 作为一个表达式使用,则必须有 else 分支, 除非编译器能够检测出所有的可能情况都已经覆盖了。
//条件可以使用任意表达式,不仅局限于常量
when(num > 5){
true -> {
println("num > 5")
}
false ->{
println("num < 5")
}
else -> {
println("num = 5")
}
}
//如果很多分支需要用相同的方式处理,则可以把多个分支条件放在一起,用逗号分隔:
when (x) {
0, 1 -> print("x == 0 or x == 1")
else -> print("otherwise")
}
//我们可以用任意表达式(而不只是常量)作为分支条件
when (x) {
parseInt(s) -> print("s encodes 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)一个特定类型的值。注意: 由于智能转换,你可以访问该类型的方法和属性而无需任何额外的检测
fun hasPrefix(x: Any) = when(x) {
is String -> x.startsWith("prefix")
else -> false
}
// 智能转换
var a: Int = 2
when(a){
!is Int -> {
println("$a 不是一个Int类型的值")
}
else -> {
a = a.shl(2)
println("a => $a")
}
}
//when 也可以用来取代 if-else if链。 如果不提供参数,所有的分支条件都是简单的布尔表达式,而当一个分支的条件为真时则执行该分支:
when {
x.isOdd() -> print("x is odd")
x.isEven() -> print("x is even")
else -> print("x is funny")
}
Kotlin废除了Java中的for(初始值;条件;增减步长)这个规则。但是Kotlin中对于for循环语句新增了其他的规则,来满足刚提到的规则。
// 循环5次,且步长为1的递增
for (i in 0 until 5){
print("i => $i \t")
}
//输出结果为 i => 0 i => 1 i => 2 i => 3 i => 4
for (i in 0 .. 5){
print("i => $i \t")
}
//输出结果为 i => 0 i => 1 i => 2 i => 3 i => 4 i => 5
for (i in 10 until 16 step 2){
print("i => $i \t")
}
//输出结果为 i => 10 i => 12 i => 14
// 循环5次,且步长为1的递减
for (i in 15 downTo 11){
print("i => $i \t")
}
//输出结果为 i => 15 i => 14 i => 13 i => 12 i => 11
for 可以循环遍历任何提供了迭代器的对象。即:
for 循环可以对任何提供迭代器(iterator)的对象进行遍历,这相当于像 C# 这样的语言中的 foreach 循环。语法如下:
for (item in collection) print(item)
for (item: Int in ints) {
// ……
}
var arrayListThree = arrayOf(2,'a',3,false,9)
var iterator: Iterator = arrayListThree.iterator()
while (iterator.hasNext()){
println(iterator.next())
}
可以查看Array.kt这个类。可以看见其中的iterator()函数,而这个函数实现了Iterator接口
for (i in "abcdefg"){
print("i => $i \t")
}
//输出结果为: i => a i => b i => c i => d i => e i => f i => g
对数组的 for 循环会被编译为并不创建迭代器的基于索引的循环。
var arrayListOne = arrayOf(10,20,30,40,50)
for (i in arrayListOne){
print("i => $i \t")
}
//输出结果为: i => 10 i => 20 i => 30 i => 40 i => 50
注意这种“在区间上遍历”会编译成优化的实现而不会创建额外对象
var arrayListTwo = arrayOf(1,3,5,7,9)
for (i in arrayListTwo.indices){
println("arrayListTwo[$i] => " + arrayListTwo[i])
}
var arrayListTwo = arrayOf(1,3,5,7,9)
for ((index,value) in arrayListTwo.withIndex()){
println("index => $index \t value => $value")
}
//输出结果为:
//index => 0 value => 1
//index => 1 value => 3
//index => 2 value => 5
//index => 3 value => 7
//index => 4 value => 9
while 和 do..while 照常使用
while (x > 0) {
x--
}
do {
val y = retrieveData()
} while (y != null) // y 在此处可见
在循环中 Kotlin 支持传统的 break 和 continue 操作符
集合的分类:
在Kotlin中,明确的区分了可变和只读的集合(list, set, map等),明确的确定了集合的可读性,有助于良好的编码,以及便于Bug的规避
Kotlin 的 List 类型是一个提供只读操作如 size、get等的接口。和 Java 类似,它继承自 Collection 进而继承自 Iterable。改变 list 的方法是由 MutableList 加入的。这一模式同样适用于 Set/MutableSet 及 Map
fun iterator(): Iterator - 返回该集合的元素的迭代器
fun iterator(): MutableIterator<T> - 返回该集合的元素的迭代器
//成员属性
val size: Int - 集合中元素的数量
//函数
isEmpty(): Boolean - 判断集合是否为空
fun contains(E): Boolean - 判断集合中是否包含某一元素
fun containsAll(Collection): Boolean - 判断集合中是否包含某一集合
fun iterator(): Iterator - 返回该只读集合的元素的迭代器
fun add(E): Boolean - 向集合中添加元素。添加成功,返回ture,否则返回false。
fun addAll(Collection): Boolean - 向集合中添加一个集合。添加成功,返回ture,否则返回false。
fun remove(E): Boolean - 移除集合中的元素。移除成功,返回ture,否则返回false。
fun removeAll(Collection): Boolean - 移除集合中的一个集合。移除成功,返回ture,否则返回false。
fun retainAll(Collection): Boolean - 判断集合中是否包含一个集合。如果包含,返回ture,否则返回false。
fun clear(): Unit - 将集合中的元素清空
Set是最简单的一种集合。集合中的对象不按特定的方式排序,并且没有重复对象
Set中不能存放重复的对象,对于Kotlin中,标准的数据类型,比如基本数据类型、String等都有了相应的判定方式,如果我们自定义了类,该如何判断对象是不是重复的呢?
在Kotlin中,判断两个对象的是否重复标准是hashCode()和equals()两个参考值,也就是说只有两个对象的hashCode值一样与equals()为真时,才认为是相同的对象。
所以自定义的类必须要要重写hashCode()和equals()两个函数
interface Set : Collection {
val size: Int - 集合中元素的数量
//查询操作
fun isEmpty(): Boolean - 判断集合是否为空
fun contains(E): Boolean - 判断集合中是否包含某一元素
fun iterator(): Iterator - 返回该只读集合的元素的迭代器
//批量操作
fun containsAll(Collection): Boolean - 判断集合中是否包含某一集合
}
interface MutableSet : Set, MutableCollection {
//查询操作
//fun iterator(): MutableIterator - 返回集合元素的迭代器
//元素操作
//fun add(E): Boolean - 向集合中添加元素。添加成功,返回ture,否则返回false。
//fun remove(E): Boolean - 移除集合中的元素。移除成功,返回ture,否则返回false。
//批量操作
fun addAll(Collection): Boolean - 向集合中添加一个集合。添加成功,返回ture,否则返回false。
fun removeAll(Collection): Boolean - 移除集合中的一个集合。移除成功,返回ture,否则返回false。
retainAll(Collection): Boolean - 判断集合中是否包含一个集合。如果包含,返回ture,否则返回false。
fun clear(): Unit - 将集合中的元素清空
}
Kotlin并没有HashSet、TreeSet和LinkedHashSet
假如,实际开发过程中,如果用到了这三个中某一个Set,比如想通过hashCode来存取对象,此时该如何是好呢?
鉴于与Java的兼容性,kotlin封装了生成HashSet、TreeSet或者LinkedHashSet的函数,我们可以用Java中的这三个类:
List的特征是其元素以线性方式存储,集合中可以存放重复对象
与set一样,Kotlin并没有提供创建List的函数,如果想创建一个List,可以调用标准库中的方法,listOf() , mutableListOf()
val bookA = Book("A", 20, "Jone")
val bookB = Book("B", 21, "Green")
val bookC = Book("C", 22, "Mark")
val bookD = Book("D", 23, "Node")
val bookE = Book("E", 24, "Blue")
val listBook = listOf(bookA, bookB, bookC, bookD, bookE)
目前 listOf 方法是使用 array list 实现的,但是未来可以利用它们知道自己不能变的事实,返回更节约内存的完全不可变的集合类型
interface List : Collection {
//查询操作
val size: Int - 集合中元素的数量
fun isEmpty(): Boolean - 判断集合是否为空
fun contains(E): Boolean - 判断集合中是否包含某一元素
fun iterator(): Iterator - 返回该只读集合的元素的迭代器
//批量操作
fun containsAll(Collection): Boolean - 判断集合中是否包含某一集合
//索引查询操作
fun get(Int): E - 查寻集合中某个位置的元素
//搜索操作
fun indexOf(E): Int - 返回列表中指定元素首次出现的索引,如果元素不包含在列表中,则返回-1
fun lastIndexOf(E): Int - 返回列表中指定元素最后一次出现的索引,如果元素不包含在列表中,则返回-1
//迭代器
fun listIterator(): ListIterator - 返回一个集合的迭代器
fun listIterator(Int): ListIterator - 从指定位置开始,返回集合的迭代器
fun subList(fromIndex: Int, toIndex: Int): List - 返回此列表中指定的[fromIndex](包括)和[toIndex](不包括)之间的集合
}
interface MutableList : List, MutableCollection {
//修改操作
fun add(E): Boolean - 向集合中添加元素。添加成功,返回ture,否则返回false。
fun remove(E): Boolean - 移除集合中的元素。移除成功,返回ture,否则返回false。
//批量操作
fun addAll(Collection): Boolean - 向集合中添加一个集合。添加成功,返回ture,否则返回false。
fun removeAll(Collection): Boolean - 移除集合中的一个集合。移除成功,返回ture,否则返回false。
retainAll(Collection): Boolean - 判断集合中是否包含一个集合。如果包含,返回ture,否则返回false。
fun clear(): Unit - 将集合中的元素清空
//索引操作
fun set(Int, E): E - 用指定的元素替换此列表中指定位置的元素,返回该位置的原元素
fun add(Int, E): Unit - 在指定位置添加一个元素
fun removeAt(Int): E - 移除指定索引处的元素
//迭代器
fun listIterator(): MutableListIterator - 返回一个集合的迭代器
fun listIterator(Int): MutableListIterator - 从指定位置开始,返回集合的迭代器
fun subList(fromIndex: Int, toIndex: Int): MutableList - 返回此列表中指定的[fromIndex](包括)和[toIndex](不包括)之间的集合
}
class Controller {
private val _items = mutableListOf()
val items: List get() = _items.toList()
}
toList是一个扩展函数,其只是单纯的赋值list内的内容,从而返回的是一个只读的List
即便原来的mutableListOf被修改了,此处得到的只读list也不会改变
“`
val mutalist = mutableListOf(a1, a2, a3)
val list = mutalist.toList()
for(item in list){
print(“$item,”)
}
mutalist.add(a4)
println()
println(“list:”)
for(item in list){
print(“$item,”)
}
//输出为
1,2,3,
list:
1,2,3,
### 3.3.4 Map与MutableList
Map是一种把键对象和值对象映射的集合,它的每一个元素都包含一对键对象和值对象。 Map没有继承于Collection接口。从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。
Kitlin中,与list、set一样的是,Map也分为只读map和可变两种Map。
Kotlin中,创建Map时,需调用标准库中的系列函数,如mapOf(),mutableMapOf()
#### 3.3.4.1 创建
- Entry
- Pair
- Pair类用来存储两个值,这两个值可以是任何类型的,可以用于任何场景,比如Map,List.
- Pair类有扩展函数 to,可快速创建Pair类的实例。
val bookA = Book("A", 20, "Jone")
val bookB = Book("A", 21, "Green")
val bookC = Book("C", 20, "Mark")
val bookD = Book("D", 22, "Node")
val bookE = Book("A", 20, "Blue")
// 基于Pair扩展函数 to
val map= mapOf(1 to bookA, 2 to bookB, 3 to bookC, 4 to bookD, 5 to bookE)
var mapMutable = mutableMapOf(1 to bookA, 2 to bookB, 3 to bookC, 4 to bookD, 5 to bookE)
//基于Pair
val mapPair = mapOf(Pair(1, bookA), Pair(1, bookB))
//创建一个空的Map
val mapEmpty = emptyMap()
//HashMap
val hashMap = hashMapOf(1 to bookA, 2 to bookB)
//LinkedHashMap
val linkedHashMap = linkedMapOf(1 to bookA, 2 to bookB)
- HashMap:Map基于散列表的实现。插入和查询“键值对”的开销是固定的。可以通过构造器设置容量capacity和负载因子load factor,以调整容器的性能。
- LinkedHashMap: 类似于HashMap,但是迭代遍历它时,取得“键值对”的顺序是其插入次序,或者是最近最少使用(LRU)的次序。只比HashMap慢一点。而在迭代访问时发而更快,因为它使用链表维护内部次序。
- mapOf和mutableMapOf()创建的Map是基于Java的LinkedHashMap。
- Kotlin现并不支持TreeMap 、WeakHashMao 、IdentifyHashMap
#### 3.3.4.2 操作
- 遍历操作
- 不管Map还是MutableMap获取到的键setKey、值setValues或者键/值对setEntry的set,都是只读的。
即便是MutableMap获取到的是MutableSet,其也是只读的,因为在Map/或者Map中,将这些set设置为了只读常量
- 使用keySet()抽取key序列,将map中的所有keys生成一个Set。
使用values()抽取value序列,将map中的所有values生成一个Collection。
为什么一个生成Set,一个生成Collection?那是因为,key总是独一无二的,value允许重复
//1.判断映射中元素的数量
var mapMutable = mutableMapOf(1 to bookA, 2 to bookB, 3 to bookC, 4 to bookD, 5 to bookE)
val size = mapMutable.siz // size = 5
//2.Map元素中Key的集合
val setKey = map.keys
setKey.forEach(::print) // Log: 1,2,3,4,5
var setMutableKey = mapMutable.keys
setMutableKey.forEach(::print) // Log: 1,2,3,4,5
//3.Map中元素的集合
val setValues = map.values
setValues.forEach(::println)
var setMutableValues = mapMutable.values
setMutableValues.forEach(::println)
//4.Map中元素的键/值对的集合
val setEntry = map.entries
setEntry.forEach{
println(“key: it.key,value: {it.value}”)
}
var setMutableEntry = mapMutable.entries
setMutableEntry.forEach{
println(“key: it.key,value: {it.value}”)
}
- 查询操作
//1.判断Mpa是否为空
map.isEmpty()
//2.判断Map是否包含某个Key
map.containsKey(2)
//3.判断Map是否包含某个元素
map.containsValue(bookA)
//4.查询Map中指定Key的元素,若存在,返回元素值,否则返回nul
map.get(2)
- 元素操作
//1.添加元素:将指定的[value]与映射中指定的[key]关联
val valuePut: Boo = mapMutable.put(6, bookE)
//2.移除元素:移除映射中指定Key的元素,返回被移除的元素,若指定的Key的元素不存在,返回null
val valueRemove: Book = mapMutable.remove(6, bookE)
//3.清空整个映射
map.clear()
### 3.3.5 强转
MutableList可以强转为List,但是**指向的是同一块内存区域**,MutableList修改后,List也会修改。 其他的也是类似的
> 如果想获得新的list对象,可使用toList
val numbers: MutableList = mutableListOf(1, 2, 3)
val readOnlyView: List = numbers
println(numbers) // 输出 “[1, 2, 3]”
numbers.add(4)
println(readOnlyView) // 输出 “[1, 2, 3, 4]”
readOnlyView.clear() // -> 不能编译
### 3.3.6 聚合类操作
所谓的聚合操作就是从值集合计算得出单个值
- any
- 如果集合中至少有一个元素与指定条件相符,则返回true;否则,返回false.在any的函数体内,指定判断元素的条件
- 函数参数类型 predicate: (T) -> Boolean
- 返回值:Boolean
val any_22: Boolean = listBook.any { it.page > 22 }
val any_33: Boolean = listBook.any { it.page > 33 }
println(“any_22: any22,any33: any_33”) // Log: any_22: true, any_33: false
- all
- 如果集合中所有元素与指定条件相符,则返回true;否则,返回false.在all的函数体内,指定判断元素的条件。
- 函数参数类型 predicate: (T) -> Boolean
- 返回值:Boolean
val all_20: Boolean = listBook.all { it.page >= 20 }
val all_22: Boolean = listBook.all { it.page > 22 }
println(“all_20: all20,all22: all_22”) // Log: all_22: true, all_33: false
- none
- 如果没有元素与指定条件相符,则返回true;否则,返回false。
- 函数参数类型predicate: (T) -> Boolean
- 返回值:Boolean
val none: Boolean = listBook.none { it.page > 50 }
println(“none: $none”) // Log:true
- count
- 返回集合与指定条件相符的元素个数。在count的函数体内,指定判断元素的条件。若不指定条件,返回集合中所有元素的个数。
- 函数参数类型 predicate: (T) -> Boolean
- 返回值:Int
val count: Int = listBook.count {it.page > 23}
println(“count: $count”)
- maxBy
- 返回使指定函数产生最大值的第一个元素。如果没有元素,则返回null。其接受的参数类型为函数参数。
- 函数参数类型selector: (T) -> R
- 返回值:T?
- minBy
- 返回使指定函数产生最小值的第一个元素。如果没有元素,则返回null。
val pageMax: Book? = listBook.maxBy (::maxPage)
println(“pageMax:$pageMax”)
fun maxPage(book: Book?): Int {
return book!!.page
}
//Log
pageMax:Book(name=’E’, page=24, author=’Blue’)
- sumBy
- 返回集合中元素进转换函数产生值的总和,其返回值类型为Int
- 函数参数类型selector: (T) -> Int
- 返回值:List
val sumBy:Int = listBook.sumBy(Book::page)
println(sumBy) // Log
- fold
- 将对集合从第一个到最后一个元素的操作结果进行累加,并加上初始值。
- 函数参数类型
R: 初始值
operation: (T, R) -> R
- 返回值:R
- foldRight
- 与fold类似,不同的是从最后一个元素到第一个元素。另外,传递的函数的参数类型也不一样,哪里不同,可以对比下ops和opsRight两个函数的参数类型。
- 函数参数类型
R: 初始值
operation: (T, R) -> R
- 返回值:R
fun ops(total: Int, book: Book): Int {
var t = total
t += book.page
println(“total: t,book: book”)
return t
}
val fold = listBook.fold(0, ::ops)
println(“fold: $fold”) // Log: fold: 110
// Log
total: 20, book: Book(name=’A’, page=20, author=’Jone’)
total: 41, book: Book(name=’B’, page=21, author=’Green’)
total: 63, book: Book(name=’C’, page=22, author=’Mark’)
total: 86, book: Book(name=’D’, page=23, author=’Node’)
total: 110, book: Book(name=’E’, page=24, author=’Blue’)
fold: 110
- reduce
- 与fold类似,不同的是其结果不包括初始值,只是将对集合从第一个元素到最后一个元素的操作结果进行累加
- 与fold不同的是,其函数类型也不一样,从reduce源码可以看出,其接受的函数接收的两个参数分别是list的数据类型T和它的父类S,最后该函数参数的返回值的类型也为S。
也就是说reduce最后返回的应该是S的一个实例
- reduceRight
- 同reduce,不同的是从最后一个元素与其前面的元素按照某个条件相加,直到第一个元素,并将求出的值返回
fun pageReduce(bookTotal: Book, book: Book): Book {
bookTotal.page += book.page
return bookTotal
}
val pageReduce = listBook.reduce (::pageReduce)
println(pageReduce)
println(listBook[0])
//仔细观察下Log,其name是”A”,也就是说这应该是第一个元素对象的name;然后再看Log2,第一个元素的page值已经修改为了110
// Log1
Book(name=’A’, page=110, author=’Jone’)
// Log2
//在循环中,函数operation修改了accumulator的值,也就意味着集合中第一个元素的值被修改了
Book(name=’A’, page=110, author=’Jone’)
- forEach
- 对每个元素执行指定的操作,实际效果就是对集合的遍历
listBook.forEach (::println)
// Log
book: Book(name=’A’, page=20, author=’Jone’)
book: Book(name=’B’, page=21, author=’Green’)
book: Book(name=’C’, page=22, author=’Mark’)
book: Book(name=’D’, page=23, author=’Node’)
- forEachIndexed
- 与forEach类似,不同的是在遍历元素的同时获得元素的索引
listBook.forEachIndexed{index, book ->
run {
println(“index: index,book: book”)
}
}
// Log
index: 0, book: Book(name=’A’, page=20, author=’Jone’)
index: 1, book: Book(name=’B’, page=21, author=’Green’)
index: 2, book: Book(name=’C’, page=22, author=’Mark’)
index: 3, book: Book(name=’D’, page=23, author=’Node’)
index: 4, book: Book(name=’E’, page=24, author=’Blue’)
### 3.3.7 筛选类操作
- drop
- 返回集合中除去前N项的所有元素。
- 函数参数类型Int
- 返回值:List
- dropLast
- 返回集合中除去最后N项的所有元素
val listDrop = listBook.drop(2)
listDrop.forEach(::println)
// Log
Book(name=’C’, page=22, author=’Mark’)
Book(name=’D’, page=23, author=’Node’)
Book(name=’E’, page=24, author=’Blue’)
- dropWhile
- dropWhile接收了一个函数参数,其返回值是Boolean,返回第一个不满足条件元素的以后的所有元素。如果所有元素都满足条件,将返回一个空的list.
- 函数参数类型predicate: (T) -> Boolean
条件函数只是用来哪一个元素为第一个不满足条件,并以此为起点添加原List中后面所有的元素
- 返回值:List
- dropLastWhile
- 返回集合中的所有元素,除去第一个不满足条件的元素,而判断顺序为从最后一个元素一次到第一个元素,如果所有元素都满足条件,返回一个空List
- filter
- 返回所有与指定条件相符的元素列表,如果所有元素都不满足指定条件,返回一个空List
- filterNot
- 返回与指定条件不符的所有元素列表,如果所有元素都满足指定条件,返回一个空List
val listDropWhile = listBook.dropWhile {it.page < 23}
listDropWhile.forEach(::println)
// Log
//Book(name=’D’, page=23, author=’Node’)
//Book(name=’E’, page=24, author=’Blue’)
val listDropLastWhile = listBook.dropLastWhile { it.page > 23 }
listDropLastWhile.forEach(::println)
// Log
//Book(name=’A’, page=20, author=’Jone’)
//Book(name=’B’, page=21, author=’Green’)
//Book(name=’C’, page=22, author=’Mark’)
//Book(name=’D’, page=23, author=’Node’)
val listFilter = listBook.filter { it.page > 22 }
listFilter.forEach(::println)
// Log
//Book(name=’D’, page=23, author=’Node’)
//Book(name=’E’, page=24, author=’Blue’)
- filterNotNull
- 返回所有元素列表,但不包括null元素
- 返回值:List
val listFilterNotNull = listBook.filterNotNull()
listFilterNotNull.forEach(::println)
- slice
- 返回指定索引的元素列表。
- 函数参数类型Iterable
- 返回值:List
val listSlice = listBook.slice(listOf(2,4))
listSlice.forEach(::println)
// Log
Book(name=’C’, page=22, author=’Mark’)
Book(name=’E’, page=24, author=’Blue’)
- take
- 返回前N个元素列表。
- 函数参数类型Int
- 返回值:List
- takeLast
- 返回最后N个元素列表
- takeWhile
- 返回满足指定条件第一个元素列表
- 函数参数类型predicate: (T) -> Boolean
- 返回值:List
- takeLastWhile
- 返回一个元素列表,返回的元素的列表是第一个满足条件的元素的后边的所有元素。也就意味着,如果最后一个元素不满足条件,将返回一个空List
val listTake = listBook.take(2)
listTake.forEach(::println)
// Log
//Book(name=’A’, page=20, author=’Jone’)
//Book(name=’B’, page=21, author=’Green’)
val listTakeWhile = listBook.takeWhile { it.page < 22 }
listTakeWhile.forEach(::println)
// Log
//Book(name=’A’, page=20, author=’Jone’)
//Book(name=’B’, page=21, author=’Green’)
val listTakeLastWhile = listBook.takeLastWhile { it.page > 22 }
listTakeLastWhile.forEach(::println)
// Log
//Book(name=’D’, page=23, author=’Node’)
//Book(name=’E’, page=24, author=’Blue’)
### 3.3.8 映射类操作
- flatMap
- 通过遍历,将每一个元素转换为一个新的对象,并创建一个新集合,把新的的对象添加至新的集合并返回新的集合列表。
- 函数参数类型transform: (T) -> Iterable): List
- 返回值:List
- map
- 返回一个列表,该列表包含对原始集合中每个元素进行转换后结果。
val listFlatMap = listOf(20, 30, 40).flatMap { it -> mutableListOf(Book(“A-” + it, it, “B- ” + it))}
listFlatMap.forEach (::println)
// Log
Book(name=’A-20’, page=20, author=’B- 20’)
Book(name=’A-30’, page=30, author=’B- 30’)
Book(name=’A-40’, page=40, author=’B- 40’)
- mapIndexed
- 返回一个列表,该列表包含对原始集合中每个元素进行转换后结果和它们的索引。
- 函数参数类型transform: (Int, T) -> R
- 返回值:List
val listMapIndexed = listOf(20, 30, 40).mapIndexed { index, it -> Book(“A-” + index, it, “B- ” + index) }
listMapIndexed.forEach(::println)
// Log
Book(name=’A-0’, page=20, author=’B- 0’)
Book(name=’A-1’, page=30, author=’B- 1’)
Book(name=’A-2’, page=40, author=’B- 2’)
- mapNotNull
- 返回一个列表,该列表包含对原始集合中非null元素转换后的结果。
- 函数参数类型transform: (T) -> R?
- 返回值:List
val listMapNotNull = listOf(20, 30, 40)
.mapNotNull (::mapNoNull)
listMapNotNull.forEach(::println)
fun mapNoNull(page: Int) : Book? {
if (page == 20) {
return null
} else {
return Book(“A-” + page, page, “B- ” + page)
}
}
//Log
Book(name=’A-30’, page=30, author=’B- 30’)
Book(name=’A-40’, page=40, author=’B- 40’)
- groupBy
- 返回一个映射表,该表包括经指定函数对原始集合中元素进行分组后的元素。
- 函数参数类型keySelector: (T) -> K
- 返回值:Map
### 3.3.9 元素类操作
- contains
- 在集合中如果找到指定元素,则返回true。
- 参数类型:T实例
- 返回值:Boolean
val contains = listBook.contains(bookA)
val contains_ = listBook.contains(bookF)
println(“contains: contains,contains: contains_”)
// Log
contains: true, contains_: false
- elementAt
- 返回指定索引位置的元素。如果索引超出这个集合的范围,则抛出IndexOutOfBoundsException。
- 参数类型: Int,索引位置
- 返回值:T
- elementAtOrNull
- 返回索引位置的元素。如果索引超出这个集合的范围,则返回null
- elementAtOrElse
- 返回指定索引位置的元素。如果索引超出这个集合的范围,则返回调用默认函数的结果
- 数类型:Index:Int 函数参数类型: defaultValue: (Int) -> T
- 返回值:T
val out = listBook.elementAtOrElse(7){index -> Book(“A-” + index, index, “B- ” + index)}
println(out)
// Log
Book(name=’A-7’, page=7, author=’B- 7’)
- first
- 返回与指定条件相符的第一个元素。如果没有元素满足条件,则抛出异常NoSuchElementException
- 函数参数类型:predicate: (T) -> Boolean
- 返回值:T
- firstOrNull
- 返回与指定条件相符的第一个元素。如果没有找到相符的元素,则返回null
- last
- 返回与指定条件相符的最后一个元素
- lastOrNull
- 返回与指定条件相符的最后一个元素。如果没有找到这样的元素,则返回null
val first = listBook.first{ it.page > 23}
println(first)
// Log
Book(name=’D’, page=23, author=’Node’)
- indexOf
- 返回第一个元素的索引。如何集合没有包含元素,则返回-1。
- 参数类型:T:元素的数据类型
- 返回值:Int
val index = listBook.indexOf(bookA)
val index_ = listBook.indexOf(bookF)
println(“index: index,index: index_”)
// Log
index: 0, index_: -1
- indexOfFirst
- 返回第一个与指定条件相符的元素索引。如果集合没有包含这样的元素,则返回 -1
- 函数参数类型:predicate: (T) -> Boolean
- 返回值:Int
- lastIndexOf
- 返回最后一个满足条件的元素索引。如果集合没有包含满足条件的元素,则返回 -1
- single
- 返回与指定条件相符的单一元素。如果没有或有多个相符的元素,则抛出异常IllegalArgumentException
- 函数参数类型:predicate: (T) -> Boolean
- 返回值:Int
- singleOrNull
- 返回与指定条件相符的单一元素。如果没有找到这样元素或有找到多个这样元素,则返回null
### 3.3.10 生成类操作
- partition
- 将原始集合拆分一对集合,一个集合包含判断条件为true的元素,另一个集合包含判断条件为false的元素。
- 函数参数类型:predicate: (T) -> Boolean
- 返回值:Pair
- plus
- 返回一个列表,该列表包含原始集合的所有元素和指定集合的所有元素。由于函数名称原因,我们可以使用“+”操作符。执行操作以后,原集合中元素列表并未改变。
- 函数参数类型:变种1:T:元素的数据类型实例 变种2:Array:元素的数据类型的集合
- 返回值类型:List<T>
- zip
- 返回一个列表,该列表由两个集合中相同索引元素建立的元素对。这个列表长度为最短集合的长度为准。
- 在新的Kotlin版本中,去掉了merge操作符,其功能被保存下来了,作为了zip的一个变种。该变种将接受两个参数,其中一个参数为预建立配对的集合,另一个参数为函数参数,这个函数参数完成了,两个两个集合的配对元素结合,并生成新的元素。然后在将其加入新建的一个集合,最后将新建的集合返回。
- 返回值类型: List
### 3.3.11 排序类操作
- reverse
- 返回逆序元素列表
- sortBy
- 返回所有元素列表,其元素通过特定的比较器分类排序。
- 参数类型:crossinline selector: (T) -> R?
- 返回值类型:List
- sortDescendingBy
- 返回所有元素的分类排序列表,其顺序为通过特定排序函数结果的降序
val sort = listBook.sortedBy { - it.page }
sort.forEach(::println)
//Log
Book(name=’E’, page=24, author=’Blue’)
Book(name=’D’, page=23, author=’Node’)
Book(name=’C’, page=22, author=’Mark’)
Book(name=’B’, page=21, author=’Green’)
Book(name=’A’, page=20, author=’Jone’)
# 四、类的定义与组成
- 类的定义
- Kotlin 中使用关键字 class 声明类
- 类声明由类名、类头(指定其类型参数、主构造函数等)和由大括号包围的类体构成
- 类头和类体都是可选的
- 如果一个类没有类体,可以省略花括号
class Test{
// 属性…
…
// 构造函数
…
// 函数
…
// 内部类
…
…
}
class Test
类的构成由五部分组成:
- 构造函数和初始化代码块
- 属性(字段)
- 函数(方法)
- 内部类(嵌套类)
- 对象声明
## 4.1 构造函数
- 在Kotlin中,允许有一个主构造函数和多个二级构造函数(辅助构造函数)。其中主构造函数是类头的一部分。
- 关键字或者构造函数名:constructor(参数)
### 4.1.1 主构造函数
- 主构造函数是类头的一部分,类名的后面跟上构造函数的关键字以及类型参数
class Person constructor(firstName: String) {
}
//如果主构造函数没有任何注解或者可见性修饰符,可以省略这个 constructor 关键字
class Person(firstName: String) {
}
//如果构造函数有注解或可见性修饰符,这个 constructor 关键字是必需的,并且这些修饰符在它前面
class Customer public @Inject constructor(name: String) { …… }
- 声明属性以及从 主构造函数 初始化属性,Kotlin 有简洁的语法
- 与普通属性一样,主构造函数中声明的属性可以是可变的(var)或只读的(val)
- 相当于声明了3个属性
class Person(val firstName: String, val lastName: String, var age: Int) {
// ……
}
- 主构造函数不能包含任何的代码。初始化的代码可以放到以 init 关键字作为前缀的初始化块(initializer blocks)中init{...}
- 在实例初始化期间,初始化块按照它们出现在类体中的顺序执行,与属性初始化器交织在一起
- 请注意,主构造的参数可以在初始化块中使用。它们也可以在类体内声明的属性初始化器中使用
class InitOrderDemo(name: String) {
val firstProperty = “First property: $name”.also(::println)
init {
println("First initializer block that prints ${name}")
}
val secondProperty = "Second property: ${name.length}".also(::println)
init {
println("Second initializer block that prints ${name.length}")
}
}
### 4.1.2 辅助(二级)构造函数
- Kotlin中支持二级构造函数。它们以constructor关键字作为前缀。
class Test{
constructor(参数列表){
}
}
- 如果一个非抽象类没有声明任何(主或次)构造函数,它会有一个生成的不带参数的主构造函数。
- 构造函数的可见性是 public
- 如果你不希望你的类有一个公有构造函数,你需要声明一个带有非默认可见性的空的主构造函数
class DontCreateMe private constructor () {
}
- 同时存在主构造函数和二级构造函数时的情况
- 如果类具有主构造函数,则每个辅助构造函数需要通过另一个辅助构造函数直接或间接地委派给主构造函数。 使用this关键字对同一类的另一个构造函数进行委派
- 请注意,初始化块中的代码实际上会成为主构造函数的一部分。委托给主构造函数会作为次构造函数的第一条语句,因此所有初始化块中的代码都会在次构造函数体之前执行。即使该类没有主构造函数,这种委托仍会隐式发生,并且仍会执行初始化块
// 这里是为了代码清晰,故而没有隐藏constructor关键字
class Test constructor(num: Int){
init {
println("num = $num")
}
//二级构造函数中的第一个参数(num),是委托了主构造函数的参数num
constructor(num : Int, num2: Int) : this(num) {
println(num + num2)
}
}
fun main(args: Array) {
var test1 = Test(1)
var test2 = Test(1,2)
}
//输出结果为:
//num = 1
//num = 1
//3
可以看出,当实例化类的时候只传1个参数的时候,只会执行init代码块中的代码。当传2个参数的时候,除了执行了init代码块中代码外,还执行了二级构造函数中的代码
- 当类的主构造函数都存在默认值时的情况
- 在 JVM 上,如果主构造函数的所有的参数都有默认值,编译器会生成 一个额外的无参构造函数,它将使用默认值。这使得 Kotlin 更易于使用像 Jackson 或者 JPA 这样的通过无参构造函数创建类的实例的库
- 同理可看出,当类存在主构造函数并且有默认值时,二级构造函数也适用
class Test constructor(num1: Int = 10 , num2: Int = 20){
init {
println("num1 = $num1\t num2 = $num2")
}
constructor(num1 : Int = 1, num2 : Int = 2, num3 : Int = 3) : this(num1 , num2){
println("num1 = $num1\t num2 = $num2 \t num3 = $num3")
}
}
fun main(args: Array) {
var test = Test()
var test1 = Test(1,2)
var test2 = Test(4,5,6)
}
输出结果为:
num1 = 10 num2 = 20 //当实例化无参的构造函数时。使用了参数的默认值
num1 = 1 num2 = 2
num1 = 4 num2 = 5
num1 = 4 num2 = 5 num3 = 6
## 4.2 属性和字段
为了更方便的理解属性和字段差异,我们做一下简化理解:
- 字段,通常叫做类成员或者类成员变量,理解为”数据成员”,用来承载数据。
- 在类中定义的成员变量
public class A{
private String s = “123”;
}
- 属性,通常可以理解为set和get方法。其属名性时根据get和set方法名得出的,规则是:去掉get或set后其剩余的字符串,属性大多是对字段的封装,限制其访问和写入
- 只局限于类中方法的声明,并不与类中其他成员相关
void setA(String s){}
String getA(){return s}
当一个类中拥有这样一对方法时,我们可以说,这个类中拥有一个可读写的a属性(注意是小写a)。如果去掉了set的方法,则是可读属性,反之亦然
### 4.2.1 属性的声明和使用
Kotlin的类可以有属性。 属性可以用关键字var 声明为可变的,否则使用只读关键字val
- 在类内声明的属性值必须得初始化,属性必须初始化,否则编译报错.或者将此属性用abstract修饰符修饰。
class Address {
var name: String = ……
var street: String = ……
var city: String = ……
var state: String? = ……
var zip: String = ……
}
> 或许你会问了,为什么不是叫字段而是叫属性呢?**因为Kotlin类中声明的变量,都会提供默认的get、set(val没有该方法)方法,所以声明的都是属性**
> 也就是说:Kotlin中没有字段,只有幕后字段(backing filed)
要使用一个属性,只要用名称引用它即可,就像 Java 中的字段:
fun copyAddress(address: Address): Address {
val result = Address() // Kotlin 中没有“new”关键字
result.name = address.name // 将调用访问器
result.street = address.street//直接使用,内部会调用访问方法
// ……
return result
}
> **在使用中,看似我们是使用了“类似字段访问”的方式,实际上是kotlin在“.”调用的过程中,会转化为对应的get|set方法**
> 当你对属性赋值的时候就会调用set方法,当你获取属性的值得时候就会使用get方法
### 4.2.2 Getters 与 Setters
完整的属性声明如下:
<可见性修饰符> var
- 【可见性修饰符】默认为被public修饰符修饰,也就意味着其他类可以通过其类任意调用。如果禁止其他类访问其,本身可以使用private修饰符修饰
- 【属性读写】只读属性和可变属性的区别
- 只读属性使用val声明,可变属性使用var声明
- 只读属性不允许setter
val simple: Int? // 类型 Int、默认 getter、必须在构造函数中初始化
val inferredType = 1 // 类型 Int 、默认 getter
- 【属性类型】属性类型如果可以从初始器 (或者从其 getter 返回值,如下文所示)中推断出来,也可以省略
var initialized = 1 // 类型 Int、默认 getter 和 setter
- 【初始化】在类内声明的属性值必须得初始化,属性必须初始化,否则编译报错.或者将此属性用abstract修饰符修饰。
- 在abstract修饰的属性值,即使不用初始化,必须声明其数据类型,并在其子类初始化
abstract class Person {
var allByDefault: Int? // 错误:需要初始化器,隐含默认 getter 和 setter
abstract var name: String
var age: Int = 10
val sex: String = “M”
private var mobile: String = “606066”
}
- 【getter|setter】编译器会自动生成getter和setter方法
- 访问其某个类的属性值,使用”.”操作符,会自动转换为调用相关get|set方法
- 编写Getters和Setters非常像一般方法,在属性声明内部:
- 按照惯例,setter 参数的名称是 value,但是如果你喜欢你可以选择一个不同的名称
- getter方法的可见性与属性的可见性一致。假如声明一个public变量,将其的getter方法用其他修饰符修饰,会报错
- setter方法可以自定义修饰符,在实例代码中就可以看到,此时setter的修饰符不一定与属性的修饰符一致,其使用范围由修饰符决定,并不一定与属性的适用范围一致。
例如,将属性no的set方法用private修饰,在此类外部并不能对此变量赋值
//示例1
val isEmpty: Boolean get() = this.size == 0
//自 Kotlin 1.1 起,如果可以从 getter 推断出属性类型,则可以省略它:
//val isEmpty get() = this.size == 0 // 具有类型 Boolean
//示例2
var stringRepresentation: String
get() = this.toString()
set(value) {
setDataFromString(value) // 解析字符串并赋值给其他属性
}
//示例3
class Person {
var name: String = “abc”
get() = field.toUpperCase()
set(value){
field = “Name: $value”
}
}
//示例4
class Person2 {
var lastName: String = "zhang"
get() = field.toUpperCase()
set
}
- 【getter|setter】对于属性,如果你想改变访问的可见性或者是对其进行注解,但是又不想改变它的默认实现,那么你就可以定义set和get但不进行实现
var setterVisibility: String = “abc” // Initializer required, not a nullable type
private set // 此 setter 是私有的并且有默认实现
var setterWithAnnotation: Any? = null
@Inject set // 用 Inject 注解此 setter
#### 4.2.2.1 幕后字段(Backing Fields)
当我们定义一个setter时:
set(value) {//错误写法
name=value //1)
}
上面 1) 处会导致循环调用,因为“对属性赋值的时候就会调用set方法,获取属性的值得时候就会使用get方法”,而name=value会再次调用set(value)
正确的写法应该是:
- value是setter的参数,其类型同于属性的类型,不爽你也可以换个其他名字
- field就是幕后字段,用于将真正的值赋值给属性,而不会导致循环调用。
- **它只能在属性的访问器中使用**,field只有在访问的时候才会产生,其他时候是不会产生的
- 如果属性至少一个访问器使用默认实现,或者自定义访问器通过 field 引用幕后字段,将会为该属性生成一个幕后字段
- 这个也是可选项,有的时候必须(比如上面的Setter),有的时候不是必须(比如上面的Getter)
- 它是一个自动的返回字段,代表的就是属性
var name: String = “abc”
set(value) {
field=value
}
var counter = 0 // the initializer value is written directly to the backing field
set(value) {
if (value >= 0)
field = value
}
#### 4.2.2.2 幕后属性(Backing Property)
如果Backing Fields不适用的话,其实可以直接使用返回属性就可以了
private var _table: Map
#### 4.2.2.3 编译期常量
已知值的属性可以使用 const 修饰符标记为 编译期常量。 这些属性需要满足以下要求:
- 位于顶层或者是 object 的一个成员
-用 String 或原生类型 值初始化
- 没有自定义 getter
这些属性可以用在注解中:
const val SUBSYSTEM_DEPRECATED: String = “This subsystem is deprecated”
@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { …… }
#### 4.2.2.4 非空属性的延迟初始化lateinit
**在类内声明的属性必须初始化,如果设置非NULL的属性,应该将此属性在构造器内进行初始化**
然而,这经常不方便。例如:属性可以通过依赖注入来初始化, 或者在单元测试的 setup 方法中初始化。 这种情况下,你不能在构造函数内提供一个非空初始器。 但你仍然想在类体中引用该属性时避免空检查。
为处理这种情况,你可以用 lateinit 修饰符标记该属性:在初始化前访问一个 lateinit 属性会抛出一个特定异常,该异常明确标识该属性被访问及它没有初始化的事实
- 该修饰符只能用于在类体中的属性(不是在主构造函数中声明的 var 属性)
- 该修饰符只能用于仅当该属性没有自定义 getter 或 setter 时
- 自 Kotlin 1.2 起,也用于顶层属性与局部变量
- 该属性或变量必须为非空类型,并且不能是原生类型(String可以,它不是原生类型)
class MyInfo {
lateinit var person: Person
fun initData() {
person = Person()
}
}
// Test
fun main(args: Array) {
val myInfo: MyInfo = MyInfo()
myInfo.initData()
myInfo.person.doSwim()
}
// Log
do Swim
在类MyInfo中,声明了以属性person,其被lateinit修饰符修饰,person并没有初始化,默认为NULL。在使用其之前,调用了initData()方法,对person进行了初始化
- 检测一个 lateinit var 是否已初始化(自 1.2 起)
- 要检测一个 lateinit var 是否已经初始化过,请在该属性的引用上使用 .isInitialized:
if (foo::bar.isInitialized) {
println(foo.bar)
}
### 4.2.3 可见性修饰符
类、对象、接口、构造函数、方法、属性和它们的 setter 都可以有 可见性修饰符。 **(getter 总是与属性有着相同的可见性。) **
在 Kotlin 中有这四个可见性修饰符:private、 protected、 internal 和 public。 如果没有显式指定修饰符的话,默认可见性是 public
- public修饰符表示 公有 。此修饰符的范围最大。当不声明任何修饰符时,系统会默认使用此修饰符。
- internal修饰符表示 模块 。对于模块的范围在下面会说明。
- protected修饰符表示 私有+子类。值得注意的是,此修饰符不能用于顶层声明,在下面可以看到。
- private修饰符表示 私有 。此修饰符的范围最小,即可见性范围最低
> Java中的可见性修饰符和Koltin中的可见性修饰符还是很有几点不同之处的:
> 1、 四种修饰符的不同。Koltin的四种修饰符(public、interna、protected、private),Java的的四种修饰符(public、protected、privavte、default(即不使用任何修饰符))。
> 2、默认修饰符的不同。Kotlin的默认修饰符为public,Java的默认修饰符为default
#### 4.2.3.1 包的可见度修饰
在Kotlin中,包级别的定义称为top-level,即直接定义在包内,可定义函数、属性、类、对象及接口。
1. public:默认修饰符,被其修饰的在任何位置都能访问
2. private:只能在当前源文件内使用
3. internal:在同一模块内使用
4. protected:无效修饰符,只用于类和接口内
// file name: example.kt
package foo
private fun foo() {} // 只在 example.kt 文件内可访问
public var bar: Int = 5 // 这个属性在任何地方都可以访问
private set // 但它的设值方法只在 example.kt 文件内可以访问
internal val baz = 6 // 在同一个模块(module)内可以访问
#### 4.2.3.2 类和接口的可见度修饰
1. public:默认修饰符,被其修饰的在任何位置都能访问
2. private:表示只在这个类(以及它的所有成员)之内可以访问
3. protected:在当前类及其子类内访问
4. internal:在同一模块内使用
> 如果你覆盖一个 protected 成员并且没有显式指定其可见性,该成员还会是 protected 可见性
open class Outer {
private val a = 1
protected open val b = 2
internal val c = 3
val d = 4 // 默认 public
protected class Nested {
public val e: Int = 5
}
}
class Subclass : Outer() {
// a 不可见
// b、c、d 可见
// Nested 和 e 可见
override val b = 5 // “b”为 protected
}
class Unrelated(o: Outer) {
// o.a、o.b 不可见
// o.c 和 o.d 可见(相同模块)
// Outer.Nested 不可见,Nested::e 也不可见
}
#### 4.2.3.3 构造器的可见度修饰
一般在类的主构造器都不使用修饰符修饰,即默认使用public修饰符。但在实际开发时,主构造器可能被其他修饰符修饰。
比如在创建单例模式时,为了防止在外部调用主构造器,主构造器就是使用private修饰符修饰。当使用其他修饰符修饰构造器时,需要明确添加一个constructor关键字
class C private constructor(a: Int) { … }
#### 4.2.3.4 局部声明的可见度修饰
局部变量, 局部函数, 以及局部类, 都不能指定可见度修饰符
#### 4.2.3.5 模块(Model)的可见度修饰
可见性修饰符 internal 意味着该成员只在相同模块内可见
模块(module)是指一起编译的一组 Kotlin 源代码文件:
- 一个 IntelliJ IDEA 模块
- 一个 Maven 工程, 或 Gradle 工程
- 通过 Ant 任务的一次调用编译的一组文件
## 4.3 对象声明
- 对象表达式是在使用他们的地方立即执行(及初始化)的;
- 对象声明是在第一次被访问到时延迟初始化的;
- 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配
### 4.3.1 对象表达式
> 在Java中,不管是为了实现接口,或者是抽象类,我们总是习惯使用匿名内部类。最熟悉的例子,莫过于对单击事件的监听,也就是这样写:
> ```
> btn.setOnClickListener(new OnClickListener{
> // 处理单击事件逻辑
> ***
> });
> ```
> 尽管该匿名类只能被使用一次,这样,我们没有必要再去实现OnClickListener接口并创建一个类,从而简化了代码
> 管Kotlin没有匿名内部类,恰巧其用object一个简单的关键字,解决了这一个问题
对象表达式是一个匿名类的实例,该匿名类可以继承自某个类或者多个接口。不过,我们需要通过object关键字将其声明,表示这是一个对象
btn.setOnClickListener(object : OnClickListener{
// 处理单击事件逻辑
*
});
- **如果被继承的基类中为有参构造器且没有无参构造器时,那么必须向构造器传递适当的参数,否则编译器会报错**
open class A(x: Int) {
public open val y: Int = x
}
interface B {……}
val ab: A = object : A(1), B {
override val y = 15
}
- **就像 Java 匿名内部类一样,对象表达式中的代码可以访问来自包含它的作用域的变量。 (与 Java 不同的是,这不仅限于 final 变量。)**
val a = 10
val listener = object : Info(“submit”),IClickListener {
override fun doClick() {
println(“a:$a”)
}
}
listener.doClick() // 打印 a:10
- **任何时候,如果我们只需要“一个对象而已”,并不需要特殊超类型,那么我们可以简单地写**
fun foo() {
val adHoc = object {
var x: Int = 0
var y: Int = 0
}
print(adHoc.x + adHoc.y)
}
- **匿名对象可以用作只在本地和私有作用域中声明的类型**
- 如果你使用匿名对象作为公有函数的返回类型或者用作公有属性的类型,那么该函数或属性的实际类型会是匿名对象声明的超类型
如果你没有声明任何超类型,就会是 Any,在匿名对象中添加的成员将无法访问
class C {
// 私有函数,所以其返回类型是匿名对象类型
private fun foo() = object {
val x: String = “x”
}
// 公有函数,所以其返回类型是 Any
fun publicFoo() = object {
val x: String = "x"
}
fun bar() {
val x1 = foo().x // 没问题
val x2 = publicFoo().x // 错误:未能解析的引用“x”
}
}
### 4.3.2 对象声明
> 注意:对象声明不能在局部作用域(即直接嵌套在函数内部),但是它们可以嵌套到其他对象声明或非内部类中
当关键字object之后指定了一个名称, 那么它就不再是“对象表达式”,而是一个“对象声明”。就像变量声明一样,对象声明不是一个表达式,不能用在赋值语句的右边。
要引用该对象,我们直接使用其名称即可
object MyInfo: Info(“submit”),IClickListener {
override fun doClick() {
println("MyInfo do click, $text") // Log: MyInfo do click, , submit
}
}
fun main(args: Array) {
MyInfo.doClick()
}
- 对象声明不能指定构造函数
- 与对象表达式相似,如果被继承的基类中为有参构造器且没有无参构造器时,那么必须向构造器传递适当的参数,否则编译器会报错
### 4.3.3 伴生对象
与 Java 或 C# 不同,在 Kotlin 中类没有静态方法。在大多数情况下,它建议简单地使用《官方推荐我们使用包级别的方法》
- 如果你需要写一个可以无需用一个类的实例来调用、但需要访问类内部的函数(例如,工厂方法),你可以把它写成该类内的一个对象声明
//这是一个对象声明
object StaticDemo{
fun test(){
print(“”)
}
}
//使用方法如下:
//StaticDemo.test()
- 如果在你的类内声明了一个伴生对象, 你就可以使用像在 Java/C# 中调用静态方法相同的语法来调用其成员,只使用类名作为限定符
- 类内部的对象声明可以用 companion 关键字标记
class Demo {
//这是一个对象,一个伴生对象
companion object {//声明在内部的,类似静态方法,可以使用 类名.方法名
fun test() {
print(“”)
}
}
fun test2() { //test2在外部,必须实例化后才可调用
}
}
//使用方法如下:
//Demo.test()
> **请注意,即使伴生对象的成员看起来像其他语言的静态成员,在运行时他们仍然是真实对象的实例成员**
- 伴生对象还可以实现接口:
interface Factory {
fun create(): T
}
class MyClass {
companion object : Factory {
override fun create(): MyClass = MyClass()
}
}
## 4.4 函数(方法)
在kotlin , 函数是一等公民,而不像在java中必须在class中包裹
### 4.4.1 函数声明
Kotlin 中的函数使用 fun 关键字声明。
Kotlin中的函数决定了一个对象能够接收到什么样的信息,函数的基本组成部分包括:名称、参数、返回值和函数体,其使用fun关键字定义函数,定义形式为:
fun methodName(param: paramType): ReturnType {
}
- 参数列表:给出了要传递给函数的信息的类型和名称
- 函数名和参数列表作为函数的唯一的标识符
- 返回类型:描述的是在调用函数之后从函数返回的值
#### 4.4.1.1 参数:默认参数、命名参数、不定数量参数
函数参数的定义使用Pascal标记法, 也就是, name: type 的格式,**多个参数之间使用逗号分隔,每个参数都必须明确指定类型**.
fun powerOf(number: Int, exponent: Int) {
…
}
- 【默认参数】
- 参数默认值的定义方法, 在参数类型之后, 添加 = 和默认值.
- 函数参数可以指定默认值, 当参数省略时, 就会使用默认值.
与其他语言相比, 这种功能使得我们可以减少大量的重载(overload)函数定义
fun doSwim(sports: String = “do swim”) {
println(sports)
}
- 【默认参数】覆盖方法总是使用与基类型方法相同的默认参数值。
- 当覆盖一个带有默认参数值的方法时,必须从签名中省略默认参数值
open class A {
open fun foo(i: Int = 10) { …… }
}
class B : A() {
override fun foo(i: Int) { …… } // 不能有默认值
}
- 【命名参数】
- 调用函数时, 可以通过参数名来指定参数. 当函数参数很多, 或者存在默认参数时, 指定参数名是一种非常便利的功能.
- 使用命名参数我们可以使代码更具有可读性
- **当一个函数调用混合使用 位置参数与命名参数时,所有位置参数都要放在第一个命名参数之前。**
例如,允许调用 f(1, y = 2) 但不允许 f(x = 1, 2)
fun register(name: String, no: Int = 1001, sex: Int = 0) {
println(“name: name,no: no, sex: $sex”)
}
//当我们采用默认方式调用时,如果未设定的参数,位于参数列表的第一个,我们可以这样调用
//相当于 register(“li”, 1001, 0)
register(“li”)
//如果我们不需要指定所有的参数, 只是修改部分默认的参数值,我们可以这样:
register(name = “wang”, no = 1003)
> 请注意,在调用 Java 函数时不能使用命名参数语法,因为 Java 字节码并不总是保留函数参数的名称。
- 【不定数量参数】
- 如果在函数被调用以前,函数的参数(通常是参数中的最后一个)个数不能够确定,可以采用不定量参数方式,定义函数参数列表
- 函数的参数(通常是最后一个)可以用 vararg 修饰符标记
- 只有一个参数可以标注为 vararg。
- 如果 vararg 参数不是列表中的最后一个参数, 可以使用命名参数语法传递其后的参数的值,或者,如果参数具有函数类型,则通过在括号外部传一个 lambda
fun specialty(vararg fruits: String, addr: String) {
for (fruit in fruits) {
println(“fruit: fruit,addr: addr”)
}
}
specialty(“苹果”, “橘子”, addr = “AAA”)
- 【不定数量参数】当我们调用 vararg-函数时,我们可以一个接一个地传参,例如 asList(1, 2, 3),或者,如果我们已经有一个数组并希望将其内容传给该函数,我们使用伸展(spread)操作符(在数组前面加 *)
fun asList(vararg ts: T): List {
val result = ArrayList()
for (t in ts) // ts is an Array
result.add(t)
return result
}
//允许将可变数量的参数传递给函数
val list = asList(1, 2, 3)
val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)
#### 4.4.1.2 返回值:明确指定、Unit、单表达式函数
- 明确指定返回值类型
- **如果一个函数体由多行语句组成的代码段,那么必须明确指定返回值类型,除非函数的的返回值为Unit**
- Kotlin 不推断具有块代码体的函数的返回类型,因为这样的函数在代码体中可能有复杂的控制流,并且返回类型对于读者(有时甚至对于编译器)是不明显的
- 如果一个函数不返回任何有意义的结果值,那么它的返回类型为Unit .
- Unit 类型只有唯一的一个值Unit,在函数中,不需要明确地返回这个值
- 对于返回值为Unit的函数,Unit可以省略。
fun register(name: String, no: Int = 1001, sex: Int = 0): Unit {
println(“name: name,no: no, sex: $sex”)
}
//上例中的代码等价于:
fun register(name: String, no: Int = 1001, sex: Int = 0) {
println(“name: name,no: no, sex: $sex”)
}
- 单表达式函数
- 如果一个函数的函数体只有一个表达式,函数体可以直接写在 “=”之后
- 如果编译器可以推断出函数的返回值类型, 那么返回值的类型定义是可省略
fun double(x: Int): Int = x * 2
fun double(x: Int) = x * 2
### 4.4.2 函数调用
#### 4.4.2.1 传统用法
- 函数的调用使用传统的方式:
val result = double(2)
- 调用类的成员函数时, 使用点号标记法(dot notation):
Sample().foo() // 创建一个 Sample 类的实例, 然后调用这个实例的 foo 函数
#### 4.4.2.2 中缀标记法(Infix notation)
中缀标记法指调用对象实例的成员函数时可以省略.符号
- 使用中缀标记法(infix notation)来调用函数, 但函数需要满足以下条件:
- 是成员函数, 或者是扩展函数
- 只有单个参数
- 使用 infix 关键字标记
class Person(var name: String, var age: Int) {
// 使用infix 关键字标记,该函数可被中缀标记法法调用
infix fun printName(addr: String) {
println("addr: $addr, name: $name")
}
}
fun main(args: Array) {
val person: Person = Person(“Jone”, 20)
// 使用中缀标记法调用扩展函数
person printName("AA-BB") // Log: addr: AA-BB, name: Jone
// 上面的语句等价于
person.printName("AA-BB")
}
### 4.4.3 函数作用域
在kotlin中函数是一等公民
- 顶层作用域,即包下的函数
- 在 Kotlin 中函数可以在文件顶层声明,这意味着你不需要像一些语言如 Java、C# 或 Scala 那样创建一个类来保存一个函数。
- 局部作用域
- 作为成员函数
- 扩展函数
#### 4.4.3.1 局部函数
- Kotlin 支持局部函数,即一个函数在另一个函数内部
- 局部函数可以访问外部函数(即闭包)的局部变量
fun dfs(graph: Graph) {
fun dfs(current: Vertex, visited: Set) {
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v, visited)
}
dfs(graph.vertices[0], HashSet())
}
//在上例中,visited 可以是局部变量
fun dfs(graph: Graph) {
val visited = HashSet()
fun dfs(current: Vertex) {
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v)
}
dfs(graph.vertices[0])
}
#### 4.4.3.2 成员函数
- 成员函数是指定义在类或对象之内的函数。
- 对成员函数的调用使用点号标记法。
class Sample() {
fun foo() { print(“Foo”) }
}
Sample().foo() // 创建 Sample 类的实例, 并调用 foo 函数
#### 4.4.3.3 扩展函数
可参看 <6.3.1 函数扩展>
### 4.4.4 泛型函数
函数可以有泛型参数,通过在函数名前使用尖括号指定:
fun singletonList(item: T): List {
// ……
}
可参看 <6.4.2 泛型函数>
### 4.4.5 尾递归函数
> 相当于注解方式的语法糖,编译器会自动优化代码
Kotlin 支持一种称为尾递归的函数式编程风格。 这允许一些通常用循环写的算法改用递归函数来写,而无堆栈溢出的风险
- 当一个函数用 tailrec 修饰符标记并满足所需的形式时,编译器会优化该递归,留下一个快速而高效的基于循环的版本
1. 要符合 tailrec 修饰符的要求, 函数必须在它执行的所有操作的最后一步, 递归调用它自身
2. 如果递归调用后还有其他逻辑代码,不能使用尾递归
3. 尾递归不能用在try/catch/finally 结构内
4. 尾递归目前只能用在JVM环境内
这段代码计算余弦的不动点(fixpoint of cosine),这是一个数学常数。 它只是重复地从 1.0 开始调用 Math.cos,直到结果不再改变,产生0.7390851332151607的结果
tailrec fun findFixPoint(x: Double = 1.0): Double
= if (x == Math.cos(x)) x else findFixPoint(Math.cos(x))
//最终代码相当于这种更传统风格的代码:
private fun findFixPoint(): Double {
var x = 1.0
while (true) {
val y = Math.cos(x)
if (x == y) return x
x = y
}
}
### 4.4.6 高阶函数与函数类型
- 所谓的高阶函数,是一种特殊的函数, 它接受函数作为参数, 或者返回一个函数
- 对于接受另一个函数作为自己参数的函数, 我们必须针对这个参数指定一个函数类型:
- 作为参数声明的“函数类型”变量声明,往往采用lamda表达式的格式(不是一个lamda表达式,表达式是有返回值的):譬如body: () -> T
- “函数类型”的格式: ->前为参数,后为返回值
- 被传入参数,使用::标示,引用一个函数 (函数引用支持)
- 被传入的参数,往往使用lamda表达式(表达式返回一个函数类型的实例)
//参数sumSom的类型是函数类型 (Int, Int, Int) -> Int,也就是说,它是一个函数,接受三个Int类型参数,并且返回一个Int
fun test(a: Int, b: Int, sumSom: (Int, Int, Int) -> Int): Int {
if (a > b) {
return sumSom(0, a, 0)
} else {
return sumSom(0, b, 0)
}
}
tailrec fun sumSom(start: Int, end: Int, result: Int): Int {
var res = result
var sta = start
while (sta <= end) {
res += sta
sta++
}
return res
}
// 测试类
fun main(args: Array) {
println(test(10, 9, ::sumSom)) // Log:55
}
从上述代码,在函数test中,sumSom参数是一个函数类型:(Int, Int, Int) -> Int,其是一个函数,接受3个Int参数,返回值是一个Int类型的值。
在test中,对传入的参数a,b进行判断,然后执行sumSom()函数并将执行结果返回。
- **在 Kotlin 中有一个约定,如果函数的最后一个参数是一个函数,并且你传递一个 lambda 表达式作为相应的参数,你可以在圆括号之外指定它**
fun lock(lock: Lock, body: () -> T): T {
lock.lock()
try {
return body()
}
finally {
lock.unlock()
}
}
//示例1:传统方式传入函数参数
fun toBeSynchronized() = sharedResource.operation()
val result = lock(lock, ::toBeSynchronized)
//示例2:lambda 表达式传入函数参数
val result = lock(lock, { sharedResource.operation() })
//示例3:lambda 表达式传入函数参数,在圆括号之外指定
lock (lock) {
sharedResource.operation()
}
- **it:单个参数的隐式名称**
- 如果函数字面值只有一个参数, 那么它的声明可以省略(连同 ->),其名称是 it
fun
- 下划线用于未使用的变量(自 1.1 起)
- 如果 lambda 表达式的参数未使用,那么可以用下划线取代其名称
map.forEach { _, value -> println(“$value!”) }
- **如要声明一个函数类型的可空变量,请将整个函数类型括在括号中并在其后加上问号**
var sum: ((Int, Int) -> Int)? = null
### 4.4.7 Lambda表达式
- [(4.6.24)Lambda表达式 in Android](http://blog.csdn.net/fei20121106/article/details/77988905)
#### 4.4.7.1 Lambda表达式的概念
Lambda 表达式, 或者匿名函数, 是一种”函数字面值(function literal)”, 也就是, 一个没有声明的函数, 但是立即作为表达式传递出去.
有点类似于java中的单函数接口的匿名内部类的使用方式,譬如onClickListener
//函数 max 是一个高阶函数类型, 也就是说, 它接受一个函数值作为第二个参数. 第二个参数是一个表达式, 本身又是另一个函数
fun max(collection: Collection, less: (T, T) -> Boolean): T? {
var max: T? = null
for (it in collection)
if (max == null || less(max, it))
max = it
return max
}
max(strings, { a, b -> a.length() < b.length() })
//后半截等价于,定义了一个函数,然后把该函数传递过去:
fun compare(a: String, b: String): Boolean = a.length() < b.length()
#### 4.4.7.2 Lambda表达式的定义
Lambda 表达式是对传统函数写法的一种简化,省略了对形参的声明等。它的完整语法形式,即函数类型的字面值如下:
- lambda 表达式总是被大括号括着
- 完整语法形式的参数声明放在大括号内,并有可选的类型标注
- 其参数(如果有的话)在 -> 之前声明(参数类型可以省略);
- 函数体(如果存在的话)在 -> 后面。
- 如果推断出的该 lambda 的返回类型不是 Unit,那么该 lambda 主体中的最后一个(或可能是单个)表达式会视为返回值
- 无参数时()不能省略
val sum = { x: Int, y: Int -> x + y }
- 一个 lambda 表达式只有一个参数是很常见的
- 如果 Kotlin 可以自己计算出签名,它允许我们不声明唯一的参数,并且将隐含地为我们声明其名称为 it
ints.filter { it > 0 } // 这个字面值是“(it: Int) -> Boolean”类型的
- 我们可以使用《限定的返回语法》从 lambda 显式返回一个值。否则,将隐式返回最后一个表达式的值
//以下两个片段是等价的:
ints.filter {
val shouldFilter = it > 0
shouldFilter
}
ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter
}
> 请注意,如果一个函数接受另一个函数作为最后一个参数,lambda 表达式参数可以在圆括号参数列表之外传递。 参见 callSuffix 的语法
### 4.4.8 匿名函数
上面提供的 lambda 表达式语法缺少的一个东西是指定函数的返回类型的能力。在大多数情况下,这是不必要的。因为返回类型可以自动推断出来。然而,如果确实需要显式指定,可以使用另一种语法: 匿名函数
- 匿名函数看起来非常像一个常规函数声明,除了其名称省略了。其函数体可以是表达式(如上所示)或代码块:
- 匿名函数的返回类型推断机制与正常函数一样:对于具有表达式函数体的匿名函数将自动推断返回类型,而具有代码块函数体的返回类型必须显式指定(或者已假定为 Unit)
fun(x: Int, y: Int): Int = x + y
fun(x: Int, y: Int): Int {
return x + y
}
ints.filter(fun(item) = item > 0)
> Lambda表达式与匿名函数之间的区别
> - 是否指定返回值的类型
> - Lambda表达式返回值的类型通过以自动推断得到
> - 匿名函数的返回值类型必须手动指定,如果未指定返回值类型,默认返回值类型为Unit
> - return的行为
> - Lambda 表达式内的 return 将会从包含这个Lambda表达式的函数中返回
> - 匿名函数内的 return 只会从匿名函数本身返
### 4.4.9 闭包
> 闭包是针对变量作用域的理解,是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域
Lambda 表达式或者匿名函数(以及局部函数和对象表达式) 可以访问其 闭包 ,即在外部作用域中声明的变量。 与 Java 不同的是可以修改闭包中捕获的变量:
var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)
### 4.4.10 内联函数
#### 4.4.10.1 概述
在说内联函数之前,先说说函数的调用过程。
调用某个函数实际上将程序执行顺序转移到该函数所存放在内存中某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方。这种转移操作要求在转去前要保护现场并记忆执行的地址,转回后先要恢复现场,并按原来保存地址继续执行。也就是通常说的压栈和出栈。因此,函数调用要有一定的时间和空间方面的开销。那么对于那些函数体代码不是很大,又频繁调用的函数来说,这个时间和空间的消耗会很大。
那怎么解决这个性能消耗问题呢,这个时候需要引入内联函数了。
内联函数就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来直接进行替换。显然,这样就不会产生转去转回的问题,但是由于在编译时将函数体中的代码被替代到程序中,因此会增加目标程序代码量,进而增加空间开销,而在时间代销上不象函数调用时那么大,可见它是以目标代码的增加为代价来换取时间的节省。
#### 4.4.10.2 inline内联
在Kotlin中,使用inline修饰符标记内联函数,既会影响到函数本身, 也影响到传递给它的Lambda表达式,这两者都会被内联到调用处
inline fun lock(lock: Lock, body: () -> T): T {
// …
}
//编译器可以直接产生下面的代码, 而不必为参数创建函数对象, 然后再调用这个参数指向的函数:
l.lock()
try {
foo()
}
finally {
l.unlock()
}
#### 4.4.10.3 noinline禁用内联
如果一个内联函数的参数中有多个 Lambda 表达式, 而你只希望内联其中的一部分, 你可以对函数的一部分参数添加 noinline 标记:
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
// …
}
可内联的 Lambda 表达式只能在内联函数内部调用, 或者再作为可内联的参数传递给其他函数, 但noinline 的 Lambda 表达式可以按照我们喜欢的方式任意使用: 可以保存在域内, 也可以当作参数传递, 等等
> 需要注意的是,如果一个内联函数没有可内联的函数参数并且没有具体化的类型参数,编译器会产生一个警告,因为内联这样的函数很可能并无益处(如果你确认需要内联,则可以用 @Suppress("NOTHING_TO_INLINE") 注解关掉该警告
#### 4.4.10.4 非局部返回
在 Kotlin 中,我们可以只使用一个正常的、非限定的 return 来退出一个命名或匿名函数。
这意味着要退出一个 lambda 表达式,我们必须使用一个标签,并且在 lambda 表达式内部禁止使用裸 return,因为 lambda 表达式不能使包含它的函数返回:
fun foo() {
ordinaryFunction {
return // 错误:不能使 foo
在此处返回
}
}
但是如果 lambda 表达式传给的函数是内联的,该 return 也可以内联,所以它是允许的:
fun foo() {
inlineFunction {
return // OK:该 lambda 表达式是内联的
}
}
这种返回(位于 lambda 表达式中,但退出包含它的函数)称为非局部返回
- 请注意,一些内联函数可能调用传给它们的不是直接来自函数体、而是来自另一个执行上下文的 lambda 表达式参数,例如来自局部对象或嵌套函数。在这种情况下,该 lambda 表达式中也不允许非局部控制流。为了标识这种情况,该 lambda 表达式参数需要用 crossinline 修饰符标记
inline fun f(crossinline body: () -> Unit) {
val f = object: Runnable {
override fun run() = body()
}
// ……
}
> break 和 continue 在内联的 lambda 表达式中还不可用,但计划支持它们。
#### 4.4.10.5 具体化的类型参数
- [具体化的类型参数](https://www.kotlincn.net/docs/reference/inline-functions.html)
有时候我们需要访问一个作为参数传给我们的一个类型:
fun TreeNode.findParentOfType(clazz: Class): T? {
var p = parent
while (p != null && !clazz.isInstance(p)) {
p = p.parent
}
@Suppress(“UNCHECKED_CAST”)
return p as T?
}
在这里我们向上遍历一棵树并且检查每个节点是不是特定的类型。 这都没有问题,但是调用处不是很优雅:
treeNode.findParentOfType(MyTreeNode::class.java)
我们真正想要的只是传一个类型给该函数,即像这样调用它:
treeNode.findParentOfType()
为能够这么做,内联函数支持具体化的类型参数,于是我们可以这样写:
inline fun TreeNode.findParentOfType(): T? {
var p = parent
while (p != null && p !is T) {
p = p.parent
}
return p as T?
}
- 我们使用 reified 修饰符来限定类型参数,现在可以在函数内部访问它了, 几乎就像是一个普通的类一样。由于函数是内联的,不需要反射,正常的操作符如 !is 和 as 现在都能用了。
- 此外,我们还可以按照上面提到的方式调用它:myTree.findParentOfType<MyTreeNodeType>()
### 4.4.11 带接收者的函数字面值
> 类似于扩展函数,它允许你在函数体内访问接收者对象的成员
Kotlin 提供了使用指定的 接收者对象 调用函数字面值的功能。 在函数字面值的函数体中,可以**调用该接收者对象上的方法而无需任何额外的限定符**
//一个函数定义 类似于定义 val i : Int
//这样的函数字面值的类型是一个带有接收者的函数类型:Int.(other: Int) -> Int
//这个函数类型和之前我们看到的函数类型(other: Int) -> Int有所不同,因为圆括号前有一个“Int.”前缀,这表示这个函数必须通过一个Int类型的接收者对象来调用
//圆括号前有Int前缀,也就是说此函数必须通过一个Int类型的接收者对象来调用
//this代表调用此函数时所使用的接收者对象,即1.sum(2)中的1
val sum : Int.(other: Int) -> Int
val i : Int
- 匿名函数语法允许你直接指定函数字面值的接收者类型。 如果你需要使用带接收者的函数类型声明一个变量,并在之后使用它,这将非常有用
val sum : Int.(other: Int) -> Int
//圆括号前有Int前缀,也就是说此函数必须通过一个Int类型的接收者对象来调用
//this代表调用此函数时所使用的接收者对象,即1.sum(2)中的1
sum = fun Int.(other: Int): Int = this + other
sum1 = fun Int.(other: Int): Int {
return (this + other)
}
//调用
//该函数字面值可以这样调用,就像它是接收者对象上的一个方法一样:
1.sum(2)
- 当接收者类型可以从上下文推断时,lambda 表达式可以用作带接收者的函数字面值
class HTML {
fun body() { …… }
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML() // 创建接收者对象
html.init() // 将该接收者对象传给该 lambda
return html
}
html { // 带接收者的 lambda 由此开始
body() // 调用该接收者对象的一个方法
}
## 4.5 内部类(嵌套类)
见<5.5 内部类(嵌套类)>
# 五、类的类别
Kotlin中的类可详细的分为:
- 抽象类
- 接口类
- 数据类
- 密封类
- 内部类(嵌套类)
- 枚举类
## 5.1 抽象类
> 对于面向对象编程来说,抽象是它的一大特征之一。在Kotlin中可以通过抽象类和接口来完成抽象。抽象类和接口有很多相似之处,又有不同之处
> kotlin的抽象 abstract比java中的能力更大
含有抽象方法的类,称为抽象类。在抽象类中,不仅可以有抽象方法,同时可以有具体实现的方法
- 类和其中的某些成员可以声明为 abstract
- 类体中的成员变量也可以声明为abstract,这意味着不能初始化
- 抽象成员在本类中可以不用实现
- 需要注意的是,我们并不需要用 open 标注一个抽象类或者函数——因为这不言而喻
- 甚至可以用一个抽象成员覆盖一个非抽象的开放成员
- 子类通过:继承父类
open class Base {
open fun f() {}
}
abstract class Derived : Base() {
override abstract fun f()
}
> 在Kotlin中冒号(:)使用的地方很多:
>
> 1. 用于变量的定义
> 2. 用于继承
> 3. 用于接口
> 4. 方法的返回类型声明
抽象类和普通类的主要有三点区别,这和java是一样的:
1. 抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public。
2. 抽象类不能用来创建对象;
3. 如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为为abstract类。如果抽象类中含有抽象属性,再实现子类中必须将抽象属性初始化,除非子类也为抽象类
### 5.1.1 抽象方法
抽象方法是一种特殊的方法:它只有声明,而没有具体的实现。抽象方法的声明格式为:
abstract fun doSwim()
1. 抽象方法必须用abstract关键字进行修饰
2. 抽象方法不用手动添加open,默认被open修饰
3. 抽象方法没有具体的实现
4. 含有抽象方法的类成为抽象类,必须由abtract关键字修饰
### 5.1.2 抽象属性
抽象属性就是在var或val前被abstract修饰,抽象属性的声明格式为:
abstract var addr : String
abstract val weight : Float
1. 抽象属性在抽象类中不能被初始化
2. 在子类没有主构造函数,要对抽象属性,手动初始化。如果子类中有主构造函数,抽象属性可以在主构造函数中声明
3. 抽象属性只能在抽象类中声明
### 5.1.3 抽象类和接口的差异
- 语法层面上的区别
1. 接口不能保存状态,可以有属性但必须是抽象的,而抽类型可以有属性。
2. 一个类只能继承一个抽象类,而一个类却可以实现多个接口
- 设计层面上的区别
1. 抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象
举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类Airplane,将鸟设计为一个类Bird,但是不能将 飞行 这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。此时可以将 飞行 设计为一个接口Fly,包含方法fly( ),然后Airplane和Bird分别根据自己的需要实现Fly这个接口
**继承是一个 “是不是”的关系,而 接口 实现则是 “有没有”的关系**
2. 抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计
对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动
## 5.2 接口类
Kotlin 的接口与 Java 8 类似,更像抽象类了:
- 使用关键字 interface 来定义接口
- **既包含抽象方法的声明,也包含实现**
- 与抽象类不同的是,接口无法保存状态
- 可以有属性但必须声明为抽象或提供访问器实现
interface MyInterface {
fun bar()
fun foo() {
// 可选的方法体
}
}
- 一个类或者对象可以实现一个或多个接口
- 关键字:冒号(:),这一点是和Java不同的。Java中使用接口使用的是implements关键字
class Child : MyInterface {
override fun bar() {
// 方法体
}
}
- 可以在接口中定义属性。在接口中声明的属性要么是抽象的,要么提供访问器的实现。在接口中声明的属性不能有幕后字段(backing field),因此接口中声明的访问器不能引用它们
interface MyInterface {
val prop: Int // 抽象的
val propertyWithImplementation: String
get() = "foo"
fun foo() {
print(prop)
}
}
class Child : MyInterface {
override val prop: Int = 29
}
### 5.2.1 多重实现中解决覆盖冲突
实现多个接口时,可能会遇到同一方法继承多个实现的问题。例如
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.foo()
super.foo()
}
override fun bar() {
super.bar()
}
}
接口 A 和 B 都定义了方法 foo() 和 bar()。 两者都实现了 foo(), 但是只有 B 实现了 bar() (bar() 在 A 中没有标记为抽象, 因为没有方法体时默认为抽象)。
- 因为 C 是一个实现了 A 的具体类,所以必须要重写 bar() 并实现这个抽象方法
- 如果我们从 A 和 B 派生 D,我们需要实现我们从多个接口继承的所有方法,并指明 D 应该如何实现它们。这一规则既适用于继承单个实现(bar())的方法也适用于继承多个实现(foo())的方法
## 5.3 数据类
我们经常创建一些只保存数据的类。 在这些类中,一些标准函数往往是从数据机械推导而来的。在 Kotlin 中,这叫做 数据类 并标记为 data:
data class User(var name: String, var age: Int)
> var 有set和get; var只有get,只能由构造函数传入
- 对于使用 data 标注的类,编译器自动从主构造函数中声明的所有属性导出以下成员:
- equals()/hashCode() 函数对;
- toString() 格式是 "User(name=John, age=42)";
- componentN() 函数 按声明顺序对应于所有属性;
- copy() 函数(见下文)
- 为了确保生成的代码的一致性和有意义的行为,数据类必须满足以下要求:
- 主构造函数需要至少有一个参数;
- 主构造函数的所有参数需要标记为 val 或 var;
- 数据类不能是抽象、开放、密封或者内部的;
- (在1.1之前)数据类只能实现接口。自 1.1 起,数据类可以扩展其他类(示例请参见密封类)
- 在 JVM 中,如果生成的类需要含有一个无参的构造函数,则所有的属性必须指定默认值
data class User(val name: String = “”, val age: Int = 0)
- 成员生成遵循关于成员继承的这些规则:
- 如果在数据类体中有显式实现 equals()、 hashCode() 或者 toString(),或者这些函数在父类中有 final 实现,那么不会生成这些函数,而会使用现有函数;
- 如果超类型具有 open 的 componentN() 函数并且返回兼容的类型, 那么会为数据类生成相应的函数,并覆盖超类的实现。如果超类型的这些函数由于签名不兼容或者是 final 而导致无法覆盖,那么会报错;
- 从一个已具 copy(……) 函数且签名匹配的类型派生一个数据类在 Kotlin 1.2 中已弃用,并且会在 Kotlin 1.3 中禁用。
- 不允许为 componentN() 以及 copy() 函数提供显式实现
### 5.3.1 copy
> copy并不是简单的复制,它会创建新的对象,对于基本类型的属性,其值会被拷贝并赋值,但是对于引用类型,其只是将其地址传递过去,还是进行浅拷贝,并没有创建新的对象
在很多情况下,我们需要复制一个对象改变它的一些属性,但其余部分保持不变。 copy() 函数就是为此而生成。对于上文的 User 类,其实现会类似下面这样:
fun copy(name: String = this.name, age: Int = this.age) = User(name, age)
这让我们可以写:
val jack = User(name = “Jack”, age = 1)
val olderJack = jack.copy(age = 2)
### 5.3.2 数据的解构
> 解构:顾名思义,就是将对象的相关属性,按照解构函数的方式,解构到相应的属性上;是从属性变量构建对象的逆向过程
前面提到了,JVM会数据类生成一些列的组件函数(componentN() 函数群),其与这些函数与类的属性对应, 函数名中的数字 1 到 N, 与属性的声明顺序一致,有了这些组件函数, 就可以在解构声明中使用数据
val jane = Person(“Jane”, 35)
val (n, a) = jane
println(“ n, a years of age”) // 打印结果将是 “Jane, 35 years of age”
### 5.3.3 标准数据类
标准库提供了 Pair 和 Triple。尽管在很多情况下命名数据类是更好的设计选择, 因为它们通过为属性提供有意义的名称使代码更具可读性
Pair
## 5.4密封类
密封类用来表示受限的类继承结构:当一个值为有限集中的类型、而不能有任何其他类型时。
> 在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例
- 要声明一个密封类,需要在类名前面添加** sealed 修饰符**。
- 虽然密封类也可以有子类,但是所有子类都必须在与密封类自身相同的文件中声明。(在 Kotlin 1.1 之前, 该规则更加严格:子类必须嵌套在密封类声明的内部)
- 一个密封类是自身抽象的,它不能直接实例化并可以有抽象(abstract)成员
- 密封类不允许有非-private 构造函数(其构造函数默认为 private)
- 扩展密封类子类的类(间接继承者)可以放在任何位置,而无需在同一个文件中
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr() //这是一个对象声明,而不是一个创建对象过程,类似于java匿名类
> (上文示例使用了 Kotlin 1.1 的一个额外的新功能:数据类扩展包括密封类在内的其他类的可能性。 )
使用密封类的关键好处在于使用 when 表达式 的时候,如果能够验证语句覆盖了所有情况,就不需要为该语句再添加一个 else 子句了。当然,这只有当你用 when 作为表达式(使用结果)而不是作为语句时才有用
fun eval(expr: Expr): Double = when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
// 不再需要 else
子句,因为我们已经覆盖了所有的情况
}
## 5.5 内部类(嵌套类)
### 5.5.1 嵌套类
说的简单一点,就是定义在类里面的类。一般把定义内部类的外围类成为包装类或者外部类
class Outer {
private val bar: Int = 1
class Nested {
fun foo() = 2
}
}
val demo = Outer().Nested().foo() // == 2
嵌套类的实例的创建方式与普通类稍有不同,需要先拥有其外部类的的实例,然后通过外部类的实例创建
### 5.5.2 内部类
类可以使用 inner 关键字来标记, 然后就可以访问外部类(outer class)的成员
class Outer {
private val bar: Int = 1
inner class Inner {
fun foo() = bar
}
}
val demo = Outer().Inner().foo() // == 1
> 内部类可以访问外部类中的成员属性和成员函数,这是因为内部类会保存一个引用, 指向
外部类的对象实例。所以,在内部类使用this关键字时,容易引起歧义,怎么消除歧义,参见限定的 this 表达式以了解内部类中的 this 的消歧义用法
### 5.5.3 匿名内部类
使用《对象表达式》创建匿名内部类实例:
window.addMouseListener(object: MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
// ……
}
override fun mouseEntered(e: MouseEvent) {
// ……
}
})
如果对象是函数式 Java 接口(即具有单个抽象方法的 Java 接口)的实例, 你可以使用带接口类型前缀的lambda表达式创建它:
val listener = ActionListener { println(“clicked”) }
## 5.6 枚举类
举类的最基本的用法是实现类型安全的枚举,关键字:enum
- **声明格式**:
enum class 类名{
…
}
- **访问枚举常量**,不需要实例化枚举类就可以访问枚举常量:
枚举类名.枚举常量.属性
示例:
enum class State{
NORMAL,NO_DATA,NO_INTERNET,ERROR,OTHER
}
// 使用中缀符号访问枚举常量
State.NORMAL.name
State.NO_DATA.name
State.NO_INTERNET.name
State.ERROR.name
State.OTHER.name
- **枚举常量的初始化**
- 因为每一个枚举都是枚举类的实例,所以他们可以是初始化过的。
enum class Color(var argb : Int){
RED(0xFF0000),
WHITE(0xFFFFFF),
BLACK(0x000000),
GREEN(0x00FF00)
}
### 5.6.1 使用枚举常量
- 每个枚举常量都包含两个属性:name(枚举常量名)和ordinal(枚举常量位置)
- 提供了values()和valueOf()方法来检测指定的名称与枚举类中定义的任何枚举常量是否匹配
- 就像在 Java 中一样,Kotlin 中的枚举类也有合成方法允许列出定义的枚举常量以及通过名称获取枚举常量
-自 Kotlin 1.1起,可以使用 enumValues<T>()和 enumValueOf<T>()函数以泛型的方式访问枚举类中的常量
enum class Color(var argb : Int){
RED(0xFF0000),
WHITE(0xFFFFFF),
BLACK(0x000000),
GREEN(0x00FF00)
}
fun main(args: Array) {
println(“name = ” + Color.RED.name + “\tordinal = ” + Color.RED.ordinal)
println(“name = ” + Color.WHITE.name + “\tordinal = ” + Color.WHITE.ordinal)
println(“name = ” + Color.BLACK.name + “\tordinal = ” + Color.BLACK.ordinal)
println(“name = ” + Color.GREEN.name + “\tordinal = ” + Color.GREEN.ordinal)
}
//输出结果为:
//name = RED ordinal = 0
//name = WHITE ordinal = 1
//name = BLACK ordinal = 2
//name = GREEN ordinal = 3
fun main(args: Array) {
println(enumValues().joinToString { it.name })
println(enumValueOf(“RED”))
}
//输出结果为:
//RED, WHITE, BLACK, GREEN
//RED
- 使用valueOf()和values()检测
- EnumClass.valueOf(value: String): 由枚举值的名称获取枚举实例
若使用Color.valueOf("不存在的枚举常量"),则会抛出IllegalArgumentException 异常,即枚举变量不存在
- EnumClass.values(): 获取所有的枚举实例
若使用Color.values()[大于枚举常量位置],则会抛出下标越界异常
```
println(Color.valueOf("RED"))
println(Color.values()[0])
println(Color.values()[1])
println(Color.values()[2])
println(Color.values()[3])
//输出结果为:
//RED
//RED
//WHITE
//BLACK
//GREEN
enum class ConsoleColor(var argb : Int){
RED(0xFF0000){
override fun print() {
println("我是枚举常量 RED ")
}
},
WHITE(0xFFFFFF){
override fun print() {
println("我是枚举常量 WHITE ")
}
},
BLACK(0x000000){
override fun print() {
println("我是枚举常量 BLACK ")
}
},
GREEN(0x00FF00){
override fun print() {
println("我是枚举常量 GREEN ")
}
};
abstract fun print()
}
fun main(args: Array) {
ConsoleColor.BLACK.print()
}
//输出结果为:
//我是枚举常量 BLACK
枚举类实现接口的情况与抽象方法类似,所有的枚举常量都应在其匿名类中实现接口的方法。
enum class Person(var type: Int): IClickListener{
STUDENT(1) {
override fun doClick() {
println("do click")
}
},
TEACHER(2) {
override fun doClick() {
println("do click")
}
},
DEAN(3) {
override fun doClick() {
println("do click")
}
},
PRINCIPAL(4) {
override fun doClick() {
println("do click")
}
};
}
Enum.kt这个源文件
默认实现了companion object {}
仅提供了两个属性
/**
* Returns the name of this enum constant, exactly as declared in its enum declaration.
*/
public final val name: String
/**
* Returns the ordinal of this enumeration constant (its position in its enum declaration, where the initial constant
* is assigned an ordinal of zero).
*/
public final val ordinal: Int
public abstract class Enum>(name: String, ordinal: Int): Comparable{
...
}
注意:这里和Java不同的点是,没有new这个关键字
创建一个类的实例,需要调用类的构造函数,就像它是一个常规函数一样,
var test = Test()
var test1 = Test(1,2)
建嵌套类、内部类和匿名内部类的类实例在嵌套类中有述
继承作为OOP的三大特性之一,Kotlin对其必然有自己的理解。Kotlin以“:”操作符,完成子类继承父类
open修饰符相当于java的final的反义词,父类默认不能被继承,父类内部的函数属性默认不能被重写,open修饰符定义开放性
final 关键字用于修饰传递过程中的父子继承能力修改,与java类似
The root of the Kotlin class hierarchy. Every Kotlin class has [Any] as a superclass.
class Example // 从 Any 隐式继承
public open class Any {
public open operator fun equals(other: Any?): Boolean
public open fun hashCode(): Int
public open fun toString(): String
}
当 Java 类型导入到 Kotlin 中时,类型 java.lang.Object 的所有引用都成了 Any。 而因为 Any 不是平台指定的,它只声明了 toString()、hashCode() 和 equals() 作为其成员, 所以为了能用到 java.lang.Object 的其他成员,Kotlin 要用到扩展函数
声明一个明确的父类, 需要在类头后加冒号再加父类即可。
open class Base(p: Int)
class Derived(p: Int) : Base(p)
类上的 open 标注与 Java 中 final 相反,它允许其他类从这个类继承。
默认情况下,在 Kotlin 中所有的类都是 final, 对应于 Effective Java书中的第 17 条:要么为继承而设计,并提供文档说明,要么就禁止继承
如果子类有主构造函数, 则基类必须在子类的主构造函数中立即初始化
open class Person(var name : String, var age : Int){
override fun toString(): String{
return "Person(name='$name', age=$age)"
}
}
class Student(name : String, age : Int, var no : String, var score : Int) : Person(name, age) {
}
如果子类没有主构造函数,则必须在每一个二级构造函数中用 super 关键字初始化基类,或委托给另一个构造函数做到这一点。
注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数:
class MyView : View {
constructor(ctx: Context) : super(ctx) {
}
constructor(ctx: Context, attrs: AttributeSet) : super(ctx,attrs) {
}
}
Kotlin 力求清晰显式,与 Java 不同,Kotlin 需要显式标注可覆盖的成员(我们称之为开放)和覆盖后的成员
open class Base {
open fun v() {}
fun nv() {}
}
class Derived() : Base() {
override fun v() {}
}
open class AnotherDerived() : Base() {
final override fun v() {}
}
如果一个类从它的直接超类继承相同成员的多个实现, 它必须覆盖这个成员并提供其自己的实现(也许用继承来的其中之一)。
为了表示采用从哪个超类型继承的实现,我们使用由尖括号中超类型名限定的 super,如 super
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.f()//调用 A.f()
super.f()//调用 B.f()
}
}
同时继承 A 和 B 没问题,并且 a() 和 b() 也没问题因为 C 只继承了每个函数的一个实现。
但是 f() 由 C 继承了两个实现,所以我们必须在 C 中覆盖 f() 并且提供我们自己的实现来消除歧义。
属性覆盖与方法覆盖类似:
open class Foo {
open val x: Int get() { …… }
}
class Bar1 : Foo() {
override val x: Int = ……
}
注意,你可以在主构造函数中使用 override 关键字作为属性声明的一部分
interface Foo {
val count: Int
}
class Bar1(override val count: Int) : Foo
class Bar2 : Foo {
override var count: Int = 0
}
当子类继承了某个类之后,便可以使用父类中的成员变量,但是并不是完全继承父类的所有成员变量。具体的原则如下:
派生类中的代码可以使用 super 关键字调用其超类的函数与属性访问器的实现:
open class Foo {
open fun f() { println("Foo.f()") }
open val x: Int get() = 1
}
class Bar : Foo() {
override fun f() {
super.f()
println("Bar.f()")
}
override val x: Int get() = super.x + 1
}
在一个内部类中访问外部类的超类,可以通过由外部类名限定的 super 关键字来实现:super@Outer:
class Bar : Foo() {
override fun f() { /* …… */ }
override val x: Int get() = 0
inner class Baz {
fun g() {
super@Bar.f() // 调用 Foo 实现的 f()
println(super@Bar.x) // 使用 Foo 实现的 x 的 getter
}
}
}
在Kotlin中,允许对类进行扩展,不需要继承或使用 Decorator 模式,通过一种特殊形式的声明,来实现具体实现某一具体功能
Kotlin 支持 扩展函数 和 扩展属性
扩展函数并没有对原类做修改,而是为被扩展类的对象添加新的函数
扩展函数定义形式:
fun receiverType.functionName(params){
body
}
fun MutableList.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' 指代 list 实例
this[index1] = this[index2]
this[index2] = tmp
}
fun main(args: Array<String>) {
val l = mutableListOf(1, 2, 3)
l.swap(0, 2) // 'swap()' 函数内的 'this' 将指向 'l' 的值
println(l.toString())
}
// Log
[3, 2, 1]
this关键字指代接收者对象(receiver object)(也就是调用扩展函数时, 在点号之前指定的对象实例).
扩展函数是静态解析的,并未对原类增添函数或者属性,也就是说对其本身没有丝毫影响
扩展不能真正的修改他们所扩展的类。通过定义一个扩展,你并没有在一个类中插入新成员, 仅仅是可以通过该类型的变量用点表达式去调用这个新函数
我们想强调的是扩展函数是静态分发的,即他们不是根据接收者类型的虚方法(运行时决定函数)。 这意味着调用的扩展函数是由函数调用所在的表达式的类型来决定的, 而不是由表达式运行时求值结果决定的
open class C
class D: C()
fun C.foo() = "c"
fun D.foo() = "d"
fun printFoo(c: C) {
println(c.foo())
}
printFoo(D())//传入了一个D的实例
//这个例子会输出 "c",因为调用的扩展函数只取决于参数 c 的声明类型,该类型是 C 类
当成员函数和扩展函数想碰时,就像一个亲生的和私生的,当私生的碰到亲生的会怎么样呢?
如果一个类定义有一个成员函数和一个扩展函数,而这两个函数又有相同的接收者类型、相同的名字并且都适用给定的参数,这种情况总是取成员函数
class C {
fun foo() { println("member") }
}
fun C.foo() { println("extension") }
当然,扩展函数重载同样名字但不同签名成员函数也完全可以:
class C {
fun foo() { println("member") }
}
fun C.foo(i: Int) { println("extension") }
调用 C().foo(1) 将输出 “extension”
fun Any?.toString(): String {
if (this == null) return "null"
// 空检测之后,“this”会自动转换为非空类型,所以下面的 toString()
// 解析为 Any 类的成员函数
return toString()
}
和函数类似,Kotlin 支持扩展属性:
val <T> List<T>.lastIndex: Int
get() = size - 1
注意:由于扩展没有实际的将成员插入类中,因此对扩展属性来说幕后字段是无效的。这就是为什么扩展属性不能有初始化器。他们的行为只能由显式提供的 getters/setters 定义
扩展属性只能被声明为val而不能被声明为var.如果强制声明为var,即使进行了初始化,在运行也会报异常错误,提示该属性没有后端域变量
val Foo.bar = 1 // 错误:扩展属性不能有初始化器
在Kotlin-伴随对象中了解到,伴随对象通过“类名.”形式调用伴随对象。假如对伴随对象声明了扩展函数该怎么调用呢?其调用方式与伴随对象一样,都是通过用类名限定符来调用。当然,扩展属性也是这样的。
//扩展函数
fun Person.Companion.doSwim() {
println("伴随对象的扩展函数")
}
//扩展属性
val Person.Companion.no: Int
get() = 10
fun main(args: Array<String>) {
println("age:${Person.age}")
Person.doSwim()
}
在MyInfo类内,创建了Person类的扩展。此时,MyInfo被成为派发接受者,而Person为扩展接受者。从下例中,可以清楚的看到,在扩展函数中,可以调用派发接收者的成员函数
class D {
fun bar() { …… }
}
class C {
fun baz() { …… }
fun D.foo() {
bar() // 调用 D.bar
baz() // 调用 C.baz
}
fun caller(d: D) {
d.foo() // 调用扩展函数
}
}
假如在调用某一个函数,而该函数在派发接受者和扩展接受者均存在,此时扩展函数将会调用哪一个呢?
class D {
fun bar() { …… }
}
class C {
fun baz() { …… }
fun bar() { …… }
fun D.foo() {
bar() // 调用 D.bar,因为D是扩展接收者
baz() // 调用 C.baz
}
fun caller(d: D) {
d.foo() // 调用扩展函数
}
}
class C {
fun D.foo() {
toString() // 调用 D.toString()
this@C.toString() // 调用 C.toString()
}
open class D {
}
class D1 : D() {
}
open class C {
open fun D.foo() {
println("D.foo in C")
}
open fun D1.foo() {
println("D1.foo in C")
}
fun caller(d: D) {
d.foo() // 调用扩展函数
}
}
class C1 : C() {
override fun D.foo() {
println("D.foo in C1")
}
override fun D1.foo() {
println("D1.foo in C1")
}
}
C().caller(D()) // 输出 "D.foo in C"
C1().caller(D()) // 输出 "D.foo in C1" —— 分发接收者虚拟解析,要看具体的调用示例对象
C().caller(D1()) // 输出 "D.foo in C" —— 扩展接收者静态解析,函数声明的类型是什么就是什么
大多数时候我们在顶层定义扩展,即直接在包里:
package foo.bar
fun Baz.goo() { …… }
要使用所定义包之外的一个扩展,我们需要在调用方导入它:
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 也属于同一种命名方式。 关于这些 Utils-类的不愉快的部分是代码写成这样:
// Java
Collections.swap(list, Collections.binarySearch(list, Collections.max(otherList)), Collections.max(list));
这些类名总是碍手碍脚的,我们可以通过静态导入达到这样效果:
// Java
swap(list, binarySearch(list, max(otherList)), max(list));
举一个自定义的例子,就拿Toast来说,到处都可以用到,所以
【图6.3.4.1】
package sangfor.com.kotlintest.Extended
import android.content.Context
import android.widget.Toast
/**
* 类描述:
* Created by yhf on 2017/12/21.
*/
fun Context.toast(message: String, length: Int = Toast.LENGTH_SHORT) = Toast.makeText(this,message,length).show()
【图6.3.4.2】
一般类和函数,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的约束对代码的限制很大。而OOP的多态采用了一种泛化的机制,在SE 5种,Java引用了泛型。
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
在Kotlin中,依然可以使用泛型,解耦类与函数与所用类型之间的约束,甚至是使用方法都与Java一致。
声明一个泛型类
class Box<T>(t: T) {
var value = t
}
通常, 要创建这样一个类的实例, 我们需要指定类型参数:
val box: Box<Int> = Box<Int>(1)
但是, 如果类型参数可以通过推断得到, 比如, 通过构造器参数类型, 或通过其他手段推断得到, 此时允许省略类型参数:
val box = Box(1) // 1 的类型为 Int, 因此编译器知道我们创建的实例是 Box<Int> 类型
泛型函数与其所在的类是否是泛型没有关系,泛型函数使得该函数能够独立于其所在类而产生变化
在有这么一句话:无论何时只要你能做到,你就应该尽量使用泛型方法,也就是说如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更明白。这种泛型使用思想,在Kotlin中依然可以延续
fun singletonList(item: T): List {
// ……
}
fun T.basicToString() : String { // 扩展函数
// ……
}
//调用
val l = singletonList(1)
现声明了一个泛型类Box,在不同的类型的类型在行为方面肯定不一样,但是在我们获取其所在类时,我们只是得到了“class com.teaphy.generic.Box”。
在这里我们不得不面对一个残酷的现实:在泛型内部,无法获得任何有关泛型参数类型的信息
class Box
(t : T) { var value = t } fun main(args: Array ) { var boxInt = Box (10) var boxString = Box ("Jone") println(boxInt.javaClass) // 打印:class com.teaphy.generic.Box println(boxString.javaClass) // 打印:class com.teaphy.generic.Box } 不管是Java还是Kotlin,泛型都是使用擦除来实现的,这意味着当你在使用泛型时,任务具体的类型信息都被擦除的,你唯一知道的就是你再使用一个对象。比如,Box和Box在运行时是想的类型,都是Box的实例
在使用泛型时,具体类型信息的擦除是我们不不懂得不面对的,在Kotlin中也为我们提供了一些可供参考的解决方案:
- 泛型约束:冒号之后指定的类型是上界,约定超类类型,使得实际类型比继续超类的子类
- 类型协变:协变注解in和out的用意,其实际上是定义了类型参数在该类或者接口的用途,是用来消费的还是用来返回的,对其做了相应的限定
- 类型投射
对于一个给定的类型参数所允许使用的类型, 可以通过泛型约束(generic constraint) 来限制
冒号之后指定的类型是上界:只有 Comparable 的子类型可以替代 T
fun <T : Comparable<T>> sort(list: List<T>) {
// ……
}
//sort(listOf(1, 2, 3)) // OK。Int 是 Comparable<Int> 的子类型
//sort(listOf(HashMap<Int, String>())) // 错误:HashMap<Int, String> 不是 Comparable<HashMap<Int, String>> 的子类型
默认的上界(如果没有声明)是 Any?
fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>
where T : Comparable<T>,
T : Cloneable {
return list.filter { it > threshold }.map { it.clone() }
}
协变注解in和out的用意,其实际上是定义了类型参数在该类或者接口的用途,是用来消费的还是用来返回的,对其做了相应的限定
在类型声明时,使用协变注解修饰符(in或者out)。由于这个注解出现在类型参数的声明处, 因此我们称之为声明处的类型变异
假设我们有一个泛型接口Source
internal interface Source<in T, out R> {
fun mapT(t: T): Unit
fun nextR(): R
}
上面我们已经了解到了协变注解in和out的用意,下面我们将会用in和out,做一件有意义的事,看下面代码
fun copy(from: Array, to: Array) {
// ...
}
fun fill(dest: Array<in String>, value: String) {
// ...
}
这里发生的事情称为类型投影:我们说from不仅仅是一个数组,而是一个受限制的(投影的)数组:我们只可以调用返回类型为类型参数 T 的方法,如上,这意味着我们只能调用 get()
对于copy函数中中,from的泛型参数使用了协变注解out修饰,意味着该参数不能在该函数中消费,也就是说在该函数中禁止对该参数进行任何操作
对于fill函数中,dest的泛型参数使用了协变注解in修饰,Array与Java的 Array
有些时候, 你可能想表示你并不知道类型参数的任何信息, 但是仍然希望能够安全地使用它. 这里所谓”安全地使用”是指, 对泛型类型定义一个类型投射, 要求这个泛型类型的所有的实体实例, 都是这个投射的子类型
对于这个问题, Kotlin 提供了一种语法, 称为 星号投射(star-projection):
如果一个泛型类型中存在多个类型参数, 那么每个类型参数都可以单独的投射. 比如, 如果类型定义为interface Function
代理类 和 目标类 继承同一接口,其中代理类持有目标类的实例,并在对应的接口方法中自定义逻辑 【代理律师:起诉前,起诉,起诉后】
代理模式(Proxy Pattern)也称为委托模式,为其他对象提供一种代理以控制对这个对象的访问
Kotlin中,委托的实现依靠于关键字 by ,by表示将抽象主题的实例(by后边的实例)保存在代理类实例的内部
比如SportsManager类继承于ISports接口,并可以ISports接口的所有的 public 方法委托给一个指定的对象:
interface ISports {
fun doSports()
}
class SwimForSports: ISports{
override fun doSports() {
println("do swim")
}
}
class SportsManager(sport: ISports): ISports by sport
fun main(args: Array) {
val swimSports: SwimForSports = SwimForSports()
SportsManager(swimSports).doSports()// Log:do swim
}
在SportsManager声明中,by子句表示,将sport保存在SportsManager的对象实例内部
而且编译器将会生成继承自 ISports 接口的所有方法, 并将调用转发给sport
所谓的委托属性,就是对其属性值的操作不再依赖于其自身的getter()/setter()方法,是将其托付给一个代理类,从而每个使用类中的该属性可以通过代理类统一管理,再也不用在每个类中,对其声明重复的操作方法
当我们使用属性的get或者set的时候,属性委托的getValue和setValue就会被调用
val/var <property name>: <Type> by
by关键字之后的表达式就是委托,属性的get()方法(以及set() 方法)将被委托给这个对象的 getValue()和setValue()方法
属性委托不必实现任何接口, 但必须提供 getValue() 函数(对于 var属性,还需要 setValue() 函数)。
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name} in $thisRef.'")
}
}
val e = Example()
println(e.p)
//输出结果:
Example@33a17727, thank you for delegating ‘p’ to me!
e.p = "NEW"
//输出结果:
NEW has been assigned to ‘p’ in Example@33a17727.
对于一个只读属性(即 val 声明的),委托必须提供一个名为 getValue 的函数,该函数接受以下:
对于一个可变属性(即 var 声明的),委托必须额外提供一个名为 setValue 的函数,该函数接受以下参数:
getValue() 或/和 setValue() 函数可以通过委托类的成员函数提供或者由扩展函数提供。 当你需要委托属性到原本未提供的这些函数的对象时后者会更便利。 两函数都需要用 operator 关键字来进行标记
在每个委托属性的实现的背后,Kotlin 编译器都会生成辅助属性并委托给它
例如,对于属性 prop,生成隐藏属性 prop$delegate,而访问器的代码只是简单地委托给这个附加属性:
class C {
var prop: Type by MyDelegate()
}
// 这段是由编译器生成的相应代码:
class C {
private val prop$delegate = MyDelegate()
var prop: Type
get() = prop$delegate.getValue(this, this::prop)
set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}
Kotlin 编译器在参数中提供了关于 prop 的所有必要信息:第一个参数 this 引用到外部类 C 的实例而 this::prop 是 KProperty 类型的反射对象,该对象描述 prop 自身
lazy() 是接受一个 lambda 并返回一个 Lazy 实例的函数,返回的实例可以作为实现延迟属性的委托: 第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果
// File: test.kt
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
val c = 200
fun main(args: Array) {
val b = 200 //此处打断点
println(lazyValue)
println(lazyValue)
}
//这个例子输出:
computed!
Hello
Hello
现声明了两个Top-level属性lazyValue 和 c,其中lazyValue为延迟加载属性(委托属性)
那么你会看到如下情形:
- var类型属性不能设置为延迟加载属性,因为在lazy中并没有setValue(…)方法
2.lazy操作符是线程安全的。如果在不考虑多线程问题或者想提高更多的性能,也可以使 用 lazy(LazyThreadSafeMode.NONE){ … }
Delegates.observable() 函数接受两个参数
这种形式的委托,采用了观察者模式,其会检测可观察属性的变化,当被观察属性的setter()方法被调用的时候”
//定义一个可观察属性,初始化值为initValue,处理器为打印日志操作lamda表达式
var name: String by Delegates.observable("initValue", {
kProperty, oldName, newName ->
println("kProperty:${kProperty.name} | oldName:$oldName | newName:$newName")
})
fun main(args: Array<String>) {
println("name: $name") // Log:nam:initValue
name = "zhang" // Log:kProperty:name | oldName:initValue | newName:zhang
name = "li" // Log:kProperty:name | oldName:zhang | newName:li
}
observable是在赋值后触发,如果你想能够截获一个赋值并“否决”它,就使用 vetoable() 取代 observable()。 在属性被赋新值生效之前会调用传递给 vetoable 的处理程序
Delegates.vetoable()函数也接受两个参数: 第一个是初始化值, 第二个是属性值变化事件的响应器(handler),是可观察属性(Observable)的一个特例
不同的是在响应器指定的自动执行执行的lambda表达式中在保存新值之前做一些条件判断,来决定是否将新值保存,也就是响应器(handler)会有一个布尔返回值
var name: String by Delegates.vetoable("initValue", {
kProperty, oldValue, newValue ->
println("oldValue:$oldValue | newValue:$newValue")
newValue.contains("initValue") //返回布尔值,标示是否执行赋值操作
})
fun main(args: Array) {
println("name: $name")
println("------------------")
name = "zhangLing"
println("name: $name")
println("------------------")
name = "initValueNew"
println("name: $name")
}
//Log
name: initValue //仅是get,无set操作,输出初始值
------------------
oldValue:initValue | newValue:zhangLing //对应赋值语句name = "zhangLing" , 根据处理器的返回值:newValue不包含initValue,不执行set
name: initValue //newValue赋值失败
------------------
oldValue:initValue | newValue:initValueNew //对应赋值语句name = "zhangLing" , 根据处理器的返回值:newValue包含initValue,执行set
name: initValueNew//newValue赋值成功
在实际开发时,我们可能会设置可为null的var类型属性,在我们使用它时,肯定是对其赋值,假如不赋值,必然要报NullPointException.
一种解决方案是,我们可以在使用它时,在每个地方不管是不是null,都做null检查,这样我们就保证了在使用它时,保证它不是null。这样无形当中添加了很多重复的代码。
在Kotlin中,Not Null委托会含有一个可null的变量并会在我们设置这个属性的时候分配一个真实的值。如果这个值在被获取之前没有被分配,它就会抛出一个异常
这个在单例App这个例子中很有用:
class App : Application() {
companion object {
var instance: App by Delegates.notNull() //必须赋值的instance对象
}
override fun onCreate() {
super.onCreate()
instance = this
}
}
在Kotlin中,存在这么一种委托方式,类的构造器接受一个map实例作为参数,将map实例本身作为属性的委托,属性的名称与map中的key是一致的,也就是意味着我们可以很简单的从一个动态地map中创建一个对象实例
这经常出现在像解析 JSON 或者做其他“动态”事情的应用中:
class User(val map: Map) {
val name: String by map
val age: Int by map
}
fun main(args: Array) {
//类的构造器接受一个 map 实例作为参数
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
//委托属性将从这个 map中读取属性值(使用属性名称字符串作为 key 值)
println(user.name) // 打印结果为: "John Doe"
println(user.age) // 打印结果为: 25
}
class User(val map: MutableMap) {
val name: String by map
val age: Int by map
}
fun main(args: Array) {
var map:MutableMap = mutableMapOf(
"name" to "John Doe",
"age" to 25)
val user = User(map)
println(user.name) // 打印结果为: "John Doe"
println(user.age) // 打印结果为: 25
println("--------------")
map.put("name", "Green Dao")
map.put("age", 30)
println(user.name) // 打印结果为: Green Dao
println(user.age) // 打印结果为: 30
}
public interface ReadOnlyProperty {
/**
* 返回给定对象的属性的值
* @param thisRef:拥有委托属性的对象
* @param property:属性的元数据
* @return:属性的值
*/
public operator fun getValue(thisRef: R, property: KProperty<*>): T
}
public interface ReadWriteProperty {
/**
* 返回给定对象的属性的值
* @param thisRef:拥有委托属性的对象
* @param property:属性的元数据
* @return:属性的值
*/
public operator fun getValue(thisRef: R, property: KProperty<*>): T
/**
* 设置给定对象的属性的值
* @param thisRef:拥有委托属性的对象
* @param property:属性的元数据
* @param value:设置的属性的值
*/
public operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}
class SingleValueVar : ReadWriteProperty {
private var value: T? = null
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
if (null != value && null == this.value) {
this.value = value
}
}
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value!!
}
}
//这是一个对象声明
//如果你需要写一个可以无需用一个类的实例来调用、但需要访问类内部的函数(例如,工厂方法),你可以把它写成该类内的一个对象声明
object DelegatesExt {
//定义一个泛型函数
fun singleValueVar(): ReadWriteProperty = SingleValueVar()
}
class Student {
var no: Int by DelegatesExt.singleValueVar()
var name: String = "wang"
override fun toString(): String {
return "no: $no | name: $name"
}
}
fun main(args: Array) {
var stu: Student = Student()
stu.no = 20
stu.name = "wang"
println(stu.toString()) // 打印:no: 20 | name: wang
stu.no = 30
stu.name = "li"
println(stu.toString()) // 打印:no: 20 | name: li
}
在上述测试示例中,我们先对Studeng的实例,暂定为A,第一次给A赋值no=20,name=”wang”,此时打印这个实例的结果为 “no: 20 | name: wang”
然后我们对A进行了第二次赋值no=30,name=”li”,但是实际上打印结果为“no: 20 | name: li”。
相比较两次结果,no的值都是20,这恰恰是我们想要的结果。因为我们对no属性设置了SingleValueVar委托,该委托的效果是只有第一次对该属性赋值时有效,再次赋值时无效
你可以将局部变量声明为委托属性。 例如,你可以使一个局部变量惰性初始化:
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
memoizedFoo 变量只会在第一次访问时计算。 如果 someCondition 失败,那么该变量根本不会计算
通过定义 provideDelegate 操作符,可以扩展创建属性实现所委托对象的逻辑。 如果 by 右侧所使用的对象将 provideDelegate 定义为成员或扩展函数,那么会调用该函数来创建属性委托实例
provideDelegate 的一个可能的使用场景是在创建属性时(而不仅在其 getter 或 setter 中)检查属性一致性。
例如,如果要在绑定之前检查属性名称,可以这样写:
class ResourceDelegate<T> : ReadOnlyProperty<MyUI, T> {
override fun getValue(thisRef: MyUI, property: KProperty<*>): T { ... }
}
class ResourceLoader<T>(id: ResourceID<T>) {
operator fun provideDelegate(
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty<MyUI, T> {
checkProperty(thisRef, prop.name)
// 创建委托
return ResourceDelegate()
}
private fun checkProperty(thisRef: MyUI, name: String) { …… }
}
class MyUI {
fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { …… }
val image by bindResource(ResourceID.image_id)
val text by bindResource(ResourceID.text_id)
}
在 Kotlin 1.1+ 中协程是实验性的。
一些 API 启动长时间运行的操作(例如网络 IO、文件 IO、CPU 或 GPU 密集型任务等),并要求调用者阻塞直到它们完成。协程提供了一种避免阻塞线程并用更廉价、更可控的操作替代线程阻塞的方法:协程挂起。
协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器!)上调度执行,而代码则保持如同顺序执行一样简单
参见协程
官方文档
Jetictors-Kotlin系列
行云间专栏-Kotlin系列
al4fun的android笔记
配置相关
Kotlin编程之AndroidStudio(包括3.0与2.x版本)配置与使用
基础部分补充
类的组成部分补充
操作部分