最近更新于 2024-05-10 10:05
1 前言
准备入 Android,抽空了解一下 Kotlin 语法。
2 环境
JDK 17.0.9
Kotlin 1.9.20
环境配置:https://blog.iyatt.com/?p=12040
3 编码规范
https://book.kotlincn.net/text/coding-conventions.html
这个编码规范可以作为参考,当然我个人而言还是喜欢沿用自己的方案。主要来自 C/C++ 的习惯风格,比如大括号独立成行,条件、循环必须加大括号(即使只有一个语句)。不过还是遇到一个特殊的,Kotlin 不建议使用下划线命名法,在 IDEA 中编辑的时候,只要用了就会出现波浪号。这个和目前的风格有点冲突还不可调和,我最开始学编程的时候是用驼峰法,后面感觉驼峰法太紧密了,不够清晰,之后就改用下划线法了,结果 Kotlin 不让用下划线法(看到波浪线太难受了,还是得妥协)。
4 探索
when 语句
Kotlin 中没有 switch,但是有 when,其作用类似
fun main()
{
val value = 10
when (value)
{
1 ->
{
println("结果为 1")
}
5 ->
{
println("结果为 5")
}
10 ->
{
println("结果为 10")
}
else ->
{
println("其它")
}
}
}
4.2 条件控制语句的返回值
Kotlin 中条件控制语句可以有返回值,这个在以前接触的语言里没见过。最终的返回值来自于匹配的代码块里最后一个语句,且接收返回值的时候,必须存在 else 以保证在没有匹配到的情况下有默认值。
Kotlin 没有类似 C/C++/Java(后有提到就简称 C 系) 的三元运算符,但是可以通过 if-else 做到三元运算符的效果,还可以提供更多 else if 条件的判断。Python 中的三元运算符语法上是用的 if-else,但是那是专用的,只是相当于把C系的问号冒号换成 if-else 而已,仅是三元运算符的符号,而不是改了 if 条件判断的用途。
if
fun main()
{
val value = 10
val ret = if (value == 1)
{
"匹配 1"
true
}
else if (value == 2)
{
println("匹配 2")
true
}
else
{
"其它"
false
}
println(ret)
}
when
fun main()
{
val value = 11
val ret = when (value)
{
1 ->
{
"结果为 1"
}
5 ->
{
"结果为 5"
}
10 ->
{
"结果为 10"
}
else ->
{
"其它"
}
}
println(ret)
}
4.3 循环控制
4.3.1 for
fun main()
{
val items = listOf("小红", "小明", "小张")
// for 遍历
for (item in items)
{
print("$item,")
}
println()
// for 遍历索引
for (idx in items.indices)
{
print("$idx ${items[idx]},")
}
println()
// forEach 遍历
items.forEach { item ->
print("$item,")
}
println()
// forEach 遍历索引
items.forEachIndexed { idx, item ->
print("$idx $item,")
}
println()
val map = mapOf("value1" to 1, "value2" to 2, "value3" to 3)
for ((key, value) in map)
{
print("$key:$value, ")
}
println()
map.forEach { (key, value) ->
print("$key:$value, ")
}
println()
}
4.3.2 while
while 和 do-while 这一点和 C 系是一样的,前者是在每次循环开始前判断是否满足条件,后者是循环结束一次后判断一次条件
fun main()
{
var x = 0
var y = 10
while (x > 0)
{
print(x--)
print(" ")
}
println()
do
{
print(y--)
print(" ")
}
while (y > 10)
println()
}
4.3.3 返回和跳转
这点和 C 系差不多,return 结束当前所在函数,break 终止所在的一层循环,continue 所在循环的当此循环结束,进入所在循环的下一轮次循环。
不过 Kotlin 中有了标签的概念,标签名可以自己设置
3.3.3.1 break 标签
break 只能跳出当前所在的一层循环,使用标签后可以跳到标签所在循环的外面,也就是可以一次性可以跳出指定层数的循环
fun main()
{
for (x in 1..3)
{
myLabel@ for (y in 1..3)
{
for (z in 1..3)
{
if (z > 1)
{
break@myLabel
}
println("$x $y $z")
}
}
}
}
3.3.3.2 break 标签
continue 只能略过所在循环的当次循环,使用标签后,可以略过标签所在位置循环的当次循环,即可以略过指定层数的循环
fun main()
{
myLabel@ for (x in 1..3)
{
for (y in 1..3)
{
for (z in 1..3)
{
if (z > 1)
{
continue@myLabel
}
println("$x $y $z")
}
}
}
}
3.3.3.3 return 标签
直接使用 return 会结束当前函数,在一些情况下只是需要局部返回,并不是要整个函数的返回。
fun foo()
{
val items = listOf(10, 11, 12, 13)
items.forEach { item ->
if (item == 11)
{
return
}
println(item)
}
println("foo 结束")
}
fun main()
{
foo()
}
使用标签限制后,这里只会在当前 Lambda 表达式返回
fun foo()
{
val items = listOf(10, 11, 12, 13)
items.forEach myLabel@{ item ->
if (item == 11)
{
return@myLabel
}
println(item)
}
println("foo 结束")
}
fun main()
{
foo()
}
这里可以简化使用隐式标签,只要标签和接受这个 Lambda 表达式的函数同名就行
fun foo()
{
val items = listOf(10, 11, 12, 13)
items.forEach { item ->
if (item == 11)
{
return@forEach
}
println(item)
}
println("foo 结束")
}
fun main()
{
foo()
}
4.4 入口函数
Kotlin 比 Java 各方面都简洁很多,特别是入口函数。因为习惯了 C/C++,刚接触 Java 的时候太不理解把入口函数放在类中的行为,必须写个类来包起来就显得很复杂。我觉得还是像 C++ 形式的最好,支持函数式,同时支持面向对象,Kotlin 这点就类似。完全面向对象太怪异了,入口函数本身也没有作为类成员的意义,像是为了面向对象设计而强制这样搞一样,不是必要的,也可能是现阶段我没理解到它的精神吧。
另外一点 Java 要求入口函数必须有参数,即使不用也要写,这一点上 Kotlin 和 C/C++ 一样不用可以省略。
然而实际上这些工作只是 Kotlin 编译器帮我们做了
fun main()
{
println("你好,世界!")
}
将编译后的字节码文件反编译为 Java,可以看到编译器给加上了类,而用 Kotlin 写的入口函数只是作为成员函数被 Java 的入口函数调用,这样 Kotlin 中的入口函数就可以没有参数。
import kotlin.Metadata;
@Metadata(
mv = {1, 9, 0},
k = 2,
d1 = {"\u0000\b\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a\u0006\u0010\u0000\u001a\u00020\u0001¨\u0006\u0002"},
d2 = {"main", "", "untitled"}
)
public final class MainKt {
public static final void main() {
String var0 = "你好,世界!";
System.out.println(var0);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
但是如果给入口函数加上参数,那么此时 Kotlin 中入口函数和 Java 的入口函数就等同于一样了
fun main(args: Array<String>)
{
println(args.size)
}
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;
@Metadata(
mv = {1, 9, 0},
k = 2,
d1 = {"\u0000\u0014\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u0011\n\u0002\u0010\u000e\n\u0002\b\u0002\u001a\u0019\u0010\u0000\u001a\u00020\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00040\u0003¢\u0006\u0002\u0010\u0005¨\u0006\u0006"},
d2 = {"main", "", "args", "", "", "([Ljava/lang/String;)V", "untitled"}
)
public final class MainKt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkNotNullParameter(args, "args");
int var1 = args.length;
System.out.println(var1);
}
}
4.5 函數
一般函数定义格式
// 默认返回值类型为 Unit
fun foo1()
{
}
// 显式返回值类型
fun foo2(): Unit
{
}
// 参数格式
fun sum1(num1: Int, num2: Int): Int
{
return num1 + num2
}
// 单行语句简化
fun sum2(num1: Int, num2: Int): Int = num1 + num2
// 参数默认值
fun sum3(num1: Int, num2: Int = 10) = num1 + num2
fun main()
{
println(sum1(9, 1))
println(sum2(11, 9))
println(sum3(10))
println(sum3(10, 20))
}
4.5.1 infix
infix fun Int.sum(num: Int) = this + num
infix fun Float.sum(num: Int) = this + num
fun main()
{
println(4.sum(6))
println(13 sum 7)
println(6.7F sum 4)
}
4.5.2 重载
函数名相同,参数个数或类型不同
fun foo()
{
println("空参数")
}
fun foo(str: String)
{
println(str)
}
fun foo(num: Int)
{
println(num)
}
fun main()
{
foo()
foo("你好,世界!")
foo(10)
}
4.5.3 Lambda 表达式
fun main()
{
// 无参数
val printHello: () -> Unit = { println("你好,世界!") }
printHello()
// 带参数
val showInfo: (String, Int) -> Unit = { name, age -> println("$name $age") }
showInfo("小红", 20)
// 带类型推断
// 无显式声明参数类型,it 关键字代表参数
val greet: (String) -> Unit = { println("你好,$it") }
greet("小强")
// 作为函数参数
// 为了提高代码可读性,一般使用不同关键词区分用途
// action:用于执行某种副作用操作或处理
// operator:用于定义运算符重载或特定的逻辑
// callback:作为回调函数的参数
// predicate:作为谓词函数的参数
// transform:作为转换函数的参数
// comparator:作为比较器的参数
// validator:作为验证函数的参数
fun funGreet(name: String, action: (String) -> Unit)
{
action(name)
}
funGreet("小明", greet)
fun calculate(x: Int, y: Int, operator: (Int, Int) -> Int): Int
{
return operator(x, y)
}
// 简化形式
val sum = { x: Int, y: Int -> x + y }
println(sum(4, 6))
println(calculate(11, 9, sum))
}
4.5.4 可变参数
fun show(vararg items: String)
{
items.forEach {
print("$it ")
}
println("\n-------------------------")
}
fun show(s: String, vararg items: String)
{
println(s)
items.forEach {
print("$it ")
}
println("\n-------------------------")
}
fun show(s1: String, s2: String = "AAA", vararg items: String)
{
println(s1)
println(s2)
items.forEach {
print("$it ")
}
println("\n-------------------------")
}
fun main()
{
val items = arrayOf("苹果", "香蕉", "菠萝")
show(*items)
show(s = "水蜜桃", *items)
show("芒果", "柿子", items = items) // 指定参数的时候,可以不加星号(展开数组,将每个元素作为一个独立参数传递),将整个数组作为一个参数传递
show(s1 = "榴莲", items = items)
}
4.6 类
4.6.1 属性
class ClassA
{
var num1 = 0
// get 方法在取属性值时会调用
// set 方法在给属性赋值时会调用
// 下面的 get 和 set 是会默认添加的,可以省略
// field 关键字代表变量自身
var num2 = 0
get() = field
set(value)
{
field = value
}
// 可以根据需要修改取值赋值规则
var num3 = 0
get() = field + 2 // 取值结果加 2
set(value)
{
field = value - 3 // 赋值结果减 3
}
}
fun main()
{
// 创建对象
val ca = ClassA()
ca.num1 = 10
println(ca.num1)
// num3 定义的取值赋值方法是默认会添加的
// 所以最终效果和 num1 不重写取值赋值方法的一样
ca.num2 = 10
println(ca.num2)
// 演示自定义的取值赋值方法
println(ca.num3) // num3 初始值为 0,但是取值方法会 +2,所以会输出 2
ca.num3 = 10 // 赋值 10,但是赋值方法会 -3,所以实际赋值是 7
println(ca.num3) // num3 实际值是 7,但是取值方法会 +2,所以会输出 9
}
4.6.2 构造函数
class ClassA
{
// 初始化代码段
init
{
println("A 执行初始化")
}
}
class ClassB constructor(name:String, age: Int) // 主构造函数
{
init
{
print("B 执行初始化:")
println("$name $age") // 主构造函数的参数可以在初始化代码段里使用
}
// 二级构造函数
// 主构造函数有参数的,在二级构造需要代理调用主构造函数 this()
// 调用二级构造函数,首先会执行 init 代码块,再执行二级构造函数的代码块
constructor(name: String, age: Int, height: Float) : this(name, age)
{
print("B 二级构造函数:")
println("$name $age $height")
}
}
// 主构造函数可以省略 constructor 关键字
// 如果需要使用无参构造,则主构造函数不写参数,再写一个无参的二级构造函数即可
//
// 我觉得这种方案更接近于 C++/Python/Java 的构造函数风格
// 直接不管主构造函数,统一使用二级构造函数,显得较为简洁
// 至于 init 的存在,可以方便统一初始化,如果有多个构造函数
// 且都需要执行同样初始化可以放在里面,没需要可以不写 init
class ClassC
{
init
{
println("C 执行初始化")
}
// 用二级构造函数实现无参构造的效果
constructor()
{
println("C 无参构造函数")
}
// 用二级构造函数实现有参构造函数
constructor(name: String, age: Int)
{
print("C 有参构造函数:")
println("$name $age")
}
}
fun main()
{
val ca = ClassA()
val cb1 = ClassB("小明", 20) // 主构造函数
val cb2 = ClassB("小张", 19, 1.77F) // 二级构造函数
val cc1 = ClassC() // 无参构造函数
val cc2 = ClassC("小强", 22)
}
4.6.3 修饰符
4.6.3.1 访问权限修饰符
- public:默认的访问权限修饰符,如果没有显式指定访问权限修饰符,那么默认为public。public修饰的成员在任何地方都可见。
- private:private修饰的成员只在定义它的类中可见,其他类无法访问。
- protected:protected修饰的成员在定义它的类中和它的子类中可见,其他类无法访问。
- internal:internal修饰的成员在同一个模块中可见。模块是指编译在一起的一组Kotlin文件。
C++ 中只有 public、private 和 protected,Java 中没有 internal,但是有一个 default,default 在同一个包里可见,也是不使用修饰符时默认的修饰符,而 Kotlin 默认是 public,只不过感觉 Java 的 default 和 Kotlin 的 internal 实际意义相近。
4.6.3.2 类属性修饰符
abstract // 抽象类
final // 类不可继承,默认属性
enum // 枚举类
open // 类可继承
annotation // 注解类
4.7 继承与重写
4.7.1 一般写法
Java 中类默认可被继承,Kotin 中默认不可被继承
// 使用 open 代表可以被继承
// 任意类默认继承自 Any,如同 Java 中任意类继承自 Object
open class Animal
{
init
{
println("Animal 初始化")
}
// 使用 open 代表可以被重写
open fun sound()
{
println("动物发出叫声")
}
}
// 和 Java 一样,类不支持多继承
class Dog : Animal()
{
init
{
println("Dog 初始化")
}
// 重写使用 override
override fun sound()
{
// super.sound() // 如果要调用父类的
println("狗发出叫声")
}
}
fun main()
{
val dog = Dog()
dog.sound()
}
4.7.2 存在构造函数
// 父类有主构造函数和二级构造函数
open class Animal(val name: String)
{
// 二级构造函数需要代理调用主构造函数 this()
constructor(name: String, age: Int) : this(name)
open fun sound()
{
println("动物 $name 发出叫声")
}
}
// 继承并调用父类构造函数
class Dog(name: String, private val age: Int) : Animal(name)
{
// “类型?”表示值可以为 null
private var color: String? = null
// 二级构造函数需要代理调用主构造函数
constructor(name: String, age: Int, color: String) : this(name, age)
{
this.color = color
}
override fun sound()
{
println("狗 $name $age ${this.color} 发出叫声")
}
}
class Cat : Animal
{
// 子类可以在二级构造函数代理调用父类构造函数
// 调用父类主构造函数
constructor(name: String) : super(name)
// 调用父类二级构造函数
constructor(name: String, age: Int) : super(name, age)
override fun sound()
{
println("猫 $name 发出叫声")
}
}
fun main()
{
val dog1 = Dog("小花", 5)
dog1.sound()
val dog2 = Dog("小黑", 3, "黑色")
dog2.sound()
val cat1 = Cat("小白")
cat1.sound()
val cat2 = Cat("小灰", 1)
cat2.sound()
}
4.7.3 属性重写
open class Animal
{
open var name: String? = null
}
class Dog : Animal()
{
// val 没有 setter 方法
// 即 var 比 val 多一个 setter 方法
// 因此可以用 var 重写 val 属性,反过来不行(我给你,你装不了可以扔掉多的;你给我,没那么多那就装不满)
// 因为都具有 getter 方法,所以这个方法必然要重写
// 如果子类 var 属性是重写的,且重写了 setter 方法,那么父类也必须是 var 属性(才具有 setter 方法)
override var name: String?
get() = "动物 " + super.name // 需要重写 getter 方法
set(name)
{
super.name = name
}
}
fun main()
{
val dog = Dog()
println(dog.name)
dog.name = "小白"
println(dog.name)
}
4.7.4 抽象
这一点和 Java 中一样,抽象类不可实例化,父类里面声明方法原型,不作具体定义,子类重写
// 抽象类自带可被继承的属性
abstract class Animal
{
abstract fun sound()
}
class Dog : Animal()
{
override fun sound()
{
println("狗发出叫声")
}
}
fun main()
{
val dog = Dog()
dog.sound()
}
4.8 接口
4.8.1 声明和默认实现
和 Java 一样,接口中可以声明抽象方法,也可以做默认方法实现
interface IMyInterface
{
fun bar()
fun boo()
{
println("boo 默认实现")
}
}
class Child : IMyInterface
{
override fun bar()
{
println("bar 重写实现")
}
}
fun main()
{
val c = Child()
c.boo()
c.bar()
}
4.9.2 属性
interface IMyInterface
{
// 提供访问器的实现
val x: String
get() = "xxx"
// 抽象属性声明
var y: String?
val z: String
}
class Child : IMyInterface
{
// 重写属性
override var y: String? = null
set(value)
{
field = "y: $field"
}
override val z: String = "你好,世界!"
}
fun main()
{
val c = Child()
c.y = "世界,你好!"
println(c.x)
println(c.y)
println(c.z)
}
4.9.2 多继承
尽管和 Java 一样,类不支持多继承,但是多个接口可以被一个类继承,接口可以继承接口
interface A
{
// 默认实现
fun foo()
{
println("A foo")
}
// 抽象函数声明
fun bar()
}
interface B
{
fun foo()
{
println("B foo")
}
fun bar()
{
println("B bar")
}
}
// 接口继承,父接口中的默认实现和抽象声明可以重写也可以不重写
interface C : A
{
override fun foo()
{
println("foo C 重写 A")
}
override fun bar()
{
println("bar C 重写 A")
}
}
// 从 A 到 C 到 D 为止,A 中所有方法都有了默认实现,D 可以不重写
class D : C
{
}
// A 中 bar 没有默认实现,继承后必须重写
class E : A
{
override fun bar()
{
println("bar E 重写 A")
}
}
// 多继承
// 父接口有相同方法名的需要重写
class F : A, B
{
override fun foo()
{
// 可以选择调用父接口的默认实现,也可以自定义
// super<A>.foo()
// super<B>.foo()
println("foo F 重写 A 和 B")
}
override fun bar()
{
println("bar F 重写 A 和 B")
}
}
fun main()
{
var d = D()
d.foo()
d.bar()
val e = E()
e.foo()
e.bar()
val f = F()
f.foo()
f.bar()
}
4.9.3 函数式接口
只有一个抽象方法的接口称为函数式接口
fun interface Calculator
{
fun foo(a: Int, b: Int): Int
}
fun main()
{
val add: Calculator = Calculator { a, b -> a + b }
val sub: Calculator = Calculator { a, b -> a - b }
println(add.foo(4, 6))
println(sub.foo(12, 10))
}
4.9 扩展
实际用处感觉和 Python 中的装饰器差不多,可以在不修改原代码的基础上进行扩展功能
// 自定义类
class Rectangle(val width: Int, val height: Int)
// 扩展函数
fun Rectangle.isSquare(): Boolean
{
return width == height
}
// 扩展属性
val Rectangle.area: Int
get() = width * height
// 使用扩展函数和扩展属性
fun main()
{
val rectangle1 = Rectangle(5, 5)
val rectangle2 = Rectangle(4, 6)
println(rectangle1.isSquare()) // 输出:true
println(rectangle2.isSquare()) // 输出:false
println(rectangle1.area) // 输出:25
println(rectangle2.area) // 输出:24
}
4.10 数据类
// 对于数据类的主构造器中定义的属性
// 提供了默认的 toString、equals、copy 等实现
data class ClassA(val name: String, val age: Int)
{
var height: Float = 0F
fun myToString(): String
{
return "ClassA(name=$name, age=$age, height=${this.height})"
}
}
fun main()
{
val ca1 = ClassA("小明", 20)
ca1.height = 1.7F
val ca2 = ca1.copy(name = "小强") // copy 默认实现
println(ca1) // toString 默认实现
println(ca2)
println(ca1.myToString()) // toString 自定义实现
println(ca2.myToString())
println(ca1 == ca2) // equals 默认实现
}
4.11 伴生
Kotlin 中没有 static,但是存在伴生对象,作用类似 static
class ClassA
{
companion object
{
const val value = 1
fun test1()
{
println("test1")
}
// 在 Kotlin 中,调用 test1 和 test2 没区别
// 但是如果是 Java 来调用,则没有注解的需要通过 类名.companion 才行
@JvmStatic
fun test2()
{
println("test2")
}
}
}
fun main()
{
println(ClassA.value)
ClassA.test1()
ClassA.test2()
ClassA.Companion.test1()
ClassA.Companion.test2()
}
4.12 枚举类
enum class Color
{
RED, GREEN, BLUE
}
// 包含属性方法
enum class Direction(val value: Int)
{
NORTH(0),
EAST(90),
SOUTH(180),
WEST(270);
fun getDescription(): String
{
return when (this)
{
NORTH -> "向北"
EAST -> "向东"
SOUTH -> "向南"
WEST -> "向西"
}
}
}
fun main()
{
val color = Color.GREEN
println(color)
val direction = Direction.EAST
println(direction.getDescription())
println(direction.value)
}