文章目录
- 一、概念
- 二、准备开始
- 三、语言基础
- 四、类的定义与组成
- 五、类的类别
- 六、类的操作
- 参考文献
一、概念
1.1 Kotlin语言介绍
Kotlin 是 JetBrains 在 2010 年推出的基于 JVM 的新编程语言。开发者称,设计它的目的是避免 Java 语言编程中的一些难题。比如:
- 创建一种兼容 Java 的语言
- 让它比 Java 更安全,能够静态检测常见的陷阱。如:引用空指针
- 让它比 Java 更简洁,通过支持 variable type inference,higher-order functions (closures),extension functions,mixins and first-class delegation 等实现
- 让它比最成熟的竞争对手 Scala语言更加简单
作为一个跨平台的语言,Kotlin 可以工作于任何 Java 的工作环境:服务器端的应用,移动应用(Android版),桌面应用程序。Kotlin这个语言从一开始推出到如今,已经有六年了。官方正式发布首个稳定版本的时间相对比较晚(2016.2),这是一门比较新的语言。其大致发展简史如下:
- 2011年7月,JetBrains推出Kotlin项目。
- 2012年2月,JetBrains以Apache 2许可证开源此项目。
- 2016年2月15日,Kotlin v1.0(第一个官方稳定版本)发布。
- 2017 Google I/O 大会,Kotlin “转正”。
Kotlin 具有很多下一代编程语言静态语言特性:如类型推断、多范式支持、可空性表达、扩展函数、模式匹配等。
Kotlin的编译器kompiler可以被独立出来并嵌入到 Maven、Ant 或 Gradle 工具链中。这使得在 IDE 中开发的代码能够利用已有的机制来构建,可以在新环境中自由使用
1.1.1 Kotlin的优势
- 相比于 Java,Kotlin 有着更好的语法结构,安全性和开发工具支持
- Kotlin 中没有基础类型,数组是定长的,泛型是安全的,即便运行时也是安全的
- 支持闭包,还可通过内联进行优化
- Java 和 Kotlin 之间的互操作性:Kotlin 可以调用 Java,反之亦可
1.1.2 Kotlin的不足
- 不支持检查异常(Checked Exceptions)
1.2 开发工具支持
1.2.1 Android Studio支持
目前AndroiStudio 3.0预览版本已自带Kotlin插件,无需做任何的配置即可开始体验。
1.2.1.1 Android Studio 3.0以下版本配置
Android Studio 3.0之前的版本,需要我们自行做些配置:在Android Studio中打开Settings,选择Plugins选项,点击Browse Repositories,在打开的窗口中搜索Kotlin,点击Install。
【图像1】
下载安装完成后会提示你重启Android Studio,重启之后,就可以使用了
1.2.1.2 Hello Kotlin
-
- 新建一个Android项目,和以前操作一样,之后我们在右键new的时候,会发现多了两项,如图:
- Kotlin File/class :这和Java Class 一样,就是一个普通的类,只不过是Kotlin语法创建;
- Kotlin Activity :这个也和平时创建Activity一样,选择模板什么的,创建Kotlin Activity;
【图像2】
-
- 第一次创建Kotlin Activity,会提示 Kotlin not configured(未提示就sync一下),如图
- 直接点configure
- 然后点 Android with Gradle
- 之后进入"Configure Kotlin in Project"的Kotlin配置界面,默认点 ok 即可:配置了kotlin的版本和支持kotlin的modules
【图像3-5】
-
- 稍等片刻,配置完成后会发现Project下的build.gradle 和 moudle下的build.gradle自动配置了一些参数,如图:
- Project下的build.gradle
[ext.kotlin_version]:语言版本
[kotlin-gradle-plugin]:完成了Gradle构建Kotlin工程的所有依赖构建执行的相关工作 - moudle下的build.gradle
[apply plugin: ‘java’ ]: 使用Gradle java、kotlin插件
[apply plugin: ‘kotlin’] : 使用Gradle java、kotlin插件
[sourceCompatibility = 1.8]: 源代码JDK兼容性配置兼容1.8往后的版本
[kotlin-stdlib-jre8] :Kotlin JVM执行环境依赖
[org.jetbrains.kotlin:kotlin-stdlib-js] : Kotlin JS执行环境依赖
[kotlin-stdlib] : Kotlin运行环境的标准库
【图6-7】
1.2.1.3 转换Java to Kotlin
安装完插件的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以下版本配置》
1.2.2 云端IDE
未来的是云的世界。不需要搭建本地开发运行环境,直接用浏览器打开 https://try.kotlinlang.org/
- 你就可以直接使用云端IDE来即时编写Kotlin代码,并运行之。一个运行示例如下图:
【图8】
- Koans 开发了一套入门问题,指导完成基本的语言学习
二、准备开始
2.1 基本语法
2.1.1 定义包名
在源文件的开头定义包名:
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()
2.1.2 定义函数
- 定义一个函数接受两个 int 型参数,返回值为 int
- 该函数只有一个表达式函数体以及一个自推导型的返回值
fun sum(a: Int , b: Int) : Int{
return a + b
}
//fun sum(a: Int, b: Int) = a + b
fun main(args: Array<String>) {
print("sum of 3 and 5 is ")
println(sum(3, 5))
}
//fun main(args: Array<String>) {
// println("sum of 19 and 23 is ${sum(19, 23)}")
//}
- 返回一个没有意义的值:
- Unit 的返回类型可以省略:
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<String>) {
printSum(-1, 8)
}
更多请参看 《4.1 函数》
2.1.3 定义变量和常量
Kotlin 中使用 var 和 val 来界定 reference 的可变性,这其实是对 final 修饰符的泛化。带来的结果是,它让我更关注 reference 应该是 mutable 还是 immutable 的。我践行的一种思想是 [ 尽量保持 reference 是 immutable 的 ]
- 常量
- Kotlin变量的声明方式与Java中声明变量有很大的区别,而且必须使用var关键字
- 定义格式: 关键字 常量名: 数据类型 = xxx
- val是Kotlin中定义常量必须使用的关键字
- ,使用大写字母表示常量,个人觉得从编码习惯的角度来说,还是保持和Java一样
fun main(args: Array<String>) {
//立即初始化
val NUM_A: Int = 100
//推导出类型
val NUM_B = 50
//没有初始化的时候,必须声明类型
val NUM_C: Int
NUM_C = 1
// c += 1 因为c是常量,所以这句代码是会报错的
}
- 变量
- Kotlin变量的声明方式与Java中声明变量有很大的区别,而且必须使用val关键字,而在Java中是使用final关键字修饰的,而且为了区别一般都用大写字母表示常量
- 定义格式: 关键字 变量名: 数据类型 = xxx
- var是Kotlin中定义变量必须使用的关键字
- 每一行代码的结束可以省略掉分号(‘;’),这一点是和Java不同的地方
fun main(args: Array<String>) {
var x = 5 // 推导出Int类型
x += 1
println("x = $x")
}
更多请参看 《3.2 属性和字段》
2.1.4 注释
与 java 和 javaScript 一样,Kotlin 支持单行注释和块注释。
// 单行注释
/* 哈哈哈哈
这是块注释 */
/*
第一层块注释
/*
第二层块注释
/*
第三层快注释
这种注释方式在java中是不支持的,但是在kotlin中是支持的。算是一个亮点吧(貌似意义不大)。
*/
*/
*/
参看文档化 Kotlin 代码学习更多关于文档化注释的语法
2.2习惯用语
2.2.1 创建DTOs(POJOs/POCOs) 数据类
- 给 Customer 类提供如下方法:
- 为所有属性添加 getters
- 如果为 var 类型同时添加 setters --equals() --haseCode() --toString() --copy() --component1() , component1() , …
data class Customer(val name: String, val email: String)
《参看 3.6 数据类》
2.2.2 函数默认值
2.3 编码规范
2.4 其他
2.4.1 空安全
Kotlin对比于Java的一个最大的区别就是它致力于消除空引用所带来的危险。在Java中,如果我们尝试访问一个空引用的成员可能就会导致空指针异常NullPointerException(NPE)的出现。在Kotlin语言中就解决了这个问题,下面来看看它是如何做到的。
- Kotlin有两种类型:一个是非空引用类型,一个是可空引用类型。
- 对于可空引用,如果希望调用它的成员变量或者成员函数,直接调用会出现编译错误,有三种方法可以调用:
- (1)在调用前,需要先检查,因为可能为null
- (2)使用b?.length的形式调用,如果b为null,返回null,否则返回b.length
- (3)使用b!!.length()的形式调用,如果b为null,抛出空指针异常,否则返回b.length
- (4)Elvis 操作符?:,如果 ?: 左侧表达式非空,elvis 操作符就返回其左侧表达式,否则返回右侧表达式
- Kotlin 的类型系统旨在从我们的代码中消除 NullPointerException。NPE 的唯一可能的原因可能是
- 显式调用 throw NullPointerException();
- 使用了下文描述的 !! 操作符;
- 外部 Java 代码导致的;
- 对于初始化,有一些数据不一致(如一个未初始化的 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 的同一个属性,那么这是不安全的,并且编译器会报告一个错误
但是我们还是需要访问该属性,对吧?有几种方式可以做到:
2.4.1.1 访问可空属性的途径
- 在条件中检查 null
首先,你可以显式检查 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<String?> = listOf("A", null)
for (item in listWithNulls) {
item?.let { println(it) } // 输出 A 并忽略 null
}
- **操作符!! **
非空断言运算符(!!)将任何值转换为非空类型,若该值为空则抛出异常。
我们可以写 b!! ,这会返回一个非空的 b 值 (例如:在我们例子中的 String)或者如果 b 为空,就会抛出一个 NPE 异常:
val l = b!!.length
因此,如果你想要一个 NPE,你可以得到它,但是你必须显式要求它,否则它不会不期而至
- Elvis 操作符 ?:
如果 ?: 左侧表达式非空,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")
// ……
}
2.4.1.2 可空类型的集合
如果你有一个可空类型元素的集合,并且想要过滤非空元素,你可以使用 filterNotNull 来实现:
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
2.4.1.3 安全的类型转换
如果对象不是目标类型,那么常规类型转换可能会导致 ClassCastException。
var a: Long = 1
val aInt: Int? = a as Int // java.lang.ClassCastException
另一个选择是使用安全的类型转换,如果尝试转换不成功则返回 null:
val aInt: Int? = a as? Int
2.4.2 解构声明
解构:顾名思义,就是将对象的相关属性,按照解构函数的方式,解构到相应的属性上;是从属性变量构建对象的逆向过程
如果想对某个类的对象使用解构,那么这个类中必须定义相关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, 与属性的声明顺序一致,有了这些组件函数, 就可以在解构声明中使用数据
2.4.2.1 原理
这也就是说,如果想对某个类的对象使用解构,那么这个类中必须定义相关componentN()函数群,否则是不能解构的;数据类会自动生成组建函数(componentN() 函数群)
一个解构声明会被编译成以下代码:
val name = person.component1()
val age = person.component2()
其中的 component1() 和 component2() 函数是在 Kotlin 中广泛使用的 约定原则 的另一个例子。 (参见像 + 和 *、for-循环等操作符)
任何表达式都可以出现在解构声明的右侧,只要可以对它调用所需数量的 component 函数即可。 当然,可以有 component3() 和 component4() 等等
请注意, componentN() 函数需要用 operator 关键字标记,以允许在解构声明中使用它们
2.4.2.2 使用
- 下划线用于未使用的变量(自 1.1 起)
对于以这种方式跳过的组件,不会调用相应的 componentN() 操作符函数
val (_, status) = getResult()
- 在 lambda 表达式中解构(自 1.1 起)
你可以对 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<Int, String> -> "$value!" }
//map.mapValues { (_, value: String) -> "$value!" }
- 解构声明也可以用在 for-循环中
变量 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<Int, Status>, 但是让数据合理命名通常更好
- 解构声明和映射
- 可能遍历一个映射(map)最好的方式就是这样,为使其能用,我们应该
通过提供一个 iterator() 函数将映射表示为一个值的序列;
通过提供函数 component1() 和 component2() 来将每个元素呈现为一对
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()
2.4.3 This 表达式
为了表示当前的 接收者 我们使用 this 表达式:
- 在类的成员中,this 指的是该类的当前对象。
- 在《扩展函数(this@C.toString())》或者《带接收者的函数字面值》中, 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
}
}
}
}
2.4.4 区间
- in
- until
- step
- downTo
- rangeTo()
区间表达式由具有操作符形式 … 的 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)
}
2.4.5 类型的检查与转换“is”与“as”
2.4.5.1 is 与 !is 操作符
我们可以在运行时通过使用 is 操作符或其否定形式 !is 来检查对象是否符合给定类型:
if (obj is String) {
print(obj.length)
}
if (obj !is String) { // 与 !(obj is String) 相同
print("Not a String")
}
else {
print(obj.length)
}
2.4.5.2 转换
- “不安全的”转换操作符
- 通常,如果转换是不可能的,转换操作符会抛出一个异常。因此,我们称之为不安全的。 Kotlin 中的不安全转换由中缀操作符 as(参见operator precedence)完成
val x: String = y as String
//请注意,null 不能转换为 String 因该类型不是可空的, 即如果 y 为空,上面的代码会抛出一个异常
//为了匹配 Java 转换语义,我们必须在转换右边有可空类型,就像:
val x: String? = y as String?
- “安全的”(可空)转换操作符
- 为了避免抛出异常,可以使用安全转换操作符 as?,它可以在失败时返回 null
- 请注意,尽管事实上 as? 的右边是一个非空类型的 String,但是其转换的结果是可空的
val x: String? = y as? String
- 智能转换
- 在许多情况下,不需要在 Kotlin 中使用显式转换操作符,因为编译器跟踪不可变值的 is-检查以及显式转换,并在需要时自动插入(安全的)转换:
fun demo(x: Any) {
if (x is String) {
print(x.length) // x 自动转换为字符串
}
}
- 请注意,当编译器不能保证变量在检查和使用之间不可改变时,智能转换不能用。 更具体地,智能转换能否适用根据以下规则:
- val 局部变量——总是可以;
- val 属性——如果属性是 private 或 internal,或者该检查在声明属性的同一模块中执行。智能转换不适用于 open 的属性或者具有自定义 getter 的属性;
- var 局部变量——如果变量在检查和使用之间没有修改、并且没有在会修改它的 lambda 中捕获;
- var 属性——决不可能(因为该变量可以随时被其他代码修改)。
2.4.6 反射
反射是语言与库中的一组功能, 可以在运行时刻获取程序本身的信息.在Kotlin中,不仅可以通过发射获取类的信息,同时可以获取函数和属性的信息。也就是说,在在运行时刻得到一个函数或属性的名称和数据类型) 可以通过简单的函数式, 或交互式的编程方式实现.
在Java平台上, 使用反射功能所需要的运行时组件是作为一个单独的JAR文件发布的( kotlinreflect.jar). 这是为了对那些不使用反射功能的应用程序, 减少其运行库的大小. 如果你需要使用反射, 请注意将这个.jar文件添加到你的项目的classpath中.
2.4.6.1 类引用
最基本的反射功能就是获取一个 Kotlin 类的运行时引用. 要得到一个静态的已知的 Kotlin 类的引用, 可以使用"类字面值(class literal) 语法(:😃":
val c = MyClass::class
类引用是一个 KClass 类型的值
- 在Kotlin中定义了系列的常量,来表示类的信息.
- simpleName: String? 类的名称
- qualifiedName: String? 类的全称,包括包名
- members: Collection
请注意,Kotlin 类引用与 Java 类引用不同。要获得 Java 类引用, 请在 KClass 实例上使用 .java 属性。
- 绑定的类引用(自 1.1 起)
- 通过使用对象作为接收者,可以用相同的 ::class 语法获取指定对象的类的引用:
val widget: Widget = ……
assert(widget is GoodWidget) { "Bad widget: ${widget::class.qualifiedName}" }
你可以获取对象的精确类的引用,例如 GoodWidget 或 BadWidget,尽管接收者表达式的类型是 Widget
2.4.6.2 函数引用
使用 :: 操作符来实现函数的引用
- 高级函数中,我们通常使用函数作为参数,在传递函数参数时通常都是用的函数引用
//当我们有一个命名函数声明如下:
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 <A, B, C> 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]"
2.4.6.3 属性引用
在Kotlin中, 对于包级别的属性可以作为对象来访问, 方法是使用 :: 操作符,我们可以获取一个类型为 KProperty对象。
var x = 1
fun main(args: Array<String>) {
println(::x.get()) // 打印结果为: "1"
::x.set(2)
println(x) // 打印结果为: "2"
}
- 对于val属性,我们可以通过KProperty的get()函数可以得到属性值, 通过它的 name 属性可以得到属性名称.
- 对于var属性,返回的属性对象的类型为 KMutableProperty, 我们不仅可以通过get()和name获取该对象的属性值和属性名称,还可以通过set()函数设置其属性值。
- 对于访问类的成员属性, 我们需要使用限定符。返回的属性对象的类型为KProperty1
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"
}
- 与 Java 反射的互操作性
- 在Java平台上,标准库包含反射类的扩展,它提供了与 Java 反射对象之间映射(参见 kotlin.reflect.jvm 包)
例如,要查找一个用作 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<Any> = o.javaClass.kotlin
2.4.6.4 构造器引用
构造器引用可以用于使用函数类型对象的地方, 但这个函数类型接受的参数应该与构造器相同, 返回值应该是构造器所属类的对象实例. 引用构造器使用 :: 操作符, 再加上类名称.
class Foo
fun function(factory: () -> Foo) {
val x: Foo = factory()
}
//调用了
function(::Foo)
2.4.6.5 绑定的函数与属性引用(自 1.1 起)
你可以引用特定对象的实例方法:
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 是等价的。
2.4.7 类型别名
类型别名为现有类型提供替代名称。 如果类型名称太长,你可以另外引入较短的名称,并使用新的名称替代原类型名。
类型别名不会引入新类型。 它们等效于相应的底层类型,它有助于缩短较长的泛型类型
- 通常缩减集合类型是很有吸引力的:
typealias NodeSet = Set<Network.Node>
typealias FileTable<K> = MutableMap<K, MutableList<File>>
- 你可以为函数类型提供另外的别名:
typealias MyHandler = (Int, String, Any) -> Unit
typealias Predicate<T> = (T) -> Boolean
- 可以为内部类和嵌套类创建新名称:
class A {
inner class Inner
}
class B {
inner class Inner
}
typealias AInner = A.Inner
typealias BInner = B.Inner
2.4.8 操作符重载
Kotlin 允许我们为自己的类型提供预定义的一组操作符的实现。这些操作符具有固定的符号表示 (如 + 或 *)和固定的优先级。为实现这样的操作符,我们为相应的类型(即二元操作符左侧的类型和一元操作符的参数类型)提供了一个固定名字的成员函数或扩展函数。 重载操作符的函数需要用 operator 修饰符标记。
另外,我们描述为不同操作符规范操作符重载的约定
2.4.8.1 一元操作
- 一元前缀操作符
表达式 | 翻译为 |
---|---|
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
这个表是说,当编译器处理例如表达式 +a 时,它执行以下步骤:
- 确定 a 的类型,令其为 T;
- 为接收者 T 查找一个带有 operator 修饰符的无参函数 unaryPlus(),即成员函数或扩展函数;
- 如果函数不存在或不明确,则导致编译错误;
- 如果函数存在且其返回类型为 R,那就表达式 +a 具有类型 R;
注意 这些操作以及所有其他操作都针对基本类型做了优化,不会为它们引入函数调用的开销。
以下是如何重载一元减运算符的示例:
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 的类型,令其为 T;
- 查找一个适用于类型为 T 的接收者的、带有 operator 修饰符的无参数函数 inc();
- 检查函数的返回类型是 T 的子类型。
计算表达式的步骤是:
- 把 a 的初始值存储到临时存储 a0 中;
- 把 a.inc() 结果赋值给 a;
- 把 a0 作为表达式的结果返回。
- 对于 a–,步骤是完全类似的。
对于前缀形式 ++a 和 --a 以相同方式解析,其步骤是:
- 把 a.inc() 结果赋值给 a;
- 把 a 的新值作为表达式结果返回
2.4.8.2 二元操作
三、语言基础
表达式是有返回值的;语句只是一条命令
3.1 基本类型
和Java一样,Kotlin也是基于JVM的,不同的是,后者是静态类型语言,意味着所有变量和表达式类型在编译时已确定。在Java中,通过装箱和拆箱在基本数据类型和包装类型之间相互转换,而,Kotlin中,所有变量的成员方法和属性都是对象
在 Kotlin 中,所有东西都是对象,在这个意义上讲我们可以在任何变量上调用成员函数和属性。
一些类型可以有特殊的内部表示——例如,数字、字符和布尔值可以在运行时表示为原生类型值(int|String|boolean),但是对于用户来说,它们看起来就像普通的类
3.1.0 值类型和引用类型的分析
原始类型:语言自有的基础数据类型,从内存操作方式上可以分为 值类型(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 这个概念)!!!
3.1.1 数字
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
3.1.1.1 字面值
注意: 不支持八进制
数值常量字面值有以下几种:
- 二进制: 0b00001011
- 十进制: 123
- Long 类型用大写 L 标记: 123L
- 十六进制: 0x0F
Kotlin 同样支持浮点数的常规表示方法:
- 默认 double:123.5、123.5e10
- Float 用 f 或者 F 标记: 123.5f
数字字面值支持下划线(自 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
3.1.1.2 装箱与拆箱
在Kotlin中,存在数字的装箱,但是不存在拆箱。因为Kotlin是没有基本数据类型的,Kotlin是万般皆对象的原则。故不存在和Java中的类似int是数据类型,Integer是整型的引用类型
在Kotlin中要实现装箱操作。首先要了解可空引用。即类似Int?(只限数值类型)这样的
val numValue: Int = 123
//装箱的过程,其实装箱之后其值是没有变化的
val numValueBox: Int? = numValue
println("装箱后: numValueBox => $numValueBox")
//输出结果为:
装箱后: numValueBox => 123
判断两个数值是否相等(),判断两个数值在内存中的地址是否相等(=),其实上面的装箱操作之后其内存中的地址根据其数据类型的数值范围而定
””与”=”是不同的,一个是判断值是否相等,一个是判断值及类型是否完全相等
- ==
结构相等由 ==(以及其否定形式 !=)操作判断
- 如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等
- 如果作用于引用类型的变量,则比较的是所指向的对象的地址
- ===
引用相等由 =(以及其否定形式 !)操作判断。a === b 当且仅当 a 和 b 指向同一个对象时求值为 true
- 对于基本数据类型,如果类型不同,其结果就是不等。如果同类型相比,与“==”一致,直接比较其存储的 “值”是否相等;
- 对于引用类型,与“==”一致,比较的是所指向的对象的地址
- equals
- equals方法不能作用于基本数据类型的变量
- 如果没有对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”
当需要可空引用时,像数字、字符会被装箱。装箱操作不会保留同一性
3.1.1.3 不支持隐式拓宽
较小的类型不能隐式转换为较大的类型,这意味着在不进行显式转换的情况下我们不能把 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
每个数字类型支持如下的转换:
- toByte(): Byte
- toShort(): Short
- toInt(): Int
- toLong(): Long
- toFloat(): Float
- toDouble(): Double
- toChar(): Char
缺乏隐式类型转换并不显著,因为类型会从上下文推断出来,而算术运算会有重载做适当转换,例如:
// 30L + 12 -> Long + Int => Long
val num = 30L + 12
print(num)
输出结果为:42