我的kotlin学习笔记(三)——类和对象

本篇主要讲类,里面也混杂着其他小点。

使用关键字class声明类,类由类名、类头(指定其类型参数,主构造函数等)和花括号包围的类体构成。类头和类体可以省略;如果没有类体,可以省略花括号。

class Invoice(name: String) {
}

// 没有类体,省略花括号
class Empty

3.1 、创建实例对象
kotlin没有new关键字,我们直接调用对象xx()

class B{}
// java 创建方式
// B b = new B();

//kotlin的风格
val B= B()

3.2、构造函数
kotlin里一个类可以有一个主构造函数和多个次构造函数。

3.2.1、主构造函数
主构造函数跟在类名(和注解、可见性修饰符)的后面,类似:

// 如果主构造函数里有注解或可见性修饰符,constructor关键字一定不能省略:
// 可见性修饰符(public、protected、private、internal)
class A public @Inject constructor(val name: String) {

}
// 否则,constructor关键字可写可不写
class A constructor(val name: String) {
}

class A(var firstName: String) {
}
// 主构造函数不能包含任何代码,初始化代码可以放到以init关键字为前缀的初始化块中。
class A(var name: String) {

    init {
        Log.e("name is ", name)
    }

    init {
        Log.e("name is ", name)
    }

    fun showName(){
        Log.e("name is ", name)
    }
}

由上可得:

  1. 主构造有注解或可见性修饰符,constructor关键字一定不能省略;
  2. init(初始化)块可以有多个,并按照它们出现的顺序执行;
  3. 主构造的参数属性默认用val(只读)声明,且只能在初始化块和类体属性里具有可读权限;可以直接用var(可变)声明主构造参数,直接将该参数升级为类属性。

(Ps:播个小插曲,fragment强烈不推荐自定义的构造函数,请看具体缘由

3.2.2、次构造函数

次构造函数由前缀constructor组成,如果有主构造函数,每个次构造函数都必须使用this关键字直接或通过别的次构造函数间接委托给主构造函数。

 // 必须是constructor前缀
 // 不带参数的次构造函数 
 constructor()

 // 带参数的次构造函数
 constructor(ctx: Context)

 class B(ctx: Context) {
    // 直接指向主构造函数
    constructor(ctx: Context, name: String) : this(ctx)

    // 通过别的次构造函数间接委托给主构造函数
    constructor(ctx: Context, name: String, name2: String): this(ctx, name)

    // 初始化块
    init{
        ...
    }

 }

注意:所有init块的代码作为主构造函数的一部分,委托给主构造函数又是次构造函数的第一条语句,因此init块的代码每次都会在次构造函数之前执行。即使该类没有主构造函数,这种委托也会隐式发生。

3.3、继承

3.3.1、公共超类
kotlin中所有类都有一个共同的超类——Any。对于没有超类型声明的类是默认超类。

(补:kotlin的超类类似java的父类,派生类类似java中的子类。)

3.3.2、open和final:
open:允许其他类继承这个类,也就是说如果超类中的属性或方法用open修饰,则子类可以继承这个相应的属性和方法。open其实是为继承而设计的。

final:默认情况下,kotlin的所有类都是final。不可被其他类继承

继承代码栗子:

// 超类,open表示该类可被继承
open class A(name: String){
    // 该属性可以被继承
    open val ss = ""
    // 该方法可以被继承
    open fun test(): String {
        return ""
    }
}

//派生类,默认是final
class B(name: String): A(name){

   // 覆盖超类的属性,可以重写get和set方法
   override var ss: String
       get() = super.ss
       set(value) {}

    // 覆盖超类的方法
    override fun test(): String {
        return super.test()
    }
}
  • var属性可以覆盖val属性,反之不行。因为一个val属性本质上声明了一个getter方法。
  • 在实例化派生类的过程中,第一步是完成基类的初始化,此时派生类的覆盖的属性还没有初始化,如果在基类初始化逻辑中使用了任何一种open成员,结果可能导致不正确的行为。所以设计一个基类时,应该避免在构造函数、属性初始化和init块中使用open成员。

3.4、属性和字段

3.4.1、属性可以用var(可变)和val(只读)声明。

举一个字符串转大写的栗子

// var声明的属性内置set和get方法
var names = ""
    // field 标识符只能在属性的访问器内
    get() = field.toUpperCase()
    set(value) {
        if (value.equals("")) {
             field = name2
        } else {
            field = value
        }
    }

// var声明的属性只有get方法
private val name2 = ""
    get() = field.toUpperCase()

注意:通过默认getter和setter访问私有属性会被优化,不会引入函数调用开销。

3.4.2、编译器常量——const
已知值的属性可以使用const修饰符标记为编译器常量,这些属性满足一下要求:

  1. 位于顶层(类的并列层)或是object的一个成员;
  2. 用String或原始类型初始化;
  3. 没有自定义gettter,即只能用val来声明变量

3.4.3、延迟初始化属性和变量

使用lateinit修饰符标记延迟初始化。

class Test{
    lateinit var b: B

    fun test() {
        // 检测一个lateinit var是否已初始化
        if (Test::b.isLateinit) {
             Log.e("延迟初始化", "延迟初始化")
        }
    }

    class B{
        var name = "jjj"
    }
}

使用注意:

  1. 该修饰符只用用在类体中的属性(不包括主构造函数中声明的var)并且该属性没有没有自定义setter和getter;
  2. 该属性或变量必须为非空类型,并且不能是原生类型(Int、Float等等);

    3.5、接口

    kotlin的接口和java8类似,既包含抽象方法,也包含实现方法。

    interface A{
        // 抽象属性,一定要实现
        val name: String

        // 提供访问器实现,可重写可不重写
        val age: String
            get() = ""

        // 抽象方法,一定要实现
        fun bar()

        // 实现方法
        fun foo(){
            Log.e("实现方法", "实现方法");
        }
    }

    interface B{
        fun bar(){
        }

        fun foo(){
        }
    }


    class Test:A, B{
        // 实现抽象属性
        override val name: String
            get() = ""

        // 实现抽象方法
        override fun bar() {
            super<B>.foo()
        }

        // 解决多接口的覆盖冲突
        override fun foo() {
            super<A>.foo()
            super<B>.foo()
        }
    }

注意:如果一个类继承多个接口,继承的接口里有两个或两个以上相同的方法,该类一定要求实现或重写这个方法。

3.6、扩展

3.6.1、扩展函数

扩展函数指在一个类上增加一个新的行为,即使我们没有这个类代码的访问权限。

在java中,通常会实现很多带有static方法的工具类,而kotlin中扩展函数的一个优势是我们不需要在调用方法的时候把整个对象当作参数传入,它表现的就像是属于这个类一样,而且我们可以使用this关键字和调用所有public方法

// 为MutableList<List>添加一个swap函数
// this关键字对应MutableList对象,<T>是泛型参数
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // “this”对应该列表
    this[index1] = this[index2]
    this[index2] = tmp
}

// 可空接收者
fun Any?.toString(): String {
    if (this == null) return "jjj"
    return toString()
}

// 加载图片时候的使用
fun ImageView.loadUrl(url: String) {
    Picasso.with(context).load(url).into(this)
}

3.6.2、扩展属性:

// 扩展属性不能有初始化器,只能有显示提供的getters/setters定义
val <T> List<T>.lastIndex: Int
    get() = size - 1

var TextView.text: CharSequence
    get() = getText()
    set(v) = setText(v)

总结下扩展的特点:

  1. kotlin的扩展函数使我们可以为现有类添加新的函数,实现某一具体功能;
  2. 扩展是静态解析的,并未对原类添加函数或属性,对类本身没有任何影响;
  3. 扩展属性只能定义类或者kotlin文件中,不允许定义在函数中;扩展方法则没有太多限定。为了便于管理,大多数时候直接在包里定义扩展。

更多延伸:点击这里

3.6、数据类

数据类有点类似javabean,数据类用data标记。数据类必须满足以下要求:

  • 主构造函数需要至少一个参数;
  • 主构造函数的所有参数标记为val或var;
  • 数据类不能是抽象(abstract)、开放(open)、密封(sealed)或者内部的;
  • (在1.1之前)数据类只能实现接口。
data class Person(val name: String, val age: Int)  {
    var id: Int = 0
}

val person1 = Person("John", 24)
val person2 = Person("John", 24)

person1.id= 10
person2.id= 20

// 输出Person(name=John, age=24)
println(person1.toString()) 

// 复制
val newPerson1 = person1.copy()
val newPerson2 = person2.copy(name = "jack")

// 解构声明
val person3 = Person("Jane", 35)
val (name, age) = person3 
println("$name, $age years of age") // 输出 "Jane

person1和person2的toString()、equals()、hasCode()、copy()只会用到name和age属性,即使id不同,它们也视为相等。由此可知,数据类的唯一性其实是由主构造函数的参数决定的。

3.6、密封类

密封类用sealed关键词表示。密封类专门为那种可以把元素地划分到几个确定的子类的类提供的封装,可以看做风壮烈的枚举,其特点如下:

  • 密封类为继承设计的,是一个抽象类;
  • 密封类的子类是确定的,除了已经定义好的子类外,它不能再有其他子类;
  • 密封类的子类只能定义在密封类的内部或同一个文件中,因为其构造方法为私有;
 // 演奏控制类(密封类)
sealed class PlayerCmd {
    val playerName: String = "Player"

    // 演奏类
    class Player(val url: String, val position: Long = 0): PlayerCmd() { 
        fun showUrl() {
            println("$url, $position")
        }
    }

    // 快进
    class Seek(val position: Long): PlayerCmd() 

    //暂停(无需进行重载的类适合用单例object)
    object Pause: PlayerCmd()
}

//(密封类的子类也可以定义在密封类的外部,但要在同一个文件中)
// 继续
object Resume: PlayerCmd() 

// 停止
object Stop: PlayerCmd() 

// 枚举适合表现简单的状态
enum class PlayerState { 
    IDLE, PAUSE, PLAYING, STOP
}


// 测试代码
PlayerCmd.Player("苍茫的天涯").showUrl()
println(PlayerCmd.Resume.playerName)

关于密封类,点击阅读更多

3.7、泛型

泛型是Java1.5的新特性,泛型的本质是参数化类型

3.7.1、泛型的使用方式

// 泛型类: 具有一个或多个类型参数的类
class Plate<T, U> {
    private T first;
    private T second;

    public Plate(T t, U U) {
        first = t;
        second = u;
    }  
}

// 泛型方法:带有类型参数的方法,可以定义在泛型类和普通类中
class Fruit {
    public <T> void addFruit(T t) {
        List<T> list = new ArrayList();
        list.add(t);
    }
}

// 泛型接口
public interface Comparator<T> {

    public int compare(T lhs, T rhs);

}

3.7.2、泛型的通配符

有时候希望传入的类型有一个指定的范围,从而可以进行一些特定的操作,这时候就是通配符边界登场的时候了。

无限制通配符

// 无限制通配符在类型不确定时使用
 private <E> void swapInternal(List<E> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

private void swap(List<?> list, int i, int j){
    swapInternal(list, i, j);
}

上界通配符 < ? extends E>

< ? extends E>表示这个泛型中的参数必须是E或者E的子类。具体是什么类型不知道,只知道它是这个范围的一种,下界通配符也是这个理。

注意:上界< ? extends E>不能往里存,只能往外取。因为编译器只知道容器的类型是E或者它派生类里的某一种(注意不是任意),但具体是什么类型不知道,所以如果往容器添加数据,容器无法识别添加数据的类型。但是往外取就不一样,容器的派生类是已知的。

下界通配符< ? super E>

< ? super E>表示这个泛型中参数必须是E或者E的父类。

注意:下界< ? super E>不能往外取,只能往里存。因为编译器知道容器类型一定是E的父类型,所以往容器里添加E或任何E的派生类都是没问题的,这些对象都可以向上转型为E。此时容器的类型实际是< ? super E>,如果我们想获取类型,是难实现的,因为我们根本不知道容器的类型E是的哪个父类。

泛型通配符更形象的介绍:拓展1
拓展2

————————–分割线————————–
泛型这块真的看了很久很久,终于有点眉目能往下记笔记了。能继续的诀窍就是不断思考和查资料,直到恍然大悟。

kotlin的型变包括协变、逆变、不变三种。

// out是型变注解,表明Source的元素类型是协变的,等价于<? extends T>
// 即Source<Int> 也是Source<Number> 的父类
interface Source<out T> {
    fun nextT(): T
}

// in是型变注解,表明Comparable的元素类型是逆变,等价于<? super T>
interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}

声明处型变

// Java
interface Source<T> {
  T nextT();
}

void demo(Source<Integer> strs) {
    // 在Java中是不允许的
    Source<Object> objs = strs; 
    // 正确方式为
    // Source<? extends Object> object = strs;
    //...
}


// kotlin
// 声明处型变:解决了上面java的限定问题
interface Source<out T> {
  T nextT();
}

fun demo(strs: Source<Integer>) {
    var x: Source<Any> = strs
}

可以发现,java的泛型型变只支持在使用时发生型变,而kotlin解决了这个不足,在声明处型变,让代码更简洁。

类型投影

fun copy(from: Array<out Number>, to: Array<Number>) {
    for (i in to.indices) {
        to[i] = from[i]
    }
}

fun fill(dest: Array<in Float>, value:Float){
    dest.set(0, value)
}

// 使用
val ints: Array<Float> = arrayOf(1f, 2f, 3f)
val any = Array<Number>(4) { 3 }
copy(ints, any) // 复诊数组
fill(any, 3f) // 添加数据

这里发生的事情称为类型投影:copy()方法的入参from的变量是一个投影的数组。

星投影

针对类型参数一无所知的情况,仍然希望以安全的方式使用,星投影问世。官网也没详说,反正我没看懂。

interface Function <in T, out U>

// 星投影
 - Function<*, String> 表示 Function<in Nothing, String>
 - Function<Int, *> 表示 Function<Int, out Any?>;
 - Function<*, *> 表示 Function<in Nothing, out Any?>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值