《Kotlin核心编程》中篇

类型系统

null 的问题及解决方案

1.null 问题

在传统编程语言如 Java 中,null 引用是一个常见的错误根源,容易引发空指针异常(NullPointerException),这类错误往往难以在编译期发现,常在运行时出现,增加了调试成本。

2.解决方案

  • 可空类型声明:Kotlin 通过在类型后加 ? 来表示可空类型,如 String?。这使开发者在代码中明确标记可能为 null 的变量,增强代码的安全性与可读性。例如,var nullableStr: String? = null

  • 安全调用操作符(?.):用于可空类型变量访问属性或调用方法,若变量为 null,表达式返回 null 而不抛出异常。例如,nullableStr?.length,若 nullableStr 为 null,该表达式返回 null。

  • Elvis 操作符(?:):用于提供默认值。当可空类型变量为 null 时,使用该操作符返回指定的默认值。如 val result = nullableStr?.length?: 0,若 nullableStr 为 null,result 取值为 0。

  • 非空断言操作符(!!):用于明确告知编译器该变量不可能为 null,若变量实际为 null,会抛出 NullPointerException。例如,val forceLength = nullableStr!!.length,若 nullableStr 为 null,会抛出异常。

类型层级

1.Any 类型

Kotlin 中所有类型的超类型是 Any,类似于 Java 中的 ObjectAny 定义了 equals()hashCode()toString() 等通用方法,所有类型的值都能赋值给 Any 类型变量。例如,val anyValue: Any = "Hello"

2.Nothing 类型

Nothing 是所有类型的子类型,没有实例。常用于表示不会正常返回的函数,如抛出异常或无限循环的函数。例如,fun fail(message: String): Nothing { throw IllegalArgumentException(message) }

3.Unit 类型

Unit 类型仅有一个实例 Unit,类似于 Java 中的 void,但在 Kotlin 中是实际类型。主要用于表示无返回有意义值的函数返回类型,如 fun printMessage(): Unit { println("This is a message") }

泛型更安全

1.泛型类和函数定义

Kotlin 支持在类、接口和函数中使用类型参数,增强代码的复用性与灵活性。例如,定义泛型类 Box<T>

Kotlin 复制代码
class Box<T>(val value: T) {
    fun getValue(): T {
        return value
    }
}

2.泛型约束

通过 where 关键字对泛型类型参数添加约束,确保类型参数满足特定条件。例如,限定泛型类型必须实现某个接口或继承某个类:

Kotlin 复制代码
fun <T : CharSequence> printLength(t: T) where T : Appendable {
    println(t.length)
}

3.泛型类型检查

在运行时,Kotlin 可对泛型类型进行一定程度的检查,尽管存在泛型擦除(后文详述),但通过特定手段(如内联函数结合 reified 关键字),可在运行时获取泛型类型信息,提高代码安全性与灵活性。例如:

Kotlin 复制代码
inline fun <reified T> isInstance(obj: Any): Boolean {
    return obj is T
}

泛型擦除和泛型变形

1.泛型擦除

与 Java 类似,Kotlin 在编译时会进行泛型擦除,即泛型类型信息在运行时会被擦除。这是为了保证与 Java 字节码的兼容性。例如,定义 List<String>List<Int>,在运行时它们的类型信息被擦除为原始类型 List

2.泛型变形

  • 协变(out):

    • 定义:使用 out 关键字声明泛型类型参数为协变。协变表示当 AB 的子类型时,Producer<A>Producer<B> 的子类型。协变类型参数只能用作输出(即作为函数的返回类型),不能用作输入(即作为函数的参数类型)。

    • 作用:协变增强了泛型的灵活性,使得代码可以更通用地处理不同子类型的对象。例如,在一个需要返回不同类型数据的生产者模式中,协变允许使用统一的接口来处理不同子类型的生产者。

Kotlin 复制代码
interface Producer<out T> {
    fun produce(): T
}
class StringProducer : Producer<String> {
    override fun produce(): String {
        return "Hello"
    }
}
val anyProducer: Producer<Any> = StringProducer()

//String 是 Any 的子类型,由于 Producer 接口的泛型参数 T 是协变的(out T),所以 StringProducer 可以赋值给 Producer<Any>。
  • 逆变(in

    • 定义:使用 in 关键字声明泛型类型参数为逆变。逆变与协变相反,当 AB 的子类型时,Consumer<B>Consumer<A> 的子类型。逆变类型参数只能用作输入(即作为函数的参数类型),不能用作输出(即作为函数的返回类型)。

    • 作用:逆变在需要处理不同类型输入的场景中很有用,例如,在一个消费者模式中,逆变允许使用一个更通用的消费者来处理不同子类型的对象。

Kotlin 复制代码
interface Consumer<in T> {
    fun consume(t: T)
}
class AnyConsumer : Consumer<Any> {
    override fun consume(t: Any) {
        println(t)
    }
}
val stringConsumer: Consumer<String> = AnyConsumer()
//这里,String 是 Any 的子类型,由于 Consumer 接口的泛型参数 T 是逆变的(in T),所以 AnyConsumer 可以赋值给 Consumer<String>。

Lambda和集合

Lambda 简化表达

Lambda 表达式是一段可传递给函数或存储在变量中的代码块。在 Kotlin 中,为了提升代码的简洁性,提供了多种 Lambda 简化表达的方式:

  • 省略参数类型:当上下文能明确推断出参数类型时,参数类型声明可省略。例如:
Kotlin 复制代码
// 完整写法,明确声明参数类型
val multiply: (Int, Int) -> Int = { a: Int, b: Int -> a * b }
// 简化写法,省略参数类型,编译器可根据上下文推断
val multiplySimplified: (Int, Int) -> Int = { a, b -> a * b }
  • 单个参数隐式名称:若 Lambda 只包含一个参数,可使用隐式名称it来引用该参数,无需显式声明。例如:
Kotlin 复制代码
// 完整写法,声明单个参数
val square: (Int) -> Int = { number -> number * number }
// 简化写法,使用it,更加简洁
val squareSimplified: (Int) -> Int = { it * it }
  • 成员引用语法:通过::操作符能够将成员函数或属性转换为函数引用,以此替代 Lambda 表达式,使代码更加清晰直观。例如:
Kotlin 复制代码
class StringUtil {
    fun upperCase(str: String): String = str.toUpperCase()
}
val util = StringUtil()
// 使用Lambda表达式
val upperCaseLambda: (String) -> String = { util.upperCase(it) }
// 使用成员引用
val upperCaseReference: (String) -> String = util::upperCase

集合 API

Kotlin 为集合操作提供了丰富的 API,极大地方便了开发者对集合的各种操作。

  • 创建集合
Kotlin 复制代码
// 创建不可变列表
val intList = listOf(1, 2, 3)
val stringList = listOf("apple", "banana", "cherry")
// 创建不可变集合
val intSet = setOf(1, 2, 3)
val stringSet = setOf("red", "green", "blue")
// 创建不可变映射
val map1 = mapOf("key1" to 1, "key2" to 2)
val map2 = mapOf("name" to "John", "age" to 30)
// 创建可变列表
val mutableIntList = mutableListOf(1, 2, 3)
val mutableStringList = mutableListOf("a", "b", "c")
// 创建可变集合
val mutableIntSet = mutableSetOf(1, 2, 3)
val mutableStringSet = mutableSetOf("one", "two", "three")
// 创建可变映射
val mutableMap1 = mutableMapOf("key1" to 1, "key2" to 2)
val mutableMap2 = mutableMapOf("city" to "New York", "country" to "USA")
  • 集合操作函数
Kotlin 复制代码
val numbers = listOf(1, 2, 3, 4, 5)
// filter操作,筛选出奇数
val oddNumbers = numbers.filter { it % 2!= 0 }
// map操作,将每个数平方
val squaredNumbers = numbers.map { it * it }
// reduce操作,计算数字乘积
val product = numbers.reduce { acc, number -> acc * number }

可变集合和只读集合

  • 可变集合:允许对集合中的元素进行添加、删除和修改操作。例如:
Kotlin 复制代码
val mutableList = mutableListOf(1, 2, 3)
mutableList.add(4)
mutableList.remove(2)
mutableList[0] = 0
  • 只读集合:仅能读取集合中的元素,不允许进行修改操作,否则会报错。例如:
Kotlin 复制代码
val readOnlyList = listOf(1, 2, 3)
// 以下操作会报错
// readOnlyList.add(4)
// readOnlyList.remove(2)
// readOnlyList[0] = 0
  • 转换操作:Kotlin 提供了相应的函数,能够在可变集合和只读集合之间进行转换。例如:
Kotlin 复制代码
val mutableList = mutableListOf(1, 2, 3)
// 转换为只读列表
val readOnlyList = mutableList.toList()
// 转换为只读集合
val readOnlySet = mutableList.toSet()
// 转换为只读映射
val readOnlyMap = mutableList.associate { it to it * 2 }.toMap()
val readOnlyList2 = listOf(1, 2, 3)
// 转换为可变列表
val mutableList2 = readOnlyList2.toMutableList()

惰性集合

惰性集合不会立即计算所有元素,而是在需要时才进行计算,这在处理大量数据时能显著提高性能。例如:

Kotlin 复制代码
val numbers = sequenceOf(1, 2, 3, 4, 5)
// 中间操作map,不会立即执行
val doubledNumbers = numbers.map { it * 2 }
// 直到调用末端操作toList,才会触发计算
val result = doubledNumbers.toList()

中间操作和末端操作

  • 中间操作:像map、filter、sortedBy等都属于中间操作,它们会返回一个新的集合或序列,并且不会立即执行计算,而是在遇到末端操作时才会被触发。例如:
Kotlin 复制代码
val numbers = listOf(1, 2, 3, 4, 5)
// 中间操作map和filter,此时不会执行
val processedNumbers = numbers.map { it * 2 }.filter { it > 5 }
  • 末端操作:例如forEach、toList、count、reduce等,它们会触发中间操作的计算,并返回最终结果或执行最终的消费操作。例如:
Kotlin 复制代码
val numbers = listOf(1, 2, 3, 4, 5)
// 中间操作map和filter,不会立即执行
val processedNumbers = numbers.map { it * 2 }.filter { it > 5 }
// 末端操作forEach,触发计算并消费结果
processedNumbers.forEach { println(it) }

内联函数

内联函数在编译时会将函数体的代码直接插入到调用处,从而避免了函数调用的开销,特别是在与 Lambda 表达式结合使用时,能有效减少 Lambda 带来的额外开销。例如:

Kotlin 复制代码
// 普通函数
fun multiply(a: Int, b: Int): Int = a * b
// 内联函数
inline fun inlineMultiply(a: Int, b: Int): Int = a * b
// 普通高阶函数
fun processList(list: List<Int>, action: (Int) -> Unit) {
    list.forEach(action)
}
// 内联高阶函数
// action作为 Lambda 表达式传递,会创建一个函数对象并进行函数调用;而在inlineProcessList函数中,由于其为内联函数,action的代码会直接被内联到forEach循环中,减少了中间环节的开销。
inline fun inlineProcessList(list: List<Int>, action: (Int) -> Unit) {
    list.forEach(action)
}

多态和扩展

多态

多态是指同一操作作用于不同的对象,可以有不同的解释和实现方式。在 Kotlin 中,多态主要通过方法重写和接口实现来体现。子类可以重写父类的方法,根据对象的实际类型来决定调用哪个类的方法。例如:

Kotlin 复制代码
open class Animal {
    open fun makeSound() {
        println("Animal makes a sound")
    }
}
class Dog : Animal() {
    override fun makeSound() {
        println("Dog barks")
    }
}
class Cat : Animal() {
    override fun makeSound() {
        println("Cat meows")
    }
}
fun main() {
    val animal1: Animal = Dog()
    val animal2: Animal = Cat()
    animal1.makeSound()
    animal2.makeSound()
}

这里,Animal类的makeSound方法被Dog和Cat类重写,通过Animal类型的变量调用makeSound方法时,实际调用的是对象具体类型的方法,实现了多态。

特设多态

特设多态是一种特殊的多态形式,通过函数重载来实现。函数重载允许在同一个类中定义多个同名但参数列表不同的函数。编译器会根据调用时传入的参数类型和数量来选择合适的函数版本。例如:

Kotlin 复制代码
class Calculator {
    fun add(a: Int, b: Int): Int {
        return a + b
    }
    fun add(a: Double, b: Double): Double {
        return a + b
    }
}
fun main() {
    val calculator = Calculator()
    val result1 = calculator.add(2, 3)
    val result2 = calculator.add(2.5, 3.5)
}

上述代码中,Calculator类的add函数有两个重载版本,分别处理Int和Double类型的参数,这就是特设多态的体现。

扩展函数

扩展函数允许在不修改类的源代码的情况下,为该类添加新的函数。通过扩展函数,我们可以为已有的类添加新的功能。例如:

Kotlin 复制代码
fun String.addExclamationMark() = this + "!"
fun main() {
    val greeting = "Hello"
    val newGreeting = greeting.addExclamationMark()
    println(newGreeting)
}

扩展函数接收器

扩展函数的接收器是指被扩展的类,在扩展函数定义中,接收器类型位于函数名之前。例如在fun String.addExclamationMark()中,String就是接收器类型,表示这个扩展函数是为String类添加的。接收器在扩展函数内部可以通过this关键字访问,也可以省略不写。通过这种方式,扩展函数可以访问接收器类的属性和方法,就像在类内部定义的方法一样。

元编程

元编程

元编程是一种编写能操作其他程序(或自身)作为数据的程序的技术。在 Kotlin 中,元编程使开发者可以在运行时获取和操作程序的结构、类型和行为等信息,增强了代码的灵活性和通用性。例如,通过元编程技术,可以实现动态代理,在运行时创建代理对象,对目标对象的方法调用进行拦截和处理,实现日志记录、事务管理等功能。

Kotlin 反射

Kotlin 反射是元编程的一种,提供了在运行时检查和操作 Kotlin 程序的类、函数、属性等元素的能力。借助反射,开发者可以在运行时获取类的构造函数、成员函数和属性,然后进行实例化对象、调用方法和访问属性值等操作。比如:

Kotlin 复制代码
import kotlin.reflect.KClass
class Person(val name: String, val age: Int)
fun main() {
    val personClass: KClass<Person> = Person::class
    val constructor = personClass.constructors.first()
    val person = constructor.call("Alice", 30)
    println(person.name)
}

在上述代码中,通过反射获取Person类的构造函数,创建了Person类的实例,并访问了其属性。反射在框架开发、依赖注入、序列化 / 反序列化等场景中广泛应用,但由于反射操作会带来一定的性能开销,在性能敏感的代码中需谨慎使用。

注解和注解处理器

注解是一种元数据,用于为程序元素(类、函数、属性等)添加额外信息,这些信息在编译时或运行时可以被读取和处理。Kotlin 提供了内置注解,开发者也可以自定义注解,常见的注解有:

Kotlin 复制代码
//@Deprecated:用于标记已过时的代码,使用时可提供替代方案或过时原因等信息。
@Deprecated("Use newFunction() instead", ReplaceWith("newFunction()"))
fun oldFunction() {
    println("This is an old function.")
}

//@JvmOverloads:Kotlin 函数默认不生成多个重载方法来适配不同参数情况,使用该注解后,会为函数生成多个重载方法,每个重载方法对应省略不同参数的情况,方便 Java 代码调用。
@JvmOverloads
fun greet(name: String = "World", age: Int = 18) = "Hello, $name, you are $age years old"
fun main() {
    println(greet())
    println(greet("Alice"))
    println(greet("Bob", 20))
}


//@BindView(ButterKnife 库):在 Android 开发中,通过该注解可以方便地绑定视图,减少findViewById的使用。
class MainActivity : AppCompatActivity() {
    @BindView(R.id.button)
    lateinit var button: Button
}

//@SerializedName(Gson 库):在使用 Gson 进行 JSON 序列化和反序列化时,用于指定对象属性与 JSON 字段之间的映射关系。
import com.google.gson.Gson
data class User(
    @SerializedName("user_name")
    val name: String?,
    @SerializedName("user_age")
    val age: Int?
)
fun main() {
    val json = """{"user_name":"John","user_age":30}"""
    val gson = Gson()
    val user = gson.fromJson(json, User::class.java)
    println("Name: ${user.name}, Age: ${user.age}")
}

//自定义注解
annotation class MyAnnotation(val value: String)
@MyAnnotation("This is a custom annotation")
class MyClass

注解处理器则负责在编译时或运行时读取和处理这些注解。在编译时,注解处理器可以生成额外的代码或执行特定的检查,如使用AutoValue库通过注解处理器自动生成不可变类和相关的辅助方法。在运行时,通过反射可以获取注解信息并进行相应的处理,如在依赖注入框架中,利用注解标识需要注入的依赖,在运行时进行依赖的查找和注入。

相关推荐
闲暇部落3 小时前
kotlin内联函数——runCatching
android·开发语言·kotlin
闲暇部落13 小时前
kotlin内联函数——takeIf和takeUnless
android·kotlin
张云瀚13 小时前
《Kotlin核心编程》下篇
kotlin·kotlin核心编程
命运之手1 天前
[ Spring ] Spring Cloud Gateway 2025 Comprehensive Overview
java·kotlin·gateway·spring-cloud
程序员江同学1 天前
Kotlin 技术月报 | 2025 年 1 月
android·kotlin
爱踢球的程序员-11 天前
Android:View的滑动
android·kotlin·android studio
划水哥~1 天前
Kotlin单例类
开发语言·kotlin
缘友一世1 天前
掌握Gradle构建脚本:Kotlin DSL配置指南与最佳实践
开发语言·kotlin·gradle
张云瀚1 天前
《Kotlin核心编程》上篇
kotlin·kotlin核心编程