5.面向对象
1.类
申明一个类:
class Person{}
如果这个类没有方法,可以省略:
class Person
;都不用写,语法很简洁
2.构造函数
如果类中有一些属性,我们也可以这样申明一个类
class Person(var name: String,var age:Int)
这里的属性申明时,可以是var也可以是val,这里的(var name: String,var age:Int),叫做主构造函数,主构造其实就是为了我们像这样申明一个类,我们例子中省略了constructor,那如果我们要对这些参数做初始化呢?主构造里面是不支持任何代码逻辑的,只能通过初始代码块来完成初始化的操作:
class Person (var name: String,var age:Int){
var nameAge:String? = null
init {
nameAge = name+age;
}
}
当我们不想用这种主构造的时候,或者我们有多个构造函数时,就得使用到次构造函数,使用次构造时,必须要把参数传递给主构造
class Person (var name: String,var age:Int){
var nameAge:String? = null
init {
nameAge = name+age;
}
constructor(name: String, age: Int,from:String):this(name,age){
nameAge = nameAge()
}
fun nameAge() = "nameAge"
}
次构造里面可以直接调用其他函数,那主构造,初始化代码块和次构造执行的顺序是怎么样的?我们反编译下就明白了:
public final class Person {
@Nullable
private String nameAge;
@NotNull
private String name;
private int age;
public Person(@NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.age = age;
this.nameAge = this.name + this.age;
}
public Person(@NotNull String name, int age, @NotNull String from) {
Intrinsics.checkNotNullParameter(name, "name");
Intrinsics.checkNotNullParameter(from, "from");
this(name, age);
this.nameAge = this.nameAge();
}
}
撇开get,set我省略掉了,最终发编出来的代码,初始化代码块是包含在构造函数当中的,所以,当我们调用一个次构造函数,最终执行顺序是主构造->初始化代码块->次构造函数.
3.创建对象
kotlin里面省去了new关键字,创建对象,简单粗暴var person = Person("Pantheon",20)
4.继承
在第一章的时候简单介绍过一点kotlin的继承,kotlin里面默认创建出来的类都是不可变的,也就是都是由final修饰的,所以一个类如果需要被继承,需要加上open关键字,而接口则天生需要被实现,所以不需要如此:
interface My{
fun doSth()
}
open class MyClass(val sth: String){
fun doSth():String = "do${sth}"
}
class MyImpl:My{
override fun doSth() {
println("doSth")
}
}
class MyClassImpl(val mySth: String): MyClass(mySth) {
}
class MyClassConstruct:MyClass{
constructor():super("other thing")
}
而继承的语法在kotlin中也发生了改变,继承类或者接口都是用:来表示继承关系,对于构造函数的处理的思想则和java类似,只不过可以在主构造中传递参数.
在上述例子中,也演示了如何实现[覆盖]一个方法.也就是对于实现[覆盖]的方法,需要加上override关键字.
5.属性
kotlin因为其简洁的语法特性,导致原本很多比较简单的概念变的复杂起来,比如我们在构造方法中,提到的,主构造和次构造,属性这一个本来不复杂的内容,也因为它简洁的语法而变得复杂.
我们可以直接把属性写在构造方法中,可以是主构造也可以是次构造,最终编译器都会帮助我们生成属性字段,并且生成set,get方法:
class Person(var name: String,var age:Int)
我们在使用时,可以像访问public属性一样访问这些字段
val person = Person("xxxx", 10)
println(person.name)
如果我们需要在body体内自己定义属性:
class Person{
var name:String? = null;
var age:Int = 18;
}
如果创建的时候就知道值是多少,可以用不可空变量申明,如果不知道值是多少,那就用可空变量来什么.
kotlin的set,get方法比较奇葩,语法类似c#
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
举个例子:
class Person{
private var name:String? = null
get() = name
set(value) {
field = parse(value)
}
var age:Int = 18;
}
这个field叫做back field,其实就是指代当前的这个属性,如果用下面这种方式赋值,会陷入无止境的递归
class Person{
private var name:String?
get() = name
set(value) {
name = parse(value)
}
var age:Int = 18;
}
fun main(vararg args:String) {
val person = Person()
person.name = "pppp"
println(person.name)
}
反编译下:
public final class Person {
private int age = 18;
@Nullable
public final String getName() {
return this.getName();
}
public final void setName(@Nullable String value) {
this.setName(value);
}
public final int getAge() {
return this.age;
}
public final void setAge(int var1) {
this.age = var1;
}
}
可以看到最终get方法生成的方法是去掉自己,而set也是掉自己,这个时候我们如果想给属性赋值,只能用back filed,这个和c#的思想是一致的.
还有一个隐藏属性的backing property感觉用处不大,不介绍了.
我们再刚刚介绍说,如果一个字段是可空的,需要用可空类型来申明,并且赋上一个默认null值,那我们想一下这个常见,在IOC中,我们注入的某一个属性,在申明上,它没有默认值,是可空类型,但是使用上,我们知道,一般不会空的,如果我们用可空类型来申明,那得写一堆?,非常不美观,这里就出现了lateinit 关键字:
class Person{
lateinit var name:String
}
用lateinit申明的属性,可以是非空类型,且没有默认值.但是我们想一下,如果我创建完了,不复制,那不一样会报空指针?
fun main(vararg args:String) {
val person = Person()
println(person.name)
}
报错如下:
Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property name has not been initialized
我们可以通过如下方式,判断字段是否初始化
val person = Person()
if (person::name.isInitialized1){
println("-----")
}
lateinit其实在生成字节码时,只在get方法上加了个是不是空值的判断,是空,抛出异常,不是则返回值.
6.代理模式
设计模式中有一个很重要的设计模式,叫做代理设计模式,代理类和实际类都实现同一个接口,访问代理对象的方法时,再去访问实际对象的方法,在kotlin中,用一个by关键字实现了这个逻辑:
interface DoSth{
fun doSth();
}
class DoSthImpl:DoSth {
override fun doSth() {
println("DoSthImpl")
}
}
class DoSthProxy(sth: DoSth):DoSth by sth ;
fun main() {
var proxy = DoSthProxy(DoSthImpl())
proxy.doSth()
}
代理对象持有实际对象的引用,通过主构造函数传入,就可以很快速的出代理模式,代理类可以重写代理方法,但是没有办法获取到实际对象的引用(虽然传入了)
class DoSthProxy(sth: DoSth):DoSth by sth {
override fun doSth() {
println("proxy")
}
}
代理类感觉用处不算大,实际使用中,以改变字节码的代理技术使用居多,而且代理对象还不能调用实际对象的方法,要么就重写,要么就只是做代理,个人感觉功能很弱.
7.代理属性
代理属性说的是这么一个功能,假如类里面有个属性,我们再访问它或者改变它的值时,可以通过代理来做一些事情,比如lazy赋值,当我们首次访问时,通过lazy后面的表达式来赋值
class Lazy{
val name:String by lazy {
println("computed!")
calName()
}
fun calName():String = "name"
}
lazy属性需要申明为不可变类型val,上面这段代码name属性的初始值则由calName来提供,接近于java的init方法,但是比init方法要复杂.by后面也可以跟个对象,代表该属性的初始值由该对象的某个方法来提供,看下这段代码:
class DelegateDemo{
var delegate1:MyDelegate by Delegate()
var delegate2:MyDelegate by Delegate()
}
class MyDelegate
class Delegate{
operator fun getValue(delegateDemo: DelegateDemo, property: KProperty<*>): MyDelegate {
println("Delegate getValue:${property.name}")
return MyDelegate()
}
operator fun setValue(delegateDemo: DelegateDemo, property: KProperty<*>, myDelegate: MyDelegate) {
println("Delegate setValue:${property.name}")
}
}
fun main() {
var demo = DelegateDemo();
demo.delegate1
demo.delegate2
demo.delegate2 = MyDelegate()
}
运行的结果如下:
Delegate getValue:delegate1
Delegate getValue:delegate2
Delegate setValue:delegate2
也就是说,代理属性如果由某个对象提供,其实就是由这个对象提供了该属性的get,set方法,也就是该类需要定义两个方法,getValue和setValue.
代理属性也可以将自己的值代理给别的属性,也就是自己的值,由别的属性来提供:
var topLevelInt: Int = 10
class ClassWithDelegate(val anotherClassInt: Int)
class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
var delegatedToMember: Int by this::memberInt
var delegatedToTopLevel: Int by ::topLevelInt
val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}
var MyClass.extDelegated: Int by ::topLevelInt
因为by后面跟的是表达式,所以这里是获取的函数引用,那哪些属性可以被代理呢:
- 全局变量,或者是同类的其他属性
- 其他类的其他属性
还有一个功能是观察属性,比如属性中的值发生了变化,可以获取到值得变化:
class Lazy{
var name:String by Delegates.observable("name"){
property, oldValue, newValue ->
println("${property.name} has changed,before:${oldValue},now:${newValue}")
}
}
fun main() {
val lazy = Lazy();
lazy.name = "Pan"
}
Delegates.observable有两个参数,第一个是初始值,第二个是个lambda表达式,执行的结果:
name has changed,before:name,now:Pan
代理属性的功能还远远不止这么些,还有map代理属性,本地代理属性等等功能,似乎这些功能在实际编码中用的不多,不做过多介绍了,有兴趣可以看看官网的介绍.
8.SAM
SAM这个东西有点类似于java的lambada定义的functional interface,定义如下:
fun interface Even{
fun invoke(num:Int):Boolean
}
fun main() {
var even:Even = Even { it % 2 == 0 }
even.invoke(10)
}
其实功能和函数类型有点冲突,而且还得特地定义出一个接口出来,感觉用处不是很大.
9.Data class
data class类似于java14中的record class,专门用于承载数据,和普通的class相比有以下的几个区别:
- 自动重写
equals和hashcode方法 - 重写了
toString方法,只输出主构造的属性 - 提供了
component1-componentN的方法,用于获取属性,获取的顺序和属性申明的顺序一致,这些方法不能被直接调用,如果有回忆之前讲函数多返回时,就用的data class,kotlin就是根据component1-componentN这些方法来推断出返回的值和类型. - 提供了一个
copy方法 - 主构造必须含有一个参数,并且必须标明是
val还是var类型,这在普通的class里面是不必要的 - 不能由这些关键字修饰
abstract, open, sealed, or inner.
申明一个data class,并创建实例
data class Person(val name: String) {
var age: Int = 0
}
fun main() {
val person = Person("Pantheon").apply { age = 10 }
val copy = person.copy(name = "Lucy")
println(copy)
}
data class的属性可以是在主构造中也可以是在body体内,输出结果
Person(name=Lucy)
10.sealed class
密封类的特性是这样,被sealed修饰的类或者接口,在最早的版本中,不能在该类申明的包以外被继承和实现,比如我引用了一个第三方类库的包中包含sealedclass,我想继承它,是不行的,所以才有了密封类的说法,它除了不让其他人实现外,还有一个比较实用的功能就是类似于java中enum的使用,但是比enum功能强大:
sealed class Week(val day: String) {
class Monday : Week("monday");
object Tuesday : Week("tuesday");
object Thursday : Week("thursday") {
override fun today() {
println("hah...today is $day")
}
}
class Friday : Week("friday") {
fun friday() {
println("saturday is coming.....")
}
}
open fun today() {
println("today is $day")
}
}
fun main() {
Week.Monday().today()
Week.Tuesday.today()
Week.Thursday.today()
Week.Friday().friday()
}
-------------------
today is monday
today is tuesday
hah...today is thursday
saturday is coming.....
从上面代码可以看到,密封类里的对象和enum区别在于,enum对象永远只有一个,而密封类里面可以是class,也可以是object,是可以创建出多个对象,而继承密封类的实现,也可以自己定义的方法,通过 Week.Friday()方法调用返回的类型是Friday类型,而不是Week,所以可以调用friday()这个方法,这和enum是最大的区别,因为enum获取的实例就是enum定义出来的类型.
密封类还可以和when和is配合使用,接近于java中对enum的Switch case判断:
fun whatDay(week: Week) = when(week){
is Week.Monday -> println("Monday")
is Week.Friday -> println("Friday")
Week.Thursday -> println("Thursday")
Week.Tuesday -> println("Tuesday")
}
fun main() {
whatDay(Week.Thursday)
}
11.泛型(in,out,where)
要说kotlin的泛型,不得不先说java的泛型,java泛型因为在运行的时候会丢失泛型,所以为了防止发生类型安全的问题,java的泛型是不允许协变的
List<String> lists = new ArrayList<>();
//编译错误 泛型不能协变
List<Object> objects = lists;
假如我们就是有需要这样的赋值的操作呢?这个时候就要用到两个关键字,一个extends,一个super
List<? super Object> lists = new ArrayList<>();
//编译通过
List<Object> objects = lists;
java里面把对泛型变量的操作划分成了两块,一个是读操作,一个是写操作,使用extends的泛型,只能读,不能写,而super关键字只能写,不能读
List<? extends Object> objects = new ArrayList<>();
//编译报错,extend只能用在读的场景
objects.add("1111");
//编译正确
Object o = objects.get(0);
List<? super Fruit> objects = new ArrayList<>();
objects.add(new Apple());
//error,类型只能是object类型
Fruit object = objects.get(0);
PECS原则
如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)
如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)
这两个关键字就刚刚对应kotlin里面的in,out关键字,in代表添加数据,也就是对应的是super关键字,而out则代表获取数据,对应的是extend关键字
open class Fruit;
class Apple:Fruit();
var list:ArrayList<in Fruit> = ArrayList()
//error
val fruit:Fruit = list.get(0)
//ok
list.add(Apple())
var list:ArrayList<out Fruit> = ArrayList()
//ok
val fruit:Fruit = list.get(0)
//error
list.add(Apple())
当泛型类型有多个时,需要有到where关键字
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
where T : CharSequence,
T : Comparable<T> {
return list.filter { it > threshold }.map { it.toString() }
}
12.object 表达式
kotlin中,如果对于一些只需要创建一次的对象,或者对于一些不需要复用的实现,可以用object关键字来申明和创建该对象,比如:
fun main() {
var pantheon = object {
var name:String = "Pantheon";
fun sayHello(){
println("hello,$name")
}
}
pantheon.sayHello()
}
这个例子可以让我们看到object的背后运作机制,它不仅仅是创建了一个匿名类,而且还用这个匿名类直接给我们创建出来了一个对象.
object也可以去实现某些特定的接口:
var pantheon = object:ObjectInterface {
var name:String = "Pantheon";
fun sayHello(){
println("hello,$name")
}
override fun doSth() {
println("doSth")
}
}
pantheon.sayHello()
pantheon.doSth()
13. companion object
kotlin中没有static关键字,class的static 方法和static属性由companion object来提供,比如Java中
public class StaticClass {
public static final String name = "Pantheon";
public static void sayHello(){
System.out.println("Hello "+name);
}
}
它的写法在kotlin中就等价于:
class StaticClass{
companion object{
val name = "Pantheon"
fun sayHello(){
println("Hello,$name")
}
}
}
之前在拓展函数中也讲过,类的静态拓展方法是需要加在companion object,在receiver申请处需要声明成class.companion.