面试储备资料我爱编程程序员

Kotlin基础

2018-06-26  本文已影响18人  銀灬楓

前言

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来表示,系统提供了数字类型的getset方法以及size属性等。创建一个数组的方式有两种:

   //方法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提供两种方式

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提供了三种标签:returnbreakcontinue,其作用方式与java中相同

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)

为了确保生成的代码的一致性和有意义的行为,数据类必须满足以下要求:

密封类

密封类用来表示受限的类继承结构:当一个值为有限集中的类型、而不能有任何其他类型时。

要声明一个密封类,需要在类名前面添加 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 表达式:

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中文文档,如果需要系统的学习这门语言,建议阅读官方文档。

上一篇下一篇

猜你喜欢

热点阅读