联系我们
简单又实用的WordPress网站制作教学
当前位置:网站首页 > 程序开发学习 > 正文

Koltin - 第一篇 - 基础语法和特性说明

作者:访客发布时间:2024-01-04分类:程序开发学习浏览:100


导读:Kotlin介绍Kotlin由捷克的一家软件公司--大名鼎鼎的IDE厂家JetBrains开发,构思于2010年,并与2011年7月推出了Kotlin项目,5...

位于俄罗斯圣彼得堡的 科特林岛屿

Kotlin 介绍

Kotlin 由捷克的一家软件公司 -- 大名鼎鼎的 IDE 厂家 JetBrains 开发,构思于2010年,并与2011年7月推出了Kotlin项目,5年之后2016年 Kotlin v1.0 诞生,2017年Google I/O大会上宣布将为Kotlin提供最佳支持。

Kotlin 语言借鉴了其他众多编程语言的优势(如Java、C#、JavaScript等),支持操作符重载,函数式编程、lambda编程和协程等,JetBrains 致力于将Kotlin打造成可替代Java的编程语言。 Kotlin 是面向 JVM的静态编程语言,在编译期确定了数据类型。Kotlin兼容 Java 6(包含)及之后的Java版本,可以与Java代码很容易的实现互操作。

Kotlin可以用来做什么

Kotlin 支持用于服务端功能开发、Web前端、Native原生开发、Android开发、多平台移动端,除了上述部分,Kotlin 还支持编写Gradle脚本,定义 DSL(特定领域语言如SQL、XML)等

Kotlin 主要特性

1.更简洁

1.1 Kotlin中的隐式实现

为了能够写出简洁的代码,Kotlin在语法方面做出了改进,并且丰富的库函数使得调用变得更简单。接下来将从几个方面举例说明:

  • Kotlin隐式地实现了 Getter/Setter访问器,数据类(Data class)中默认实现了toString/hashCode/equals等模板函数,如下:

Kotlin 实现模板函数

data class Book(val name: String) {
    //data class 是数据类,已经隐式地实现了 equals、hashCode、toString
    val authors: List<String?>? = null
    var price: Double = 0.0 //使用默认的getter/setter
    get() = field + 1 //自定义模板函数getter
    set(value) {   //自定义模板函数 setter
        field = value - 1
    }
}
public class Book {

    private String name;
    private String author;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Book book = (Book) o;
        return Objects.equals(name, book.name) && Objects.equals(author, book.author);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, author);
    }

    @Override
    public String toString() {
        return "Book{" +
            "name='" + name + '\'' +
            ", author='" + author + '\'' +
            '}';
    }
}

1.2 集合的函数式API让操作集合显得更加简单

  • filtermap API,前者会遍历集合元素,返回符合给定的表达式条件的元素,返回值是新的集合;后者在遍历集合元素的同时,将给定的函数作用于每个集合元素,然后得到新的集合:

    val list = listOf(5,6,2,20,1) //初始化一个集合
    val newList = list.filter{it % 2 == 0} //filter 
    newList.forEach{
      println(it) //输出 6、2、20
    }
    
    val newList = list.map{it * it} //map
    newList.forEach{
     println(it) //输出 25、36、9、4、400、1
    }
    

Kotlin 中还有用于条件判断的集合函数式API,如 all、any、count、find等。有可以把集合按条件转换分组的 groupBy。还有处理嵌套集合的flatMap和**flatten。**Kotlin标准库中实现了对这些API的支持,使得调用看上起极其简单,但这些API的滥用带来的后果更容易被忽视。下面将对提及的集合函数式API做出解释:

  • all 用于判断集合中是否所有元素都符合某个条件;

  • any 判断集合中是否存在任意一个复合条件的元素;

  • count 返回符合条件的元素数量;

  • find 函数找到集合中第一个符合条件的元素返回,若没有找到则返回null;

  • groupBy 按照一定的特征将集合分组,结果将是map类型;

  • flatMap (平铺),将集合中每个元素做变换,之后将元素的子集合合并成新的集合返回,相当于 map + flatten;

  • flatten (平铺), 不再对集合元素做变换,仅合并所有的子集合;

1.3 Kotlin 支持Lambda表达式

Kotlin 天生就支持lambda表达式,在Android中假如我们需要为某个组件设置事件监听用于接收用户的触摸事件,如果是Java 8之前,需要传入一个对象或者匿名类对象,但使用Lambda则更加直接,如下所示:

Lambda 能够很方便地去除重复代码,尤其在Kotlin中,Lambda与集合的函数式API、扩展函数以及高阶函数的结合能够使编码更加高效简洁。

1.4 其他

";"作为一句Java代码的结尾,Kotlin并不支持,同时创建对象时也可以省略 new 关键字

2. 更安全

为了避免程序出现某些类型的错误(如类型安全、空指针等),Kotlin提供了诸如自动类型推导、类型判断和转换、可空和非可控类型变量以及空值调用的诸多方式,如下:

2.1 类型检查与转换

类型检查与转换,使用 关键字 "is" 检查属性的类型,如果属性和目标类型一致,类型检查的同时属性会转换为目标类型的成员。而在Java 中常见的 ClassCastException 是因为在转换类型的时候忘记了检查数据类型是否合适。除了使用 "is" ,Kotlin 还提供了关键字 “as” 用来支持安全的类型转换:

2.2 空值安全与安全调用

空值安全,Kotlin 的类型系统提供了可空类型非空类型来减少NPE(NullPointerException)的发生,此外还提供了一些空值的安全调用方法。

  1. 可空类型在定义时需要在类型之后加上 “?”,表示该属性可赋空值

  2. 非空类型定义时不能在类型后追加 “?”,定义时若未声明类型但赋值内容非空,视为非空类型

  3. 安全调用 “?.”,Kotlin 强制要求在调用可空类型时,必须使用安全调用符,“?.”调用空值引用时实际上并没有发生调用,只要可空类型引用值非空时才会执行调用;

  4. Elvis 操作符 “?:”,Elvis 操作符左侧表达式非空,则返回其左侧表达式,否则使用操作符右侧的表达式;

  5. 非空断言运算符 “!!”,“!!”将任何值转换为非空类型,若值为null则抛出NPE异常

3. 与 Java 互操作

Kotlin 可以很方便地与Java互相调用,IDE 工具还为Java代码转换成Kotlin提供了便利,此外Java中的基础类型、装箱原始类型(如Integer)、非原生的内置类型(如Object、String、Cloneable..)等在Kotlin中都有特定的类型映射,这些类型映射发生在编译期间。 另一方面Java几乎可以无缝操作或创建Kotlin的实例,但仍然有差异值得注意。

3.1 Kotlin 调用 Java

Getter/Setter

Getter/Setter Kotlin 隐式实现了属性访问器,所以Java代码在访问Kotlin的属性时会通过getter/setter方式访问。此外Kotlin 将遵循Java Getter/Setter(以get开头且无参数,以set开头存在一个参数,Boolean类型以 is开头) 约定的方法视为属性。如果Java中只有一个 setter访问器,Kotlin将不视其为属性;

关键字转义

Kotlin的 is、in、object等关键字,如果在Java中使用了Kotlin的关键字作为函数名,Kotlin代码中需要为关键字添加 `` 进行转义。

例如:

foo.`is`()
空安全与平台类型
  1. Java中的所有引用都可能为null,因此Kotlin对此放宽了限制。Java中声明的类型在Kotlin中被认为是平台类型,IDE或编译器在展示平台类型时会在类型之后添加 "!",如图所示:

    可空类型表示法:

    Java中普通字符串变量:

    Kotlin中的平台类型:

  2. 具有可空性注解的Java类型并不表示为平台类型,而是表示为实际可空或非空的 Kotlin 类型,注解的类型包含JetBrains/Android等可空和非空注解;

  3. 除以上区别,Kotlin和Java在泛型、反射、数组、可变参数、访问静态成员...等方面也有不同,详细可参考 Kotlin 中调用 Java - Kotlin 语言中文站

3.2 Java调用Kotlin

1. 属性

Kotlin属性经过编译后在Java中会以Getter/Setter方式被调用,即通过访问器调用,而私有字段编译后与属性名称相同,但限于拥有幕后字段的属性(简书 - 幕后字段与幕后属性)。

2.包级函数

这里的包级函数指的是在Kotlin文件(如App.kt)中定义的函数,这些函数在Java中调用时会以 Kotlin文件的 文件名 + Kt结尾的Java类的静态函数出现,还可以使用 @file:JvmName 指定编译后的类名。如果在不同文件中指定了相同的类名,还需要使用 @JvmMultifileClass 将其合并成一个统一的外观类,该类包含了这些文件中所定义的函数。

指定类名并且合并多个相同类名的文件:

以静态函数方式调用:

3.实例字段

Kotlin的属性通常以getter/setter形式被Java代码访问,如果要以字段形式被访问,需要用@JvmField 修饰该属性。

4.静态
  • 在具名对象或伴生对象的Kotlin属性,使用 @JvmField 修饰后使其成为相应类的静态字段
  • 在具名对象或伴生对象的延迟初始化属性(lateinit var ..),在Java中同样是以静态字段形式出现
  • 在类中或顶层中以 const 声明的属性,在Java中会成为静态字段
  • Java除了将包级函数作为静态函数调用,具名对象和伴生对象的函数同样可以成为静态函数,但需要声明 @JvmStatic
5.可见性
  • kotlin中 private 转换成Java之后保持不变,顶层 private 经转换后将成包级局部声明
  • protected 转换之后保持不变,但Kotlin中无法访问相同包下其他类的受保护成员
  • public 保持不变
  • internal 在Kotlin中internal限制仅对相同module下的成员可见,转换成Java以后会成为public。
6.Nothing

Nothing 类型是个特殊存在,运行时没有实例存在,表示一个无法达到的代码位置。例如作为返回值使用,除非在可空状态时可返回null,否则就只能抛出异常。和Java中的 Void类似,Nothing?Void 等价,只接受null,而Nothing类型返回值表示永远不会返回。Nothing在Java中没有可转换的类型

注:除了以上差别,可参考官方文档 查看更多细节

4.支持函数式编程

与传统面向对象编程语言相比(Java 8也引入了函数式编程),Kotlin增加了对面向函数编程(点击了解函数式编程)的支持,函数式编程中,函数作为一等公民,意味着函数可以被绑定到变量、作为参数传递或从其他函数返回。

在面向函数编程范式中,“一切都是函数”,函数是第一等公民,允许函数作为普通数据进行传递,它的思想就是把计算过程尽量写成一系列嵌套的函数调用。函数式编程语言提倡在有限的几种数据结构之上(如List/Map/Set),运用函数组合自底向上构建构建世界。

下面将介绍函数式编程中两个重要的概念,lambda 表达式和高阶函数。

4.1 函数式编程之 - Lambda表达式

Lambda 表达式本质上是传递给其他函数的一小段代码,Kotlin标准库中大量地使用了lambda表达式。试想一下如何去传递一个函数或者代码块呢,在Android中可以将函数或代码块放入Runnable任务交给其他函数,或者在为View组件设置事件监听时传入一个接口类型的匿名对象,该匿名对象内部包含了允许被调用的函数,如下:

view.post(object: Runnable() {
   override fun run() {
     //your code  函数或代码           
   }
 })

view.setOnClickListener(object : View.OnClickListener {
  override fun onClick(v: View?) {
    //收到事件回调后,do something
  }
})

上面的示例把函数或代码块交给了两个匿名类型对象,然后将匿名对象再交给的函数。如果使用Lambda的方式会更加简单,直接传递函数即可:

view.post { /**your code  函数或代码**/ }
view.setOnClickListener { /**收到事件回调后,do something**/ }

使用Lambda表达式 后省略了匿名类对象,也省略了回调函数,只需要实现对应的代码就可以。需要注意的是,Lambda表达式支持在调用接收函数式接口(仅有一个抽象方法的接口,但允许有多个非抽象成员)作为参数的方法时使用lambda,示例中Runnable 只有一个抽象函数 run,View.OnClickListener 也只有一个抽象函数 onClick。

4.1.2 Lambda 表达式语法

{ x : Int, y : Int -> x + y } Kotlin 的lambda 表达式始终使用花括号包围,“x : Int, y : Int” 部分是表达式参数,“->” 右侧是函数体。Lambda可以存储在一个变量中,把这个变量当做普通函数一样对待,调用时传入对应的参数。

//声明表达式并赋值给变量
val lambda = { x: Int, y: Int -> x * y }

lambda(9, 9) //调用lambda表达式

Kotlin中有语法约定,若lambda表达式是函数调用的最后一个实参,它可以放在括号的外面:

view.post() { /**your code  函数或代码**/ }
//这里lambda表达式是post函数唯一的实参,因此可以把表达式放在括号外面

Kotlin 还规定,当lambda表达式是函数唯一的实参时,括号也可以省略:

view.post { /**your code  函数或代码**/ }

虽然Lambda使用很方便,需要注意的是本质上lambda表达式每一次运用都会额外创建一个类,如果它捕获了某个变量(例如在onClick函数内修改表达式所处的函数的变量时,会生成一个该变量的包装类),则每次调用还会创建一个新的对象。

     //编译之前的代码
     var kt = 2
     view.setOnClickListener {  doSomeThing(kt) }

      //2 被编译后代码
      final IntRef kt = new IntRef(); //生成变量的一个包装类对象
      kt.element = 2;
      //还是创建了匿名类对象
      view.setOnClickListener((OnClickListener)(new OnClickListener() {
         public final void onClick(View it) {
            MainActivity.this.doSomeThing(kt.element);
         }
      }));

4.2 高阶函数

按照定义,高阶函数是指以另一个函数作为参数或返回值的函数。Kotlin中,函数可以使用lambda或函数引用表示,所以任何以lambda或者函数引用作为参数的函数,或返回值为lambda或函数引用的函数,或两个条件都满足的函数都是高阶函数。

4.2.1 函数类型

变量有其类型,Kotlin中函数也有类型。将一个lambda表达式保存在局部变量,则依赖于Kotlin的类型推导,例如:

val sum = {x:Int, y:Int -> x * y} 
val act = {println(100)}

上面lambda表达式的声明类型如下:

val sum :(Int,Int) -> Int = {x,y -> x * y}
val act :() -> Unit = {println(100)}

声明上面的lambda表达式的类型时, 第一行的表达式有两个Int类型参数,并且有Int类型返回值。第二行表达式没有声明参数和返回值,Unit表示函数不返回任何有用的值,Unit类型返回值在声明普通函数时可以省略,但是函数的类型声明需要这样一个显示的返回类型,因此这种场景下不能省略。

声明函数类型需要将函数的参数放在括号内部,紧接着是一个箭头 “->”和函数返回类型

(Int , String) -> Unit
(Int , Int) -> Int

此外,函数类型的参数、返回值允许标记为可空类型:

(Int ?, Int?) -> Int ?

声明一个可空类型的函数类型,需要把整个函数类型用括号包围,并在括号后加一个 “?”

var funOrNull : ()
4.2.2 函数类型参数 & 调用函数类型参数

函数可以接收函数类型的参数,使用方式如下:

fun performReq(uri:String, callback : (code:Int,content:String) -> Unit){
//...
}

上面的函数接收两个参数,字符串和一个函数类型参数,callback是函数类型参数的名称,并且函数类型参数有两个参数,没有返回值。

调用函数类型参数和调用普通函数一样:

fun performReq(uri:String, callback : (code:Int,content:String) -> Unit){
    //...
    callback(3,uri.getPath()) //调用函数类型参数
}

除了函数类型参数,Kotlin中高阶函数还能定义函数类型的返回值,返回一个函数: 假如计算运输费用时,需要根据运输的方式作为计算的前提,可以定义一个函数根据条件选择恰当的逻辑并将其作为函数返回:

enum class Delivery{STANDARD,EXPEDITED}
class Order(val itemCount:Int)

fun getShippingCostCalculator(delivery:Delivery): (Order) -> Double{
      if(delivery == Delivery.EXPEDITED){
        return {order -> 6 + 2.1 * order.itemCount}
      }
      return {order -> 1.2 * itemCount}
}

getShippingCostCalculator 函数返回一个函数,该函数接收的参数类型为Order,并返回Double类型结果,getShippingCostCalculator的 return 表达式返回函数体,箭头左侧 order是实参,右侧是函数体。return 表达式可以返回 lambda表达式、一个成员引用、函数类型的局部变量或其他函数类型的表达式。

函数类型声明的背后实际上是普通的接口,一个函数类型的变量是FunctionN接口的一个实现。Kotlin标准库定义了一系列接口,对应不同数量参数的函数,Function0(没有参数的函数)、Function1<P1,R>(一个参数的函数)...,每个接口都定义了一个invoke函数,调用invoke方法,就会执行函数。

4.3 函数式编程小结

函数式编程是一种编程范式,更是一种编程思想。面向对象编程中把数据或行为封装在特定的数据结构,而数据结构又可以对外隐藏其实现细节,只暴露接口 。而函数式编程中数据和函数是分离的,它的核心是函数与函数的组合与交换。函数式编程涉及的内容除了lambda编程和高阶函数之外,还有很多需要学习,例如闭包、内联函数、函数作用域等,限于篇幅此处不再赘述。

Kotlin 基础语法介绍

1. Kotlin 类型系统

1.1 基本数据类型和其他类型

Java 中的基本数据类型和引用类型是分开的,Java的基本数据类型还有对应的包装类型,但是Kotlin并不区分它们,Kotlin中可以对一个数字类型的值调用方法。Kotlin尽可能高效地表示这些数据类型,多数情况下变量、属性、参数和返回类型会尽可能被编译成Java中的基本类型,但是泛型除外,泛型类型参数的基本类型会被编译成对应的java包装类型。

  • 整数类型:Byte、Short、Int、Long
  • 浮点数类型:Float、Double
  • 字符类型:Char
  • 布尔类型:Boolean

此外,Kotlin中还有其他一些类型:

  • Any (所有非空类型的超类型,对应的可空类型是 Any?),相当于Java中的超类型 Object , Any是引用类型,所以在被赋予数字时会被装箱;

  • Unit 类型完成了Java中void一样的功能,可以用作函数返回类型。不同的是Unit是一个完备的类型,可以作为类型参数使用;

  • Nothing类型,表示函数永不返回,该类型没有任何的值,只有当做函数返回值或被当做泛型函数返回值类型才会有意义。

Unit 类型使用:

interface Processor<E> {
    fun process(): E
}

//泛型指定了Unit类型
class NoResultProcessor : Processor<Unit> {
    override fun process() {
        //Unit 返回类型,并不需要return语句,也不需要此时显式的声明 Unit
    }
}

1.2 变量

Kotlin中,变量的声明可以使用两个关键字 valvar,前者(value)表示只读,即不可变引用,val 变量一经初始化就不能再次赋值,对应Java中的final变量;后者(variable)可读可写,是可变引用,对应于Java中的普通非final变量; Kotlin变量声明方式是 **关键字 + 变量名 [: 变量类型][?] = 初始化值 ,**因为Kotlin支持类型推导,变量类型可以因此省略,如果指定了 "?"(可空类型,表示变量可以被赋null值),否则变量在被赋值时,如果遇到空值或可空类型的变量,编译器将提示报错。

val valueName : String? = null
var variableName : String = ""

1.3 属性

类的概念是把数据和处理数据的代码封装成单一的实体,而属性就是指其中表示数据的字段。在Java中,这些字段还经常是私有的,然后提供getter/setter访问器,而Kotlin中属性的定义和变量保持一致,在声明属性的同时还声明了对应的访问器(只读属性只有getter)。

1.4 自定义访问器

有的时候我们需要访问属性的时候做一些验证或者其他操作,就需要自定义访问器。Kotlin自定义访问器需要在属性下方手动实现,如下: image.png

需要注意的是 "field"标识符在getter/seter中用于访问属性,任何使用默认getter/setter或自定义访问器中使用了field的,编译器会为属性生成后端域变量。 在Java中访问这些属性时仍会以常见的getter/setter方式访问 get/set/is + 字段名,在Kotlin中除了属性名称,其他部分均已省略。

1.5 集合与数组

Kotlin 以Java集合库为基础构建,通过扩展函数增加特性来增强它,以下将介绍如何在Kotlin中使用集合以及它的差异性:

1.5.1 创建集合

Kotlin中提供了一组创建集合的API如下:

集合类型只读可变
ListlistOfmutableListOf、arrayListOf
SetsetOfmutableSetOf、hashSetOf、linkedSetOf、sortedSetOf
MapmapOfmutableMapOf、hashMapOf、linkedMapOf、sortedMapOf
1.5.2 只读集合与可变集合

一个重要的特征是Kotlin将访问集合数据的操作和修改集合数据的接口拆分,使用kotlin.collections.Collection 接口可以遍历集合的元素,获取大小并判断其中的元素,若想要移除或修改某个集合元素就要使用kotlin.collections.MutableCollection。这样的好处是假如你的函数的入参使用了Collection接口,那么向外传递的信息就是该函数并不会修改集合的数据,此外只读集合不被允许作为函数的可变集合类型参数传递。

Kotlin 集合接口层级结构

2.类与对象

Kotlin中对类、抽象类、接口的声明并没有改变原有的方式,仍然沿用 class、abstract class、interface 等关键字,但是Kotlin中类默认是final类型,不可继承,同样的函数也是final类型,如果类想要允许被继承,函数想要允许被重写,它们必须都使用open关键字修饰,Kotlin认为如果基类没有提供明确的实现规则(哪些方法可以重写,如何重写),实现者可能会有按照基类作者预期之外的方式重写方法的风险。此外Kotlin中的可见性修饰符默认为 public

2.1 类的构造

Kotlin 使用 constructor 和 init 两个关键字构造对象,前者用于声明构造函数,关键字可以被可见性修饰符修饰(比如private constructor 表示这个类将不允许被其他类实例化),还可以在其后的括号中声明构造参数;后者表示一个初始化语句块,初始化语句块可以包含类被创建时的执行代码,并且会与主构造方法一起使用。此外Kotlin 和Java一样允许一个类有多个构造函数,但Kotlin 将定义在类名之后的构造函数视为主构造函数。

class Person constructor(val name: String) {

    init {
        //do something,与主构造函数一起使用,由于主构造函数的语法限制,
        //不能包含初始化语句,所以在这里执行一些操作
    }

    constructor(age: Int) : this("") {
        //从构造函数,如果定义了主构造函数,就必须有从构造函数使用 this关键字调用主构造函数
        this.age = age
    }

    constructor(age: Int, address: String) : this(0) {
        //从构造函数
        this.age = age
        this.address = address
        println("构造函数2参数 执行")
    }

    private var age: Int = 0
    private var address: String? = null
}

Person(10, "杭州市") //创建一个类的实例,不需要使用 new 关键字
  1. 主构造函数允许使用 val/var 关键字在其中定义属性,但从构造函数不允许那样做

  2. 如果定义了主构造函数同时有多个从构造函数,需要确保调用任一从构造函数创建对象时,主构造函数都要被调用,可以通过在从构造函数后 : this(参数列表,...) 的方式

  3. 从构造函数后 : this(参数列表...) 也可以用于调用其他构造函数(非主构造函数)

  4. 如果一个类没有声明任何构造函数,默认地会拥有一个无参主构造函数

  5. 如果主构造函数没有注解或可见性修饰符,可以省略掉 constructor 关键字

  6. 构造函数中如果声明了属性或参数,建议使用默认值从而避免实现多个构造函数

2.2 类与继承

Kotlin 不再使用关键字 “extends”,想要在Kotlin中继承一个类或实现一个接口,格式如下: **class 类名 : 父类/接口**

interface Ability {
fun talk() //接口和接口函数并不需要open修饰,因为它们原本就是非final类型å
}

open class Human { //open关键字表示该类可以继承
 open fun move() {} //open修饰的函数可以被重写
 open fun think() {}
}

class People : Human(), Ability { //people类继承了Human,同时实现了Ability接口
 override fun talk() {}
}

注:继承父类时,需要提供一个父类的构造函数,如果父类中未定义构造函数,则使用默认构造函数。

2.3 数据类、密封类

2.3.1 数据类(data class)

和Java一样,Kotlin类也有许多开发者想要重写的函数:toString、equals和hashCode,这些方法属于模板方法但是在Java中却要耗费一番功夫。使用数据类则可以在Kotlin中省略掉很多不必要的麻烦,它将默认生成这些模板函数,定义数据类索要做的就是 用 关键字 data 修饰类,并且定义一个至少含一个构造参数的主构造函数。

data class User(val name: String, val age: Int) {
    val address:String? = null
}
2.3.2 密封类(sealed class)

密封类用于定义受限的类继承结构,密封类要求定义一个父类且必须声明 seald 关键字,该类所有的子类必须在父类的内部定义。密封类特别适合一种使用场景,例如当使用 when 表达式(像是Java中的switch分支控制语句,但更强大)时,使用密封类作为控制语句的条件分支,当密封类所给定的条件发生变化时(例如增加或删除了一个子类条件),具有返回值的when表达式将编译失败并提示哪里出了问题。

sealed class AA {
    class A1(val value: Int) : AA()
    class A2(val value: Int) : AA()
    class A3(val value: Int) : AA()
}

//when表达式使用密封类,因为穷举了所有的条件,不再需要 else 分支
fun switch1(a: AA): Int = when (a) { 
    is AA.A1 -> a.value
    is AA.A2 -> a.value
}

//when表达式默认必须有else分支
fun switch2(case:Int):Int = when(case){ 
    1 -> ...
    2 -> ...
    ...
    else -> ...
}

密封类 AA的所有子类必须是其嵌套类,当增加或减少子类时,如果when表达式未更新,编译器将提示出错。

2.4 object

Kotlin 中将在多种场合中出现 object 关键字,object 将对象的声明和实例化合二为一,它的使用场景如 对象声明、伴生对象和对象表达式。

2.4.1 对象声明

Kotlin中对象声明可以以一句话定义一个类和一个该类的变量。对象声明的类变量可以拥有属性、方法以及初始化语句块,还能继承或实现接口,但不允许构造函数,因为对象声明在定义时就立即创建了。Kotlin 中访问对象声明的变量时,直接使用 类名并调用函数或属性即可,和Java中的静态函数或静态属性一样。Java中访问对象声明变量,需要在类名之后追加一个 INSTANCE,然后再调用其函数。

object Func {
    private const val value = 1
    fun addValue(v: Int) = value + v
}

Func.addValue(2) //Kotlin中调用
Func.INSTANCE.addValue(2); //Java 中调用
2.4.2 伴生对象

在类中定义的对象之一可以使用 companion关键字 和 object 标记,由companion标记的对象称为外部容器类的伴生对象,可以通过容器类名称访问该对象的方法和属性,但在Java中需要在容器类之后加上伴生对象的类名称。

在Kotlin中类不能拥有静态成员,Kotlin提供的顶层函数 和 对象声明在大多数情况下可以替代Java中静态方法,但是顶层函数不支持访问类的私有成员,伴生对象此时就可以发挥其作用了。

伴生对象声明方式和调用:

class Person constructor(val name: String) {

    private var age: Int = 0
    private var address: String? = null
    private fun contact() {
        println("private 函数 contact 被调用")
    }

    //声明伴生对象,伴生对象可以访问容器的私有成员
    companion object {
        fun visitPerson() {
            val p = Person("张三")
            println("访问容器类属性 address:${p.address} 调用函数contact:${p.contact()}")
        }
    }
}

Person.visitPerson() //访问伴生对象,使用容器类即可
2.4.3 对象表达式

object 关键字除了声明对象外,还能声明匿名对象。Kotlin匿名对象可以实现多个接口或不实现接口,并且匿名对象也不是单例,每次表达式执行都会创建一个新的对象实例。与Java不同的是,Kotlin匿名对象在访问创建它的函数中的变量时,并没有限制仅能访问final类型变量,还可以在对象表达式中对变量的值进行修改。

var index = 0
view.setOnClickListener(object : View.OnClickListener { //对象表达式
    override fun onClick(v: View?) {
        index++ //修改外部变量的值
    }
 })

3.控制流

3.1 if 表达式

Kotlin 中 if 表达式 会返回一个值,if 的分支可以是代码块,代码块最后一行可以作为返回结果。

fun max(a: Int, b: Int) = if (a > b) a else b

val max = if (a > b) {
        //do something
        a
    } else {
        //do something
        b
    }

3.2 when 表达式

when 表达式取代了Java中的switch语句,when 将其参数和所有分支条件按顺序对比,直至找到满足条件的分支,when表达式必须有else分支,除非编译器能够检测出已覆盖所有的情况。when表达式分支语句可以对应一个代码块,多个分支条件可以放在一起,中间用 “,”隔开,when表达式支持使用 “is”关键字判断参数是否是某种类型。

fun case(value: Int) = when (value) {
    1 -> println("条件1")  
    2, 3 -> println("条件2") //多个条件一起判断,使用 “,”隔开
    in 4..5 -> println("在4 ~ 5的范围内") // in 表示是否在某个区间范围
    else -> {
        println("没有匹配的条件")
    }
}

fun switch(obj: Any) = when (obj) {
    is String -> {  
        println("String 类型")
    }
    //is 判断是否是某种类型,如果是将自动转换到该类型
    is Person -> {
        println("person name:${obj.name}")
    }
    else -> {
        throw IllegalArgumentException("exception")
    }
}

3.3 for 循环

for 循环可以为任何提供 iterator 的对象进行遍历,for 循环还支持使用区间表达式:

val list = arrayListOf(1, 2, 4, 5, 6, 7, 213, 4, 12, 67, 7, 5, 0, 9)

    //遍历集合
    for (it: Int in list) { 
        println(it)
    }

    //遍历 0 ~ 100 的数字区间
    for (it in 0..100) { 
        println(it)
    }

    //遍历一个数字区间,倒序至0,并非逐一遍历,每次跨两个元素
    for (it in 100 downTo 0 step 2) {
        println(it)
    }

4.认识函数

Kotlin 中函数的定义方式:

可见性修饰符  fun关键字 函数名称(参数1:参数1类型, ..) : 返回值类型{ //函数体 }

Kotlin中函数支持命名参数参数默认值,前者是当调用Kotlin函数时,可以显式地标明一些参数的名称,避免混淆,提高可读性;后者指的是在定义函数时就为某些参数预设值,在参数之后使用 “=”赋值,这样即使调用时不传该参数也没有问题,该参数将使用早已设定的默认取值,好处是可以省略一些的重载函数。 函数定义

4.1 顶层函数

Kotlin 支持将函数在代码文件的顶层实现,它不从属于任何类,调用时仅需import该方法,不用额外包一层类。Java中调用顶层方法时使用 函数所在文件名 + "Kt" + "." + 函数。

//使用注解为该文件起名
@file:JvmName("StrFunctionSet")
package com.dan.kotlinbox.utils

//1 顶层函数
@JvmName("strIsBad") //使用注解为函数改名
fun isNullOrBlank(string: String?): Boolean = string.isNullOrBlank() ||
        "null" == string || "NULL" == string || "Null" == string

class StringUtil {

    companion object {
        //2 传统的工具类 + 静态函数
        @JvmStatic
        fun isNullOrBlank(string: String?): Boolean =
            string.isNullOrBlank() ||
                    "null" == string || "NULL" == string || "Null" == string
    }
}

//Java调用 - 使用更改后的文件名
StrFunctionSet.StrFunctions("null"); 
//Java调用 - 使用未修改的文件名
StringUtilKt.StrFunctions("hey");
//Kotlin调用,仅导入了位于包下的方法
isNullOrBlank("null")

4.2 局部函数

局部函数是指定义在函数中的函数,目的是解决一些常见的代码重复问题。例如我们可能会把一个大型函数拆分成诸多小的函数代码块以重用它,或者提取这些方法组合成一个内部类以保持结构。Kotlin提供的局部函数作为另一种摆脱重复的方式值得一试:

//函数需要对这些字段做验证,如果使用局部函数,我们可以把验证部分提取出来,避免重复的代码
fun saveUser(p: Person) {
    if (p.name.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${p.id};empty name")
    }
    if (p.address.isNullOrEmpty()) {
        throw IllegalArgumentException("Can't save user ${p.id}; null or empty address")
    }
    //save action
}
//使用局部函数改进之后
fun saveUser(p: Person) {
    fun validate(value: String?, fieldName: String?) {
        if (value.isNullOrEmpty()) {
            throw IllegalArgumentException("Can't save user ${p.id}; null or empty $fieldName")
        }
    }

    validate(p.name, "name")
    validate(p.address, "address")
}

4.3 扩展函数

Kotlin 能够扩展一个类的新功能而无需继承该类或者使用像装饰者这样的设计模式。 这通过叫做 扩展 的特殊声明完成。例如,Kotlin 允许为不能修改的来自第三方库的类扩展函数和属性,就像目标类原本就有的函数和属性一样。

声明一个扩展函数,我们需要用一个 接收者类型 也就是被扩展的类型来作为函数的前缀,还需要一个接收者对象可用this指代,位于函数体内。示例如下: 为ArrayList 集合添加一个swap函数

fun ArrayList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // “this” 指的是列表本身,即接收者对象
    this[index1] = this[index2]
    this[index2] = tmp
}

val <T> ArrayList<T>.lastIndex: Int //扩展属性,为ArrayList类型扩展了新的属性
    get() = size - 1 //扩展属性必须定义getter
  • ArrayList.swap 前半部分(ArrayList)为该扩展函数的接收类型
  • 扩展函数内部 this 指代接受类型对象自身

扩展函数是静态分发的,并不能真正的修改他们所扩展的类。通过定义一个扩展,你并没有在一个类中插入新成员, 仅仅是可以通过该类型的变量用点表达式去调用这个新函数。 调用的扩展函数是由函数调用所在的表达式的类型来决定的, 而不是由表达式运行时求值结果决定的。

open class Shape

class Rectangle: Shape()

fun Shape.getName() = "Shape"

fun Rectangle.getName() = "Rectangle"

fun printClassName(s: Shape) {
    println(s.getName())
}    

 //输出的内容是 Shape,getName扩展函数与其接收者类型绑定。
printClassName(Rectangle())

4.4 内联函数

内联函数使用 inline 关键字声明,它修饰的函数体是内联的,函数体会被直接替换到函数被调用到的地方,而不是正常调用。可以使用内联函数解决lambda的额外开销,lambda表达式每次都会生成新的类,通过将lambda内联,可以避免创建新的对象。

 inline fun doTask(run: () -> Unit) {
        println("doTask -> 任务开始")
        run()
        println("doTask -> 任务结束")
    }

    fun invokeTask() {
        println("invokeTask -> 准备任务开始")
        doTask {
            for (i in 100 downTo 0) {
                println(i)
            }
        }
        println("invokeTask -> 任务完成")
    }

上面的示例展示了一个内联函数 doTask,接收一个函数类型参数,可以是lambda表达式,invokeTask 函数会调用内联函数 doTask,这段代码实际上会是 doTask函数体内部的代码被复制到 invokeTask 函数内部,同时被内联的还有lambda函数体 for循环语句:

 public final void doTask(@NotNull Function0 run) {
      int $i$f$doTask = 0;
      Intrinsics.checkNotNullParameter(run, "run");
      String var3 = "doTask -> 任务开始";
      System.out.println(var3);
      run.invoke();
      var3 = "doTask -> 任务结束";
      System.out.println(var3);
   }

   public final void invokeTask() {
      String var1 = "invokeTask -> 准备任务开始";
      System.out.println(var1);
      int $i$f$doTask = false;
      String var3 = "doTask -> 任务开始";
      System.out.println(var3);
      int var4 = false;
      int i = 100;

      //这段代码在源码中体现的是一段lambda表达式,此时没有生成新的类
      for(boolean var6 = false; i >= 0; --i) {
         System.out.println(i);
      }

      var3 = "doTask -> 任务结束";
      System.out.println(var3);
      var1 = "invokeTask -> 任务完成";
      System.out.println(var1);
   }

内联的注意事项

一般来讲,参数如果被直接调用或作为参数传递给另一个 inline 函数,它是可以被内联的。否则编译器会禁止参数内联并给出错误信息。根据这个规则,并不是所有Lambda都可以被内联,如果lambda表达式参数在某个位置被保存起来,以待后续使用,这样的lambda将不会被内联。

内联函数不仅节省了函数调用的开销,同时针对lambda参数的优化,避免了生成新的对象。但是使用内联时应注意代码的长度,如果要内联的函数很大,将它的字节码拷贝到每一个调用点都会极大地增加字节码长度。

本文总结

本文主要围绕Kotlin的特性和基础语法,并对比Java语言,针对Kotlin做了并不深入的介绍,但是Kotlin编程的内容是广泛的,如Kotlin的并发框架协程委托、操作符重载、IO流、反射和泛型等并未涉及,此外Kotlin对原生开发、多平台编程、web开发等都不是本文的关注点,如感兴趣可关注官方文档学习。后续将继续补充 Kotlin 相关内容。

参考资料

  • 《Kotlin 实战》 Dmirty Jemeroy & Svetlana Isakova《电子工业出版社》
  • Kotlin 语言中文站
  • 面圈网 - Kotlin极简教程
  • 解道JDON - 函数式编程与面向对象编程比较

本文源自对我的语雀笔记 [编程语言 - 初步认识](www.yuque.com/infinite_kn… 《编程语言 - 初步认识 Kotlin》) 修订后发布。

文末推荐

推荐一款 Markdown 写作软件: MarkText ,由于下载过程可能卡慢,这里奉上阿里云盘的下载链接 windows版本 marktext-setup.exe;此外可关注Markdown 入门基础 | Markdown 官方教程。


标签:第一篇语法特性基础Koltin


程序开发学习排行
最近发表
网站分类
标签列表