Kotlin 学习笔记——概览、类与对象
编辑概览OverView
- Kotlin的变量或方法声明和Java相比是“反”的,也就是类型声明在后,变量或方法名在前;
- Kotlin更严格,这点体现在方法声明时就确定变量是否可变,类默认不可继承;
- Kotlin更安全,变量声明的类都是非空的,如果允许为空,则需要在类后面加
?
; - Kotlin用
println()
函数进行打印,并且每行结束不用加;
; - Kotlin注释格式与Java相同;
- Kotlin支持字符串模板表达式,使用
"${expression}"
的格式可以对expression
进行运算并将其转为字符串; - Kotlin字符串可以直接用
==
进行比较;
变量与函数
Kotlin变量声明格式如下:
val/var a: Type = someThing
其中val
代表不可变,对应Java中的final Type
;var
代表可变变量,就等同于Java中普通的变量。Type
是变量类型,不可空,如果允许为空,则用Type?
表示。在声明时便要求声明变量为val
或var
,是Kotlin安全性上的体现,一般情况下,不可变对象都是线程安全的。
Kotlin一般函数声明如下:
fun funcationName(arg: Type): ReturnType {
// 函数体
}
可以看到参数的声明也是后置类型,ReturnType
是函数的返回类型,也可以不写返回类型代表无返回值。Kotlin函数还有很多用法,我们将在后面再提到。
在变量或函数声明最开始都可以加private
等关键字进行修饰。
循环、List和Map
Kotlin只有for循环和Java稍有不同,for循环基本结构为:
for (item: Type in collection) { /* ... */ } // 可以省略类型
若使用数字迭代,则常用以下两种方式:
// 区间,左闭右闭。下面i = 1,2,3
for (i in 1..3) { /* ... */ }
// until函数,左闭右开,符合常用习惯。下面i = 1,2
for (i in 1 until 3) { /* ... */ }
// 可以用 step 和 downTo 控制步长与顺序。下面i = 10,8,6,4,2
for (i in 10 downTo 2 step 2) { /* ... */ }
Kotlin使用listOf()
生成List,使用MapOf()
生成Map,用法如下:
val list = listOf<String>("a", "b", "c")
print(list[1]) // b
val map = mapOf<String, Int>("key1" to 1, "key2" to 2)
print(map["key1"]) // 1
之后会对集合进行更详细说明。
一切皆对象
Kotlin相对于Java面向对象的程度更高。在Kotlin中,Java的boolean
,int
,char
,double
等数据类型的首字母全部变成大写,即Boolean
,Int
,Char
等,这意味着这些数据类型都是对象,可以使用下面这种方法进行操作:
1.toChar()
true.and(false)
1.2.compareTo(1)
类Class
在Java中,所有对象都继承自Object
,而在Kotlin中,所有对象均继承自Any
(如果可能是null
,则继承Any?
)。Any
类中有三个方法,分别为hashCode()
,equals()
与toString()
。类声明与创建方法如下:
class Name {
// 类主体
}
// 如果类中没有body体,可以用这种方式
class Empty
// 创建类,Kotlin中没有 new 关键字
val person: Person = Person()
// Kotlin拥有类型推导机制,可以省略变量类型声明
val person = Person()
构造函数Constructor
每个类都可以有一个主构造函数和次构造函数。
主构造函数
主构造函数如下所示:
class Person constructor(firstName: String) { /* ... */ }
// 构造函数无注解时也可以省略 constructor 关键字
class Person(firstName: String) { /* ... */ }
// 若有注解,则 constructor 关键字不能省略
class Person public @Inject constructor(firstName: String) { /* ... */ }
// 推荐使用
class Person(val firstName: String) { /* ... */ }
// 或者
class Person(private val firstName: String) { /* ... */ }
主构造函数没有函数体,如果想进行初始化工作(就像Java中的构造函数一样),可以放在init
块中,init
块中的函数会在创建对象时被调用:
class Person(firstName: String) {
init {
println("构造函数")
}
}
Kotlin的主构造函数有点像是语法糖,当你调用构造函数时,Kotlin编译器会自动将参数赋值给主构造函数中的声明的参数,这个过程类似于Java中的:
class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
Person person = new Person("Bob");
所以,Kotlin主构造函数中的参数可以在类body中任意的地方使用。Kotlin中类,字段和函数默认修饰符都是public
,protected
和private
关键字作用和Java中保持一致,但是使用internal
表示同一module下可见。
如果没有指定主构造函数,则编译器会为其自动生成一个无参的主构造函数,这点和Java是一致的。
次构造函数
次构造函数是在类body中使用constructor
函数进行表示:
class Person {
constructor(name: String) { /* ... */ }
}
一个类可以有多个次构造函数,但只能有一个主构造函数。如果一个类既有主构造函数又有次构造函数,那么每个次构造函数必须直接或间接调用主构造函数,使用this
关键字:
class Person(val name: String) {
constructor(name: String, age: Int) : this(name) { /* ... */ }
constructor(name: String, age: Int, sex: String) : this(name, age) { /* ... */ }
}
主构造函数调用在此构造函数之前。注意,**只有主构造函数中的参数可以在类body中的任何地方调用,次构造函数的参数只能在构造函数中使用。**下面是一个较为完整的例子:
class Person(val name: String) {
init {
println("name: ${this.name}")
}
constructor(name: String, age: Int) : this(name) {
println("name: $name, age: $age")
}
constructor(name: String, age: Int, sex: String) : this(name, age) {
println("name: $name, age: $age, sex: $sex")
}
}
fun main() {
val person = Person("张三")
}
// 输出:
// name: 张三
// name: 张三, age: 18
// name: 张三, age: 18, sex: 男
Kotlin构造函数参数支持默认值,如下:
class Person(val name: String = "李四") {
init {
println("name: ${this.name}")
}
constructor(name: String = "王五", age: Int = 20) : this(name) {
println("name: $name, age: $age")
}
constructor(name: String = "赵六", age: Int = 21, sex: String = "男") : this(name, age) {
println("name: $name, age: $age, sex: $sex")
}
}
fun main() {
val person = Person()
val person1 = Person(name="甲")
val person2 = Person(name="乙", age=10)
val person3 = Person(age=30)
val person4 = Person(sex="女")
}
// 第一行代码输出:
// name: 李四
// 第二行代码输出:
// name: 甲
// 第三行代码输出:
// name: 乙
// name: 乙, age: 10
// 第四行代码输出:
// name: 王五
// name: 王五, age: 30
// 第五行代码输出:
// name: 赵六
// name: 赵六, age: 21
// name: 赵六, age: 21, sex: 女
可以看到Kotlin会优先使用参数较少的构造方法,当传入的参数不在此构造方法时,Kotlin才会选择参数更多的构造方法。注意次构造函数中参数不允许使用val/var
修饰。
继承Inheritance
上面已经说到,所有的类都直接或间接继承自Any
类。在Kotlin中,所有的类默认都是final
的,因为类默认是不可继承的,如果允许其被其他类继承,则需要使用open
关键字进行修饰:
open class Person { /* ... */ }
父类指定了主或次构造函数时:如果子类有主构造函数,则父类必须被在继承时立即初始化:
open class Father(val lastName: String) {
init {
println("父亲姓 ${lastName}")
}
}
class Son(val lastNameSon: String) : Father(lastNameSon) {
init {
println("儿子姓 $lastNameSon")
}
}
父类指定了主或次构造函数时:如果子类没有主构造函数,那么它的每个次构造函数都必须使用super
初始化父类,或者委派给另外一个有此功能的此构造函数:
open class Father(val lastName: String)
class Son : Father {
constructor(familyName: String) : super(familyName) {
println("这里用的是父类的字段lastName=$lastName")
}
// 委派给子类的第一个次构造函数
constructor(familyName: String, age: Int) : this(familyName) {
println("通过儿子的姓我们知道父亲姓 $lastName,儿子 $age 岁了")
}
}
fun main() {
val son = Son("王")
val son2 = Son("王", 20)
}
// 第一行代码输出:
// 这里用的是父类的字段lastName=王
// 第二行代码输出:
// 这里用的是父类的字段lastName=王
// 通过儿子的姓我们知道父亲姓 王,儿子 20 岁了
父类没有指定主或次构造函数时:如果子类没有指定主构造函数和次构造函数,那么在继承时使用父类默认的构造函数;否则在主构造函数处继承父类构造函数或者次构造函数处使用super
实例化父类:
open class Father
class Son : Father()
class Son2 : Father {
constructor() : super() { /* ... */ }
}
Kotlin与Java一样,仅仅支持单继承,但是可以实现多个接口。
重写override
在Java中重写父类方法使用的是@Override
注解,而在Kotlin中则使用override
关键字,除此以外函数默认也是final
的,如果需要被在子类中重写同样需要使用open
关键字进行修饰:
open class Father {
open fun eat() { /* ... */ }
fun sleep() { /* ... */ }
}
class Son : Father() {
override fun eat() { /* .... */ }
}
使用final
关键字可以使方法不再次被子类重写(只被自己重写):
class Son : Father() {
final override fun eat() { /* .... */ }
}
Kotlin也支持属性重写,用法和函数重写类似:
open class Father() {
open val age: Int = 40
}
class Son1(override val age: Int = 22) : Father() // age不可更改
class Son2 : Father() {
override var age = 20; // age此时可以更改
}
抽象类Abstract Class
Kotlin中的抽象类与Java类似,使用abstract
关键字进行修饰,需要注意的是当继承抽象类时,需要对父类(抽象类)进行实例化:
abstract class Father
class Son : Father()
抽象类默认也是open
的。
接口Interface
Kotlin使用interface
声明接口,接口中的方法默认为open
,且支持默认方法实现;接口中也可以有属性:
interface Worker {
val count: Int // 抽象的属性,不允许赋值
fun run() {
println("Worker中的方法")
}
}
注意,实现类中对接口中的属性进行重写,不可以将var
属性变为val
属性,但可以将val
变为var
。
当子类同时实现或继承接口与父类时,重写规则如下:
open class Machine {
open fun run() {
println("Machine中的方法")
}
}
class Computer : Machine(), Worker {
val app = "QQ"
override fun run() {
println("Computer中的方法")
}
fun runAll() {
run()
// 通过这种方式调用父类或接口的方法
super<Machine>.run()
super<Worker>.run()
}
}
fun main() {
val com = Computer()
com.runAll()
}
// 输出
// Computer中的方法
// Machine中的方法
// Worker中的方法
内部类Inner Class
嵌套类就是在类中的类:
class Computer {
val app = "QQ"
class Desktop {
fun print() {
println("嵌套类中的方法")
}
}
}
嵌套类无法访问外部类的方法或属性,但是使用inner
关键字修饰嵌套类时,嵌套类就变成了内部类,此时就可以访问到外部类的属性或方法了,这是因为此时内部类含有一个外部类的引用(编译器自动生成的):
open class Machine {
open fun playOuter() {
println("父类方法")
}
}
class Computer : Machine() {
val app = "QQ"
override fun playOuter() {
println("外部类的方法")
}
inner class Desktop {
fun print() {
println(app)
}
fun play() {
playOuter() // 调用外部类方法
super@Computer.playOuter() // 调用外部类的父类的方法
}
}
}
fun main() {
val desktop = Computer().Desktop()
desktop.print()
desktop.play()
}
// 输出:
// QQ
// 外部类的方法
// 父类方法
数据类Data Class
在Java中写JavaBean时,我们常常需要对其重写equals()
,hashCode()
,toString()
等方法以便于使用,但这些都是一些重复的工作,我们也可以借助Lombok中的@Data
注解帮我们完成这些工作,而在Kotlin中,这些工作都可以借助关键字data
帮我们实现。数据类的声明如下:
data class User(val name: String, val age: Int)
另外,数据类还给我们提供了函数copy()
帮我们实现数据类的复制,copy()
函数会针对每个数据类生成其对应的构造方法,以上面的数据类为例,它生成的copy()
方法与调用方式如下:
copy(val name: String, val age: Int)
// 如何使用
val oldUser: User = User("李四", 20)
val newUser: User = oldUser.copy(name="王五")
数据类还提供了componentN()
函数返回数据类的第N个属性,以上面的数据类为例,componentN()
函数使用如下:
val user = User("张三", 20)
val name1 = user.component1() // name1 = "张三"
val age1 = user.component2() // age1 = 20
// 或者(这里的component1(),component2()被自动调用)
val (name2, age2) = user // (name2 = "张三", age2 = 20)
// 还可以这么用
val collection = listOf(User("a", 1), User("b", 2), User("3", 3))
for ((name, age) in collection) {
println("name is $name")
println("age is $age")
}
数据类也可以有类body,我们同样可以使用自己的方式重写toString()
等方法。**如果想让数据类拥有无参的构造方法,可以给所有参数指定默认值。**如果不想让编译器自动将参数添加到构造方法中,可以将其放在body中:
data class User(val name: String) {
var age: Int = 18
}
数据类目前有以下几种限制:
-
主构造函数中至少有一个参数;
-
主构造函数中的参数必须使用
val
/var
修饰; -
数据类的父类中的
final
方法不会被重写; -
数据类不能被
abstract
,open
,sealed
或inner
修饰; -
若父类有
open componentN()
并且返回兼容的类型,则会被重写;若返回不兼容类型,则报错;
密封类Sealed Class
密封类只能被和它同文件的类可见,对其他文件的类都不可见(但是它的子类可以被其他文件中的类可见)。密封类使用sealed
关键字修饰:
sealed class Example
密封类是抽象的,而且不可以被实例化,但是可以拥有抽象属性。密封类不允许有非private
的构造方法。
下面的是常用用途:
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
data class Other(val number: Double) : Expr()
// when相当于Java中的switch,但是有更强大的功能如类型转换,我们将在函数一节中说明
fun eval(expr: Expr): Double {
return when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
else -> Double.NaN
}
}
单例类Object Class
在Java中使用单例类是常用的操作,在这里我们就不给出Java中单例的实现(可以看这篇)。Kotlin中原生支持了单例类的实现:
object Singleton {
fun doSomeThing() { /* ... */ }
}
这种方式声明的对象是线程安全的,且在第一次访问时才会被实例化,由JVM保证单例。
调用其方法可以通过类似Java中的静态访问的方式进行:
Singleton.doSomeThing()
单例类也可以继承其他父类。单例类可以作为嵌套类在其他类中,但是不可以在内部类中(即不能在inner class
):
class Test {
object Singleton1 // 可以
inner class Inner {
object Singleton2 // 不可以
}
class Inner2 {
object Singleton3 // 可以
}
}
object
关键字还有一个作用是作为匿名内部类的生成,用法如下:
interface Teacher {
fun speak()
}
fun giveAClass(t: Teacher) {
t.speak()
}
fun main() {
giveAClass(object : Teacher {
override fun speak() {
println("临时老师开始说话")
}
})
// 还可以这么用
val point = object {
var x: Int = 100
var y: Int = 100
}
println(point.x)
}
这种通过匿名内部类间接实现传输函数的方法在Java中比较常见,但是Kotlin中还有更好的方法,我们将在函数一节中继续说明。
除此之外,object
还可以作为伴生类的关键字。
伴生类Companion Class
伴生类的声明方法如下:
class MyClass {
companion object Factory {
val type = "A"
fun create(): MyClass {
return MyClass()
}
}
}
伴生类可以让我们用类似Java中静态方法调用的形式掉用某些方法,上面的例子中我们就可以这样调用:
val type = MyClass.type
val instance = MyClass.create()
// 或者
val type = MyClass.Factory.type
val instance = MyClass.Factory.create()
伴生类的类名声明时可以去掉,这时候Kotlin会自动用Companion
作为伴生类的名称。注意,**一个类中只能有一个伴生类。**伴生类同样可以继承。
内联类Inline Class
内联类目前还在测试中,可能之后会有变化。内联类表示如下:
inline class Wrapper(val value: Type)
在业务中,有时候因为业务逻辑需要对某些“东西”进行抽象,而单独创建对象表示这种“东西”在运行时可能又比较重,加大了系统负担,内联类便是用来解决这种问题的,内联类可以看作一种包装(Wrapper),它是对需要包装的东西的抽象,在编译时,Kotlin会为其生成一个包装器,在运行时,它要么用这个包装器代表,要么用它主构造函数中的参数类型代表,因此内联类有且只能有一个主构造函数属性,这个属性将可能在运行时代替这个内联类。有个简单的例子:
inline class Password(val vaule: String)
这样便为密码字符串生成了一个内联类,既可以方便开发,又不用增加系统创建对象的开销。关于“代表”也有个例子,比如Kotlin中的Int
,它既可以用Java中的原始类型int
表示,也可以用Integer
表示。
内联类可以继承其他类,但是不能被其他类所继承,内联类中可以定义函数或者其他属性,但是这些调用在运行时都将变成静态调用。
委派Delegation
Kotlin原生支持委派模式,所谓委派模式,是指将一个对象的工作委派给另一个对象,在Kotlin中,委派有类委派与属性委派,通过by
关键字实现,通过委派,我们可以将父类的初始化工作交给子类接受的参数:
interface Base {
val value: Int
fun printValue()
fun print()
}
class BaseImpl : Base {
override val value = 10
override fun printValue() {
println("BaseImpl value=${this.value}")
}
override fun print() {
println("BaseImpl print")
}
}
class Delegation(val base: Base) : Base by base {
override val value = 20
override fun print() {
println("Delegation print")
}
}
fun main() {
val baseImpl = BaseImpl()
val delegation = Delegation(baseImpl)
delegation.printValue()
delegation.print()
}
// 输出:
// BaseImpl value=10
// Delegation print
可以看到使用委派对象后,调用方法时会优先调用重写的方法,但是委派对象只能访问自己的接口成员(虽然在Delegation
中重写了接口成员,但是委派对象在调用printValue()
时只能访问自己的接口成员,并不能访问Delegation
中的接口成员)。
Kotlin提供了函数lazy()
实现了属性委托,通过by lazy({...})
的方式为属性提供委托并实现了懒加载:
val lazyValue: String by lazy {
println("进行某些操作")
"Hello"
}
这里lazy
使用了lambda表达式,如果你还不清楚lambda表达式,这里你可以简单的理解为当代码运行到这里时,会自动执行块中的代码,并将最后一行的值作为块的返回值。通过lazy
函数进行委托,只有在第一次访问lazyValue
时才会对其进行初始化并记住其值,之后的访问将会直接返回这个值。
lazy()
函数默认是线程安全的,如果需要多个线程同时访问可以传参LazyThreadSafetyMode.PUBLICATION
;如果不需要线程同步可以传参LazyThreadSafetyMode.NONE
。
还有一个常用的委派方法是使用Map进行传参,这个方法常常用于解析JSON,用法如下:
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
val user = User(mapOf(
"name" to "张三",
"age" to 20
))
println(user.name) // 张三
println(user.age) // 20
- 0
- 0
-
分享