变量
在Kotlin中定义一个变量,只允许变量前声明两种关键字:val和var
- val(value的简写)用来声明一个不可变的变量,这种变量在初始赋值之后就不能再重新赋值,对应Java中的final变量。
- var(variable的简写)用来声明一个可变的变量,这种变量在初始赋值之后可以被重新赋值,对应Java中的非final变量。
这是因为Kotlin拥有出色的类型推导机制。但是,如果我们要对一个变量延迟赋值的话,Kotlin就无法自动推导它的类型了。这时候就需要显式地声明变量类型才行,语法如下:
val a: Int = 10
注意,这里的Int首字母是大写的,这表示Kotlin完全抛弃了Java中的基本数据类型,全部使用了对象数据类型。在Kotlin中,Int变成了一个类,它拥有自己的方法和继承结构。下面是Java中每一个基本数据类型在Kotlin中对应的对象数据类型:
Java基本数据类型 | Kotlin对象数据类型 | 数据类型说明 |
---|---|---|
int | Int | 整型 |
long | Long | 长整型 |
short | Short | 短整型 |
float | Float | 单精度浮点型 |
double | Double | 双精度浮点型 |
boolean | Boolean | 布尔型 |
char | Char | 字符型 |
byte | Byte | 字节型 |
值得注意的是,Kotlin一旦推导出一个变量的类型,后续是不允许改变的,这一点与Python不同。
函数
Kotlin中定义函数的语法规则如下:
fun methodName(param1: Int, param2: Int): Int{
return 0
}
参数的声明格式是参数名:参数类型
,如果该函数不接收任何参数,那么一对空括号即可。参数括号后面的部分说明了函数的返回类型,若函数不需要返回任何数据,这部分可以不写。
当一个函数只有一行代码时,Kotlin允许我们不必编写函数体,可以直接将唯一的一行代码卸载函数定义的尾部,中间用等号连接即可。例如:
fun largerNumber(num1: Int, num2: Int): Int = max(num1, num2)
由于Kotlin具有出色的类型推导机制,所以此处的函数返回值类型Int实际上可以略去不写,因为max()函数返回的是一个Int值,而largerNumber()的尾部又使用了等号连接了max()函数,因此Kotlin可以推导出largerNumber()的返回值必然是Int类型,这样就不需要再显式地声明返回值类型了,代码可以进一步简化为如下形式:
fun largerNumber(num1: Int, num2: Int) = max(num1, num2)
可见性修饰符
修饰符 | Java | Kotlin |
---|---|---|
public | 所有类可见 | 所有类可见 (默认) |
private | 当前类可见 | 当前类可见 |
protected | 当前类、子类、同一包路径下的类可见 | 当前类、子类可见 |
default | 同一包路径下的类可见 (默认) | 无 |
internal | 无 | 同一模块中的类可见 |
条件语句
if条件语句
Kotlin中的if语句和Java中的if语句几乎没有任何区别,下面是一个简单的例子:
fun largerNumber(num1: Int, num2: Int): Int{
var value = 0
if (num1 > num2) {
value = num1
} else {
value = num2
}
return value
}
Kotlin中的if语句相比于Java有一个额外的功能,它是可以有返回值的,返回值就是if语句每一个条件中最后一行代码的返回值。因此,上述代码可以简化为如下形式:
fun largerNumber(num1: Int, num2: Int): Int{
val value = if (num1 > num2) {
num1
} else {
num2
}
return value
}
在这里,我们可以将value改成由val声明的,因为现在没有重新赋值的需要了。其实,value在这里是一个多于的变量,我们可以直接将if语句返回,如下:
fun largerNumber(num1: Int, num2: Int): Int{
return if (num1 > num2) {
num1
} else {
num2
}
}
接着,我们发现,整个函数体中只有“一行代码”,因此可以继续简化:
fun largerNumber(num1: Int, num2: Int) = if (num1 > num2) num1 else num2
}
when条件语句
Kotlin中的when语句有些类似于Java中的switch语句,但又比switch语句强大得多。
精确匹配
when语句允许传入一个任意类型的参数,然后可以在when的结构体中定义一些列的条件,格式是:匹配值 -> {执行逻辑}
下面是一个查询考试成绩的函数,输入一个学生的姓名,返回该学生的考试成绩:
fun getScore(name: String) = when (name) {
"Tom" -> 86
"Jim" -> 77
"Jack" -> 95
"Lily" -> 100
else -> 0
}
类型匹配
除了精确匹配之外,when语句还允许进行类型匹配。举例来说,我们定义一个checkNumber()函数,如下:
fun checkNumber(num: Number) {
when (num) {
is Int -> println("number is Int")
is Double -> println("number is Double")
else -> println("number not support")
}
}
其中,is关键字就是类型匹配的核心,它相当于Java中的insanceof关键字。
不带参数的when语句
when还有一种不带参数的用法,能发挥很强的扩展性。以刚才的getScore()为例,还可以这么写:
fun getScore(name: String) = when {
name == "Tom" -> 86
name == "Jim" -> 77
name == "Jack" -> 95
name == "Lily" -> 100
else -> 0
}
【注】Kotlin中判断字符串或对象是否相等可以直接使用==关键字,而不需要像Java那样调用equals()方法。
假设所有名字以Tom开头的人,分数都是86分,那么用带参数的when语句就无法实现,而使用不带参数的when语句就可以这样写:
fun getScore(name: String) = when {
name.startsWith("Tom") -> 86
name == "Jim" -> 77
name == "Jack" -> 95
name == "Lily" -> 100
else -> 0
}
循环语句
while循环
while循环无论是在语法还是使用技巧上都和Java中的while循环没有任何区别。
for循环
Java中的for-i循环在Kotlin中被舍弃了,而for-each循环则被Kotlin进行了大幅度的加强,变成了for-in循环。例如:
for main() {
for (i in 0..10) {
println(i)
}
}
其中0..10
表示一个区间,这是Java中所没有的东西,在Kotlin中,它表示[0,10],是一个闭区间。..
是创建两端闭区间的关键字,在..
的两边指定区间的左右端点即可创建一个区间。
另外,可以使用until
关键字来创建一个左闭右开的区间,例如:val range = 0 until 10
,它的数学表达方式是[0,10)。
默认情况下,for-in循环每次执行循环时会在区间范围内递增1,相当于Java for-i循环中的i++,而如果你想跳过其中一些元素,可以使用step
关键字:
for main() {
for (i in 0 until 10 step 2) {
println(i)
}
}
如果你想创建一个降序的区间,可以使用downTo
关键字,这是一个两端闭区间,也可以与step关键字结合使用。用法如下:
for main() {
for (i in 10 downTo 1) {
println(i)
}
}
面向对象编程
类的定义与实例化
class Person{
var name = ""
var age = 0
fun eat() {
println(name + " is eating.")
}
}
Kotlin中实例化一个类不需要new关键字。
val p = Person()
构造函数
Kotlin中将构造函数分成了两种:主构造函数和次构造函数。
主构造函数
每个类默认都会有一个不带参数的主构造函数,当然你也可以显式地给它指明参数。主构造函数的特点是没有函数体,直接定义在类名的后面即可。比如下面这种写法:
class Person (val name: String, val age: Int){
fun eat(){
println(name + " is eating.")
}
}
这里我们将姓名name和年龄age这两个字段都放到了主构造函数当中,这就表明在对Person类进行实例化的时候,必须传入构造函数中要求的所有参数,比如:
val person = Person("Jack", 10)
由于构造函数中的参数是在创建实例的时候传入的,不像之前的写法那样还需要重新赋值,因此我们可以将参数全部声明为val。
如果要在主构造函数中编写一些逻辑,可以使用Kotlin为我们提供的一个init
结构体,所有的主构造函数中的逻辑都可以写在里面:
class Person (val name: String, val age: Int){
init{
println("Primary Constructor: Hello, my name is " + name + ", and I'm " + age +" years old.")
}
fun eat(){
println(name + " is eating.")
}
}
val person = Person("Jack", 10)
上述语句在创建Person实例时,便会输出如下句子:
次构造函数
一个类只能有一个主构造函数,但是可以有多个次构造函数。次构造函数也可以实例化一个类,但它是有函数体的。 Kotlin规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)。
次构造函数是通过constructor
关键字来定义的。
class Person (val name: String, val age: Int){
init{
println("Primary Constructor: Hello, my name is " + name + ", and I'm " + age +" years old.")
}
constructor() : this("**", 0){
println("Secondary Constructor-1: " + "name = " + name + ", age = "+ age)
}
constructor(name: String) : this(name, 0){
println("Secondary Constructor-2: " + "name = " + name + ", age = "+ age)
}
fun eat(){
println(name + " is eating.")
}
}
这里我们定义了两个次构造函数:第一个次构造函数不接收任何参数,通过this
关键字调用了主构造函数,将name和age初始化为 ** 和0。第二个次构造函数接收一个参数name,然后它又通过this
关键字调用了主构造函数,为name赋初始值,并将age初始化为0。
至此,我们共有3中方式对Person类进行实例化:
val person1 = Person("Jack", 10) // 调用主构造函数
person1.eat()
val person2 = Person() // 调用第一个次构造函数
person2.eat()
val person3 = Person("Tom") // 调用第二个次构造函数
person3.eat()
输出结果如下:
继承
Kotlin中任何一个非抽象类默认都是不可以被继承的,相当于Java中给类声明了final关键字。
之所以这样设计,其实和val关键字的原因是差不多的,因为类和变量一样,最好都是不可变的。在Java中,一个好的编程习惯是,除非一个变量明确允许被修改,否则都应该给它加上final关键字;如果一个类不是专门为继承而设计的,那么就应该主动为它加上final声明,禁止它被继承。但不是每个人都能养成这种良好的习惯,所以Kotlin在设计时就采用了和Java完全不同的方式,提供了val和var这两个关键字,必须由开发者主动声明该变量是可变的还是不可变的。总之,请永远优先使用val来声明一个变量,而当val没有办法满足你的需求是再使用var。
因此,
第一件事,给一个类加上open
关键字,表示这个类是专门为继承而设计的。
open class Person (val name: String, val age: Int){
init{
println("Person--Primary Constructor")
}
constructor() : this("**", 0){
println("Person--Secondary Constructor-1")
}
constructor(name: String) : this(name, 0){
println("Person--Secondary Constructor-2")
}
}
第二件事,让子类继承它,Java中继承的关键字是extends,而Kotlin中变成了一个冒号:
class Student : Person() {
var sno = ""
var grade = 0
}
除此之外,我们注意到Person类的后面加上了一对括号,这是Java中继承时所不需要的。这涉及到Java继承中的一个规定,子类中的构造函数必须调用父类的构造函数,这个规定在Kotlin中也要遵守。
子类的主构造函数调用父类中的哪个构造函数,在继承的时候通过括号来指定。 所以,Person类后面的一对空括号表示Student类的主构造函数在初始化的时候回调用Person类的无参构造函数,即使在无参数的情况下,这对括号也不能省略。
val student = Student()
我们创建一个Student对象试试,运行程序,输出结果如下:
可以看到,在实例化的过程中,调用了父类Person的第一个无参的次构造函数,而这个次构造函数又先调用了自己的主构造函数。
让我们再看一个例子:
class Student(val sno: String, val grade: Int, name: String, age: Int) : Person() {
init{
println("Student--Primary Constructor")
}
constructor(name: String, age: Int) : this("", 0, name, age) {
println("Student--Secondary Constructor-1")
}
constructor() : this("", 0){
println("Student--Secondary Constructor-2")
}
}
注意,我们在Student类的主构造函数中,增加name和age这两个字段时,不能再将它们声明成val,因为在主构造函数中声明成val或var的参数将自动称为该类的字段,这就会导致和父类中同名的name和age字段造成冲突。因此,这里的name和age参数前面不用再加任何关键字,让它的作用域仅限定在主构造函数当中即可。
这里我们定义了两个次构造函数:第一个次构造函数接收name和age参数,然后它又通过this
关键字调用了主构造函数,并将sno和grade这两个参数赋值成初始值;第二个次构造函数不接收任何参数,它通过this
关键字调用了我们刚才定义的第一个次构造函数,并将name和age参数也赋值成初始值,由于第二个次构造函数间接调用了主构造函数,因此这仍然是合法的。
此时,我们共有3种方式来对Student类进行实例化:
val student1 = Student() //第二个无参的次构造函数
val student2 = Student("Jack", 19) // 第一个带两个参数的次构造函数
val student3 = Student("a123", 5, "Jack", 19) // 主构造函数
输出结果如下:
student1的实例化过程:先调用自己的无参构造函数Sencondary Constructor-2,由2调用1,由1调用主构造函数,再由主构造函数调用父类Person的无参构造函数Sencondary Constructor-1,再由1调用主构造函数。
有一种很特殊的情况:类中只有次构造函数,没有主构造函数。这种情况十分少见,但在Kotlin中是允许的。当一个类没有显式地定义主构造函数且定义了次构造函数时,它就是没有主构造函数的。例如:
class Student : Person {
constructor(name: String, age: Int) : super(name, age){
}
}
注意这里的代码变化,首先Student类的后面没有显式地定义主构造函数,同时又因为定义了次构造函数,所以现在Student类是没有主构造函数的。那么既然没有主构造函数,继承Person类的时候也就不需要再加上括号了(前面说过,子类的主构造函数调用父类中的哪个构造函数,在继承的时候通过括号来指定)。
另外,由于没有主构造函数,次构造函数只能直接调用父类的构造函数,上述代码也是将this关键字换成了super
关键字,这部分和Java比较像。
接口
接口是用于实现多态编程的重要组成部分。我们都知道,Java是单继承结构的语言,任何一个类最多只能继承一个父类,但是却可以实现任意多个接口,Kotlin也是如此。
我们可以在接口中定义一系列的抽象行为,然后由具体的类去实现。下面还是通过具体的代码来学习一下,首先创建一个Study接口,并在其中定义几个学习行为。右击com.example.helloworld包→New→Kotlin File/Class,在弹出的对话框中输入“Study”,创建类型选择“Interface”。
interface Study {
fun readBooks()
fun doHomework()
}
接下来让Student类去实现Study接口:
class Student(name: String, age: Int) : Person(name, age), Study {
override fun readBooks() {
println(name + " is reading.")
}
override fun doHomework() {
println(name + " is doing homework.")
}
}
Java中继承使用的关键字是extends
,实现接口使用的关键字是implements
,而Kotlin中统一使用冒号:
,中间用逗号进行分隔。上述代码就表示Student类继承了Person类,同时还实现了Study接口。另外接口的后面不用加上括号,因为它没有构造函数可以去调用。
Study接口中定义了readBooks()和doHomework()这两个待实现函数,因此Student类必须实现这两个函数,如果Student类中没有对着两个函数进行实现,那编译器会报错。Kotlin中使用override
关键字来重写父类或者实现接口中的函数。
fun main() {
val student = Student("Jack", 19)
doStudy(student)
}
fun doStudy(mystudy: Study){
mystudy.readBooks()
mystudy.doHomework()
}
首先在main()里创建了一个Student类的实例,这里是可以直接调用该实例的readBooks()和doHomework()函数的。但我们没有这么做,而是将它传入到了doStudy()函数中。doStudy()函数接收一个Study类型的参数,由于Student类实现了Study接口,因此Student类的实例是可以传递给doStudy()函数的,接下来我们调用了Study接口的readBooks()和doHomework()函数,这种就叫作面向接口编程,也可以称为多态。
了让接口的功能更加灵活,Kotlin还增加了一个额外的功能:**允许对接口中定义的函数进行默认实现。**其实Java在JDK 1.8之后也开始支持这个功能了,因此总体来说,Kotlin和Java在接口方面的功能仍然是一模一样的。
interface Study {
fun readBooks()
fun doHomework() {
println("do homework default implementation.")
}
}
上述代码给Study接口中的函数doHomework()加上了函数体。**如果接口中的一个函数拥有了函数体,这个函数体中的内容就是它的默认实现。**现在当一个类去实现Study接口时,只会强制要求实现readBooks()函数,而doHomework()函数则可以自由选择实现或者不实现,不实现时就会自动使用默认的实现逻辑。