kotlin 介绍
Kotlin 是一种现代、简洁且强大的编程语言,由 JetBrains 于 2011 年推出。它旨在解决 Java 的一些缺点,例如冗长的代码和缺乏安全性。Kotlin 与 Java 兼容,这意味着它可以在 Java 虚拟机 (JVM) 上运行,并且可以使用 Java 库。
Kotlin 的主要特点包括:
- 简洁: Kotlin 的语法简洁且易于学习,这使得它成为初学者和经验丰富的开发人员的理想选择。
- 安全: Kotlin 是一种静态类型语言,这意味着它可以在编译时检测到许多错误。这有助于防止运行时错误并提高代码的可靠性。
- 互操作性: Kotlin 与 Java 兼容,这意味着它可以与 Java 代码一起使用。这使得 Kotlin 成为现有 Java 项目的理想选择。
- 支持函数式编程: Kotlin 支持函数式编程范式,这使得它非常适合编写并发代码和处理大数据。
- 强大的工具支持: Kotlin 拥有强大的工具支持,包括集成开发环境 (IDE) 和编译器。这使得 Kotlin 非常适合大型项目和团队开发。
Kotlin 在许多领域都有着广泛的应用,包括:
- Android 开发: Kotlin 是 Google 官方推荐的 Android 开发语言之一。它非常适合编写 Android 应用,因为它可以提高代码的可读性和安全性。
- Web 开发: Kotlin 可以用于编写 Web 应用,例如 REST API 和 Web 服务。它与 Spring Boot 等流行的 Web 框架兼容。
- 桌面应用开发: Kotlin 可以用于编写桌面应用,例如图形用户界面 (GUI) 应用和命令行工具。它与 Swing 和 JavaFX 等流行的桌面应用框架兼容。
- 游戏开发: Kotlin 可以用于编写游戏,例如 2D 和 3D 游戏。它与 LibGDX 等流行的游戏开发框架兼容。
Kotlin 是一种非常有前途的编程语言,它拥有强大的功能和广泛的应用领域。随着 Kotlin 的不断发展,它将在越来越多的领域得到应用。
kotlin 的基础知识
kotlin的数据类型
在Kotlin中,数据类型可以大致分为两大类:基本类型和引用类型。
基本类型
基本类型(或称为原始类型)在Kotlin中有特殊的表示方式,但底层上它们是借用了Java的原始类型。Kotlin的基本类型包括:
- 数字类型:包括
Int
、Long
、Short
、Byte
、Double
、Float
。这些类型分别对应于Java的int
、long
、short
、byte
、double
、float
。 - 字符:
Char
,用于表示单个字符。 - 布尔:
Boolean
,用于表示真(true
)或假(false
)。
引用类型
引用类型是指除了基本类型以外的所有类型,它们引用的是对象的内存地址。在Kotlin中,几乎一切都是对象,这包括了:
- 字符串:
String
,表示字符序列。 - 数组:
Array
,用来存储固定大小的同类型元素。 - 集合类型:包括
List
、Set
、Map
等,这些类型在Kotlin中是接口,具体实现依赖于标准库中的类,如ArrayList
、HashSet
、HashMap
等。 - 数字类型:
Number
,Number
类型是一个表示数值的泛型类,它是所有内置数值类型的超类。这包括Byte
,Short
,Int
,Long
,Float
, 和Double
。这意味着你可以使用Number
类型来引用任何数值类型的变量,而不需要具体指定它是什么类型。这在处理需要泛型或者不确定具体数值类型的情况下非常有用。 - 自定义类:你可以定义自己的类,这些类的实例都是引用类型。
特殊类型
- Any:Kotlin中所有类的超类,相当于Java中的
Object
。如果一个函数可以接受任何类型,那么它的参数类型可以用Any
。 - Unit:表示函数没有返回值的类型,相当于Java的
void
。 - Nothing:表示永远不会返回的类型。通常用于函数,如果函数是抛出异常,那么它的返回类型就是
Nothing
。这对于类型推断非常有用。 - Nullable类型:在类型名后面加
?
表示该类型的变量可以持有null
值。例如,String?
可以是一个字符串,也可以是null
。
Kotlin在设计上力求简洁和安全,其中类型系统的设计就体现了这一点,例如通过区分可空类型和非空类型,编译器能够在编译时期捕获大多数的空指针异常。
类定义
class MyClass(val name: String) {
// 类体
}
class
关键字表示这是一个类定义。MyClass
是类的名称。(val name: String)
是类的主构造函数。主构造函数是在创建类实例时调用的函数。val
关键字表示name
是一个只读属性。String
是name
的类型。name: String
是参数列表。参数列表包含主构造函数的参数。{}
是类体的开始和结束。类体包含类的成员。- 类成员可以是属性、函数、构造函数等。
属性
属性是类的成员,用于存储数据。属性可以是只读的或可写的。
- 只读属性使用
val
关键字声明。只读属性只能在主构造函数中初始化。 - 可写属性使用
var
关键字声明。可写属性可以在主构造函数中或类体中初始化。
class MyClass(val name: String) {
var age: Int = 0
}
age
是一个可写属性。Int
是age
的类型。0
是age
的初始值。
在 Kotlin 中,属性修饰符用于控制属性的可见性和可变性。这些修饰符可以应用于类、对象和接口中的属性。
以下是 Kotlin 中可用的属性修饰符:
- public: 表示属性对任何代码都是可见的,包括在其他模块中。
- protected: 表示属性对类及其子类是可见的,但不包括在其他模块中。
- internal: 表示属性仅对同一个模块中的代码是可见的。
- private: 表示属性仅对声明它的类是可见的。
除了这些可见性修饰符之外,Kotlin 还提供了以下可变性修饰符:
- val: 表示属性是不可变的,这意味着它不能被重新赋值。
- var: 表示属性是可变的,这意味着它可以被重新赋值。
属性修饰符可以组合使用,例如:
private val name: String = "John Doe"
这将创建一个名为 name
的私有不可变属性,并将其初始化为字符串 "John Doe"
。
属性修饰符的示例
以下是一些属性修饰符的示例:
- public val name: String - 创建一个公共不可变属性名为
name
,类型为String
。 - protected var age: Int - 创建一个受保护的可变属性名为
age
,类型为Int
。 - internal val address: String - 创建一个内部不可变属性名为
address
,类型为String
。 - private var balance: Double - 创建一个私有可变属性名为
balance
,类型为Double
。
最佳实践
- 使用
val
来声明不可变属性,除非您需要显式地重新赋值。 - 使用
var
来声明可变属性,但只在需要时才使用它。 - 考虑使用
private
来声明属性,除非您需要在其他类或模块中访问它们。 - 考虑使用
internal
来声明属性,如果您需要在同一个模块中的其他类中访问它们。 - 考虑使用
protected
来声明属性,如果您需要在子类中访问它们。 - 考虑使用
public
来声明属性,如果您需要在其他模块中访问它们。
属性的 get/set 方法定义
Kotlin 属性的 set/get 方法允许您控制对属性值的访问。
- set() 方法用于设置属性值。
- get() 方法用于获取属性值。
您可以通过在属性声明中使用 set
和 get
关键字来定义这些方法。例如:
class A {
var a: Int = 0
set(value) {
if (value < 0) {
throw IllegalArgumentException("a must be positive, but was $value")
} field = value
}
get() = field
}
在上面的示例中,A
类有一个名为 a
的属性,它是一个 Int 类型整形。
a
属性的 set()
方法检查新值是否为负数,如果是则抛出异常。否则,它将新值存储在 field
字段中。
a
属性的 get()
方法返回 field
字段的值。
属性重写
属性重载是 Kotlin 中的一项功能,它允许您为同一属性定义多个不同的实现。这在您需要在不同情况下使用不同值的属性时非常有用。
要重载属性,您需要使用 override
关键字。例如,以下代码演示了如何重载一个名为 name
的属性:
class Person(val name: String) {
override var name: String = ""
}
在上面的代码中,Person
类有一个名为 name
的属性,它被初始化为一个空字符串。Person
类还重载了 name
属性,这样就可以在对象创建后更改该属性的值。
要使用重载的属性,您需要使用 super
关键字。例如,以下代码演示了如何使用重载的 name
属性:
val person = Person("Alice")
person.name = "Bob"
在上面的代码中,person
对象的 name
属性最初被设置为 “Alice”。但是,使用 person.name = "Bob"
语句将该属性的值更改为 “Bob”。
属性重载是一个强大的功能,它可以用于各种情况。例如,您可以使用属性重载来:
- 在不同的环境中使用不同的配置值。
- 在不同的设备上使用不同的资源。
- 为不同的用户提供不同的体验。
属性重载是一种非常灵活的功能,它可以帮助您编写更灵活和可重用的代码。
函数
函数是类的成员,用于执行代码。函数可以有参数,也可以没有参数。
class MyClass(val name: String) {
fun greet() {
println("Hello, $name!")
}
}
greet
是一个函数。()
是参数列表。参数列表包含函数的参数。{}
是函数体的开始和结束。函数体包含函数的代码。
指向性传参
fun sum(a: Int, c: (Int) -> Unit, b: Int = 2) {
c(a + b)
}
sum(a = 1,b = 4, c = {
println(it)
})
- 函数修饰符
inline
:将函数内联到调用它的位置,而不是创建一个单独的函数调用。这可以提高性能,尤其是在函数很小且经常被调用的时候。tailrec
:表示函数是尾递归的,即函数的最后一步是递归调用自身。这可以防止栈溢出,因为尾递归函数不需要为每次递归调用创建一个新的栈帧。suspend
:表示函数是一个挂起函数,即它可以在协程中被挂起和恢复。挂起函数可以使用await
关键字来等待其他挂起函数的结果。noinline
:表示函数不能被内联。这通常用于防止函数被内联到一个不能访问其自由变量的上下文中。crossinline
:表示函数可以被内联,但它不能访问其自由变量。这通常用于防止函数被内联到一个会改变其自由变量值的上文中。varargs
:表示函数可以接受可变数量的参数。可变参数必须是函数的最后一个参数。operator
:表示函数可以被用作运算符。例如,你可以定义一个+
运算符函数来重载加法运算。infix
:表示函数可以被用作中缀运算符。例如,你可以定义一个+
中缀运算符函数来重载加法运算。external
:表示函数是一个外部函数,即它不是由 Kotlin 编译器生成的。外部函数通常用于调用原生库中的函数。expect
:表示函数是一个期望函数,即它将在另一个模块中实现。期望函数通常用于创建库,以便其他模块可以引用库中的函数,而无需知道库的实现细节。
构造函数
构造函数是类的成员,用于创建类实例。构造函数可以在类名后面使用 ()
调用。
val myClass = MyClass("John")
MyClass("John")
是一个构造函数调用。myClass
是一个MyClass
类的实例。
kotlin 的构造函数
Kotlin的构造函数可以分为两种类型:主构造函数和次构造函数。
- 主构造函数:
主构造函数是类头的一部分,可以在类名后面声明。主构造函数没有任何代码体,它定义了类的属性。
例如:
class Person(val name: String, val age: Int) {
// 类的属性在主构造函数中定义
}
在上面的例子中,Person
类有两个属性name
和age
,它们被声明在主构造函数中。
- 次构造函数:
次构造函数是可选的,用于提供额外的初始化逻辑。一个类可以有多个次构造函数,它们需要使用constructor
关键字来声明。
例如:
class Person(val name: String, val age: Int) {
constructor(name: String) : this(name, 0) {
// 次构造函数中调用了主构造函数
}
}
在上面的例子中,Person
类有一个主构造函数和一个次构造函数。次构造函数接受一个名为name
的参数,并调用了主构造函数来初始化剩余的属性。
除了主、次构造函数外,Kotlin还支持初始化块(初始化代码块)和委托给其他构造方法。这些功能提供了更灵活和强大的初始化选项。
继承
继承允许一个类从另一个类继承属性和函数。继承使用 :
关键字表示。
class MyClass(val name: String) {
fun greet() {
println("Hello, $name!")
}
}
class Subclass(name: String) : MyClass(name) {
fun goodbye() {
println("Goodbye, $name!")
}
}
Subclass
从MyClass
继承属性和函数。Subclass
有一个自己的构造函数。Subclass
有一个自己的函数goodbye
。
接口
接口是一个定义方法签名的类。接口不能有属性或函数的实现。接口使用 interface
关键字声明。
interface MyInterface {
fun greet()
}
class MyClass : MyInterface {
override fun greet() {
println("Hello!")
}
}
MyInterface
是一个接口。MyClass
实现MyInterface
接口。MyClass
必须实现greet
方法。
初始化
Kotlin 中类的初始化分成了三块,他们的执行顺序也是依次执行的:
- 主构造方法
- 次构造方法
- 初始化块(initializer blocks),即 init 块
这边常用的 init 块的两种场景是:
- 多个构造方法存在的情况下,可以将统一处理的逻辑加到 init 块中
- 在只存在主构造方法的情况下,相关初始化属性的逻辑处理可以放到 init 块中
class CountDownView : LinearLayout {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
orientation = HORIZONTAL
gravity = Gravity.CENTER_VERTICAL + Gravity.START
}
}
抽象类
抽象类是一个不能被实例化的类。抽象类必须至少有一个抽象方法。抽象方法没有实现。抽象类使用 abstract
关键字声明。
abstract class MyClass {
abstract fun greet()
}
class Subclass : MyClass() {
override fun greet() {
println("Hello!")
}
}
MyClass
是一个抽象类。Subclass
从MyClass
继承。Subclass
必须实现greet
方法。
枚举类
枚举类是一个包含一组常量的类。枚举类使用 enum class
关键字声明。
enum class MyEnum {
A, B, C
}
val myEnum = MyEnum.A
MyEnum
是一个枚举类。A
,B
,C
是枚举类的常量。myEnum
是一个MyEnum
类的实例。
内部类
Kotlin 中的内部类与 Java 中的内部类非常相似,它们都可以访问外部类的成员。Kotlin 中的内部类可以分为两种:嵌套类和局部类。
嵌套类
嵌套类是声明在另一个类中的类。嵌套类可以访问外部类的成员,包括私有成员。嵌套类通常用于将相关类组织在一起,或者将实现细节隐藏在外部类中。
class Outer {
private val name = "Outer"
class Nested {
fun accessOuter() {
println("Name: $name")
}
}
}
fun main() {
val outer = Outer()
val nested = Outer.Nested()
nested.accessOuter()
}
局部类
局部类是声明在函数或其他局部作用域中的类。局部类只能访问其所在作用域内的成员,包括私有成员。局部类通常用于将实现细节隐藏在函数或其他局部作用域中。
fun main() {
fun localClassExample() {
class Local {
fun accessLocal() {
println("Local class")
}
}
val local = Local()
local.accessLocal()
}
localClassExample()
}
内部类的访问控制
内部类的访问控制与 Java 中的内部类非常相似。内部类可以具有以下访问控制修饰符:
public
:内部类可以从任何地方访问。protected
:内部类只能从其所在的包及其子包中访问。internal
:内部类只能从其所在的模块中访问。private
:内部类只能从其所在的类中访问。
内部类的使用场景
内部类可以用于以下场景:
- 将相关类组织在一起。
- 将实现细节隐藏在外部类中。
- 在函数或其他局部作用域中创建临时类。
- 创建匿名类。
匿名内部类
匿名内部类是没有任何名称的内部类。匿名内部类通常用于实现接口或抽象类。
fun main() {
val runnable = object : Runnable {
override fun run() {
println("Running...")
}
}
val thread = Thread(runnable)
thread.start()
}
总结
Kotlin 中的内部类与 Java 中的内部类非常相似,它们都可以访问外部类的成员。Kotlin 中的内部类可以分为两种:嵌套类和局部类。嵌套类是声明在另一个类中的类,局部类是声明在函数或其他局部作用域中的类。内部类可以用于将相关类组织在一起,将实现细节隐藏在外部类中,在函数或其他局部作用域中创建临时类,创建匿名类。
密封类
Kotlin 密封类是一种限制子类只能在同一文件中定义的类。这与 Java 中的枚举类相似,但密封类更灵活,因为它允许子类具有不同的构造函数和属性。
密封类的语法如下:
sealed class Shape {
class Circle(val radius: Double) : Shape()
class Square(val sideLength: Double) : Shape()
class Rectangle(val width: Double, val height: Double) : Shape()
}
在上面的示例中,Shape
是一个密封类,它有三个子类:Circle
、Square
和 Rectangle
。这些子类只能在同一文件中定义,并且不能在其他文件中使用。
密封类可以用于以下场景:
- 当您需要限制子类只能在同一文件中定义时。
- 当您需要确保所有子类都具有相同的基类时。
- 当您需要在子类之间进行模式匹配时。
密封类的主要优点是它可以提高代码的可读性和可维护性。通过将所有子类都放在同一文件中,您可以更容易地看到它们之间的关系并理解它们是如何工作的。此外,密封类还可以帮助您避免在子类之间出现不一致的情况。
密封类的主要缺点是它可能会限制您的灵活性。如果您以后需要添加一个新的子类,则必须修改密封类并重新编译所有相关的代码。
总体而言,密封类是一种非常强大的工具,可以帮助您编写更清晰、更可维护的代码。如果您需要限制子类只能在同一文件中定义,那么密封类是一个很好的选择。
以下是密封类的几个示例:
- 枚举类:枚举类是一种特殊的密封类,它只能有一个实例。枚举类通常用于表示一组有限的可能值。
- 数据类:数据类是一种密封类,它具有自动生成的构造函数、getter 和 setter 方法,以及
toString()
和equals()
方法。数据类通常用于表示数据对象。 - 接口:接口是一种密封类,它只能包含抽象方法。接口通常用于定义一组方法,这些方法必须由实现该接口的类实现。
数据类
数据类(data class)是 Kotlin 中定义数据的一种方式,它提供了简洁的语法和许多有用的功能。数据类与普通类相似,但具有以下特点:
- 数据类的主构造函数必须包含至少一个参数。
- 数据类的主构造函数的参数必须都是 val 或 var 类型的。
- 数据类的主构造函数的参数必须都是不可变的类型。
- 数据类的主构造函数的参数不能有默认值。
- 数据类的主构造函数的参数不能是可变参数。
- 数据类的主构造函数的参数不能是 Lambda 表达式。
数据类提供了以下功能:
- 自动生成的 equals() 和 hashCode() 方法。
- 自动生成的 toString() 方法。
- 自动生成的 copy() 方法。
- 自动生成的 componentN() 方法。
数据类非常适合用于表示具有多个属性的对象,例如 Person、Address、Car 等。
以下是一个数据类的例子:
data class Person(val name: String, val age: Int)
这个数据类表示一个人,它具有两个属性:name 和 age。
我们可以使用数据类来创建对象:
val person = Person("John", 30)
我们可以使用数据类的属性来访问对象的数据:
println(person.name) // John
println(person.age) // 30
我们可以使用数据类的 copy() 方法来创建新的对象:
val newPerson = person.copy(age = 31)
这个新的对象与 person 对象相同,但 age 属性的值为 31。
我们可以使用数据类的 componentN() 方法来访问对象的数据:
val (name, age) = person
这与以下代码相同:
val name = person.name
val age = person.age
数据类是 Kotlin 中定义数据的一种非常方便的方式。它们提供了简洁的语法和许多有用的功能。
伴生对象 companion object
伴生对象(companion object)是 Kotlin 中的一个特殊类,它与类本身紧密相关,并且可以访问类本身的私有成员。伴生对象通常用于定义静态方法和属性,这些方法和属性与类本身相关,但并不属于任何特定实例。
要声明一个伴生对象,您可以在类中使用 companion object
关键字,如下所示:
class MyClass {
companion object {
// 伴生对象的内容
}
}
伴生对象可以访问类本身的私有成员,包括私有构造函数。这使得伴生对象非常适合用于初始化类本身的私有状态,或者提供对类本身私有成员的访问。
伴生对象还可以定义静态方法和属性,这些方法和属性与类本身相关,但并不属于任何特定实例。例如,您可以使用伴生对象来定义一个工厂方法,该方法可以创建该类的实例,如下所示:
class MyClass {
companion object {
fun create(): MyClass {
// 创建 MyClass 的实例并返回
}
}
}
val instance = MyClass.create()
伴生对象还可以定义静态属性,这些属性与类本身相关,但并不属于任何特定实例。例如,您可以使用伴生对象来定义一个常量,该常量包含类的版本号,如下所示:
class MyClass {
companion object {
const val VERSION = "1.0.0"
}
}
println(MyClass.VERSION) // 输出: 1.0.0
最佳实践:
- 使用伴生对象来定义静态方法和属性,这些方法和属性与类本身相关,但并不属于任何特定实例。
- 使用伴生对象来初始化类本身的私有状态,或者提供对类本身私有成员的访问。
- 不要在伴生对象中定义与特定实例相关的方法或属性。
泛型
kotlin的泛型介绍
Kotlin 的泛型设计让你的代码更加灵活和复用性更高。它允许你定义一个工作于多种类型上的类、接口或方法。这里是一些关键概念和它们的应用方式:
1. 泛型类和接口
在 Kotlin 中,你可以创建泛型类或接口,这样它们就可以用不同的数据类型实例化。这是通过在类或接口名后添加尖括号(<
和>
)和类型参数来实现的。比如:
class Box<T>(t: T) {
var value = t
}
这里,T
是一个类型参数,可以在创建Box
类的实例时替换成任何类型。
2. 泛型函数
函数也可以是泛型的,方法类似。类型参数位于函数名之前:
fun <T> singletonList(item: T): List<T> {
return listOf(item)
}
这个例子中的singletonList
函数可以用任何类型的item
调用,并返回包含该项的List
。
3. 泛型约束
Kotlin 允许你限制泛型类型参数必须继承自特定的父类型。这是通过where
关键字实现的:
fun <T> append(item: T, list: List<T>) where T : CharSequence, T : Appendable {
// ...
}
这个例子中,T
必须同时是CharSequence
和Appendable
的子类型。
4. 类型擦除和型变
- 类型擦除:Kotlin 的泛型在运行时类型信息被擦除。这意味着在运行时你不能直接查询泛型的具体类型。
- 型变:Kotlin 通过使用
in
(逆变)和out
(协变)关键字处理泛型的型变。out
使得一个泛型类可以安全地作为其类型参数的超类型使用,而in
则相反。
5. 星号投影(Star Projection)
星号投影是一种特殊的类型参数,用于表示你不关心具体的类型参数。例如,如果你有一个List<*>
,这表示你对列表中元素的具体类型不感兴趣。
通过这些机制,Kotlin 的泛型提供了代码复用和类型安全性的强大工具,同时保持了灵活性和表达力。
基础概念
泛型类型参数:在定义类、接口或函数时,你可以声明一个或多个类型参数,使其能够处理不同的数据类型。这些类型参数在使用时会被实际的类型替换。
使用示例:
class Box<T>(t: T) {
var value = t
}
val box: Box<Int> = Box(1) // 指定T为Int类型
泛型约束
你可以限制泛型类型参数必须继承自特定的父类型,这称为泛型约束。Kotlin 使用 :
来指定约束。
约束示例:
fun <T: Number> List<T>.sum(): T {
// ...
}
这个函数限制了泛型类型 T
必须是 Number
或其子类型。
类型擦除
Kotlin 的泛型在运行时会进行类型擦除,这意味着泛型信息不会在运行时保留。因此,你不能直接查询一个泛型的运行时类型,但是可以通过其他方式(例如传递类引用)来间接获取这些信息。
星号投影(Star Projection)
有时候,你可能不关心泛型的具体类型参数,只是想表达某种泛型无论其类型参数如何都可以接受。这时,可以使用星号投影。
示例:
fun printList(list: List<*>) {
list.forEach { println(it) }
}
变型:in、out
Kotlin 中的泛型变型通过关键字 in
和 out
来控制泛型类型的协变和逆变。
- 协变(out):如果类的泛型类型参数只作为输出,可以使用
out
关键字。这意味着子类型关系将被保留。 - 逆变(in):如果类的泛型类型参数只作为输入,可以使用
in
关键字。逆变操作反转了子类型关系。
变型示例:
class Producer<out T> { /*...*/ }
class Consumer<in T> { /*...*/ }
Kotlin 的泛型系统既强大又灵活,通过合理使用,可以编写出既类型安全又易于维护的代码。
定义变量和常量
kotlin 定义变量和常量
变量
变量用于存储可以更改的值。要定义变量,请使用 var
关键字,后跟变量的名称和类型。例如:
var name = "John Doe"
这将创建一个名为 name
的变量,并将其初始化为字符串 "John Doe"
。
不可变变量
不可变变量是使用val
关键字来定义的。一旦给这样的变量分配了一个值,就不能再更改这个值。换句话说,这种类型的变量是只读的。
这里有一个简单的例子来演示如何声明和使用不可变变量:
fun main() {
val name = "John" // 声明一个不可变变量并初始化为"John"
println(name) // 输出: John
// 下面这行将会引发编译时错误,因为我们试图修改一个不可变的变量
// name = "Doe"
}
在上面的例子中,我们创建了一个名为name
的不可变变量,并将其初始化为字符串"John"。尝试更改name
的值将会导致编译错误,因为val
关键字表示该变量是只读的。
使用不可变性是函数式编程范式中的一个重要概念。它有助于创建更安全、更易于维护和并发友好的代码。通过限制对数据结构内容修改的能力,可以降低程序中出现意外和难以跟踪错误的风险。此外,在多线程环境下,由于数据本身不会改变,所以读取操作无需额外同步措施,从而简化了并发程序设计。
在Kotlin中,默认推荐先考虑使用不可变对象(通过val
定义),除非有特定需求需要修改对象状态才使用可变对象(通过var
定义)。
常量
常量用于存储不能更改的值。在kotlin中要定义常量,需要在类的顶级或者伴生对象中才可定义
class Test {
companion object {
const val HELLO = "hello"
}
}
这将创建一个名为 PI
的常量,并将其初始化为数字 3.141592653589793
。
类型推断
Kotlin 支持类型推断,这意味着您不必显式指定变量或常量的类型。编译器将从变量或常量的值推断出类型。例如:
var name = "John Doe"
val PI = 3.141592653589793
编译器将推断 name
的类型为 String
,PI
的类型为 Double
。
可变性和不可变性
变量是可变的,这意味着它们的值可以更改。常量是不可变的,这意味着它们的值不能更改。
范围
变量和常量的范围是指它们可以在程序中使用的位置。局部变量只能在定义它们的函数或代码块中使用。全局变量可以在程序的任何地方使用。
初始化
变量和常量必须在使用前初始化。变量可以通过赋值运算符(=
)初始化。常量可以通过 val
关键字或 const
关键字初始化。
const
关键字用于初始化编译时常量。编译时常量在编译时计算,并且不能在运行时更改。
使用
变量和常量可以使用其名称访问。例如:
println(name)
println(PI)
这将输出:
John Doe
3.141592653589793
对象定义
kotlin 的对象定义通过 var 关键字指定格式为 var [对象名称]:[对象显示类型指定] = [对象值]
// 类定义
class MyClass {
// 类成员变量
var name: String = "John Doe"
var age: Int = 25
// 类成员函数
fun sayHello() {
println("Hello, my name is $name and I am $age years old.")
}
}
// 对象定义
val myObject = MyClass()
// 访问类成员变量
println(myObject.name) // John Doe
// 访问类成员函数
myObject.sayHello() // Hello, my name is John Doe and I am 25 years old.
kotlin 的多线程
多线程创建
Kotlin 是一种支持多线程的现代编程语言。多线程允许程序同时执行多个任务,从而提高程序的效率和性能。Kotlin 提供了多种方式来创建和管理线程,包括:
- 协程 (Coroutine):协程是 Kotlin 中的一种轻量级线程,它可以暂停和恢复执行。协程可以轻松地创建和管理,并且它们不会阻塞线程。
- 线程 (Thread):线程是操作系统中的一个执行单元,它可以独立于其他线程运行。线程可以创建和管理,但它们比协程更重量级,并且它们可能会阻塞线程。
- 并发 (Concurrency):并发是指多个任务同时执行。Kotlin 提供了多种并发库,可以帮助您编写并发程序。
以下是一些使用 Kotlin 进行多线程编程的示例:
// 创建一个协程
val coroutine = launch {
// 执行协程中的代码
}
// 创建一个线程
val thread = Thread {
// 执行线程中的代码
}
// 使用并发库编写并发程序
val executorService = Executors.newFixedThreadPool(4)
executorService.submit {
// 执行并发任务
}
Kotlin 的多线程特性可以帮助您编写高效、高性能的程序。如果您需要在程序中同时执行多个任务,那么可以使用 Kotlin 的多线程特性来实现。
结构化并发
协程
协程是一种轻量级的并发原语,它允许您在单个线程中暂停和恢复多个任务。这意味着您可以编写并发代码,而无需担心线程管理和同步。
异步编程
异步编程是一种编程范式,它允许您在不阻塞当前线程的情况下执行任务。这意味着您可以同时执行多个任务,而无需等待每个任务完成。
通道
通道是一种通信机制,它允许协程之间交换数据。通道可以是单向或双向的,并且可以缓冲数据。
选择器
选择器是一种机制,它允许协程等待多个通道上的事件。当某个通道上有事件发生时,选择器会通知协程,以便协程可以处理该事件。
并发库
Kotlin 提供了丰富的并发库,包括协程、异步编程、通道和选择器。这些库可以帮助您编写高效且可扩展的并发代码。
示例
以下是一个使用协程和通道实现的简单并发程序:
import kotlinx.coroutines.*
fun main() = runBlocking {
val channel = Channel<Int>()
launch {
for (i in 1..10) {
channel.send(i)
}
channel.close()
}
for (value in channel) {
println(value)
}
}
这个程序使用协程来同时执行两个任务:发送数据和接收数据。协程 launch
用于发送数据,协程 for
用于接收数据。通道 channel
用于在两个协程之间交换数据。
优点
Kotlin 的结构性并发具有以下优点:
- 易于使用:Kotlin 的并发库易于使用,即使您是并发编程的新手,也可以轻松上手。
- 高效:Kotlin 的并发库非常高效,可以帮助您编写高性能的并发程序。
- 可扩展:Kotlin 的并发库非常可扩展,可以帮助您编写可扩展到大量并发任务的程序。
缺点
Kotlin 的结构性并发也有一些缺点:
- 学习曲线陡峭:Kotlin 的并发库学习曲线陡峭,需要花费一些时间才能掌握。
- 可能难以调试:并发程序可能难以调试,尤其是当您不熟悉并发编程时。
结论
Kotlin 的结构性并发非常强大,可以帮助您编写高效且可扩展的并发程序。但是,Kotlin 的并发库学习曲线陡峭,可能难以调试。如果您是并发编程的新手,建议您先学习一些并发编程的基本知识,然后再使用 Kotlin 的并发库。
launch 和 async 的区别
launch 和 async 都是 Kotlin 协程中的函数,用于在协程中执行代码块。它们的主要区别是 async 返回一个 Deferred<T>
对象,而 launch 不返回任何值。
launch 函数用于在协程中执行代码块,但不关心代码块的返回值。例如,以下代码使用 launch 函数在协程中打印一条消息:
GlobalScope.launch {
println("Hello, world!")
}
async 函数用于在协程中执行代码块,并返回代码块的返回值。例如,以下代码使用 async 函数在协程中计算一个数的平方,并返回结果:
val result = GlobalScope.async {
5 * 5
}
launch 和 async 函数都可以使用 await()
函数来等待协程执行完成。例如,以下代码使用 await()
函数来等待 async 函数执行完成,并打印结果:
val result = GlobalScope.async {
5 * 5
}
println(result.await())
launch 和 async 函数的主要区别如下:
- launch 函数不返回任何值,而 async 函数返回一个
Deferred<T>
对象。 - launch 函数不能使用
await()
函数来等待协程执行完成,而 async 函数可以使用await()
函数来等待协程执行完成。
launch 和 async 函数都可以用于在协程中执行代码块。launch 函数适用于不关心代码块返回值的情况,而 async 函数适用于需要使用代码块返回值的情况。
suspend 关键字
suspend
关键字是 Kotlin 协程中使用的一种关键字,它允许协程在执行过程中暂停,并在稍后恢复执行。
协程是一种轻量级的并发原语,它允许在一个线程中执行多个任务,而无需创建和管理多个线程。协程可以使用 suspend
关键字来暂停执行,直到某个条件满足,例如等待网络请求的返回或数据库查询的结果。
以下是一些使用 suspend
关键字的示例:
// 等待网络请求的返回
suspend fun fetchUserData(userId: Int): User {
val response = HttpClient.get("https://example.com/users/$userId")
return response.body()
}
// 等待数据库查询的结果
suspend fun findUserByName(name: String): User {
val query = "SELECT * FROM users WHERE name = ?"
val statement = db.compileStatement(query)
statement.bindString(1, name)
val resultSet = statement.executeQuery()
return resultSet.firstOrNull()?.toUser()
}
在上面的示例中,fetchUserData
和 findUserByName
函数都被标记为 suspend
函数,这意味着它们可以在协程中调用。当这些函数在协程中调用时,协程会暂停执行,直到函数返回结果。
suspend
关键字还可以用于挂起函数,即在函数执行过程中暂停执行,直到某个条件满足。例如,以下代码使用 suspend
关键字来挂起函数,直到通道 channel
中有数据可用:
suspend fun receiveData(): Int {
return channel.receive()
}
当 receiveData
函数在协程中调用时,协程会暂停执行,直到 channel
中有数据可用。当数据可用时,协程会恢复执行并返回数据。
总结:
suspend
关键字允许协程在执行过程中暂停,并在稍后恢复执行。suspend
函数可以在协程中调用,当suspend
函数在协程中调用时,协程会暂停执行,直到函数返回结果。suspend
关键字还可以用于挂起函数,即在函数执行过程中暂停执行,直到某个条件满足。
runBlocking 的作用
runBlocking 是 Kotlin 协程库中一个重要的函数,它允许您在协程作用域之外执行协程。
协程是一种并发编程的工具,它允许您将一个任务分解成多个子任务,并在多个线程上同时执行这些子任务。协程与线程不同之处在于,协程是轻量级的,并且可以在同一个线程上切换执行,这使得协程的开销非常小。
runBlocking 函数的作用是创建一个协程作用域,并在该作用域内执行指定的协程。在协程作用域之外,您不能直接执行协程,因为协程需要在协程作用域内才能运行。
runBlocking 函数的语法如下:
fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> Unit) {
val newContext = context + CoroutineName("runBlocking")
val coroutine = newCoroutine(newContext, block)
coroutine.join()
}
其中,context
是协程作用域的上下文,block
是要在协程作用域内执行的协程。
runBlocking 函数的用法非常简单,您只需要在要执行协程的地方调用 runBlocking 函数,并在函数体内指定要执行的协程即可。
例如,以下代码演示了如何使用 runBlocking 函数执行一个协程:
runBlocking {
// 在协程作用域内执行的代码
val result = async {
// 在子协程中执行的代码
delay(1000)
return@async "Hello, world!"
}
val message = result.await()
println(message)
}
这段代码首先调用 runBlocking 函数创建了一个协程作用域,然后在协程作用域内执行了一个协程。协程首先调用 async 函数创建一个子协程,然后在子协程中执行 delay 函数让子协程暂停 1 秒,然后返回 “Hello, world!”。最后,协程调用 result.await() 函数等待子协程完成,然后将子协程的返回值打印到控制台。
runBlocking 函数是一个非常有用的函数,它允许您在协程作用域之外执行协程。这使得您可以在任何地方使用协程,而无需担心协程作用域的问题。
并发作用域
Kotlin 中的并发作用域是一种组织和管理协程的机制。它允许你在一个作用域内启动和管理一组协程,并为这些协程提供一个公共的上下文。
协程作用域可以让你在代码中显式地定义协程的执行范围,并控制协程的启动、取消和异常处理。这可以使你的代码更易于阅读、理解和维护。
Kotlin 提供了多种内置的并发作用域,包括:
- GlobalScope: 全局作用域,可以在任何地方启动协程。
- CoroutineScope: 由协程构建器函数(如
launch
和async
) 创建的作用域,用于在协程内部启动新的协程。 - SupervisorJob: 用于创建子协程不会影响父协程的作用域。
- plus: 用于组合多个作用域。
你还可以使用 CoroutineScope
接口创建自定义的并发作用域。
以下是一些使用并发作用域的示例:
// 使用 GlobalScope 启动一个新的协程
GlobalScope.launch {
// ...
}
// 使用 CoroutineScope 启动一个新的协程
fun myCoroutineScope(): CoroutineScope {
return CoroutineScope(SupervisorJob())
}
fun main() {
val scope = myCoroutineScope()
scope.launch {
// ...
}
// 使用 coroutineScope() {} 创建
coroutineScope {
}
}
并发作用域还可以用于控制协程的取消和异常处理。例如,你可以使用 SupervisorJob
创建一个作用域,使得子协程不会影响父协程。如果子协程抛出异常,父协程将继续执行。
以下是一个使用 SupervisorJob
的示例:
val scope = CoroutineScope(SupervisorJob())
scope.launch {
try {
// ...
} catch (e: Exception) {
// 处理异常
}
}
scope.launch {
// ...
}
在上面的示例中,即使第一个协程抛出异常,第二个协程也会继续执行。
总结:
Kotlin 中的并发作用域是一种组织和管理协程的机制。它允许你在代码中显式地定义协程的执行范围,并控制协程的启动、取消和异常处理。这可以使你的代码更易于阅读、理解和维护。
kotlin 的空判断
1. 使用 ==
或 !=
运算符
最简单的方法是使用 ==
或 !=
运算符来判断变量是否为 null
。
if (variable == null) {
// variable is null
} else {
// variable is not null
}
2. 使用 ?:
运算符
?:
运算符可以用来将一个变量的值赋给另一个变量,如果第一个变量为 null
,则将第二个变量的值赋给第一个变量。
val value = variable ?: "default value"
3. 使用 ?.
运算符
?.
运算符可以用来安全地访问对象的属性或调用对象的方法。如果对象为 null
,则不会访问属性或调用方法,而是返回 null
。
val length = variable?.length
4. 使用 Elvis
运算符
Elvis
运算符 (?:
) 可以用来将一个变量的值赋给另一个变量,如果第一个变量为 null
,则将第二个变量的值赋给第一个变量。
val value = variable ?: "default value"
5. 使用 when
表达式
when
表达式可以用来匹配变量的值,并执行不同的代码。
when (variable) {
null -> {
// variable is null
}
else -> {
// variable is not null
}
}
6. 使用 if
表达式
if
表达式可以用来判断变量是否为 null
,并执行不同的代码。
val value = if (variable != null) {
variable
} else {
"default value"
}
kotlin 的! 和 !!
- !(非空断言运算符):! 用来表示一个变量或表达式一定是非空的,并且不会抛出
NullPointerException
。如果变量或表达式为 null,那么 ! 会抛出NullPointerException
。例如:
val name: String? = "Kotlin"
val length = name!!.length // 6
- !!(非空断言运算符):!! 与 ! 类似,但它更具安全性。!! 会在运行时检查变量或表达式是否为 null,如果为 null,则会抛出
IllegalStateException
。例如:
val name: String? = null
val length = name!!?.length // null
!! 通常用于那些你确信不会为 null 的变量或表达式,例如在构造函数中初始化的属性。
! 和 !! 的主要区别在于:
- ! 会在编译时检查变量或表达式是否为 null,!! 会在运行时检查。
- ! 会抛出
NullPointerException
,!! 会抛出IllegalStateException
。
! 和 !! 都可以用于非空断言,但 !! 更具安全性。
kotlin 的 when
when 表达式是 kotlin 中用于多路选择语句的替代。它类似于 Java 中的 switch 语句,但更灵活,因为它可以处理任意数量的条件,并且可以使用范围和模式匹配。
when 表达式的基本语法如下:
when (expression) {
condition1 -> result1
condition2 -> result2
...
else -> resultN
}
其中,expression 是要评估的表达式,condition 是要检查的条件,result 是要执行的代码。
when 表达式支持以下类型的条件:
- 常量值: 可以使用常量值作为条件,例如:
when (x) {
1 -> println("x is 1")
2 -> println("x is 2")
3 -> println("x is 3")
else -> println("x is not 1, 2, or 3")
}
- 范围: 可以使用范围作为条件,例如:
when (x) {
in 1..10 -> println("x is between 1 and 10")
in 11..20 -> println("x is between 11 and 20")
else -> println("x is not between 1 and 20")
}
- 模式匹配: 可以使用模式匹配作为条件,例如:
when (x) {
is String -> println("x is a string")
is Int -> println("x is an integer")
is Double -> println("x is a double")
else -> println("x is not a string, integer, or double")
}
when 表达式还支持以下类型的结果:
- 代码块: 可以使用代码块作为结果,例如:
when (x) {
1 -> {
println("x is 1")
y++
}
2 -> {
println("x is 2")
z--
}
else -> {
println("x is not 1 or 2")
}
}
- 表达式: 可以使用表达式作为结果,例如:
when (x) {
1 -> y++
2 -> z--
else -> 0
}
when 表达式还支持以下类型的操作符:
- ==: 等于号操作符用于比较两个值是否相等,例如:
when (x) {
1 == y -> println("x is equal to y")
else -> println("x is not equal to y")
}
- !=: 不等于号操作符用于比较两个值是否不相等,例如:
when (x) {
1 != y -> println("x is not equal to y")
else -> println("x is equal to y")
}
- <: 小于号操作符用于比较两个值是否小于,例如:
when (x) {
x < y -> println("x is less than y")
else -> println("x is not less than y")
}
- <=: 小于等于号操作符用于比较两个值是否小于等于,例如:
when (x) {
x <= y -> println("x is less than or equal to y")
else -> println("x is not less than or equal to y")
}
- >: 大于号操作符用于比较两个值是否大于,例如:
when (x) {
x > y -> println("x is greater than y")
else -> println("x is not greater than y")
}
- >=: 大于等于号操作符用于比较两个值是否大于等于,例如:
when (x) {
x >= y -> println("x is greater than or equal to y")
else -> println("x is not greater than or equal to y")
}
when 表达式是一个非常灵活和强大的工具,可以用于处理各种各样的条件和结果。它比 Java 中的 switch 语句更灵活,因为它可以处理任意数量的条件,并且可以使用范围和模式匹配。
kotlin 的进阶特性
集合与 vararg 的转换
集合与 vararg 的转换可以通过使用 toTypedArray()
和 toList()
函数来完成。
如果有一个集合,想要将其转换为 vararg 参数,可以使用 toTypedArray()
函数。这个函数会返回一个数组,其中包含集合的所有元素。例如:
val list = listOf("a", "b", "c")
val array = list.toTypedArray()
如果有一个 vararg 参数,想要将其转换为集合,可以使用 toList()
函数。这个函数会返回一个包含 vararg 参数的新列表。例如:
fun printList(list: List<String>) {
println(list)
}
printList(*array)
在上面的代码中,*
运算符用于将数组展开为 vararg 参数。
总结起来,集合与 vararg 的转换可以通过以下方式完成:
- 将集合转换为 vararg 参数:使用
toTypedArray()
函数。 - 将 vararg 参数转换为集合:使用
toList()
函数,并使用*
运算符展开数组。
kotlin 中通过参数名称传递参数
在 kotlin 中,我们有一个如下的函数
fun test(a: Int = 1, vararg list: String) {
println(a)
list.forEach {
println(it)
}
}
当我们在调用的时候,我们对默认值的传参并不关心,当我只需要为 b
传参时,就会出现以下问题
可以发现,我们在当第一位参数有默认值,且我们并不关心他的传参的时候,会出现我们的参数传递不到指定的参数上的问题,这个时候就需要使用到 kotlin 的特性,根据属性名传递参数的特性了
在 Kotlin 中,可以使用扩展函数来实现根据属性名传递参数的功能。以下是如何实现的步骤:
- 创建一个扩展函数,该函数接受一个对象和一个参数名作为参数,并返回该对象中具有该参数名的属性值。
fun <T> T.getProperty(propertyName: String): Any? {
val property = this::class.java.getDeclaredField(propertyName)
property.isAccessible = true
return property.get(this)
}
- 使用扩展函数来获取对象中具有指定参数名的属性值。
val person = Person("John", 30)
val name = person.getProperty("name")
val age = person.getProperty("age")
- 将扩展函数与参数名一起传递给另一个函数。
fun printPersonInfo(person: Person, propertyName: String) {
val propertyValue = person.getProperty(propertyName)
println("$propertyName: $propertyValue")
}
printPersonInfo(person, "name")
printPersonInfo(person, "age")
这种方法的好处是,它可以使代码更加简洁和可读。此外,它还可以提高代码的可维护性,因为当需要更改参数名时,只需要更改扩展函数中的代码即可。
所以我们这里的具体调用方式就变成了
var params = arrayOf("1", "2", "3", "4")
test(list = params)
运算符重载
在 Kotlin 中可以为类型提供预定义的一组操作符的自定义实现。这些操作符具有预定义的符号表示(如 +
或 *
)与优先级。为了实现这样的操作符,需要为相应的类型提供一个指定名称的成员函数或扩展函数。这个类型会成为二元操作符左侧的类型及一元操作符的参数类型。
要重载运算符,请使用 OPERATOR
修饰符标记相应的函数:
interface IndexedContainer {
operator fun get(index: Int)
}
中缀函数
在Kotlin中,中缀函数是一种特殊的函数调用语法,可以使函数调用更加简洁和可读。中缀函数需要满足以下条件:
- 函数必须是成员函数或扩展函数。
- 函数必须只有一个参数。
- 函数的参数必须是无参数的,即不能有默认值。
定义中缀函数需要在函数前面加上infix
关键字。例如:
infix fun String.isEqualTo(other: String) = this == other
该代码定义了一个名为isEqualTo
的中缀函数,它接受一个字符串参数,并返回两个字符串是否相等。
使用中缀函数时,可以省略点号和括号,直接将它放在两个对象之间。例如:
val result = "hello" isEqualTo "world"
这样就能够判断两个字符串是否相等了。
需要注意的是,虽然中缀函数可以使代码更加简洁和可读,但滥用它可能会降低代码的可读性。因此,在使用中缀函数时应谨慎考虑是否真正需要使用它们。中缀is须是
以下是一些其他中缀函数的示例:
String.plus()
:将两个字符串连接在一起。List.plus()
:将两个列表连接在一起。Int.compareTo()
:比较两个整数的大小。Double.times()
:将一个数字乘以另一个数字。
您可以使用中缀表示法来调用这些中缀函数。例如,以下代码将字符串 “Hello” 和 “World” 连接在一起:
val result = "Hello" + "World"
println(result) // 输出: HelloWorld
中缀表示法是一种非常方便的语法,可以使您的代码更简洁和更易读。
内联函数
Kotlin 的内联函数(inline functions)是一种在编译时期将函数体代码“内联”到调用处的特性,这意味着编译器会将内联函数的代码直接替换到每一个调用点,而不是正常的函数调用(即通过栈进行调用)。这个特性对于减少运行时开销尤其有用,特别是在使用高阶函数(如函数作为参数或返回值的函数)时。
优点:
- 减少运行时开销:由于不需要通过栈进行函数调用,可以减少额外的开销。
- 优化性能:对于小的函数,内联可以避免函数调用的开销,有助于提高性能。
- 更好的Lambda表达式支持:内联函数允许lambda表达式在编译时被内联,这对于Kotlin中的高阶函数非常有用,因为它可以避免创建匿名类实例并减少内存使用。
缺点:
- 增加编译后的代码量:如果一个内联函数在多处被调用,那么每个调用点都会被替换成函数体的代码,这可能会增加最终的代码量。
- 滥用可能导致性能问题:如果过度使用内联功能,尤其是在大型函数上,可能会导致编译后的代码膨胀,反而降低性能。
如何使用:
在 Kotlin 中,你可以通过在函数声明前添加 inline
关键字来创建内联函数。例如:
inline fun <T> measureTime(block: () -> T): T {
val start = System.nanoTime()
val result = block()
val end = System.nanoTime()
println("Time taken: ${(end - start) / 1_000_000} ms")
return result
}
上述代码反编译的结果:
public final class InlineKt {
public static final Object measureTime(@NotNull Function0 block) {
int $i$f$measureTime = 0;
Intrinsics.checkNotNullParameter(block, "block");
long start = System.nanoTime();
Object result = block.invoke();
long end = System.nanoTime();
String var7 = "Time taken: " + (end - start) / (long)1000000 + " ms";
System.out.println(var7);
return result;
}
public static final void main() {
Function0 result = (Function0)null.INSTANCE;
int $i$f$measureTime = false;
// 可以看到,measureTime方法的方法体都被内敛到了调用出
long start$iv = System.nanoTime();
Object result$iv = result.invoke();
long end$iv = System.nanoTime();
String var7 = "Time taken: " + (end$iv - start$iv) / (long)1000000 + " ms";
System.out.println(var7);
System.out.println(result$iv);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
在上面的例子中,measureTime
函数接受一个 lambda 表达式作为参数,并计算执行该 lambda 所需的时间。因为 measureTime
函数被标记为内联,所以它的调用会在编译时被替换成具体的计时逻辑和 lambda 表达式的代码,从而避免了函数调用的开销。
- 对于小而频繁调用的函数,尤其是那些作为参数接受 lambda 表达式的函数,考虑使用内联。
- 避免将大型函数标记为内联,因为这可能会导致编译后的代码膨胀。
- 内联不仅限于函数,Kotlin 的
inline
关键字也可用于内联属性和类。
传入的参数不想被内联时
一个高阶函数一旦被标记为内联,它的方法体和所有 Lambda 参数都会被内联。
inline fun test(block1: () -> Unit, block2: () -> Unit) {
block1()
println("xxx")
block2()
}
test()
函数被标记为了 inline
,所以它的函数体以及两个 Lambda 参数都会被内联。但是由于我要传入的 block1
代码块巨长(或者其他原因),我并不想将其内联,这时候就要使用 noinline
。
inline fun test(noinline block1: () -> Unit, block2: () -> Unit) {
block1()
println("xxx")
block2()
}
这样, block1
就不会被内联了。篇幅原因,这里就不展示 Java 代码了,相信你也能很容易理解 noinline
。
扩展函数
Kotlin 的扩展函数是一种强大的特性,允许你向一个已经存在的类添加新的方法,而不需要修改其源代码或者使用继承。这是一种在不直接修改类定义的情况下,扩展类功能的优雅方式。扩展函数可以在任何类上定义,包括库中的类或者你自己定义的类。
定义扩展函数
要定义一个扩展函数,你需要使用以下语法:
fun ClassName.extensionFunctionName(parameters): ReturnType {
// 函数体
}
其中 ClassName
是你想要扩展的类的名称,extensionFunctionName
是你给这个扩展函数起的名字,parameters
是函数的参数(如果有的话),ReturnType
是函数返回值的类型。
示例
假设你有一个 String
类型的数据,你想要添加一个扩展函数来判断字符串是否是回文字符串(正读和反读都相同)。
fun String.isPalindrome(): Boolean {
return this == this.reversed()
}
现在,你可以在任何 String
对象上调用 .isPalindrome()
方法来检查它是否是回文字符串。
val example = "madam"
println(example.isPalindrome()) // 输出:true
扩展函数的作用域
扩展函数只在它被定义的作用域内可见。通常,你会在顶层定义扩展函数,这样它就在整个包内可见。如果你只想在某个特定的文件或类内使用这个扩展函数,你可以将其定义在文件或类的内部。
注意点
- 扩展函数并不真正修改它扩展的类。它们通过静态解析的方式在调用点被解析。
- 在扩展函数内部,你可以通过
this
关键字来访问接收者对象(即调用该扩展函数的对象)。 - 扩展函数不能访问接收者类的私有或者受保护的成员。
示例
fun String.lastChar(): Char {
return this[this.length - 1]
}
fun main() {
println("Kotlin".lastChar())
}
反编译的结果:
public final class ExtensionKt {
public static final char lastChar(@NotNull String $this$lastChar) {
Intrinsics.checkNotNullParameter($this$lastChar, "$this$lastChar");
return $this$lastChar.charAt($this$lastChar.length() - 1);
}
public static final void main() {
char var0 = lastChar("Kotlin");
System.out.println(var0);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
可以看到,
一元操作
一元前缀操作符
表达式 | 翻译为 |
---|---|
+a |
a.unaryPlus() |
-a |
a.unaryMinus() |
!a |
a.not() |
这个表是说,当编译器处理例如表达式 +a
时,它执行以下步骤:
- 确定
a
的类型,令其为T
。 - 为接收者
T
查找一个带有operator
修饰符的无参函数unaryPlus()
,即成员函数或扩展函数。 - 如果函数不存在或不明确,则导致编译错误。
- 如果函数存在且其返回类型为
R
,那就表达式+a
具有类型R
。
这些操作以及所有其他操作都针对基本类型做了优化,不会为它们引入函数调用的开销。
以下是如何重载一元减运算符的示例:
data class Point(val x: Int, val y: Int)
operator fun Point.unaryMinus() = Point(-x, -y)
val point = Point(10, 20)
fun main() {
println(-point) // 输出“Point(x=-10, y=-20)”
}
递增与递减
表达式 | 翻译为 |
---|---|
a++ |
a.inc() + 见下文 |
a-- |
a.dec() + 见下文 |
inc()
和 dec()
函数必须返回一个值,它用于赋值给使用 ++
或 --
操作的变量。它们不应该改变在其上调用 inc()
或 dec()
的对象。
编译器执行以下步骤来解析_后缀_形式的操作符,例如 a++
:
- 确定
a
的类型,令其为T
。 - 查找一个适用于类型为
T
的接收者的、带有operator
修饰符的无参数函数inc()
。 - 检测函数的返回类型是
T
的子类型。
计算表达式的步骤是:
- 把
a
的初始值存储到临时存储a0
中。 - 把
a0.inc()
结果赋值给a
。 - 把
a0
作为该表达式的结果返回。
对于 a--
,步骤是完全类似的。
对于_前缀_形式 ++a
和 --a
以相同方式解析,其步骤是:
- 把
a.inc()
结果赋值给a
。 - 把
a
的新值作为该表达式结果返回。
二元操作
算术运算符
表达式 | 翻译为 |
---|---|
a + b |
a.plus(b) |
a - b |
a.minus(b) |
a * b |
a.times(b) |
a / b |
a.div(b) |
a % b |
a.rem(b) |
a..b |
a.rangeTo(b) |
a..<b |
a.rangeUntil(b) |
对于此表中的操作,编译器只是解析成_翻译为_列中的表达式。
下面是一个从给定值起始的 Counter
类的示例,它可以使用重载的 +
运算符来增加计数:
data class Counter(val dayIndex: Int) {
operator fun plus(increment: Int): Counter {
return Counter(dayIndex + increment)
}
}
in 操作符
表达式 | 翻译为 |
---|---|
a in b |
b.contains(a) |
a !in b |
!b.contains(a) |
对于 in
和 !in
,过程是相同的,但是参数的顺序是相反的。
索引访问操作符
表达式 | 翻译为 |
---|---|
a[i] |
a.get(i) |
a[i, j] |
a.get(i, j) |
a[i_1, ……, i_n] |
a.get(i_1, ……, i_n) |
a[i] = b |
a.set(i, b) |
a[i, j] = b |
a.set(i, j, b) |
a[i_1, ……, i_n] = b |
a.set(i_1, ……, i_n, b) |
方括号转换为调用带有适当数量参数的 get
和 set
。
invoke 操作符
表达式 | 翻译为 |
---|---|
a() |
a.invoke() |
a(i) |
a.invoke(i) |
a(i, j) |
a.invoke(i, j) |
a(i_1, ……, i_n) |
a.invoke(i_1, ……, i_n) |
圆括号转换为调用带有适当数量参数的 invoke
。
广义赋值
表达式 | 翻译为 |
---|---|
a += b |
a.plusAssign(b) |
a -= b |
a.minusAssign(b) |
a *= b |
a.timesAssign(b) |
a /= b |
a.divAssign(b) |
a %= b |
a.remAssign(b) |
对于赋值操作,例如 a += b
,编译器执行以下步骤:
- 如果右列的函数可用:
- 如果相应的二元函数(即
plusAssign()
对应于plus()
)也可用,a
is a mutable variable, and the return type ofplus
is a subtype of the type ofa
, 那么报告错误(无法区分)。 - 确保其返回类型是
Unit
,否则报告错误。 - 生成
a.plusAssign(b)
的代码。
- 如果相应的二元函数(即
- 否则试着生成
a = a + b
的代码(这里包含类型检测:a + b
的类型必须是a
的子类型)。
赋值在 Kotlin 中_不是_表达式。
相等与不等操作符
表达式 | 翻译为 |
---|---|
a == b |
a?.equals(b) ?: (b === null) |
a != b |
!(a?.equals(b) ?: (b === null)) |
这些操作符只使用函数 equals(other: Any?): Boolean
, 可以覆盖它来提供自定义的相等性检测实现。不会调用任何其他同名函数(如 equals(other: Foo)
)。
===
和!==
(同一性检测)不可重载,因此不存在对他们的约定。
这个 ==
操作符有些特殊:它被翻译成一个复杂的表达式,用于筛选 null
值。 null == null
总是 true,对于非空的 x
,x == null
总是 false 而不会调用 x.equals()
。
比较操作符
表达式 | 翻译为 |
---|---|
a > b |
a.compareTo(b) > 0 |
a < b |
a.compareTo(b) < 0 |
a >= b |
a.compareTo(b) >= 0 |
a <= b |
a.compareTo(b) <= 0 |
所有的比较都转换为对 compareTo
的调用,这个函数需要返回 Int
值
属性委托操作符
provideDelegate
、 getValue
以及 setValue
操作符函数已在委托属性中描述。
具名函数的中缀调用
可以通过中缀函数的调用 来模拟自定义中缀操作符。
为类添加拓展函数
// 在String类中添加一个名为"toTitleCase"的拓展函数
fun String.toTitleCase(): String {
// 将字符串中的每个单词的首字母大写
return this.split(" ").joinToString(" ") { it.capitalize() }
}
// 使用拓展函数
val str = "hello world"
val titleCaseStr = str.toTitleCase()
println(titleCaseStr) // 输出:Hello World
在这个例子中,我们定义了一个名为 toTitleCase()
的拓展函数,它将字符串中的每个单词的首字母大写。我们使用 split()
函数将字符串拆分为单词,然后使用 joinToString()
函数将单词重新连接起来,每个单词的首字母大写。最后,我们使用 capitalize()
函数将每个单词的首字母大写。
拓展函数可以帮助我们为现有的类添加新的功能,而无需修改类的源代码。这使得我们可以轻松地扩展现有库的功能,或者为我们的代码添加自定义功能。。
kotlin 的 lambda
Lambda 表达式(也称为匿名函数)允许您在不创建命名方法的情况下定义函数。Lambda 表达式通常用于作为函数参数传递,或者在集合上执行操作。
1. Lambda 表达式的语法
Lambda 表达式的语法如下:
{ parameters -> expression }
其中:
parameters
是 lambda 表达式的参数列表。expression
是 lambda 表达式的函数体。
例如,以下 lambda 表达式计算两个数字的和:
{ a: Int, b: Int -> a + b }
2. 使用 Lambda 表达式
您可以将 lambda 表达式作为函数参数传递,或者在集合上执行操作。
例如,以下代码使用 lambda 表达式将集合中的每个元素加 1:
val numbers = listOf(1, 2, 3, 4, 5)
val incrementedNumbers = numbers.map { it + 1 }
println(incrementedNumbers) // 输出: [2, 3, 4, 5, 6]
3. Lambda 表达式的类型推断
Kotlin 编译器可以自动推断 lambda 表达式的类型。这意味着您不必显式地指定 lambda 表达式的类型。
例如,以下 lambda 表达式计算两个数字的和:
{ a, b -> a + b }
Kotlin 编译器会自动推断出这个 lambda 表达式的类型是 (Int, Int) -> Int
。
4. Lambda 表达式的最佳实践
- 使用 lambda 表达式来简化代码。
- 避免在 lambda 表达式中使用复杂的逻辑。
- 使用 lambda 表达式来传递函数作为参数。
- 使用 lambda 表达式来在集合上执行操作。
lambda 指定入参名称
val lambdaTest2: (a:Long) -> Unit = {a ->
println(a)
}
lambda 传入拓展函数
val lambdaTest: (init: Body.() -> Long) -> Unit = { init ->
var body = Body() // 创建接收者对象
var long = body.init() // 将该接收者对象传给该 lambda
}
println(lambdaTest { // 带接收者的 lambda 由此开始
body() // 调用该接收者对象的一个方法
})
lambda 为作用域起别名
val lambdaTest: (init: Body.() -> Long) -> Long = lambdaTest@{ init -> // 通过name@的语法来为整个lambda函数代码块取一个别名
var body = Body()
var long = body.init()
return@lambdaTest long // 通过别名将返回值返回,默认的return返回的是调用方也就是main()
}
lambda 表达式在函数入参中的函数简写
入参只有一个 lambda 表达式的情况
// 当lambda只有一个入参且是lambda表达式的时候,可以直接省略()使用{}直接传入一个函数实现
val lambdaTest3: (() -> Unit) -> Unit = {
it()
}
lambdaTest3{
println("lambdaTest3")
}
多个入参, 但是 lambda 表达式在最后一个参数的情况
val lambdaTest4: (a:Int,() -> Unit) -> Unit = {a, b ->
println(a)
b()
}
// 这种情况可以将lambda不写在()内,直接通过{}的方式实现函数传入
lambdaTest4(1) {
println("lambdaTest4")
}
闭包
Kotlin 闭包(closures)是函数嵌套在另一个函数内部,并能够访问外部函数作用域中的变量。闭包在 Kotlin 中有广泛的应用,例如实现事件处理、创建私有函数、以及模拟对象。
以下是一个简单的 Kotlin 闭包示例:
fun main() {
val outerVariable = 10
val closure = {
println("Outer variable: $outerVariable")
}
closure() // 输出: Outer variable: 10
}
在这个示例中,outerVariable
是一个定义在主函数(main
)作用域中的变量。closure
是一个嵌套函数,它可以访问外部函数的作用域,包括 outerVariable
变量。当调用 closure()
时,它将打印出 outerVariable
的值。
闭包的优点在于,它可以访问外部函数作用域中的变量,即使外部函数已经执行完毕。这使得闭包非常适合实现事件处理、创建私有函数、以及模拟对象。
闭包的应用
- 事件处理: 闭包可以被用来实现事件处理。当一个事件发生时,闭包可以被调用来处理该事件。例如,一个按钮点击事件的处理函数可以是一个闭包。
- 创建私有函数: 闭包可以被用来创建私有函数。私有函数只能在定义它们的函数内部被访问。这使得闭包非常适合实现一些只在特定函数中使用的辅助函数。
- 模拟对象: 闭包可以被用来模拟对象。对象是一种封装了数据和行为的实体。闭包可以被用来创建具有类似行为的实体,而不需要创建一个真正的对象。
闭包的注意事项
- 闭包会捕获外部函数作用域中的变量。 这意味着,如果外部函数作用域中的变量被修改,那么闭包也会受到影响。
- 闭包可能会导致内存泄漏。 如果闭包捕获了对外部函数作用域中对象的引用,那么该对象可能无法被垃圾回收器回收。这可能会导致内存泄漏。
// accumulate是一个无参数的,返回"函数类型"的,函数
// "accumulate()"表示:是一个无参数的,名为accumulate的函数,记作函数A
// "() -> Unit"表示; 这是一个无参数的,返回空值Unit的,函数类型,暂记作函数B
// 将这个函数B,"() -> Unit",作为返回值,即返回一个是“函数类型”的返回值
// 在函数accumulate()中,即返回这个东东,"{ println(count++) }",这个代码块,block
// 但是注意,此时,这个block,是不会运行里面的内容的
// 因为它仍表示为一个函数类型的对象,只有在这个函数类型对象后加一个"()"
// 这个block,才会被激活
fun accumulate():() -> Unit{
var sum = 0
return {
println(sum++)
}
}
fun main(args: Array<String>) {
// 这里定义counting = accumulate(),则counting变为函数类型
// 函数类型后面加一个"()",即可“被激活”
// 多次调用counting,则表示,多次调用"同一个accumulate()"
// 所以同一个accumulate()里的sum值保留,故能够累加
// 但是,没有被申明的accumulate(),则分别视作单独的函数
// 不具备累加的能力
val counting = accumulate()
counting() // 输出结果:0
counting() // 输出结果:1
counting() // 输出结果:2
accumulate()() // 输出结果:0
accumulate()() // 输出结果:0
accumulate()() // 输出结果:0
println("counting: "+counting)
// 输出结果: counting: Function0<kotlin.Unit>
println("counting(): "+counting())
// 先输出: 3; 后输出: counting(): kotlin.Unit
println("accumulate(): "+accumulate())
// 输出结果: accumulate(): Function0<kotlin.Unit>
}
优雅的 Stream API
TODO…
Scope函数
在Kotlin中,作用域函数(Scope functions)是一种非常有用的语法特性,允许你对对象执行代码块,而不需要显式地引用对象本身。Kotlin提供了几种作用域函数:let
, run
, with
, apply
, 和 also
。每个函数都提供了一种不同的方式来处理对象上的操作和表达式,使代码更加简洁、易读。
let
函数: 使用它来执行一个或多个操作在一个对象的非空实例上。let
通常用于处理可空对象及进行链式调用转换。在let
的代码块内部,对象通过it
引用,除非你声明了另一个名字。
val str: String? = "Hello"
str?.let {
// 在这里,it代表str
print(it.length)
}
run
函数:run
和let
相似,但它在对象的上下文中执行代码块,并且返回代码块的最后一个表达式的结果。对于非空对象的调用,使用run
可以让你执行一个更复杂的操作序列,并且返回一个结果。
val result = "Hello".run {
// 这里可以直接访问对象的属性和函数
length
}
with
函数:with
不是扩展函数,它作为一个函数参数传递对象,并执行给定的代码块。它非常适合对一个对象执行多个操作时使用。
val message = StringBuilder()
with(message) {
append("Hello, ")
append("world!")
toString()
}
apply
函数:apply
几乎与run
相同,但它返回的是对象本身而不是最后的表达式结果。这非常适合进行对象的配置。
val person = Person().apply {
name = "John"
age = 25
}
also
函数:also
和let
相似,但区别在于also
返回的是对象本身,而不是代码块的结果。它主要用于对象的额外操作,而不改变对象本身。
val numbers = mutableListOf("One", "Two", "Three")
numbers.also {
print("The list elements before adding new one: $it")
}.add("Four")
- scope的使用时机图
每个作用域函数都有其特定的用例,选择哪一个取决于你想要的操作类型以及返回值。
常用的复杂对象
字符串
Kotlin 的字符串是不可变的 Unicode 字符序列。它们使用双引号 (“) 或三重引号 (”“”") 来表示。
val str1 = "Hello, world!"
val str2 = """
This is a multiline
string.
"""
字符串可以连接起来形成新的字符串。
val str3 = str1 + str2
字符串也可以使用索引来访问其字符。
val firstChar = str1[0] // 'H'
val lastChar = str1[str1.length - 1] // '!'
字符串还支持各种操作,例如大小写转换、子字符串提取和正则表达式匹配。
val upperStr = str1.toUpperCase() // "HELLO, WORLD!"
val subStr = str1.substring(7, 12) // "world"
val matches = str1.matches(Regex("Hello, \\w+!")) // true
字符串模板可以用于轻松地将变量插入字符串中。
val name = "Alice"
val age = 20
val greeting = "Hello, $name! You are $age years old."
字符串模板还可以用于格式化数字。
val price = 10.99
val formattedPrice = "The price is $%.2f."
字符串是 Kotlin 中一种非常强大的类型。它们可以用于各种目的,例如文本处理、格式化和模板化。
集合相关
创建数组
// 创建一个整型数组
val myIntArray = intArrayOf(1, 2, 3, 4, 5)
// 创建一个字符串数组
val myStringArray = arrayOf("a", "b", "c", "d", "e")
// 创建一个混合类型数组
val myMixedArray = arrayOf(1, "a", 3.14, true, 'c')
// 访问数组元素
println(myIntArray[0]) // 输出:1
println(myStringArray[2]) // 输出:c
println(myMixedArray[3]) // 输出:true
// 遍历数组
for (element in myIntArray) {
println(element)
}
// 使用下标遍历数组
for (i in myStringArray.indices) {
println(myStringArray[i])
}
// 使用 forEach() 遍历数组
myMixedArray.forEach { element ->
println(element)
}
// 创建二维数组
val my2DArray = arrayOf(
intArrayOf(1, 2, 3),
intArrayOf(4, 5, 6),
intArrayOf(7, 8, 9)
)
// 访问二维数组元素
println(my2DArray[0][1]) // 输出:2
println(my2DArray[2][2]) // 输出:9
// 遍历二维数组
for (row in my2DArray) {
for (element in row) {
println(element)
}
}
创建各种集合
kotlin 创建可变集合
- 使用
mutableListOf()
函数创建一个可变列表:
val mutableList = mutableListOf(1, 2, 3)
- 使用
mutableSetOf()
函数创建一个可变集合:
val mutableSet = mutableSetOf(1, 2, 3)
- 使用
mutableMapOf()
函数创建一个可变映射:
val mutableMap = mutableMapOf("one" to 1, "two" to 2, "three" to 3)
- 使用
Array()
函数创建一个可变数组:
val mutableArray = Array(5) { 0 }
- 使用
mutableIterable()
函数创建一个可变迭代器:
val mutableIterable = mutableListOf(1, 2, 3).asIterable()
kotlin 创建不可变集合
要创建不可变集合,可以使用 Kotlin 的 listOf()
、setOf()
和 mapOf()
函数。这些函数返回一个不可变的集合,其中元素不能被添加、删除或修改。
以下是如何使用这些函数创建不可变集合的示例:
// 创建一个不可变的列表
val myList = listOf(1, 2, 3)
// 创建一个不可变的集合
val mySet = setOf(1, 2, 3)
// 创建一个不可变的映射
val myMap = mapOf(1 to "one", 2 to "two", 3 to "three")
需要注意的是,这些函数返回的集合是不可变的,这意味着它们不能被修改。如果您尝试修改这些集合,将会得到一个编译错误。
如果您需要一个可变的集合,可以使用 Kotlin 的 mutableListOf()
、mutableSetOf()
和 mutableMapOf()
函数。这些函数返回一个可变的集合,其中元素可以被添加、删除或修改。
kotlin 创建各种空集合
// 创建一个空的 List
val emptyList = emptyList<Int>()
// 创建一个空的 Set
val emptySet = emptySet<String>()
// 创建一个空的 Map
val emptyMap = emptyMap<Int, String>()
// 创建一个空的 Array
val emptyArray = arrayOf<Int>()
// 创建一个空的 ByteArray
val emptyByteArray = ByteArray(0)
// 创建一个空的 ShortArray
val emptyShortArray = ShortArray(0)
// 创建一个空的 IntArray
val emptyIntArray = IntArray(0)
// 创建一个空的 LongArray
val emptyLongArray = LongArray(0)
// 创建一个空的 FloatArray
val emptyFloatArray = FloatArray(0)
// 创建一个空的 DoubleArray
val emptyDoubleArray = DoubleArray(0)
// 创建一个空的 BooleanArray
val emptyBooleanArray = BooleanArray(0)
// 创建一个空的 CharArray
val emptyCharArray = CharArray(0)
创建一个 range
// 创建一个range
var intRange = 1..10
// 创建一个rangeTo
var intRangeTo = 1.rangeTo(10)
kotlin 的二元组和三元组
二元组 (Pair)
二元组是一种数据结构,它包含两个元素。它可以用 Pair<A, B>
表示,其中 A
和 B
是两个类型。例如,以下代码创建一个二元组,其中第一个元素是字符串,第二个元素是整数:
val pair = Pair("Hello", 1)
你可以使用 first
和 second
属性来访问二元组中的元素:
val firstElement = pair.first
val secondElement = pair.second
你也可以使用解构声明来访问二元组中的元素:
val (firstElement, secondElement) = pair
三元组 (Triple)
三元组是一种数据结构,它包含三个元素。它可以用 Triple<A, B, C>
表示,其中 A
、B
和 C
是三个类型。例如,以下代码创建一个三元组,其中第一个元素是字符串,第二个元素是整数,第三个元素是布尔值:
val triple = Triple("Hello", 1, true)
你可以使用 first
、second
和 third
属性来访问三元组中的元素:
val firstElement = triple.first
val secondElement = triple.second
val thirdElement = triple.third
你也可以使用解构声明来访问三元组中的元素:
val (firstElement, secondElement, thirdElement) = triple
二元组和三元组都是不变的数据结构,这意味着它们一旦被创建就不能被修改。
使用二元组和三元组
二元组和三元组可以用于各种场景。例如,你可以使用它们来存储一个键和一个值,或者来存储一个对象的两个或三个属性。
以下是一些使用二元组和三元组的示例:
- 使用二元组来存储一个键和一个值:
val map = mutableMapOf(Pair("key1", "value1"), Pair("key2", "value2"))
- 使用三元组来存储一个对象的三个属性:
data class Person(val name: String, val age: Int, val gender: String)
val person = Person("John", 30, "male")
二元组和三元组是 Kotlin 中非常有用的数据结构。它们可以帮助你组织和存储数据,并使你的代码更易于阅读和理解。
评论区