Kotlin 基础入门
函数类型
在 Kotlin 中 函数是一等公民。
// 定义
fun greetPeople(name: String, makeGreet: (String) -> Unit)
// 调用
greetPeople("Jimy", :: greetingWithChinese)
在上面的定义中,markGreet 是个什么?是个函数类型对象的引用,只有对象才能被作为函数的参数。也就是说,我们需要把一个函数类型的一个具体 “对象” 当做函数参数传递。
这个 makeGreet 被称之为函数引用,Function Reference,这也是 Kotlin 官方的说法。
这也说明,在 Kotlin 中,函数也是一个对象。
函数引用
在 Kotlin 里,一个函数名的左边加上双冒号,它就不表示这个函数本身了,而表示一个对象,或者说一个指向对象的引用,但,这个对象可不是函数本身,而是一个和这个函数具有相同功能的对象。
// 这是函数的定义
fun greetingWithChinese(name: String) {
println("早上好, $name !")
}
:: greetingWithChinese // 表示函数对象
既然它表示一个函数的对象,那么,我们也可以把它赋值给一个变量。
val a = :: greetingWithChinese
val b = a
所以,我们也可以把这个变量作为函数参数来调用
greetPeople("Jimy", a)
greetPeople("Jimy", b)
甚至,我们还可以直接通过这个引用调用 invoke()
来执行这个函数对象。
(:: greetingWithChinese).invoke("老王")
a.invoke("Jimy")
b("Lucy") // 还可以这样调用,这个是 Kotlin 的语法糖,实际上调用的 invoke 方法
我们可以对一个函数对象调用 invoke()
方法, 但是,不能对一个函数调用 invoke()
方法
greetingWithChinese("Jimy") // 这个编译报错
高阶函数
事实上,函数不仅可以当做一个函数的参数,还能作为函数的返回值,
定义:一个函数如果参数类型是函数或者返回值类型是函数,那么这就是一个高阶函数。
换个角度看函数类型。
fun greetingWithChinese(name: String) {
println("早上好, $name !")
}
如果将这个函数的参数类型和返回值类型,抽象出来,它的函数类型就是 (String) -> Unit
,
那么,我们就可以直接声明一个变量,指定这个变量的函数类型是它,
val a = (String) -> Unit -> Int = :: greetingWithChinese
注意
前面说了,可以把这个变量,当做参数传递给另一个函数。那么思考一下,有没有一种可能:我把这个函数本身直接挪过来作为参数使用呢?什么意思?
// 这个是之前说的调用方式
greetPeople("Jimy", a)
我能不能向下面这么写?
greetPeople(
"Lucy",
fun greetingWithChinese(name:String) {
println("早上好, $name !")
}
)
如图:
咦,编辑器报错了,Anonymous functions with names are prohibited
禁止使用带有名称的匿名函数。提示我把函数名称移除。
greetPeople(
"Lucy",
fun(name: String) {
println("早上好, $name !")
}
)
这样就可以了。这也很好理解,函数名称是给其他地方调用的,当前这种情况下这个函数作为参数,并不会在其它地方调用,这个函数名称也就没有什么作用,不允许有函数名称,这种写法叫做匿名函数。
而匿名函数实质上是一个表达式。
【】【】图【】【】!!!
既然是表达式,那也就可以赋值给一个变量:
// 把匿名函数赋值给变量 c
val c = fun(name: String) {
println("早上好, $name !")
}
// 将 c 作为参数传递,调用 greetPeople
greetPeople("Lucy", c)
那又有人会问了,能否把一个常规有名字的函数赋值给一个变量。比如
// 编译报错
val d = fun greetingWithChinese(name: String) {
println("早上好, $name !")
}
这是不允许的。
讲道理,我都要把它赋值给一个变量了,那么它本身的名字完全没有必要。
有名字的函数不能赋值给一个变量,但是前面提到了,有名字的函数的引用,用双冒号的形式,则可以。
只有对象才可以赋值给一个变量,仔细想一想,匿名函数,其实不是函数,是一个对象。它是一个表达式,本质上是一个与它形式相同(参数,返回值一致)的函数类型的具体对象。
它和一个函数前面加上双冒号是一样的。
Lambda 表达式
对于一个函数类型的变量,我们除了可以把一个函数对象或者一个匿名函数赋值给它,还可以赋值一个 lambda 表达式。
什么是 lambda 表达式?
// 匿名函数
val c = fun(name: String) {
println("早上好, $name !")
}
// 等价于下面的 lambda 表达式
{ name: String -> Unit
println("早上好, $name !")
}
一个完整的 Lambda 表达式声明如下:
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
由于 Kotlin 支持类型推导,所以它可以简化为
val sum: (Int, Int) -> Int = { x, y -> x + y }
或者:
val sum = { x: Int, y: Int -> x + y }
这个 lambda 表达式,也是一个函数类型的具体对象。所以也可以当做另一个函数的参数传入。
回到前面的例子,调用 greetPeople()
就可以写为
greetPeople(
"Lucy",
{ name: String -> Unit
println("早上好, $name !")
})
而 Kotlin 中的函数,如果最后一个参数是 lambda 表达式,则可以将 lambda 表达式写到参数外面:
greetPeople("Lucy") { name: String -> Unit
println("早上好, $name !")
}
就是这么个形式,还有,lambda 表达式支持类型推导,前面定义了函数
fun greetPeople(name: String, makeGreet: (String) -> Unit)
从这个函数定义处,已经知道,调用时 lambda 函数表达式应该是什么样的函数类型,它的参数,返回值类型都确定了,所以可以在 lambda 表达式中省略,如下:
greetPeople("Lucy") { name ->
println("早上好, $name !")
}
再来看 Android 中常用的一个例子, 给 View 设置点击事件:
public interface OnClickListener {
void onClick(View v);
}
public void setOnClickListener(OnClickListener listener) {
this.listener = listener;
}
java 中调用:
view.setOnClickListener(new OnClickListener() {
@Override
void onClick(View v) {
doSomething();
}
});
到 Kotlin 中就可以这么写:
fun setOnclickListener(onClick: (View) -> Unit) {
this.onClick = onClick
}
view.setOnclickListener({view: View -> Unit
doSomething()
})
将 lambda 表达式移到括号外面:
view.setOnclickListener() {view: View -> Unit
doSomething()
}
参数和返回值类型可推导,省略参数和返回值类型:
view.setOnclickListener() {view ->
doSomething()
}
如果 lambda 表达式是唯一参数,则括号都可以直接省略,同时 lambda 表达式如果是单参数,则这个参数也可以直接省略不写。因为对于 Kotlin 的 Lambda,单参数有一个默认的名称:it
, 使用的时候用 it
替代。
view.setOnclickListener() {
doSomething()
}
所以就变成我们经常看到的样子了。
对于多参数的 lambda 表达式,我们不能省略掉参数,但是如果参数没有被用到,可以用 _
来代替。
所以 Kotlin lambda 表达式本质是什么呢?
其实和匿名函数一样,本质上也是一个函数类型的具体对象。塔可以作为函数的参数传入,也可以赋值给一个变量。
另外 lambda 表达式也可以进行自调用
{x: Int, y: Int -> x + y}(1, 2)
总结一下
lambda 表达式语法:
1. lambda 表达式必须通过 {} 来包裹
2. 如果 lambda 声明了参数部分的类型,且返回值支持类型推导,则 lambda 表达式变量就可以省略函数类型声明
3. 如果 lambda 变量声明了函数类型,那么 lambda 表达式的参数部分的类型就可以省略
4. 如果 lambda 表达式返回的不是 Unit 类型,则默认最后一行表达式的值类型就是返回值类型。
lambda 表达式和函数的区别
- fun 在没有等号、只有花括号的情况下,就是代码块函数体,如果返回值非 Unit,必须带 return
fun foo(x: Int) {
print(x)
}
fun foo(x: Int, y: Int): Int {
return x + y
}
- fun 带有等号,没有花括号,是单表达式函数体,可以省略 return
fun foo(x: Int, y: Int) = x + y
- 不管是用 val 还是 fun 声明,如果是等号加花括号的语法,就是声明一个 lambda 表达式。
val foo = { x: Int, y: Int ->
x + y
}
// 调用方式: foo.invoke(1, 2) 或者 foo(1, 2)
fun foo(x: Int) = { y: Int ->
x + y
}
// 调用方式: foo(1).invoke(2) 或者 foo(1)(2)
类、接口与对象
类
Kotlin 中用 'class' 声明类,如果没有实体类,可以省略 {}
Kotlin 多个类可以声明在同一个文件中
构造函数
在 Kotlin 中的一个类可以有一个主构造函数以及一个或多个次构造函数。主构造函数是类头的一部分:它跟在类名(与可选的类型参数)后。
class Student constructor(id: Int, name: String, age: Int) {
}
如果主构造函数没有任何注解或者可见性修饰符,可以省略这个 constructor 关键字。
class Student(id: Int, name: String, age: Int) {
constructor(name: String, age: Int): this(0, name, age) // 次构造函数
}
init 代码块
类中允许存在多个 init
代码块,对于不同的业务初始化可以放在不同的 init 代码块中,按代码顺序一次执行。
类似于 java 类中的静态代码块。
init 代码块中可以直接访问构造函数的参数,其它地方不可以。
成员变量
声明
- 和 java 类似,可以在类代码块中直接声明。
- 构造方法参数加上
var
或者val
修饰,则可以视为类中声明了一个同名的成员变量。也可以加上可见性修饰符。
初始化
成员变量声明时需进行初始化,或者显式声明延迟初始化,使用关键字 lateinit
显示声明延迟初始化。
class Student(id: Int, var name: String, var age: Int) {
constructor(name: String, age: Int): this(0, name, age)
init {
println("student name is $name")
}
fun printInfo() {
age += 1
}
lateinit var gender: String // 延迟初始化
}
继承
使用 :
open class Person(id: Int, name: String)
class Student(id: Int, var name: String, var age: Int): Person(id,name) {
// ...
}
被继承的类需要用 open
修改,同样父类的中法,默认是不可被重写的,若允许被重写,需要使用 open
修饰。
接口
interface MyInterface {
fun bar()
val propertyWithImplementation: String
get() = "foo"
fun foo() {
print(prop)
}
}
接口允许有属性,接口中的方法允许有默认实现,接口不需要用 open
修饰,因为接口就是用来让子类实现,然后被被子类重写的。包括属性也是可以被重写的。接口可以继承自其它接口。
内部类
java 中在类中再声明类,称之为内部类,如果使用 static 修饰,则为静态内部类。
class A {
private String a
class B {
}
static class C {
}
}
请问 B 和 C 中能访问 A 的成员 a 吗?
在 Kotlin 中没有 static 关键字,默认声明就是静态内部类,如要声明内部类,需要添加 inner
关键字
class A {
// 声明内部类需要使用 inner 关键字
inner class B {
}
// 静态内部类
class c {
}
}
伴生对象
Kotlin 中没有了 static 关键字,但是有伴生对象的概念,类似于 java 中的 static
class Student {
companion object {
const val TAG = "Student" // 常量
var count = 0 // 静态变量
// 静态方法
fun test() {
}
}
}
object
是在 Kotlin 中的用法。
- 在类中与
companion
一起,表示伴生对象。 - 声明类。Kotlin 中除了用 class 声明类意外,还可以用 object 来声明一个类,用 object 声明的类,天生单例。
object SystemUtils {
...
}
- 实现某个接口,或者抽象类:
interface ICount {
fun count(num: Int): Int
}
val myCount = object: ICount {
override fun count(num: Int): Int {
TODO("Not yet implemented")
}
}
可见性修饰符
- public 公开,可见性最大,哪里都可以引用
- private 私有,可见性最小,根据声明位置可以分为类中可见和文件中可见。
- protected 保护, 相当于 private + 子类可见。
- internal 内部,同一 module 可见
java 中 protected 表示包内可见 + 子类可见
Kotlin protected 的可见范围收窄了,原因是 相比于包, Kotlin 更注重 module。
另外 private 修饰 java 中的内部类对外部类可见,Kotlin 中的内部类对外部类不可见。
Kotlin 中的类和方法,不写时,默认是 public + final 的。
数据类
data class Student(val name: String, var age: Int = 18)
- 自动生成
getter()/setter()
方法 !!!??? - 编译器自动生成
equals()/hashCode()
方法 - 编译器自动生成
toString()
方法 - 编译器自动生成 `componentN()`` 函数(解构)
- 编译器自动生成
copy
函数
data class Student(val name: String, var age: Int = 18) {
var isBoy = true // 该属性不会在生成的方法中
}
密封类
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
可以用来代替枚举,使用 When 表达式时如果能覆盖所有,则无需 else 分支。
fun eval(expr: Expr): Double = when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
// 不再需要 `else` 子句,因为我们已经覆盖了所有的情况
}
其它方便的特性:
函数参数默认值
fun sayHi(name: String = "world") = println("Hello " + name)
重载函数再也不用写很多个了。
本地函数(嵌套函数)
本地函数是个啥玩意?
我们知道在函数中可以声明局部变量(这不是废话吗?)在 Kotlin 中我们甚至还可以在方法中类似声明局部变量一样声明一个方法,称之为本地函数。
fun login(user: String, password: String, illegalStr: String) {
// 验证 user 是否为空
if (user.isEmpty()) {
throw IllegalArgumentException(illegalStr)
}
// 验证 password 是否为空
if (password.isEmpty()) {
throw IllegalArgumentException(illegalStr)
}
// 执行登录
}
校验参数这不部分代码有些冗余,我们可以抽成一个方法,但是我们又没有必要暴露到其它地方,因为只有这个 login 方法会用到,则可以在 login 方法内部声明一个嵌套函数
fun login(user: String, password: String, illegalStr: String) {
fun validate(value: String, illegalStr: String) {
if (value.isEmpty()) {
throw IllegalArgumentException(illegalStr)
}
}
validate(user, illegalStr)
validate(password, illegalStr)
// 执行登录
}
try-catch 表达式
Kotlin 中 try-catch 语句也可以是一个表达式,允许代码块的最后一行作为返回值
val a: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null }
其它知识:
数组、集合(可变集合、不可变集合)、泛型(逆变和协变)、枚举、委托、注解、反射
内联函数/中缀函数
== 和 ===
Kotlin 中用 ==
判断 equals 相等,对象内容相等
===
判断相等引用的内存地址相等
拓展函数和拓展熟悉
顾名思义,可以给一个类“增加”额外的方法和属性。
举个例子
data class Student(val name: String)
这个类本身没有“上课”这个方法,我现在给它增加一个拓展函数。
fun Student.attendClass() {
println("${this.name} 正在上课")
}
这样,Student 这个类的实例对象就可以调用这个拓展方法了。
val student = Student("Jimy")
student.attendClass()
拓展方法直接定义在文件中。
拓展属性类似。
想一想:三方库里面的类,不能直接修改,是不是可以增加拓展方法和拓展属性了?有没有很激动。
Top-level
kotlin 文件已 .kt
为后缀,在 Kotlin 文件中,我们可以直接声明的变量,方法,也就是不在 class 内部声明,称之为 top-level declaration 顶层声明。
// 属于 package,不在 class/object 内
const val KEY = "123456"
fun test() {
println("---test---")
}
它不属于任何 class , 而是直接属于 package, 它和静态变量一样是全局的,使用起来更方便,调用它的时候连类名都不用写:
import com.sharpcj.demo.test
import com.sharpcj.demo.KEY
test()
val x = KEY
结合前面说的,写工具类一般就有三种方式:
// companion object
class Util {
companion object {
fun test1() {
}
fun test2() {
}
}
}
// object
object Util {
fun test1() {
}
fun test2() {
}
}
// top level 方法
fun test1() {
}
fun test2() {
}
建议:
- 如果想写工具类的功能,直接创建文件,写 top-level「顶层」函数。虽然没有 class 的概念,但是相关的方法,写在同一个文件中,方便阅读管理。
- 如果需要继承别的类或者实现接口,就用
object
或companion object
。
by lazy (委托)
前面提到,如果一个属性需要延迟初始化,可以使用 lateinit
进行修饰,另外还有另外一种方式,就是使用 by lazy
方法初始化。
在给一个变量赋值的时候使用 by lazy
代码块,可以做到单例,在第一次使用时初始化。by lazy 实际上使用了 Kotlin 的委托特性,底层原理和 DCL 的单例模式类似。
val student: Student by lazy {
Student(name = "Lucy", age = 18)
}
这里顺便说一下用 Kotlin 实现单例,看看有多有多方便了。
前面提到,用 object 声明一个类,即是单例的。
object Singleton
再看看 DCL 的方式,java 实现:
public class Singleton {
private Singleton(){}
private volatile static Singleton instance;
private static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
再看看 Kotlin 实现
class Singleton private constructor() {
companion object {
val instance: Singleton by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
Singleton()
}
}
}
这样是不是很方便,当然上述方式也有一个缺点,就是获取单例的时候没有办法传参,那么改为和 java 一样的方式看看:
class Singleton private constructor() {
companion object {
@Volatile
private var instance: Singleton? = null
fun getInstance(): Singleton {
return instance?: synchronized(this) {
instance?:Singleton().also {
// 初始化工作
instance = it
}
}
}
}
}
这次可以传参进来了,代码依然简洁很多。
其它:
数组、集合(可变集合和不可变集合,集合操作符)、泛型(协变和逆变)、注解、反射。
委托、内联函数、中缀函数、携程等等。