Kotlin的延迟初始化
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin的延迟初始化相关的知识,希望对你有一定的参考价值。
参考技术AKotlin中有两种延迟初始化的方式。一种 lateinit var ,一种 by lazy
在AS中Tools—>Kotlin—>show Kotlin Bytecode—>Decompile反编译Kotlin字节码得到
可以看到:我们在引用变量的时候会进行判断,如果为空(也就是未初始化),就会抛出异常。
by是属性委托关键字
注释:使用指定的初始化函数[initializer]来创建一个[Lazy]类型的对象,默认的线程模型是[LazyThreadSafetyMode.SYNCHRONIZED]。默认返回了SynchronizedLazyImpl对象:
我们反编译Kotlin字节码得到
可以看到,生成了一个getter方法,return一个(String)var1.getValue(),也就是获取str会通过getValue。而SynchronizedLazyImpl实现了Lazy的value方法, 调用getValue方法返回_value, _value默认值是UNINITIALIZED_VALUE,表示为初始化,就会调用传入的initializer进行初始化,并把值赋给 _value,以后每次调用直接返回。
lateinit var和by lazy都可以推迟初始化,lateinit var只是在编译期忽略对属性未初始化进行检查,何时初始化还需开发者自行决定。而by lazy在被第一次调用的时候能自动初始化,做到了真正的延迟初始化的行为。
Kotlin学习总结——变量常量数据类型条件语句
Kotlin学习总结(一)——变量、常量、数据类型、条件语句
参考博客:Kotlin教程——史上最全面、最详细的学习教程,持续更新中…
一、变量、常量和注解
1.1 变量
kotlin
变量的声明方式与Java
中声明变量有很大的区别,而且必须使用var
或val
关键字。其中:
var
: 用此关键字声明的变量表示可变变量,即可读且可写。相当于Java
中普通变量val
: 用此关键字声明的变量表示不可变变量,即可读且不可写。相当于Java
中用final
修饰的变量(不是常量)
1.1.1 基础用法
类型推断:对于已经生命并赋值的变量,允许你省略类型定义
//立即初始化
var var_a: Int = 10
//推导出类型
var var_b = 5
//没有初始化的时候,必须声明类型
var var_c: Float
var_c = 12.3f
var_c += 1
println("var_a => $var_a \\t var_b => $var_b \\t var_a => $var_c")
//立即初始化
val val_a: Int = 100
//推导出类型
val val_b = 50
//没有初始化的时候,必须声明类型
val val_c: Int
val_c = 1
// val_c += 1 因为c是常量,所以这句代码是会报错的
println("val_a => $val_a \\t val_b => $val_b \\t val_c => $val_c")
打印结果为:
var_a => 10 var_b => 5 var_a => 13.3
val_a => 100 val_b => 50 val_c => 1
- 其中。
var
和val
是Kotlin
中定义变量必须使用的关键字。- 每一行代码的结束可以省略掉分号
;
,这一点是和Java
不同的地方。当然,第一次写可能会有一点不习惯。print()
与println()
都是打印方法,后者打印完成之后会换一行。此两个方法和Java
的打印方法是一模一样的。$
符号表示引用的意思。这里理解为字符串模板,
1.1.2 在类中声明以及声明可空变量
1.1.2.1 类中声明变量
- 只有在顶层声明的情况下是可以不用实例化的。
- 而在一个类中去定义变量,这种情况被称为声明类的属性。
- 其特点如下:必须初始化,如果不初始化,需使用
lateinit
关键字。
例:
class Test1{
// 定义属性
var var_a : Int = 0
val val_a : Int = 0
// 初始化
init {
var_a = 10
// val_a = 0 为val类型不能更改。
println("var_a => $var_a \\t val_a => $val_a")
}
}
Test1()
输出结果为:
var_a => 10 val_a => 0
1.1.2.2 声明可空变量
在Kotlin
中当我们可以确定这个属性或变量一定不为空时,就用上面的方法定义变量。否则就把它声明为可空变量。
可空变量的特点:
- 在声明的时候一定用标准的声明格式定义。不能用可推断类型的简写。
- 变量类型后面的
?
符号不能省略。不然就和普通的变量没区别了。- 其初始化的值可以为
null
或确定的变量值。
定义:
var/val 变量名 : 类型? = null/确定的值
例:
class Test2{
// 声明可空变量
var var_a : Int? = 0
val val_a : Int? = null
init {
var_a = 10
// val_a = 0 为val类型不能更改。
println("var_a => $var_a \\t val_a => $val_a")
}
}
Test2()
输出结果为:
var_a => 10 val_a => null
1.1.3 后期初始化与延迟初始化
当在类中定义一个变量(属性)的时候是必须初始化的。这在平时的实际开发中能满足大部分的需求。但是还是有一些特殊的场景中不能满足。比如说:Android
开发中对组件变量的声明与赋值,以及在使用Dagger2
注解变量等。这就需要Kotlin
中特有的后期初始化属性来满足这个需求了。当然这里还为大家讲解延迟初始化,在实际的开发中也是很有用处的。
1.1.3.1 后期初始化属性
声明后期初始化属性的特点:
- 使用
lateinit
关键字- 必须是可读且可写的变量,即用
var
声明的变量- 不能声明于可空变量。
- 不能声明于基本数据类型变量。例:
Int
、Float
、Double
等,注意:String
类型是可以的。- 声明后,在使用该变量前必须赋值,不然会抛出
UninitializedPropertyAccessException
异常。
代码示例:
// 声明组件
private lateinit var mTabLayout : TabLayout
lateinit var a : Int // 会报错。因为不能用于基本数据类型。
// 赋值
mTabLayout = find(R.id.home_tab_layout)
// 使用
mTabLayout.setupWithViewPager(mViewPager)
1.1.3.2 延迟初始化属性
所谓延迟初始化即:指当程序在第一次使用到这个变量(属性)的时候在初始化。
声明延迟初始化属性的特点:
- 使用
lazy{}
高阶函数,不能用于类型推断。且该函数在变量的数据类型后面,用by
链接。- 必须是只读变量,即用
val
声明的变量。
实例讲解:同样是Android
中常见的例子
// 声明一个延迟初始化的字符串数组变量
private val mTitles : Array<String> by lazy {
arrayOf(
ctx.getString(R.string.tab_title_android),
ctx.getString(R.string.tab_title_ios),
ctx.getString(R.string.tab_title_h5)
)
}
// 声明一个延迟初始化的字符串
private val mStr : String by lazy{
"我是延迟初始化字符串变量"
}
1.2 Kotlin之常量的用法
Kotlin
中声明常量的方式和在Java
中声明常量的方式有很大的区别。
Kotlin
中使用val
时候对应的Java
代码:
val numA = 6 //Kotlin中的
等价于
public final int numA = 6//Java中的
很显然,Kotlin
中只用val
修饰还不是常量,它只能是一个不能修改的变量。
那么常量怎么定义呢?其实很简单,在val
关键字前面加上const
关键字。
即:
const val NUM_A = 6
其特点:const
只能修饰val
,不能修饰var
声明常量的三种正确方式
- 在顶层声明
- 在
object
修饰的类中声明,在kotlin
中称为对象声明,它相当于Java
中一种形式的单例类- 在伴生对象中声明
举例说明:
// 1. 顶层声明
const val NUM_A : String = "顶层声明"
// 2. 在object修饰的类中
object TestConst{
const val NUM_B = "object修饰的类中"
}
// 3. 伴生对象中
class TestClass{
companion object {
const val NUM_C = "伴生对象中声明"
}
}
fun main(args: Array<String>) {
println("NUM_A => $NUM_A")
println("NUM_B => ${TestConst.NUM_B}")
println("NUM_C => ${TestClass.NUM_C}")
}
输出结果为:
NUM_A => 顶层声明
NUM_B => object修饰的类中
NUM_C => 伴生对象中声明
1.3 Kotlin之注释
Kotlin
中的注释几乎和Java
没什么区别。唯一的区别在于Kotlin
中的多行注释中可以嵌套多行注释,而Java
中是不能的。
- 多行注释嵌套
举例:
/*
第一层块注释
/*
第二层块注释
/*
第三层快注释
这种注释方式在java中是不支持的,但是在kotlin中是支持的。算是一个亮点吧(貌似意义不大)。
*/
*/
*/
二、数据类型
2.1 数值类型
2.1.1 关键字
Kotlin
中的数字的内置类型(接近与Java
),其关键字为:
Byte
=> 字节 => 8位Short
=> 短整型 => 16位Int
=> 整型 => 32位Long
=> 长整型 => 64位Float
=> 浮点型 => 32位Double
=> 双精度浮点型 => 64位
例:
var a: Byte = 2
var b: Short = 2
var c: Int = 2
var d: Long = 2L //长整型由大写字母L标记
var e: Float = 2f //单精度浮点型由小写字母f或大写字符F标记
var f: Double = 2.0
println(" a => $a \\n b => $b \\n c => $c \\n d => $d \\n e => $e \\n f => $f);
输出结果为:
a => 2
b => 2
c => 2
d => 2
e => 2.0
f => 2.0
2.1.2 进制数
- 二进制数
- 八进制数(Kotlin不支持)
- 十进制数
- 十六进制数
例:
var g = 0x0F //十六进制数
var h = 0b00001011 //二进制数
var k = 123 //十进制数
// ps:Kotlin不支持八进制数
println(" g => $g \\n h => $h \\n k => $k);
输出结果为:
g => 15
h => 11
k => 123
2.1.3 数字类型字面常量的下划线
作用:分割数字进行分组,使数字常量更易读
例:
val oneMillion = 1_000_000
val creditCardNumber = 1234_5678_9012_3456L
val socialSecurityNumber = 999_99_9999L
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_10010010
println("oneMillion => $oneMillion")
println("creditCardNumber => $creditCardNumber")
println("socialSecurityNumber => $socialSecurityNumber")
println("hexBytes => $hexBytes")
println("bytes => $bytes")
输出结果为:
oneMillion => 1000000
creditCardNumber => 1234567890123456
socialSecurityNumber => 999999999
hexBytes => 4293713502
bytes => 3530134674
2.1.4 装箱与拆箱
在
Kotlin
中,存在数字的装箱,但是不存在拆箱。因为Kotlin
是没有基本数据类型的,Kotlin
是万般皆对象的原则。
在Kotlin
中要实现装箱操作。首先要了解可空引用。即类似Int?
(只限数值类型)这样的。
例:
val numValue: Int = 123
//装箱的过程,其实装箱之后其值是没有变化的
val numValueBox: Int? = numValue
println("装箱后: numValueBox => $numValueBox")
输出结果为:
装箱后: numValueBox => 123
2.1.5 == 与 ===
判断两个数值是否相等(
==
),判断两个数值在内存中的地址是否相等(===
),其实上面的装箱操作之后其内存中的地址根据其数据类型的数值范围而定。
例:
val numValue: Int = 128
val numValueBox: Int? = numValue
/*
比较两个数字
*/
var result: Boolean
result = numValue == numValueBox
println("numValue == numValueBox => $result") // => true,其值是相等的
result = numValue === numValueBox
/*
上面定义的变量是Int类型,大于127其内存地址不同,反之则相同。
这是`kotlin`的缓存策略导致的,而缓存的范围是` -128 ~ 127 `。
故,下面的打印为false
*/
println("numValue === numValueBox => $result")
输出结果为:
numValue == numValueBox => true
numValue === numValueBox => false
2.1.6 转换
- 显式转换
较小的类型不会被隐式转换为更大的类型,故而系统提供了显式转换。提供的显式转换方法如下:
toByte()
=> 转换为字节型toShort()
=> 转换为短整型toInt()
=> 转换为整型toLong()
=> 转换为长整型toFloat()
=> 转换为浮点型toDouble()
=> 转换为双精度浮点型toChar()
=> 转换为字符型toString()
=> 转换为字符串型
例:
var numA: Int = 97
println(numA.toByte())
println(numA.toShort())
println(numA.toInt())
println(numA.toLong())
println(numA.toFloat())
println(numA.toDouble())
println(numA.toChar())
println(numA.toString())
输出结果为:
97
97
97
97.0
97.0
97
a
97
- 隐式转换
类型是从上下文推断出来的,即算术运算则被重载为适当的转换
例:
// 30L + 12 -> Long + Int => Long
val num = 30L + 12
print(num)
输出结果为:
42
2.2.7 位运算符
Kotlin中对于按位操作,和Java是有很大的差别的。Kotlin中没有特殊的字符,但是只能命名为可以以中缀形式调用的函数,下列是按位操作的完整列表(仅适用于整形(Int)和长整形(Long):
shl(bits)
=> 有符号向左移 (类似Java
的<<
)shr(bits)
=> 有符号向右移 (类似Java
的>>
)ushr(bits)
=> 无符号向右移 (类似Java
的>>>
)and(bits)
=> 位运算符and
(同Java
中的按位与)or(bits)
=> 位运算符or
(同Java
中的按位或)xor(bits)
=> 位运算符xor
(同Java
中的按位异或)inv()
=> 位运算符 按位取反 (同Java
中的按位取反)
例:
/*
位运算符
支持序列如下:shl、shr、ushr、and、or、xor
*/
var operaNum: Int = 4
var shlOperaNum = operaNum shl(2)
var shrOperaNum = operaNum shr(2)
var ushrOperaNum = operaNum ushr(2)
var andOperaNum = operaNum and(2)
var orOperaNum = operaNum or(2)
var xorOperaNum = operaNum xor(2)
var invOperaNum = operaNum.inv()
println("shlOperaNum => $shlOperaNum \\n " +
"shrOperaNum => $shrOperaNum \\n " +
"ushrOperaNum => $ushrOperaNum \\n " +
"andOperaNum => $andOperaNum \\n " +
"orOperaNum => $orOperaNum \\n " +
"xorOperaNum => $xorOperaNum \\n " +
"invOperaNum => $invOperaNum")
输出结果为:
shlOperaNum => 16
shrOperaNum => 1
ushrOperaNum => 1
andOperaNum => 0
orOperaNum => 6
xorOperaNum => 6
invOperaNum => -5
2.2 布尔类型(Boolean)
Boolean
关键字表示布尔类型,并且其值有true
和false
例:
var isNum: Boolean
isNum = false
println("isNum => $isNum")
输出结果为:
isNum => false
2.3 字符型(Char)
1、关键字
Char
为表示字符型,字符变量用单引号(‘ ’)表示。并且不能直接视为数字,不过可以显式转换为数字。
例:
var char1: Char
char = 'a'
//char1 = 1 => 这句代码会直接出错
println("char1 => $char1")
输出结果为:
char1 => a
2、显示转换为其他类型
字符型的变量不仅可以转换为数字,同时也可转换为其他类型
例:
var var1 = char1.toByte()
var var2 = char1.toInt()
var var3 = char1.toString()
var var4 = char1.toFloat()
var var5 = char1.toShort()
println("var1 => $var1 \\n var2 => $var2 \\n var3 => $var3 \\n var4 => $var4 \\n var5 => $var5")
输出结果为:
var1 => 97
var2 => 97
var3 => a
var4 => 97.0
var5 => 97
PS:除了可以转换类型外,当变量为英文字母时还支持大小写转换。
例:
/*
当字符变量为英文字母时,大小写的转换
*/
var charA: Char = 'a'
var charB: Char = 'B'
var charNum: Char = '1'
var result: Char
// 转换为大写
result = charA.toUpperCase()
println("result => $result")
// 转换为小写
result = charB.toLowerCase()
println("result => $result")
//当字符变量不为英文字母时,转换无效
result = charNum.toLowerCase()
println("result => $result")
输出结果为:
result => A
result => b
result => 1
3、字符转义
同Java
一样,使用某些特殊的字符时,要使用转义。下列是支持的转义序列:
\\t
=> 表示制表符\\n
=> 表示换行符\\b
=> 表示退格键(键盘上的Back建)\\r
=> 表示键盘上的Enter
键\\\\
=> 表示反斜杠\\'
=> 表示单引号\\"
=> 表示双引号\\$
=> 表示美元符号,如果不转义在kotlin
中就表示变量的引用了- 其他的任何字符请使用Unicode转义序列语法。例:’\\uFF00’
例:
println("\\n 换行符")
println("\\t 制表符")
println(" \\b 退格键")
println("\\r Enter键同样换行")
println('\\\\')
println('\\'')
println('\\"')
println('\\$')
println('\\uFF01')
输出结果为:
换行符
制表符
退格键
Enter键同样换行
\\
'
"
$
!
2.4 字符串类型(String)
1、关键字
String
表示字符串类型。其是不可变的。所以字符串的元素可以通过索引操作的字符:str[index]
来访问(字符类型)。可以使用for
循环迭代字符串:
其中str[index]
中的str
为要目标字符串,index
为索引
例:
val str: String = "kotlin"
println("str => $str")
//迭代
for (s in str){
var c : Char = s
print(c)
print("\\t")
}
输出结果为:
str => kotlin
k o t l i n
2、 字符串字面量
在
Kotlin
中, 字符串字面量有两种类型:
- 包含转义字符的字符串 转义包括(
\\t
、\\n
等),不包含转义字符串的也同属此类型- 包含任意字符的字符串 由三重引号(
""" .... """
)表示
例:
// 类型1:
var str1: String = "hello\\t\\tkotlin"
println(str1)
str1 = "hello kotlin"
println(str1)
// 类型2:
val str2 = """ fun main(args: Array<String>){
println("我是三重引号引用的字符串,我可以包含任意字符")
} """
println(str2)
输出结果为:
hello kotlin
hello kotlin
fun main(args: Array<String>){
println("我是三重引号引用的字符串,我可以包含任意字符")
}
PS: 可以使用
trimMargin()
函数删除前导空格 ,默认使用符号(|
)作为距前缀,当然也可以使用其他字符。例:右尖括号(>
)、左尖括号(<
)等。
例:
val str3: String = """
> I`m like Kotlin .
> I`m like Java .
> I`m like Android .
> I`m like React-Native.
""".trimMargin(">")
println(str3)
输出结果为:
I`m like Kotlin .
I`m like Java .
I`m like Android .
I`m like React-Native.
3、字符串模板
使用字符串模板的符号为(
$
)。在$
符号后面加上变量名或大括号中的表达式
例:
val text1: String = "我来了!"
var text2: String = "$text1 kotlin"
var text3: String = "$text2 ${text1.length} 哈哈!!!!"
println(text1)
println(text2)
println(text3)
输出结果为:
我来了!
我来了! kotlin
我来了! kotlin 4 哈哈!!!!
2.5 数组型(Array)
Kotlin
中数组由Array<T>
表示,可以去看看源码实现,里面就几个方法- 创建数组的3个函数
arrayOf()
arrayOfNulls()
- 工厂函数(
Array()
)
1、arrayOf()
创建一个数组,参数是一个可变参数的泛型对象
例:
var arr1 = arrayOf(1,2,3,4,5) //等价于[1,2,3,4,5]
for (v in arr1){
print(v)
print("\\t")
}
var arr2 = arrayOf("0","2","3",'a',32.3f)
for (v in arr2){
print(v)
print("\\t")
}
输出结果为:
1 2 3 4 5
0 2 3 a 32.3
2、arrayOfNulls()
用于创建一个指定数据类型且可以为空元素的给定元素个数的数组
例:
var arr3 = arrayOfNulls<Int>(3)
//如若不予数组赋值则arr3[0]、arr3[1]、arr3[2]皆为null
for(v in arr3){
print(v)
print("\\t")
}
println()
//为数组arr3赋值
arr3[0] = 10
arr3[1] = 20
arr3[2] = 30
for(v in arr3){
print(v)
print("\\t")
}
输出结果为:
null null null
10 20 30
3、工厂函数
- 使用一个工厂函数
Array()
,它使用数组大小和返回给定其索引的每个数组元素的初始值的函数。Array()
=> 第一个参数表示数组元素的个数,第二个参数则为使用其元素下标组成的表达式
例:
var arr4 = Array(5,{index -> (index * 2).toString() })
for (v in arr4){
print(v)
print("\\t")
}
输出结果为:
0 2 4 6 8
4、原始类型数组
Kotlin
还有专门的类来表示原始类型的数组,没有装箱开销,它们分别是:
ByteArray
=> 表示字节型数组ShortArray
=> 表示短整型数组IntArray
=> 表示整型数组LongArray
=> 表示长整型数组BooleanArray
=> 表示布尔型数组CharArray
=> 表示字符型数组FloatArray
=> 表示浮点型数组DoubleArray
=> 表示双精度浮点型数组- PS:
Kotlin
中不支持字符串类型这种原始类型数组,可以看源码Arrays.kt
这个类中并没有字符串数组的声明。而源码中以上是关于Kotlin的延迟初始化的主要内容,如果未能解决你的问题,请参考以下文章 Kotlin类的初始化 ④ ( lateinit 延迟初始化 | ::属性名称.isInitialized 检查属性是否初始化 | lazy 惰性初始化 )
Kotlin类的初始化 ④ ( lateinit 延迟初始化 | ::属性名称.isInitialized 检查属性是否初始化 | lazy 惰性初始化 )
错误记录Kotlin 延迟加载初始化报错 ( lateinit property string has not been initialized )