[Kotlin]Kotlin学习笔记(四):类与对象、泛型详解

时间:2021-04-16 20:08:14

>与类定义有关的关键字

  • open :标示一个类,使得这个类可以被继承
  • abstract :标示一个抽象类,这个类默认为open;
  • interface :标示一个接口,默认为open;
  • internal :标示一个内部类(本篇略去);
  • final :标示一个final类,不可被继承和重写;
  • constructor :构造器关键词,用于主构造器时可以省略;
  • init :初始化代码块,详见下文;
  • public :权限修饰符,同JAVA;
  • private :权限修饰符,同JAVA;
  • protected 权限修饰符,同JAVA;
  • lateinit :延迟初始化,用法见:>lateinit与by lazy的应用<
  • set()/get() :属性获取方法,涉及Backing Field机制,详见下文;
  • override :函数/方法重写;
  • field :后端变量(backing field关键字);


>类的定义

[权限修饰符][final|open|abstruct] class [<泛型>] 类名 [主构造器权限修饰符][主构造器] [:继承关系]{
$类属性定义
$类方法定义
}
    接口的定义稍微不同:

interface 接口名 {
$接口属性定义
$接口方法定义
}


>类/接口属性定义

    在Kotlin中,没有字段的设计,只有属性。

    什么是字段和属性?

  • 字段,也被叫做类成员,其一般定义形式为val s:Striing = "字段";
  • 属性,带有set()和get()方法;在Java中,对于属性atr,若存在setATR(...)及getATR(),则属性atr为类的一个属性。

    让我们看一下一个完整的非泛型类定义:

class DemoClass constructor(name:String) : InfoInterface { // 默认为final类、public类,是非open的、不可被重写
//默认属性与方法是public的
private val id:Int by lazy { 5 } // 惰性加载
protected lateinit var sex:String // 延迟加载,仅对可空对象可进行如此操作
public var name:String=name // 与主构造器中的参数名字相同,会被建议修改。但并不影响代码运行

private var age:Int = 0
get() = field
set(value) {
if(value < 0){
field = 0
} else {
field = value
}
}//属性get()与set()的重写

init {
println("初始化代码块")
}
constructor(name:String,sex:String):this(name){
println("构造函数1")
this.sex=sex
}
constructor(name:String,sex:String,a:Int):this(name){
println("构造函数2")
this.sex=sex
age=a
}

//这是一个方法,重写了接口InfoInterface中的方法
override fun info(){
println("id=${this.id} \n name=$name \n sex=$sex \n age=$age")
}
}
    刚刚提到了属性与字段的定义,一定有人问前三个属性(id sex name)是不是字段,因为并没有看到相应的get和set方法。答案是否定的。

    kotlin中不存在字段,只有属性。事实上,kotlin为默认为每个属性生成一个get与set函数:

var attr:Class = $VALUE
get() = field
set(value) = { field = value }

    其中的field就是后端变量(backing field)的关键字。对于刚刚提到的三个属性(id sex name),实际上kotlin就为他们默认生成了这样的方法。

    回到最初的类函数定义,观察第四个属性age:

private var age:Int = 0
get() = field
set(value) {
if(value < 0){
field = 0
} else {
field = value
}
}

    这段代码的意思是,当调用属性age时,会直接返回这个属性的值( get()=field );当进行赋值时,若所赋的值大于0则正常赋值,否则,赋值为0;本质上,get()后面只要是个表达式就可以,所以也可以这么写:

private val age:Int = 0
get() = if( field > 0 ) field else 0
set(value) { field = value }

    所以,kotlin中,对一个属性的完整定义应该为:

var <propertyName>[: <PropertyType>] [= <property_initializer>]     
[<getter>]
[<setter>]

  注意,只有var才能有set()方法总的来说,后端变量机制就是对get()和set()的重写,field代指的是该变量未更新前的值


>类/接口方法定义

    类的方法的定义:

[权限修饰符] [abstruct|final] fun 方法名 (参数表){
函数体
}
   如果是重写的方法,需要在前面加上一个 override关键词。


>类的构造

   kotlin中,类的构造,分为主构造器和次构造器,有以下几点需要注意区分:

  • 主构造器的关键字constructor可以省略不写,但次构造器必须写;
  • 次构造器都是public的;
  • 主构造器只能存在一个,而次构造器可以有很多个;
  • 若存在主构造器,每个次构造器使用时,必须使用:this()继承所有来自主构造器的参数;
  • 主构造器接受lambda表达式;
  • 若主构造器中某个变量被声明为val/var时,那么这实际上可以被当做一个类属性。
    关于上面提到的最后一条特性( 很多教程中都没有提到这一点),用下面这个例子来说明就好了:

    [Kotlin]Kotlin学习笔记(四):类与对象、泛型详解

   什么是主构造器的用法?看我在文章开头给的那个例子,可以看到在类的第一行给出了主构造器:

class DemoClass constructor(name:String)
   其参数常用于类属性的初始化(kotlin中定义类时就要保证类属性不为空),详见 >这篇文章<中关于主构造器赋值的内容。

   在我给出的例子中,有两个次构造器:

    constructor(name:String,sex:String):this(name){
println("构造函数1")
this.sex=sex
}
constructor(name:String,sex:String,a:Int):this(name){
println("构造函数2")
this.sex=sex
age=a
}
    他们都继承了来自主构造器的属性name:String,同时,次构造器1与次构造器2在参数上不同,这就使得在构造时,下面的三个语句进行对象实例化,构造时调用是完全不同的内容:

val d:DemoClass = DemoClass("shepibaipao","girl")//调用次构造器1
val d:DemoClass = DemoClass("shepibaipao","girl",20)//调用次构造器2
val d:DemoClass = DemoClass("shepibaipao")//调用主构造器
    但无论如何,在调用次构造器前,一定会调用init{}语句块。具体调用顺序为: 主构造器>init{}语句块>次构造器

    由于constructor都是public类型的,想要写出一个单实例类,没法像java一样操作。

    解决方法是:在kotlin中,主构造器是可以具有private属性的

class demo private constructor(name:String){}//此时constructor不可省略


-----------------------------------------------


>Kotlin中的泛型

    Kotlin中的泛型函数定义如下:

fun <T> funcA(t:T) {
println(t)
}

    Kotlin中的泛型类定义如下:

class A <T>(t:T){
var v = t
}

    Koltin中的泛型,是经过类型擦除的,看下面这个例子:

var aInt = A<Int>(100086)
var aString = A<String>("shenpibaipao")

println(aInt.javaClass) // 打印:class Box
println(aString.javaClass) // 打印:class Box
    这意味着,在编译成字节码时,class A <T> 与 class A <Int>  没有任何差异。

>通配符

    先来回顾一下java中的通配符

  • < ? extends Father > 上边界限定通配符,可读不可写,在PECS原则中也被称为生产者。
  • < ? super Child > 下边界限定通配符,可写不可读,在PECS原则中也被称为消费者。
   那么,Kotlin中有没有相应的机制呢?答案是肯定的,这些机制被称为: 类型协变类型投射泛型约束

>1.类型协变

   类型协变用 in 注明消费者,用 out 注明生产者:

class A <in C, out P>{
fun consume(c:C){ // 只消费(写入)C的对象c
//deal c
}
fun produce():P{
val p:P by Proxy()
return p // 只生产(读出)P的对象p
}
}

>2.类型投射

    kotlin中的类型投射,主要针对“星号投射”,抛开那些绕来绕去的说法,单刀直入吧,对于:

class  A < in C , out P >( c:C , val p:P ){
fun f1(c:C){
println(c)
}
fun f2():P{
return p
}
}

  • 当用 * 代替in的类型C时,表示in Nothing,即完全不可写;
  • 当用 * 代替out的类型P时,表示out Any?,即可以任意读出P及其父类。
    val o1:A<*,String> = A (7,"yes") // 只可以读出String的任意父类
val o2:A<Int,*> = A (7,"yes") // 可以安全写入Int
val o3:A<*,*> = A (7,"yes") // 只可以读出String的任意父类,不能安全写入Int
>3.泛型约束
    Kotlin中的泛型约束相当于<? extends Father>:

class D < T : List<T> >{
}
    这样,List的所有子类均可被接受。当有多个上界时,可以这么写:

class D <T> where T: Comparable<T> , T:List<T>{

}