Kotlin语法基础

kotlin创建

如果要新建一个支持kotlin的Android项目,在创建工程时选择kotlin语言即可。

此时项目中两个build.gradle文件会多了kotlin依赖代码:

项目根目录下build.gradle:

buildscript {
    ext.kotlin_version = 'xxx'
    repositories {
        google()
        
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:xxx'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

    }
}

app目录下build.gradle

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'
...

android {
    ...
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    ...
}

如果希望在现有项目上支持kotlin,即在项目中添加上述依赖即可。

一、变量与常量

image

1.1 变量

Kotlin变量声明与Java存在较大区别,需使用varval关键字

  1. var:用此关键字声明的变量相当于Java中普通变量
  2. val:用此关键字声明的变量相当于Java中用final修饰的变量

变量声明格式

// 关键字 变量名 : 数据类型 = xxx
var var_a: Int = 10 //等价于Java中  int var_a = 10;
//Kotlin可以自动推导数据类型
var x = 5   //自动推导其类型为Int,(快捷键显示ctrl + shift + p)

Kotlin具有类型推导机制,初始化时可以根据变量或常量右边的值来推导出其类型

Kotlin中默认访问的修饰符是public,且如果变量没有初始化,需要显示声明类型并指明类型是否可null

kotlin中语句不需要在行末加分号(加了也不报错)

可空变量

Kotlin中,所有的变量默认都是不允许为空,如果给它赋值null,会报错;但在有些场景需要其允许为空,如从服务端获取一个JSON数据并解析,这时候可以使用可空变量解除限制

如果我们不能确定一个属性或变量一定不为空,将其定义为可空变量

可空变量声明格式:

var/val 变量名 : 类型? = null/确定的值

class Test{
    var var_a : Int? = 0
    var var_b : Int = 12
    val val_a : Int? = null
    var_a = null
    var_b = null    //报错,不能为null
}

对可空变量调用可能引起空指针,kotlin提供一些方法进行相关处理

可空变量空指针处理

Kotlin中对可空变量的空指针的处理方法有:判空处理、?.处理、使用Elvis操作符

  • 使用前判空处理

    可以在调用可空变量前进行空判断

    var str : String? = null
    if (str == null){
        println("变量str为空")
    }else{
        println("str.length => ${str.length}")
    }
    
  • 使用?.处理

    即使调用前检查非空,也可能因为在多线程情况下被其他线程改成空。
    Kotlin通过?.解决,保证变量非空,线程安全。如果可空类型变量为null时,返回null

    var str : String? = null
    println(str?.length)   // 当变量str为null时,会返回空(null)
    
  • 使用let操作符
    let操作符的作用是用符号?.验证的时候忽略掉null情况

    val arrTest : Array = arrayOf(1,2,null,3,null,5,6,null)
    for (index in arrTest) {
        index?.let { println("index => $it") }    //1、2、3、5、6,忽略null的情况
    }
    
  • 使用Elvis操作符

    Elvis操作符表示屏蔽,安全的操作符,这种操作符有三种,分别是:?:!!as?。其优先级较低,比加减乘除运算符低,使用时最好记得加上括号

    ?:操作符

    对一个可空类型变量,如果变量不为空则使用,为空则使用设定好的默认值

    val testStr : String? = null
    var length = 0
    length = testStr?.length ?: -1    //testStr为null,返回默认值-1
    

    ?:一般与?.连用

    !!操作符

    使用可空变量后加上!!操作符,如果为空会抛出NullPointException异常

    val testStr : String? = null
    println(testStr!!.length) //抛出 NullPointException 异常
    

    as?操作符

    as表示类型转换,如果不能正常转换会抛出ClassCastException异常,使用as?则会返回null,不会抛出异常

    val num : Int? = "Kotlin" as Int  //抛出异常
    val num1 : Int? = "Kotlin" as? Int
    println("num1 = $num1")   //num1 = null ,不会抛出异常
    

延迟初始化属性

如果变量一定不为空,但无法立即赋值,例如findViewById()需要在onCreate之后才能调用。Kotlin提供了延迟初始化功能

延迟初始化属性特点:

  1. 使用lateinit关键字且必须是var声明的变量
  2. 不能声明于可空变量和基本数据类型,如Int、Float
  3. 必须在使用变量前赋值,不然抛UninitializedPropertyAccessException异常。
  4. 检测一个lateinit var是否已经初始化,可在属性应用上使用 .isInitialized
  5. 该修饰符只能用于类属性
private lateinit var mTabLayout : TabLayout
lateinit val a : Int // 报错,关键字必须是var,不能为基本数据类型

if (::mTabLayout.isInitialized) {   //使用前最好检测一下是否已经赋值,防止抛异常
    ......
}

延迟属性

延迟属性即在程序第一次使用到这个变量的时候初始化

延迟属性特点:

  1. 使用lazy{}高阶函数,不能用于类型推断,用于变量数据类型后,用by链接
  2. 只能是用val声明的变量
  3. 可以使用于类属性与局部变量
val name : String by lazy {
    "nihao"
}
//声明一个延迟初始化数组
val mTitles : Array by lazy {
    arrayOf(
        "hello",
        "world",
        "haha"
    )
}

1.2 常量

常量修饰定义格式

const val NUM = 6

常量的注意事项:

  1. const只能修饰val,不能修饰var
  2. 常量必须声明在对象(包括伴生对象)或者顶层中,因为常量是静态的。
  3. 只有基本类型和String类型可以声明为常量
// 1. 顶层声明
const val NUM_A : String = "顶层声明"

// 2. 在object修饰的类中
object TestConst{    const val NUM_B = "object修饰的类中"}

// 3. 伴生对象中
class TestClass{    
    companion object {
        const val NUM_C = "伴生对象中声明"    
    }
}

二、数据类型

image

在Kotlin中,需要牢记一点:所有东西都是对象

2.1 基本类型

数值类型

Kotlin中数字内置类型关键字与Java大致相同

类型 Kotlin Java
布尔 Boolean Boolean
字节 Byte Byte/byte
短整型 Short short/Short
整型 Int & Long int/Integer & long/Long
浮点型 Float & Double float/Float & double/Double
字符 Char char/Character
字符串 String String
var a: Byte = 2
var b: Short = 2
var c: Int = 2
var d: Long = 2L         //长整型由大写字母L标记,小写的l会报错
var e: Float = 2f        //单精度浮点型由小写字母f或大写字符F标记
var f: Double = 2.0  

进制数

Kotlin支持进制数,但不支持八进制数

var g = 0x0F            //十六进制数
var h = 0b00001011      //二进制数
var k = 123             //十进制数
println(" g => $g \n h => $h \n k => $k)
// g => 15 
// h => 11 
// k => 123 

数字分组

Kotlin支持分割数字进行分组,使数字常量更易读

val oneMillion = 1_000_000
println("oneMillion => $oneMillion")    //oneMillion => 1000000

装箱与数值比较

在Kotlin中,存在数字装箱,但不存在拆箱,因为Kotlin中没有基本数据类型。在kotlin中,是否装箱根据场合来定

val numValue: Int = 123     //数值类型,未进行装箱操作
val numValueBox: Int? = numValue    //装箱操作,可空引用会把数字进行装箱
println("装箱后: numValueBox => $numValueBox") //装箱后: numValueBox => 123

Kotlin中数值比较通过(==)判断。

数字装箱后值不变,然其内存中地址是根据其数据类型的数值范围而定,可能发生变化,两个数值在内存中的地址是否相等用(===)判断

类型转换

Kotlin提供显式转换和隐式转换

  • 显式转换

    数字类型支持toByte()toShort()toInt()toLong()toFloat()toDouble()toChar()显示转换

    val numA: Int = 97
    println(numA.toByte())    //Int 显式转换为 Byte
    println(numA.toShort())
    //.......
    
  • 隐式转换

    其中较小的类型不能隐式转换为较大类型

    val b: Byte = 1 // Int 隐式转换为Byte类型
    val i: Int = b    //报错,Byte不能隐式转换为Int类型
    val num = 30L + 12    // 30L + 12 -> Long + Int => Long
    

位运算

Kotlin中没有特殊字符来表示,只可用中缀方式调用具名函数(只用于IntLong

shl(bits) – 有符号左移(类似Java<<
shr(bits) – 有符号右移(类似Java>>
ushr(bits) – 无符号右移(类似Java>>>
and(bits) – 位与(同Java中的按位与)
or(bits) – 位或(同Java中的按位或)
xor(bits) – 位异或(同Java中的按位异或)
inv() – 位非(同Java中的按位取反)

 val x = (1 shl 2) and 0x000FF000

布尔类型

布尔用 Boolean 类型表示,它有两个值:truefalse

布尔运算

|| – 短路逻辑或,与Java一致

&& – 短路逻辑与,与Java一致

! - 逻辑非,与Java一致

字符型

字符用 Char 类型表示,字符变量用单引号(' ')表示,与Java不同,它们不能直接当作数字

fun check(c: Char) {
    if (c == 1) { // 错误:类型不兼容,不能直接当作数字使用,需要进行类型转换
        // ……
    }
}

字符类型转换

字符型变量不能直接当作数字,可以转换为数字,也可转换为其他类型和英文大小写转换

fun decimalDigitValue(c: Char): Int {
    if (c !in '0'..'9')
    throw IllegalArgumentException("Out of range")
    return c.toInt() - '0'.toInt() // 显式转换为数字
}

//大小写转换
var charA: Char = 'a'
var charB: Char = 'B'
var charNum: Char = '1'
result = charA.toUpperCase()
result = charB.toLowerCase()
result = charNum.toLowerCase()  //字符变量不是英文时,转换无效,还为1

字符转义

Kotlin同Java一样,支持以下转义序列:

\t => 表示制表符

\n => 表示换行符

\b => 表示退格键(键盘上的Back建)

\r => 表示键盘上的Enter

\\ => 表示反斜杠

\' => 表示单引号

\" => 表示双引号

\$ => 表示美元符号,如果不转义在kotlin中就表示变量的引用了

其他的任何字符请使用Unicode转义序列语法。例:'\uFF00'

println("\n  换行符")
println("\t  制表符")
println(" \b  退格键")

2.2 字符串

定义

字符串用 String 类型表示,字符串的元素——字符可以使用索引运算符访问: s[i]

val str: String = "kotlin"
for (s in str){
    print(s)
    print("\t")
}
// k    o   t   l   i   n

字符串字面量

Kotlin 有两种类型的字符串字面值:

  1. 转义字符串可以有转义字符
  2. 原始字符串可以包含换行以及任意文本
val s = "Hello, world!\n"   //字符串含有转义字符

//字符串包含换行
val text = """
    for (c in "foo")
        print(c)
"""

可以使用trimMargin()函数删除前导空格 ,默认使用符号(|)作为距前缀,当然也可以使用其他字符。例:右尖括号(>)、左尖括号(<)等

val str3: String = """
    > I`m like Kotlin .
    > I`m like Java .
    > I`m like Android .
    > I`m like React-Native.
""".trimMargin(">")
println(str3)
// I`m like Kotlin .
// I`m like Java .
// I`m like Android .
// I`m like React-Native.

也可以使用trimIndent()函数删除共同的缩进空格

字符串模板

字符串字面值可以包含模板表达式*,即一些小段代码,会求值并把结果合并到字符串中。 模板表达式以美元符($)开头,在$符号后面加上变量名或大括号中的表达式

val text1: String = "我来了!"
var text2: String = "$text1 kotlin" //我来了! kotlin
var text3: String = "$text2 ${text1.length} 哈哈!!!!" //我来了! kotlin 4 哈哈!!!!

大多数情况下,优先使用字符串模板或原始字符串,而不是字符串连接

2.3 数组

Kotlin Java
整型 IntArray int[]
整型装箱 Array Integer[]
字符 CharArray char[]
字符装箱 Array Character[]
字符串 Array String[]
... ... ...

Java中的数组创建方式示例:

//java
int[]c = new int[]{1,2,3,4};

Kotlin中不装箱的写法(等价表示java中的原生类型数组):

val c0 = intArrayOf(1,2,3,4,5)  //[1,2,3,4,5]
val arr = IntArray(5)           //[0, 0, 0, 0, 0]
val arr1 = IntArray(5) { 42 }   //[42, 42, 42, 42, 42]
val c1 = IntArray(5){it + 1}    //[0, 1, 2, 3, 4]
println(c1.contentToString)

同理,可以使用FloatArrayBooleanArray等,其中contentToString是kotlin中数组的便捷打印方法

除却前述原型数组的创建,Kotlin中还有装箱类型数组和对象类型数组,常用方法有arrayOf()arrayOfNulls()Array()emptyArray()

var arr1 = arrayOf(1,2,3,4,5)   // Array,等价于[1,2,3,4,5]

var arr2 = arrayOfNulls(3) //Array,其arr3[0]、arr3[1]、arr3[2]皆为null
arr3[0] = 10

var arr3 = Array(5,{index -> (index * 2).toString() })  //等价于 ["0","2","4","6","8"]
val arr4 = Array(5) { i -> (i * i).toString() } //等价于 ["0", "1", "4", "9", "16"]

val arrayEmpty = emptyArray()       //相当于arrayOfNulls(0)

与Java一样,Kotlin的array默认大小就是给定的,不过kotlin提供了一个添加元素的api,plus()

public actual operator fun  Array.plus(elements: Array): Array {
    val thisSize = size
    val arraySize = elements.size
    val result = java.util.Arrays.copyOf(this, thisSize + arraySize)
    System.arraycopy(elements, 0, result, thisSize, arraySize)
    return result
}

Kotlin数组的plus()函数其实是拷贝原来的数组,返回一个新数组

var array4 = arrayOf("1",2,3)
array4.plus("1")
for (i in array4){
    println(i)  //array4不变,所以打印结果是1、2、3,没有4
}

fill()数组填充

array提供了fill函数,给原有的数组填充数组(如果原来有数据将会被覆盖)

var array4 = arrayOf("1","2","3")
array4.fill("4",0,2)    //往数组里填入数据"4",从下标0到2,不包含2,
//打印出来结果为4、4、3

数组遍历

kotlin中数组的遍历方式主要如下三种:

val e = floatArrayOf(1f,2f,3f,4f)
//方式一,使用for in方式
for(element in e) {
    println(element)
}
//方式二,使用forEach高阶函数方式遍历
e.forEach{element ->
          println(element)
         }
//方式三,使用数组下标遍历
for(index in e.indices) {
    println("index=$index")     //输出0、1、2、3,输出数组下标
}

2.4 Range

Kotlin中Range表示区间的意思,即范围,其中除了until是前闭后开,其他都是前闭后闭

val range: IntRange = 0..1000   //表示[0,1000],前闭后闭区间
val range1 = 0..1000 step 2  //表示[0,1000],步长为2
val range1: IntRange = 0 until 1000 //表示[0,1000),前闭后开
val charRange = 'a'..'z'
val intRangeReverse = 10 downTo 1 //[10,1]

除了IntRange,还有CharRangeLongRange

val floatRange = 1f..2f     //连续区间,不可数
val doubleRange = 2.0..4.0  //连续区间,不可数

Range一般是用于遍历

val range = 0..1000
// 步长为 2,输出:0, 2, 4, 6, 8, 10,....1000,
for (i in range step 2) {   
    print("$i, ")
}
//输出:4, 3, 2, 1,
for (i in 4 downTo 1) {
    print("$i, ")
}

三、控制流

[图片上传失败...(image-c1b5dc-1644831342962)]

Kotlin中的控制流语句大体与Java相似,废除了switch语句,新增了When分支语句

if else语句

var max = a 
if (a < b) max = b

if (a > b) {
    max = a
} else {
    max = b
}

需要特别注意的是,Kotlin中不存在三元运算,直接用if....else替代

var numB: Int = (numA > 2) ? 3 : 5  //直接报错
val max = if (a > b) a else b

Kotlin中的if可以作为一个表达式并返回一个值,每一个if分支都是一个代码块,并且返回了一个值。

val max = if (a > b) {
    print("Choose a")
  a
} else {
    print("Choose b")
    b
}

for循环语句

Kotlin废除了Java中的for(初始值;条件;增减步长)这个规则,其提供了迭代器用来遍历任何东西

语法格式为:

for (item in collection) print(item)
  • 数字区间上迭代遍历

// 递增,范围:until[n,m),关键字until,左闭右开
for (i in 0 until 5){
print("i => $i \t")
}

//递减,范围:downTo[n,m],关键字downTo,左右闭合
for (i in 15 downTo 11){
print("i => $i \t")
}

// 递增,范围..[n,m],关键符号 .. ,左右闭合
for (i in 20 .. 25){
print("i => $i \t")
}

//设置循环步长,关键字step
for (i in 10 until 16 step 2){
print("i => $i \t") //10 12 14
}


- 遍历字符串

```kotlin
for (i in "abcdefg"){
    print("i => $i \t")       //i => a    i => b ......
}
  • 遍历数组

var arrayListOne = arrayOf(10,20,30,40,50)
for (i in arrayListOne){
print("i => $i \t") //i => 10 i => 20 ......
}

var arrayListTwo = arrayOf(1,3,5,7,9)
for (i in arrayListTwo.indices){
println("arrayListTwo[$i] => " + arrayListTwo[i])
}
// arrayListTwo[0] => 1
// arrayListTwo[1] => 3
//......

var arrayListTwo = arrayOf(1,3,5,7,9)
for ((index,value) in arrayListTwo.withIndex()){
println("index => value")
}
//index => 0 value => 1
//index => 1 value => 3
//......


- 列表或数组扩展函数遍历

```kotlin
var arrayListThree = arrayOf(2,'a',3,false,9)
var iterator: Iterator = arrayListThree.iterator()

while (iterator.hasNext()){
    println(iterator.next())
}

when表达式

Kotlin中废除掉Java中的switch语句,新增when语句

简单形式

when (2) {
    1 -> print("x == 1")
    2 -> print("x == 2")
    else -> { // 注意这个块
        print("x is neither 1 nor 2")
    }
}
// x == 2

多分支处理

//x = 0,1,输出内容一样
when (x) {
    0, 1 -> print("x == 0 or x == 1")
    else -> print("otherwise")
}

分支条件

可以用任意表达式(而不只是常量)作为分支条件

when (x) {
    parseInt(s) -> print("s encodes x")
    else -> print("s does not encode x")
}

when (x) {
    in 1..10 -> print("x is in the range")
    in validNumbers -> print("x is valid")
    !in 10..20 -> print("x is outside the range")
    else -> print("none of the above")
}

检查特定类型值

可以检测一个值是(is)或者不是(!is)一个特定类型的值,类似于Java中的instanceof关键字

when("abc"){
    is String -> println("abc是一个字符串")
    else -> {
        println("abc不是一个字符串")
    }
}

不使用表达式,取代if

when 也可以用来取代 if-else if链。 如果不提供参数,所有的分支条件都是简单的布尔表达式,而当一个分支的条件为真时则执行该分支

when {
    x.isOdd() -> print("x is odd")
    x.isEven() -> print("x is even")
    else -> print("x is funny")
}

while语句

同Java中while循环一样

do...while语句

同Java中do...while循环一样

break与continue

Kotlin 支持传统的 breakcontinue 操作符

  • return:默认从最近的封闭函数或匿名函数返回
  • break:终止最近的闭合循环
  • continue:前进到最近的封闭循环的下一个步骤(迭代)。

四、函数

image

Kotlin中,函数有自己的类型,是“一等公民”,其可以赋值给变量,可以作为参数传递,也可以直接调用

4.1 函数声明

//java
Food cook(String name) {
    //......
}
// kotlin
fun cook(name : String) : Food {
    // .....
}

函数声明

Kotlin中函数声明的关键字为:fun,声明格式为

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

fun double(x: Int): Int {
    return 2 * x
}

在Kotlin中定义的函数无返回值时,返回Unit或忽略即可

fun abc(): Unit{}

fun abc(){}

Receiver接收者

这里只要牢记一点:函数的Receiver是它的调用者

class Foo {
    fun bar() = println("bar")
}

Foo foo = Foo()
foo.bar() // foo实例对象就是bar方法的Receiver

函数类型

顶级函数的类型

// 顶级函数的类型
fun foo() {...} // () -> Unit
fun foo(p0: Int): String {...}  // (Int) -> String

成员函数的类型

// 类中函数的类型
class Foo {
    fun bar(p0: String, p1: Long): Any {...}   // Foo.(String, Long) -> Any 
}

4.2 函数调用

在前述我们就说了,在Kotlin中函数是“一等公民”,可以赋值,可以作为参数传递

函数的引用

这里要注意的是,函数名不是函数引用,Kotlin中提供了双冒号::限定符,将函数名转成函数引用。

关于函数的引用,主要分为顶层、局部、扩展函数引用类成员函数引用

// 定义一个顶级函数
fun foo() {...}     //() -> Unit
// 获取顶级函数foo的引用
val block = ::foo   //block的数据类型为:KFunction0,其中0表示没有函数参数,Unit表示无返回值

private fun test(aa:Int) :String{...}
val block = ::test //block数据类型为:KFunction1
// 定义一个成员函数
class Foo {
    fun bar(p0: String, p1: Long): Any {...} 
}

// 获取成员函数的引用,通过类名::方法名
val block = Foo::bar // KFunction3 ,其函数引用对应的类型就会在参数列表第 1 位多一个 Receiver 参数(用于接收方法依赖的对象实例)

//赋值给变量
val f1: () -> Unit = ::foo
val f2: (Int) -> String = ::foo
val f3: (Foo, String, Long) -> Any = Foo::bar

// 因为类型可推导,可以去掉函数类型声明
val f = ::foo
val f = ::foo
val f = Foo::bar

对于object或者companion object中的方法,可以像顶层函数一样直接调用,不依赖对象实例。

注意:以下二者是等价的

Foo.(String, Long) -> Any
(Foo, String, Long) -> Any

函数使用与调用

传统方法调用函数:

val result = double(2)

使用点表示法调用成员函数:

class Test{
    fun foo(){}
}

Test().foo()    //创建类Test实例并调用foo()

将函数对象作为函数的某个参数进行调用:

class Foo {
    fun bar(p0: String, p1: Long): Any {
        println(p0 + p1)
        return p0
    }
}

fun test(block: Foo.(String, Long) -> Any) {
    // 调用p函数
    block(Foo(), "Ryan", 3L)
    // 或者
    block.invoke(Foo(), "Ryan", 3L)
}


val block: (Foo,String,Long) -> Any = Foo::bar
test(block)

函数的引用可以通过invoke()操作符调用:block.invoke(...) 或者block(...)

fun add(arg0: Int,arg1: Int): Int {
    return arg0 + arg1
}

fun test(int: Int,block:(Int,Int) -> Int): Int{
    return int + block.invoke(12,13)
}

fun main(args: Array) {
    val addVariant : (Int,Int) -> Int = ::add
    println(test(12,addVariant))
    println(test(12){arg1:Int,arg2:Int ->
        arg1 + arg2
    })
}

下面再通过一个示例重点演示函数的调用

class Bird {
    var name:String = "喜鹊"

    fun fly() {
        println("$name fly")
    }

    fun bar(p0: String, p1: Long): Any {
        return p0 + name + p1
    }
}
fun main() {
    val tt = Bird::bar
    test(tt)
    test { bird, string, long ->
        return@test bird.name + string + long
    }

    test1(tt)
    test1 { s, l ->
        return@test1 this.name + s + l
    }

    val ff = Bird::fly
    test2(ff)
    test2 {
        this.name = "老鹰"
        println(this.name + "fly")
    }
}

fun test(a:(Bird, String, Long) -> Any) {
    println(a.invoke(Bird(),"春天",1L))
}

fun test1(b: Bird.(String, Long) -> Any) {
    println(b(Bird(),"夏天",12))
}

fun test2(b: Bird.() -> Unit) {
    println("test2")
    val bird = Bird()
    bird.name = "麻雀"
    b.invoke(bird)
}
春天喜鹊1
喜鹊春天1
夏天喜鹊12
喜鹊夏天12
test2
麻雀 fly
test2
老鹰fly

通过前述示例可知

  1. 对于成员函数的引用,(Bird, String, Long) -> AnyBird.(String, Long) -> Any是等价的
  2. 对于A.(B) -> C这种形式的引用,可以使用this表达式访问接收者对象

4.3 函数参数

在Kotlin中,函数参数是一个值得重点关注的部分

参数

函数参数使用 Pascal 表示法定义,即 name: type。参数用逗号隔开,每个参数必须有显式类型:

fun powerOf(number: Int, exponent: Int) { /*……*/ }

函数的参数也可以为可空类型

var myName : String? = "rengwuxian"
fun cook(name: String) : Food {}
cook(myName)    //报错,传了一个可空参数

var myName : String? = "rengwuxian"
fun cook(name: String?) : Food {}
cook(myName)    //正常运行

默认参数

函数参数可以有默认值,省略相应的参数时使用默认值。默认值通过类型后面的 = 及给出的值来定义。

fun read(b: Array, off: Int = 0, len: Int = b.size) { /*……*/ }

具有默认参数时,可以对参数有默认值的参数不传递参数值

覆盖方法总是使用与基类型方法相同的默认参数值。 当覆盖一个带有默认参数值的方法时,必须从签名中省略默认参数值

open class A {
    open fun foo(i: Int = 10) { /*……*/ }
}
class B : A() {
    override fun foo(i: Int) { /*……*/ }  // 不能有默认值
}

具名参数

即在使用函数时显示使用参数名 = 参数值这种方式传递参数

fun reformat(str: String,
             normalizeCase: Boolean = true,
             upperCaseFirstLetter: Boolean = true,
             divideByCamelHumps: Boolean = false,
             wordSeparator: Char = ' ') {
    /*……*/
}

reformat(str)   //使用默认参数调用
reformat(str, true, true, false, '_')   //使用非默认参数调用
//使用具名参数调用
reformat(str,
         normalizeCase = true,
         upperCaseFirstLetter = true,
         divideByCamelHumps = false,
         wordSeparator = '_'
        )

reformat(str, wordSeparator = '_')  //使用部分具名参数调用

关于默认参数与具名参数,需要注意的是:如果一个默认参数在一个无默认值参数前,该无默认值参数的使用只能通过具名参数调用,所有位置参数要放在第一个具名参数之前

fun foo(bar: Int = 0, baz: Int) { /*……*/ }
foo(baz = 1) // 使用默认值 bar = 0

简单来说就是,允许调用 f(1, y = 2) 但不允许 f(x = 1, 2)

如果在默认参数之后最后一个参数是lambda表达式,那么它既可以作为具名参数在括号内传入,也可以在[括号外]传入

fun foo(bar: Int = 0, baz: Int = 1, qux: () -> Unit) { /*……*/ }

foo(1) { println("hello") }     // 使用默认值 baz = 1
foo(qux = { println("hello") }) // 使用两个默认值 bar = 0 与 baz = 1

foo { println("hello") }        // 使用两个默认值 bar = 0 与 baz = 1

变长参数

函数中的参数是不定数量的个数并且是同一个类型,则可是使用vararg修饰符去修饰这个变量,则被vararg修饰的参数相当于一个固定类型的数组

fun 函数名(vararg 参数名 : 类型,...) :返回值{}
fun  asList(vararg ts: T): List {
  val result = ArrayList()
  for (t in ts) // ts is an Array
        result.add(t)
  return result
}

val list = asList(1, 2, 3)

4.4 本地函数

本地函数即可以在函数内部再声明一个函数仅供外部函数使用

假设一个普通登录校验函数如下:

fun login(user: String, password: String, illegalStr: String) {
    // 验证 user 是否为空
    if (user.isEmpty()) {
        throw IllegalArgumentException(illegalStr)
    }
    // 验证 password 是否为空
    if (password.isEmpty()) {
        throw IllegalArgumentException(illegalStr)
    }
}

若检查参数这部分有点冗余,又不想作为一个单独函数对外暴露,可以使用嵌套函数,在内部声明一个函数:

fun login(user: String, password: String, illegalStr: String) {
    fun validate(value: String) {
        if (value.isEmpty()) {
                                              
            throw IllegalArgumentException(illegalStr)
        }
    }
    validate(user)
    ...
}

前述的嵌套函数在login函数外是无法访问的

一般情况下不建议使用本地函数,具体原因可以反编译为java函数看其内部实现可知

4.5 重载函数

在Java中是没有默认值参数的概念,当我们需要从Java中调用Kotlin中的默认值重载函数的时候,必须显示的指定所有参数值

在kotlin中

fun f(a: String, b: Int = 0, c: String="abc"){
...
}

相当于在java中声明:

void f(String a, int b, String c){
}

kotlin中给出了一种方案,就是使用@JvmOverloads注解,这样就会自动生成多个重载方法供Java调用

@JvmOverloads fun f(a: String, b: Int=0, c:String="abc"){
}

相当于在java中声明了如下三个方法:

void f(String a)
void f(String a, int b)
void f(String a, int b, String c)

该注解也可以用在构造方法和静态方法中

4.6 其他

单表达式

当函数返回单个表达式时,可以省略花括号并且在 = 符号之后指定代码体即可

fun test1() = 2                     // 自动推断为:返回类型为Int

fun test2(num : Int) = num * 2      // 自动推断为:返回类型为Int

fun test3(x : Float, y : Int = 2) = x * y  // 和默认参数一起使用,返回值为Float型

多返回值

Kotlin中可以使用PairTriple实现函数多返回值,最后通过结构声明将值取出。

fun multiReturnValues(): Triple {
    return Triple(1, 3L, 4.0) 
}

// 通过解构语法获取多返回值
val (a, b, c) = multiReturnValues()

五、字符串

关于字符串的基本使用在前述数据类型时已经介绍过了,这里简单介绍一下一些常用方法,方法使用时查阅

字符串属性

属性 描述
length: Int 它返回字符串序列的长度。
indices: IntRange 它返回当前char序列中有效字符索引的范围。
lastIndex: Int 它返回char序列中最后一个字符的索引。
val str : String = "Hello world!"
println("strLength = ${str.length}")        //strLength = 12
println("strIndices = ${str.indices}")      //strIndices = 0..11
println("strLastIndex = ${str.lastIndex}")  //strLastIndex = 11

字符串查找

函数 描述
first() 获取第一个元素,为空抛出异常
firstOrNull() 获取第一个元素,为空返回null
last() 获取最后一个元素,为空抛出异常
lastOrNull() 获取最后一个元素,为空返回null
find{} 高阶函数,对firstOrNull()进行处理
findLast{} 高阶函数,对lastOrNull()进行处理
indexOf() 查找某一个元素或字符串在原字符串中第一次出现的下标。
indexLastOf() 查找某一个元素或字符串在原字符串中最后一次出现的下标。
indexOfFirst{} indexOf()
indexOfLast{} indexLastOf()
val str : String = "Hello world!"

println("strFirst = ${str.first()}")            // strFirst = H
println("strFind = ${str.find { it == 'H' }}")  // strFind = H
println("strFind = ${str.find { it == 'W' }}")  // strFind = null
println("strLast = ${str.last()}")              // strLast = !

println("strIndexOfFirst = ${str.indexOfFirst { it == 'H'}}")   //strIndexOfFirst = 0
println("strIndexOf = ${str.indexOf("Hello",0)}")               //strIndexOf = 0

字符串截取

函数 描述
subString() 截取字符串
subSequence() 截取字符串
val str = "Kotlin is a very good programming language"

// 当只有开始下标时,结束下标为length - 1
println("subStr = ${str.substring(10)}")    //subStr = a very good programming language
println(str.substring(0,15))    // Kotlin is a ver
println(str.substring(IntRange(0,15)))  //Kotlin is a very

println(str.subSequence(0,15))  //Kotlin is a ver
println("subSeq = ${str.subSequence(IntRange(0,15))}")  //subSeq = Kotlin is a very

字符串替换

函数 描述
replace(oldChar,newChar,ignoreCase = false) 把原字符串中的某一个字符全部替换成新的字符。然后返回新的字符串
replace(oldValue,newValue,ignoreCase = false) 把原字符串中的某一个字符全部替换成新的字符。然后返回新的字符串
replace(regex,replacement) 根据定义的正则规则去匹配源字符串,把满足规则的字符串替换成新的字符串。
replace(regex: Regex, noinline transform: (MatchResult) -> CharSequence) 根据定义的正则规则去匹配源字符串,把满足规则的字符串通过transform{}高阶函数映射的新字符串替换。
replaceFirst() 满足条件的第一个字符或字符串替换成新的字符或字符串
replaceBefore() 截取满足条件的第一个字符或字符串后面的字符串,包含满足条件字符或字符串自身,并在其前面加上新的字符串。
replaceBeforeLast() 截取满足条件的最后一个字符或字符串后面的字符串,包含满足条件字符或字符串自身,并在其前面加上新的字符串。
replaceAfter() 截取满足条件的第一个字符或字符串前面的字符串,包含满足条件字符或字符串自身,并在其后面加上新的字符串。
replaceAfterLast() 截取满足条件的最后一个字符或字符串前面的字符串,包含满足条件字符或字符串自身,并在其后面加上新的字符串。
// 字符替换
val str = "Kotlin is a very good programming language"
println(str.replace('a','A'))   //Kotlin is A very good progrAmming lAnguAge


//字符串替换
val str = "Kotlin is a very good programming language"
println(str.replace("Kotlin","Java"))   //Java is a very good programming language


//正则匹配替换字符串
val str = "1234a kotlin 5678 3 is 4"
println(str.replace(Regex("[0-9]+"),"kotlin"))  //kotlina kotlin kotlin kotlin is kotlin


//正则匹配并通过transform{}映射替换字符串
val str = "1234a kotlin 5678 3 is 4"
val newStr = str.replace(Regex("[0-9]+"),{
    "abcd "
})  //abcd abcd abcd abcd a kotlin abcd abcd abcd abcd  abcd  is abcd


// replaceFirst()
val str = "Kotlin is a very good programming language"
println(str.replaceFirst('a','A'))  //Kotlin is A very good programming language
println(str.replaceFirst( "Kotlin","Java")) //Java is a very good programming language


//replaceBefore()
val str = "Kotlin is a very good programming language"
println(str.replaceBefore('a',"AA"))    //AAa very good programming language
println(str.replaceBefore("Kotlin","Java")) //JavaKotlin is a very good programming language


//replaceBeforeLast()
val str = "Kotlin is a very good programming language"
println(str.replaceBeforeLast('a',"AA"))    //AAage
println(str.replaceBeforeLast("Kotlin","Java")) //JavaKotlin is a very good programming language


//replaceAfter()
val str = "Kotlin is a very good programming language"
println(str.replaceAfter('a',"AA")) //Kotlin is aAA
println(str.replaceAfter("Kotlin","Java"))  //KotlinJava


//replaceAfterLast()
val str = "Kotlin is a very good programming language"
println(str.replaceAfterLast('a',"AA")) //Kotlin is a very good programming languaAA
println(str.replaceAfterLast("Kotlin","Java"))  //KotlinJava

字符串分割

函数 描述
split() 分割函数
splitToSequence() 分割函数
//使用正则表达式分割
var str2 = "1 kotlin 2 java 3 Lua 4 JavaScript"
val list3 = str2.split(Regex("[0-9]+"))
for (str in list3){
    print("$str \t")
}
//kotlin  java  Lua  JavaScript     
val list4 = str2.split(Pattern.compile("[0-9]+")) //与前述等价


//使用字符或字符串分割
val str1 = "Kotlin is a very good programming language"
val list2 = str1.split(" ")
val str3 = "a b c d e f g h 2+3+4+5"
val list5 = str3.split(' ','+')

splitToSequence()使用方法与split()一样

other

函数 描述
count() 获取字符串长度
count{} 高阶函数
isEmpty() 判断其length是否等于0不可用于可空字符串
isNotEmpty() 判断其length是否大于0不可用于可空字符串
isNullOrEmpty() 判断该字符串是否为null或者其length是否等于0
isBlank() 判断其length是否等于0,或者判断其包含的空格数是否等于当前的length不能直接用于可空的字符串
isNotBlank() isBlank()函数取反
isNotOrBlank() 判断该字符串是否为null。或者调用isBlank()函数
plus() 字符串链接
reversed() 字符串反转
startsWith() 判断字符串的起始
endsWith() 判断字符串的结尾
// count()
val str = "kotlin very good"
println("str.count() => ${str.count()}")    //str.count() => 16


// count{}
val str = "kotlin very good"
val count = str.count { it == 'o' }
println("count : $count")   //count : 3


//字符串验证
val str : String? = "kotlin"
println(str?.isEmpty()) //false
println(str?.isNotEmpty())  //true
println(str.isNullOrEmpty())    //false
println(str?.isBlank()) //false
println(str?.isNotBlank())  //true
println(str.isNullOrBlank())    //false


//字符串链接
val oldStr = "kotlin"
println(oldStr.plus(" very good"))  //kotlin very good
println(oldStr + " very good")  //kotlin very good


//判断字符串起始
val str = "kotlin"
str.startsWith('k')         // 是否有字符`k`起始
str.startsWith("Kot")       // 是否由字符串`kot`起始
str.startsWith("lin",3)     // 当起始位置为3时,是否由字符串`lin`起始
// true     true     true


//判断字符串结尾
val str = "kotlin"
println(str.endsWith("lin"))  // 是否由字符串`lin`结尾
println(str.endsWith('n'))    // 是否由字符`n`结尾
// true     ture

更多字符串操作可以参看标准库

六、顶层函数

在前述内容介绍时也曾多次涉及到顶层函数,那么什么是顶层函数?为什么用顶层函数替代Javastatic函数?

Java中有静态函数和静态属性概念,它们一般作用就是为了提供一个全局共享访问区域和方法。我们一般的习惯的写法就是写一个类包裹一些static修饰的方法,然后在外部访问的直接利用类名.方法名访问。

静态函数内部是不包含状态的,它的输入仅仅来自于它的参数列表,而它的输出也仅仅依赖于它参数列表。

Kotlin中则认为一个函数或方法有时候并不是属于任何一个类,它可以独立存在。所以在Kotlin中类似静态函数和静态属性会去掉外层类的容器,一个函数或者属性可以直接定义在一个Kotlin文件的顶层中,在使用的地方只需要import这个函数或属性即可。

如果你的代码还存在很多以"Util"后缀结尾的工具类,是时候去掉了。

基本使用

创建一个顶层文件:

image

在顶层文件定义函数

//这个顶层函数不属于任何一个类,不需要类容器,不需要static关键字
fun formateFileSize(size: Double): String {
    if (size < 0) {
        return "0 KB"
    }

    val kBSize = size / 1024
    if (kBSize < 1) {
        return "$size B"
    }

    val mBSize = kBSize / 1024
    if (mBSize < 1) {
        return "${BigDecimal(kBSize.toString()).setScale(1, BigDecimal.ROUND_HALF_UP).toPlainString()} KB"
    }

    val mGSize = mBSize / 1024
    if (mGSize < 1) {
        return "${BigDecimal(mBSize.toString()).setScale(1, BigDecimal.ROUND_HALF_UP).toPlainString()} MB"
    }

    val mTSize = mGSize / 1024
    if (mTSize < 1) {
        return "${BigDecimal(mGSize.toString()).setScale(1, BigDecimal.ROUND_HALF_UP).toPlainString()} GB"
    }
    return "${BigDecimal(mTSize.toString()).setScale(1, BigDecimal.ROUND_HALF_UP).toPlainString()} TB"
}

顶层函数实质原理

  1. 顶层文件会反编译成一个容器类。
  2. 顶层函数会反编译成一个static静态函数

七、中缀调用

中缀调用:对只有一个参数的函数调用进行简化,省略了类名或者对象名+"."+函数名调用方式

val map = mapOf(Pair(1, "A"), Pair(2, "B"), Pair(3, "C"))
//前述等价于
val map = mapOf(1.to("A"), 2.to("B"), 3.to("C"))
//前述亦等价于 to函数的中缀调用
val map = mapOf(1 to "A", 2 to "B", 3 to "C")
//to实际上一个返回Pair对象的函数
val strA = "A"
val strB = "B"

if (StringUtils.equals(strA, strB))
//前述等价于 sameAs的中缀调用
if (strA sameAs strB)

private infix fun String.sameAs(strB: String): Boolean {
    return this == strB
}
val list = listOf(1, 3, 5, 7, 9)
val element = 2

if (list.contains(element))
//前述等价于 into的中缀调用
if (element into list)

基本使用

中缀调用类似运算符使用,调用结构为:A (中缀函数名) B

例如: element into list

实质原理

to函数

public infix fun  A.to(that: B): Pair = Pair(this, that)
image

使用infix关键字修饰的函数,传入A,B两个泛型对象,“A.to(B)”结构是一种特殊结构暂时把它叫做带接收者的结构,后面的this就是指代A,并且函数的参数只有一个,返回的是一个Pair对象。

this指代A,that就是传入的B类型对象。

into函数

infix fun  T.into(other: Collection): Boolean = other.contains(this)
image

使用infix关键字修饰的函数,泛型T元素是否存在于泛型T集合之中。

从前述可知,使用中缀调用的函数都是infix修饰的函数,也称为infix函数。

如何写一个infix函数?必须满足如下要求:

  1. 必须是成员函数或扩展函数
  2. 必须只有一个参数
  3. 方法前必须加infix关键字
  4. 其参数不得接收可变数量参数且不能有默认值
/**
* 中缀成员函数
*/

class Student {
    var kotlinScore = 0.0
     
    infix fun addKotlinScore(score: Double): Unit {
        this.kotlinScore = kotlinScore + score
    }
}

val student = Student()
student addKotlinScore 95.00
print(student.kotlinScore) // will print "95.0"
/**
* 中缀扩展函数
*/
package foo.bar; //将扩展方法放到包内,作用域将是整个包
 
infix fun Int.ride(num: Int): Int{
    println("num= $num")
    return 2 * num
}
 
import foo.bar.* //使用时需要导包
 
object MyTest {
    @JvmStatic
    fun main(arg: Array) {
        println(3.ride(2))
    }
}

中缀函数优先级

  1. 中缀函数调用的优先级低于算术操作符、类型转换以及 rangeTo 操作符

    1 shl 2 + 3  等价于         1 shl (2 + 3)
    0 until n * 2 等价于        0 until (n * 2)
    xs union ys as Set<*>  等价于  xs union (ys as Set<*>)
    
  2. 中缀函数调用的优先级高于布尔操作符 && 与 ||、is- 与 in- 检测以及其他一些操作符。

    a && b xor c  等价于   a && (b xor c)
    a xor b in c  等价于  (a xor b) in c
    

八、运算符重载

首先看一下常见的运算符

==      // 对应 equals
+       // 对应 plus
-       // 对应 minus
*       // 对应 times
/       // 对应 div
%       // 对应 rem
a++     // 对应 inc
a..b    // 对应 rengeTo
a in b  // 对应 contains
a[i]    // 对应 get
a()     // 对应 invoke
...

Kotlin支持运算符重载,可重载的运算符列表参见官方文档

Kotlin通过调用自己代码中定义特定的函数名的函数(成员函数或者扩展函数),并且用operator修饰符标记,来实现特定的语言结构,例如:如果你在一个类上面定义了一个特定函数命名plus的函数,那么按照Kotlin的约定,可用在这个类的实例上使用+运算符。

运算符重载示例:

// 重载String中的-运算符
operator fun String.minus(other: String): String {
    return this.replaceFirst(other.toString(), "")
}
val a = "HelloWorld"
val b = "World"
val c = a - b // "Hello"

你可能感兴趣的:(Kotlin语法基础)