这一篇是Kotlin的基础,主要包含一下几点:
1)声明函数、变量、类、枚举以及属性
2)Kotlin中的控制结构
3)智能转换
4)抛出和处理异常
1、基本要素:函数和变量
在这节主要介绍组成Kotlin程序的基本要素:函数和变量。
1.1程序员学习一门语言的开始,打印Hello,world!
fun main(args: Array<String>) {
println("Hello world")
}
从这样简单的一小段代码中观察到哪些特性和语法?看看下面这个列表:
1)关键字fun用来声明一个函数。没错,Kotlin编程有很多乐趣(fun)!
2)参数的类型写在它的名称后面。稍后会看到,变量的声明也是这样。
3)函数可以定义在文件的最外层,不需要把它放在类中。
4)数组就是类。和Java不同,Kotlin没有声明数组类型的特殊语法。
5)使用println代替了System.out.println。Kotlin标准库给Java标准库函数提供了许多语法更加简洁的包装,而println就是其中一个。
6)和许多其他现代语言一样,可以省略每行代码结尾的分号。
1.2函数
/**
* 代码块函数体
*/
fun max(a:Int, b:Int):Int{
return if(a > b ) a else b
}
函数的声明以关键字fun开始,函数名称紧随其后:这个例子中函数名称是max,接下来是括号括起来的参数列表,参数列表后面跟着函数的返回类型,它们之间用一个冒号隔开。
表达式和语句
在Kotlin中,if是表达式,而不是语句。语句和表达式的区别在于,表达式有值,并且能作为另一个表达式的一部分使用;而语句总是包围着它代码块中的顶层元素,并且没有自己的值。在Java中所有的控制结构都是语句。而在Kotlin中,除了(for、do和do/while)以外大多数控制结构都是表达式。这种结合控制结构和其他表达式的能力让我们可以简明扼要的表示许多常见的模式。
另一方面,Java中的赋值操作是表达式,在Kotlin中反而变成了语句。这有助于避免比较赋值之间的混淆,而这种混淆是常见的错误来源。
表达式函数体
可以让前面的函数变的更加简单。因为他的函数体是由单个表达式构成的,可以用这个表达式作为完整的函数体,并去掉花括号和return语句:
/**
* 表达式函数体
*/
fun max2(a:Int,b:Int) = if(a > b) a else b
fun max3(a:Int,b:Int): Int = if(a > b) a else b
如果函数体写在花括号中,我们说这个函数有代码块体。如果他直接返回了一个表达式,它就是表达式体。
为什么在max3函数中可以不声明返回类型?作为一门静态类型语言,Kotlin不是要求每个表达式都应该在编译期具有类型吗?事实上,每个变量和表达式都有类型,每个函数都有返回类型。但是对表达式体函数来说,编译器会分析作为函数体的表达式,并把它的类型作为函数的返回类型,即使没有显示的写出来。这种分析通常称作类型推导。注意,只有表达式体函数的返回类型可以省略。
1.3 变量
在Java中声明变量的时候会以类型开始。在Kotlin中这样是行不通的,因为许多变量声明的类型都可以省略。所以Kotlin中以关键字开始,然后是变量名称,最后可以加上类型(不加也可以)
val question = "The Ultimate Question of Life, the Universe, and Everything"
val answer = 42
val answer2: Int = 42
和表达式体函数一样,如果你不指定变量的类型,编译器会分析初始化器表达式的值,并把它的类型作为变量的类型。
如果变量没有初始化器,需要显式的制定它的类型。
val answer3: Int
answer3 = 43
如果不能提供可以赋给这个变量值得信息,编译器就无法推断出它的类型。
可变变量和不可变变量
声明变量的关键字有两个:
1)val(来自value)不可变引用。使用val声明的变量不能再初始化后再次赋值。它对应的是Java的fianl变量。
2)var(来自variable)可变引用。这种变量的值可以被改变。这种声明对应的是普通(非final)的Java变量。
默认情况下,应该尽量地使用val关键字来声明所有的Kotlin变量,仅在必要的时候换成var。使用不可变引用、不可变对象及无副作用的函数让你的代码更接近函数式编程风格。
在定义类val变量的代码块执行期间,val变量只能进行唯一一次初始化。但是,如果编译器能确保只有唯一一次初始化语句会被执行,可以根据条件使用不同的值来初始化它:
val message: String
if(canPerformOperation()){
message = "Success"
}else{
message = "Failed"
}
注意:尽管val引用自身是不可变得,但是他指向的对象可能是可变的。例子:
val languages = arrayListOf("Java")
languages.add("Kotlin")
即使var关键字允许变量改变自己的值,但他的类型却改变不了。例子:
使用字符串字面值会发生错误,因为他的类型(String)不是期望的类型(Int)。编译器只会根据初始化器来推断变量的类型,在决定类型的时候是不会考虑后续的赋值操作。如果需要在变量中存储不匹配类型的值,必须受到把值转换或强制转换到正确的类型。
1.4更简单的字符串格式化:字符串模板
这个例子介绍了一个新特性,叫做字符串模板。在代码中,你声明了一个变量oldest,并在后面的字符串字面值中使用了它。Kotlin让你可以在字符串字面值中引用局部变量,只需要在变量名称前面加上字符$。这等价于Java中的字符串链接("Hello ," + oldest + "!"),效率一样但是更加紧凑。还可以引用复杂的表达式,而不是简单的变量名称,只需要把表达式用花括号括起来:
2、类和属性
面向对象编程对我们来说可能不是什么新鲜的概念,你也许非常熟悉类的抽象机制。Kotlin这方面的概念你也会觉得似曾相识,但是你会发现许多常见的任务使用更少的代码就可以完成。先来看一个简单的JavaBean类People目前它只有一个属性,name。
public class People {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
在Java中,构造方法的方法体常常包含完全重复的代码:它把参数赋值给有着相同名称的字段。在Kotlin中,这种逻辑不用这么多的样板代码就可以表达。Kotlin中的People类.
class People(val name: String)
看起来不错,不是吗?如果你试过其他的一些现代JVM语言,你也许见过类似的事情。这种类通常被叫做值对象。
注意从Java到Kotlin的转换过程中public修饰符消失了。在Kotlin中,public是默认的可见性,所以你能省略它。
2.1属性
类的概念就是把数据和处理诗句的代码封装成一个单一的实体。在Java中,数据存储在字段中,通常还是私有的。途观想让类的使用者访问到数据,得提供访问器方法:一个getter和一个setter方法。在Java中,字段和其他访问器的组合常常叫做属性,而许多框架严重依赖这个概念。在Kotlin中,属性是头等的语言特性,完全代替了字段和访问器方法。在类中声明一个属性和声明一个变量一样:使用val和var关键字。声明val的属性是只读的,而var属性是可变的。
class People{
val name: String = "hello world"
var isMarred: Boolean? = null
}
基本上,当你声明属性的时候,你就声明了对应的访问器(只读属性有一个getter,而可写属性既有getter也有setter)。访问器的默认实现非常简单:创建一个存储值得字段,以及返回值得getter和更新值得setter。但是如果需要,可以声明自定义的访问器,使用不同的逻辑来计算和更新属性的值。
val people = People("zhangsan",true)
println(people.isMarred)
println(people.name)
现在,可以直接引用属性,不再需要调用getter。逻辑没有变化,但代码更简洁了。
2.2自定义访问器
/**
* 自定义访问器
*/
class Rectangle(val height:Int,val width:Int){
val isSquare:Boolean
get() {
return height == width
}
}
属性isSquare不需要字段来保存它的值。它只有一个自定义实现的getter。他的值是每次访问属性的时候计算数来的。这个get方法还可以这样写:
/**
* 自定义访问器
*/
class Rectangle(val height:Int,val width:Int){
val isSquare: Boolean
get() = height == width
var isNotSquare: Boolean = false
set(v) { !isSquare}
}
3表示和处理选择:枚举和“when”
3.1声明枚举类
enum class Color{
RED,ORANGE,YELLOW,GREEN,BLUE,INDIGO,VIOLET
}
这是极少数Kotlin声明比Java使用了更多关键字的例子:Kotlin用了enum class两个关键字,而Java只有enum一个关键字。
声明一个带属性的枚举类:
enum class Color(val r:Int,val g:Int,val b :Int){
RED(255,0,0),ORANGE(255,165,0),YELLOW(255,255,0),
GREEN(0,255,255),BLUE(0,0,255),INDIGO(75,0,130),
VIOLET(238,130,238);//这是Kotlin语法中唯一必须使用分号的地方:如果要在枚举类中定义任何方法,就要使用分号把枚举常量列表和方法 定义分开
fun rgb() = (r * 256 + g) * 256 + b
}
枚举常量用的声明构造方法和属性的语法与之前常规类一样。
3.2使用when处理枚举类
在Java中可以用switch语句完成,而Kotlin对应的结构是when。和if相似,when是一个有返回值的表达式,因此可以写一个直接返回when表示式的表达式体函数。
使用when来选择正确的枚举值
fun getMnemonic(color:Color) = when(color){
Color.RED -> "Richard"
Color.ORANGE -> "of"
Color.YELLOW -> "York"
Color.GREEN -> "Gave"
Color.BLUE -> "Battle"
Color.INDIGO -> "In"
Color.VIOLET -> "Vain"
}
代码根据传进来的color值找到对应的分支。和Java不一样,你不需要再每个分支都写上break语句(在Java中遗漏break通常会导致bug),如果匹配成功,只有对应的分支会执行。也可以把多个值合并到同一个分支,只需要使用逗号隔开这些值。在一个when分支上合并多个选项:
/**
* 在一个when分支上合并多个选项
*/
fun getWarmth(color:Color) = when(color){
Color.RED,Color.ORANGE,Color.YELLOW -> "warm"
Color.GREEN -> "neutral"
Color.BLUE,Color.INDIGO,Color.VIOLET -> "cold"
}
导入枚举常量后不用限定词就可以访问
/**
* 导入枚举常量后不用限定词就可以直接访问
* 导入的语法:import com.houde.first.Color.*
*/
import com.houde.first.Color.*
fun getWarmth2(color: Color) = when(color){
RED , ORANGE , YELLOW -> "warm"
GREEN -> "neutral"
BLUE , INDIGO , VIOLET -> "cold"
}
3.3在when结构中使用任意对象
Kotlin中的when结构比Java中的switch强大的多。switch要求必须使用常量(枚举常量、字符串或者数字字面值)作为分支条件,和它不一样,when预习使用任何对象。在when分支中使用不同的对象:
/**
* when强大的地方在于可以是任何表达式,而咋Java中只能是字符串,数字,枚举常量
* 这种调用一次就会生成一个set对象,
*/
fun mix(c1:Color,c2:Color) = when(setOf(c1,c2)){
setOf(RED,YELLOW) -> "ORANGE"
setOf(YELLOW,BLUE) -> "GREEN"
setOf(BLUE,VIOLET) -> "INDIGO"
else -> throw Exception("Dirty color")
}
如果颜色c1和c2分别为RED和YELLOW(反过来也可以),它们混合后的结果就是ORANGE,依次类推。这个地方使用了set比较这个调色板。Kotlin标准函数库中有一个setOf函数可以创建出一个Set,它会包含所有指定为函数实参的对象。set这种集合的条目顺序并不重要,只要两个set中包含一样的条目,它们就是相等的。所以如果setOf(c1,c2)和setOf(RED,YELLOW)是相等的,意味着c1是RED和c2是YELLOW的时候相等,反过来也成立。
when表达式把它的实参依次和所有分支匹配,直到某个分支满足条件。这里setOf(c2,c2)被用来检查是否和分支条件相等:先和setOf(RED,YELLOW)比较,然后是其他颜色的set,一个接一个。如果没有其他的分支满足条件,else分支会执行。
3.4使用不带参数的when
/**
* 不带参数的when
* 这种不用生成额外的对象,但是理解有点困难
*/
fun mixOptimized(c1: Color,c2: Color) = when{
(c1 == RED && c2 == YELLOW) || (c1 == YELLOW && c2 == RED) -> "ORANGE"
(c1 == YELLOW && c2 == BLUE) || (c1 == BLUE && c2 == YELLOW) -> "GREEN"
(c1 == BLUE && c2 == VIOLET) || (c1 == VIOLET && c2 == BLUE) -> "INDIGO"
else -> throw Exception("Dirty color")
}
3.5智能转换:合并类型检查和转换
在Kotlin中,使用is检查来判断一个变量是否是某种类型。is检查和Java中的instanceOf相似。但是在Java中,如果已经检查过一个变量是某种类型并且要把它当做这种类型来访问成员时,在instanceOf检查之后还需要显示的加上类型转换。在Kotlin中,编译器帮我们完成了这样的工作。如果检查过一个变量是某种类型,后面就不再需要转换它,可以就把它当做你检查过的类型使用。事实上编译器为你执行类类型转换,我们把这种行为成为智能转换。
/**
* 智能转换:合并类型检查和转换
*/
interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
/**
* 在kotlin中,你要使用is检查来判断一个变量是否是某种类型
* is检测跟Java中的instanceOf相似,但是is判断为true,不用类型转换就是转换为判断的类型
*/
fun eval(e:Expr):Int{
if(e is Num){
return e.value
}
if(e is Sum){
return eval(e.left) + eval(e.right)
}
throw IllegalArgumentException("Unknown expression")
}
智能转换只在变量经过is检查且之后不再发生变化的情况下有效。当你对一个类的属性进行智能转换的时候,就像这个例子中一样,这个属性必须是一个val属性,而且不能有自定义的访问器。否则,每次对属性的访问是否都能返回同样的值将无从验证。使用as关键字来表示到特定类型的显示转换:
val n = e as Num
3.6重构:用when代替if
/**
* 把if换成when
*/
fun eval2(e:Expr):Int = when(e){
is Num -> e.value
is Sum -> eval2(e.left) + eval2(e.right)
else -> throw IllegalArgumentException("Unknown expression")
}
如果if分支中只有一个表达式,花括号是可以省略的。如果if分支是一个代码块,代码块中的最后一个表达式会被作为结果返回。
/**
* 使用when代替if层叠
*/
fun eval3(e: Expr):Int =
when(e){
is Num ->
e.value
is Sum ->
eval3(e.right) + eval3(e.left)
else ->
throw IllegalArgumentException("Unknown expression")
}
when表达式并不仅限于检查值是否相等,那是之前看到的。而这里使用了另外一种when分支的形式,允许检查when实参值类型。当分支逻辑太过复杂时,可以使用代码块作为分支体
3.7代码块作为if和when的分支
if和when都可以使用代码块作为分支体。这种情况下,代码块中的最后一个表达式就是结果。如果要在例子函数中加入日志,可以在代码块中实现它并像之前一样返回最后的值。
/**
* 使用分支中含有混合操作的when
*/
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("Unknown expression")
}
规则——代码块中最后的表达式就是结果,在所有使用代码块并期望得到一个结果的地方成立。
4.迭代事物:while循环和for循环
4.1while循环
Kotlin有while循环和do-while循环,它们的语法和Java中相应的循环没有什么区别。
4.2迭代数字:区间和数列
区间本质上就是两个值之间的间隔,这两个值通常是数字:一个起始值,一个结束值,使用 .. 运算符来表示区间:
val oneToTen = 1..10
注意Kotlin的区间是包含的或者闭合的,意味着第二个值始终是区间的一部分。用整数区间做最基本的事情就是循环迭代其中所有值。如果能迭代区间中所有的值,这样的区间被称作数列。例子是一个叫Fizz-Buzz的游戏。编写一个程序从1到100.当遇到数字为3的倍数的时候,点击“Fizz”替代数字,5的倍数用“Buzz”代替,既是3的倍数又是5的倍数点击“FizzBuzz”。
fun fizzBuzz(i:Int) = when{
i % 15 == 0 -> "FizzBuzz"
i % 3 == 0 -> "Fizz"
i % 5 == 0 -> "Buzz"
else -> "$i"
}
/**
* ..表示区间包左又包右
* in表示在集合或区间之内
* !in表示不在集合或区间之内
* until 表示区间包左不包右
* downTo 下降到
* step 步长 step 必须>0 否则抛异常java.lang.IllegalArgumentException: Step must be positive, was: -1.
*
*/
fun main(args: Array<String>) {
for(i in 1..100 step 2)
println(fizzBuzz(i))
for(i in 100 downTo 1 step 2)
println(fizzBuzz(i))
}
4.3迭代map
初始化并迭代map
fun main(args: Array<String>) {
val binaryReps = TreeMap<Char,String> ()
for(c in 'a'..'f'){
binaryReps[c] = Integer.toBinaryString(c.toInt())
}
for((letter,binary) in binaryReps){
println("$letter = $binary")
}
}
代码展示里for循环允许展开迭代中的集合的元素(在这个例子中,展开map的键值对集合)。把展开的结果保存到了两个独立的变量中:letter的键,binary的值。根据键来访问和更新map的简明语法。可以使用map[key]度气质,并使用map[key] = value设置它们,而不是调用get和put。
可以用这样展开的语法在迭代集合的同时跟踪当前项的下标。不需要创建一个单独的变量来存储下标并手动增加它,代码如下:
val list = arrayListOf("10","11","12","13","14","15")
for((index,element) in list.withIndex()){
println("$index = $element")
}
结果
4.4使用in检查集合和区间成员
使用in运算符来检查一个值是否在区间中,或者它的逆运算,!in,来检查这个值是否不在区间中。
val list = arrayListOf("10","11","12","13","14","15")
println("10 in list = ${"10" in list}")
println("10 in list = ${"10" !in list}")
in运算符和!in也使用于when表达式。
/**
* 用in 检查作为when分支
*/
fun recognize(c: Char) = when(c){
in 'a'..'z',in 'A'..'Z' -> "It's a letter"
in '0'..'9' -> "It's a digit"
else -> "I don't known"
}
区间也不仅限于字符。假如有一个支持实例比较操作的任意类(实现了java.lang.Comparable接口),就能创建这种类型的对象的区间。如果是这样的区间,并不能列举出这个区间中的所有对象。想想这种情况:例如,是否可以列举出java和kotlin之间的所有字符串呢?答案是不能。但是仍然可以使用in运算符检查一个其他的对象是否属于这个区间:
println("Kotlin" in "Java".."Scale")
true
注意,这里字符串是按照字面表顺序进行比较的,因为String就是这样实现Comparable接口的。
in检查同样适用于集合:
println("Kotlin" in setOf("Java" , "Scale"))
false
5.Kotlin中的异常
Kotlin中的异常处理和Java以及其他语言的处理方式相似。一个函数可以正常结束,也可以在出现错误的情况下抛出异常。方法的调用者能捕获到这个异常并处理它;如果没有被处理,异常会沿着调用栈再次抛出。
Kotlin中异常处理语句的基本形式和Java类似,抛出异常的方式也不例外:
if(number !in 0..100){ throw IllegalArgumentException("A percentage value must be between 0 and 100 : $number") } 和所有其他的类一样,不必使用new关键字来创建异常实例。和Java不同的是,Kotlin中throw结构是一个表达式,能作为另一个表达式的一部分使用:val percentage = if (number in 0..100){ number }else{ throw IllegalArgumentException("A percentage value must be between 0 and 100 : $number") } 在这个例子中,如果条件满足,程序的行为是正确的,而percentage变量会用number初始化。否则,异常将会被抛出,而变量也不会初始化。
5.1 try catch 和finally
和Java一样,使用带有catch和finally子句的try结构来处理异常。会在下面的代码清单中看到这个结构,这个例子从给定的文件中读取一行,尝试把它解析成一个数字,返回数字:或者当这一行不是有效数字时返回null。
/** * 像在Java中使用try */ fun readNumber(reader:BufferedReader): Int? { try { val line = reader.readLine() return Integer.parseInt(line) }catch (e:NumberFormatException){ return null }finally { reader.close() } } 和Java最大的区别是throws子句没有出现在代码中:如果用Java来写这个函数,你会显示地在函数声明后写上throws IOException。你需要这样做的原因是IOException是一个受检异常。在Java中,这种异常必须显示地处理这个函数的受检异常。如果调用另外一个函数,需要处理这个函数的受检异常,或者声明自己的函数也抛出这些异常。和其他JVM语言一样,Kotlin并不区分受检异常和未受检异常。不用制定函数抛出异常,而且可以处理也可以不处理异常。这种设计师基于Java中使用异常的实践做出的决定。经验显示这些Java规则常常导致许多毫无意义的重新抛出或者忽略异常的代码,而且这些规则不能总是保护你免受可能发生的错误。
5.3 try作为表达式
/** * try 作为表达式 */ fun readNumber2(reader:BufferedReader) { val number =try { Integer.parseInt(reader.readLine()) } catch (e: NumberFormatException) { null } finally { reader.close() } println(number) }
Kotlin中的try关键字就像if和when一样,引入了一个表达式,可以把它的值赋给一个变量。不同于if,总是需要用花括号把语句主体括起来。和其他语句一样,如果其他主体包含多个表达式,那么整个try表达式的值就是最后一个表达式的值。
如果一个try代码块执行一切正常,代码块中最后一个表达式就是结果。如果捕获到一个异常,相应catch代码块中最后一个表达式就是结果。如上面的例子如果抛出异常,捕获到NumberFormatException,结果值就是null。