六,类的进阶

1,类的构造器

当我们定义一个Person类时

class Person(var name:String,age:Int)
  • 带 var/val 字段的属性 是类内全局可见的
  • 而后面age 不带var/val 该属性仅在构造器内可见(init块,属性初始化)

上面的定义相当于

class Person(var name: String, age: Int) {
    var age = 0
    init {
        this.age = age
        println(this.age)
    }
}
<1>init块
  • init块跟Java中的构造块基本类似,唯一的区别是Kotlin中init块中能访问到构造函数中的不带var/val 的变量

例如

class Person(var name: String, age: Int) {
    var age = 0
    init {
        this.age = age
        println(this.age)
    }
    var testa=3
    init {
        this.age=testa
        println(this.age)  
    }
}

他们会一起编译到函数的构造函数中一起执行

<2>类的属性必须初始化

Kotlin类的属性必须初始化,类的生命周期和属性的相同,这样使用者在调用类的属性时不会有负担

<3>类的继承

子类继承父类必须调用父类的构造方法

abstract class Anim

class Pig(var name: String):Anim(){
    //必须调用父类构造函数
}
<4>副构造器
  • 副构造器:定义在主构造器后在类内部定义的构造器都被成为福构造器
  • 副构造器必须调用到主构造器,确保构造路径唯一性
abstract class Anim

class Pig(var name: String) : Anim() {
    //必须调用父类构造函数
    var age = 0;
    constructor(age: Int) : this("fff") {
        this.age = age
    }
}
不定义主构造器(不推荐)
abstract class Anim
class Dog : Anim{
    var name:String
    var age:Int
    constructor(name: String,age:Int):super(){
        this.name=name
        this.age= age
    }
}
一般情况下有这种情况的,比较推荐使用:主构造器+默认参数的形式
<5>工厂函数
fun main() {
    //Person的工厂方法
    Person("1111111")
    //String中系统的工厂方法
    String(charArrayOf('1'))
    //自定义String
    String(IntArray(2){ it })
}

val persons=HashMap<String,Person>()

fun Person(name: String):Person{
    //缓存person对象
    return persons[name]?:Person(name,25).also { persons[name] = it }
}

fun String(ints:IntArray):String{
    return ints.contentToString()
}

工厂函数和扩展方法:个人理解扩展方法的场景更大一些,而工厂函数的意义就是为了创建该类的对象,扩展方法可以返回该类对象也可以返回为Unit

2,类与成员的可见性

<1>模块的概念
  • Intelij IDEA 模块
  • Maven工程
  • Gradle SourceSet
  • Ant 任务中一次调用<kotlinc>的文件
  • 直观的讲模块的可以认为是一个jar包,一个aar包,通常一个jar包或者aar包里面所有的源文件会一次通过<kotlinc>通过编译
<2> internal VS default
  • 一般由SDK或者公共组件开发者用于隐藏内部实现细节
  • default 可通过外部创建相同的包名进行访问,访问控制比较弱
  • default 会导致不同抽象层次的类聚集到相同的包之下
  • internal 可方便处理内外隔离,提升模块代码内聚减少接口暴露
  • internal 修饰的Kotlin类或者成员在Java中可直接访问
针对Kotlin中被internal修饰的类能被Java访问,可以通过一些特殊手段让其不能访问,例如下例:
internal class CoreApiKotlinA {
    @JvmName("%abcd")
    internal fun a(){
        println("Hello A")
    }
}
<3>构造器可见性
class Cat :Anim {
    var name: String
    var age: Int
    private constructor(name: String, age: Int):super() {
        this.name = name
        this.age = age
    }
}

可用于单例

<4>类的属性可见性
class Half (private var name: String,var age: Int){
    private var sex:Boolean=true
    init {
        println("$name---$age")
    }
}
//只能访问到age和sex
var half = Half("111", 2)
half.age=11

属性的getter和setter

- 属性的getter必须和属性的可见 一致

下例中 sex是public 所以不能把getter设置为private

class Half (private var name: String,var age: Int){
    var sex:Boolean=true
    private set
    //private get
}
- 属性的setter可见性不得大于属性的可见性
class Half(private var name: String, var age: Int) {
    private var firstName: String = ""
//    public set
}
<5>顶级声明的可见性(Kotlin独有)
  • 顶级声明指文件内直接定义的属性,函数,类等
  • 顶级声明不支持protect
  • 顶级声明被private修饰表示文件内可见

3,类属性的延迟初始化

为什么要延迟初始化?

  • 类属性必须在构造时进行初始化
  • 某些成员只有在类初始化后才会别初始化
class Beer(var name: String,var age: Int){
   lateinit var firstName:String
}

常见的例子有:在Android中控件的初始化是在Activity的onCreate()方法中才能初始化,可以使用lateinit进行延迟初始化

lateinit的注意事项
  • lateinit 会让编译器忽略变量的初始化,不支持Int等基本类型
  • 开发者必须能够在完全确定变量的生命周期下使用lateinit
  • 不要在复杂的逻辑中使用lateinit,它只会让代码变得更加脆弱
  • Kotlin在1.2引入判断lateinit属性是否初始化的api 最好不要用
lazy

4,代理 Delegate

接口代理

对象X 代替当前类A 实现接口B的方法

我 代替 你 处理了 它

属性代理

对象X代替属性a实现getter/setter方法

<1>接口代理

定义接口

interface InterFace {
    fun funA()
    fun funB()
    fun funC()
}

定义增强接口类

 class InterfaceWrapper(var inter:InterFace):InterFace{
     override fun funA() {
         inter.funA()
     }

     override fun funB() {
         inter.funB()
     }

     override fun funC() {
         println("---------------")
         inter.funC()
     }

代理形式

 class InterWrapper (private val inter: InterFace):InterFace by inter{
         override fun funC() {
             println("---------------")
         }
     }

接口 inter 代理 InterWrapper 实现 接口 InterFace 的方法

对于对象inter的唯一要求就是实现被代理的接口,因为如果inter没有实现接口方法的话,就不具有代理的能力

小案例 创建一个SupportArray

class SupperArray<E>(
    private val list: MutableList<E?> = ArrayList(),
    private val map: MutableMap<String, E> = HashMap()
) : MutableList<E?> by list, MutableMap<String, E> by map {
    override fun isEmpty(): Boolean {
        return list.isEmpty() && map.isEmpty()
    }

    override fun clear() {
        list.clear()
        map.clear()
    }

    override val size: Int
        get() = list.size + map.size

    override fun set(index: Int, element: E?): E? {
        if (list.size <= index) {
            repeat(index - list.size + 1) {
                list.add(null)
            }
        }
        return list.set(index, element)
    }

    override fun toString(): String {
        return "list:${list};map:${map}"
    }
}

SupportArray

  • 实现 MutableList 被list代理了接口的实现
  • 实现MutableMap 被map代理了接口的实现

使用SupperArray

fun main() {
    val supperArray = SupperArray<String>()
    supperArray.add("Hello")
    supperArray["Hello"] = "wwwwwwwwww"
    supperArray["Word"] = "fffffffffffff"
    supperArray[4] = "$$$$$$$$$"
    println(supperArray)
}

输出

list:[Hello, null, null, null, $$$$$$$$$];
map:{Word=fffffffffffff, Hello=wwwwwwwwww}
<2>属性代理
getter方法代理
class PersonNow(var sex: Boolean) {
    val firstName: String by lazy {
        if (sex)
            "nan"
        else
            "nv"
    }
}

源码分析

--lazy方法

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = 
SynchronizedLazyImpl(initializer)

--Lazy.kt

@kotlin.internal.InlineOnly
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, 
property: KProperty<*>): T = value

可以看到Lazy定义的方法 getValue

getValue(thisRef: Any?, property: KProperty<*>)

要想代理属性的getter需要重写该方法

setter方法代理

对于setter方法,其代理可分为两步:开始设置和设置之后

Delegates.observable代理设置之后
class Manger {
    var state:Int by Delegates.observable(0){
        property, oldValue, newValue ->
        println("$oldValue,$newValue")
    }
}
Delegates.vetoable代理设置之前
class Mantou {
    var ss: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
        println("$oldValue,$newValue")
        newValue % 2 == 0
    }
}

调用

fun main() {
    val mantou = Mantou()
    mantou.ss = 1
    mantou.ss = 2
    mantou.ss = 3
    mantou.ss = 4
}

结果

0,1
0,2
2,3
2,4

Delegates.vetoable 给定初始化值后其结果返回值为Boolean,如果返回true表示允许本次对属性的设置,属性的值为newValue,返回false表示拦截此次设置值,属性的值仍然为oldValue

源码得知
Delegate.observable 返回 ReadWriteProperty

public interface ReadWriteProperty<in R, T> {
    public operator fun getValue(thisRef: R, property: KProperty<*>): T
    public operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

对于var 来说需要实现下列方法,就可以代理属性

    public operator fun getValue(thisRef: R, property: KProperty<*>): T
    public operator fun setValue(thisRef: R, property: KProperty<*>, value: T)

对val来说 实现下方法即可代理属性

  public operator fun getValue(thisRef: R, property: KProperty<*>): T

具体例子

class Food {
    var x: Int by X()
}

class X {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        println("getValue")
        return 2
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, i: Int) {
        println("setValue")
    }
}

fun main() {
    val food = Food()
    food.x=1;
    println(food.x)
}

运行结果

setValue
getValue
2

对food 的属性 x 调用其getter或者setter时会调用的属性代理

5,单例object

Kotlin中单例

  • 使用object进行定义
  • 默认生成无参构造函数且不能自定义构造器,但是可以通过init{}代码块进行一些初始化操作
  • 跟普通的Kotlin类一样可以实现接口,可接继承其他类
  • 其本质就是Kotlin使用饿汉式内部生成了INSTANCE的实例
public object Sigten {
    var x: Int = 0
    var y: String = ""
    fun mm() {}
}

Kotlin中访问

fun main() {
    Sigten.x = 3
    Sigten.y = "WWWWWWWW"
    println("${Sigten.x}||${Sigten.y}")
}

Java中访问

Sigten.INSTANCE.mm();
Sigten.INSTANCE.setX(1);
Sigten.INSTANCE.getX();

由于Kotlin是一门跨平台语言,不能因为Jvm上有静态变量就定义出相应的变量类型,这对于C语言和JavaScript行不通

@JvmStatic 模拟JVM平台的静态变量
public object Sigten {
    @JvmStatic var x: Int = 0
    @JvmStatic fun mm() {
    }
}

Java调用

Sigten.mm();
Sigten.setX(1);
Sigten.getX();
@JvmField 不让Kotlin属性生成getter和setter,直接访问属性
public object Sigten {
    @JvmStatic var x: Int = 0
    @JvmField var y:Int=0
    @JvmStatic fun mm() {
    }
}

Java调用

Sigten.mm();
Sigten.setX(1);
Sigten.getX();
Sigten.y=1;
伴生对象
  • 是某个类的另一半
  • 其名称和所在类相同
  • 伴生对象也是单例的
class FoodX {
    companion object {
        @JvmStatic var x: Int = 0
        @JvmStatic fun mm() {
        }
    }
}

想当于Java中

public class FoodX {
    public static int x = 0;
    public static void mm(){}
}

6,内部类

  • 内部类分为静态内部类和非静态内部类
  • 静态内部类不需要引用外部对象,可直接创建对象
  • 非静态内部类需要引用外部对象,容易引起内存泄露
class Outer{
    //内部类
    inner class Inner(){}
    //静态内部类
    class StaticInner(){}
}

使用

fun main() {
    //创建非静态内部类
    var inner = Outer().Inner()
    //创建静态内部类
    var staticInner = Outer.StaticInner()
}
<1>内部object
  • 内部object 不存在非静态,都是静态
object OuterObject {
    object InnerStaticObject {
        var x: Int = 0
    }
}

使用

OuterObject.InnerStaticObject.x=3
<2>匿名内部类
object :Runnable{
        override fun run() {
        }
    }
和Java中不同的是,Kotlin中匿名内部类支持多实现或继承父类

例如

object :Runnable,Cloneable{
        override fun run() {
        }
    }
Java中也可以实现方法中多继承 使用LocalClass
public class TestObjeck {
    public static void main(String[] args) {
        class LocalClass implements Cloneable,Runnable{
            @Override
            public void run() {
            }
        }
    }
}
Kotlin中不仅支持LocalClass 还支持localMethod(方法中定义方法)
  object :Runnable,Cloneable{
        override fun run() {
        }
    }

    class LocalClass:Runnable,Cloneable{
        override fun run() {
        }
    }

    fun localMethod(){}

7,数据类 data class

component: 定义在主构造器中的属性又被称为component

  • 数据类和一般类最大的区别就在于component
  • Kotlin中的data class 不等价与Java bean
  • 编译器会基于component 自动生成 equals/hashCode/toString/copy
data class TT(val name: String, val age: Int) {}
fun main() {
    var tt = TT("", 4)
}
如何合理的使用data class
  • 单纯的把data class 当成数据结构使用既是纯数据,大多数情况下不需要额外的实现
  • 主构造函数的变量类型 最好为基本类型\String 或者其他data class 保证数据类为纯数据
  • component 不能定义getter和setter 为了防止通过getter或者setter 进行数据篡改
  • 定义构造函数的变量 最好定义为 val ,如果可变会导致其hashCode等值的改变 如果该对象在hashset中 则无法移除该元素

为了实现一些特殊的需求,需要data class 有无参构造和可以被继承,官方提供了 noArg 和allOpen 插件进行支持

id 'org.jetbrains.kotlin.plugin.noarg' version '1.3.60'
id 'org.jetbrains.kotlin.plugin.allopen' version '1.3.60'

8, 枚举类 enum class

  • 和 Java中枚举相似 区别是通过 enum class 定义

无参构造枚举

enum class CC {
    Idle, Busy
}

有参构造枚举

enum class DD(arg: Int) {
    Idle(0), Busy(1)
}

枚举实现接口--统一实现方式

enum class FF : Runnable {
    Idle, Busy;

    override fun run() {
    }
}

枚举实现接口--单独实现方式

enum class EE : Runnable {
    Idle {
        override fun run() {
        }
    },
    Busy {
        override fun run() {
        }
    };
}
<1>因为enum 枚举本身是个类 所以可以为其定义扩展方法
<2>枚举本事是可数的所在when 条件语句中可以不加else
<3> 因为枚举本身有顺序的概念所以可以对其进行比较大小
<4> 枚举有顺序所以也有区间概念
enum class Color {
    White, Red, Black, Pink, Ori, Yellow
}
fun main() {
    val ran = Color.Black..Color.Yellow
    val color = Color.White
    color in ran
}

9,密封类 sealed class

  • 密封类是一种特殊的抽象类
  • 密封类的子类必须定义在与自身相同的文件中
  • 因为密封类的子类定义范围有限,所以密封类的子类有限,一旦定义好密封类的子类后,外部不可能出现其他子类
  • 密封类首先是个抽象类,其次是个密封类

例子

sealed class PlayState

object Idle : PlayState()

object Playing : PlayState() {
    fun start() {}
    fun stop() {}
}

object Error : PlayState() {
    fun recover() {}
}

使用上面定义的几个类型做个小案例

class Player {
    var state: PlayState = Idle

    fun play() {
        this.state = when (val state = this.state) {
           is Idle -> {
                Playing.also {
                    it.start()
                }
            }
            is Playing -> {
                state.stop()
                state.also {
                    it.start()
                }
            }
            is Error -> {
                state.recover()
                Playing.also {
                    it.start()
                }
            }
        }
    }
}
如何区分使用密封类还是枚举呢
  1. 如果需要进行类型区分则是使用密封类,如果要进行值的区分 则用枚举
  2. 枚举类不能创建对象,而密封类可以创建对象

枚举不能创建对象这点个人理解起来比较难一点,为什么不能枚举就不能创建对象呢?
因为枚举就是实例对象的存在<O(∩_∩)O哈哈~>
而密封类则不同,他的分支是类型,只要是子类的对象就行,因此可以创建多个对象

10,内联类 inline class

  • 内联类是对某一个类型的包装
  • 类似于Java中装箱类型的一种类型
  • 编译器会尽可能使用被包装的类型进行优化
  • 内联类目前在1.3版本中处于公测阶段,谨慎使用
使用注意
  1. 内联类不能有backingfiled 只能有方法
  2. 内联类可以实现接口,但不能继承父类也不能被继承

例子

inline class BoxInt(private val value: Int) {
    operator fun inc(): BoxInt {
        return BoxInt(value + 1)
    }
}
内联类的使用场景
  • 官方 使用内联类 实现无符号类型
  • 内联类模拟枚举,因为枚举内存开销比较大,和dex文件大小
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352