《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库通过注解处理器自动生成不可变类和相关的辅助方法。在运行时,通过反射可以获取注解信息并进行相应的处理,如在依赖注入框架中,利用注解标识需要注入的依赖,在运行时进行依赖的查找和注入。

相关推荐
モンキー・D・小菜鸡儿12 小时前
Android14 新特性与适配指南
android·kotlin·安卓新特性
モンキー・D・小菜鸡儿15 小时前
Android15 新特性与适配指南
android·kotlin·安卓新特性
儿歌八万首16 小时前
Jetpack Compose 实战:实现手势缩放图片 (Zoomable Image) 组件
kotlin·android jetpack
モンキー・D・小菜鸡儿16 小时前
Android13 新特性与适配指南
gitee·kotlin·安卓新特性
天下无敌笨笨熊21 小时前
kotlin函数式编程
开发语言·数据库·kotlin
QING6181 天前
Kotlin Flow 去重 (distinctUntilChanged) 详解
kotlin·android studio·android jetpack
QING6181 天前
Kotlin Flow 节流 (Throttle) 详解
android·kotlin·android jetpack
Kapaseker1 天前
Context 知多少,组件通联有门道
android·kotlin
儿歌八万首2 天前
Jetpack Compose 实战:打造高性能轮播图 (Carousel) 组件
android·前端·kotlin
QING6182 天前
Kotlin Flow 防抖(Debounce)详解
android·kotlin·android jetpack