Kotlin学习笔记(一)

早就听说Kotlin简洁又方便,最近正好准备看《第一行代码》第三版,那就跟着书边学边做笔记吧

一、变量和函数

1、变量:

由于Kotlin的自动推导机制,所以在声明一个变量时,一般只用到两个关键字:val、var
val(value):声明不可变的变量,赋值之后不能重新赋值,对应Java中final修饰的变量
var(variable):声明可变的变量,赋值后仍可赋值

Kotlin每行代码的结尾不用加分号
例子:

val a = 10
println("a = " + a)

变量延迟赋值时,需要显示的声明变量的类型

//延迟赋值
val a: Int 10

//正常赋值
val a = 10

kotlin中,val声明的变量一旦赋值,便不可再修改,这解决了Java中final关键字没有合理利用的问题(除非一个变量明确允许被修改,否则都应该给他加上final)

使用诀窍:永远优先使用val来声明变量,当val没有办法满足你的需求时再使用var

2、函数

函数语法规则:

fun 函数名(参数名1: 参数类型,参数名2: 参数类型): 返回类型{
}

fun methodName(param1: Int,param2: Int): Int{
    return 0
}

如果没有返回值,那么返回类型不必写

Kotlin语法糖:
当一个函数中只有一行代码时,Kotlin允许我们不必编写函数体,可以直接将唯一的一行代码写在函数定义的尾部,中间用=连接即可

//max()是kotlin的内置函数,直接调用
fun largerNumber(num1: Int,num2: Int) = max(num1,num2)

此处省略了返回值类型、return关键字、函数体

二、程序的逻辑控制

1、if语句

kotlin的条件语句主要两种形式:if和when

if语句相较于Java中的if,增加了返回值这一额外功能

fun largerNumber(num1: Int,num2: Int): Int{
    val value = if(num1>num2){
        num1    
    }else{
        num2
    }
    return value
}

可优化之处:

  1. value变量多余
  2. 根据上面的kotlin语法糖,可以省略return和方法体
fun largerNumber(num1: Int,num2: Int) = if(num1>num2)
        num1    
    else
        num2
    

2、when条件语句

Kotlin中的when是对Java中switch的改良(由于switch的break常发生错误),when和if类似,也有返回值
用法:

fun getStore(name: String) = when(name){
    "Tom" -> 86
    "Jim" -> 77
    else -> 0
}

注:

  1. 使用when时,最后也要有个else与之匹配
  2. when语句允许传入一个任何类型的参数,内部的结构体格式:
匹配值 ->{执行逻辑}
  1. when语句还允许进行类型匹配:
    is关键字类似于Java中的instanceof关键字
when(num){
    is Int -> println("num is Int")
    is Double -> println("num is Double")
    else -> println("num not support")
}
  1. when语句有时还可以不带参数
    kotlin判断字符串或对象是否相等可以直接使用==关键字,而不用像Java去调用equals()方法
fun getStore(name: String) = when{
    name == "Tom" -> 86
    name == "Jim" -> 77
    else -> 0
}

3、循环语句

kotlin中的while的用法和Java一样,但对for做了大幅度的修改

kotlin对Java中的for-each循环进行了加强,而舍弃了for-in的用法

区间:

val range = 0..10
//区间 [0,10]   两端都是闭区间

val range = 0 until 10
//区间 [0,10)   左开右闭

默认情况下,for-in循环每次执行循环时会在区间范围递增1,若想跳过其中一些元素,可以使用step关键字

for(i in 0 until 10 step 2){//每次循环会在范围区间内递增2
}

除了 ..until使范围区间升序外,我们也可以创建降序的区间,使用downTo关键字

for(i in 10 downTo 1){
}
//区间 [10,1]

如果有一些特殊场景使用for-in循环无法实现的话,我们还可以改用while循环的方式进行实现

三、面向对象编程

1、类与对象

kotlin中定义类时,也是 字段+函数

kotlin中实例化一个类的方式和java类似,只是不使用new关键字了

class Person{
    var name = ""
    var age = 0

    fun eat(){
        ...
    }
}

fun main(){
    val p = Person()
    p.name = "Jack"
    p.age = "11"
    p.eat
}

这样设计的好处是,当我们调用类的构造函数时,更能突出我们只是对这个类进行实例化的意图

2、继承与构造函数

kotlin中任何一个非抽象类默认都是不可以被继承的,相当于java中给类声明了final关键字。类和变量一样,最好都是不可变的,而一个类允许被继承的话,无法预知其子类会如何实现,会存在一定的风险

如果一个类不是抓men为继承而设计的,那么就应该主动将它加上final声明,禁止它可以继承

为了使一个类能被继承,需要准备2步

  1. 使用open关键字对类进行修饰
    加上open关键字之后,我们就是在主动告诉kotlin编译器,Person这个类是专门为继承而设计的
  2. 使用:(冒号) 去替代java中继承时的extends关键字
open class Person{
}

class Student : Person(){
}
思考:为什么继承Person时,后面还要接一个括号

首先我们得了解kotlin的构造函数分为:主构造函数和次构造函数

1)主构造函数:

最常用的构造函数,每个类都会默认有一个不带参数的主构造函数。主构造函数的特点是没有函数体,直接定义在类名后面即可

class Student(val sno:String, val grade: Int) : Person(){
}

在这里,表面了对Student类进行实例化的时候,必须传入构造函数中所要求的所有函数,从上面可以看出,每个参数都用val修饰的,因为构造函数的参数都是在创建实例时传入的,不像之前那样还能在函数体中重新赋值,所以都用的val修饰。

val student = Student("a123",11)

由于主构造函数没有函数体,那么我们想要在主构造函数中写一些逻辑,就可以使用kotlin提供的init结构体,所有主函数的逻辑都可以写在里面

class Student(val sno:String, val grade: Int) : Person(){
    init{
        ...
    }
}

不过在大多数情况下,我们都不需要编写init结构体

根据继承特性规定,子类构造函数必须调用父类的构造函数。但是kotlin的主构造函数没有函数体,无法实现直接调用父类的构造函数,所以kotlin设计了括号来解决这个问题,子类的主构造函数调用父类中的哪个构造函数,在继承时,通过括号来指定

class Student(val sno:String, val grade: Int) : Person(){
}

Person后的一对空括号表示Student类的主构造函数在初始化时,会调用Person类的无参构造函数。即使在无参数的情况下,这对括号也不能省略

Person中没有无参构造时,需要根据其现有的主构造函数去继承,所以我们在继承Person时,应该给Person传入name和age参数,但是Student没有这两个参数,所以我们可以在Student的主构造函数中加入这两个字段,这样就能传给Person中的构造函数了

open class Person(val name: String,val age: Int){
}

class Student(val sno:String, val grade: Int, name: String,age: Int) : Person(name,age){
}

在Student要传到Person的两个字段中,我们没有使用var或val去对两个变量进行修饰。因为在主构造函数中用val或var修饰的参数将自动成为该类的字段,这样和父类中同名的name和age字段会产生冲突。所以我们不使用var或val对其进行修饰,让他的作用域仅限于主构造函数中。

2)次构造函数

kotlin中任何一个类只能有一个主构造函数,但是可以有多个次构造函数。次构造函数也可以用于对象的实例化,但它是有函数体的。

当一个类既有主构造函数,又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)

class Student(val sno: String,val grade: Int,name: String,age: Int) : Person(name,age){

    //次构造函数()
    constructor(name: String,age: Int) : this("",0,name,age){//这里的this是调用的主构造函数
    }

    //无参次构造函数
    constructor() : this("",0){//这里的this是调用的上面的次构造函数
    }
}

次构造函数是用constructor关键字定义来的,第二个次构造函数中,通过this去调用了第一个次构造函数,而第一个次构造函数通过this调用了主构造函数,这样第二个次构造函数就间接的调用了主构造函数。

val student1 = Student()
val student2 = Student("Jack",12)
val student3 = Student("a123",2,"Jack",12)

主构造函数和次构造函数都能用于对象的实例化

特殊情况

类中只有次构造函数,没有主构造函数

class Student: Person{
    constructor(name: String,age: Int): super(name,age){//用super去调用父类的构造函数
    }
}

由于Student类没有显示的定义主构造函数,但又定义了次构造函数,所以Student类是没有主构造函数的,所以他继承Person时,也不需要在Person的后面添加括号来表面要继承的构造函数。
由于Student没有主构造函数,所以其次构造函数只能通过super关键字去调用父类的构造函数

3、接口

kotlin中,:(冒号)不仅替代了java中的extends关键字,也替代了implements关键字,所以类在继承父类和实现接口时都使用:(冒号),中间用,分开

interface Studey{
    fun readBooks()
}

class Student(name: String,age: Int): Person(name,age),study {
    override fun readBooks() {
        ...
    }
}

kotlin使用override关键字来重写父类或者实现接口中的函数

kotlin对接口增加了一个额外的功能:允许对接口中定义的函数进行默认实现

interface Study {
    fun readBooks()

    fun doHomework(){
        println("default")
    }
}

如果接口中的一个函数拥有了函数体,这个函数体中的内容就是它的默认实现。当子类去实现这个接口时,会强制要求实现readBooks()函数,而doHomework()函数自由选择实现或不实现(和Java中的抽象类相似)

函数可见修饰符
权限修饰符 Kotlin Java
public 所有类可见(默认) 所有类可见
private 当前类可见 当前类可见
protected 当前类和子类可见 当前类、子类、同包下的类可见
default(什么都不写) 同包可见(默认)
internal 同一模块的类可见

4、数据类与单例类

1)数据类

java中定义一个数据类时,通常为其添加 字段+setter+getter,还有配合系统类进行处理的equals()、hashCode()、toString()方法等配套方法,但是这些方法只是给数据类提供了功能,并没有实际的逻辑意义。
kotlin创建数据类时,只用一行代码就能完成java中的上述功能,大大减少开发的工作量

data class CellPhone(var brand: String,val price: Double)

data关键字表明你希望这个类是一个数据类,kotlin会自动傍你生成equals()、hashCode()、toString()等无实际逻辑意义的代码
如果没有data关键字修饰这个类,那么输出对象的时候,输出的是一串hash码,因为kotlin没有为其生成toString()方法。

2)单例类

相较于Java创建单例时的私有化构造函数、使用静态方法等操作,kotlin只需要将一个正常类的class关键字换成object关键字修饰即可,这样由kotlin背后帮我们创建一个Singleton实例,并保证全局只会存在一个Singleton实例

object Singleton {
    fun singletonTest(){
        ...
    }
}

fun main(){
    Singleton.singletonTest()
}

四、Lambda编程

1、集合的创建与遍历

1)List

在Java中创建并初始化一个集合时,可能会先实例化一个ArrayList对象,再把元素一个一个add进集合里,kotlin也支持这样的操作,但是太过繁琐,所以kotlin提供了一个内置的listOf()函数来简化这一过程

val list = listOf("Apple","Banana","Orange","Pear")
for (fruit in list) {
        
}

但是listOf创建的list是一个只能用于读取,但不能对集合进行添加、修改、删除的不可变集合。所以,想要创建一个可变集合,需要使用mutableListOf()函数

val list = mutableListOf("Apple","Banana","Orange","Pear")
list.add("Watermelon")
list.remove("Apple")
2)Set

set的使用,和list很类似,只是把创建的方法改成了setOf和mutableSetOf()函数

3)Map

Map与Java中的语法类似,也可以通过put()和get()方法对Map进行添加和读取数据,但kotlin却不建议使用这两个函数,而是推荐使用一种类似于数组下标的语法结构

val map = HashMap()
//存值
map["Apple"] = 1
//取值
val number = map["Apple"]

和list一样,这样一个一个的存太过繁琐,kotlin提供了一对mapOf()和mutableMapOf()函数来简化这一过程

val map = mapOf("Apple" to 1,"Banana" to 2,"Orange" to 3)
for ((fruit, number) in map) {
}

这里的to并不是关键字,而是infix函数

2、集合的函数式Api

1)Lambda表达式

Lambda就是一小段可以作为参数传递的代码
Lambad语法结构

{参数名1:参数类型,参数名2:参数类型 -> 函数体}

->符号表示参数列表的结束以及函数体的开始,函数体中可以编写任意行代码(但不建议写太长),并且最后一行代码会自动作为Lambda表达式的返回值。

Lambda表达式的简化过程

  1. 传入一个lambda表达式到maxBy函数中,因为maxBy接收的就是一个lambda类型的参数,所以可以直接传进去。
val maxLengthFruit = list.maxBy({fruit: String -> fruit.length})
  1. Lambda参数是函数的最后一个参数时,可以将Lambda表示移到函数括号外面
val maxLengthFruit = list.maxBy(){fruit: String -> fruit.length}
  1. 如果Lambda参数时函数唯一的参数,可以直接省略函数后面的括号
val maxLengthFruit = list.maxBy{fruit: String -> fruit.length}
  1. 由于Kotlin的推导机制,Lambda表达式中的参数列表在大多数情况下都不必声明参数类型
val maxLengthFruit = list.maxBy{fruit-> fruit.length}
  1. 当Lambda表达式的参数列表只有一个参数时,也不必声明参数类型,而是可以使用it关键字来替代
val maxLengthFruit = list.maxBy{it.length}
2)filter函数

filter函数是用来过滤集合中数据的,可单独使用,也可配合刚才的map一起使用

val newList = list.filter { it.length <= 5 }
    .map { it.toUpperCase() }

先使用filter函数再用map函数,会高效得多,如果先用map,会对集合中的所有元素先映射转换后再过滤;而先用filter函数,会是对filter后的元素进行映射,提高效率

3)any函数、all函数

any函数用于判断集合中是否存在一个元素满足指定条件
all函数用于判断集合中是否所有元素都满足指定条件。

3、Java函数式API的使用

kotlin中调用Java方法时也可以使用函数式Api。
当在Kotlin中调用Java方法,并且该方法接收一个Java单抽象方法接口参数,就可以使用函数式Api
Java单抽象接口:接口中只有一个待实现方法。如Runnable接口

public interface Runnable{
    void run();
}

以线程举例:

//java
new Thread(new Runnable() {
    @Override
    public void run() {

    }
}).start();

//kotlin
Thread(object : Runnable{
    override fun run() {
        
    }
})

由于kotlin完全舍弃了new关键字,因此创建匿名类实例时就不能再使用new了,而是改用object关键字

对kotlin代码进行简化:

  1. 因为Runnable类中只有一个待实现的方法:
Thread(Runnable { 
    
}).start()
  1. 如果Java方法的参数列表中不存在一个以上Java单抽象方法接口参数,我们还可以将接口进行省略
Thread({ 
            
}).start()
  1. 和之前kotlin中函数式Api用法类似,当Lambda表达式是方法的最后一个参数时,可将Lambda表达式移到方法括号的外面。并且,当Lambda表达式是方法的唯一一个参数时,可省略方法后面的括号
Thread{ 
            
}.start()

此用法在Android中的View设置点击监听时有体现

button.setOnClickListener{
    //...
}

五、空指针检查

kotlin为了避免产生空指针异常,所以默认所有的参数和变量都不可为空。kotlin将空指针异常的检查提前到了编译时期,如果我们的程序存在空指针异常的风险,在编译时期就会直接报错

1)空的类型系统 ?

写程序过程中,需要某个参数为null时怎么办呢?
kotlin提供了一套可为空的类型系统
用法:在类型名的后面加上一个问号

fun doStudy(studey: Study?){
    if(study != null){
    
    }
} 

使用可为空的类型系统后,需要自己处理空指针异常,不然代码不能编译通过。

2)判空辅助工具 ?.

如果每次都用if去处理空指针异常,会让代码显得啰嗦
所以用到了kotlin中的判空辅助工具 —— ?.操作符
使用:

//java
if(a != null){
    a.dosomthing          
}

//kotlin
a?.dosomthing
3)判空辅助工具 ?:

同样?:操作符:这个操作符左右两边都接收一个表达式,如果左边表达式的结果不为空,就返回左边表达式的结果,否则返回右边表达式的结果。

val c = if(a != null){
    a
}else{
    b
}

val c = a ?: b

综合使用:

fun getTextLength(text: String): Int{
    if(text != null){
        return text.length
    }
    return 0
}

fun getTextLength(text: String?) = text?.length ?: 0
4)判空辅助工具 !!——非空断言工具

当我们非常确信这里的对象不会为空时,想告诉kotlin,不用做空指针检查了

content!!.toUpperCase()

当你想要使用非空断言工具的时候,最好提醒一下自己,是不是有更好的实现方式

5)let

let不是操作符也不是关键字,而是一个函数(标准函数)。let函数的特性配合?.操作符在空指针检查时可以起到很大作用

//java
fun doStudy(study: Study){
    stydy?.readBooks()
    stydy?.doHomework()
}


//kotlin
fun doStudy(study: Study){
    stydy?.let{
        it.readBooks()
        it.doHomework()
    }
}

let让study对象本身作为参数传到Lambda表达式中,但当Lambda参数列表只有一个参数时,可以省略,并用it关键字代替

let函数是完全可以处理全局变量的判空问题的
当用if(study != null)这类判断空指针时,由于全局变量随时可能被其他线程修改,所以无法保证if语句中的study变量有没有空指针风险

六、Kotlin中的小魔术

1、 字符串内嵌表达式

对于java中使用+来连接字符串的方式,kotlin使用内嵌表达式${}来替代。

”hello,${obj.name}. nice to meet you“

当内嵌表达式只有一个变量时,可以省略其大括号

”hello,$obj.name. nice to meet you“

2、函数的参数默认值

构造函数默认值:在定义函数时给任意函数设定一个默认值,这样当调用此函数时就不会强制要求调用方为此函数传值,在没有传值的情况下自动使用参数的默认值

由于次构造函数在kotlin中很少用,因为kotlin给函数设定参数默认值的功能,使其很大程度上能替代次构造函数的作用

fun printParams(num: Int,str: String = "hello"){
    println("num is $num,str is $str")
}

当调用这个函数时,可以选择不传参数给第二个参数printParams(123)

fun printParams(num: Int = 123,str: String){
    println("num is $num,str is $str")
}

但当给第一个参数使用默认值时,我们使用printParams("hello")会出现类型不匹配问题,因为编译器会以为我们是给第一个参数赋值。所以kotlin提供了键值对的方式来传参printParams(str="hello")

函数默认值是如何替代次构造函数的呢?
class Student(val sno: String,val grade: Int,name: String,age: Int): Person(name,age){
    constructor(name: String,age: Int): this("",0,name,age){
    }

    constructor(): this("",0){
    }
}

这里的次构造函数主要是提供用更少的参数对Student进行实例化的方式。当加入默认参数值后,可以直接用一个主构造函数搞定

class Student(val sno: String = "",val grade: Int = 0,name: String = "",age: Int = 0): Person(name,age){
}

你可能感兴趣的:(Kotlin学习笔记(一))