虽然 Kotlin 推出了一段时间了,但是我一直持观望态度。之前一直觉得 Kotlin 只是在 Java 的基础上增加了一些新的特性,如果本质的性能问题没有提升,那么也没必要深入掌握这种语言。但最近因为开发的时候遇到空指针的判断问题,感觉 Java 的处理方式非常繁琐,所以希望诉诸于 Kotlin. 本次梳理的主要目标在于,
.kt
,编译之后还是生成 class 文件,只是编译器使用的是 kotlinc
(对应于 javac),执行 class 的时候还是使用 java;在 Kotlin 中文件名称和文件的内容没有关系(在 Java 中文件名和类名相同),并且文件内部定义的是函数还是类都没关系。比如,下面是定义在目录 me/shouheng/demo1/FirstDemo.kt
中的类和函数,这里类和函数处于文件的同一层次。另外,一个文件中还可以定义多个类和多个函数,都没有问题。
package me.shouheng
class Person (age : Int, name : String) // 声明了一个类
class Person2 (val age : Int, val name : String) // 声明了一个类
class Person3 (val age : Int, val name : String) { // 声明了一个类
var married : Boolean = false
// get() = married // 不允许会造成递归和栈溢出
val adult get() = age > 18
}
fun doSomething(person: Person) : Int {
// 在字符串中使用 “$+变量名” 的格式进行占位,相当于 "My name is" + persion + "!"
println("My name is $person!")
}
// “if else” 对应于 Java 中的 “? :” 三元运算符
fun max(a : Int, b : Int) : Int = if (a > b) a else b
fun min(a : Int, b : Int) = if (a < b) a else b
对于 Kotlin 来说,类和函数的真实包名是由文件中的 package 关键字指定的,与文件结构没有必然的关系。当然,我们建议按照 Java 的规则使其对应起来,因为这样维护起来更好、逻辑更清晰些。
上面是 Kotlin 中类和函数的定义的方式,总结下来区别有:
fun
,覆写的话就在函数名前面加上 override fun
;$+变量名
的格式进行占位(这叫字符串模板),如果希望使用美元符号,前面加上反斜杠即可;: Int
可省略,因为类型可以推断;age
和 name
而前者啥局部变量都没有;get()
和 set()
来重写 getter 和 setter 方法,但注意 Kotlin 中的类的字段兼有方法和字段两重含义,重写 get()
的时候再调用该字段会不断递归调用,造成栈溢出!(所以,一般情况下,使用默认的 get() 和 set() 逻辑即可,这是符合规范的,如果想增加新的逻辑,可以增加一个新的方法,就像 adult 字段一样。)再以下面的程序为例,可以总结如下,
args[0]
这样获取数组元素;var
和 val
两个关键字,系统可以自动推断类型,var
声明的变量可以二次赋值,而 val
不行,后者相当于 final
的;var
类型的变量可以二次赋值,但是两次赋值的类型必须相同;new
关键字(抛出异常的时候自然也一样);fun main(args : Array<String>) {
test(args)
}
fun test(args : Array<String>) {
val a = Person(10, "Ming")
var b: Person
b = Person(11, "Xing")
doSomething(a)
doSomething(b)
}
声明枚举的方式如下,也可以给枚举添加一些方法,方式同 Java.
enum class City {
BEIGING, SHANGHAI, GUANGZHOU
}
enum class City2(level:Int) {
BEIGING(1), SHANGHAI(2), GUANGZHOU(3)
}
类似于 Java 中的 switch,但是它的每个条件中默认加入了 break. 另外,它还有一个比较好的地方是,它会检查枚举是否都包含进去了,如果没全部包含,它会提示你全包含或者加入 else 语句。
// 示例 1
fun multiple(city: City2) = when(city) {
City2.BEIGING->{
2*10000
2+2
}
City2.SHANGHAI,City2.GUANGZHOU->3*10000
else -> 5
}
// 示例 2
interface Expr
class Num(val num : Int) : Expr
class Sum(val a : Int, val b : Int) : Expr
fun testExpr(e : Expr) = when(e) {
is Num -> e.num
is Sum -> e.a + e.b
else -> throw IllegalArgumentException("Invalid e")
}
另外,从示例 2 中可以看出,
is
来判断类型,类似于 instanceOf,并且判断完之后不需要强转就能直接使用;->
才行哦! val nums = 1..10
println(nums) // 输出结果是 1..10
for (num in nums) {
print(num)
} // 输出结果是 12345678910
for (i in 10 downTo 5 step 2) {
print(i)
} // 输出结果是 1086
for (i in 1 until 6) {
print(i)
} // 输出结果是 12345
val map = HashMap<Char, String>()
for (c in 'A'..'F') {
map[c] = Integer.toHexString(c.toInt())
}
for ((k, v) in map) { // 输出结果是
print("<$k,$v> ")
}
..
来得到一个区间,它默认是闭区间的;10 downTo 5 step 2
理解成一个整体,表示从 10 到 5 每次递减 2;1 until 6
理解成一个整体,表示从 1 到 6(不包含 6).try..catch
语句的基本结构如下,和 Java 基本相似,只是 catch 中声明变量的方式,下面的函数会当小于 0 时返回 -1,否则返回 1. 另外,kotlin 中不分受检异常和非受检异常,不会强制你捕获异常。
fun tryTest(i : Int) = try {
if (i < 0) throw IllegalArgumentException("< 0")
else 1
} catch (e : Exception) {
-1
}
函数是 Kotlin 中非常重要的概念,Kotlin 提供了许多便利的函数。
fun MyFun(a : String = "a", b : String) {
println("$a $b")
}
fun String.lastChar() : Char = this[length - 1] // 为 String 增加函数
val String.lastChar: Char
get() = get(length - 1) // 为 String 增加属性
fun varFun(vararg args : String) { // 缺省参数
for (arg in args) println(arg)
}
fun main(args : Array<String>) {
MyFun(a = "x", b = "y") // 输出 x y,允许指定参数的名称
MyFun(b = "y") // 输出 a y,使用默认的参数
println("ABC".lastChar()) // 使用拓展函数
println("ABC".lastChar) // 使用拓展属性
val args = arrayOf("A", "B", "C")
varFun(*args)
val map = mapOf(1.to("A"), 2 to("B"), 3 to "C", 4 to 5) // 中缀
for ((k, v) in map) {
print("<$k, $v> ")
} // 输出 <1, A> <2, B> <3, C> <4, 5>
}
a
;vararg
,也许是因为 ..
被当作其他用途了,当传入数组的时候的需要解包,也就是数组前面加 *
;Pair
键值对;toRegex()
方法才行;"""$"""
可以直接当作美元;public final
的,即公共且无法继承;interface IClickable {
fun onClick()
fun defaulFun() { // 默认函数,不需要任何声明
println("I'm defaulFun().")
}
fun defaulFun2() {
println("I'm defaulFun2().")
}
}
open class Handler1 : IClickable { // 实现接口,并且该类可以被继承
override fun onClick() {
println("Handler1#onClick()!")
}
override fun defaulFun() {
super.defaulFun() // 调用接口的实现
println("LaLaLa...")
}
inner class Innter {
fun doNothing() {
this@Handler1.onClick()
}
}
}
fun main(args : Array<String>) {
val handler = Handler1()
handler.onClick()
handler.defaulFun()
handler.defaulFun2()
}
super.
即可;super<接口>.方法
的形式调用;open
关键字即可;abstract
关键字的用法和 Java 一样;internal
表示模块内可见,protected
表示子类可见,private
表示类内可见,并且子类可见并不代表模块内可见,两个之间没有关系;inner
修饰的时候它是内部类,否则都属于静态内部类;this@外部类名称
访问外部类的方法和变量;sealed
来修饰,那么它的所有子类必须以内部类的形式定义(必须全部罗列在类内部);open class DemoClass constructor(val par1 : String, _par2 : String, val par3 : String = "", _par4 : String) {
val par2 = _par2
val par4 : String
init {
par4 = _par4
}
} // 构造方法和各种参数
class DemoClass2 private constructor() { } // private 的构造方法
class DemoClass3(val par : String) { } // 构造方法的另一种写法
class DemoClass4(v1: String) : DemoClass(v1, v1, v1, v1) // 覆写的时候
class DemoClass5 : DemoClass { // 覆写的时候和构造方法的其他写法
constructor(v : String) : super(v, v, v, v)
constructor(v : String, v2 : String) : super(v, v, v2, v2)
}
class DemoClass6 {
var par : Int = 0
get() = field + 1
set(value) {
field = value + 1
}
var par2 : Int = 0
private set // 修改默认访问权限
}
如上,可以
private
修饰使其成为私有构造方法;声明类的时候几个特殊的关键字(感觉这部分设计的几个关键字考虑的过于繁琐,因为只适用于具体的场景而失去了通用性)
// data 测试
class NormalClass(val name : String, val age : Int)
data class DataClass constructor(val name : String, val age : Int)
// test object
object ObjectClass {
const val filed = 10
fun testFun() = print("test fun")
}
// factory
class Instance private constructor(){
val name = "123"
companion object {
fun get() = Instance()
}
}
// with
fun getString() : String = with(LinkedList<Int>()) {
for (i in 1..10) {
this.add(i) // 这里的 this 就是上面传入的列表
}
this.toString()
}
// apply
fun getString2() : String = LinkedList<Int>().apply {
for (i in 1..10) {
add(i)
}
}.toString()
fun main(args : Array<String>) {
println(NormalClass("A", 10)) // me.shouheng.chapter3.NormalClass@2de80c
println(DataClass("A", 10)) // DataClass(name=A, age=10)
print(ObjectClass.testFun())
print(ObjectClass.filed)
println(Instance.get().name) //
}
data
表示这个类是数据类,会自动为其生成 toString()
, hashCode()
和 equals()
等方法,普通类跟 Java 中的 Object 一样;object
声明的类不允许有构造方法,其他和类一样,调用的时候使用类名的方式来调;(给人的感觉有些像类的静态方法和静态字段)companion
说明定义的实例是类的内部实例,比如上面可以访问私有构造方法;这种对象叫伴生对象,顾名思义就是伴随外部类生存的……with
表示以某个类作为开始,对其进行操作,最后返回;apply
对应于 with,表示对某个实例进行某种操作;(省去了声明一个实例的过程,仅此而已,但是新添加一个语法……)Kotlin 对空类型的处理比较好:默认所有的参数都是非空的,除非显式声明其可以为空。而 Java 中默认全部都是可空的。这可以有效帮助我们减少程序中的 NPE.
fun testFun1(param : String) {
print(param.length)
}
fun testFun2(param : String?) {
// print(param.length) // 编译器提示错误
}
fun main(args : Array<String>) {
// testFun1(null) // 编译器提示错误
testFun2(null)
val str : String? = null
println(str?.length) // null
println(str ?: "B") // B
val b = "AA".let { it + "A" } // AAA
println(b)
}
?
在类型的后面则说明这个变量是可空的;?.
,以 a?.method()
为例,当 a 不为 null 则整个表达式的结果是 a.method()
否则是 null;?:
,以 a ?: "A"
为例,当 a 不为 null 则整个表达式的结果是 a,否则是 “A”;as?
,以 foo as? Type
为例,当 foo 是 Type 类型则将 foo 转换成 Type 类型的实例,否则返回 null;!!
,用在某个变量后面表示断言其非空,如 a!!
;let
表示对调用 let 的实例进行某种运算,如 val b = "AA".let { it + "A" }
返回 “AAA”;lateinit
表示随后进行初始化;toXX()
方法;Any
和 Any?
分别是所有非空和空类型的超类;Unit
相当于 Java 中的 void,返回 Unit 就相当于返回 void;强化了函数式编程,从类文件结构中可以看出,把函数提升到和类同一高度
使用 Java 中的接口来理解函数式编程即可