本文是kotlin系列第二篇
- 类
- 类的成员和可见性修饰符
- 构造函数
- 继承,抽象和接口
- 拓展方法
- 空类型安全
- 智能类型转换
- 类属性的延迟初始化
- 代理Delegate
- 单例object
- 内部类
- 数据类data class
- 枚举类enum class
- 密封类sealed class
- 内联类inline class
一、类
在Java中,类基于Object,而在Kotlin中类基于 Any,所有类都默认继承Any
Any类中,只有3个方法,分别是equals,hashCode和toString。
package kotlin
public open class Any {
public open operator fun equals(other: Any?): Boolean
public open fun hashCode(): Int
public open fun toString(): String
复制代码
-
声明类的关键字是 class
-
声明类的格式
class Test{
// 属性...
...
// 构造函数
...
// 函数
...
// 内部类
...
...
}
复制代码
- 需要注意的是,如果类没有结构体,那么大括号是可以被忽略的
class Test
复制代码
说一千道一万,不如来个小例子
class Animal {
fun eat() {
println("每天都是吃吃吃")
}
fun move() {
println("每天都是浪浪浪")
}
}
fun main() {
var ani = Animal()
ani.eat()
ani.move()
}
复制代码
输出
每天都是吃吃吃
每天都是浪浪浪
复制代码
.
.
关于kt类需要注意的几个点
Kotlin的接口是可以包含属性声明,Kotlin默认的声明是fianl 和public的,意味默认不允许被继承
(
Kotlin的类和方法默认都是final的
(准确来说是 public final),这意味着,默认是不允许被继承的
,一个类想要允许被继承
,那么需要显示地声明为 open
。(Java的类默认是不会加上的final的,也就是随意继承))
一个方法如果如果想要被重写,也需要声明为open。如果类不是open的,类的成员也不允许open。
open class Animal {
fun eat() {
println("每天都是吃吃吃")
}
open fun move() {
println("每天都是浪浪浪")
}
}
// kt的继承用的是 : ,不是extends
class Bird : Animal() {
// eat 方法不是open,不允许被复写
// move方法就允许被复写
override fun move() {
super.move()
println("一直都在天上,顽皮小鸟喝不醉")
}
}
fun main() {
var b = Bird()
b.eat()
b.move()
}
复制代码
输出
每天都是吃吃吃
每天都是浪浪浪
一直都在天上,顽皮小鸟喝不醉
复制代码
当然了,除了open,我们也能使用 abstract 让类变成抽象类,这样也能被继承。抽象的我们另外再说。
二、类的成员和访问修饰符
类的成员
- 构造函数和初始化块(
Constructors and initializer blocks
) - 函数(
Functions
) - 属性(
Properties
) - 嵌套类和内部类(
Nested and Inner Classes
) - 数据对象(
Object Declarations
)
访问修饰符/可见性修饰符
类的修饰符包括 classModifier 和_accessModifier_:
classModifier: 类属性修饰符,标示类本身特性。
- abstract // 抽象类
- final // 类不可继承,默认属性
- enum // 枚举类
- open // 类可继承,类默认是final的
- annotation // 注解类
accessModifier: 访问权限修饰符
- public kt中的默认修饰符,全局可见
- protected 受保护修饰符,类及子类可见
- private 私有修饰符,类内修饰只有本类可见,类外修饰文件内可见
- internal 模块内可见 (这个是kt独有的,java中没有)
1、与java不同,kotlin中啥也不写,默认就是public
的,而java中不写默认是default
包内可见
2、kotlin中多一个限制可见性的internal
关键字,去掉了default
关键字
3、protected
只能用来修饰成员。
对于
protected
, java是包内可见,而kotlin是类内可见,这点不同,当然子类肯定都是可见的,kotlin中protected
不能用来修饰类
// 文件名:example.kt
package foo
private fun foo() {} // 在 example.kt 内可见
public var bar: Int = 5 // 该属性随处可见
internal val baz = 6 // 相同模块内可见
复制代码
三、kotlin类的构造函数
kotlin类构造函数的最大特色,就是,就是分为 主构造函数
和 次构造函数
。
- 主构造函数 – 初始化类的简洁方法(仅有一个)
- 次构造函数 – 放置其他初始化逻辑(允许多个)
在kotlin中,constructor关键用户声明构造方法,在一定条件下,这个constructor允许被省略。
kotlin中,构造函数是通过 constructor 关键字来标明的,对于主构造函数来说,它的位置在类的标题中声明,而对于次级构造函数来说它的位置在类中。
主构造函数
主构造函数
是类标头
的一部分- 用
括号
括起来的代码块
是主构造函数
- 主构造函数允许含有默认值,其实这也想当初初始化
- 主构造函数的语法受约束,不能包含任何代码。如果有初始化操作,应该放在
init
这个特有的代码块里面
也可以认为,主构造函数的初始化,要么是设置默认值,要么就就必须在init代码块里面实现
- 声明一个构造函数一般需要用到
constructor
关键字,但是当constructor关键字没有注解
和可见性修饰符
作用于它时,constructor关键字可以省略。 - 当我们定义一个类并没有声明一个主构造函数的时候,kotlin会默认为我们生成一个无参的主构造函数,这一点和java一样。
说完了,来代码吧。
// constructor(name:String,age:Int) 就是主构造函数
// 主构造函数是类标头的一部分
class Person constructor(name:String,age:Int,hobby:String = "睡觉觉"){
val pName:String
val pAge:Int
val pHobby:String
// this.name = name 不能这么写,初始化的操作必须放在 init 里面
// init 代码块用户初始化构造函数
init {
this.pName = name
this.pAge = age
this.pHobby = hobby
println("名字: $pName")
println("年龄: $pAge")
println("爱好: $pHobby")
}
}
fun main() {
var tony = Person("王哈哈",18)
}
输出:
名字: 王哈哈
年龄: 18
爱好: 睡觉觉
复制代码
.
.
其实就上面这个 Person 类而言,constructor 是可以被省略的。
// 比如这个类就可以省略 constructor
// 因为这个 constructor 没有注解 和 可见性修饰符
//class Animal constructor (move:String){
class Animal(move:String){
val aMove:String;
init {
this.aMove = move;
}
}
// 这个 constructor 就不能省略,有可见性修饰符
class Animal2 private constructor(move:String){
val aMove:String;
init {
this.aMove = move;
}
}
复制代码
次构造函数
在Kotlin中,一个类还可以包含一个或多个次构造函数。它们是使用 constructor 关键字创建的。
次构造函数在Kotlin中并不常见。 当您需要扩展提供以不同方式初始化类的多个构造函数的类时,次要构造函数的最常见用法就出现了。
主要看以下几点:
次构造函数
一定需要使用constructor
定义- 在Kotlin中,您还可以使用
this()
来从同一类
的另一个构造函数
(如Java中)调用构造函数。 - 通过在一个类中使用
主和次
级构造函数,次级构造函数
需要授权给(委托给)``主构造函数
,也就是次级构造函数会直接
或者间接
调用主构造函数。使用this()
关键字对同一个类中的另一个构造函数进行授权(委托)。 - 子类构造函数可以通过
super
关键字调用父类的构造方法
次构造函数的大概有两种使用方式
- 直接使用次构造函数
- 主 次 构造函数一起使用
直接使用次构造函数
class Car{
constructor(name: String) {
println("车的名字是 $name")
}
}
fun main() {
var car = Car("大黄蜂")
}
输出
车的名字是 大黄蜂
复制代码
如上,只有次构造函数。
主 次 构造函数一起使用
主要看看代码里面的 this
-
在Kotlin中,您还可以使用 this() 来从同一类的另一个构造函数(如Java中)调用构造函数。
-
通过在一个类中使用主和次级构造函数,次级构造函数需要授权给(委托给)主构造函数,也就是次级构造函数会
直接
或者间接
调用主构造函数。使用this()
关键字对同一个类中的另一个构造函数进行授权(委托)。
class Animal(val name: String) {
init {
val outPutName = "init 动物叫做: $name"
println(outPutName)
}
// 使用 this() 来从同一类的另一个构造函数(如Java中)调用构造函数。
// 直接 委托给主构造方法
constructor(name: String, age: Int): this(name) {
println("直接委托: 名字:$name 年龄:$age")
}
// 通过上面的构造方法 间接 委托给主构造方法
constructor(name: String, age: Int, move: String): this(name, age) {
println("简介委托: 名字:$name 年龄:$age 动起来:$move" )
}
}
fun main() {
var bird1 = Animal("主黄蜂")
println("==========")
var bird2 = Animal("次黄蜂1号",18)
println("==========")
var bird3 = Animal("次黄蜂2号",18,"一飞冲天")
}
复制代码
.
.
- 子类构造函数可以通过super关键字调用父类的构造方法。(不这么干还会报错)
open class Car{
constructor(name: String) {
println("Car类 车的名字是 $name")
}
}
// 子类利用super调用父类的构造方法
class NiceCar : Car{
constructor(name: String,spec: String):super (name) {
println("NiceCar 车的名字是 $name 。 技能是:$spec")
}
}
fun main() {
var niceCar = NiceCar("大黄蜂","每天都在天上飞"
}
复制代码
构造函数和继承
继承这一块,本来是后面讲,但是这里讲到构造函数,就干脆放一起了。
- 当父类存在
主构造函数
,子类也必须有一个构造函数(可以是 主构造函数 或者 次构造函数),不然报错
open class Animal(name: String){
}
// 主构造
class Bird(name: String) : Animal(name) {
}
// 次构造
class Bird : Animal {
constructor(name: String) : super(name)
}
复制代码
.
.
- 当父类存在多个构造函数,子类的主构造函数一般实现参数最多父类中参数最多的构造函数。子类中参数少的可以用this关键字引用。
open class Animal{
constructor(name:String)
constructor(name:String,gender:String)
constructor(name:String,gender:String,age: Int)
}
class Bird(name: String, gender: String, age: Int) : Animal(name, gender, age) {
constructor(name: String) : this(name,"",0)
}
复制代码
四、继承,抽象和接口
继承
继承需要用到 open
和 :
,一个类如果想被继承,就需要声明为open。继承需要用到 :
(java中是extends),类的方法
和属性
,如果想要被复写
,也需要声明为open
。
来一份小代码,演示下 open,: 注意看方法和属性是否为open
open class Animal{
var hobby:String = "爱好"
open var name:String ="动物";
fun move(){
println("运动")
}
open fun eat(){
println("吃吃吃")
}
}
// 继承一下
class Bird1 : Animal() {
}
// 继承一下
class Bird2 : Animal() {
// 这是能复写是因为open了
// hobby没有open无法override
override var name: String
get() = super.name
set(value) {"鸟呀"}
// 这是能复写是因为open了
// move无法复写,无法override
override fun eat() {
super.eat()
println("吃点虫子吧")
}
}
fun main() {
var b1 = Bird1();
var b2 = Bird2();
println(b1.name)
println(b1.hobby)
b1.eat()
b1.move()
println("===============")
println(b2.name)
println(b2.hobby)
b2.eat()
b2.move()
}
输出:
动物
爱好
吃吃吃
运动
===============
动物
爱好
吃吃吃
吃点虫子吧
运动
复制代码
.
.
- 父类的open函数子类可以重写这点我知道了,但是父类中没有用open修饰的函数,子类也不能起相同名字的函数名。
抽象类
-
Kotlin中抽象可分为抽象类、抽象函数、抽象属性,
抽象类的成员包括抽象函数和抽象属性
。抽象类的成员只有定义而没有实现,也不能有实现,否则就是非抽象类了
。 -
声明抽象使用
abstract
关键字进行修饰,抽象类的子类必须全部重写带有abstract
修饰的抽象属性
和函数
。抽象类中允许存在非抽象的函数和属性。 -
抽象类默认就是open修饰的,可以直接继承。不像普通类要继承需要声明为open
-
抽象类的属性可以的保存状态的。(接口的属性不能保存状态,必须抽象)
abstract class Car{
// 非抽象的 属相和函数,可以有初始化值和实现
// whoMake有值,说明抽象属性是可以保存状态的
var whoMake = "人类制造"
// 抽象类中的普通函数,默认final,没有指定为open,子类不能复写
fun oKCar(){
println("检验合格")
}
// 抽象成员不能有初始化值
abstract var name:String
// 抽象方法不能有实现(不能有{})
abstract fun spec(spe:String)
}
class NiceCar : Car() {
// 必须复写的,不然报错
override var name:String = "上天汽车"
// 必须复写的,不然报错
override fun spec(spe: String) {
println(spe)
}
}
fun main() {
var niceCar = NiceCar()
println(niceCar.whoMake)
niceCar.oKCar()
println(niceCar.name)
niceCar.spec("每天都在天上飞")
}
输出:
人类制造
检验合格
上天汽车
每天都在天上飞
复制代码
接口类
kt使用关键字interface定义接口,一个类可实现一个或多个接口
-
接口的实现使用的关键字是:冒号(
:
)。(这一点是和Java
不同的。Java
中使用接口使用的是implements
关键字) -
接口只能继承接口,不能继承类
-
kt接口的成员支持private和public两种访问修饰符,默认public(java的接口的成员的修饰符只能是public)
-
Kotlin 的接口可以有抽象方法也可以有非抽象方法。接口中的函数,如果带了结构体/方法体(带了{}),那么就不再是抽象方法。
-
接口中的函数,一般是没有方法体的(默认是public abstract),那么必须复写;如果函数有方法体(public open),可以不复写。
-
接口无法保存状态。
-
接口可以有属性但必须声明为抽象,或着提供访问器实现(提供一个get方法,但提供了就不再是抽象函数)。
-
接口的属性不能幕后字段(backing field),其实就是不能直接赋值。
接口中的函数
interface SimpleInterface{
// 定义一个无参数无返回值的方法
fun fun1()
//定义一个有参数的方法
fun fun2(num: Int)
// 定义一个有参数有返回值的方法
fun fun3(num: Int) : Int
// 下面的两个方法是有结构体, 故可以不重写
//定义一个无参数有返回值的方法
fun fun4() : String{
return "fun4"
}
//定义一个无结构体函数,大括号是可以省略的
fun fun5(){
// 如果函数中不存在表达式,大括号可以省略。
// 如fun1一样
}
}
class Demo2 : SimpleInterface{
// fun1,fun2,fun3必须复写(因为接口中他们没有返回值),不然报错
override fun fun1() {
println("我是fun1()方法")
}
override fun fun2(num: Int) {
println("我是fun2()方法,我的参数是$num")
}
override fun fun3(num: Int): Int {
println("我是fun3()方法,我的参数是$num,并且返回一个Int类型的值")
return num + 100
}
override fun fun4(): String {
println("我是fun4()方法,并且返回一个String类型的值")
/*
接口中的fun4()方法默认返回”fun4“字符串.
可以用super.fun4()返回默认值
也可以不用super关键字,自己返回一个字符串
*/
return super.fun4()
}
/*
接口中的fun5()带有结构体,故而可以不用重写,
fun4()同样
*/
// override fun fun5() {
// super.fun5()
// }
}
fun main() {
var demo = Demo2()
demo.fun1()
demo.fun2(5)
println(demo.fun3(3))
println(demo.fun4())
//可以不重写该方法直接调用
demo.fun5()
}
输出:
我是fun1()方法
我是fun2()方法,我的参数是5
我是fun3()方法,我的参数是3,并且返回一个Int类型的值
103
我是fun4()方法,并且返回一个String类型的值
fun4
复制代码
接口中的属性
接口可以有属性但必须声明为抽象,或着提供访问器实现(提供一个get方法,但提供了就不再是抽象函数)
interface SimpleInterface{
// 抽象属性 接口中的属性默认就是抽象的
val num: Int //等价于 public abstract val num: Int
// 非抽象属性 提供了访问器
// 等价于 public open val hobby: String
val hobby: String
get() = "一些爱好"
fun fooNum() {
print(num)
}
}
class Demo2 : SimpleInterface{
// 因为num是抽象的,所以这里我们必须复写
override val num: Int
get() = 100
}
fun main() {
var demo2 = Demo2()
println(demo2.num)
// 因为hobby提供了访问器get,所以我们能直接访问到
println(demo2.hobby)
// 调用接口中的方法
demo2.fooNum()
}
复制代码
.
.
接口的属性不能幕后字段(backing field),其实就是不能直接赋值。
接口继承
一个接口可以从其他接口派生,从而既提供基类型成员的实现,也声明新的函数与属性。很自然地,实现这样接口的类只需定义所缺少的实现
interface Person : Named {
var firstName: String
var lastName: String
override val name: String get() = "$firstName $lastName"
}
// Employee实现自 Person,而Person实现自Named
class Employee : Person {
// Employee不需要复写name属性了
// 按道理 Employee 应该复写爷爷类的name属性
// 但是因为他爹Person已经复写过了,所以Employee不需要复写name属性了
// 当然了,只要Employee愿意,也可以自己复写name
override var firstName: String = ""
override var lastName: String = ""
}
fun main() {
var emp = Employee()
emp.firstName = "大海"
emp.lastName = "李"
println(emp.name)
}
输出:
大海 李
复制代码
解决覆盖冲突
如果甲接口和乙接口都有相同的方法,那么丙派生自甲乙,这个时候,应该怎么办呢?
interface A {
fun foo() { println("A-foo") }
fun goo()
fun move()
}
interface B {
fun foo() { println("B-foo") }
fun goo() { println("B-goo") }
fun move()
}
// 我们注意到,A和B都实现foo方法,两者的foo都不是抽象方法了
// A的goo方法是抽象方法,A的goo是 非抽象方法
// A和B的move方法都是抽象方法
class C : A {
// 因为A中goo和move都是抽象的
override fun goo() { println("C-goo") }
override fun move() {println("C-move") }
}
class D : A, B {
// 多继承,A和B都实现了foo,所以这里需要复写
override fun foo() {
super<A>.foo()
super<B>.foo()
println("D-foo")
}
//多继承,因为B的goo方法自己有实现,所以我们这里只需要复写superB的
override fun goo() {
super<B>.goo()
println("D-goo")
}
//多继承,因为A和B的move都是抽象的,所以这里我们不需要superA或者B
override fun move() {
println("D-move")
}
}
fun main() {
var c = C();
c.foo()
c.goo()
c.move()
println("=========")
var d = D();
d.foo()
d.goo()
d.move()
}
输出:
A-foo
C-goo
C-move
=========
A-foo
B-foo
D-foo
B-goo
D-goo
D-move
复制代码
五、拓展方法
拓展,这个可是个好东西。
kotlin
支持在不修改类
代码的情况下,动态为类添加属性(扩展属性
)和方法(扩展方法
)。
举个例子,一个三方库你觉得很好用,但是用着用着拍大腿说他为什么没有xxx方法,但是你有修改不了它的代码,这个时候,就该 拓展
登场了。
扩展方法执行静态解析(编译时),成员方法执行动态解析(运行时)。
原理
kotlin扩展属性、方法时看起来是为该类动态添加了成员,实际上并没有真正修改这个被扩展的类,kotlin实质是定义了一个函数,当被扩展的类的对象调用扩展方法时,kotlin会执行静态解析,将调用扩展函数静态解析为函数调用。
格式
其实很简单,就是放方法名前面加上 类型.
来一个感受感受
class Person{
fun eat(){
println("吃吃吃")
}
}
fun Person.playMan(){
println("玩世不恭")
}
fun main() {
var per = Person()
per.eat()
per.playMan()
}
输出:
吃吃吃
玩世不恭
复制代码
拓展方法
当拓展遇到本尊 真假美猴王
如果被扩展的类的扩展方法与该类的成员方法名字和参数一样,该类对象调用该方法时,调用的会是成员方法。
成员方法优先于拓展方法
class Person{
fun eat(){
println("吃吃吃")
}
}
fun Person.playMan(){
println("玩世不恭")
}
fun Person.eat(){
println("大吃一惊")
}
fun main() {
var per = Person()
per.eat() // 拓展遇到本尊,就会怂了
per.playMan()
}
复制代码
系统自带的类也能拓展
举个例子,为系统类String添加拓展方法
//为String定义一个拓展方法
fun String.lastIndex() = length - 1
复制代码
str.lastIndex()方法执行的过程为:
1、检查str类型(发现为String类型); 2、检查String是否定义了lastIndex()成员方法,如果定义了,编译直接通过;
3、如果String没定义lastIndex()方法,kotlin开始查找程序是否有为String类扩展了lastIndex()方法(即是否有fun String.lastIndex()),如果有定义该扩展方法,会执行该扩展方法;
4、既没定义lastIndex()成员方法也没定义扩展方法,编译自然不通过。
当子父类都拓展了同一个方法
由于静态调用扩展方法是在编译时执行,因此,如果父类和子类都扩展了同名的一个扩展方法,引用类型均为父类的情况下,会调用父类的扩展方法。
/**
* 父类
*/
open class ExtensionTest
/**
* 子类
*/
class ExtensionSubTest : ExtensionTest()
/**
* 父类扩展一个test方法
*/
fun ExtensionTest.test() = println("父类扩展方法")
/**
* 子类扩展一个test方法
*/
fun ExtensionSubTest.test() = println("子类扩展方法")
fun main() {
val father : ExtensionTest = ExtensionTest()
father.test()//调用父类扩展方法
// 注意类型为 ExtensionTest
val child1 : ExtensionTest = ExtensionSubTest()
child1.test()//引用类型为父类类型,编译时静态调用的还是父类的扩展方法
// 注意类型为 ExtensionSubTest
val child2 : ExtensionSubTest = ExtensionSubTest()
child2.test()//此时才是调用子类的扩展方法
}
输出:
父类扩展方法
父类扩展方法
子类扩展方法
复制代码
可空类型扩展方法
在扩展函数内, 可以通过 this 来判断接收者是否为 NULL,这样,即使接收者为 NULL,也可以调用扩展函数。
fun Any?.toString(): String {
if (this == null) return "null"
// 空检测之后,“this”会自动转换为非空类型,所以下面的 toString()
// 解析为 Any 类的成员函数
return toString()
}
fun main(arg:Array<String>){
var t = null
println(t.toString())
}
输出:
null
复制代码
以类成员方式定义扩展方法
在某个类里面为其他类定义扩展方法、属性,该扩展的方法,只能在该类中通过被扩展的类的对象调用扩展方法。
以类成员方式定义的扩展,属于被扩展的类,因此在扩展方法直接调用被扩展的类的成员(this可以省略),同时因为它位于所在类中,因此又可以直接调用所在类的成员。
/**
* 定义一个类包含test方法
*/
class ExtensionTest {
fun test() = println("ExtensionTest的test方法")
}
/**
* 定义一个类包含test方法,包含ExtensionTest的一个扩展方法
*/
class ExtensionTest2 {
val a = "a"
fun test() = println("ExtensionTest2的test方法")
fun ExtensionTest.func() {
println(a)//调用扩展类的成员
test()//调用被扩展类的成员,相当于this.test()
this@ExtensionTest2.test()//同名的需要用this@类名的方式来调用
}
fun info(extensionTest: ExtensionTest) {
extensionTest.func()
}
}
fun main() {
val extensionTest = ExtensionTest()
val extensionTest2 = ExtensionTest2()
extensionTest2.info(extensionTest)
}
输出:
a
ExtensionTest的test方法
ExtensionTest2的test方法
复制代码
带接收者的匿名扩展函数
- 扩展方法(fun 类名.方法名())去掉方法名就是所谓的带接收者的匿名扩展函数,接收者就是类本身,形如:fun Int.() : Int。
/**
* 定义一个空类
*/
class ExtensionTest
/**
* 为空类定义一个带接收者的匿名扩展函数
*/
var noNameExtensionFun = fun ExtensionTest.(param: String): String {
println(param)
return "我是来自带接收者的匿名扩展函数的返回值"
}
fun main() {
val extensionTest = ExtensionTest()
println(extensionTest.noNameExtensionFun("向带接收者的匿名函数传入的参数"))//使用匿名扩展函数
}
输出:
向带接收者的匿名函数传入的参数
我是来自带接收者的匿名扩展函数的返回值
复制代码
与普通函数一样,匿名扩展方法也有函数类型,上述例子中,函数类型为:ExtensionTest.(String) -> String
拓展属性
- kotlin允许动态为类扩展属性,扩展属性是通过添加get、set方法实现,没有幕后字段(不能直接赋值)。
扩展属性也没有真的为该类添加了属性,只能说是为该类通过get、set方法计算出属性。
拓展属性的限制:
1、扩展属性不能有初始值;
2、扩展属性不能用filed关键字访问幕后字段;
3、val必须提供get方法,var必须提供get和set方法。
来份代码
/**
* 定义一个类,包含属性param1、属性param2
*/
class ExtensionTest(var param1: String, var param2: String)
/**
* 为该类扩展属性extensionParam
*/
var ExtensionTest.extensionParam: String
set(value) {
param1 = "param1$value"
param1 = "param2$value"
}
get() = "$param1-$param2"
复制代码
匿名拓展和lambda
如果能根据上下文推断出接收者类型,则可以使用lambda表达式
/**
* 定义一个空类
*/
class ExtensionTest
/**
* 定义一个函数,形参为ExtensionTest.(String) -> String类型,相当于同时为ExtensionTest类扩展了一个匿名扩展函数
*/
fun test(fn: ExtensionTest.(String) -> String) {
val extensionTest = ExtensionTest()
println("${extensionTest.fn("匿名扩展函数传入形参")}")
}
fun main() {
test {
println(it)
// println("123")
"返回值(匿名扩展函数)"
}
}
输出:
匿名扩展函数传入形参
返回值(匿名扩展函数)
复制代码
六、空类型安全
先来个例子吧
// name为不可为空的变量, 不能赋值为null ,若有判断 if(name==null) 无意义,因为肯定不为null
var name : String = ""
// hobby 为可空变量 因为 String?
var hobby : String? = ""
复制代码
Kotlin 空类型分为:可空类型 和 非空类型;
可空类型
:使用?
操作符声明可空类型,值可以为null,避免抛出 NPE(NullPointerException);非空类型
:常规变量不能容纳 null
let操作符
如果要只对非空值执行某个操作
,安全调用操作符 ?
可以与 let
一起使用
例子:例如筛选出一个集合非空的元素
fun main() {
// 不用 let操作符 的常规写法
fun filterArr1(arr: Array<String?>):Unit{
for (item in arr){
if (item==null){
continue
}else{
println("非空元素为->"+item)
}
}
}
// ?.let 写法
// ?.let 出来的数据,都是非空的
fun filterArr2(arr:Array<String?>):Unit{
for (item in arr){
item?.let {
println("非空元素为->"+item)
}
}
}
var testArray = arrayOf("什么","是","快乐",null,"星球",null)
filterArr1(testArray)
println("==========")
filterArr2(testArray)
}
输出:
非空元素为->什么
非空元素为->是
非空元素为->快乐
非空元素为->星球
==========
非空元素为->什么
非空元素为->是
非空元素为->快乐
非空元素为->星球
复制代码
Evils操作符(?: !! as)
Evils操作符分为,?: !! as as?
?:
如果?:
左侧表达式非空,elvis 操作符就返回其左侧表达式,否则返回右侧表达式。!!
在一个可空变量使用的时候后面加上!! ,则当该变量为null的时候抛出空指针异常as和as?
分2种情况- 当使用as的时候若类型转换失败则抛出类型转换(ClassCastException)异常
- 当使用as?的时候若类型转换失败则返回null,不会抛出异常
所以!抛出异常的 !! as 尽量少用,用as? ?. ?: let来代替
来点例子吧
- 来个
?:
如果?:
左侧表达式非空,elvis 操作符就返回其左侧表达式,否则返回右侧表达式。
fun main() {
var str: String? = null
val len1 = str?.length ?: -1
println(len1) // 输出 -1
str = "hello"
val len2 = str?.length ?: -1
println(len2) // 输出 5
}
复制代码
- 来个
!!
在一个可空变量使用的时候后面加上!! ,则当该变量为null的时候抛出空指针异常 NPE
fun main() {
var s: String? = null
println(s!!.length) //运行时候回报错
}
复制代码
空指针
-
来个
as
和as?
- 当使用as的时候若类型转换失败则抛出类型转换(ClassCastException)异常
- 当使用as?的时候若类型转换失败则返回null,不会抛出异常
as
的例子
fun main() {
var name:String?= 12 as String
//运行时候回报错 ClassCastException
// java.lang.Integer cannot be cast to java.lang.String
println(name)
}
复制代码
as?
的例子
fun main() {
var name:String?= 12 as? String
println(name) // 输出 null
}
输出:
null
复制代码
七、智能类型转换
kotlin的只能类型转换,可以分为两个情况
- 自动推断类型并转换
- 空类型的安全转换
自动推断类型并转换
在 Kotlin 中,只要对类型进行了判断,就可以直接通过父类的对象去调用子类的函数了
open class Person{}
class Student: Person() {
fun study(){
println("学一学")
}
}
fun main() {
var person:Person = Student()
if(person is Student){
person.study()
}
}
输出:
学一学
复制代码
如果是java中想做这种操作,就显得麻烦很多。
需要这样:
public class Person{
}
public class Student extends Person{
public void study(){
System.out.println("我在学习一门新的语言 Kotlin !");
}
}
public static void main(String[] args){
Person person = new Student();
if(person instanceof Student){
((Student) person).study();
}
}
复制代码
如果不进行智能类型转换呢?
上述的例子中,如果我们不进行 类型判断,直接把父类强转为子类调用study,是会报错的。
fun main() {
val person = Person()
(person as Student).study()
}
复制代码
当然这也是有解决办法,那就是可空类型转换。
空类型的安全转换
空类型的安全转换,其实就是我们文章前面说到的 Evils 操作符里面的 as?
和 as
val person: Person = Person()
val student:Student? =person as? Student
println(student) // null
复制代码
八、类属性的延迟初始化
kotlin中声明属性的时候,是要求必须初始化的,否则就会编辑器就会报错。
class Cat() {
val name: String // 报错
var baby: Int // 报错
val hobby: String = "睡觉" // 不报错
}
复制代码
但是在开发中我们很多时候并不是立即初始化属性。
比如定义控件名称的时候,我们不会在定义时就fingViewById来初始化控件,而是在页面生命周期函数里面来初始化控件。kotlin为开发者们提供了延迟初始化的方案。
kotlin中延迟初始化,一共分别两种。
lateinit
by lazy
lateinit
在var使用
.
使用 lateinit 需要注意几个点
-
lateinit
只能用于var
(val不行),所以经常是lateinit var
连用 -
lateinit
只能
用来修饰类属性
,不能用来修饰局部变量 -
lateinit
不能修饰基本数据类型
,如果int(kotlin基本类型:Double、Int、Float、Long、Short、Byte) -
lateinit用于只能
生命周期
流程中进行获取或者初始化的变量,比如 Android 的onCreate()
-
lateinit var的作用就是让编译期在检查时不要因为属性变量未被初始化而报错。不是说真的可以为空。Kotlin相信当开发者显式使用lateinit var关键字的时候,他一定也会在后面某个合理的时机将该属性对象初始化的。
class Cat() {
// lateinit 延迟初始化
lateinit var catName: String // 报错
}
fun main() {
var cat = Cat()
//println(cat.catName) // 没初始化,直接这么用是有问题的
// 报错: lateinit property catName has not been initialized
// 初始化了,调用就没问题了
cat.catName = "蓝猫"
println(cat.catName)
}
输出:
蓝猫
复制代码
lateinit在Android中使用
private lateinit var s: String
private val ss:String by lazy { "132" }
fun main() {
print("懒加载 ss = $ss")
try {
print("没有初始化 是s = $s \n") // 必须要初始化之后才能使用
}catch (e:Exception){
e.printStackTrace()
}
s = "123456"
print("初始化之后的 s = $s")
}
复制代码
再来一个
class MainActivity : AppCompatActivity() {
private lateinit var bt: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bt = findViewById(R.id.bt)
bt.setOnClickListener {
Toast.makeText(baseContext, "click", Toast.LENGTH_SHORT).show()
}
}
}
复制代码
.
.
.
by lazy 在val使用
-
by lazy
本身是一种属性委托。属性委托的关键字是by
-
lazy
只能
修饰val
,即不可变变量 -
应用于单例模式(if-null-then-init-else-return),而且
当且仅当变量被第一次调用
的时候,委托方法才会执行
。 -
by lazy
可以使用于类属性
或者局部变量
。
来个例子
class Cat() {
// by lazy 当且仅当变量被第一次调用的时候,委托方法才会执行。
val name by lazy {
println("只调用一次")
"大脸猫"
}
}
fun main() {
val lazyCat = Cat()
// lazyCat.name的时候,委托方法执行
val name = lazyCat.name
// lazyCat1.name执行的时候,委托方法上面执行过了,不再执行了
val name1 = lazyCat.name
println("====")
println("lazy-name:$name")
println("lazy-name:$name1")
}
输出:
只调用一次
====
lazy-name:大脸猫
lazy-name:大脸猫
复制代码
在 Android 中使用
class MainActivity : AppCompatActivity() {
private val bt by lazy {
findViewById<Button>(R.id.bt)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bt.setOnClickListener {
Toast.makeText(baseContext, "click", Toast.LENGTH_SHORT).show()
}
}
}
复制代码
# lazy 的3种延迟模式
- LazyThreadSafetyMode.
SYNCHRONIZED
(默认模式) - LazyThreadSafetyMode.
PUBLICATION
(可多次 Initializer) - LazyThreadSafetyMode.
NONE
(没有锁用于同步,多个实例)
在使用 lazy 延迟初始化的时候,Kotlin提供了3中模式,源码如下:
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
private val sss:String by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { "最后一个函数 可以放在外面 " }
复制代码
.
当我们模式都不用的情况下,默认使用 LazyThreadSafetyMode.SYNCHRONIZED 线程安全模式。源码如下:
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
复制代码
by lazy实现一个单例
双重校验锁式模式的单例
//Java实现
public class SingletonDemo {
private volatile static SingletonDemo instance;
private SingletonDemo(){}
public static SingletonDemo getInstance(){
if(instance==null){
synchronized (SingletonDemo.class){
if(instance==null){
instance=new SingletonDemo();
}
}
}
return instance;
}
}
//kotlin实现
class SingletonDemo private constructor() {
companion object {
val instance: SingletonDemo by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
SingletonDemo() }
}
}
复制代码
这里面涉及到几个东西,一个是lambda,一个是高阶函数(将函数用作参数或返回值的函数),一个是属性委托。
九、代理Delegate
代理模式和委托模式类似,但是两者不能认为是同一种模式。
- 委托模式 Delegation pattern
- 代理模式 Proxy Pattern
Kotlin 使用 by 关键字来实现委托(delegat) 模式
委托模式 是软件设计模式中的一项基本技巧,在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理
关于代理和委托,可以参考这个文章
[Kotlin | 委托(Delegation)详解](juejin.cn/post/700932…
十、单例object
kt的单例的多种实现方式
- 饿汉式单例
- 饿汉式单例
- 线程安全的懒汉式
- 双重校验锁式(Double Check)
- 静态内部类式
饿汉式单例
Kotlin引入了一个叫做object的类型,用来很容易的实现单例模式。
// 超简易的单例
object SimpleSington {
fun test() {}
}
//在Kotlin里调用
SimpleSington.test()
复制代码
是的你没看错,一行object SimpleSington{}
实现单例
这是这是kt的一个语法糖。
其内部真正的实现是这样的
public final class SimpleSington {
public static final SimpleSington INSTANCE;
private SimpleSington() {
INSTANCE = (SimpleSington)this;
}
static {
new SimpleSington();
}
}
复制代码
因而Kotlin这个超简版单例实现省去了,使用如下
- 显式声明静态instance变量
- 将构造函数private化处理
但是在Java和Kotlin混编时,Java代码中调用则需要注意,使用如下
SimpleSington.INSTANCE.test();
复制代码
饿汉式单例
针对饿汉式的潜在问题,我们可以使用懒汉式来解决,即将实例初始化放在开始使用之前。Kotlin版的懒汉式加载代码如下
class LazySingleton private constructor(){
companion object {
val instance: LazySingleton by lazy { LazySingleton() }
}
}
复制代码
- 显式声明构造方法为private
- companion object用来在class内部声明一个对象
- LazySingleton的实例instance 通过lazy来实现懒汉式加载
- lazy默认情况下是线程安全的,这就可以避免多个线程同时访问生成多个实例的问题
关于懒汉的选择
关于如何选择饿汉式还是懒汉式,通常应该从两方面考虑
- 实例初始化的性能和资源占用
- 编写的效率和简洁
对于实例初始化花费时间较少,并且内存占用较低的话,应该使用object形式的饿汉式加载。否则使用懒汉式。
线程安全的懒汉式
大家都知道在使用懒汉式会出现线程安全的问题,需要使用使用同步锁,在Kotlin中,如果你需要将方法声明为同步,需要添加 @Synchronized注解。
//Java实现
public class SingletonDemo {
private static SingletonDemo instance;
private SingletonDemo(){}
public static synchronized SingletonDemo getInstance(){//使用同步锁
if(instance==null){
instance=new SingletonDemo();
}
return instance;
}
}
//Kotlin实现
class SingletonDemo private constructor() {
companion object {
private var instance: SingletonDemo? = null
get() {
if (field == null) {
field = SingletonDemo()
}
return field
}
@Synchronized
fun get(): SingletonDemo{
return instance!!
}
}
}
复制代码
单例 双重校验锁式(Double Check)
//Java实现
public class SingletonDemo {
private volatile static SingletonDemo instance;
private SingletonDemo(){}
public static SingletonDemo getInstance(){
if(instance==null){
synchronized (SingletonDemo.class){
if(instance==null){
instance=new SingletonDemo();
}
}
}
return instance;
}
}
//kotlin实现
class SingletonDemo private constructor() {
companion object {
val instance: SingletonDemo by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
SingletonDemo() }
}
}
复制代码
.
.
如何在Kotlin版的Double Check,给单例添加一个属性?
class SingletonDemo private constructor(private val property: Int) {//这里可以根据实际需求发生改变
companion object {
@Volatile private var instance: SingletonDemo? = null
fun getInstance(property: Int) =
instance ?: synchronized(this) {
instance ?: SingletonDemo(property).also { instance = it }
}
}
}
复制代码
静态内部类式
//Java实现
public class SingletonDemo {
private static class SingletonHolder{
private static SingletonDemo instance=new SingletonDemo();
}
private SingletonDemo(){
System.out.println("Singleton has loaded");
}
public static SingletonDemo getInstance(){
return SingletonHolder.instance;
}
}
//kotlin实现
class SingletonDemo private constructor() {
companion object {
val instance = SingletonHolder.holder
}
private object SingletonHolder {
val holder= SingletonDemo()
}
}
复制代码
十一、内部类
内部类使用 inner 关键字来表示。
内部类会带有一个对外部类的对象的引用,所以内部类可以访问外部类成员属性和成员函数。
class Outer {
private val bar: Int = 1
var v = "成员属性"
/**嵌套内部类**/
inner class Inner {
fun foo() = bar // 访问外部类成员
fun innerTest() {
var o = this@Outer //获取外部类的成员变量
println("内部类可以引用外部类的成员,例如:" + o.v)
}
}
}
fun main(args: Array<String>) {
val demo = Outer().Inner().foo()
println(demo) // 1
val demo2 = Outer().Inner().innerTest()
println(demo2) // 内部类可以引用外部类的成员,例如:成员属性
}
复制代码
匿名内部类
对象表达式的格式: Object[: 若干个父类型,中间用逗号分隔]
使用对象表达式来创建匿名内部类:
class Test {
var v = "成员属性"
fun setInterFace(test: TestInterFace) {
test.test()
}
}
/**
* 定义接口
*/
interface TestInterFace {
fun test()
}
fun main(args: Array<String>) {
var test = Test()
/**
* 采用对象表达式来创建接口对象,即匿名内部类的实例。
*/
test.setInterFace(object : TestInterFace {
override fun test() {
println("对象表达式创建匿名内部类的实例")
}
})
}
复制代码
嵌套类
嵌套类、内部类 两者 不是 同一个东西
我们可以把类嵌套在其他类中
class Outer { // 外部类
private val bar: Int = 1
class Nested { // 嵌套类
fun foo() = 2
}
}
fun main(args: Array<String>) {
val demo = Outer.Nested().foo() // 调用格式:外部类.嵌套类.嵌套类方法/属性
println(demo) // == 2
}
复制代码
十二、数据类data class
Kotlin 可以创建一个只包含数据的类,关键字为 data:
例子:
data class User(val name: String, val age: Int)
复制代码
数据类需要满足以下条件:
- 主构造函数至少包含一个参数。
- 所有的主构造函数的参数必须标识为
val
或者var
; - 数据类不可以声明为
abstract
,open
,sealed
或者inner
; - 数据类不能继承其他类 (但是可以实现接口)。
copy()
复制使用
copy()
函数,我们可以使用该函数复制对象并修改部分属性
使用 copy 类复制 User 数据类,并修改 age 属性:
data class User(val name: String, val age: Int)
fun main(args: Array<String>) {
val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)
println(jack)
println(olderJack)
}
输出:
User(name=Jack, age=1)
User(name=Jack, age=2)
复制代码
数据类以及解构声明
组件函数允许数据类在解构声明中使用:
val jane = User("Jane", 35)
val (name, age) = jane // 组件函数允许数据类在解构声明中使用
println("$name, $age years of age") // prints "Jane, 35 years of age"
复制代码
标准数据类
标准库提供了 Pair 和 Triple 。在大多数情形中,命名数据类
是更好的设计
选择,因为这样代码可读性更强而且提供了有意义的名字和属性。
十三、枚举类enum class
-
枚举类最基本的用法是实现一个类型安全的枚举。
-
枚举常量用逗号分隔,每个枚举常量都是一个对象。
enum class Color{
RED,BLACK,BLUE,GREEN,WHITE
}
复制代码
枚举初始化
每一个枚举都是枚举类的实例,它们可以被初始化:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
复制代码
.
默认名称为枚举字符名,值从0开始。若需要指定值,则可以使用其构造函数:
enum class Shape(value:Int){
ovel(100),
rectangle(200)
}
复制代码
.
枚举还支持以声明自己的匿名类及相应的方法、以及覆盖基类的方法。
enum class ProtocolState {
WAITING {
override fun signal() = TALKING
},
TALKING {
override fun signal() = WAITING
};
abstract fun signal(): ProtocolState
}
复制代码
如果枚举类定义任何成员,要使用分号将成员定义中的枚举常量定义分隔开
.
使用枚举常量
Kotlin 中的枚举类具有合成方法,允许遍历定义的枚举常量,并通过其名称获取枚举常数。
先来看看 valueOf
和 values
这两个方法
EnumClass.valueOf(value: String): EnumClass // 转换指定 name 为枚举值,若未匹配成功,会抛出IllegalArgumentException
EnumClass.values(): Array<EnumClass> // 以数组的形式,返回枚举值
复制代码
获取枚举相关信息:
val name: String //获取枚举名称
val ordinal: Int //获取枚举值在所有枚举数组中定义的顺序
复制代码
看个例子,你就明明白白
enum class Color{
RED,BLACK,BLUE,GREEN,WHITE
}
fun main() {
var color:Color=Color.BLUE
println(Color.values().joinToString ())
println(Color.valueOf("RED"))
println(color.name)
println(color.ordinal)
}
输出:
RED, BLACK, BLUE, GREEN, WHITE
RED
BLUE
2
复制代码
十四、密封类sealed class
特点
-
密封类用来表示受限的类继承结构
:当一个值为有限几种的类型, 而不能有任何其他类型时。
-
声明一个
密封类
,使用sealed
修饰类,密封类可以有子类
,但是所有的子类都必须要内嵌在密封类中
。- 在某种意义上,他们是枚举类的扩展。枚举类型的值集合 也是受限的,但
每个枚举常量只存在一个实例
,而密封类 的一个子类可以有可包含状态的多个实例
。
- 在某种意义上,他们是枚举类的扩展。枚举类型的值集合 也是受限的,但
-
sealed
不能修饰
interface
,abstract
(会报 warning,但是不会出现编译错误)
某种程度上,密封类是一个强化的枚举类,
枚举更在意数据
,Sealed 更在意类型
来点例子
例子1
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
fun eval(expr: Expr): Double = when (expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
}
复制代码
使用密封类的关键好处在于使用 when 表达式 的时候,如果能够 验证语句覆盖了所有情况,就不需要为该语句再添加一个 else 子句了。
fun eval(expr: Expr): Double = when(expr) {
is Expr.Const -> expr.number
is Expr.Sum -> eval(expr.e1) + eval(expr.e2)
Expr.NotANumber -> Double.NaN
// 不再需要 `else` 子句,因为我们已经覆盖了所有的情况
}
复制代码
.
.
例子2
// 外部无法实例化密封的类
// 外部只能实例化他的子类
sealed class Color {
// 只能在内部继承密封类
class Red(val value: Int) : Color()
class Green(val value: Int) : Color()
class Blue(val name: String) : Color()
}
fun isInstance(color: Color) {
when (color) {
// 必须写全所有条件,否则报错
is Color.Red -> println("这是红色")
is Color.Green -> println("这是绿色")
is Color.Blue -> println("这是蓝色")
}
}
fun main() {
isInstance(Color.Red(123))
}
输出:
这是红色
复制代码
十五、内联类inline class
-
Kotlin 引⼊了⼀种被称为内联类的特殊类,它通过在类的前⾯定义⼀个 inline 修饰符来声明。
-
内联类必须含有唯⼀的⼀个属性在主构造函数中初始化。在运行时,将使用这个唯⼀属性来表示内联类的实例。这就是内联类的主要特性,类的数据被 “内联”到该类使用的地方。
-
内联类的唯一作用是成为某种类型的包装(类似于Java的装箱类型 Integer、Double)
内联类的注意点
- 最多一个参数 (类型不受限制)(内联类必须含有唯一的一个属性在主构造函数中)
- 不能有幕后字段(不能直接赋值)
- 被包装类不能是泛型
- 不能有 init 块
- 不能继承其他类,也不能被继承
- 从接口继承,具有属性和方法
例子
inline class BoxInt(val value: Int): Comparable<Int> {
override fun compareTo(other: Int)
= value.compareTo(other)
operator fun inc(): BoxInt {
return BoxInt(value + 1)
}
}
复制代码
Typealias
看起来与内联类相似,但是类型别名只是为现有类型提供了可选名称,而内联类则创建了新类型。
参考:
www.cnblogs.com/nicolas2019…
www.jianshu.com/p/ff6f5101e…
www.jianshu.com/p/c9bd56a64…
droidyue.com/blog/2017/0…
www.jianshu.com/p/5797b3d0e…