《Kotlin核心编程》学习笔记
Part 0 背景
- 编程语言可以依靠功能累加来构建所谓的语法,同样也可以通过简单完备的理论来发展语言特性。
- Scala的设计理念是 more than Java(不止是Java),而Kotlin的设计理念是 better Java(更好的Java)
- Kotlin旨在成为一门提高Java生产力的更好的编程语言,实用性更强一点
Part 1 基础
- Unit是一个类型,而void只是一个关键字
- 单行表达式与等号的语法定义的函数叫表达式函数体,普通的函数声明叫做代码块函数体
if
是一个表达式,它的返回值是各个逻辑分支的相同类型或公共父类- 由于Kotlin支持子类型和继承,因此不能做到全局类型推导,所以一般还是要显示声明类型
var
即varible
,是变量,val
即value
,是var
+final
,是引用不可变的val
引用的数组内容可变,是因为Kotlin设计时更多考虑数组这种大型数据结构的拷贝成本,因此是存储在堆内存中的- 所谓“副作用”就是修改了某处的值,比如修改了某个外部变量的值,UI操作,IO操作等等。
- 应当优先使用
val
来避免一些副作用,而当在局部使用且使用安全时,可以用var
,这是一种防御性的编码思维模式,不可变即意味着更加容易推理 - 以其他函数作为参数或返回值的函数叫做高阶函数
- 通过
::
来实现对于某个类的方法进行引用 - Kotlin在JVM层设计了一个
Function
类型,包括Function0 Function1 ... Function22
的主要目的就是来兼容Java的Lambda表达式,后面的数字表示接收的参数数量,之所以是22是因为业界标准是这样,当然如果需要超过22个参数时,可以用FunctionN
- 不管是用
val
还是fun
,如果是单等号加花括号的语法,那么构建的就是一个Lambda表达式,Lambda的参数在花括号内部声明,使用时需要调用.invoke
或()
- 柯里化(Currying)是指把接受多个参数的函数转换为一些列仅接受单一参数的函数的过程,在返回最终结果值之前,前面的函数依次接受单个参数,然后返回下一个新函数。柯里化是为了简化Lambda演算理论中的函数接受多参数而出现的
- 表达式可以是一个值、操作符、函数或他们的组合,总的来说就是一个可以返回值的语句
- 当
if
作为表达式时,else
是必须被考虑的,因此表达式可以消除副作用的出现 - 一切表达式的设计让开发者在设计业务时,促进了避免创造副作用的逻辑设计,从而让程序变得更加安全
- 在Java中
Void
类类似于Integer
,Integer
是为了对基本类型int
的实例进行装箱操作,而Void
的设计则是为了对应void
。由于void
表示没有返回值,因此Void
类不能具有实例,它继承自Object
- 表达式更倾向于自成一块,具备很好的隔离性
try/catch/finally
表达式的返回类型由try/catch
决定- Kotlin中的判等性主要有两种类型:
==
和===
。其中,用==
来判断结构相等,也即字符串的内容相等。用===
来判断引用相等,与之相反的是!==
。如果比较的是在运行时的原始类型,比如int
,那么===
也等同于==
Part 2 进阶
1 类和对象
- 在Kotlin中可以通过构造方法参数指定默认值来实现方法重载。
- 构造方法的参数名前可以没有
var
和val
,如果带上了var
和val
,就等同于声明了一个同名的属性。 - 正常情况下,Kotlin规定类中的所有非抽象属性成员都必须在对象创建时被初始化值。
by lazy语法的特点如下:
- 该变量必须是引用不可变的,而不腻通过var来声明
- 在被首次调用时,才会进行赋值操作。一旦被赋值,后续他将不能被更改。
- lazy的背后是接受一个lambda并返回一个Lazy
实例的函数,第一次访问改属性时,会执行lazy对应的Lambda表达式并记录结果,后续访问改属性时只是返回记录的结果。 - 系统会给lazy属性加上同步锁,也就是LazyThreadSafetyMode.SYNCHRONLIZED,他在同一时刻只允许一个线程对lazy属性进行初始化,所以他是线程安全的。
Kotlin并不主张用Java中的构造方法重载,来解决多个构造参数组合调用的问题。取而代之的方案是,利用构造参数默认值及用
val
var
来声明构造参数的语法,,以更简洁的构造一个类对象。- 每个类最多存在一个主构造方法和多个从构造方法,如果主构造方法存在注解或可见性修饰符,也必须像从构造方法一样加上
constructor
。 - 每个从构造方法由两部分组成,一部分是对其他构造方法的委托,另一部分是由花括号包裹的代码块。执行顺序上会先执行委托的方法,然后执行自身代码块的逻辑。
- 子类应该尽量避免重写父类的非抽象方法,因为一旦父类变更方法,子类的调用很可能会出错,而且重写父类的非抽象方法也违背了面向对象设计原则中的“里氏替换原则”。
关于可见性修饰符:
- Kotlin中的默认修饰符与Java不同,Kotlin中是public,Java中是default
- Kotlin中有一个独特的internal
- Kotlin可以在一个文件内单独声明方法及常量,同样支持可见性修饰符
- Java中除了内部类可以用
private
修饰符,其他类都不允许使用,而Kotlin都可以 - Kotlin和Java中的
protect
修饰符的访问范围不同,Java中是包、类及子类可访问,而Kotlin只允许类及子类。
模块内可见是指该类只对一起编译的其他Kotlin文件可见,开发工程与第三方类库不属于同一个模块,这时吐过还想使用该类的话,只有复制源码一种方式了,这便是Kotlin种
internal
修饰符的一个作用提现- 用
val
声明的属性将只有getter
方法,因为他不可修改;而var
修饰的属性同时有getter
和setter
方法。 - 用
private
修饰的属性编译器将会省略getter
和setter
方法,因为在类外部已经无法访问他了,这两个方法也就没有了存在的意义。 - 内部类包含着其对外部类的引用,在内部类中我们可以使用外部类中的属性,而嵌套类不包含对其外部类实例的引用,所以他无法调用其外部类的属性。
copy
方法的主要作用就是帮我们从已有的数据类对象中拷贝一个新的的数据类型对象,在copy
的执行过程中,若未指定具体属性的值,那么新生产的对象的属性的值将使用被copy
对象的属性的值,这便是浅拷贝。- 解构是通过编译器约定时实现的,当然Kotlin对于数组的解构也有一定的限制,在数组中他默认最多允许赋值5个变量,因为若是变量过多,效果反而会适得其反。
static
修饰的内容是属于类的,而不是某个具体对象的,但定义的时候却跟普通的变量和方法混在一起,显得格格不入,所以Kotlin有了object
来替代。- 伴生是相较于一个类而言的,因此伴生跟Java中的
static
修饰效果性质一样,全局只有一个单例。他需要生命在类的内部,在类被装载时被初始化。 - 当你的匿名内部类使用的类接口只需要实现一个方法时,使用Lambda表达式更合适;当匿名内部类内有多个方法实现的时候,使用object表达式更合适。
2.代数数据类型
- 当我们利用类进行组合的时候,实际上就是一种product操作,积类型可以看作同时持有某些类型的类型。
- 和类型是类型安全的,他是一种闭环,是一种OR的关系
- 模式匹配就是表达式匹配
3.类型系统
- 由于
null
只能被存储在Java的引用类型的变量中,所以在Kotlin中基本数据类型的可空版本都会使用该类型的包装形式。 目前解决NPE问题的方式有三种:
- 用
try catch
捕获异常 - 用
Optional<T>
类似的类型包装 - 用
@NotNull/@Nullable
注解来标注
- 用
Kotlin在方法参数上标注了
@Nullable
,在实现上依旧是用if else
来对可空情况进行判断,这么做的原因是:- 兼容Java老版本
- 实现Java和Kotlin的互转换
- 在性能上达到最佳
Kotlin的可空类型实质上只是在Java的基础上进行了语法层面的包装。
Kotlin可空类型优于Java Optional的地方体现在:
- Kotlin可空类型兼容性更好
- Kotlin可空类型性能更好,开销更低
- Kotlin可空类型语法简洁
建议用 Either代替可空类型,Either只有两个子类型,Left和Right,如果Either[A,B]包含的是A的实例,那他就是Left实例,否则就是Right实例。
- 当且仅当Kotlin的编译器确定在类型检查后变量不会再改变,才会产生Smart Casts
- Java并不能在真正意义上被称作一门纯面向对象语言,因为他的原始类型的值和函数等并不能视作对象
- 在Kotllin的类型系统中,并不区分原始类型(基本数据、类型)和包装类型,我们使用的始终是同一个类型
- 与
Object
作为Java类层级结果的顶层类似,Any
类型是Kotlin中所有非空类型的超类,Any?
是Any
的父类型。 - 平台类型本质上就是Kotlin不知道可空性信息的类型,所有Java引用类型在Kotlin中都表现为平台类型。当在Kotlin中处理平台类型的值的时候,既可以被当做可空类型来处理,也可以被当做非空类型来处理。
- 继承 和 子类型化 其实是两个完全不同的概念。子类型化的核心是一种类型的代替关系,是一种类型语义的关系,继承则强调的是一种“实现上的复用”。
- Kotlin中的可空类型可以看做所谓的
Union Type
,近似于数学中的并集 Nothing
是没有实例的类型,Nothing
类型的表达式不会产生任何值。任何返回值为Nothing
的表达式之后的语句都是无法执行的,Kotlin中的return
throw
等返回值都是Nothing
- Kotlin中的
Int
等同于Java中的int
,而Int?
等同于Integer
,这种技巧让Kotlin更接近纯面向对象语言。 - 泛型出现的很重要的原因是为了能在代码编译的时候发现错误,而不是让错误的代码发布到生产环境中,也就是让类型更安全。
泛型有一下几点优势:
- 类型检查,能在编译时检查出错误
- 更加语义化
- 自动类型转换
- 能写出更加通用的代码
where
关键字能实现对泛型参数类型添加多个约束条件。- 数组是协变的,而List是不变的,也就是说。Object[]是所有其他对象数组的父类
- Java中的泛型是类型擦除的,也就是伪泛型,简单来说就是无法在运行时获取到具体的类型
- 泛型是指,对于类A和B,若A是B的父类,则A[]是B[]的父类
- 因为Kotlin中的数组是支持泛型的,所以Kotlin中数组就是不变的
- Java为了填补自己埋下的坑,即向后兼容,所以只能用类型擦除的方式来实现泛型。
- 类型检查是编译器在编译期就会进行的,所以类擦除不会影响到。
- 泛型的类型参数不是真正将类型擦除,还是会保留类型信息放到对应class的常量池中的。
- 匿名内部类在初始化的时候就会绑定父类或父接口与的相应信息,这样就能通过获取父类或父接口的泛型信息来实现我们的需求,Gson就是这样设计的。
- Kotlin的内联函数在编译时编译器会把相应函数的字节码插入调用的地方。
- 普通的泛型是不变的。但是如果在定义的泛型类和泛型方法的泛型参数前面加上
out
关键词,说明这个泛型类及泛型方法是协变的,这在Java里面是用通配符上界<? extends T>
来实现的 - 如果List支持协变,那么他将无法添加元素,只能从里面读取内容
- 类似于
out
,但又与out
完全对立,int
关键词可以将泛型变为逆变的,逆变的List则只支持添加而不支持读取,这在Java里面用通配符下界<? super T>
来实现
4.Lambda和集合
with
和apply
可以在写Lambda的时候省略多次书写对象名,内部用this
指代。fold
方法需要接收两个参数,第一个参数initial
是初始值,第二个参数operation
是一个函数,这个函数的参数有两个,第一个是上次调用该函数的结果(如果是第一次调用,则默认为初始值),第二个参数则是当前遍历到的集合元素,故fold
可以用作迭代。reduce
方法只接受一个参数,该参数为一个函数,具体的实现方式也跟fold
一样,只是没有初始值,因为默认初始值是集合的第一个元素。- 当我们仅仅需要对一个集合进行扁平化操作的时候,使用
flatten
就可以了,如果需要对其中的元素做一些加工,则需要考虑使用flatmap
HashSet
是用Hash散列来存放数据的,不能保证元素的有序性;而TreeSet
的底层结构是二叉树,他能保证元素的有序性。在不指定Set的具体实现时,我们一般说Set是无序的。- Kotlin的集合目前没有不可变集合,只能称为只读集合。只读列表在某些情况下是安全的,但并不总是安全的。
- Kotlin中的序列操作分为两类,一类是中间操作,一类是末端操作。
- 普通集合在进行链式操作的时候会先再List上调用中间操作产生一个结果列表,然后再在这个结果列表上应用下一个中间操作并产生下一个结果列表。而序列不一样,序列会将所有操作应用到一个元素上,也就是第一个元素执行完所有操作之后,第二个元素才会去执行。
- Kotlin的内联函数设计出来主要是为了优化Lambda表达式带来的开销。
- 内联函数不是万能的,在一些场合下应当避免使用内联函数:
- JVM对普通函数已经进行了内联优化,所以我们不用对普通函数加
inline
- 尽量避免对具有大量函数体的函数进行内联,这样会导致过多的字节码数量
- 一旦一个函数被定义为内联函数,便不能获取闭包类的私有成员,除非把他声明为
internal
- JVM对普通函数已经进行了内联优化,所以我们不用对普通函数加
- 内联函数的函数体及参数会直接代替具体的调用,所以其中的
return
相当于会直接打断原来的调用。 - Kotlin与Java一样会有类型擦除,所以不能直接获取一个参数的类型,然而内联函数会直接在字节码中生成相应的函数体的实现,所以这种情况下反而可以获得参数的具体类型,通常用
reified
来修饰这一效果。
5.多态与扩展
- 我们用一个子类继承一个父类的时候,就是子类型多态;另一种常见的多态是参数多态。另外,运算符重载又称为特设多态。
- 用子类型代替超类型,就是子类型多态。
- 最常见的参数多态就是泛型。
- 扩展属性和方法的实现运行在Class的实例上,而不会修改Class实例
- 特设多态可理解为,一个多态函数是有多个不同的实现,依赖于其实参调用相应版本的函数。
operator
的作用是将一个函数标记为重载一个操作符或者实现一个约定- 当扩展函数在一个类内部时,只能再该类和该类的子类汇总进行调用。
- 扩展函数和现有类的成员方法同时存在时,将会使用默认类的成员方法。
let
和apply
的区别在于,apply
返回的是原来的对象,而let
返回的是闭包里的值takeIf
与filter
类似,只是takeIf
只操作单条数据- 成员函数与扩展函数最重要的区别是,成员函数是动态调用的,而扩展函数是静态的
6.元编程
- 描述数据的数据是元数据,描述程序的数据就是程序的元数据,操作元数据的编程就是元编程。元编程可以用一句话概括:程序即是数据,数据即是程序。
- 元编程就像高阶函数一样,是一种更高阶的抽象,高阶函数将函数作为输入和输出,而元编程将程序本身输入或输出
- 目前主流的元编程实现方式有:
- 运行时通过API暴露程序信息
- 动态执行代码
- 通过外部程序实现
具体的实现如反射、宏、模板元编程、路径依赖类型