Kotlin 新特性你真的了解吗?

开发者技术前线

共 27708字,需浏览 56分钟

 · 2021-07-04

点击“开发者技术前线”,选择“星标”

让一部分开发者先看到未来

前言

Kotlin作为Android开发的新选择,具有简洁、安全、函数式编程等特点,在实际开发中使用起来非常方便。经过一段时间的实际应用,对Kotlin的基础使用做个总结,主要对Kotlin基础应用结合实例进行介绍,从基本语法、类与对象和函数三个方面展开。

一.基本语法


1.1 变量声明

Kotlin中使用var/val关键字来声明变量
var关键字声明的是可变变量,val关键字声明的是只读变量
/**  * 学生类  */ 
class Student {     
  //可变变量声明关键字var
  var name: String = "小明"     
  //不指定变量类型的隐式声明
  var age = 10   
  //只读变量声明关键字val
  val sex: String = "男"    
  
  fun learn() {    
    print("$name is learning")    
    }

Kotlin中编译器可以通过变量的值来自动推导变量是什么类型的,这种功能称为自动类型推导,不指定变量类型的声明方式叫隐式声明。

1.2 语句

when表达式
//when语句可以作为表达式,符合条件的分支就是整个表达式的值
 val b = when (num) {     
    in 0..9 -> {true}     
    else -> { false }
   } 
in关键字的使用
//判断是否在区间内 
if (num in 1..9) {     
    print("ok"
}  
//不在区间内 
if (num !in 1..9) {     
    print("no"
}  
//遍历数组 
for (name in names) {     
    print(name)
}  
//判断name是否在数组内
if (name in names) {     
    print("ok"

类型转换
Kotlin中可以使用 is 关键字进行类型判断
fun foo(o: Any): Int {     
      if (o is String) {         
         //判断完类型之后,o会被自动转换为String类型         
         return o.length    
      }      
      //可以使用!is来取反    
      if (o !is String) {         
          return 0  
       }     
       return 0
    } 
空值检测
Kotlin中可以使用?来进行空值检测 例如str?.length 来表示str不为空时执行获取长度操作,可以避免空指针异常。

1.3 函数声明

声明一个简单的函数如下:
fun plus(x: Int, y: Int) : Int {    
     return x + y 
 } 
上述函数是声明一个plus()函数,接收两个参数x和y  返回Int类型的值
Kotlin中方法声明的关键字是fun,声明的定义格式为:


见性修饰符  fun  函数名(参数名:类型,...) : 返回值类型{   函数体 }

Tips:
1.如果函数体内实现很简单只有一行代码那么函数也可以这样写
fun plus(x: Int, y: Int): Int = x + y 
2.如果返回值是编译器能够推断出的类型例如Int 那么返回值类型也可以省略
fun plus(x: Int, y: Int) = x + y

1.4 函数的默认参数

Kotlin中可以使用默认参数实现重载类似功能,减少重载数量
fun plus(x: Int, y: Int = 10) : Int {     
      return x + y 

plus()函数中y参数有默认值10 那么我们调用的时候可以通过plus(11,20) 指定y的值,也可以省略y使用默认值 plus(11) y使用默认值10

可变参数

//java中,可变参数使用...表示
public void selectCourse(String... strArray){
    
}

//kotlin中,可变参数使用vararg关键字表示 
fun selectCourse(vararg strArray: String?) {    
   

二.类与对象


2.1 类的构造函数

Kotlin中一个类可以一个主构造函数以及一个或者多个次构造函数,主构造函数是类头的一部分:它跟在类名之后,如果主构造函数没有任何注解或者可见性修饰符,可以省略constructor关键字
//声明带一个参数的主构造函数
class Person constructor(name:String){
   
   init {
        //初始化的代码可以放到init初始化块中
        //初始化块是主构造函数的一部分,因此所有的初始化块中的代码都会在次构造函数体之前执行
    }
    
    //次级构造函数委托给主构造函数直接委托
    constructor(name:String,parent:Person):this(name){
    
    }
    
    //委托给别的次级构造函数间接委托
    constructor(name:String,parent:Person,age:Int):this(name,parent){
     
    }
}

2.2 类的继承

在Kotlin中所有类都有一个共同的超类Any,对于没有超类型声明的类是默认超类,Any有三个方法:equals()、hashCode()和toString(),为所有Kotlin类都定义了这些方法。
如果派生类有一个主构造函数,其基类型则必须用基类的主构造函数参数初始化
class Derived(p: Int) : Base(p){}
如果派生类没有主构造函数,那么每个次级构造函数必须使用super关键字初始化其基类型,或委托给另一个构造函数做到这一点,这种情况下不同的次级构造函数可以基类型的不同的构造函数
class MyView : View {    
     constructor(ctx: Context) : super(ctx)     
     constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
覆盖规则
//对于可覆盖的成员需要使用显示修饰符open
   open class Rectangle {
        open var length = 0
        open fun draw() { /*.......*/
        }
    }

    interface Polygon {
        //接口中的成员默认open
        fun draw() { /*........*/
        }
    }

    class Square() : Rectangle(), Polygon {
        override fun draw() {
            super<Rectangle>.draw() //调用Rectangle.draw()
            super<Polygon>.draw() //调用Polygon.draw()
        }
    }
Kotlin中,如果一个类从它的直接超类集成相同成员的多个实现,它必须覆盖这个成员并提供其自己的实现。为了表示从哪个超类型继承的实现,使用由尖括号中超类型名称限定super,例如super<Base>

2.3 类的属性

2.3.1 getter与setter
声明属性时是自动生成的getter与setter,我们也可以自定义getter和setter
val修饰的属性只有getter
var age: Int = 11
        get() {
            return field
        }
        set(value) {
            field = value + 1
        }
2.3.2 幕后字段
下面的代码会引起崩溃
class Person {
    var name = ""
        set(value) {
            this.name = value
        }
}
将上面代码转换成java
public final class Person {
   
   private String name = "Paul";

   public final String getName() {
      return this.name;
   }

   public final void setName( String value) {
      this.setName(value);
   }
}
可以看到setName()被无限调用导致崩溃,可以使用幕后字段来解决
在Kotlin中,如果属性至少一个访问器使用默认实现,那么Kotlin会自动提供幕后字段,用关键字field表示,幕后字段主要用于自定义getter和setter中,并且只能在getter和setter中访问。
上面代码应该修改成
class Person {
    var name = ""
        set(value) {
           filed = value
        }
}
满足下面条件之一的属性拥有幕后字段:
  • 使用默认getter/setter的属性,一定有幕后字段。对于var属性来说,只要getter/setter中有一个使用默认实现,就会生成幕后字段;
  • 在自定义getter/setter中使用了field的属性。
2.3.3 常量
val的值并不是不可能变化的
//每次获取currentTimeMills都是不同的
val currentTimeMills: Long
        get() {
            return System.currentTimeMillis()
        }
Kotlin中得到常量有两种方式1.使用const 2使用@JvmField注解
 class Person {
    companion object{
        //使用const修饰符
        const val TAG = "Person"
    }

    //使用@JvmField注解方式 
    //其内部原理是抑制编译器生成相应的getter方法并且无法重写val的get方法
    @JvmField
    val TAG = "Person"
}
2.3.4 属性延迟初始化
一般地,属性声明为非空类型必须在构造函数中初始化,可以用lateinit修饰符标记该属性使其可以延迟初始化,在初始化前访问一个lateinit属性会抛出异常

2.4 内部类

Kotlin 的内部类默认为静态内部类,添加 inner 标记后变为非静态内部类,能够访问外部类的成员,内部类会带有一个对外部类的对象的引用
class Outer {
        private val b: Int = 1
       
        inner class Inner {
            fun foo(): Int = b
        }
    }

    val d = Outer().Inner().foo() // ===1

2.5 数据类

数据类是Kotlin中用来保存数据的类,使用data关键字标记,具有以下要求:
  • 主构造函数至少有一个参数
  • 主构造函数的参数需要显示的标记为val或者var
  • 数据类不能是抽象,开放,密封或者内部的
Tips:
类中声明的属性在toString(),equals(),hashCode()以及copy()的实现中被排除
data class Person(val name: String) {
    var age: Int = 0
}

fun foo() {
    val person1 = Person("John")
    val person2 = Person("John")
    person1.age = 10
    person2.age = 20
    //person1和person2虽然有不同的年龄但是会视为相等
    person1.equals(person2) //true
}

2.6 枚举类

Kotlin中枚举类中每一个枚举都是一个对象,并且之间用逗号分隔
    enum class Direction {
        NORTH, SOUTH, WEST, EAST
     }

    enum class Color(val rgb: Int) {
        RED(0xFF0000),
        GREEN(0x00FF00),
        BLUE(0x0000FF)
     }
枚举常量的匿名类
  • 要实现枚举常量的匿名类,则必须提供一个抽象方法(必须重写的方法)。且该方法定义在枚举类内部。而且必须在枚举变量的后面。
  • 枚举变量之间使用逗号(,)分割开。但是最后一个枚举变量必须使用分号结束。不然定义不了抽象方法
enum class Color(val rgb: Int) {
        RED(0xFF0000) {
            override fun print() {
                print("red")
            }
        },
        GREEN(0x00FF00) {
            override fun print() {
                print("green")
            }
        },
        BLUE(0x0000FF) {
            override fun print() {
                print("blue")
            }
        };

        abstract fun print()
    }

fun main() {
        Color.BLUE.print()
    }
枚举类的使用
  • 每个枚举常量都包含两个属性:name(枚举常量名)和ordinal(枚举常量位置)
  • 提供了values()和valueOf()方法来检测指定的名称与枚举类中定义的任何枚举常量是否匹配。

2.7 委托

2.7.1 类委托
一个标准类委托的实现如下:
//创建接口
    interface Base {
        fun print()
    }

    //实现此接口的被委托的类
    class BaseImpl(val x: Int) : Base {
        override fun print() {
            print(x)
        }
    }

    //通过关键字by 建立委托类
    class Agent(b: Base) : Base by b

    fun main(args: Array<String>) {
        val b = BaseImpl(1)
        Agent(b).print() // 输出 1 
    }
Agent()接收一个Base实例,并通过by关键字与其建立委托关系。
如果Agent()中覆盖实现了print()方法,那么将使用覆盖的实现,而不是委托对象中的实现
class Agent(b: Base) : Base by b {
        override fun print() {
            print("123")
        }
    }
    
 fun main(args: Array<String>) {
        val b = BaseImpl(1)
        Agent(b).print() // 输出 123 
  }
2.7.2 属性委托
委托属性的实现
class Delegate {
        operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
            return "thank you for delegating '${property.name}' to me"
        }

        operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
            print("$value has been assigned to '${property.name}'")
        }
    }

class Example {
        var p: String by Delegate()
    }

 fun foo() {
        val e = Example()
        print(e.p)  // thank you for delegating 'p' to me

        e.p = "new" // new has been assigned to 'p'
    }
属性name将它访问器的逻辑委托给了Delegate对象,通过by关键字对表达式Delegate()求值获取这个对象。任何符合属性代理规则都可以使用by关键字。属性代理类必须要遵循getValue(),setValue()方法约定,getValue、setValue方法可以是普通方法也可以是扩展方法,并且是方法是支持运算符重载。如果是val修饰的属性只需要具备getValue()方法即可。
属性代理基本流程就是代理类中的getValue()方法包含属性getter访问器的逻辑实现,setValue()方法包含了属性setter访问器的逻辑实现。当属性name执行赋值操作时,会触发属性setter访问器,然后在setter访问器内部调用delegate对象的setValue()方法;执行读取属性name操作时,会在getter访问器中调用delegate对象的getValue方法.
介绍几种常用的标准委托
延迟属性Lazy
class LazySample {
        val lazyStr: String by lazy {
            print("init")
            "123"
        }
    }
    
 fun main(args:Array<String>){
        val sample = LazySample()
        print("lazy = ${sample.lazyStr}")
        print("lazy = ${sample.lazyStr}")
    }
    // 输出 
    // init 
    // lazy = 123  
    // lazy = 123
属性非空强校验
var name: String by Delegates.notNull()

fun init(name: String) {
        this.name = name
    }

fun main(args: Array<String>) {
        val student = Student()
        //初始化要在使用之前不然会报异常->IllegalStateException
        student.init("张三")
        print(student.name)
    }
可观察属性
private var name: String by Delegates.observable("oldValue") { property, oldValue, newValue ->
        print("${property.name} 属性变化: $oldValue -> $newValue")
    }

    private var age: Int by Delegates.observable(0) { property, oldValue, newValue ->
        print("${property.name} 属性变化: $oldValue -> $newValue")
    }

    private fun print() {
        print("name = $name")
        print("age = $age")
    }

    fun main(args: Array<String>) {
        print()
        name = "Bob"
        age = -1 //小于0,修改失败
        print()
        age = 28
        print()
    }

    /*
    * name = oldValue
    * age = 0
    * name 属性变化: oldValue -> Bob
    * age 属性变化: 0 -> -1
    * name = Bob
    * age = 0
    * age 属性变化: 0 -> 28
    * name = Bob
    * ag

三.函数


3.1 局部函数

Kotlin支持局部函数,即一个函数在另一个函数内部,局部函数可以访问外部函数(即闭包)的局部变量
 fun outer(str: String) {
        fun inner(index: Int) {
            str.substring(0, index)
        }
        inner(2)
    }

3.2 函数类型

Kotlin中使用类似 (Int) -> String 的一系列函数类型来处理函数的声明
  • 所有函数类型都有一个圆括号括起来的参数类型列表以及一个返回类型:(A, B) -> C 表示接受类型分别为 A 与 B 两个参数并返回一个 C 类型值的函数类型
  • 函数类型可以有一个额外的接收者类型,它在表示法中的点之前指定:类型 A.(B) -> C 表示可以在 A 的接收者对象上以一个 B 类型参数来调用并返回一个 C 类型值的函数.
  • 挂起函数属于特殊种类的函数类型,它的表示法中有一个 suspend 修饰符 ,例如 suspend () -> Unit 或者 suspend A.(B) -> C
  • 函数类型可以使用圆括号进行接合:(Int) -> ((Int) -> Unit)
  • 箭头表示法是右结合的,(Int) -> (Int) -> Unit 与(Int) -> ((Int) -> Unit)等价,但不等于 ((Int) -> (Int)) -> Unit。
  • typealias ClickHandler = (Button, ClickEvent) -> Unit 通过使用类型别名给函数类型起一个别称
//(A,B) -> C
    val fun1: (String, String) -> Unit = { s1, s2 ->
        print("$s1 and $s2")
    }

    //A.(B) -> C
    val fun2: String.(String) -> Unit = { s ->
        print("$this $s")
    }

    fun foo() {
        fun1("123""456")
        fun1.invoke("123""456")

        fun2("123""456")
        fun2.invoke("123""456")
        "123".fun2("456")
    }

3.3 Lambda表达式

Lambda表达式总是在花括号中,参数声明放在花括号内,并有可选的类型标注,函数体跟在一个->符号之后,如果推断出的该Lambda的返回类型不是Unit,那么最后一个表达式会视为返回值。

val sum = { x: Int, y: Int -> x + y }
如果函数作为最后一个参数,那么相应的参数传入的Lambda表达式可以放到圆括号之外,这种语法称为拖尾lambda表达式,如果是lambda表达式作为调用时的唯一参数,那么圆括号可以完全省略
fun sum(m: Int, n: Int, param: (x: Int, y: Int) -> Int): Int {
        return param.invoke(m, n)
    }

fun main(args: Array<String>) {
        sum(1, 2) { x, y -> x + y }
  }
lambda表达式的参数未使用的情况下,可以用下划线取代其名称
fun sum(m: Int, n: Int, param: (x: Int, y: Int) -> Int): Int {
        return param.invoke(m, n)
    }

fun main(args: Array<String>) {
        sum(1, 2) { x, _ -> x + 10 }
  }

3.4 匿名函数

匿名函数区别于lambda表达式的不同点是,可以显示的指定返回值类型。
fun(x: Int, y: Int): Int = x + y
如果应用中需要显示的指定返回值类型,可以使用匿名函数

3.5 高阶函数

高阶函数是奖函数用作参数或者返回值的函数
fun sum(m: Int, n: Int, param: (x: Int, y: Int) -> Int): Int {
        return param.invoke(m, n)
    }

fun main(args: Array<String>) {
        //函数类型作为参数
        val param: (x: Int, y: Int) -> Int = { x, y -> x + y }
        sum(1, 2, param)
    }
    
fun sum(): (x: Int, y: Int) -> Int {
        return { x, y -> x + y }
    }

    fun main(args: Array<String>) {
        //函数类型作为返回值类型
        sum().invoke(1, 2)
    }    

3.6 中缀表示法

标有infix关键字的函数也可以使用中缀表示法调用。中缀函数必须满足以下要求:
  • 它们必须是成员函数或扩展函数;
  • 它们必须只有一个参数;
  • 其参数不得接受可变数量的参数且不能有默认值。
 infix fun Int.sum(x: Int): Int {
        return 1
    }

 fun main(args: Array<String>) {
        //用中缀表示法调用该函数
        1 sum 2
        //等同于调用
        1.sum(2)
   }

3.7 内联函数

使用高阶函数会带来一些运行时的效率损失:每一个函数都是一个对象,并且会捕获一个闭包。即那些在函数体内会访问到的变量。内存分配(对于函数对象和类)和虚拟调用会引入运行时间开销。
inline 修饰符影响函数本身和传给它的 lambda 表达式:所有这些都将内联到调用处。
fun main(args: Array<String>) {
        print("start")
        show("123")
        print("end")
    }

inline fun show(str: String) {
        print(str)
    }
转换成java代码后
 public final void main(@NotNull String[] args) {
      System.out.print("start");
      System.out.print("123");
      System.out.print("end");
   }

   public final void show(@NotNull String str) {
      System.out.print(str);
   }
不带inline修饰时
 public final void main(@NotNull String[] args) {
      System.out.print("start");
      this.show("123")
      System.out.print("end");
   }

   public final void show(@NotNull String str) {
      System.out.print(str);
   }
一般情况下当对于带有lambda参数的函数,建议使用inline进行修饰。

3.8 标准库函数

最后介绍几种标准库函数。
函数名称定义功能
run        public inline funrun(block: () -> R): R = block()                                                               调用run函数块。返回值为函数块最后一行,或者指定return表达式
apply           public inline funT.apply(block: T.() -> Unit): T { block(); return this }调用某对象的apply函数,在函数块内可以通过 this 指代该对象。返回值为该对象自己
let          public inline fun <T, R> T.let(block: (T) -> R): R = block(this)调用某对象的let函数,则该对象为函数的参数。在函数块内可以通过 it 指代该对象。返回值为函数块的最后一行或指定return表达式
alsopublic inline funT.also(block: (T) -> Unit): T { block(this); return this }调用某对象的also函数,则该对象为函数的参数。在函数块内可以通过 it 指代该对象。返回值为该对象自己
withpublic inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()将某对象作为函数的参数,在函数块内可以通过 this 指代该对象。返回值为函数块的最后一行或指定return表达式

总结

以上是结合实例对从三个方面对Kotlin的基础应用介绍。相对来讲函数相关的知识点是比较重要的,函数作为Kotlin中的一等公民,可以作为参数或者返回值,也是Kotlin的一大特点。我们更需要去深入了解其中的原理并在应用中灵活应用,这样能起到事半功倍的效果。

—  —

点这里👇关注我,记得标星呀~


前线推出学习交流一定要备注:研究/工作方向+地点+学校/公司+昵称(如JAVA+上海

扫码加小编微信,进群和大佬们零距离



END



好文点个在看吧!
浏览 14
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报