前言
2017年谷歌IO大会宣布,将Android
开发的官方语言更换为Kotlin
,作为Android
开发有必要对Kotlin
语言进行了解使用。
Kotlin
语言提供了与java语言100%的互操作性,将现代语言的优势带入Android
开发的过程当中,并引入了内联函数以及lambda
表达式等使得代码更精简,运行效率更高。
对于熟悉java开发的开发人员来说,kotlin
的学习成本会大大降低,并且以及在很多大型互联网公司进入应用阶段。
1. 基础类型
Kotlin
所支持的基础数据类型包括:数字、字符、布尔、数组、字符串。
1.1 数字
与java当中的数字类型相似,kotlin内置的数字类型包括
类型 | 字节 |
---|---|
Double | 64 |
Float | 32 |
Long | 64 |
Int | 32 |
Short | 16 |
Byte | 8 |
字面值常量包括
类型 | 表示 |
---|---|
十进制 | 123 |
二进制 | 0b00001011 |
十六进制 | 0x0F |
对于各个类型之间的转换与java有所不同,在java当中,当小类型向大的类型赋值时会发生隐式转换:
int a = 123;
long b = a;
System.out.println(a==b);//输出true
但是在kotlin当中不存在这种隐式转换,在涉及到这种类型转换的地方,使用显示转换。
val a:Int = 123
val b:Long = a //报错
val b:Long = a.toLong()
1.2 字符
字符类型使用Char
类型标识。
val c:Char = '0'
val str:String = c.toString()//Char类型转换为String str = "0"
val i:Int = c.toInt()//Char类型转换为Int,转换为字符对应Unicode编码值 i=48
1.3 布尔
与java中的布尔类型相似,布尔类型只有两个值true
或者false
,使用Boolean
类型表示:
val b1:Boolean = true
val b2:Boolean = false
1.4 数组
数组类型使用Array
来表示,系统提供了数字类型的get
和set
方法以及size
属性等。创建一个数组的方式有两种:
- 使用
arrayOf
、intArrayOf
、shortArrayOf
函数初始化数组。 - 使用
Array
的构造函数初始化数组。
//方法1 使用`arrayOf`函数初始化数组
val array1 = arrayOf(1, 2, 3)
//遍历数组的方法后续介绍
for(i in array1.indices){
println("array1[$i] = ${array1.get(i)}")
}
val array2:IntArray = intArrayOf(100,200,300)
for(i in array2.indices){
println("array2[$i] = ${array2.get(i)}")
}
//方法2 使用`Array`的构造函数初始化数组
//Array(size: Int, init: (Int) -> T)
val array3 = Array(5) {
it*it
}
for(i in array3.indices){
println("array3[$i] = ${array3.get(i)}")
}
输出结果
array1[0] = 1 array1[1] = 2 array1[2] = 3
array2[0] = 100 array2[1] = 200 array2[2] = 300
array3[0] = 0 array3[1] = 1 array3[2] = 4 array3[3] = 9 array3[4] = 16
1.5 字符串
字符串使用String
类型表示。与java中的字符串类似可以使用+
符号进行字符串链接
val str = "aaaa"
println(str+"bbbb") //输出aaaabbbb
对于字符串换行,kotlin
提供两种方式
- 双引号
"
和\n
的方式 - 三引号使用原始字符串
"""
val str = "aaaa\nbbbb"
val str2 = """aaaa
|bbbb
""".trimMargin() //trimMargin() 方法去除前导空格
//默认 | 用作边界前缀,但你可以选择其他字符并作为参数传入,比如 trimMargin(">") 。
//两种方式输出结果相同
aaaa
bbbb
kotlin
提供了字符串模板
,即字符串中含有少部分代码。字符串模板的使用方式为$变量
或者${表达式}
的方式
val str = "aa"
println("变量 str 的值为$str,长度为${str.length}")
//输出结果
变量 str 的值为aa,长度为2
2. 控制流
2.1 if
在kotlin
当中,if是一个含有返回值的表达式,除了像java
当中作为语句处理之外,在每个分支语块下的最后的表达式将作为返回值返回。
//val代表不可变参数,类似于java的final,var代表可变参数
var a = 1
if (a in 1..10) {
println("a is in 1..10")
} else {
println("unkonwn")
}
a = 15
//判断a的范围并返回
val result = if (a in 1..10) {
"a is in 1..10"
} else if (a in 11..20) {
"a is in 11..20"
} else {
"unkonwn"
}
println(result)
//结果输出
a is in 1..10
a is in 11..20
2.2 when
when
表达式类似于java
中的switch操作符。与if
同样,既可以作为语句也可以作为表达式返回值。
val i = 1
when(i){
1 -> println("branch 1")
2 -> println("branch 2")
3 -> println("branch 3")
4 -> println("branch 4")
else -> println("unknown")
}
val result = when(i){
1 ->"branch 1"
2 -> "branch 2"
3 -> "branch 3"
4 -> "branch 4"
else -> "unknown"
}
println("result is $result")
//输出结果
branch 1
result is branch 1
相比于如果对于多种分支返回相同的结果,可以集中处理,并且可以使用表达式作为分支条件,而不是变量。
when(i){
//如果是1或者2
1,2 -> println("i is 1 or 2")
//如果i的取值在3--10
in 3..10 -> println("i is in 3--10")
//如果i是int类型
is Int -> println("i is Int")
else -> println("unknown")
}
2.3 for
for
循环可以对任何提供了迭代器的对象进行遍历。
遍历一个数组
//使用索引遍历数组
for(i in array1.indices){
println("array1[$i] = ${array1.get(i)}")
}
//使用库函数遍历
for ((index, value) in array1.withIndex()) {
println("the element at $index is $value")
}
遍历数字区间
//遍历1--3
for (i in 1..3) { println(i)
}
//遍历6--0 步长为2
for (i in 6 downTo 0 step 2) {
println(i) }
遍历map集合
for((key,value) in values){
println("key is $key,value is $value")
}
2.4 while
while
语法的用法与java中基本相同
var index = 0
while (index < items.size) {
println("item at $index is ${items[index]}")
index++
}
2.5 标签
Kotlin
提供了三种标签:return
、break
和continue
,其作用方式与java中相同
- return:从包含它的函数中返回。
- break:从包含它的循环中返回。
- continue:继续下一次循环。
除了正常类似java中的调用方式外,还可以配合标签使用。标签的格式为标识符后跟 @ 符号,例如:abc@ 、loop@。
代码说明
fun main(args: Array<String>) {
var result = 0
loop@ for (i in 0..10) {
for (j in 0..10) {
if (i == 2 && j == 2) {
//如果不加标签跳出j的循环,加标签后跳出loop指定循环
println("break to loop")
break@loop
}else{
result++
}
}
}
println("result is $result")
}
//结果输出
break to loop
result is 24
使用标签跳出lambda表达式
fun foo() {
listOf(1, 2, 3, 4, 5).forEach {
// 在lambda表达式中不使用标签,直接退出到函数调用者,即退出最近的一个fun()
if (it == 3) return
print(it)
}
}
fun foo() {
listOf(1, 2, 3, 4, 5).forEach lit@{
// 使用标签可以退出当前lambda表达式,如果不显式添加标签,默认标签与函数名相同
if (it == 3) return@lit
print(it)
}
}
也可以在标签处添加返回值如:return@loop 1
3. 类
在kotlin
当中的类使用关键字class
声明class A{}
,类中的成员包括:构造函数和初始化块
、属性
、函数
、嵌套类和内部类
以及对象声明
。
3.1 构造函数和初始化块
kotlin
的构造函数与java
不同,分为主构造函数和次构造函数,构造函数使用关键字constructor
声明。主构造函数内不能有任何代码,其声明方式如下:
/**
* 在主构造函数中初始化属性,当不声明构造函数时会默认有一个无参构造函数
* class ClassName public constructor()
*/
class Person constructor(firstName: String)
//也可以在构造函数中对参数进行赋值操作
class TestClass constructor(val key: String = "a", val value: String = "b"):ParentClass(key)
如果主构造函数没有任何注解或者可⻅性修饰符,可以省略这个 constructor 关键字。
class Person(firstName: String)
在类内部可以声明次构造函数
,同样使用constructor
关键字声明。
//每个次构造函数都需要委托给主构造函数,委托方式有两种
//1.直接委托给主构造函数
constructor(key: String, value: String, param1: String) : this(key, value){
println("constructor 1")
}
//2. 通过其他次构造函数委托
constructor(key: String, value: String, param1: String, param2: String) : this(key, value, param1) {
println("constructor 2")
}
构造函数内部不能包含代码,类的初始化工作就放在了初始化代码块中。初始化代码块使用init
表示。
//初始化代码可以放在init关键字表示的代码块中处理
//在类对象初始化时,与类属性顺序执行
//实际上这部分代码会成为主构造函数的一部分,在次函数委托给主函数之后,与次函数第一行执行这部分代码
val classKey = "the Key is : $key".also(::println)
init {
println("first init code in this")
}
val classValue = "the value is : $value".also(::println)
init {
println("second init code in this")
}
3.2 接口
Kotlin
的接口使用interface
来表示。
interface Base {
val message: String
fun print()
}
/**
* 实现接口
*/
class BaseImpl(val x: Int) : Base {
override val message = "nnnn"
override fun print() {
println(message)
}
}
3.3 继承
在Kotlin
中所有类都有一个共同的超类Any
,这对于没有超类型声明的类是默认超类。如果想要显式的声明一个超类,我们使用open
关键字。默认情况下Kotlin
中的类都是不允许被继承的,只有用open
关键字声明的类才允许被继承。
/**
* open关键字与final关键字相反,它允许其他类对它进行继承。
* 默认情况下Kotlin中的类都是public final类型
*/
open class ParentClass(key: String)
如果子类没有主构造函数,则子类的所有构造函数都必须使用super
关键字初始化其基类型,或委托给另一个构造函数做到这一点。如果子类有主构造函数,子类可以在继承时直接使用父类主构造函数初始化。
open class ParentClass(key: String) {
constructor(key: String, value: String) : this(key) {
println("parent constructror 1")
}
}
//当子类没有主构造函数时,可以在子类的构造方法后继承不同的父构造方法
class Child: ParentClass {
constructor(key: String, value: String):super(key)
//委托给不同的父构造函数
constructor(key: String, value: String, param1: String) : super(key, value) {
println("constructor 1")
}
}
//当子类有构造函数时,需要继承父类的构造函数
class Child(key: String) : ParentClass(key) {
//子类有主构造函数,委托给子类的主构造函数进行。
constructor(key: String, value: String):this(key)
constructor(key: String, value: String, param1: String) : this(key, value) {
println("constructor 1")
}
}
对于父类的属性和方法,如果允许子类对其进行覆盖,则需要用open
关键字显式标注。在子类中使用override
关键字覆盖。
open class ParentClass(key: String) {
/**
* 声明为open关键字表示其可以被子类复写,如果不声明open关键字,默认为public final
* 使用var可以覆盖val属性,反之则不可以,因为val属性默认只有getter方法,而var有getter和setter方法。
*/
open val a = "aaa".also {
println(it)
}
/**
* 声明为open关键字表示其可以被子类复写,如果不声明open关键字,默认为public final
*/
open fun method(param:String) {
println("......")
}
}
class Child : ParentClass {
override val a = "bbb".also { println(it) }
override fun method(param: String) {
super.method(param)
println("[[[[[[")
}
}
新建子类对象时,会先完成基类的初始化操作,然后在初始化子类。
在 Kotlin 中,实现继承由下述规则规定:如果一个类从它的直接超类继承相同成员的多个实现,它必须覆盖这个成员并提供其自己的实现(也许用继承来 的其中之一)。为了表示采用从哪个超类型继承的实现,我们使用由尖括号中超类型名限定的 super,如 super<Base> :
open class A {
open fun f() { print("A") } fun a() { print("a") }
}
interface B {
open fun f() { print("B") } // 接口成员默认就是“open”的 fun b() { print("b") }
}
class C : A(),B {
// 编译器要求覆盖 f():
override fun f() {
super<A>.f() // 调用 A.f()
super<B>.f() // 调用 B.f()
}
}
3.4 伴生对象
在Kotlin
当中并没有静态方法,也就没有办法使用单例等不需要创建类的实例,通过类名就可以访问的函数。这时候就用到了伴生对象。伴生对象用companion
关键字标记。
class InstanceClass {
companion object A{
fun create(): InstanceClass = InstanceClass()
}
}
fun main(args:Array<String>){
val instance = InstanceClass.create()
}
这里需要注意的是,这种用法看起来像是类似于Java中的静态方法,实际上是相当于声明一个静态内部类A,然后调用静态内部类中的方法。
3.5 其他类
数据类
在Kotlin
中使用data
标记的类为数据类。
data class User(val name: String, val age: Int)
为了确保生成的代码的一致性和有意义的行为,数据类必须满足以下要求:
- 主构造函数需要至少有一个参数;
- 主构造函数的所有参数需要标记为 val 或 var ;
- 数据类不能是抽象、开放、密封或者内部的;
- (在1.1之前)数据类只能实现接口。
密封类
密封类用来表示受限的类继承结构:当一个值为有限集中的类型、而不能有任何其他类型时。
要声明一个密封类,需要在类名前面添加 sealed 修饰符。虽然密封类也可以有子类,但是所有子类都必须在与密封类自身相同的文件中声明。
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
//使用when区分不同的情况。
fun eval(expr: Expr): Double = when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
// 不再需要 `else` 子句,因为我们已经覆盖了所有的情况
}
嵌套类和内部类
一个类可以定义到另一个类当中
class Outer {
private val bar: Int = 1
class Nested {
fun foo() = 2
}
}
val demo = Outer.Nested().foo() // == 2
使用inner
标记为内部类,并可以访问外部类的成员。
class Outer {
private val bar: Int = 1
inner class Inner {
fun foo() = bar
}
}
val demo = Outer().Inner().foo() // == 1
匿名内部类的实现采用对象表达式的方式
window.addMouseListener(object: MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
// ......
}
override fun mouseEntered(e: MouseEvent) { // ......
}
})
如果对象是函数式Java
接口(即具有单个抽象方法的Java
接口)的实例,你可以使用带接口类型前缀的lambda表达式创建它:
val listener = ActionListener { println("clicked") }
枚举类
kotlin
中的每一个枚举常量都是一个对象,用逗号分隔
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
//带有初始化信息的枚举类
enum class EnumClass(var x:Int,var y:Int){
NOUTH(0,0),SOUTH(90,90),WEST(180,180),EAST(270,270);
init {
print("init $this ")
x = x+1
y = -y
}
}
//调用枚举类型
fun main(args:Array<String>){
//在初次调用枚举类型时初始化所有枚举常量
println(EnumClass.EAST)
println(EnumClass.NOUTH)
}
//输出结果
init NOUTH init SOUTH init WEST init EAST EAST
NOUTH
3.6 扩展
在java
当中我们经常会写一些Utils
,比如说StringUtils
等来做一些对String
的扩展功能封装。在Kotlin
中可以直接对类的功能进行扩展,而无需继承该类。
我们对MutableList<Int>
类进行扩展函数。
//扩展函数,扩展方法convert()实现对列表的倒置
fun MutableList<Int>.convert(){
for(i in 0..(this.size/2)){ // “this”对应该列表
val tmp = this[i]
this[i] = this[this.size-1-i]
this[this.size-1-i] = tmp
}
}
//扩展属性,获取最后一个元素。
val <T> List<T>.lastData: T get() = this[this.size-1]
fun main(args: Array<String>) {
val list = mutableListOf(1, 2, 3, 4, 5)
println(list.lastData)
list.convert()
println(list)
}
//输出结果
5
[5, 4, 3, 2, 1]
注意:当扩展函数与成员函数相同时,在调用时总是取成员函数。
3.7 委托
委托是kotlin
实现的继承的一种替代方式,委托的内容通过by
关键字实现。
/**
* 定义Base接口
*/
interface Base {
val message: String
fun print()
}
/**
* 对Base接口的实现
*/
class BaseImpl(val x: Int) : Base {
override val message = "base message"
override fun print() {
println(message)
}
}
/**
* 委托给Base类的具体实现
*/
class Dervied(b: Base) : Base by b {
//调用委托对象b时,不会访问到
override val message = "dervied message"
}
fun main(args: Array<String>) {
val b = BaseImpl(1)
//因为将print()函数的处理委托给Base b来处理,所以当访问不到Dervied()里面的内容时
//直接执行b的print方法,输出`base message`
val a = Dervied(b)
a.print()
//如果有对b中的内容进行重写,则优先使用Dervied中的变量或方法
println(a.message)
}
//输出结果
base message
dervied message
委托属性
Kotlin
本身支持委托属性,同样通过by
关键字实现。
val str:String by Delegate()
实现委托后属性对应的get()
和 set()
会被委托给by后面的getValue()
和setValue()
方法,为此接收委托的类需要实现对应的getValue()
和setValue()
方法.(val 可以只实现getValue方法,因为val属性是只读的)
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.")
}
//thisRef表示被委托的对象自身
//property保存被委托对象对于自身的描述
//value表示被赋予的新值,因为str是String类型,所以这里value只接收String类型
}
Kotlin
提供了几种标准委托:延迟属性(lazy)
、可观察属性(Observable)
.
延迟属性(lazy)
接收一个lambda表达式作为参数,第一次调用 get() 会执行已传递给
lazy() 的 lambda 表达式并记录结果,后续调用 get() 只是返回记录的结果。
//String为lazy后lambda表达式最后的值,在第一次调用到lazy变量时才初始化并调用lazy后lambda表达式,之后调用则直接返回最后的值。
val lazy: String by lazy {
println("computed!")
"Hello"
}
fun main(args: Array<String>) {
println(a.lazy)
println(a.lazy)
}
//输出结果
computed!
Hello
Hello
可观察属性(Observable)
接受两个参数:初始值
和修改时处理程序
。每当变量的值在发生改变时都会交给修改时处理程序
处理。
// Delegates.observable标准委托,在修改属性值时调用
// 接收两个参数分别为初始化的值以及对修改时的监听(类似Android中的handler)
// 监听方法中的三个参数分别为修改的属性、旧值、新值
var observable: String by Delegates.observable("initValue") { prop, old, new ->
println("$old ->> $new")
}
// Delegates.vetoable标准委托,在修改属性值时调用
// 接收两个参数分别为初始化的值以及对修改时的监听(类似Android中的handler),并判断是否对属性的值进行修改
// 返回true时对值进行修改操作,返回false时不对值进行修改
// 监听方法中的三个参数分别为修改的属性、旧值、新值
var observable1: String by Delegates.vetoable("initValue") { prop, old, new ->
println("$old ->> $new")
new.equals("bb")
}
fun main(args: Array<String>) {
a.observable = "111"
a.observable = "222"
println(a.observable1)
a.observable1 = "bb"
println(a.observable1)
a.observable1 = "aa"
println(a.observable1)
}
//输出结果
initValue ->> 111
111 ->> 222
initValue1
initValue1 ->> bb
bb
bb ->> aa
bb
4. 其他
命名参数
如果需要调用的方法有很多参数,就可以使用命名参数的方式。
//函数定义
fun reformat(str: String,
normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ' ') {
......
}
//调用时可以只修改必要的参数,对于默认参数不进行修改的可以不须修改
reformat(str, wordSeparator = '_')
高阶函数
Kotlin
支持将函数作为返回值或者函数使用,作为函数类型时,使用一个圆括号括起来的参数类型列表以及一个返回类型:(A, B) -> C 表示接受类型分别为 A 与 B 两个参数并返回一个 C 类 型值的函数类型。
函数类型可以有一个额外的接收者类型,它在表示法中的点之前指定:类型 A.(B) -> C 表示可以在 A 的接收者对象上以一个 B 类型参数来调 用并返回一个 C 类型值的函数。带有接收者的函数字面值通常与这些类型一起使用。
//带接收者类型为String类型,Int为调用类型即times的类型
val repeat: String.(Int) -> String = { times -> repeat(times) }
//带与不带接收者的函数类型非字面值可以互换,其中接收者可以替代第一个参数,反之亦然
val twoParameters: (String, Int) -> String = repeat
既然函数可以作为参数使用,那么带有函数类型的函数被称为高阶函数。
//定义函数example,它的参数是一个函数类型的参数
fun example(times: Int, computeFoo: () -> String) {
//example内部执行times次computeFoo函数,并输出执行结果
var i = 0
while(i<times){
println("${computeFoo()}")
i++
}
}
fun main(agrs: Array<String>) {
//调用example方法
a.example(2) {
"aaa"
}
}
//输出结果
aaa
aaa
内联函数
kotlin
引入了内联函数,内联函数就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来直接进行替换,用以消除高阶函数以及函数类型的不必要开销。
//定义内联函数synchronized用于实现同步,它接收两个参数锁Lock以及同步的内容action()
inline fun <T> synchronized(lock:Lock,action:()->T):T{
lock.lock()
try {
return action()
}
finally {
lock.unlock()
}
}
val l=Lock()
//类似于java中的调用,使用synchronized同步代码块
synchronized(l){
...
}
inline
修饰符也可以标注独立的属性访问器
val foo: Foo
inline get() = Foo()
var bar: Bar get() = ......
inline set(v) { ...... }
解构声明
解构声明可以帮助我们把单一对象分解成多个.
//定义Persion数据类以及其主构造函数
//当声明为数据类时不需要提供主构造函数的componentN方法
data class Person(var name:String,var age:Int){
var address = ""
//定义次构造函数
constructor(name:String,age:Int,address:String):this(name,age){
this.address = address
}
//定义componentN方法用于解构声明
operator fun component3():String{
return this.address
}
}
fun main(args:Array<String>){
//声明Person类
val person = Person("小明",18,"北京市朝阳区")
//解构声明调用
//结构声明在编译时会分解成
//String name = person.component1();
//int age = person.component2();
//String address = person.component3();三个步骤来做,所以需要在类中提供这三个方法
val (name,age,address) = person
}
解构声明也可以在for循环中使用
//map使用解构声明
var map = HashMap<String, Person>()
map.put("1", personA)
map.put("2", personB)
map.put("3", personC)
map.put("4", personD)
map.put("5", personE)
for ((key, value) in map) {
println("key: $key, value: $value")
}
//对象使用解构声明
var personA: Person = Person("Door", 22, "ShanDong")
var personB: Person = Person("Green", 30, "BeiJing")
var personC: Person = Person("Dark", 23, "YunNan")
var personD: Person = Person("Tool", 26, "GuanDong")
var personE: Person = Person("Mark", 24, "TianJin")
var pers = listOf(personA, personB, personC, personD, personE)
for ((name, age) in pers) {
println("name: $name, age: $age")
}
This表达式
为了表示当前的 接收者 我们使用 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
}
}
}
}
结尾
本篇内容只是简单介绍了kotlin
的一些常规用法,借鉴于kotlin中文文档
,如果需要系统的学习这门语言,建议阅读官方文档。
网友评论