Kotlin入门

作者: BigDevil_S | 来源:发表于2019-10-13 23:00 被阅读0次

    这篇文章会列出我认为入门需要掌握的特性,如果要想应用到项目中去的话可以先去GitHub上找一些优秀的Kotlin项目学习一下Kotlin的编程思想。

    Kotlin中文站:里面有一些参考资料以及一些推荐书籍。

    如何在IDE中查看Kotlin编译过后的Java代码

    很多时候直接看下编译后的Java代码比看别人的解释容易得多。
    以Android Studio为例:Tools -> Kotlin -> Show Kotlin Bytecode -> 会出现一个小窗口,点击Decompile

    基本语法

    基本语法可以看这个java-to-kotlin(这个很重要,一定要先看一下,内容并不多),里面是关于Java与Kotlin语法上的不同,当然只看这些是不够的

    Lambda 表达式

    在Kotlin中大家基本上都会用lambda表达式去写代码,这里不建议直接去看Kotlin lambda表达式的语法,自己刚开始写Demo的时候编译器会给出一些警告,指出某些代码可以被转换成lambda表达式,然后让编译器去帮你转换,有了这些基本印象后再去看语法会感觉比较友好,具体的语法这里就不说了,内容还是比较多的,可以自行查找。

    访问修饰符

    private :类内部可见

    protected:类内部及其子类可见

    internal:Module内可见

    public :全局可见

    变量

    • var表示可变变量(variable),val表示不可变变量(value),也就是后者是被final修饰的
    • 变量类型不是必须的,编译器可以自动识别,比如val name = "xiaoming"编译器会自动识别为String类型
    • Kotlin是空安全的,对象默认是不能为null的,如果想要赋值为null在声明对象类型时需要加上?,且可空类型不能直接调用其方法,需要使用?.!!,如果为null前者会返回null后者会抛出空指针异常,具体如下:
    var str1: String = "abc"
    str1 = null // 编译器会报错
    var str2: String? = "abc"
    str2 = null // 没问题
    
    str1.length // 没问题
    str2.length // 编译器会报错
    
    val out = str2?.length //没问题
    println(out) // 这里输出null
    
    str2!!.length // 这里运行时抛出空指针
    
    if (str2 != null )  {
        str2.length // 没问题
    }
    
    • varval都是private的(就算显式声明了public也会被编译成private),编译器会自动生成getter/setter方法(val只有getter),在Kotlin中可以直接使用.来调用,编译器会自己去调用它的getter/setter方法,如user.id = 1会去调用user对象的setId(1)
      如果不想让成员变量被外部访问,可以显示声明变量为private,如private val id = 1,这样编译器就不会生成getter/setter
      当然getter/setter方法是可以被修改的,操作如下:
    class User{
        var id = 0
            get() = field - 1
            set(value) {
                field = value + 1
            }
    }
    

    其中field被称为”幕后字段“指的就是当前变量,像下面这种写法是错误的,因为id = value + 1会继续调用idsetId(value + 1)方法,就会产生死循环。

    class User{
        var id = 0
            set(value) {
                id = value + 1
            }
    }
    

    字符串模板

    在Kotlin中不需要使用+来拼接字符串,一个字符串中可以通过$来获取变量值,如下

        val name = "xiaoming"
        println("name is $name") // 获取变量值
        println("length is ${name.length}") // 使用表达式
        println("${'$'}29.18") // 打印 $ 符号
    

    上面的代码分别会输出

    name is xiaoming

    length is 8

    $29.18

    函数

    • 所有函数默认是public
    • 所有函数默认是final的(抽象函数和接口函数除外),想要不final需要加上open关键字修饰
        // 可以被重写
        open fun canOverride() {}
    
        // 无法被重写
        fun cannotOverride() {}
    
    • 重写父类函数需要在函数声明前加上overrider关键字
        override fun toString() = "kotlin"
    
    • 如果函数只有一个语句可以直接用赋值的形式
        fun plus(x: Int,y: Int) : Int = x + y // 此处返回类型可以省略
        fun sout(s: String) = print(s)
    
    • Kotlin中函数的参数是可以有默认值的,这样就不用写重载函数了
        fun userInfo(id: Int, name: String = "xiaoming", sex: String = "male") {
            println("id: $id, name: $name, sex: $sex")
        }
    
        fun test() {
            userInfo(1) // 输出:id: 1, name: xiaoming, sex: male
            userInfo(2, "xiaohong") // 输出:id: 2, name: xiaohong, sex: male
            userInfo(1, sex = "female") // 输出:id: 1, name: xiaoming, sex: female
        }
    

    如果想要提供给Java使用可以加上@JvmOverloads,这样编译成Java代码时就会生成对应重载函数

        @JvmOverloads
        fun userInfo(id: Int, name: String = "xiaoming", sex: String = "male") {
            println("id: $id, name: $name, sex: $sex")
        }
    
    • 无返回类型的函数会返回一个Unit对象,Unit可以理解成Java的Void,可以作为泛型对象,如果将Unit类型的函数赋值给一个变量,编译器会给变量赋值一个Unit单例
        val a = test()
    

    编译为Java之后就是这样的

        Unit a = Unit.INSTANCE;
    

    Unit直接继承于Any类(即Java中的Object类),只重写了toString方法

        override fun toString() = "kotlin.Unit"
    

    嵌套函数

    在Kotlin中函数是可以嵌套的,内部函数可以访问到外部函数的局部变量,且其本身只能被外部函数访问。

        fun outFun(name: String) {
            fun nestFun() {
                print(name)
            }
            nestFun()
        }
    

    扩展函数

    这是Kotlin的一个非常好用的特性,它可以为一个类添加函数,在这个函数中可以访问到对象的公有属性和方法,声明完扩展函数之后,该类及其子类的对象就可以直接通过.来调用这个函数,比如为CharSequence类添加一个toList方法,将字符逐个添加到一个列表中然后返回

    fun CharSequence.toList(): List<Char> {
        val list = ArrayList<Char>()
        for (char in this) {
            list.add(char)
        }
        return list
    }
    
    

    声明完上面这段代码之后,就可以直接调用了

        val s = "asdf"
        val list = s.toList()
    

    除了扩展函数之外,Kotlin还支持扩展属性,但是扩展属性不能直接赋值,只能设置它的getter/setter方法,实际上编译成Java代码后还是扩展了两个方法

    var StringBuilder.lastChar: Char
        get() = get(length - 1)
        set(value) = setCharAt(length - 1, value)
    

    高阶函数

    在Kotlin中函数可以作为参数传入到另一个函数中

        fun run(block: () -> Unit) {
            block()
        }
    

    上面就是一个简单的高阶函数,可以这样使用,运行之后就会输出run,这里用到了Lambda 表达式,不懂的可以再去看下

        run {
            print("run")
        }
    

    通过::来获取某个对象的方法来传入(::也可以用来获取变量)

        val runnable = Runnable {
            print("run")
        }
        run(runnable::run)
    

    内联函数

    通过inline修饰的函数为内联函数

    当我们使用高阶函数时,传入的函数对象会被编译成一个对象,然后再调用该对象的方法,这样会增加内存和性能的开销,而如果使用内联函数的话就可以将方法的调用转换为语句的调用

        fun run(block: () -> Unit) {
            block()
        }
    
        fun testRun() {
            run {
                print("run")
            }
        }
    

    上面的代码编译成Java代码大致是这样的,可以看到在testRun方法中,直接被编译成了语句调用

       public final void run(@NotNull Function0 block) {
          block.invoke();
       }
    
       public final void testRun() {
          String var = "run";
          System.out.print(var);
       }
    

    使用内联函数可以避免产生多余的对象,但是会增加编译后代码量,所以要避免内联代码块过大的函数,如果一个函数中有包含多个函数参数,可以通过noinline关键字来避免内联

    inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {}
    

    • 所有类都继承于Any类,相当于Java中的Object
    • 类与函数一样默认都是public final的,想要类可以被继承同样需要用open关键字来修饰
    • 类的继承与接口的实现都是通过:来表示的,中间由,隔开且没有顺序要求
    open class A()
    interface B
    class C : A(), B
    
    • 类的构造函数在init代码块中实现,如果不需要对构造函数添加访问修饰符或者注解,那么constructor关键字可以省略
    class Test constructor(a:Int) {
        init {
            println("Here is the constructor.")
            println("value a= $a")
        }
    }
    

    如果只想通过构造函数给成员变量赋值的话可以直接这样

    class Test(val a:Int){
        ...
    }
    

    如果想让构造函数私有可以这样

    class Test private constructor() {
        ...
    }
    
    • 如果需要多个构造函数可以在类的内部使用constructor关键字来声明,这在Kotlin中叫做次构造函数,而声明在类名上的叫做主构造函数,次构造函数需要直接或间接的继承主构造函数,具体可以看这里
    class Test() {
        init {
            ...
        }
        constructor(a:Int): this() {
            ...
        }
    }
    
    • Kotlin中有两种class类,一种就是Java中Class,另外一种是Kotlin自由的KClass,它们的获取方式也不一样
        val kClazz = Person::class
        val clazz = Person::class.java
    

    嵌套类

    嵌套类编译成Java代码之后就是静态内部类

    class Out {
        ...
        class Nest {
           ... 
        }
    }
    val nestClass = Out.Nest()
    

    内部类

    Kotlin中内部类需要使用inner关键字修饰

    class Out {
        ...
        inner class Inner {
           ...
        }
    }
    val innerClass = Out().Inner()
    

    object关键字

    object关键字可以简单地创建一个单例类,其中的变量和方法都可以直接通过类名来调用

    fun main(args: Array<String>) {
        val string = Singleton.str;
        Singleton.printMessage(string)
    }
    
    object Singleton {
        val str = "Singleton"
        fun printMessage(message: String) = println("$str $message")
    }
    

    可以用来写工具类

    object StringUtils {
        fun isEmpty(str: String?) = str == null || str == ""
    }
    

    伴生对象

    Kotlin中并没有static关键字,如果需要静态变量或是静态方法的话就要使用伴生对象,使用companion object来声明,伴生对象的命名可以省略,编译器会使用默认的命名Companion,一个类只能拥有一个伴生对象

    fun main(args: Array<String>) {
        val test = Test.create()
        println(Test.TAG)
    }
    
    class Test {
        companion object {
            val TAG = "KotlinTest"
            fun create() = Test()
        }
    }
    

    上面的代码会将TAG编译成Test类的静态成员变量,而create()方法其实不是一个真正的静态方法,它是属于伴生对象类的一个普通的public成员方法,伴生对象实际上是外部类的一个静态单例内部类,虽然可以直接通过Test类来调用create(),但其实编译过后是这样的

        // kotlin code
        val test = Test.create()
    
        // java code
        Test test = Test.Companion.create();
    

    如果想要生成一个真正的静态方法,可以使用@JvmStatic注解来实现
    下面使用objectcompanion object写一个延时加载的单例类

    class Singleton private constructor() {
        companion object {
            fun getInstance() = Holder.instance
        }
        object Holder {
            val instance = Singleton()
        }
    }
    

    数据类

    数据类类似于lombok的@Data注解,可以自动生成toString()equals()hashcode()copy()等方法,具体可以去看一下编译成的Java代码

    data class User(val id:Int, val name:String, val age:Int)
    

    除了常用的getter/setter之外还可以这样用

        val user1 = User(1, "Ben", 25)
        val user2 = user1.copy(id= 2, age = 23)
        val (id, name, age) = user1
    

    第二行代码将user1拷贝给了user2并修改了idage,第三行代码将user1的三个数据分别赋值给了idnameage三个变量,这个被称为解构,之所以可以这样写是因为数据类还实现了componentN(),后面在运算符重载会讲到

    密封类

    密封类本身是个抽象类,主要特点是它的构造方法是私有的,直接继承它的子类只能定义在密封类所在的文件中,无法定义在别的文件中,也就是说它限制了外部继承,直接子类就是确定的那几个,所以密封类也可以理解为功能更多的枚举类,因为它可以有更多的属性以及方法

    sealed class Person(val name: String, var age: Int) {
        class Male(name: String, age: Int) : Person(name, age)
        class Female(name: String, age: Int) : Person(name, age)
    }
    

    Kotlin也对密封类使用when语句也做了优化,可以不写else,因为子类是确定的那几个

    fun test(person: Person) {
        when (person) {
            is Person.Male -> println("male")
            is Person.Female -> println("female")
        }
    }
    

    操作符重载

    Kotlin中的各种操作符都对应着一种方法,比如+-*/分别对应着plusminustimesdiv,这些方法都是可以被重载的,编写时需要在方法前面加上operate来修饰

    fun main(args: Array<String>) {
        val a = Point(1, 4)
        val b = Point(2, 3)
        val c = a + b
        c.printPoint()
    }
    
    class Point(val x: Int, val y: Int) {
        operator fun plus(another: Point): Point {
            return Point(x + another.x, y + another.y)
        }
    
        fun printPoint() {
            println("x= $x, y= $y")
        }
    }
    

    上面的代码运行之后会输出"x= 3, y= 7",此外还有很多操作符,这里就不列举了,前面说到的componentN()其实也是操作符对应的方法,val (id, name, age) = user编译之后就是

          int id = user.component1();
          String name = user.component2();
          int age = user.component3();
    

    常量

    使用const val来声明一个常量,常量只能在objectcompanion object或是类的外部声明,且只能是基本数据类型或是String类型,因为常量要求在编译期就能确定它的值

    const val pi = 3.14
    
    object A {
        const val pi = 3.14
    }
    
    class B {
        companion object {
            const val pi = 3.14
        }
    }
    

    前面说过val是不可变变量,它和常量的区别是const val编译之后是public的而valpirvate的,访问val只能通过它的getter方法,而getter方法又是可以修改的(如下),对于开发者来说,常量应该是一个确定的值,所以val不是常量

    object A {
        val pi = 3.14
            get() = field + Math.random()
    }
    

    其他

    下面的这些还是属于Kotlin入门的范畴,并不是不重要,只是内容较多,就不详细讲了,网上相关的文章也有很多

    • Kotlin标准库
      Kotlin标准库提供了一些好用的函数,可以看下那些函数的实现,对学习Kotlin也有很大帮助
    • 委托/委托属性
      Kotlin语言是原生支持委托的,其中还包括了委托属性
    • 泛型
      Kotlin的泛型和Java的泛型大致上是相同的,但是写法上还是有点区别的,而且Kotlin可以使用inlinereified来支持真泛型
    • 协程
      Kotlin是有协程库来支持协程的,协程可以理解为运行在线程中的线程,比线程更轻量,使用协程一定程度上可以简化代码
    • 集合操作符
      Kotlin为集合提供了一系列操作符(实际上是集合的扩展函数),类似于RxJava,可以链式调用
    • 与Java交互
      一般使用Kotlin开发避免不了与Java交互,对于调用Java代码或是让Java调用Kotlin代码都可能会存在一些问题
    • Anko
      Anko是为android设计的一个代码库,对于android开发可以了解一下

    相关文章

      网友评论

        本文标题:Kotlin入门

        本文链接:https://www.haomeiwen.com/subject/axnplctx.html