Kotlin学习这个系列文章是我在阅读了 《Kotlin In Action》 书籍之后,按照书籍中的目录结构写的。有几点需要说明一下:
首先我们先看一段代码
//Kotlin
fun main(array: Array<String>) {
println("Hello,world!")
}
//Java
void main(String[] array) {
System.out.println("Hello,world!");
}
俗话说的好学习一门语言都从输出 “Hello,world!” 开始。从这一段代码中我们就可以看到Kotlin的很多特性和语法了
下面我们用一张标注图来表示Kotlin函数的基本结构:
很直观的可以看出跟Java的函数结构几乎是一样的。都包括这几部分:
老司机们一看这个结构就能够和java关联起来了,so easy嘛。wait!
return if (a > b) a else b
是什么鬼啊?!怎么跟Java中的三元操作符 ?: 一样?这就要说到Kotlin和Java中 语法 和 表达式 的区别了。
语法和表达式
语法和表达式的区别在于,表达式是有值的,并且可以嵌套在另一个表达式中;而语法是对代码块的封装,本身是没有值的。对于Kotlin来说除了循环(for、do和do/while)以外的控制结构都是表达式(例如上面代码中的if),而Java中所有的控制结构都是语法。(需要特别注意的是,在Java中赋值操作是表达式,而Kotlin中则是语法。这样做是为了避免和比较操作符的混淆)后面会经常看到控制结构和其他表达式结合在一起的情况。
对于上面的函数我们可以对它简化成下面的样子:
fun max(a: Int, b: Int): Int = if (a > b) a else b
因为这个函数体只有一个表达式,所以可以直接用表达式来当函数体同时可以省略掉花括号和return。这样子看起来会非常的简洁。一般的如果函数体在花括号里面,就说这个函数有 代码块体。如果它直接返回了一个表达式,就说它有 表达式体。
在进一步简化,可以写成下面的代码:
fun max3(a: Int, b: Int) = if (a > b) a else b
这次直接把返回类型给去掉了。这里就设计到Kotlin的智能转换了,后面会讲到。同时注意只有 表示式体函数 才能把返回类型省略, 代码块体函数 必须显式写出具体的返回类型。
说完函数,咱们再来说说Kotlin中的变量。照例先来看代码
val question = "The Ultimate Question of Life, the Universe, and Everything"
val answer = 42
// val answer: Int = 42
var yearsToCompute = 7.5e6 //7.5*10^6 = 7500000.0
通过比较 question 和 yearsToCompute 这两个变量,我们发现存在两个修饰符,var,val,这在Kotlin中代表可变变量和不可变变量。
Kotlin中大部分时候 推荐使用val来声明变量,仅在必要的时候换成var 。通常情况下,使用 不可变引用,不可变集合及无副作用的函数 会让你的代码更加的接近函数式编程风格。
上面说到 val 声明的变量不能在初始化之后赋值,但是如果编译器能够保证 在变量声明周期内只有一条初始化语句 ,那就可以通过不同的条件来初始化它比如:
val message: String
if (canPerformOperation()) {
message = "Success"
} else {
message = "Failed"
}
注意,尽管val是不可变的,但是它指向的对象可能是可变的。例如:
val languages = arrayListOf("Java")
languages.add("Kotlin")
这段代码的意思就是声明了一个不可变变量,并指向了一个ArrayList集合的内存地址。其中变量指向的ArrayList这个集合容器的地址是不能修改的,但是里面具体的元素对应的内存地址是可以修改的。
接下来我们比较一下 question 和 answer 这两个变量,他们的初始化值分别是String类型的和Int类型的,但是他们都没有声明对应的类型。这个和表达式体函数一样,编译器会分析初始化器表达式的值,并把表达式的类型作为变量的类型。如果显式的表达的话就是第三个变量的声明方式。这让Kotlin的变量变的像JS这种动态语言一样,可以 省略变量类型/函数返回值类型 ,显的格外的简洁。
需要注意的是
var answer = 42
answer = "no answer" //wrong
在初始化之后,变量的类型是不能够被改变的。
在了解了Kotlin的函数和变量之后,我们在来学习一下Kotlin的新特性,字符串模板。
在平时开发的过程中肯定会经常遇到在一段字符串中几个值需要动态的去设置的。这个时候我们会通过这几种方式来处理:
void printValue(String value) {
System.out.print("value:" + value);
//或者
System.out.print(String.format("valur:$s" + value));
}
可以说是信手捏来,但是这两种方式都会给人一种割裂感,那Kotlin中是如果处理的呢。直接看代码:
fun printValue(value: String) {
println("value:${value}")
}
是不是感觉紧凑多了,直接在双引号里面就完成了字符串的动态添加。这个在Kotlin中就叫做 字符串模板 。
使用也非常的方便只需要在字符串中通过 在变量(或者是表达式)前添加 $ 符号,就可以引用了。需要注意的是如果想要输出 $ 符号,需要通过反斜杠 \ 来转义。
这一小节中,让我们大致了解一下Kotlin中的类(在后面文章中会有详细的介绍),同时引出Kotlin中 属性 这个概念,话不多说直接上代码。
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
这是一个Java中很常见的Bean类,里面只有一个私有的final类型的变量name,一个构造函数和一个name对应的get方法。这其中很多都是样板代码,比如说对name字段的赋值,对应的get方法等等,那么在Kotlin中要代表这样一个类需要怎么表达呢?
class Person(val name: String)
对,就是这么简单,一行代码就可以表达Java中需要九行代码才能表达的意思了。通常我们把这种只有数据没有其他代码的类叫做 值对象。注意在Kotlin中类默认是public的,所以默认可以省略。
Kotlin中在类后面括号的部分叫做 主构造器,这部分概念后面文章会详细说。
在上面Person类中,像name这种字段和其对应的get方法(还有set方法,一起叫做访问器方法)这种组合通常被叫做属性。在Kotlin中 属性是头等的语言特性 ,完全代替了字段和访问器方法。声明属性也是用 val 和 var 关键字,分别表示 不可变属性 和 可变属性 。
在前面我们说到变量,在用法上跟属性一毛一样,可能有人会问了这两个有什么区别,在我理解看来其实这两个概念只是对应的范畴不同,你可以说声明了一个变量,也可以说声明了一个属性。变量对应的是常量,属性则是对字段和访问器方法这种组合的叫法。需要说明的是在Kotlin中也存在 字段 ,叫做 Backing Field(在后面章节会详细的说),但是它 只存在于访问器 中,其他任何地方都没有 字段 这个概念。
首先我们来看一下声明一个完整属性的语法:
var <PropertyName>[: <PropertyType>] [= <Property_initializer>]
[<getter>]
[<setter>]
其中Property_initializer,getter,setter都是可选的,如果初始化器可以推断属性的类型,那么PropertyType也是可以省略的。
接下来我们看一下Kotlin和Java中分别该如何使用 属性 。我们先声明一个Person类:
class Person(
val name: String, //只读属性:编译器会对应的生成一个字段和一个对应的getter方法
var isMarried: Boolean //可写属性:编译器会生成一个字段和对应的getter,setter方法
)
如果把这段代码编译成字节码,和Java中同样的Bean类编译成字节码,这两者几乎没有什么差别。话不多说,让我们来看一下Java中和Kotlin中如何调用这个Person类的。
//Java
Person person = new Person("Bob", true);
System.out.print(person.getName());
System.out.print(person.isMarried());
//Kotlin
val person = Person("Bob", true)
print(person.name)
print(person.isMarried)
从这段代码中可以得出下面一些结论:
如果我需要统计一个变量的访问次数或者修改次数,或者对设置的值做一个校验,在Kotlin中该如何实现呢?这个时候就需要自定义属性的访问器了。上代码:
class Rectangle(val height: Int, val width: Int) {
val isSquare: Boolean
get() {
return height == width
}
}
我们定义了一个矩形类,其中有一个属性用来表示是否是正方形。
可以看到我们重写了get方法,在其中通过判断宽高是否相等来得出是否是正方形。
同样的我们可以直接用表达式体函数来简化:
class Rectangle(val height: Int, val width: Int) {
val isSquare: Boolean
get() = height == width
}
//对比默认的getter和setter实现
var language: String = "Kotlin"
get() {
return field //前面说到的 Backing Field
}
set(value) {
field = value
}
一个没有参数的函数和声明带自定义getter的属性哪个比较好?其实两者之间在性能上没有什么差别,唯一的差异在于可读性上。一般如果是当前类的特征,应该把它声明成属性。
大致了解了Kotlin中的类,函数和属性,咱们在来说说更高一层的,Kotlin中的目录和包结构。
直接上结论,Kotlin完全可以拿Java的目录结构来使用,但是也有一些不一样的点:
package geometry.example
import geometry.shape.createRandomRectangle //直接导入函数
//也可以import geometry.shape.* 也可以这样引入包中所有的类,属性和函数
fun main(args: Array<String>) {
print(createRandomRectangle().isSquare) //使用就跟调用当前类函数一样
}
Kotlin在目录结构上非常的灵活,但是大多数情况下参照Java的目录布局来创建文件依然是不错的实践。尤其是Java代码和Kotlin代码混合的项目,可以避免很多错误和误解的情况。
这一章让我们了解一下Kotlin中的枚举,和Kotlin中的 “switch case” 语法
首先让我们创建一个包含颜色的枚举类:
enum class Color {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}
Kotlin中的枚举跟Java一样,不单单是一个值的列表,它也可以声明函数和属性,如下面的代码:
//声明枚举常量
enum class Color(val r: Int, val g: Int, val b: Int) {
RED(255, 0, 0),
ORANGE(255, 165, 0),
YELLOW(255, 255, 0),
BLUE(0, 0, 255),
INDIGO(75, 0, 130),
VIOLET(238, 130, 238),
GREEN(0, 255, 0); //在枚举类中声明了方法后,在末尾必须有一个分号,来区分方法和实例。这也是Kotlin中唯一需要在末尾添加分号的地方
fun rgb() = (r * 256 + g) + b //给枚举类定义了一个方法
}
Kotlin中枚举的使用跟Java基本上是一致的,你在Java中能够声明函数,变量等等,你在Kotlin中同样都可以做到。
在Kotlin中要达到Java中 switch case 语句的效果就要使用到 when 这个控制结构了。通过跟枚举类的结合,我们来看看该怎么使用 when。
fun getMnemonic(color: Color) = //直接返回一个 when 表达式
when (color) { //如果颜色和枚举类相等就返回对应的字符串
Color.BLUE -> "Blue1"
Color.RED -> "Red2"
Color.GREEN -> "Green3"
Color.YELLOW -> "Yellow4"
}
>>> print(getMnemonic(Color.BLUE))
Blue1
fun getWWarmth(color: Color) =
when (color) {
Color.RED, Color.YELLOW -> "warm" //合并多个值到一个分支上,逗号分隔
Color.BLUE -> "cold"
Color.GREEN -> "neutral"
}
>>> print(getWWarmth(Color.RED))
warm
Kotlin中的 when 比Java中的 switch 强大的多。在Java中 switch 必须要求使用常量(枚举常量,字符串常量和数字字面值)作为分支条件,而 when 可以使用任何对象来当分支条件。下面来写一个函数来混合两种颜色,通过这个函数来看 when 是多么的强大。
fun mix(c1: Color, c2: Color) =
when (setOf(c1, c2)) { //when 表达式的实参能够是任何对象,它被检查是否与分支条件相等
setOf(Color.RED, Color.YELLOW) -> Color.ORANGE //列举出能够混合的颜色对
setOf(Color.YELLOW, Color.BLUE) -> Color.GREEN
setOf(Color.BLUE, Color.VIOLET) -> Color.INDIGO
else -> throw Exception("Dirty color")
}
在 when 结构中可以使用任何表达式,这会让你的代码既简洁又清晰。
在上面的例子中我们发现每次在做分支匹配的时候都会创建了Set对象,这个效率是很低的。我们都知道有时候为了性能,就需要在可读性上做出妥协。那让我们来看一下,怎么样提高上一小节中 mix 函数的性能吧。
fun mixOptimized(c1: Color, c2: Color) =
when { //没有传实参给 when
(c1 == Color.RED && c2 == Color.YELLOW) ||
(c1 == Color.YELLOW && c1 == Color.RED) -> Color.ORANGE
(c1 == Color.YELLOW && c2 == Color.BLUE) ||
(c1 == Color.BLUE && c2 == Color.YELLOW) -> Color.GREEN
(c1 == Color.BLUE && c2 == Color.VIOLET) ||
(c1 == Color.VIOLET && c2 == Color.BLUE) -> Color.INDIGO
else -> throw Exception("Dirty color")
}
>>> print(mixOptimized(Color.BLUE, Color.YELLOW))
GREEN
如果 when 没有传实参,那么分支条件可以是任意的表达式。mixOptimized 函数跟上面的 mix 函数效果是一样的,这种写法的优点在于性能好,不会额外的创建对象,缺点就是可读性上比较差。
前面在介绍变量的时候我们就说到过智能转换这个概念,在这一章节通过 when 结构来对这个概念有一个深入的了解。
对于(1+2)+4这样的算术你会采用怎么样的形式编码呢?我们把它存储在一个 树状结构 中,结构中每个节点要么是一次求和(Sum)要么是一个数字(Num)。Num永远都是 叶子节点 ,而Sum节点又有 两个子节点 :它们是求和运算的两个参数。直接上代码:
interface Expr
class Num(val value: Int) : Expr //简单的值对象,只有一个属性value,实现了Expr接口
class Sum(val left: Expr, val right: Expr) : Expr //Sum运算的实参可以是任何Expr:Num或者另一个Sum
我们用图来表示一下这个结构:
表达式Sum(Sum(Num(1),Num(2)),Num(4))的表示法
Expr接口有两种实现,所有为了计算出表达式的结果值,得尝试两种选项:
我们先按照Java的编码风格但是用Kotlin的语法来编码:
fun eval(e: Expr): Int {
if (e is Num) {
val n = e as Num //显式的转换成类型Num是多余的
return n.value
}
if (e is Sum) {
return eval(e.right) + eval(e.left) //变量e被智能的转换了类型
}
throw IllegalArgumentException("Unknown expression")
}
>>> println(eval(Sum(Sum(Num(1),Num(2)),Num(4))))
7
接下来的章节让我们看一下如果用Kotlin的编程风格该如果实现这段逻辑。
前面我们已经了解过在Kotlin中,if 类似于Java中的 ?: 三元操作符,因为 if 是可以有返回值的。那么对于前面的 eval 函数,让我们去掉 return 语句和花括号,直接使用 if 表达式作为函数体。
fun eval(e: Expr): Int =
if (e is Num) {
e.value
} else if (e is Sum) {
eval(e.right) + eval(e.left)
} else {
throw IllegalArgumentException("UnKnow Exception")
}
接下来我们在用 when 来重构这段代码
fun eval(e: Expr): Int =
when (e) {
is Num -> //检查实参类型的 when 分支
e.value //这里应用了智能转换
is Sum ->
eval(e.right) + eval(e.left)
else ->
throw IllegalArgumentException("UnKnow Expression")
}
当分支逻辑太复杂的时候,可以用代码块作为分支体。下面就来看看要怎么来使用。
fun evalWithLogging(e: Expr): Int =
when (e) {
is Num -> {
println("num:${e.value}")
e.value //返回代码块最后的表达式
}
is Sum -> {
val left = evalWithLogging(e.left)
val right = evalWithLogging(e.right)
println("sum:$left+$right")
left + right
}
else -> throw IllegalArgumentException("UnKnow Expression")
}
>>> evalWithLogging(Sum(Sum(Num(1), Num(2)), Num(4))
num:1
num:2
sum:1+2
num:4
sum:3+4
7
分支中的代码块也遵循 最后的表达式就是结果 。
这一章节我们看来一下Kotlin中关于循环的特性。其中 while 循环跟Java中的基本没有差别。for 循环在Kotlin中只有一种表现形式,跟Java中的 for-each 循环一致。在使用到循环的场景中,最先想到的肯定是集合,这一章节会讲到,同时也会覆盖到其他场景。
Kotlin中有 while 和 do-while 循环,跟Java中的 while 和 do-while 语法没有什么区别:
while (condition){ //当 condition 为true时执行循环体
//TODO
}
do {
//TODO
} while (condition) //循环体第一次会无条件的执行,此后,只有在 condition 为true时才执行
先说明一区间和数列的概念:
val oneToTen = 1..10 //这里的区间表示方式是包含结束值的
下面用一个 Fizz-Buzz 游戏来展现一下 for 循环的用法。
Fizz-Buzz游戏是一种用来打发长途驾驶旅程的不错方式,还能帮助回忆起被遗忘的除法技巧。游戏玩家轮流递增计数,遇到能被3整除的数字就用单词fizz 代替,遇到能被5整除的数字则用单词buzz 代替。如果一个数字是3和5的公倍数,就用“FizzBuzz”。
用 for 表达式来输入游戏中1到100之间所有数字的正确答案。
fun fizzBuzz(i: Int) = when {
i % 15 == 0 -> "FizBuzz "
i % 3 == 0 -> "Fizz "
i % 5 == 0 -> "Buzz"
else -> "$i "
}
>>> for (i in 1..100) {
print(fizzBuzz(i))
}
1 2 Fizz 4 Buzz Fizz 7 ...
变一下迭代规则,从100开始到1并且只输出偶数
>>> for (i in 100 downTo 1 step 2) {
printValue(fizzBuzz(i))
}
上面说到的 … 或者 downTo 区间表示方式都是包含结束值的,如果不想包含结束值的话可以使用 unit 函数来创建区间例如:
for(x in 0 unit 100){}
这就是一个从0到100不包含100的数列。
看看如何打印字符二进制表示的小程序。
val binaryReps = TreeMap<Char, String>() //创建一个TreeMap
for (c in 'A'..'F') { //使用字符区间迭代从A 到F 之间的字符
binaryReps[c] = Integer.toBinaryString(c.toInt()) //把ASCII码转换成二进制,同时根据键c把值存储到map中
}
for ((letter, binary) in binaryReps) {
println("$letter = $binary")
}
在平常开发中在迭代集合时,经常需要获取到当前项的下标。那么在Kotlin中该如何获取到集合的下标呢。
val list = arrayListOf("10", "11", "1001") //创建ArrayList对象。
for ((index, element) in list.withIndex()) { //withIndex() 根据下标来迭代
println("$index:$element")
}
后面章节会深入了解 withIndex 的内容。
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z' //底层实现 就是 a<=c && c<=z || c<=A || c<=Z 在区间中,返回true
fun isNotDigit(c: Char) = c !in '0'..'9' // 不在这个区间返回true
>>> println(isLetter('q'))
true
>>> println(isNotDigit('x'))
true
```java
**in** 运算符和 **!in** 也适用于 **when** 表达式:
```Kotlin
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'
fun recognize(c: Char) = when (c) {
in '0'..'9' -> "It's a digit!"
in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
else -> "I don't know..."
}
>>> println(recognize('8'))
It's a digit!
区间并不局限于字符 。能够组成区间的对象底层实现了 java.lang.Comparable 接口。并不是所有的区间都能够迭代,例如就不能列举出“Java”和“Kotlin”之间所有的字符串。对于这种区间仍然可以使用 in 来检查一个对象是否属于这个区间:
>>> println("Kotlin" in "Java".."Scala") //相当于 Kotlin<=Java && Scala<=Kotlin。
//字符串是按照字母表顺序进行比较的。可以看String中Comparable接口的实现
true
in 同样适用于集合:
println("Kotlin" in setOf("Java", "Scala"))
false
Kotlin的异常处理和Java以及其他许多语言的处理方式都很类似。一个函数能够正常结束,也可以在出现错误的情况下抛出异常。方法的调用者能捕获这个异常并处理它;如果没有被处理,异常会沿着调用栈再次抛出。我们来看一段抛出异常的例子:
if (percentage !in 0..100) {
throw IllegalArgumentException("A percentage value must be between 0 and 100:$percentage")
}
直接看例子,从给定的文件中读取一行,尝试把它解析成一个数字,返回这个数字;或者当这一行不是一个有效数字时返回null。
fun readeNumber(reader: BufferedReader): Int? {
try {
val line = reader.readLine()
return Integer.parseInt(line)
} catch (e: NumberFormatException) {
return null
} finally {
reader.close()
}
}
在Java中异常分为两种:checkedExcepiton(受检异常)和uncheckedExcepiton(未受检异常)。未受检异常包括两种,一种是RuntimeException,另一种是Error。第一种比如说NullPointerException,ClassCastException等等,这种异常是不需要主动抛出或者捕获的。Error一般是系统层面上的异常,不需要捕获。
让我们修改一下上面的代码例子,看一下Kotlin和Java中关于异常的另一个显著的差异。
fun readeNumber(reader: BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine()) //变成“try”表达式的值
} catch (e: NumberFormatException) {
return
}
println(number)
}
>>> val reader = BufferedReader(StringReader("not a number"))
>>> readNumber(reader) //没有任何输出
最后我们总结一下这几个章节的内容
下一章我们会讲 函数的定义与调用 。未完待续~