变量
变量是一门编程语言最基础和重要的功能,kotlin的变量定义很有特色。
- 自动推导类型。C++,java等高级语言在声明变量的时候需要指明变量类型,如
int value = 0;
。但是kotlin拥有出色的类型推导机制,在变量声明并初始化的时候,可以根据初始化值的不同,自动推导出变量类型,如
//整型变量
val num1 = 0
//浮点数
val num2 = 0.0
//字符串
val str = "Hello world!"
- 显式声明变量类型。当变量声明但不初始化的时候,编译器无法自动推导出变量类型,这时候需要我们进行显式的声明变量类型,如
val num:Int
- var和val。如前所述,kotlin在声明变量时无需指明变量类型,而是由编译器自动推导,但是在变量声明前添加两种关键字,即
val
和var
。
- val (value)的简写,表明这是一个常量,必须初始化,且初始化以后不能再改变。对应java的final。
- var(variable)的简写,表明这是一个变量。
技巧:尽量使用val。大型项目的开发需要多人协作,我们通常不希望自己的变量被别人随便修改,因为这很容易出现一些难以排查的bug。因此,我们要尽可能使用val
,只有当val无法满足需求时,才使用var
。
- 纯面向对象。java中的变量类型分为基本数据类型和对象数据类型,而kotlin摒弃了java中的基本数据类型,全部都用对象实现。
int
是java中的关键字,而Int
(注意到这里首字母大写)是kotlin中的一个类,拥有自己的继承结构与类方法。类似的,Double,Float,Long
等也都是类。
函数
任何高级语言都实现了函数,这大大提高了代码复用率,并使逻辑清晰,减少了程序员犯错的可能性。
- 函数定义的基本格式。
fun function(paraml1:Int, paraml2:Int):Int
{
return 0
}
- 当函数只有一行代码时,不必编写函数体,如实现求两个数中较大的那个函数定义如下。
fun largeNumber(num1:Int, num2:Int) = max(num1, num2)
- 函数的默认参数值。给函数参数设置默认参数值,则不必要求该函数被调用时,必须为此变量赋值,而使用默认的参数值。不同于java的是,kotlin不必规定默认参数必须放在所有参数的最右边。这是因为kotlin不必按照定义的顺序传参。请看下面的传参方式。
var num = largerNumber(num1 = 1, num2 = 2)
if条件语句
- kotlit的if语句基础功能与绝大多数高级语言一样,如下面的函数的功能返回两个数中较大的那个。
fun largerNumber(num1:Int, num2:Int):Int
{
var value = 0
if(num1 > num2)
{
value = num1
}
else
{
value = num2
}
return value
}
- kotlin的if语句可以有返回值,其返回值就是每一个条件分支中的最后一行代码,因此上面的代码可以改写成如下形式。
fun largerNumber(num1:Int, num2:Int):Int
{
return if(num1 > num2)
{
num1
}
else
{
num2
}
}
when语句
- when语句类似于java中的switch。当条件分支较多时,用when语句可以让代码显得很简洁,如实现一个用阿拉伯数组转换成汉字显示的星期几如下。
fun convert(x:Int):String
{
var result = ""
when(x)
{
1 -> result = "星期一"
2 -> result = "星期二"
3 -> result = "星期三"
4 -> result = "星期四"
5 -> result = "星期五"
6 -> result = "星期六"
7 -> result = "星期日"
else -> result = "非法输入"
}
return result
}
- when语句与switch的不同之处如下:
- 无需在每个分支后面增加break语句。java中的switch语句中每个case后面都要手动添加break语句,否则执行完当前case之后,就会顺序执行完之后所有的case,而kotlin则只执行当面语句。
- when语句允许传入任意类型的参数。java中的switch语句中的判断类型只能是整型或者短整型。
- when语句支持类型匹配。请看下面的例子。这里
Number
是Kotlin内置的一个抽象类,Int
,Long
,Double
,FLoat
等都是其子类。而is关键字用来判断数据类型,相当于java的instanceof
关键字。
fun whenTest(x:Number)
{
when(x)
{
is Int -> println("$x is Int")
is Double -> println("$x is Double")
else -> println("无法识别")
}
}
- when扩展用法:不带参数。当
when
不带参数时,将完整的判断表达式写在分支当中。请看下面的例子。
fun pieseWiseFun(x:Double):Int
{
var result = 0
when
{
x < 0 -> result = 0
x < 1 -> result = 1
else -> result = 2
}
return result
}
循环语句
kotlin循环语句主要是when循环和for循环,when循环基本和java一样。
for循环
- 区间
- 双端闭区间:
val range = 0..5
。等价于0,1,2,3,4,5
。 - 左闭右开区间:
val range = 0 until 5
。 - 步长设置:
val range = 0 until 5 step 2
等价于0,2,4
。 - 降序区间:
5 downTo 1
等价于5,4,3,2,1
。
- for in 循环:对区间,集合,数组等遍历。
for(i in 1 until 10)
{
println("i = $i")
}
面向对象编程
构造函数
构造函数是在生成对象时调用,其作用主要是完成类内字段的初始化。kotlin的构造函数比较复杂,分为主构造(没有函数体)和次构造(有函数体)。
- 主构造函数:没有函数体。每个类只能有一个主构造函数,下面的例子定义了一个Person类,其数据成员是
name
和age
。
注意
- 这里的构造函数的定义和java不太一样,直接和类的定义写在了一起,而不是单独定义了一个函数。
- 在主构造函数中声明为
val
和var
的参数,将自动变成该类的数据成员。
open class Person(val name:String, val age:Int) {
fun eat(){
println(name + " is eating, He is " + age + " years old.");
}
}
- 主构造函数中写函数体的方法。如果我们想在主构造函数中实现一些逻辑,可以采用kotlin提供的init结构体,如下所示。可以看到,这里我们只是将其数据成员打印而已。
open class Person(val name:String, val age:Int) {
init {
println("name = $name")
println("age = $age")
}
fun eat(){
println(name + " is eating, He is " + age + " years old.");
}
}
- 次构造函数:可以有函数体,用关键字
constructor
修饰。当一个类既有主构造函数,又有次构造函数时,次构造必须调用主构造,如下所示。这里的次构造函数仅有一个输入参数,通过调用主构造函数完成初始化。
这里的次构造函数提供了一个只需一个name参数的构造方法。
open class Person(val name:String, val age:Int) {
init {
println("name = $name")
println("age = $age")
}
constructor(name: String):this(name, 0)
{
}
fun eat(){
println(name + " is eating, He is " + age + " years old.");
}
}
注意:当在类中不显式的定义主构造函数而定义了此构造函数时,这个类就没有主构造函数,只有次构造函数。此时,次构造函数需要调用其父类的构造函数。这种情况很少发生。
类继承
继承是面向对象的重要特性之一,其特点是在现有类的基础上拓展端口生成新的类。如此便可在降低程序员工作量的同时提高代码的可靠性。
- kotlin默认类是不可继承的。众所周知,java中有关键字
final
,其主要作用是声明该类时终结类,不能被别的类继承。kotlin中任何一个非抽象类默认情况下都是不可继承的,相当于被声明为java中的final
。之所以这么设计,是因为如果一个类允许被继承,那么它无法预知他的子类如何实现,可能出现一些意想不到的bug。因此除非这个类是专门为继承而设计的,否则都应该定义为不可被继承的。 - 关键字:open。在类定义时,用关键字open修饰,可以将类声明为可以被继承的。请看一个继承的例子。
//基类:请注意这里的关键字open
open class Person{
var name = ""
var age = 0
}
//子类
class Student(val grade:Int, name: String, age:Int):Person(){
}
几点说明
- 这里的Person类被关键字
open
修饰,意在告诉编辑器,Person类可以被继承。 - java中的继承采用关键字
extends
,而kotlin中用:
。 - Person类中没有显式的定义构造函数,但是编译器自动生成了一个不用任何参数的构造函数,即默认构造函数。
- kotlin中的继承的特殊之处在于,Person后面有一对括号。这是因为子类的构造函数必须调用基类的构造函数,这就相当于在Student的主构造函数中调用Person类中的默认构造函数。
- Student类中,主构造函数的参数有3个。其中
grade
被val修饰,编译器自动将被val
和var
修饰的变量设置为本类的字段。而name
和age
则没有被修饰,因为他们是为其基类准备的参数。
lambda编程
lambda是一段可以作为参数传递的代码。正常情况下,调用函数时只能传入变量,而借助lambda,我们可以传入代码。传入的这段代码不宜过长,否则影响可读性。
- lambda的语法结构。运算符
->
是参数和函数体的分隔符,函数体中的最有一行是lambda表达式的返回值。
{参数1:参数类型,参数2:参数类型 -> 函数体}
lambda最常见的用法是和集合一起使用,我们先看看集合的定义。
2. 不可变集合:必须初始化,且初始化之后就变成只读的,不能修改,增加,删除元素。用函数listOf()
定义。
val list = listOf("Apple", "Banana", "Orange", "Pear")
3.可变集合:用函数mutableListOf()
函数定义。
val list2 = mutableListOf("Red", "Blue", "Green")
集合的函数式API
考虑这样的需求:如何寻找上述集合中长度最长的一个元素,这里可以借助lambda表达式完成。函数maxBy()
的功能是根据穿进去的参数,选择集合中该参数最大的那个。
注意到这里maxBy()
的参数是一个lambda表达式:
{fruit:String -> fruit.length}
val maxLengthFruit = list1.maxBy({fruit:String -> fruit.length})
上述表达式可以进行简化,简化规则如下
- 当lambda表达式是函数的最后一个参数时,可以将lambda表达式写在函数的小括号后面。
val maxLengthFruit = list1.maxBy(){fruit:String -> fruit.length}
- 当lambda表达式是函数的唯一参数时,可以省略小括号。
val maxLengthFruit = list1.maxBy{fruit:String -> fruit.length}
当lambda表达式中只有一个参数时,不必进行参数声明,并用it
代替。
val maxLengthFruit = list1.maxBy{it.length}
java函数式API的使用
- 如果在kotlin中调用了一个java方法,并且该方法知接收一个单抽象方法接口参数,就可以使用函数式API。所谓单抽象方法接口是指接口中只有一个待实现的方法。
java中有一个常见的单抽象方法接口——Runnable()
,此方法中只有一个待实现的方法,如下所示:
public interface Runnable(){
void run()
}
Runnable
常常结合线程一起使用,Thread
类的构造方法中接收一个Runnable
接口,并创建一个子线程,如下所示。
new Thread(new Runnable(){
@Override
public void run()
{
printLn("This is a Thread")
}
}).start
这里使用了匿名类的写法,将其翻译成kotlin就是
Thread(object : Runnable{
override fun run() {
println("This is a Thread")
}
}).start()
注意到这里符合使用lambda的条件,因此我们改写之。
Thread(Runnable {
println("This is a Thread")
}).start()
因为这里Runnable
只有一个待实现的函数,因此即使我们不写函数名,也不会产生歧义。
但这里还不是最简形式,当我们该方法的形参表中仅有一个单抽象方法接口时,我们还可以省略接口名,如下所示
Thread({
println("This is a Thread")
}).start()
当lambda是最后一个表达式时,还可以将lambda表达式写在小括号外面。
当lambda表达式是唯一的参数时,还可以将小括号省略,因此最终的简化形式如下
Thread{
println("This is a Thread")
}.start()
空指针检查
空指针异常是常见的bug之一。为了解决这个问题,kotlin有一系列的措施。
- 可空数据类型:我们之前所见的数据类型,如
Int,String
等都是不可空的。在其后面加上问号,即Int?,String?
就是可空数据类型。 - 判空运算符 ?.:其调用形式同“.”,即
对象名?.方法()
。其作用是当对象非空时,正常调用。当对象是空时,什么也不做。