Kotlin基础3——定义类、初始化、继承、对象、接口、抽象类、泛型

一.定义类

1.field

针对定义的每一个属性,Kotlin都会生成一个field、一个getter、以及一个setter,field用来存储属性数据,你不能直接定义field,Kotlin会封装field,保护它里面的数据,只暴露给getter和setter使用。属性的getter方法决定你如何读取属性值,每个属性都有getter方法,setter方法决定你如何给属性赋值,所以只有可变属性才会有setter方法,尽管Kotlin会自动提供默认的getter和setter方法,但在需要控制如何读写属性时,也可以自定义它们。

定义一个Player类,自定义(重写)属性的setter和getter方法

image.png

2.计算属性

计算属性是通过一个覆盖的get或set运算符来定义,这时field就不需要了。

image.png

3.防范竞态条件

如果一个类属性既可空又可变,那么引用它之前你必须保证它非空,一个办法是用also标准函数。

image.png

二.初始化

1.主构造函数

我们在Player类的定义头中定义一个主构造函数,使用临时变量为Player的各个属性提供初始值,在Kotlin中,为便于识别,临时变量(包括仅引用一次的参数),通常都会以下划线开头的名字命名。

image.png

2.在主构造函数里定义属性

Kotlin允许你不使用临时变量赋值,而是直接用一个变量同时指定参数和类属性,通常,我们更喜欢用这种方式定义类属性,因为他会减少重复代码。

image.png

3.次构造函数(关键字:constructor)

有主就有次,对应主构造函数的就是次构造函数,我们可以定义多个次构造函数来配置不同的参数组合。

image.png

4.使用次构造函数,定义初始化代码逻辑。

image.png

5.默认参数

定义构造函数时,可以给构造函数参数指定默认值,如果用户调用时不提供值参,就使用这个默认值。

image.png

6.初始化块(关键字:init)

初始化块可以设置变量或值,以及执行有效性检查,如检查传给某构造函数的值是否有效,初始化块代码会在构造类实例时执行。

image.png

7.初始化顺序

1.主构造函数里声明的属性;
2.类级别的属性赋值;
3.init初始化块里的属性赋值和函数调用;
4.次构造函数里的属性赋值和函数调用;

image.png

image.png

8.延迟初始化

使用lateinit关键字相当于做了一个约定:在用它之前由开发人员负责初始化
如果无法确认lateinit变量是否完成初始化,可以执行isInitialized进行检查

image.png

9.惰性初始化

延迟初始化并不是推后初始化的唯一方式,你也可以暂时不初始化某个变量,直到首次使用它,这个叫作惰性初始化。

image.png

10.初始化陷阱一

在使用初始化块时,顺序非常重要,你必须保证块中的所有属性已经完成初始化。

错误代码:

image.png

正确代码:

image.png

11.初始化陷阱二

这段代码编译没有问题,因为编译器看到name属性已经在init块里初始化了,但代码一运行就会抛出空指针异常,因为name属性还没有赋值,firstLetter函数就应用了它了。

image.png

12.初始化陷阱三

因为编译器看到所有属性都初始化了,所以代码编译没问题,但运行结果却是null,问题出在哪了?在用initPlayerName函数初始化playerName时,name属性还未完成初始化,只需要将playName和name位置互换即可运行正常。

错误代码:

image.png

正确代码:

image.png

三.继承

1.继承

类默认都是封闭的,要让某个类开放继承,必须使用open关键字修饰它。

image.png

2.函数重载

父类的函数也要以open关键字修饰,子类才能覆盖它。

image.png

3.类型检测

Kotlin的is运算符是个不错的工具,可以用来检查某个对象的类型。

image.png

运行结果:

image.png

4.Kotlin层次

无需再代码里显示指定,每一个类都会共同继承一个叫Any的超类

image.png

5.类型转换

as操作符声明,这是一个类型转换

image.png

6.智能类型转换

Kotlin编译器很聪明只要能确定any is 父类条件检查属实,它就会将any当作子类类型对待,因此,编译器允许你不经类型转换直接使用。

image.png

四.对象

1.object关键字

使用object关键字,你可以定义一个只能产生一个实例的类——单例。

使用obect关键字有三种方式:
-对象声明;
-对象表达式;
-伴生对象;

2.对象声明

对象声明有利于组织代码和管理状态,尤其是管理整个应用运行生命周期内的某些一致性状态。

image.png

运行结果:

image.png

init初始化块只执行了一次,说明对象只创建了一次。

3.对象表达式

有时候你不一定非要定义一个新的命名类不可,也许你需要某个现有类的一种变体实例,但只需用一次就行了,事实上,对于这种用完就丢的类的实例,连命名都可以省了。这个对象表达式是某个类的子类,这个匿名类依然遵循object关键字的一个规则,即一旦实例化,该匿名类只能由唯一一个实例存在。

image.png

运行结果:

image.png

4.伴生对象

如果你想将某个对象的初始化和一个类实例捆绑在一起,可以考虑使用伴生对象,使用companion修饰符,你可以在一个类定义里声明一个伴生对象,一个类里只能有一个伴生对象。

image.png

5.嵌套类

如果一个类只对另一个类有用,那么将其嵌入到该类中并使这两个类保持在一起是合乎逻辑的,可以使用嵌套类。

image.png

运行结果:

image.png

6.数据类(关键字:data)

数据类,是专门设计用来存储数据的类。
数据类提供了toString的个性化实现。
==符号默认情况下,比较对象就是比较它们的引用值,数据类提供了equals和hashCode的个性化实现。

image.png

运行结果:

image.png

7.copy

除了重写Any类的部分函数,提供更好用的默认实现外,数据类还提供了一个函数,它可以用来方便的复制一个对象。假设你想创建一个Student实例,除了name属性,它拥有和另一个现有Student实例完全一样的属性值,如果Student是个数据类,那么复制现有Student实例就很简单了,只要调用copy函数,给想修改的属性传入值参就可以了。

image.png

运行结果:

image.png

8.结构声明

结构声明的后台实现就是声明component1、component2等若干个组件函数,让每个函数负责管理你想返回的一个属性数据,如果你定义一个数据类,它会自动为所有定义在主构造函数的属性添加对应的组件函数。

image.png
image.png

9.使用数据类的条件

正是因为上述这些特征,你才会倾向于用数据类来表示存储数据的简单对象,对于那些经常需要比较、复制或打印自身内容的类,数据类尤其适合它们。然而,一个类要成为数据类,也要符合一定条件。总结下来,主要有三个方面:
-数据类必须有主构造函数,该主构造函数至少带一个参数。
-数据类主构造函数的参数必须是val或var。
-数据类不能使用abstract、open、sealed和inner修饰符。

10.枚举类

枚举类,用来定义常量集合的一种特殊类。

image.png

运行结果:EAST

枚举类也可以定义函数


image.png

运行结果:Coordinate(x=15, y=19)

11.运算符重载(关键字:operator)

如果要将内置运算符应用在自定义类身上,你必须重写运算符函数,告诉编译器该如何操作自定义类。

image.png

运行结果:Coordinate(x=15, y=26)

常见操作符

image.png

12.代数数据类型

可以用来表示一组子类型的闭集,枚举类就是一种简单的ADT(代数数据类型)。

image.png

13.密封类(关键字:sealed)

对于更复杂的ADT,你可以使用Kotlin的密封类(sealed class)来实现更复杂的定义,密封类可以用来定义一个类似于枚举类的ADT,但你可以更灵活地控制某个子类型。
密封类可以有若干个子类,要继承密封类,这些子类必须和它定义在同一个文件里。

image.png

五.接口

1.接口定义

Kotlin规定所有的接口属性和函数实现都要使用override关键字,接口中定义的函数并不需要open关键字修饰,它们默认就是open的。

image.png

2.默认实现

只要你愿意,你可以在接口里提供默认属性的getter方法和函数实现。

image.png

六.抽象类

要定义一个抽象类,你需要在定义之前加上abstract关键字,除了具体的函数实现,抽象类也可以包含抽象函数——只有定义,没有函数实现。

image.png

七.定义泛型类

泛型类的构造函数可以接受任何类型。
MagicBox类指定的泛型参数由放在一对<>里的字母T表示,T是个代表item类型的占位符。MagicBox类接受任何类型的item作为主构造函数值(item:T),并将item值赋给同样是T类型的subject私有属性。

image.png

注意:泛型参数通常用字母T(代表应为type)表示,当然,想用其他字母,甚至是英文单词都是可以的。不过,其他支持泛型的语言都在用这个约定俗成的T,所以建议继续用它,这样写出来的代码别人更容易理解。

八.泛型函数

1.泛型函数

泛型参数也可以用于函数。
定义一个函数用于获取元素,当且仅当MagicBox可用时,才能获取元素。

image.png

运行结果:you find Jack

2.多泛型参数

泛型函数或泛型类也可以有多个泛型参数。

image.png

运行结果:Man(name='Jack', age='30')

九.泛型类型约束

1.泛型类型约束

如果要确保MagicBox里面只能装指定类型的物品,如Human类型,那就需要用到泛型类型约束了。

image.png

2.vararg关键字与get函数

MagicBox能存放任何类型的Human实例,但一次只能放入一个,如果需要放入多个实例,则入参需要用vararg关键字修饰。

image.png

3.[]操作符取值

想要通过[]操作符取值,可以重载运算符函数 get函数。

image.png

4.协变,关键字:out

out(协变),如果泛型类型作为函数的返回(输出),那么使用out,可以称之为生产类/接口,因为它主要是用来生产(produce)指定的泛型对象。

image.png

5.逆变,关键字:in

in(逆变),如果泛型类只将泛型类型作为函数的入参(输入),那么使用in,可以称之为消费者类/接口,因为它主要是用来消费(consume)制定的泛型对象。

image.png

6.不变,关键字:invariant

如果泛型类既将泛型类型作为函数参数,又将泛型类型作为函数的输出,那么既不用out也不用in(其实就是什么都不用加,默认就是invariant修饰)。

image.png

7.为什么使用in&out?

举个例子,我们定义了一个汉堡类对象,它是一种快餐,也是一种食物

image.png
image.png

当我们定义一个FoodStore类实现Production接口,泛型类型为Food,再定义一个FastFoodStore类,同样实现Production接口,泛型类型为FastFood(FastFood是Food的子类)

image.png

因为两个类都实现了Production接口,所以定义一个Production类型的对象,泛型类型都是Food类型,这时如果用FoodStore来初始化没问题,因为FoodStore类的泛型就是Food类型,如果用FastFoodStore来初始化呢?Java中子类泛型对象是不可以赋值给父类泛型对象的,但是Kotlin中,泛型使用out修饰就可以实现这种初始化

image.png

同理,定义Everybody类实现Consumer接口,泛型类型为Food,定义ModernPeople类,同样实现Consumer接口,泛型类型为FastFood

image.png

如果定义一个Consumer类型的对象,泛型为Burger,使用Everybody来初始化,在Java中是不可以的,因为Java中的父类泛型对象不能给子类泛型对象赋值,在Kotlin中使用in修饰泛型就可以了

image.png

综上,总结如下:
父类泛型对象赋值给子类泛型对象时,泛型需要使用in修饰。
子类泛型对象赋值给父类泛型对象时,泛型需要使用out修饰。
类比于Java,其实Java也有相应的方案来处理协变和逆变:

协变:List items,items可以用来读取其中的子元素,但是不能向items中写入数据(不能add),因为我们不确定哪个对象符合那个未知的Object
逆变:List items,items可以用来写入数据,但是不能读取

8. reified

有时候,你可能想知道某个泛型参数具体时什么类型,reified关键字能帮你检查泛型参数类型。Kotlin不允许对泛型参数T做类型检查,因为泛型参数类型会被类型擦除,也就是说,T的类型信息在运行时是不可知的,Java也有这样的规则

image.png

如果想对泛型参数做类型检查,可以使用reified来修饰泛型

image.png

需要注意的是,使用reified来修饰泛型,方法需要内联(使用inline修饰方法)

你可能感兴趣的:(Kotlin基础3——定义类、初始化、继承、对象、接口、抽象类、泛型)