类 ★
属性★
属性 = 字段 + setter/getter
声明属性
class Address {
var name: String = "Holmes, Sherlock"
var street: String = "Baker"
var city: String = "London"
var state: String? = null
var zip: String = "123456"
}
var 和 val
var
定义一个可读可写的属性, val
定义一个只读的属性
this.name
就相当调用了 getName
函数, this.name = "嘿嘿"
, 就相当于调用了 setName("嘿嘿")
kotlin
中所有的字段都需要初始化, 不像 java
那样字段有默认值
-
var allByDefault: Int?
错误:需要显式初始化器,隐含默认getter
和 setter -
var initialized = 1
类型Int
、kotlin
生成getter 和 setter
-
val inferredType = 1
类型Int
、默认getter
-
val simple: Int?
错误: 需要在构造函数中初始化或者添加初始化值, 类型Int
、kotlin
生成getter
自定义访问器
给字段下面加上自定义的 get
和 set
class Address {
val name: String
get() {
println("zhazha")
return field
}
var age: Int = 0
get() = field
set(value) {
field = value
}
}
var setterVisibility: String = "abc"
private set // 此 setter 是私有的并且有默认实现
var setterWithAnnotation: Any? = null
@Inject set // ⽤ Inject 注解此 setter
幕后字段
无限递归问题:
class Teacher {
var name: String
get() = this.name
set(value) {
this.name = value
}
}
error: get() = this.name
和 this.name = value
get() = this.name
会调用 name
的 getter
函数, 然后又遇到 this.name
再次调用 getter
无限循环
同理: this.name = value
的 this.name = xxx
会调用 this.name
的 setter
函数, 接着无限递归
解决办法是: field
幕后字段
class Teacher {
var name: String = ""
get() = field
set(value) {
field = value
}
}
关于 延迟属性
的研究
属性未必已经需要主动初始化, 比如 spring bean 注入
就是, 但 kotlin
强制要求程序员初始化属性, 这导致很多二次赋值的问题, 虽然影响不大, 但对于有强迫症的人来说简直不共戴天
这里推荐几种方式进行属性延迟赋值
lateinit
延迟
非基本数据类型推荐使用
class ServiceImpl : Service {
@Resource
private lateinit var dao: DaoImpl
}
但这种方式只能用于 非基础数据类型
和 var
标记的属性
使用可空类型
如果该属性可能为 null, 推荐使用
class ServiceImpl : Service {
@Resource
private var dao: DaoImpl? = null
}
lazy
需要延迟调用的 val 推荐使用
class Bird(val weight: Double, val age: Int, val color: String) {
val sex: String by lazy {
if (color == "yellow") "male" else "female"
}
}
lazy 的原理
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
lazy 函数
, 该函数返回Lazy<T>
对象, lazy
还有一个 lambda表达式
的参数 该表达式 返回一个 T 类型
的对象
Lazy<T>
这个 T
就是 lambda 表达式
返回的
访问sex
会调用 getValue
返回这个 T
(仅在第一次调用时才会调用)
这是委托 by 的能力, 读取就固定调用
getValue
函数, 写入会固定调用setValue
函数
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
而这个 value
的 get
函数将会被执行
override val value: T
get() {
val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}
如果没有对 lazy
函数添加一个同步属性, 始终都会执行到
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
这段代码
这里的 initializer
就是下面的返回值 T
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N6Mp9dvc-1655378804210)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2b8e8c7d6ef54be5b058c611ac407141~tplv-k3u1fbpfcp-watermark.image?)]
然后返回 typedValue
也就是 lambda 的返回值
by lazy {}
方式需要注意
- 只能用于
val
修饰的变量, 不能通过var
来声明 - 只有在首次被调用时才会初始化
让基本数据类型也拥有延迟效果
基本数据类型, 推荐使用委托
使用委托的形式延迟初始化基本数据类型
by Delegates.notNull<T>()
class Bird(private val color: String) {
val sex: String by lazy {
if (color == "yellow") "male" else "female"
}
lateinit var name: String
var age: Int by Delegates.notNull<Int>()
}
在
kotlin
中, 不推荐java
的那种方式生成构造函数, 而是使用默认参数配合构造函数
幕后属性
下面这段代码实现了一个延迟属性功能
private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
get() {
if(_table == null) {
_table = HashMap()
}
return _table ?: throw AssertionError("Set to null by another thread")
}
个人认为 幕后属性 可以使用在 data class
中配合使用
构造函数 ★
★ 在kotlin
中构造函数有主构造函数
, 初始化代码块(init)
和次构造函数
主构造函数 ★
class Person constructor(val name: String) {}
换成 java
源码 类似于:
public final class Person {
private final String name;
public Person(String name) {
this.name = name;
}
public final String getName() {
return this.name;
}
// public final void setName(String name) {
// this.name = name;
// }
}
为什么需要主构造函数?
kotlin
设计主构造函数的可能是简化代码吧
class Foo {
val bar: Bar
constructor(barValue: Bar) {
bar = barValue
}
}
class Foo(val bar: Bar)
原型是这样: class Foo constructor(val bar: Bar)
只不过constructor
关键字在没有注解, 类似private
这样的访问修饰符可以省略
class Person private /* @Inject */ constructor(name: String) {
val name: String = name.uppercase()
}
主构造函数带来的问题
- 主函构造函数增加了新手入门的难度( 很奇葩的设计
- 添加了主构造函数, 还需要考虑构造函数的顺序 ( 奇葩
- 主构造函数内部不能有别的操作, 只有赋值操作, 如果还有别的操作还需要使用
init
代码块, 在init
代码块中初始化 (超级奇葩 - 如果类的属性增多, 你会发现一部分属性在主构造函数的小括号内, 一部分属性在类的作用域内, 阅读性变低 (更加奇葩
- 看截图
主构造函数必须最先执行, 次构造函数次之, 所以在主构造函数中无法初始化 age1
和 age2
无法被初始化, 所以报错
解决方法是:
- 把
age1
和age2
放入主构造函数的函数参数列表中
open class A(
var name: String
val age1: Int
val age2: Int
) {
constructor(_age: Int) : this("name", _age, _age) {
}
}
init
代码块
open class A(var name: String) {
val age1: Int
val age2: Int
init {
// 这里的 0 可以改成在主构造函数传入参数 比如: open class A(var name: String, _age: Int) , 这样次构造函数的 this("name") 就需要更改了 this("name", _age), 次构造函数后面的函数代码块就不需要了
this.age1 = 0
this.age2 = 0
}
constructor(_age: Int) : this("name") {
this.age1 = _age
this.age2 = _age
}
}
有新手会问为什么不用
lateinit
, 那是因为lateinit
只能用于var
且非基本类型(Int, Double这种基础类型)上
- 删掉主构造函数
写好字段, 使用快捷键, 就可以创建上面的次构造函数
事情解决了!!!
当然我们也可以灵活运用 主构造函数 + init
代码块 + 参数的默认值
让你选你会选哪一种???
我的结论: 怎么简单怎么来
主构造函数和初始化语句块(init
)
- 是什么?
class Person(_nickName: String) {
val nickName: String
// 这就是 init 初始化代码块
init {
this.nickName = "${_nickName.lenght} - ${_nickName}"
}
}
注意1,
constructor
修饰符省略掉了, 有前提:
- 没有注解.
- 没有可见修饰符.
注意2: 主构造函数上
Person(val nickName: String)
和Person(_nickName: String)
的区别在于 带val/var
的将变成nickName
属性, 不带的变成_nickName
构造函数参数
- 为什么需要初始化代码块?
kotlin
主构造函数除了 this.nickName = nickName
这些赋值操作外, 没有任何的操作, 所以需要初始化代码块进行其他操作 this.nickName = "${_nickName.lenght} - ${_nickName}"
, 所以初始化代码块诞生了
初始化代码块属于主构造函数体的代码之一, 和 主构造函数
和类作用域内属性
属于同一个作用域
将其换成 java
源码就知道了:
class Person {
private String nickName;
Person(String _nickName) {
// 主函数自己的代码
// balabala.....
// init 代码块内部的代码 or 主函数之外的字段初始化代码 (按照定义先后顺序)
this.nickName = "${_nickName.lenght} - ${_nickName} // 这行不是 java 代码
}
}
构造方法参数默认值
class Person(val nickName: String , var isSubscribed: Boolean = false)
★ 如果全部是 默认值 会生成一个无参数主构造函数
class Person(val nickName: String = "", var isSubscribed: Boolean = false)
子类初始化父类字段 ★
★ 子类有责任初始化 父类字段
open class User(val nickName: String) {}
class FacebookUser(nickName: String) : User(nickName) {}
这非常的重要, 子类有责任将值给父类初始化, 父类未初始化的属性必须要子类来初始化(想要子类对象的话)
继承和实现怎么看?
interface View {}
open class Button : View {}
open class RadioButton : Button() {}
引号继承和实现区别一目了然, 实现直接写上
View
, 继承则是调用 父类构造函数Button()
, 一个 没有()
一个有()
如果不声明任何构造函数, 它会生成一个无参数构造函数
open class Button
定义 private 构造函数
class Person private constructor(val nickName: String) {}
这种类可以使用伴生对象构建并使用, 伴生对象就是类的对象, 而该对象的函数未必是静态的哦, 以后会学到
当然你还可以写个次构造函数, 在末尾(c++叫初始化成员列表的位置)调用主构造函数
次构造函数 ★
class Person(val name: String) {
var age: Int = 0
// 这个就是次构造函数
constructor(name: String, age: Int) : this(name) {
this.age = age
}
}
构造函数优先级 ★
主构造函数优先级
高于 init初始化代码块和主构造函数外字段
高于 次构造函数
class Person(var name: String = "1") {
var age: Int
init {
if (this.name == "1") {
println("主构造函数第一时间初调用了")
}
this.name = "3"
println("init 代码块初调用了")
this.age = 2
}
constructor(age: Int) : this() {
println("次构造函数调用了")
this.age = age
}
override fun toString(): String {
return "Person(name='$name', age=$age)"
}
}
fun main() {
val person = Person(age = 4)
println(person)
}
在主构造函数中定义变量(注意不是属性是作为参数的变量), 则可以使用
_
的方式在区别, 比如:_name
或者_nickName
等等
注意:
init
代码块最后都会成为主构造函数的函数体内部的代码
class Person(var name: String) {
// init 和 下面 age 的初始化顺序优先级按照定义顺序判断优先级
init {
if (this.name == "haha") {
this.name = "zhazha"
}
}
val age: Int = 1
}
将会变成
public final class Person {
private final String name;
private final int age;
public Person() {
this.name = name;
if (Intrinsics.areEqual(this.name, "haha")) {
this.name = "zhazha"
}
this.age = 1
}
// get/set ...
}
注意5: 只有主构造函数可以在小括号内声明成员属性, 次构造函数不允许
里氏代替原则
在父子类中, 子类最好只能实现父类抽象的方法, 非抽象的方法能不重写就别重写, 这很符合
里氏代替原则
什么是里氏代替原则?
他有四种设计原则:
- 子类可以实现父类的所有抽象方法, 但非抽象方法子类最好不要再去重写
- 子类可以增加自己特有的方法
- 当子类的方法实现父类的方法时, 方法的前置条件(即方法的形参)要比父类方法的输入参数更加宽泛
- 当子类的方法实现父类的抽象方法时, 方法的后置条件(即方法的返回值)要比父类更严格.
类的默认修饰符: final
kotlin
很好的实现了这种原则, 所有默认没有被标记为open
的类/方法
都会被修饰为final
, 子类全部无法重写, 除非在类/方法
上添加open
当然如果是字段的话, 需要看修饰字段的是 val
还是 var
, 如果是 val
会携带 final
private class TheBird(
val weight: Double = 500.0,
var color: String = "blue",
var age: Int = 1
) {
fun fly() {}
}
final class Bird {
private final double weight;
@NotNull
private String color;
private int age;
public final void fly() {
}
public final double getWeight() {
return this.weight;
}
@NotNull
public final String getColor() {
return this.color;
}
public final void setColor(@NotNull String var1) {
this.color = var1;
}
public final int getAge() {
return this.age;
}
public final void setAge(int var1) {
this.age = var1;
}
public Bird(double weight, @NotNull String color, int age) {
super();
this.weight = weight;
this.color = color;
this.age = age;
}
public Bird(double var1, String var3, int var4, int var5, DefaultConstructorMarker var6) {
if ((var5 & 1) != 0) {
var1 = 500.0;
}
if ((var5 & 2) != 0) {
var3 = "blue";
}
if ((var5 & 4) != 0) {
var4 = 1;
}
this(var1, var3, var4);
}
public Bird() {
this(0.0, (String)null, 0, 7, (DefaultConstructorMarker)null);
}
}
类默认 final 真的好么?
在 kotlin官方有讨论: Classes final by default - Language Design - Kotlin Discussions (kotlinlang.org)
存在的问题:
spring
框架可能存在问题, 需要重新实现Spring
框架的部分功能. 比如Spring
使用注解对一些类进行增强, 由于kotlin
类不能被继承导致增强失败- 使用
kotlin
编写第三方库, 有些时候需要增强这些库, 需要继承库内的类, 如果是kotlin
则不可以直接继承
优点:
- 默认
final
比较安全 kotlin
推荐你使用扩展而不是继承去增强类- 还可以借助
val
配合 智能类型转换
同时
kotlin
还提供sealed
密封类对继承进行限制, 若要继承一个类必须在同一个文件
kotlin 初始化带来 bug 以及解决方案
private class Demo01 {
val name: String
private fun first() = name[0]
init {
// first 还没初始化呢, 直接就调用了? 这时候只能 报错 NullPointerException
println(first())
name = "zhazha"
}
}
fun main() {
val demo01 = Demo01()
}
class Demo02(_name: String) {
val playerName: String = initPlayerName()
val name: String = _name
private fun initPlayerName(): String = name
}
fun main() {
val demo02 = Demo02("zhazha")
println(demo02.playerName) // 最终输出 null
}
解决方案任何属性都需要先初始化再使用
接口★
在 kotlin
中实现一个接口需要实现未实现的方法和未初始化的属性
这里需要分清楚什么是字段? 什么是属性? 属性 = 字段 + getter/setter
interface MyInterface {
var name: String
val age:Int
fun bar()
fun foo() {
println(this::javaClass)
}
}
这里需要强调上面的 foo
方法, 该方法在接口中被认为是 默认方法
默认方法在接口中的实现比较复杂, 这涉及到 kotlin
早期对标的是jdk1.6
, 那时的类不允许有默认方法, 所以kotlin
的实现方式比较有意思
kotlin
并未沿用jdk 8
的接口默认方法在
jdk8
之后,java
的 接口可以存在默认方法
和静态方法
下面是 java
源码
public interface MyInterface {
@NotNull
String getName();
void setName(@NotNull String var1);
int getAge();
void bar();
void foo();
@Metadata(
mv = {1, 5, 1},
k = 3
)
public static final class DefaultImpls {
public static void foo(@NotNull MyInterface $this) {
String var1 = "zhazha";
boolean var2 = false;
System.out.println(var1);
}
}
}
kotlin
编译器生成了个 DefaultImpls
内部静态类,
然后以静态的方式写了个了和接口中的 foo
同名函数, 参数传递了个 MyInterface $this
需要注意 this
, 在 interface
中不存在 this
对象, 该 this
是在 object:MyInterface
的时候产生的匿名对象或者是实现该接口的子类 this 对象
所以我们需要 object: MyInterface
接口:
fun main() {
val obj = object: MyInterface {
override var name: String
get() = TODO("xxxxx")
set(value) {}
override val age: Int
get() = TODO("xxxxx")
override fun bar() {
// to do
}
}
// 默认函数按照需要重写
obj.foo()
}
接口中可以有接口也可以有默认方法还可以有属性(不带字段的属性
)
interface Named {
val name: String
interface Name {
val names: String
}
}
interface Person : Named {
val firstName: String
val lastName: String
override val name: String
get() = "$firstName $lastName"
}
class NameClass(override val names: String) : Named.Name {
}
data class Employee(override val firstName: String, override val lastName: String) : Person {
val position: Pair<Double, Double> = Pair(0.0, 0.0)
}
fun main(args: Array<String>) {
val employee = Employee("zzz", "ddd")
println(employee.name)
}
接口不允许有记录数据的字段, 所以在接口中定义的字段被kotlin
处理成 set/get
方法
public interface MyInterface {
@NotNull
public String getName();
public void setName(@NotNull String var1);
public int getAge();
}
接⼝继承
interface Named {
val name: String
}
interface Person : Named {
val firstName: String
val lastName: String
override val name: String
get() = "$firstName $lastName"
}
data class Employee(override val firstName: String, override val lastName: String) :Person {
val position: Pair<Double, Double> = Pair(0.0, 0.0)
}
接口的属性
我在很多地方说都过了, 属性在 kotlin
中表示 getter/setter
至于 field
有没有都无所谓
val
修饰的属性 默认 filed
被 private
修饰 , val
保证只有 getter
没有setter
如果我们在 setter
前面加上 private
, kotlin
将不会自动生成 setter
而 getter
的可见性修饰符必须与声明字段的可见性修饰符相同
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wYLh8xwQ-1655378804222)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/94ebf6af482044f4aacb11b93c0ada70~tplv-k3u1fbpfcp-watermark.image?)]
如果 getter 必须是 private 则可以这样:
接⼝中的属性默认是抽象属性
接口不会有字段, 但是可以有抽象属性, 抽象属性是一种没有 field 幕后字段
只有 getter
和 setter
的属性
private interface User {
val nickName: String // 只有 getNickName 函数, 接口不允许有字段
}
// 主构造属性会被初始化, 所以需要添加字段和 get 函数
class PrivateUser(override val nickName: String) : User {
}
class SubscribingUser(val email: String) : User {
override val nickName: String
// 重写了 get 访问器, 则不需要字段
get() = email.substringBefore('@')
}
class FaceBookUser(val accountId: Int): User {
// 初始化了 nickName, 生成 get访问器和 nickName字段
override val nickName = "name: $accountId"
}
子类重写接口属性, 根据子类具体的情况判断是否定义字段, 如果子类重写字段的 get/set
函数没有涉及 field
(或者说get/set
函数不依赖重写的字段本身)则不会直接定义一个字段, 只有 get/set
函数, 例如:
class SubscribingUser(var email: String) : User {
override var nickName: String
get() = email.substringBefore('@')
set(value) {
this.email = value.uppercase()
}
}
上面这段代码就不会产生字段, 直接生成 get/set 函数
public final class SubscribingUser implements User {
// 字段只有这一个
private String email;
public String getNickName() {}
public void setNickName(@NotNull String value) {}
public final String getEmail() {}
public final void setEmail(@NotNull String var1) {}
public SubscribingUser(@NotNull String email) {}
}
接口属性未必一定需要重写
interface User {
val email: String
val nickName: String
get() = email.substringBefore('@')
}
上面第一个属性 email
子类必须要重写, 但下面一个 nickName
在子类可以被继承
函数式接口
单一抽象方法的接口, 叫函数式接口或者叫SAM接口
fun interface KRunnable {
fun invoke()
}
注意前面的 fun
用来区分 普通接口 和 函数式接口
在
java
中函数式接口
需要写上@FunctionInterface
注解, 来标注, 但不是强迫性的, 而 kotlin 中的函数式接口必须在interface
之前加上fun
才能代表函数式接口
-
java
的接口只要只有一个未实现的抽象方法, 都可以被kotlin
编译器识别为 函数式接口(有多少默认函数无所谓) -
函数式接口可以有函数式接口构造函数
val kRunnable = KRunnable { println("函数式接口的特点") }
特点就是不需要 new
, 直接写就行
非函数式接口不能这样:
interface IRunnable {
fun invoke()
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TzjnLxyq-1655378804225)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8c42999cd2b148cca65c5e2e9b82be3d~tplv-k3u1fbpfcp-watermark.image?)]
调用函数式接口的方法
fun interface IntPredicate {
fun accept(i: Int): Boolean
}
fun isInt(i: Int, funcType: IntPredicate): Boolean = funcType.accept(i)
fun main(args: Array<String>) {
val a = 19
println(isInt(a){
it is Int
})
}
接口的多继承
接口和java
中的接口一样, 接口之间可以多继承, 实体类也可以多实现接口
interface A {}
interface B {}
interface C : A, B {}
class D : A, B {}
如果接口 A
和 接口 B
使用有一个相同的方法, fun Hello(): Unit
被 D
发现也没事, 实现只有一个
interface A {
fun hello()
}
interface B {
fun hello()
}
class D : A, B {
override fun hello() {
}
}
还有这么个案例:
interface Flyer {
fun fly()
fun kind() = "flying animals"
}
interface Animal {
val name: String
fun eat()
fun kind() = "flying animals"
}
private class Bird : Flyer, Animal {
override fun fly() {
println("I can fly")
}
override val name: String
get() = "燕子"
override fun eat() {
println("I can eat")
}
override fun kind(): String {
println(super<Animal>.kind())
println(super<Flyer>.kind())
return "my name is ${this.name}"
}
}
如果存在多个同名的默认方法, 需要通过 super<T>
这种方式指定调用
实现接口的属性和方法都必须加上 override
关键字, 且不能省略(和 java
不同)
上面的源码还可以这么搞:
private class Bird(override val name: String) : Flyer, Animal {}
将接口的属性放到子类的主构造函数中重写
其实接口中的属性只有 setter/getter 函数
, 没有 field 字段
在 Bird
中实现的 name
可以有 field
字段
final class Bird implements Flyer, Animal {
@NotNull
private final String name;
}
当然我们还可以让 name 字
段在 Bird
中不存在, 只保留 getter/setter
private class Bird : Flyer, Animal {
override val name: String
get() = "xiaobai"
}
还有这么个案例:
interface Flyer {
fun fly()
fun kind() = "flying animals"
}
interface Animal {
val name: String
fun eat()
fun kind() = "flying animals"
}
private class Bird : Flyer, Animal {
override fun fly() {
println("I can fly")
}
override val name: String
get() = "燕子"
override fun eat() {
println("I can eat")
}
override fun kind(): String {
println(super<Animal>.kind())
println(super<Flyer>.kind())
return "my name is ${this.name}"
}
}
如果存在多个同名的默认方法, 需要通过 super<T>
这种方式指定调用
实现接口的属性和方法都必须加上 override
关键字, 且不能省略(和 java
不同)
上面的源码还可以这么搞:
private class Bird(override val name: String) : Flyer, Animal {}
将接口的属性放到子类的主构造函数中重写
其实接口中的属性只有 setter/getter 函数
, 没有 field 字段
在 Bird
中实现的 name
可以有 field
字段
final class Bird implements Flyer, Animal {
@NotNull
private final String name;
}
当然我们还可以让 name 字
段在 Bird
中不存在, 只保留 getter/setter
private class Bird : Flyer, Animal {
override val name: String
get() = "xiaobai"
}
[外链图片转存中…(img-WzK3NKm4-1655378804227)]