一.定义类
1.field
针对定义的每一个属性,Kotlin都会生成一个field、一个getter、以及一个setter,field用来存储属性数据,你不能直接定义field,Kotlin会封装field,保护它里面的数据,只暴露给getter和setter使用。属性的getter方法决定你如何读取属性值,每个属性都有getter方法,setter方法决定你如何给属性赋值,所以只有可变属性才会有setter方法,尽管Kotlin会自动提供默认的getter和setter方法,但在需要控制如何读写属性时,也可以自定义它们。
定义一个Player类,自定义(重写)属性的setter和getter方法
2.计算属性
计算属性是通过一个覆盖的get或set运算符来定义,这时field就不需要了。
3.防范竞态条件
如果一个类属性既可空又可变,那么引用它之前你必须保证它非空,一个办法是用also标准函数。
二.初始化
1.主构造函数
我们在Player类的定义头中定义一个主构造函数,使用临时变量为Player的各个属性提供初始值,在Kotlin中,为便于识别,临时变量(包括仅引用一次的参数),通常都会以下划线开头的名字命名。
2.在主构造函数里定义属性
Kotlin允许你不使用临时变量赋值,而是直接用一个变量同时指定参数和类属性,通常,我们更喜欢用这种方式定义类属性,因为他会减少重复代码。
3.次构造函数(关键字:constructor)
有主就有次,对应主构造函数的就是次构造函数,我们可以定义多个次构造函数来配置不同的参数组合。
4.使用次构造函数,定义初始化代码逻辑。
5.默认参数
定义构造函数时,可以给构造函数参数指定默认值,如果用户调用时不提供值参,就使用这个默认值。
6.初始化块(关键字:init)
初始化块可以设置变量或值,以及执行有效性检查,如检查传给某构造函数的值是否有效,初始化块代码会在构造类实例时执行。
7.初始化顺序
1.主构造函数里声明的属性;
2.类级别的属性赋值;
3.init初始化块里的属性赋值和函数调用;
4.次构造函数里的属性赋值和函数调用;
8.延迟初始化
使用lateinit关键字相当于做了一个约定:在用它之前由开发人员负责初始化
如果无法确认lateinit变量是否完成初始化,可以执行isInitialized进行检查
9.惰性初始化
延迟初始化并不是推后初始化的唯一方式,你也可以暂时不初始化某个变量,直到首次使用它,这个叫作惰性初始化。
10.初始化陷阱一
在使用初始化块时,顺序非常重要,你必须保证块中的所有属性已经完成初始化。
错误代码:
正确代码:
11.初始化陷阱二
这段代码编译没有问题,因为编译器看到name属性已经在init块里初始化了,但代码一运行就会抛出空指针异常,因为name属性还没有赋值,firstLetter函数就应用了它了。
12.初始化陷阱三
因为编译器看到所有属性都初始化了,所以代码编译没问题,但运行结果却是null,问题出在哪了?在用initPlayerName函数初始化playerName时,name属性还未完成初始化,只需要将playName和name位置互换即可运行正常。
错误代码:
正确代码:
三.继承
1.继承
类默认都是封闭的,要让某个类开放继承,必须使用open关键字修饰它。
2.函数重载
父类的函数也要以open关键字修饰,子类才能覆盖它。
3.类型检测
Kotlin的is运算符是个不错的工具,可以用来检查某个对象的类型。
运行结果:
4.Kotlin层次
无需再代码里显示指定,每一个类都会共同继承一个叫Any的超类
5.类型转换
as操作符声明,这是一个类型转换
6.智能类型转换
Kotlin编译器很聪明只要能确定any is 父类条件检查属实,它就会将any当作子类类型对待,因此,编译器允许你不经类型转换直接使用。
四.对象
1.object关键字
使用object关键字,你可以定义一个只能产生一个实例的类——单例。
使用obect关键字有三种方式:
-对象声明;
-对象表达式;
-伴生对象;
2.对象声明
对象声明有利于组织代码和管理状态,尤其是管理整个应用运行生命周期内的某些一致性状态。
运行结果:
init初始化块只执行了一次,说明对象只创建了一次。
3.对象表达式
有时候你不一定非要定义一个新的命名类不可,也许你需要某个现有类的一种变体实例,但只需用一次就行了,事实上,对于这种用完就丢的类的实例,连命名都可以省了。这个对象表达式是某个类的子类,这个匿名类依然遵循object关键字的一个规则,即一旦实例化,该匿名类只能由唯一一个实例存在。
运行结果:
4.伴生对象
如果你想将某个对象的初始化和一个类实例捆绑在一起,可以考虑使用伴生对象,使用companion修饰符,你可以在一个类定义里声明一个伴生对象,一个类里只能有一个伴生对象。
5.嵌套类
如果一个类只对另一个类有用,那么将其嵌入到该类中并使这两个类保持在一起是合乎逻辑的,可以使用嵌套类。
运行结果:
6.数据类(关键字:data)
数据类,是专门设计用来存储数据的类。
数据类提供了toString的个性化实现。
==符号默认情况下,比较对象就是比较它们的引用值,数据类提供了equals和hashCode的个性化实现。
运行结果:
7.copy
除了重写Any类的部分函数,提供更好用的默认实现外,数据类还提供了一个函数,它可以用来方便的复制一个对象。假设你想创建一个Student实例,除了name属性,它拥有和另一个现有Student实例完全一样的属性值,如果Student是个数据类,那么复制现有Student实例就很简单了,只要调用copy函数,给想修改的属性传入值参就可以了。
运行结果:
8.结构声明
结构声明的后台实现就是声明component1、component2等若干个组件函数,让每个函数负责管理你想返回的一个属性数据,如果你定义一个数据类,它会自动为所有定义在主构造函数的属性添加对应的组件函数。
9.使用数据类的条件
正是因为上述这些特征,你才会倾向于用数据类来表示存储数据的简单对象,对于那些经常需要比较、复制或打印自身内容的类,数据类尤其适合它们。然而,一个类要成为数据类,也要符合一定条件。总结下来,主要有三个方面:
-数据类必须有主构造函数,该主构造函数至少带一个参数。
-数据类主构造函数的参数必须是val或var。
-数据类不能使用abstract、open、sealed和inner修饰符。
10.枚举类
枚举类,用来定义常量集合的一种特殊类。
运行结果:EAST
枚举类也可以定义函数
运行结果:Coordinate(x=15, y=19)
11.运算符重载(关键字:operator)
如果要将内置运算符应用在自定义类身上,你必须重写运算符函数,告诉编译器该如何操作自定义类。
运行结果:Coordinate(x=15, y=26)
常见操作符
12.代数数据类型
可以用来表示一组子类型的闭集,枚举类就是一种简单的ADT(代数数据类型)。
13.密封类(关键字:sealed)
对于更复杂的ADT,你可以使用Kotlin的密封类(sealed class)来实现更复杂的定义,密封类可以用来定义一个类似于枚举类的ADT,但你可以更灵活地控制某个子类型。
密封类可以有若干个子类,要继承密封类,这些子类必须和它定义在同一个文件里。
五.接口
1.接口定义
Kotlin规定所有的接口属性和函数实现都要使用override关键字,接口中定义的函数并不需要open关键字修饰,它们默认就是open的。
2.默认实现
只要你愿意,你可以在接口里提供默认属性的getter方法和函数实现。
六.抽象类
要定义一个抽象类,你需要在定义之前加上abstract关键字,除了具体的函数实现,抽象类也可以包含抽象函数——只有定义,没有函数实现。
七.定义泛型类
泛型类的构造函数可以接受任何类型。
MagicBox类指定的泛型参数由放在一对<>里的字母T表示,T是个代表item类型的占位符。MagicBox类接受任何类型的item作为主构造函数值(item:T),并将item值赋给同样是T类型的subject私有属性。
注意:泛型参数通常用字母T(代表应为type)表示,当然,想用其他字母,甚至是英文单词都是可以的。不过,其他支持泛型的语言都在用这个约定俗成的T,所以建议继续用它,这样写出来的代码别人更容易理解。
八.泛型函数
1.泛型函数
泛型参数也可以用于函数。
定义一个函数用于获取元素,当且仅当MagicBox可用时,才能获取元素。
运行结果:you find Jack
2.多泛型参数
泛型函数或泛型类也可以有多个泛型参数。
运行结果:Man(name='Jack', age='30')
九.泛型类型约束
1.泛型类型约束
如果要确保MagicBox里面只能装指定类型的物品,如Human类型,那就需要用到泛型类型约束了。
2.vararg关键字与get函数
MagicBox能存放任何类型的Human实例,但一次只能放入一个,如果需要放入多个实例,则入参需要用vararg关键字修饰。
3.[]操作符取值
想要通过[]操作符取值,可以重载运算符函数 get函数。
4.协变,关键字:out
out(协变),如果泛型类型作为函数的返回(输出),那么使用out,可以称之为生产类/接口,因为它主要是用来生产(produce)指定的泛型对象。
5.逆变,关键字:in
in(逆变),如果泛型类只将泛型类型作为函数的入参(输入),那么使用in,可以称之为消费者类/接口,因为它主要是用来消费(consume)制定的泛型对象。
6.不变,关键字:invariant
如果泛型类既将泛型类型作为函数参数,又将泛型类型作为函数的输出,那么既不用out也不用in(其实就是什么都不用加,默认就是invariant修饰)。
7.为什么使用in&out?
举个例子,我们定义了一个汉堡类对象,它是一种快餐,也是一种食物
当我们定义一个FoodStore类实现Production接口,泛型类型为Food,再定义一个FastFoodStore类,同样实现Production接口,泛型类型为FastFood(FastFood是Food的子类)
因为两个类都实现了Production接口,所以定义一个Production类型的对象,泛型类型都是Food类型,这时如果用FoodStore来初始化没问题,因为FoodStore类的泛型就是Food类型,如果用FastFoodStore来初始化呢?Java中子类泛型对象是不可以赋值给父类泛型对象的,但是Kotlin中,泛型使用out修饰就可以实现这种初始化
同理,定义Everybody类实现Consumer接口,泛型类型为Food,定义ModernPeople类,同样实现Consumer接口,泛型类型为FastFood
如果定义一个Consumer类型的对象,泛型为Burger,使用Everybody来初始化,在Java中是不可以的,因为Java中的父类泛型对象不能给子类泛型对象赋值,在Kotlin中使用in修饰泛型就可以了
综上,总结如下:
父类泛型对象赋值给子类泛型对象时,泛型需要使用in修饰。
子类泛型对象赋值给父类泛型对象时,泛型需要使用out修饰。
类比于Java,其实Java也有相应的方案来处理协变和逆变:
协变:List <? extends Object> items,items可以用来读取其中的子元素,但是不能向items中写入数据(不能add),因为我们不确定哪个对象符合那个未知的Object
逆变:List<? super String> items,items可以用来写入数据,但是不能读取
8. reified
有时候,你可能想知道某个泛型参数具体时什么类型,reified关键字能帮你检查泛型参数类型。Kotlin不允许对泛型参数T做类型检查,因为泛型参数类型会被类型擦除,也就是说,T的类型信息在运行时是不可知的,Java也有这样的规则
如果想对泛型参数做类型检查,可以使用reified来修饰泛型
需要注意的是,使用reified来修饰泛型,方法需要内联(使用inline修饰方法)