Kotlin学习笔记

本文深入讲解Kotlin语言的基础知识,涵盖基本类型、控制流、类与对象、泛型及函数等内容,适合初学者快速掌握Kotlin核心概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

基本类型

1.nullable

2. ==,===

3. 几个复合符号

4.内联类

5. 字符串

1. 导入

控制流:if、when、for、while

if

 When

For

类与对象

类与继承

构造函数

属性与字段

幕后字段

幕后属性

接口

可见性修饰符

扩展

扩展函数

扩展属性

伴生对象的扩展

导入顶层定义的扩展

对象表达式与对象声明

对象表达式

对象声明

伴生对象

对象表达式和对象声明之间的语义差异

密封类

泛型

函数

单表达式函数

函数作用域

高阶函数与 lambda 表达式

函数类型

函数类型实例化

匿名函数


二话不说,直接开干。

基本类型

在 Kotlin 中,所有东西都是对象,在这个意义上讲我们可以在任何变量上调用成员函数与属性。 一些类型可以有特殊的内部表示——例如,数字、字符以及布尔值可以在运行时表示为原生类型值,但是对于用户来说,它们看起来就像普通的类。 在本节中,我们会描述 Kotlin中使用的基本类型:数字、字符、布尔值、数组与字符串。

1.nullable

Kotlin是null安全的语言,因此Byte、Short、Int、Long型变量都不能接受null值,如果要存储null值,则应该使用Byte?、Short?、Int?、Long?类型。

添加“?”后缀与不加后缀还有一个区别:普通类型的变量将会映射成Java的基本类型;带“?”后缀的整型变量将会映射成基本类型的包装类。

2. ==,===

==比较的是数值是否相等, 而===比较的是两个对象的地址是否相等。在 Java 平台数字是物理存储为 JVM 的原生类型,除非我们需要一个可空的引用(如Int? )或泛型。 后者情况下会把数字装箱。

val a: Int = 999
    val b: Int? = a
    val c: Int? = a
    println(b == c)    //true
    println(b === c)   //false


    val a: Int = 999
    val b: Int = a
    val c: Int = a
    println(b == c)     // true
    println(b === c)    // true


    val a: Int? = 999
    val b: Int? = a
    val c: Int? = a
    println(b == c)    //true
    println(b === c)   //true

val a: Int = 10000
println(a === a) // 输出“true”
val boxedA: Int? = a
val anotherBoxedA: Int? = a
println(boxedA === anotherBoxedA) // !!!输出“false”!!!

3. 几个复合符号

https://i-blog.csdnimg.cn/blog_migrate/58e0aa20a4212c09dc9ab898ef513891.png

https://i-blog.csdnimg.cn/blog_migrate/c053d8130e5c4078513f3a2caef39ba3.png

https://i-blog.csdnimg.cn/blog_migrate/c0ce7f33e9eca75193986f078b250f0f.png

https://i-blog.csdnimg.cn/blog_migrate/eb6fd7eb040d574747348b70004d392f.png

https://i-blog.csdnimg.cn/blog_migrate/fd816c15394c102b0565df9bcb017e0b.png

4.内联类

// 不存在 'Password' 类的真实实例对象
// 在运行时,'securePassword' 仅仅包含 'String'
val securePassword = Password("Don't try this in production")

内联类必须含有唯一的一个属性在主构造函数中初始化。在运行时,将使用这个唯一属性来表示内联类的实例,类似于内联函数中的代码被内联到该函数调用的地方。

5. 字符串

两种类型的字符串字面值: 1.[转义字符串]可以有转义字符, 以及2.[原始字符串]可以包含换行以及任意文本。

val s = "Hello, world!\n"

//使用三个引号( """ )分界符括起来,内部没有转义并且可以包含换行以及任何
//其他字符
val text = """
for (c in "foo")
print(c)
"""

原始字符串与转义字符串内部都可以包含模板表达式, ,即一些小段代码,会求值并把结果合并到字符串中。 模板表达式以美元符( $ )开头,由一个简单的名字构成:

fun main() {
//sampleStart
val i = 10
println("i = $i") // 输出“i = 10”

val s = "abc"
println("$s.length is ${s.length}") // 输出“abc.length is 3”
//sampleEnd
}

//在原始字符串中表示字面值 $ 字符(它不支持反斜杠转义):
val price = """
${'$'}9.99
"""

 

源文件通常以包声明开头:

package org.example

fun printMessage() { /*……*/ }

class Message { /*……*/ }

// ……

1. 导入

//有多个包会默认导入到每个 Kotlin 文件中:
kotlin.*
kotlin.annotation.*
kotlin.collections.*
kotlin.comparisons.* (自 1.1 起)
kotlin.io.*
kotlin.ranges.*
kotlin.sequences.*
kotlin.text.*

//根据目标平台还会导入额外的包:
JVM:
java.lang.*
kotlin.jvm.*
JS:
kotlin.js.*

//关键字 import 并不仅限于导入类;也可用它来导入其他声明:
import org.example.Message // Message 可访问
import org.test.Message as testMessage // testMessage 代表“org.test.Message”

控制流:if、when、for、while

if

if 是一个表达式,即它会返回一个值.,
// 传统用法
var max = a
if (a < b) max = b
// With else
var max: Int
if (a > b) {
max = a
} else {
max = b
}
//作为表达式, 如果你使用 if 作为表达式而不是语句(例如:返回它的值或者把它赋给变
//量),该表达式需要有 else 分支。
val max = if (a > b) a else b

if 的分支可以是代码块,最后的表达式作为该块的值:
val max = if (a > b) {
print("Choose a")
a
} else {
print("Choose b")
b
}

 When

when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> { // 注意这个块
print("x is neither 1 nor 2")
}
}
像 if 一样,可以作为表达式或者语句(例如:返回它的值或者把它赋给变量)
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")
}

For

//可以循环遍历任何提供了迭代器的对象
fun main() {
//sampleStart
for (i in 1..3) {
println(i)
}
for (i in 6 downTo 0 step 2) {
println(i)
}
//sampleEnd
}

fun main() {
val array = arrayOf("a", "b", "c")
//sampleStart
for (i in array.indices) {
println(array[i])
}
//sampleEnd
}

类与对象

类与继承

类声明由类名、类头(指定其类型参数、主构造函数等)以及由花括号包围的类体构成。类头与类体都是可选的; 如果一个类没有类体,可以省略花括号。

class Empty
class Invoice { /*……*/ }

构造函数

class Person constructor(firstName: String) { /*……*/ }
//主构造函数没有任何注解或者可见性修饰符,可以省略这个 constructor 关键字:
class Person(firstName: String) { /*……*/ }
//主构造函数不能包含任何的代码,在实例初始化期间,初始化块按照它们出现在类体中的顺序执行,与属性
//初始化器交织在一起:
//sampleStart
class InitOrderDemo(name: String) {
val customerKey = name.toUpperCase()
val firstProperty = "First property: $name".also(::println)
init {
println("First initializer block that prints ${name}")
}
val secondProperty = "Second property: ${name.length}".also(::println)
init {
println("Second initializer block that prints ${name.length}")
}
}
//sampleEnd
fun main() {
InitOrderDemo("hello")
}

声明属性以及从主构造函数初始化属性,Kotlin 有简洁的语法:

class Person(val firstName: String, val lastName: String, var age: Int) { /*……*/ }

与普通属性一样,主构造函数中声明的属性可以是可变的( var )或只读的( val )。如果构造函数有注解或可见性修饰符,这个 constructor 关键字是必需的,并且这些修饰符在它前面:

class Customer public @Inject constructor(name: String) { /*……*/ }

属性与字段

幕后字段

是不是Kotlin 所有属性都会有幕后字段呢?当然不是,需要满足下面条件之一:

         1.使用默认 getter / setter 的属性,一定有幕后字段。对于 var 属性来说,只要 getter / setter 中有一个使用默认实现,就会生成幕后字段;

         2.在自定义 getter / setter 中使用了 field 的属性

         幕后字段field指的就是当前的这个属性,它不是一个关键字,只是在setter和getter的这个两个特殊作用域中有着特殊的含义,就像一个类中的this,代表当前这个类。

// 例子一
class Person {
    var name:String = ""
        get() = field 
        set(value) {
            field = value
        }
}
// 例子二
class Person {
    var name:String = ""
}
//例子三
class Person(var gender:Gender){
    var name:String = ""
        set(value) {
            field = when(gender){
                Gender.MALE -> "Jake.$value"
                Gender.FEMALE -> "Rose.$value"
            }
        }
}


enum class Gender{
    MALE,
    FEMALE
}

fun main(args: Array<String>) {
    // 性别MALE
    var person = Person(Gender.MALE)
    person.name="Love"
    println("打印结果:${person.name}")
    //性别:FEMALE
    var person2 = Person(Gender.FEMALE)
    person2.name="Love"
    println("打印结果:${person2.name}")
}
//没有幕后字段的例子
class NoField {
    var size = 0
    //isEmpty没有幕后字段
    var isEmpty
        get() = size == 0
        set(value) {
            size *= 2
        }
}

幕后属性

对外表现为只读,对内表现为可读可写,我们将这个属性成为幕后属性。

private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
    get() {
        if (_table == null) {
            _table = HashMap() // 类型参数已推断出
        }
        return _table ?: throw AssertionError("Set to null by another thread")
}

接口

可见性修饰符

         在 Kotlin 中有这四个可见性修饰符: private 、protected 、 internal 和 public,类、对象、接口、构造函数、方法、属性和它们的 setter 都可以有 可见性修饰符。 (getter

总是与属性有着相同的可见性。)

         局部变量、局部函数和局部类不能有可见性修饰符。

扩展

扩展函数

fun MutableList<Int>.swap(index1: Int, index2: Int) {
	val tmp = this[index1] // “this”对应该列表
	this[index1] = this[index2]
	this[index2] = tmp
}
val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // “swap()”内部的“this”会保存“list”的值

扩展属性

注意:由于扩展没有实际的将成员插入类中,因此对扩展属性来说幕后字段是无效的。这就是为什么扩展属性不能有初始化器。他们的行为只能由显式提供的 getters/setters 定义。

val <T> List<T>.lastIndex: Int
get() = size – 1
val House.number = 1 // 错误:扩展属性不能有初始化器

//而:
var counter = 0 // 注意:这个初始器直接为幕后字段赋值
set(value) {
if (value >= 0) field = value
}

伴生对象的扩展

如果一个类定义有一个伴生对象 ,你也可以为伴生对象定义扩展函数与属性。就像伴生对象的常规成员一样, 可以只使用类名或者类名.Companion作为限定符来调用伴生对象的扩展成员:

class MyClass {
companion object { } // 将被称为 "Companion"
}
fun MyClass.Companion.printCompanion() { println("companion") }
fun main() {
MyClass.printCompanion()
}

导入顶层定义的扩展

package org.example.declarations
fun List<String>.getLongestString() { /*……*/}
//要使用所定义包之外的一个扩展,我们需要在调用方导入它:
package org.example.usage
import org.example.declarations.getLongestString
fun main() {
val list = listOf("red", "green", "blue")
list.getLongestString()
}

对象表达式与对象声明

对象表达式

object [: 0~N个父类型]{
    //对象表达式的类体部分
}

window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { /*……*/ }
override fun mouseEntered(e: MouseEvent) { /*……*/ }
})

open class A(x: Int) {
public open val y: Int = x
}
interface B { /*……*/ }
val ab: A = object : A(1), B {
override val y = 15
}

请注意,匿名对象可以用作只在本地和私有作用域中声明的类型。如果你使用匿名对象作为公有函数的返回类型或者用作公有属性的类型,那么该函数或属性的实际类型会是匿名对象声明的超类型,如果你没有声明任何超类型,就会是 Any 。在匿名对象中添加的成员将无法访问。

class C {
// 私有函数,所以其返回类型是匿名对象类型
private fun foo() = object {
val x: String = "x"
}
// 公有函数,所以其返回类型是 Any
fun publicFoo() = object {
val x: String = "x"
}
fun bar() {
val x1 = foo().x // 没问题
val x2 = publicFoo().x // 错误:未能解析的引用“x”
}
}

对象声明

总是在 object 关键字后跟一个名称。 就像变量声明一样,对象声明不是一个表达式,不能用在赋值语句的右边。

object  ObjectName [:  0~N个父类型]{
    //对象表达式的类体部分
}
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) {
// ……
}
val allDataProviders: Collection<DataProvider>
get() = // ……
}

DataProviderManager.registerDataProvider(……)

object DefaultListener : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { …… }
override fun mouseEntered(e: MouseEvent) { …… }
}

注意:对象声明不能在局部作用域(即直接嵌套在函数内部),但是它们可以嵌套到其他对象声明或非内部类中。

伴生对象

//类内部的对象声明可以用 companion 关键字标记:
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}

val instance = MyClass.create()
//可以省略伴生对象的名称,在这种情况下将使用名称 Companion :
class MyClass {
companion object { }
}
val x = MyClass.Companion

//其自身所用的类的名称(不是另一个名称的限定符)可用作对该类的伴生对象 (无论是否具名)的引用:
class MyClass1 {
companion object Named { }
}
val x = MyClass1
class MyClass2 {
companion object { }
}
val y = MyClass2

对象表达式和对象声明之间的语义差异

对象表达式和对象声明之间有一个重要的语义差别:

对象表达式是在使用他们的地方立即执行(及初始化)的;

对象声明是在第一次被访问到时延迟初始化的;

伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配。

密封类

         虽然密封类也可以有子类,但是所有子类都必须在与密封类自身相同的文件中声明。

sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()

         一个密封类是自身抽象的,它不能直接实例化并可以有抽象( abstract )成员。扩展密封类子类的类(间接继承者)可以放在任何位置,而无需在同一个文件中。使用密封类的关键好处在于使用 when 表达式。当然,这只有当你用 when 作为表达式(使用结果)而不是作为语句时才有用。

fun eval(expr: Expr): Double = when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
// 不再需要 `else` 子句,因为我们已经覆盖了所有的情况
}

枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的子类类型集合也是受限的,密封类的一个子类可以有可包含状态的多个实例。

泛型

参数化类型是不变的( invariant ),类型信息在编译之后已经写入class文件中:

public class Stack<E> {
publi c Stack();
publ E vo i d push(E e);
public E pop();
public boolan i sEmpty();
}

增加一个方法:

之后使用:

修改参数类型为:

才能正确运行。

再增加一个方法:

之后使用:

后有类似以上报错信息,修改参数类型:

         注意: 不要用通配符类型作为返回类型。除了为用户提供额外的灵活性之外,它还会强制用户在客户端代码中使用通配符类型。我们可以标注 Source的类型参数 T 来确保它仅从 Source<T> 成员中返回(生产),并从不被消费。

         in,它使得一个类型参数逆变:只可以被消费而不可以被生产。逆变类型的一个很好的例子是 Comparable :

fun copy(from: Array<out Any>, to: Array<Any>) { …… },from 不仅仅是一个数组,而是一个受限制的(投影的)数组:我们只可以调用返回类型为类型参数 T 的方法,如上,这意味着我们只能调用get() 。用法是对应于Java 的 Array<? Extends Object> 。

 

fun fill(dest: Array<in String>, value: String) { …… }

Array<in String> 对应于 Java 的 Array<? super String> ,也就是说,你可以传递一个

CharSequence 数组或一个 Object 数组给 fill() 函数。

 

函数

         Kotlin 函数都是头等的,这意味着它们可以存储在变量与数据结构中、作为参数传递给其他高阶函数以及从其他高阶函数返回。可以像操作任何其他非函数值一样操作函数。

单表达式函数

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

fun double(x: Int): Int = x * 2

当返回值类型可由编译器推断时,显式声明返回类型是可选的:

fun double(x: Int) = x * 2

函数作用域

         函数可以在文件顶层声明,此外除了顶层函数,Kotlin 中函数也可以声明在局部作用域、作为成员函数以及扩展函数。

局部函数

fun dfs(graph: Graph) {
    val visited = HashSet<Vertex>()
    fun dfs(current: Vertex) {
        if (!visited.add(current)) return
        for (v in current.neighbors)
        dfs(v)
    }
    dfs(graph.vertices[0])
}

 尾递归函数

         当一个函数用 tailrec 修饰符标记并满足所需的形式时,编译器会优化该递归,留下一个快速而高效的基于循环的版本:

val eps = 1E-10 // "good enough", could be 10^-15
tailrec fun findFixPoint(x: Double = 1.0): Double
    = if (Math.abs(x - Math.cos(x)) < eps) x else findFixPoint(Math.cos(x))

这段代码计算余弦的不动点(fixpoint of cosine),这是一个数学常数。最终代码相当于这种更传统风格的代码:

val eps = 1E-10 // "good enough", could be 10^-15
private fun findFixPoint(): Double {
    var x = 1.0
    while (true) {
        val y = Math.cos(x)
        if (Math.abs(x - y) < eps) return x
        x = Math.cos(x)
    }
}

要符合 tailrec 修饰符的条件的话,函数必须将其自身调用作为它执行的最后一个操作。在递归调用后有更多代码时,不能使用尾递归,并且不能用在 try/catch/finally 块中。目前在Kotlin for JVM 与 Kotlin/Native 中支持尾递归。

高阶函数与 lambda 表达式

函数类型

         所有函数类型都有一个圆括号括起来的参数类型列表以及一个返回类型: (A, B) -> C表示接受类型分别为 A 与 B 两个参数并返回一个 C 类型值的函数类型。 参数类型列表可以为空,如 () -> A 。 Unit 返回类型不可省略。

         函数类型可以有一个额外的接收者类型,它在表示法中的点之前指定: 类型 A.(B) ->C 表示可以在 A 的接收者对象上以一个 B 类型参数来调用并返回一个 C 类型值的函数。 带有接收者的函数字面值通常与这些类型一起使用。

挂起函数属于特殊种类的函数类型,它的表示法中有一个 suspend 修饰符 ,例如

suspend () -> Unit 或者 suspend A.(B) -> C 。

如需将函数类型指定为可空,请使用圆括号: ((Int, Int) -> Int)? 。

函数类型可以使用圆括号进行接合: (Int) -> ((Int) -> Unit)箭头表示法是右结合的, (Int) -> (Int) -> Unit 与前述示例等价,但不等于 ((Int) -> (Int)) -> Unit 。

         使用类型别名给函数类型起一个别称:

         typealias ClickHandler = (Button, ClickEvent) -> Unit

函数类型实例化

有几种方法可以获得函数类型的实例:

使用函数字面值的代码块,采用以下形式之一:

lambda 表达式: { a, b -> a + b } ,

匿名函数: fun(s: String): Int { return s.toIntOrNull() ?: 0 }

带有接收者的函数字面值可用作带有接收者的函数类型的值。

 

使用已有声明的可调用引用:

顶层、局部、成员、扩展函数: ::isOdd 、 String::toInt ,

顶层、成员、扩展属性: List<Int>::size ,

构造函数: ::Regex

这包括指向特定实例成员的绑定的可调用引用: foo::toString 。

 

使用实现函数类型接口的自定义类的实例:

class IntTransformer: (Int) -> Int {
override operator fun invoke(x: Int): Int = TODO()
}
val intFunction: (Int) -> Int = IntTransformer()

带与不带接收者的函数类型非字面值可以互换,其中接收者可以替代第一个参数,反之亦然。例如, (A, B) -> C 类型的值可以传给或赋值给期待 A.(B) -> C 的地方,反之亦然:

fun main() {
//sampleStart
    val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }
    val twoParameters: (String, Int) -> String = repeatFun // OK
    fun runTransformation(f: (String, Int) -> String): String {
        return f("hello", 3)
    }
    val result = runTransformation(repeatFun) // OK
//sampleEnd
    println("result = $result")
}

匿名函数

fun(x: Int, y: Int): Int = x + y
fun(x: Int, y: Int): Int {
return x + y
}
ints.filter(fun(item) = item > 0)

         Lambda表达式与匿名函数之间的另一个区别是非局部返回的行为。一个不带标签的 return语句总是在用 fun 关键字声明的函数中返回。这意味着 lambda 表达式中的 return 将从包含它的函数返回,而匿名函数中的 return 将从匿名函数自身返回。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值