Kotlin基础入门

函数

函数和方法指的是同一个东西,只是叫法不一样,在Kotlin中,习惯函数多一些。

函数构成:

屏幕截图 2026-01-09 200801

fun 是构建函数时的关键词。

Kotlin中必需要且仅要一个main函数作为程序的切入口,会首先执行main函数里的指令,是程序中的起点。

函数命名:首单词小写,后续单词首字母大写,运用动词或动词词组作为函数名。

有返回值的函数结构:

屏幕截图 2026-01-10 185507

实参和形参

形参是定义函数时的函数输入的变量名称,实参是调用函数时对应的形参的传递给函数的值。

有形参的函数的结构:

屏幕截图 2026-01-10 191110

形参位于括号里,形式:变量1名称: 变量1数据类型; 变量2名称: 变量2数据类型

形参类似于val定义的变量,Kotlin中形参不可变。

调用函数传递实参时添加形参称为“具名实参”,可以用不同于形参的位置来传入实参。

函数名称和形参统称为“函数签名”。

函数语法糖

当有返回值的函数体内的代码只有一行的时候,有一个语法糖:

fun name(parameters): return-data-type = return后面的代码

例如:fun largerNumber(num1: Int, num2: Int): Int = max(num1, num2) 就是

fun largerNumber(num1: Int, num2: Int): Int {

    return max(num1, num2)

的语法糖,该例子还可以进一步简化,因为max函数返回的值一定是整数,返回值的数据类型可以简化掉,变成fun largerNumber(num1: Int, num2: Int) = max(num1, num2)

变量

如何声明一个变量:

屏幕截图 2026-01-09 212242

val 为不可变变量,var 为可更新可变变量。尽可能使用 val 而不是 var。

有初始值则数据类型可以省略,没有则不能省略。

变量命名:仍然是驼峰形式

基本数据类型有:Int, Double, Float, String, Boolean。

Boolean 类型可串联至 String 类型。

程序的逻辑控制

程序的执行语句分为三种:顺序语句,条件语句和循环语句。

条件语句

条件语句分为if和when两种。

if语句

一个例子:

fun largerNumber(num1: Int, num2: Int): Int {

    var value0 = 0

    if (num1 > num2) {

        value0 = num1

    } else {

        value0 = num2

    }

    return value0

}

因为value0的值后来要发生变化,因此类型是var。

if语句大部分和java一样,但额外的是Kotlin的if语句可以有返回值的,返回值是if语句每一个条件下的最后一行代码的返回值,使用if语句每一个条件的最后一行代码作为返回值。因此原例子可以改写为:

fun largerNumber(num1: Int, num2: Int): Int {

    val value0 = if (num1 > num2) {

        num1

    } else {

        num2

    }

    return value0

}

因为这里的value0不需要再次赋值,所以类型是val。仔细观察可以发现value0也是不必要的,上述例子可以改为

fun largerNumber(num1: Int, num2: Int): Int {

    return if (num1 > num2) {

        num1

    } else {

        num2

    }

}

因为函数体里的代码虽然不是一行代码,但作用和一行代码一样,即返回if语句的返回值,故利用语法糖可以改为

fun largerNumber(num1: Int, num2: Int) = if (num1 > num2) {

    num1

} else {

    num2

}

这个函数还可以精简。

fun largerNumber(num1: Int, num2: Int) = if (num1 > num2) num1 else num2

when语句

when语句也可以有返回值。

例如

fun getScore(name: String) = if (name == "Tom") {
    86
} else if (name == "Jim") {
    77
} else if (name == "Jack") {
    95
} else if (name == "Lily") {
    100
} else {
    0
}

可以通过when语句精简成

fun getScore(name: String) = when (name) {

    "Tom" -> 86

    "Jim" -> 77

    "Jack" -> 95

    "Lily" -> 100

    else -> 0

}

注意else不要漏掉或者忽略。when语句的内部是“匹配值 -> { 执行逻辑 }”,当执行逻辑只有一行的时候,{}可以省略。上述例子是精确匹配,when语句还可以进行类型匹配,例如

fun checkNumber(num: Number) {

    when (num) {

        is Int -> println("number is Int")

        is Double -> println("number is Double")

        else -> println("number not support")

    }

}

其中,is 关键字是上述例子的类型匹配中的核心,相当于java的instanceof关键字。Number是Kotlin内置的一个抽象类,是Int, Long, Float, Double等与数字相关的类的父类。

还有一种用when语句的方法来写上面的getScore函数,就是不在when语句中传入形参。

fun getScore(name: String) = when {

    name == "Tom" -> 86

    name == "Jim" -> 77

    name == "Jack" -> 95

    name == "Lily" -> 100

    else -> 0

}

这种用法将逻辑判断的表达式完整地写在when语句的结构体中,虽然在本例中看起来有点冗余,但在需要逻辑判断的时候却非常好用。例如名字是Tom开头的人都是一样的86分时,我们可以把第一行代码写成name.startsWith("Tom") -> 86,其他不变。

循环语句

while语句

和java中没有任何区别。while循环会先判断布尔表达式的真假,只有是真的情况下才会运行结构内部的代码,如果布尔表达式一开始为假,循环内容一次都不会执行。

结构为

while (布尔表达式) {

    //循环内容

}

例如

var x = 1

while (x < 4) {

    println("$x is " + x)

    x++

}

for语句

 for-in循环。

区间的概念:0..10指的是一个从0到10的闭区间,数学上就是[0, 10]

例如:

for (i = 0..10) {

    println(i)

}

实际应用中,左闭右开的区间更加常用,0 until 10指的是左闭右开从0到10的区间,数学上就是[0, 10),例如:

for (i = 0 until 10) {

    println(i)

}

默认中,for-in循环每次都会递增1,如果想要递增2、3的话,就是0 until 10 step 2或者0 until 10 step 3,例如

for (i = 0 until 10 step 2) {

    println(i)

}

用..和until创建的区间都是升序的区间,可以用downTo创建降序的闭区间,例如10 downTo 1数学上指的就是[10, 1]。

面向对象编程

类和对象

Kotlin是面向对象编程的语言,这意味着它可以创建类。类是对事物的一种封装,通常来说就是对事物的一种概括,例如一本书《毛选》就是一本书,它所属于的类就是book,类名一般是名词。类中有属性,这是这一类事物的通用属性,例如书有价格,那么价格就是书的属性,属性一般是名词;类中有函数(也叫作方法),就是类的行为,例如人要吃饭和睡觉,那么吃饭和睡觉就是人这一类的方法,方法一般是动词。

同Java一样,Kotlin也是通过class来声明类的。一个类例如

class Person {

    var name = ""

    var age = 0

    fun eat() {

        println("$name is eating and he or she is $age years old.")

    }

}

注意定义类中的属性是用var,因为创建实例的时候需要对类中的属性进行重新赋值,所以只能用var。

对上述的类创建一个实例,例如

fun main() {

    val p = Person()

    p.name = "Mary"

    p.age = 18

    p.eat()

}

与Java不同,创建实例的时候不需要new关键字。

概括事物创建类,定义属性和方法(能力、行为)为字段和函数(方法),创建实例,对类中的属性进行赋值,调用类中的字段和方法。

继承与构造函数

与Java不同,Kotlin中任何一个非抽象类默认都是不可以被继承的,因为类和变量一样,最好不可变。而抽象类因为无法创建实例,只有抽象类的非抽象子类才能创建这个子类的实例,所以默认是可以被继承的。Java中的对象指的就是类的实例。Kotlin中的抽象类和Java中的抽象类没有区别。

定义Java中的抽象类:在class前面加上abstract关键字,抽象类除了不能有实例之外和其他普通类没有区别。例如

abstract class Book {

}

属性和方法和构造函数都可以有。如果需要设计一个类使得该类中的一个方法的实现由它的子类来具体确定,那么就可以定义这个类为抽象类,这个方法为抽象方法。抽象方法同样由abstract关键字来定义。抽象方法没有方法体,只有一个方法名,方法名之后不需要有花括号。例如

abstract class Book {

    abstract fun read()

}

如果一个类中包含一个抽象方法,那么这个类一定是抽象类;任何包含抽象方法的抽象类的非抽象子类必须重写父类中的这个抽象方法,或者这个子类也是一个抽象类(这样就可以不用重写)。构造方法不可以声明为抽象方法。抽象类中的抽象方法的数量没有限制,任意。

在Kotlin中,要让一个类B继承另外一个类A,我们需要做两件事:让A可以被继承,让B继承A。因为在Kotlin中,普通的类默认是不能够被继承的。

要让一个类可以被继承,只要在class前面加上open关键字就可以了。例如

open class Person {

    var name = ""

    var age = 0

}

我们让Student类继承Person类,例如

class Student : Person() {

    var sno = ""

    var grade = 0

}

冒号指的是让Student类继承Person类,那么后面那个小括号是什么意思和作用呢?这个涉及到Kotlin的构造函数。Kotlin的构造函数分为主构造函数和次构造函数。

在Kotlin中,任何一个类都默认有一个不带参数的主构造函数,你也可以给它指明参数,它的特点是没有函数体,直接定义在类名后面,例如

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

}

这样,你创建Student的实例时必须传入主构造函数的两个参数sno和grade。例如

val s1 = Student("a123", 2)

因为你是创建实例时传入的参数,没有重新赋值,所以属性的变量类型可以是val。

主构造函数没有函数体,如果想在主构造函数中写代码,可以再init结构体中写,例如

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

    init {

        println("student id is $sno")

    }

}

这里在init结构体中写代码,如果创建一个Student的实例,那么init结构体的代码也会被执行。

迄今为止,我们仍然没有说构造函数和那对小括号有什么关系。这涉及另外一个知识点,Java中子类的构造函数必须调用父类的构造函数,Kotlin中也是如此。子类的主构造函数也必须调用一个父类中的构造函数,虽然我们可以在init结构体中调用,但因为绝大部分时候我们都是不写init结构体的,所以有另外一种办法,子类的主构造函数调用父类的哪个构造函数,由继承的时候由括号来指定。

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

}

上述代码中Person后面空括号表示的意思是Student子类的主构造函数在继承的时候调用Person父类中的无参数主构造函数(因为每个类默认都有一个无参数的主构造函数,而Person类在定义的时候没有其他构造函数),因此在括号内没有参数的情况下,也不能被省略。

如果在Person类中将名字和年龄都放到主构造函数中,例如

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

}

那么前面为Person创建实例的代码会出错。上述Student类在继承的时候也会报错。

我们专注于Student类的继承这块。想要解决这个错误,就必须给Person类的构造函数传入name和age字段,例如

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

}

这时候,Student类调用的Person类的构造函数就是有name和age参数的主构造函数,而由于Student类的主构造函数需要调用它,因此在Student的主构造函数中,也有name和age属性。注意,不能在Student类的主构造函数中在name和age前面加val或者var,因为加上之后会变成Student类的属性,并与父类的属性造成冲突。因此前面不加关键字,让它的作用域仅限于Student类的主构造函数中。

我们可以对Student类进行实例化,例如

val s1 = Student("a123", 2, "Lily", 12)

 关于次构造函数:任何类都最多只能有一个主构造函数,只能是零个或者一个,但次构造函数的数量没有限制,可以是零个,一个或者多个。次构造函数也可以用于实例化一个类,但它有函数体。Kotlin规定,如果一个类既有主构造函数,又有次构造函数,那么这个类中的所有次构造函数必须调用其主构造函数,包括间接调用。次构造函数通过constructor关键字来定义,例如

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) {

    }

}

其中,this关键字用于调用同类中的另外一个构造函数。第一个次构造函数接收name和age两个参数,同时通过this关键字调用主构造函数并将sno和grade初始化为“”和0,第二个次构造函数不接收参数,同时通过this关键字调用第一个次构造函数,并将name和age也初始化为""和0,由于第二个构造函数间接调用了主构造函数,这仍然是合法的。

因此,我们有三种方式来将Student进行实例化。例如

val s1 = Student("a123", 3, "Lily", 14)

val s2 = Student("Jack", 13)

val s3 = Student()

关于主构造函数和次构造函数,还有一种特殊情况,十分少见,那就是没有主构造函数只有次构造函数。在Kotlin中,如果没有显式地定义主构造函数但定义了次构造函数,那么此时类就是没有主构造函数的,只有次构造函数,因此此时的子类里的次构造函数只能直接调用父类的构造函数,例如

class Student : Person {

    constructor(name: String, age: Int) : super(name, age) {

    }

}

其中,因为没有主构造函数,所以Person后面没有小括号,同时,子类中的次构造函数只能用super关键字来直接调用父类中的构造函数。没有主构造函数时,所有次构造函数只能用super关键来直接调用父类中的构造函数,来初始化父类。

接口

 接口是实现多态编程的重要组成部分。Java是单继承的语言,任何类都最多只能继承一个类,但可以实现任意多个接口。Kotlin也是如此。接口由interface关键字定义,接口中的函数不要求有函数体,例如

interface Study {

    fun readBooks()

    fun doHomework()

}

在Kotlin中,接口本身不能有构造函数,不能被实例化。例如

fun main() {

  val study = Study()

}

是不合法的。

在Kotlin中,实现接口的关键字是冒号,中间用逗号分隔开,且接口后面不用加括号,因为接口中没有构造函数可以调用。例如

class Student(name: String, age: Int) : Person(name, age), Study {

    override fun readBooks() {

        println("$name is reading books.")

    }

    override fun doHomework() {

        println("$name is doing homework.")

    }

}

上述代码表示Student类继承了Person父类,并实现了Study接口。其中,我们用override关键字来重写父类中的方法或者实现接口中的函数。

我们可以通过实例化Student类来调用接口中的两个函数,例如

fun main() {

    val s1 = Student("Lily", 12)

    doStudy(s1)

}

fun doStudy(study: Study) {

    study.readBooks()

    study.doHomework()

}

上述代码中,我们可以通过Student类的实例来直接调用Study接口的两个函数,也可以通过上述代码的方式,来创建一个doStudy()函数(接收一个Study类型的参数(函数和类的参数类型也可以是接口类型)),因为Student类实现了Study接口,所以它的实例是可以传递给doStudy()函数当实参的,在doStudy()函数中调用Study接口里的两个函数,这整个过程叫做面向接口编程,也称为多态。

在Kotlin中,可以在接口中的函数进行默认实现。例如

interface Study {

    fun readBooks()

    fun doHomework() {

        println("do homework default implementation.")

    }

}

这时,在Student类中,可以重写doHomework()函数也可以不重写,但一定要重写readBooks()函数。

接口中的没有函数体的函数默认是抽象的,而实现接口的类必须重写接口中的抽象函数。在类实现多个(两个或以上)不同接口且接口中有重复函数名的函数时,也必须重写重复的函数。

Java中有四种修饰符,public,private,protected,default。

Kotlin中也有四种修饰符,public,private,protected,internal。

修饰符 Java Kotlin
public 所有类可见 所有类可见,默认
private 当前类可见 当前类可见
protected 当前类、子类、同一包路径下的类可见 当前类、子类可见
default 同一包路径下的类可见,默认 没有
internal 没有 同一模块中的类可见

如果我们开发了一个模块供别人使用,但有一些函数只允许在模块内部调用,不想暴露给外部,就可以将此函数声明成internal。

数据类和单例类

Java中的数据类,通常需要重写equals()、hashCode()、toString()这几个方法,equals()用于判断两个数据类是否相等,hashCode()方法作为equals()的配套方法,也需要一起重写,否则会导致HashMap、HashSet等hash相关的系统类无法正常工作,toString()方法用于提供更清晰的输入日志。

假设我们新构建一个手机数据类,字段只包含品牌和价格两个字段。例如

data class Cellphone(val brand: String, val price: Double)

上述代码只在class前面加了data,表明这个类是一个数据类。Kotlin会根据主构造函数中的参数帮你将equals()、hashCode()、toString()等方法自动生成。而且当一个类中没有代码时,尾部的花括号也可以省略。我们可以创建Cellphone对象,看看加data和不加data有什么不同。

fun main() {

    val c1 = Cellphone("Samsung", 1299.99)

    val c2 = Cellphone("Samsung", 1299.99)

    println(c1)

    println(c2)

    println("cellphone1 equals cellphone2 ${c1 == c2}")

}

加data时,输出是

Cellphone(brand=Samsung, price=1299.99)

Cellphone(brand=Samsung, price=1299.99)

cellphone1 equals cellphone2 = true

不加data时,输出是

Cellphone@7530d0a

Cellphone@1ddc4ec2

cellphone1 equals cellphone2 = false

单例类:用于只允许一个类在全局最多只能拥有一个实例。定义单例类的关键字是object,把普通类中的class改成object后,这个类就变成了一个单例类。例如

object Singleton {

    fun singletonTest() {

        println("singletonTest is called.")

    }

}

上述代码创建了一个单例类,并在其中定义了一个singletonTest函数。

调用单例类中的函数的代码如下:

Singleton.singletonTest()

上述代码中,Kotlin在背后自动帮我们创建了一个Singleton类的实例,并保证全局只存在一个Singleton实例。

Lambda编程

集合的创建与遍历

传统意义上的集合主要是List和Set,广泛一点,Map这样的键值对的数据结构也算是集合。

在Java中,List、Set、Map在Java中都是接口,List的主要实现类是ArrayList和LinkedList,Set的主要实现类是HashSet,Map的主要实现类是HashMap。

如果我们要创建一个包含水果名称的集合,可以用以下代码:

val list = ArrayList<String>()

list.add("Apple")

list.add("Banana")

list.add(1)

以上代码可以创建一个集合,但是因为最后加入的1不是String类型,会报错。对于需求,我们有另外一种解决方式,如下:

val list = listOf("Apple", "Banana", 1)

以上代码不仅更加简洁的创建了一个集合,而且最后加入的1也不会报错。

如果你想要创建一个只由一种数据类型组成的集合,建议用方法一,其他情况下用方法二。

我们可以用for-in循环来对集合进行遍历,例如

for (i in list) {

    println(i)

}

需要注意的是,listOf()函数创建的是一个不可变集合,也就是我们无法增加、修改、删除,只能用于读取,初衷和val不可变变量、类默认不可继承是一样的。对于需要进行变化的集合,我们可以用mutableListOf()函数,和listOf()函数一样的用法,也是集合内包含不同类型的数据合法。List集合可以用索引来读取,例如:list[元素索引]

Set集合和List集合的用法几乎一摸一样,但初始化的简单方式换成了相应的setOf()和mutableSetOf()(允许不同类型的数据同在一个Set集合中),初始化的复杂方式换成了HashSet<数据类型>()(只允许一种类型的数据),而且Set集合里面不允许有重复元素,如果有重复元素只会保留其中一个。Set集合的读取方式目前没找到。

Map集合的使用方法:Map是键值对的形式,如果我们给上述水果名称添加一个对应编号,则可以这样:

val map1 = HashMap<String, Int>()

map1.put("Apple", 1)

map1.put("Banana", 2)

添加的简单方式可以是:map["Apple"] = 1

读取数据的时候可以这样写:val num = map["Apple"],同样,以上这种方式只适合Map集合的数据类型固定的情况。

同样地,Kotlin有简单版本的初始化Map集合的方式,mapOf()函数和mutableMapOf()函数,与List和Set类似,例如:

val map1 = mapOf("Apple" to 1, "Banana" to 2, 34 to "Grape")

同样地,以上方式适合除了数据类型必须固定情况外的所有情况。

以上方式用for-in遍历的代码如下:

for ((k1, k2) in map1) {

    println("k1 is $k1 and k2 is $k2.")

}

集合的函数式API

Lambda的定义:一小段可以作为参数传递的代码,借助Lambda可以传入一小段代码,而Kotlin对这段代码的长度没有限制,但通常不建议太长,影响代码可读性。

Lambda表达式的语法结构:{参数名1: 参数类型, 参数名2: 参数类型 -> 函数体}

上述结构中,最外部是一对花括号,如果有参数传入,需要声明参数列表,参数列表结尾->符号,表示参数列表的的结束和函数体的开始,函数体中可以编写任意行代码(不建议太长,影响可读性),最后一行代码自动作为Lambda表达式的返回值。

例如,找到一个水果名称集合中的单词最长的水果名称,我们可以这样写:

val list = listOf("Apple", "Banana", "Grape", "Orange", "Pear", "Watermelon")

val lambda = { fruit: String -> fruit.length }

val maxLengthFruit = list.maxBy(lambda)

以上代码中,maxBy函数的工作原理是根据我们传入的条件(本例中为单词长度)来遍历集合,找到该条件下的最大值。本例中,它接受一个Lambda类型的参数,并且在遍历集合时把每次遍历的值作为参数传递给Lambda表达式。

我们不需要专门定义一个lambda变量,因此

val maxLengthFruit = list.maxBy({ fruit: String -> fruit.length })

因为Kotlin规定,当Lambda这个参数是函数的最后一个参数时,可以将Lambda表达式移到括号外,因此

val maxLengthFruit = list.maxBy() { fruit: String -> fruit.length }

如果Lambda这个参数是函数的唯一参数时,函数的括号可以省略,因此

val maxLengthFruit = list.maxBy { fruit: String -> fruit.length }

因为Kotlin中的出色的类型推导,因此String也是不必要的,因此

val maxLengthFruit = list.maxBy {fruit -> fruit.length}

当Lambda表达式的参数列表只有一个参数时,不必声明参数名,可以用it关键字替代,因此

val maxLengthFruit = list.maxBy { it.length }

map函数时最常用的一种函数式API,用于将集合中的每个元素映射成另一个值,映射的规则在Lambda表达式中指定,最终生成一个新集合。例如我们想将所有水果名变成大写,可以

val newList = list.map { it.uppercase() }

以上代码中,toUpperCase()函数已被弃用,用uppercase()代替,下例中同样toLowerCase()已被弃用,用lowercase()代替。

转换成小写,可以

val newList2 = list.map { it.lowercase() }

取单词首字母,可以

val newList3 = list.map { it[0] }

映射成单词长度的集合,可以

val newList4 = list.map { it.length }

另外一个比较常用的函数式API,filter函数,用于过滤集合中的数据,可以单独使用,也可以搭配map函数一起使用。例如我们只想保留5个单词以内的水果并大写:

val newList5 = list.filter { it.length <= 5 }

                             .map { it.uppercase() }

以上代码中,如果先用map再用filter也可以达到同样的效果,但是效率会比较低一些,因为先过滤再映射效率会更高。

还有两个比较常用的函数式API,any和all函数,any函数用于判断集合中是否存在元素使得条件成立,all函数用于判断集合中的所有元素是否都符合条件。例如

val anyResult = list.any { it.length <= 5 }  //输出true

val allResult = list.all { it.length <= 5 }  //输出false

调用Java方法时使用函数式API

疑问

疑问一

例如

class Person {

    var name = ""

    var age = 0

}

fun main() {

    val p = Person()

    p.name = "Lily"

    p.age = 12

}

我们为什么不可写成这样来实现同样的功能呢?

class Person {

    val name: String // 这里使用val关键字先不给name赋值,等到实例化的时候再给它赋值,为什么不可以,会报错?

    val age: Int // 同上

}

fun main() {

    val p = Person()

    p.name = "Lily"

    p.age = 12

posted @ 2026-01-09 21:31  HelenHung  阅读(9)  评论(0)    收藏  举报