哥哥带你学Kotlin
声明,这里是我平时日常的笔记Zone,所以记录可能会偏向于我认为的重点区域,会有些疏漏或者缺失的地方,或是排版或者文案有些凌乱,但我既然发布出来了,我会尽全力去完善,各位看官大家一起努力,愿意听到各位的批评指正,共同进步……有问题可联系微信dk2582525775,期待骚扰……
……官方教程……进阶教程……在线工具……
Kotlin 优点Kotlin程序文件以.kt 结尾,比如 hello.kt 、app.kt
1,安装Kotlin插件
2,创建新工程
3,将Java代码转换为Kotlin
4,工程中配置Kotlin
Kotlin 基础语法
1,包声明
kotlin源文件不需要相匹配的目录和包,源文件可以放在任何文件目录。如果没有指定包,会导入默认default包。
2,默认导入
有多个包会默认导入到每个Kotlin文件中:
3,函数定义
1,使用关键字fun,参数格式为:参数:类型
//Int 是参数 返回值也为Int public 方法需明确写出返回类型
public fun sum(a:Int ,b :Int) :Int{
return a+b;
}
2,无返回值函数(类似Java中的void)
fun printsum(a:Int):unit{ //如果返回值是unit类型,可以省略,对于public也适用
print(a+b);
}
3,可变长参数函数
fun vars(vararg v:Int){
for(vt int v){
print(vt);
}
}
4,lambda匿名函数
val sumLambda:(Int ,Int) -> Int - {x ,y -> x+y}
print(sumLambda(1,2));
4,定义常量与变量
1,可变变量定义:var
2,不可变变量:val
val a :Int = 1 ; //如果不在声明时初始化必须提供变量类型
5,注释
1,单行注释
2,多行注释
6,字符串模板
$ 表示一个变量名或者变量值
$ {},$ a
7,Null检查机制
Kotlin的空安全设计,有两种处理方式,字段后加 !!像Java一样跑出空异常,另一种字段后加 ?可不做处理返回值为null 或配合?:做空判断处理
var age : String? = '23' //类型后面加?表示可以为空
val ages = age!!.toInt(); //抛出空异常
val ages1 = age?.toInt(); //不做处理返回Null
val ages2 = age?.toInt()?:-1; //age 为空返回 -1
当一个引用可能为null时,对应的类型声明必须明确的标记为可为null
8,类型检测及自动类型转换
我们可以用is运算符检测一个表达式是否某类型的一个实例(类似于Java中的instanceof关键字)
if (obj is String ){ } //做过类型判断以后,obj会被系统自动转换为string类型
if(obj !is String){ } //在这里还有一种用法,!is
9,区间
区间表达式由具有操作符形式的 。。的rangeTo函数辅助 in !in 形成。区间是为任何可比较类型定义的,但对于整行原生类型,它有一个优化的实现。
for ( i in 1..4) //输出1234
for(i in 1..10 step 2){} //输出13578 //使用step制定步长
for(i in 4 downTo 1 step 3){} //输出 42
for(i in 1 unitl 10){}//使用util 函数排除结束元素
10,Kotlin基本数据类型
不同于Java,字符不属于数值类型,是一个独立的数据类型
11,比较两个数字
Kotlin中没有基础数据类型,只有封装的数字类型,每定义一个变量,其实Kotlin都帮你封装了一个对象,这样就可以保证不会出现空指针。数字类型也一样,所以在比较两个数字的时候,就有比较数据大小和比较两个对象是否相同的区别了。
在Kotlin中,三个等号===表示比较对象的地址,两个==表示比较两个值的大小
//经过了装箱,创建了两个不同的对象
val boxedA: Int? = a val anotherBoxedA: Int? = a
//虽然经过了装箱,但是值是相等的,都是10000
println(boxedA === anotherBoxedA) // false,值相等,对象地址不一样
println(boxedA == anotherBoxedA) // true,值相等
12,类型转换
由于不同的表示方式,较小类型并不是较大类型的子类型,较小类型不能隐式的转换为较大类型,这意味着在不进行显示转换的情况下我们不能把Byte型值赋给一个Int变量。
可以用toInt()的方法。toByte(): BytetoShort(): ShorttoInt(): InttoLong(): LongtoFloat(): FloattoDouble(): DoubletoChar(): Char
13,位操作符
shl(bits) – 左移位 (Java’s <<)shr(bits) – 右移位 (Java’s >>)ushr(bits) – 无符号右移位 (Java’s >>>)and(bits) – 与or(bits) – 或xor(bits) – 异或inv() – 反向
14,字符
和Java不一样,Kotlin中Char不能直接和数字操作,Char必须是单引号包含起来的。
特殊字符可以用反斜杠转义。 支持这几个转义序列:\t、 \b、\n、\r、\'、\"、\\ 和 \$。 编码其他字符要用 Unicode 转义序列语法:'\uFF00'。
15,数组
数组用类Array实现,数组闯将的两种方式,一种是使用函数arrayOf(),另外一种是使用工厂函数。
val a = arrayOf(1,2,3)
val b = Array(3,{i -》(i*2)})
[] 运算符代表调用成员函数 get() 和 set()。
注意: 与 Java 不同的是,Kotlin 中数组是不型变的(invariant)。
除了类Array,还有ByteArray, ShortArray, IntArray,用来表示各个类型的数组,省去了装箱操作,因此效率更高,其用法同Array一样:
16,字符串
和Java一样,string是不可变的,方括号【】语法可以方便的获取字符串中的某个字符,也可以通过for循环类遍历
支持“””三引号的多行字符
支持通过trimMargin()方法来删除多余的空白
默认用 | 坐钱边界,也可以选择其他字符作为参数传入
17,字符串模板
原生字符串和转义字符串内部都支持模板。 如果你需要在原生字符串中表示字面值 $ 字符(它不支持反斜杠转义)可以使用${'$'}9.99
18,条件控制
我们可以把if表达式结果赋值给一个变量
val max = if (a > b){}
val c = if (conditon) a else b
19,When表达式
如果很多分支需要用相同的方式处理,则可以把多个分支条件放在一起,用逗号分隔
when (x){
1,2 -> prinit()
else ->{}
我们也可以检测一个值在(in)或者不在(!in)一个区间或者集合中:
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
}
when 也可以用来取代 if-else if链。 如果不提供参数,所有的分支条件都是简单的布尔表达式,而当一个分支的条件为真时则执行该分支:
when {
x.isOdd() -> print("x is odd")
x.isEven() -> print("x is even")
else -> print("x is funny")
}
20,Kotlin循环控制
循环体可以是一个代码块
for(item:Int in ints){}
for(i in array.indices){}
注意这种"在区间上遍历"会编译成优化的实现而不会创建额外对象。或者你可以用库函数 withIndex:
for ((index, value) in array.withIndex())
println("the element at $index is $value")
}
在kotlin中任何表达式都可以用标签来标记,标签的格式为标识符后跟@符号,我们可以用标签限制break和continue
Kotlin有函数字面量,局部函数和对象表达式,因此函数可以被嵌套。便签限制return允许我们从外层函数返回,最重要的一个用途就是从lambda表达式中返回。
回想一下我们这么写的时候:
fun foo() { ints.forEach { if (it == 0) return print(it) }}
这个 return 表达式从最直接包围它的函数即 foo 中返回。 (注意,这种非局部的返回只支持传给内联函数的 lambda 表达式。) 如果我们需要从 lambda 表达式中返回,我们必须给它加标签并用以限制 return。
fun foo() { ints.forEach lit@ { if (it == 0) return@lit print(it) }}
现在,它只会从 lambda 表达式中返回。通常情况下使用隐式标签更方便。 该标签与接受该 lambda 的函数同名。
fun foo() { ints.forEach { if (it == 0) return@forEach print(it) }}
或者,我们用一个匿名函数替代 lambda 表达式。 匿名函数内部的 return 语句将从该匿名函数自身返回
fun foo() { ints.forEach(fun(value: Int) { if (value == 0) return print(value) })}
21,Kotlin类和对象
Kotlin 类可以包含:构造函数和初始化代码块、函数、属性、内部类、对象声明。
// Kotlin 中没有 new 关键字
Koltin 中的类可以有一个 主构造器,以及一个或多个次构造器,主构造器是类头部的一部分,位于类名称之后:
如果主构造器没有任何注解,也没有任何可见度修饰符,那么constructor关键字可以省略。
getter 和 setter 都是可选如果属性类型可以从初始化语句或者类的成员函数中推断出来,那就可以省去类型,val不允许设置setter函数,因为它是只读的。
Kotlin 中类不能有字段。提供了 Backing Fields(后端变量) 机制,备用字段使用field关键字声明,field 关键词只能用于属性的访问器
非空属性必须在定义的时候初始化,kotlin提供了一种可以延迟初始化的方案,使用 lateinit 关键字描述属性:
lateinit var subject: TestSubject
主构造器中不能包含任何代码,初始化代码可以放在初始化代码段中,初始化代码段使用 init 关键字作为前缀。
注意:主构造器的参数可以在初始化代码段中使用,也可以在类主体n定义的属性初始化代码中使用。 一种简洁语法,可以通过主构造器来定义属性并初始化属性值(可以是var或val):
类也可以有二级构造函数,需要加前缀 constructor:
class Person { constructor(parent: Person) { parent.children.add(this) }}
如果类有主构造函数,每个次构造函数都要,或直接或间接通过另一个次构造函数代理主构造函数。在同一个类中代理另一个构造函数使用 this 关键字
class Person(val name: String) { constructor (name: String, age:Int) : this(name) { // 初始化... }}
如果一个非抽象类没有声明构造函数(主构造函数或次构造函数),它会产生一个没有参数的构造函数。构造函数是 public 。如果你不想你的类有公共的构造函数,你就得声明一个空的主构造函数:
class DontCreateMe private constructor () {}
注意:在 JVM 虚拟机中,如果主构造函数的所有参数都有默认值,编译器会生成一个附加的无参的构造函数,这个构造函数会直接使用默认值。这使得 Kotlin 可以更简单的使用像 Jackson 或者 JPA 这样使用无参构造函数来创建类实例的库。
class Customer(val customerName: String = "")
22,抽象类
抽象是面向对象编程的特征之一,类本身,或类中的部分成员,都可以声明为abstract的。抽象成员在类中不存在具体的实现。
注意:无需对抽象类或抽象成员标注open注解。
23,嵌套类
class Outer {
// 外部类 private val bar: Int = 1
class Nested {
// 嵌套类
fun foo() = 2
}
}// 调用格式:外部类.嵌套类.嵌套类方法/属性
24,内部类
内部类使用 inner 关键字来表示。
内部类会带有一个对外部类的对象的引用,所以内部类可以访问外部类成员属性和成员函数。
class Outer {
private val bar: Int = 1
/**嵌套内部类**/ inner class Inner {
fun innerTest() {
var o = this@Outer //获取外部类的成员变量
25,匿名内部类
26,类的修饰符
27,Kotlin继承
Kotlin中所有类都继承该Any类,它是所有类的超类,对于没有超类型声明的类是默认超类。
any默认提供了三个函数
equals
hashcode
tostring
注意:Any 不是 java.lang.Object。如果一个类要被继承,可以使用 open 关键字进行修饰
构造函数
子类有构造函数
如果子类有主构造函数, 则基类必须在主构造函数中立即初始化。
如果子类没有主构造函数,则必须在每一个二级构造函数中用 super 关键字初始化基类,或者在代理另一个构造函数。初始化基类时,可以调用基类的不同构造方法。
28,重写
在基类中,使用fun声明函数时,此函数默认为final修饰,不能被子类重写。如果允许子类重写该函数,那么就要手动添加 open 修饰它, 子类重写方法使用 override 关键词:
如果有多个相同的方法(继承或者实现自其他类,如A、B类),则必须要重写该方法,使用super范型去选择性地调用父类的实现。
属性重写使用 override 关键字,属性必须具有兼容类型,每一个声明的属性都可以通过初始化程序或者getter方法被重写:
1、子类继承父类时,不能有跟父类同名的变量,除非父类中该变量为 private,或者父类中该变量为 open 并且子类用 override 关键字重写:
2,子类不一定要调用父类和接口中共同拥有的同名的方法
3,关于子类不能用 val 重写父类中的 var,我的猜测是:子类重写父类属性,也就相当于必须重写该属性的 getter 和 setter 方法,而子类中的 val 不能有 setter 方法,所以无法“覆盖”父类中 var 的 setter 方法,相当于缩小了父类中相应属性的使用范围,是不允许的,就像我们不能把父类中一个 public 方法重写成 private 方法一样。
4、如果一个变量想要在定义的时候被初始化,则该变量必须拥有 backing field 字段,该变量的默认 getter 和 setter 方法中是有定义 field 字段的,但是如果我们重写了这个变量的 getter 方法和 setter 方法,并且在 getter 方法和 setter 方法中都没有出现过 filed 这个关键字,则编译器会报错,提示 Initializer is not allowed here because this property has no backing field,除非显式写出 filed 关键字(哪怕它什么都不干,只要放在那里就可以了,我理解是出现一次就相当于“声明”过了,就可以用了,而在定义变量的时候初始化是要求 field 被“声明”过才可以):
29,Kotlin接口
Kotlin 接口与 Java 8 类似,使用 interface 关键字定义接口,允许方法有默认实现:
接口中的属性只能是抽象的,不允许初始化值,接口不会保存属性值,实现接口时,必须重写属性:
实现多个接口时,可能会遇到同一方法继承多个实现的问题。
实例中接口 A 和 B 都定义了方法 foo() 和 bar(), 两者都实现了 foo(), B 实现了 bar()。因为 C 是一个实现了 A 的具体类,所以必须要重写 bar() 并实现这个抽象方法。然而,如果我们从 A 和 B 派生 D,我们需要实现多个接口继承的所有方法,并指明 D 应该如何实现它们。这一规则 既适用于继承单个实现(bar())的方法也适用于继承多个实现(foo())的方法。
30,Kotlin扩展
Kotlin 可以对一个类的属性和方法进行扩展,且不需要继承或使用 Decorator 模式。
扩展是一种静态行为,对被扩展的类代码本身不会造成任何影响。
扩展函数可以在已有类中添加新的方法,不会对原类做修改,扩展函数定义形式:
扩展函数是静态解析的,并不是接收者类型的虚拟成员,在调用扩展函数时,具体被调用的的是哪一个函数,由调用函数的的对象表达式来决定的,而不是动态的类型决定的
若扩展函数和成员函数一致,则使用该函数时,会优先使用成员函数。
31,扩展空对象
在扩展函数内, 可以通过 this 来判断接收者是否为 NULL,这样,即使接收者为 NULL,也可以调用扩展函数。例如:
扩展属性允许定义在类或者kotlin文件中,不允许定义在函数中。初始化属性因为属性没有后端字段(backing field),所以不允许被初始化,只能由显式提供的 getter/setter 定义。
32,伴生对象扩展
如果一个类定义有一个伴生对象 ,你也可以为伴生对象定义扩展函数和属性。
伴生对象通过"类名."形式调用伴生对象,伴生对象声明的扩展函数,通过用类名限定符来调用:
33,扩展的作用域
34,扩展声明为成员
在一个类内部你可以为另一个类声明扩展。
在这个扩展中,有个多个隐含的接受者,其中扩展方法定义所在类的实例称为分发接受者,而扩展方法的目标类型的实例称为扩展接受者。
在 C 类内,创建了 D 类的扩展。此时,C 被成为分发接受者,而 D 为扩展接受者。从上例中,可以清楚的看到,在扩展函数中,可以调用派发接收者的成员函数。
假如在调用某一个函数,而该函数在分发接受者和扩展接受者均存在,则以扩展接收者优先,要引用分发接收者的成员你可以使用限定的 this 语法。
以成员的形式定义的扩展函数, 可以声明为 open , 而且可以在子类中覆盖. 也就是说, 在这类扩展函数的派 发过程中, 针对分发接受者是虚拟的(virtual), 但针对扩展接受者仍然是静态的。
伴生对象内的成员相当于 Java 中的静态成员,其生命周期伴随类始终,在伴生对象内部可以定义变量和函数,这些变量和函数可以直接用类名引用。
对于伴生对象扩展函数,有两种形式,一种是在类内扩展,一种是在类外扩展,这两种形式扩展后的函数互不影响(甚至名称都可以相同),即使名称相同,它们也完全是两个不同的函数,并且有以下特点:
(1)类内扩展的伴随对象函数和类外扩展的伴随对象可以同名,它们是两个独立的函数,互不影响;
(2)当类内扩展的伴随对象函数和类外扩展的伴随对象同名时,类内的其它函数优先引用类内扩展的伴随对象函数,即对于类内其它成员函数来说,类内扩展屏蔽类外扩展;
(3)类内扩展的伴随对象函数只能被类内的函数引用,不能被类外的函数和伴随对象内的函数引用;
(4)类外扩展的伴随对象函数可以被伴随对象内的函数引用,;
35,Kotlin数据类和密封类
Kotlin 可以创建一个只包含数据的类,关键字为 data:
编译器会自动的从主构造函数中根据所有声明的属性提取以下函数:
equals() / hashCode()
toString() 格式如 "User(name=John, age=42)"
componentN() functions 对应于属性,按声明顺序排列
copy() 函数
如果这些函数在类中已经被明确定义了,或者从超类中继承而来,就不再会生成。
为了保证生成代码的一致性以及有意义,数据类需要满足以下条件:
主构造函数至少包含一个参数。
所有的主构造函数的参数必须标识为val 或者 var ;
数据类不可以声明为 abstract, open, sealed 或者 inner;
数据类不能继承其他类 (但是可以实现接口)。
复制使用 copy() 函数,我们可以使用该函数复制对象并修改部分属性, 对于上文的 User 类,其实现会类似下面这样:
36,数据类以及解构声明
组件函数允许数据类在解构声明中使用:
标准数据类:标准库提供了 Pair 和 Triple 。在大多数情形中,命名数据类是更好的设计选择,因为这样代码可读性更强而且提供了有意义的名字和属性。
37,密封类
密封类用来表示受限的类继承结构:当一个值为有限几种的类型, 而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合 也是受限的,但每个枚举常量只存在一个实例,而密封类 的一个子类可以有可包含状态的多个实例。声明一个密封类,使用 sealed 修饰类,密封类可以有子类,但是所有的子类都必须要内嵌在密封类中。sealed 不能修饰 interface ,abstract class(会报 warning,但是不会出现编译错误)
38,Kotlin泛型
泛型,即 "参数化类型",将类型参数化,可以用在类,接口,方法上。与 Java 一样,Kotlin 也提供泛型,为类型安全提供保证,消除类型强转的烦恼。
声明一个泛型类:
39,泛型约束
我们可以使用泛型约束来设定一个给定参数允许使用的类型。Kotlin 中使用 : 对泛型的的类型上限进行约束。最常见的约束是上界(upper bound):
40,型变
Kotlin 中没有通配符类型,它有两个其他的东西:声明处型变(declaration-site variance)与类型投影(type projections)
声明处型变
声明处的类型变异使用协变注解修饰符:in、out,消费者 in, 生产者 out。
使用 out 使得一个类型参数协变,协变类型参数只能用作输出,可以作为返回值类型但是无法作为入参的类型:
41,星号投射
有些时候, 你可能想表示你并不知道类型参数的任何信息, 但是仍然希望能够安全地使用它. 这里所谓"安全地使用"是指, 对泛型类型定义一个类型投射, 要求这个泛型类型的所有的实体实例, 都是这个投射的子类型。
对于这个问题, Kotlin 提供了一种语法, 称为 星号投射(star-projection):
假如类型定义为 Foo , 其中 T 是一个协变的类型参数, 上界(upper bound)为 TUpper ,Foo<> 等价于 Foo<out TUpper> . 它表示, 当 T 未知时, 你可以安全地从 Foo<> 中 读取TUpper 类型的值.
假如类型定义为 Foo , 其中 T 是一个反向协变的类型参数, Foo<> 等价于 Foo<inNothing> . 它表示, 当 T 未知时, 你不能安全地向 Foo<> 写入 任何东西.
假如类型定义为 Foo<T> , 其中 T 是一个协变的类型参数, 上界(upper bound)为 TUpper , 对于读取值的场合, Foo<*> 等价于 Foo<out TUpper> , 对于写入值的场合, 等价于 Foo<in Nothing> .
如果一个泛型类型中存在多个类型参数, 那么每个类型参数都可以单独的投射. 比如, 如果类型定义为interface Function<in T, out U> , 那么可以出现以下几种星号投射:
Function<*, String> , 代表 Function<in Nothing, String> ;
Function<Int, *> , 代表 Function<Int, out Any?> ;
Function<, > , 代表 Function .
注意: 星号投射与 Java 的原生类型(raw type)非常类似, 但可以安全使用
42,Kotlin枚举类
枚举类最基本的用法是实现一个类型安全的枚举。
枚举常量用逗号分隔,每个枚举常量都是一个对象。
43,kotlin对象表达式和对象声明
Kotlin 用对象表达式和对象声明来实现创建一个对某个类做了轻微改动的类的对象,且不需要去声明一个新的子类。
对象表达式
通过对象表达式实现一个匿名内部类的对象用于方法的参数中:
44,对象声明
Kotlin 使用 object 关键字来声明一个对象。
Kotlin 中我们可以方便的通过对象声明来获得一个单例。
45,伴生对象
类内部的对象声明可以用 companion 关键字标记,这样它就与外部类关联在一起,我们就可以直接通过外部类访问到对象的内部元素。
对象表达式和对象声明之间的语义差异
对象表达式和对象声明之间有一个重要的语义差别:
对象表达式是在使用他们的地方立即执行的
对象声明是在第一次被访问到时延迟初始化的
伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配
46,Kotlin委托
委托模式是软件设计模式中的一项基本技巧。在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。
Kotlin 直接支持委托模式,更加优雅,简洁。Kotlin 通过关键字 by 实现委托。
类的委托即一个类中定义的方法实际是调用另一个类的对象的方法来实现的。以下实例中派生类 Derived 继承了接口 Base 所有方法,并且委托一个传入的 Base 类的对象来执行这些方法。
47,属性委托
属性委托指的是一个类的某个属性值不是在类中直接进行定义,而是将其托付给一个代理类,从而实现对该类的属性统一管理。
属性委托语法格式:
val/var <属性名>: <类型> by <表达式>
var/val:属性类型(可变/只读)
属性名:属性名称
类型:属性的数据类型
表达式:委托代理类
by 关键字之后的表达式就是委托, 属性的 get() 方法(以及set() 方法)将被委托给这个对象的 getValue() 和 setValue() 方法。属性委托不必实现任何接口, 但必须提供 getValue() 函数(对于 var属性,还需要 setValue() 函数)。
定义一个被委托的类该类需要包含 getValue() 方法和 setValue() 方法,且参数 thisRef 为进行委托的类的对象,prop 为进行委托的属性的对象。
48,标准委托
Kotlin 的标准库中已经内置了很多工厂方法来实现属性的委托。
延迟属性 Lazy
lazy() 是一个函数, 接受一个 Lambda 表达式作为参数, 返回一个 Lazy <T> 实例的函数,返回的实例可以作为实现延迟属性的委托: 第一次调用 get() 会执行已传递给 lazy() 的 lamda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。
49,可观察属性Observable
observable 可以用于实现观察者模式。
Delegates.observable() 函数接受两个参数: 第一个是初始化值, 第二个是属性值变化事件的响应器(handler)。
在属性赋值后会执行事件的响应器(handler),它有三个参数:被赋值的属性、旧值和新值:
把属性储存在映射中
一个常见的用例是在一个映射(map)里存储属性的值。 这经常出现在像解析 JSON 或者做其他"动态"事情的应用中。 在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。