Java代码在运行前需要编译生成一种特殊的class文件,然后Java虚拟机会识别并解释这些class文件,而Kotlin作为一种新的编程语言,就是将其代码同样编译生成为这样的class文件,也就是说,虽然看起来是两者是两种不同的编程语言,但其实其实质都是一样的。
运行Kotlin代码大致存在三种方法:
这里便使用Android Studio运行Kotlin代码。
如在MainActivity的同级包结构下创建一个Kotlin文件,然后编写HelloWorld:
package com.example.helloworld
fun main() {
println("Hello world!")
}
在Android Studio的run中输出结果为:
Hello world!
不过这里编译中途报了一个错:
Manifest merger failed with multiple errors, see logs
这是因为sdk和Kotlin编译器版本不匹配,解决版本:
在Kotlin中定义一个变量,只允许在变量前声明两种关键字:
比如,这样的代码是合理的:
package com.example.helloworld
fun main() {
val a = 10
var b = 20
b = 30
println("a =" + a + ",b = " + b)
}
这是因为Kotlin采用了类型推导机制,但如果要对一个变量延迟赋值的话,就需要显式声明变量类型:
package com.example.helloworld
fun main() {
val a = 10
var b: Int
b = 30
println("a =" + a + ",b = " + b)
}
此时也就限定了变量b只能为Int类型,而不能使用其它类型的值进行赋值。
同时从上面可以看到,Kotlin的变量类型Int是大写的,这与Java不同,这也就意味着Kotlin中变量采用的对象数据类型,即Int是一个类,其它数据类型也是类似的,也需要大写。
Kotlin中函数的语法规则为:
fun func(param1:Int, param2:Int):Int {
return 0
}
其形式为:
比如,下面的代码就简单说明了函数的使用:
package com.example.helloworld
import kotlin.math.max
fun Larger(num1:Int, num2:Int):Int {
return max(num1, num2)
}
fun main() {
val a = 10
var b: Int
b = 30
println("Larger =" + Larger(a,b))
}
同时上面的形式还可以使用Kotlin中的语法糖形式。当一个函数中只有一行代码时,Kotlin中可以不用编写函数体,而将唯一的一行代码写在函数定义的末尾。比如上面的代码就可以转化为:
package com.example.helloworld
import kotlin.math.max
//fun Larger(num1:Int, num2:Int):Int = max(num1, num2)
fun Larger(num1:Int, num2:Int) = max(num1, num2)
fun main() {
val a = 10
var b: Int
b = 30
println("Larger =" + Larger(a,b))
}
Kotlin中的条件语句主要有两种实现形式:if语句和when语句。
先看一下if条件语句:
package com.example.helloworld
import kotlin.math.max
fun Larger(num1:Int, num2:Int):Int {
if (num1 > num2) {
return num1
} else {
return num2
}
}
fun main() {
val a = 10
var b: Int
b = 30
println("Larger =" + Larger(a,b))
}
而Kotlin中的if语句还有一个额外的功能,就是if语句是可以有返回值的,返回值就是if语句每一个条件中最后一行代码的返回值。因此,上面的代码可以写为:
package com.example.helloworld
import kotlin.math.max
fun Larger(num1:Int, num2:Int):Int {
return if (num1 > num2) {
num1
} else {
num2
}
}
fun main() {
val a = 10
var b: Int
b = 30
println("Larger =" + Larger(a,b))
}
即可以使用if语句来为return提供返回值,或者是为变量赋值。
而采用之前提到的语法糖的形式,又可以将代码变为:
package com.example.helloworld
import kotlin.math.max
fun Larger(num1:Int, num2:Int):Int = if (num1 > num2) num1 else num2
fun main() {
val a = 10
var b: Int
b = 30
println("Larger =" + Larger(a,b))
}
when条件语句有点像是Java中的switch语句。
package com.example.helloworld
import kotlin.math.max
fun getScore(name:String) = if (name == "Tom") {
86
} else if (name == "Jim") {
77
} else if(name == "Jack") {
95
} else {
0
}
fun main() {
println("Score =" + getScore("Tom"))
}
上面的代码使用when条件语句就是:
package com.example.helloworld
import kotlin.math.max
fun getScore(name:String) = when (name) {
"Tom" -> 86
"Jim" -> 77
"Jack" -> 95
else -> 0
}
fun main() {
println("Score =" + getScore("Tom"))
}
而当执行逻辑只有一行代码时,{}可以省略。
同时when语句还允许进行类型匹配:
package com.example.helloworld
import kotlin.math.max
fun checkNumber(num:Number) {
when (num){
is Int -> println("num is Int")
is Double -> println("num is Double")
else -> println("no support")
}
}
fun main() {
checkNumber(10.1)
}
同时还可以使用when的不带参数的用法:
package com.example.helloworld
import kotlin.math.max
fun getScore(name:String) = when {
name == "Tom" -> 86
name == "Jim" -> 77
name == "Jack" -> 95
else -> 0
}
fun main() {
println("Score =" + getScore("Tom"))
}
Kotlin中循环语句也存在两种形式,while循环和for循环。
不过Kotlin中的while循环和Java中的while循环使用方法类似,这里只说明for循环。
package com.example.helloworld
fun main() {
for (i in 1..10) {
println(i)
}
}
在上面的for循环中,1..10是区间的形式,即[1, 10]。可以看出这里的for循环和Java中的for循环形式还是有区别的。
但如果需要区间[1, 10)的话,就是下边的形式:
package com.example.helloworld
fun main() {
for (i in 1 until 10) {
println(i)
}
}
而如果要修改步长的话,就是下边的形式:
package com.example.helloworld
fun main() {
for (i in 1 until 10 step 2) {
println(i)
}
}
而如果需要一个降序的闭区间,就是下边的形式:
package com.example.helloworld
fun main() {
for (i in 10 downTo 1) {
println(i)
}
}
package com.example.helloworld
class Person {
var name = ""
var age = 0
fun printInfo() {
println("name is " + name + ",age is " + age)
}
}
fun main() {
var tmp = Person()
tmp.name = "Tom"
tmp.age = 15
tmp.printInfo()
}
从上面来看,Kotlin中类的定义和实例化和Java是类似的,只是去掉了new关键字。
这里先看一个继承的简单示例:
package com.example.helloworld
open class Person {
var name = ""
var age = 0
fun printInfo() {
println("name is " + name + ",age is " + age)
}
}
class Student:Person() {
var grade = 0
}
fun main() {
var tmp = Student()
tmp.name = "Tom"
tmp.age = 15
tmp.grade = 3
}
在上面的继承中,存在两个和Java代码不同的地方:
上面只是Kotlin中继承的简单示例,这里再看一下构造函数。Kotlin中构造函数分为两种:主构造函数和次构造函数。
主构造函数没有函数体,直接定义再类名后面,每个类默认都会有一个不带参数的主构造函数,用户也可以为其显式指明参数。
package com.example.helloworld
open class Person {
var name = ""
var age = 0
fun printInfo() {
println("name is " + name + ",age is " + age)
}
}
class Student(val grade:Int):Person() {
}
fun main() {
var tmp = Student(10)
tmp.name = "Tom"
tmp.age = 15
}
上面直接在构建Student对象时就进行了属性赋值,而如果需要在主构造函数中编写一些代码,就需要init结构体:
class Student(val grade:Int):Person() {
init {
println("grade is " + grade)
}
}
这里就可以解释为什么Kotlin类继承中父类后需要存在括号。根据继承特性的规定,子类的构造函数必须调用父类的构造函数,可是主构造函数并没有函数体,因此Kotlin中的子类主构造函数通过括号指定调用父类的哪个构造函数。
也即上面的内容中,表示Student类的主构造函数在初始化时会调用person类的无参构造函数,即使在午餐的情况下,该括号也不可省略。
package com.example.helloworld
open class Person(val name:String, val age:Int) {
fun printInfo() {
println("name is " + name + ",age is " + age)
}
}
class Student(name:String, age:Int, val grade:Int):Person(name, age) {
init {
println("grade is " + grade)
}
}
fun main() {
var tmp = Student("Tom", 10,3)
}
上面的形式就可以调用父类的有参构造形式,不过需要保证父类存在该形式。
而在Student类的参数声明中,也没有将父类的参数声明为val,这是因为在主构造函数中声明为val或var的参数将自动称为该类的字段,会导致冲突。因此Student中name和age的声明前不用加任何关键字,使其作用域限定在主构造函数中。
而次构造函数在一个类中可以存在多个,次构造函数也可以用于实例化一个类,这点和主构造函数没有什么不同,只是次构造函数是有函数体的。
Kotlin规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(间接调用):
package com.example.helloworld
open class Person(val name:String, val age:Int) {
fun printInfo() {
println("name is " + name + ",age is " + age)
}
}
class Student(name:String, age:Int, val grade:Int):Person(name, age) {
init {
println("grade is " + grade)
}
constructor(name:String, age:Int):this(name, age, 0) {
}
constructor():this("",0) {
}
}
fun main() {
var tmp = Student("Tom", 10,3)
}
从上面可以看到,次构造函数是通过sonstructor关键字来定义的,上面定义了两个次构造函数,又通过次构造函数调用了主构造函数,完成了类对象的实例化。
而当类中只有次构造函数,没有主构造函数时:
package com.example.helloworld
open class Person(val name:String, val age:Int) {
fun printInfo() {
println("name is " + name + ",age is " + age)
}
}
class Student:Person {
constructor(name:String, age:Int):super(name, age) {
}
constructor():super("",0) {
}
}
fun main() {
var tmp = Student()
}
比如上面的代码,Student没有显式定义主构造函数,但是存在两个次构造函数,即此时没有主构造函数,而既然子类没有主构造函数,继承父类时就不需要加上括号了。而由于没有主构造函数,次构造函数只能直接调用父类的构造函数,因此也就使用了super关键字。
package com.example.helloworld
interface Study {
fun readBook()
fun doHomework()
}
open class Person(val name:String, val age:Int) {
fun printInfo() {
println("name is " + name + ",age is " + age)
}
}
class Student(name:String, age:Int, val grade:Int):Person(name, age), Study {
init {
println("grade is " + grade)
}
override fun readBook() {
println(name + " is reading.")
}
override fun doHomework() {
println(name + " is doing Homework.")
}
}
fun doStudy(study: Study) {
study.readBook()
study.doHomework()
}
fun main() {
var tmp = Student("Tom", 10,3)
doStudy(tmp)
}
上面的代码中,定义了父类和接口,并在子类中继承了父类,实现了接口。Java中继承使用extends关键,接口实现使用implements关键字,而Kotlin中统一使用逗号分隔。同时接口后面不需要添加括号,因为其没有构造函数调用。
子类接口中方法的实现要使用override关键字来声明,而在main中调用doStudy,doStudy接口Study接口的形式,然后调用接口中函数,这个过程就叫做多态。
同时Kotlin中还支持对接口的默认实现:
package com.example.helloworld
interface Study {
fun readBook()
fun doHomework() {
println("do homework default implements")
}
}
open class Person(val name:String, val age:Int) {
fun printInfo() {
println("name is " + name + ",age is " + age)
}
}
class Student(name:String, age:Int, val grade:Int):Person(name, age), Study {
init {
println("grade is " + grade)
}
override fun readBook() {
println(name + " is reading.")
}
}
fun doStudy(study: Study) {
study.readBook()
study.doHomework()
}
fun main() {
var tmp = Student("Tom", 10,3)
doStudy(tmp)
}
上面接口的定义中,doHomework中存在函数体,因为接口实现只会强制要求实现readBook,而不会要求强制实现doHomework,这样子类Student调用的就是接口的默认实现了。
在上面的代码中,并不对属性和方法进行隔离,即没有使用可见性修饰符对其变量和方法进行修饰,而Java中存在public,private,protected和default四种修饰符来实现封装和隔离。
在Kotlin中,也存在四种修饰符,分别是public,private,protected和internal,需要使用哪种修饰符时,直接在fun关键字前定义即可。
在项目开发中,数据类通常需要重写equals,hashCode,toString等方法,但其实这些代码具有极高的重复性。
而在Kotlin中,创建数据类可以直接使用如下代码:
package com.example.helloworld
data class Cellphone(val brand:String, val price:Double)
fun main() {
val Cellphone1 = Cellphone("HW", 5648.5)
val Cellphone2 = Cellphone("HW", 5648.5)
println(Cellphone1)
println(Cellphone1 == Cellphone2)
}
在上面的代码中,使用data来声明一个类会自动生成equals,hashCode,toString等方法,以减少重复性的开发工作。
同时当一个类中没有任何代码时,可以省略末尾的大括号。
而在Kotlin中,创建单例类的方式也很简单,只需要将class关键字修改为object关键字即可:
package com.example.helloworld
object Singleton {
fun singletonTest() {
println("Test")
}
}
fun main() {
Singleton.singletonTest()
}
上面的代码也就实现了单例模式,Kotlin会自动创建一个Singleton的实例,并保证全局只会存在一个Singleton实例。
如果用户想要创建一个字符串列表的实例,可能需要构建ArrayList
package com.example.helloworld
fun main() {
var tmp = listOf("apple","banana","orange")
for(item in tmp) {
println(item)
}
}
上面的代码就使用了listOf来创建字符串列表,然后通过for循环逐个进行打印。不过listOf函数创建的是一个不可变的集合,即只可用于读取,而无法修改。
而如果需要创建可以修改的集合,就需要使用mutableListOf函数:
package com.example.helloworld
fun main() {
var tmp = mutableListOf("apple","banana","orange")
tmp.add("pear")
for(item in tmp) {
println(item)
}
}
上面提到的是List的用法,而Set集合的用法大致类似,不过函数名换为了setOf和mutableSetOf。不过Set集合中存放的都是唯一元素,即不存在重复元素。
而Map的添加之前是先创建HashMap
package com.example.helloworld
fun main() {
var tmp = HashMap()
tmp["apple"] = 10
tmp["banana"] = 20
tmp["orange"] = 30
for ((key, value) in tmp) {
println("key is " + key + ",value is " + value)
}
}
这样的写法看起来也有点麻烦,在Kotlin中还可以使用mapOf和mutableMapOf函数来简化该过程:
package com.example.helloworld
fun main() {
var tmp = mapOf("apple" to 10, "banana" to 20, "orange" to 30)
for ((key, value) in tmp) {
println("key is " + key + ",value is " + value)
}
}
这样看起来就简洁多了。
这里主要看一下Lambda表达式的语法结构。
比如要查找列表中字符串最长的元素:
package com.example.helloworld
fun main() {
var tmp = listOf("apple","banana","orange")
var maxLengthFruit = ""
for (item in tmp) {
if (item.length > maxLengthFruit.length) {
maxLengthFruit = item
}
}
println("max length fruit is " + maxLengthFruit)
}
上面的代码可以实现,但是如果使用集合的函数式API,则有:
package com.example.helloworld
fun main() {
var tmp = listOf("apple","banana","orange")
var maxLengthFruit = tmp.maxByOrNull { it.length }
println("max length fruit is " + maxLengthFruit)
}
通俗来讲,Lambda是一小段可以作为参数传递的代码,其语法结构为:
{param1:type,param2:type -> func}
上面定义中:
比如上面的代码,其本质为:
package com.example.helloworld
fun main() {
var tmp = listOf("apple","banana","orange")
var lambda = {fruit:String -> fruit.length}
var maxLengthFruit = tmp.maxByOrNull(lambda)
println("max length fruit is " + maxLengthFruit)
}
即maxByOrNull是一个普通的函数,其可接收一个Lambda形式的参数,并且在遍历集合时将每次遍历的值作为参数传递到Lambda表达式,然后找到其最大值。
上面的代码当然也可以这样写:
package com.example.helloworld
fun main() {
var tmp = listOf("apple","banana","orange")
var maxLengthFruit = tmp.maxByOrNull({fruit:String -> fruit.length})
println("max length fruit is " + maxLengthFruit)
}
而Kotlin规定,当Lambda参数式函数的最后一个参数时,可以将Lambda表达式移到函数括号的外面:
package com.example.helloworld
fun main() {
var tmp = listOf("apple","banana","orange")
var maxLengthFruit = tmp.maxByOrNull(){fruit:String -> fruit.length}
println("max length fruit is " + maxLengthFruit)
}
而如果Lambda表达式是函数唯一的参数,可以将函数括号省略:
package com.example.helloworld
fun main() {
var tmp = listOf("apple","banana","orange")
var maxLengthFruit = tmp.maxByOrNull{fruit:String -> fruit.length}
println("max length fruit is " + maxLengthFruit)
}
而Kotlin又存在类型推导机制,因此Lambda表达式中的参数列表多数情况下可以不必声明参数类型:
package com.example.helloworld
fun main() {
var tmp = listOf("apple","banana","orange")
var maxLengthFruit = tmp.maxByOrNull{fruit -> fruit.length}
println("max length fruit is " + maxLengthFruit)
}
而当Lambda表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用it关键字代替:
package com.example.helloworld
fun main() {
var tmp = listOf("apple","banana","orange")
var maxLengthFruit = tmp.maxByOrNull{it.length}
println("max length fruit is " + maxLengthFruit)
}
这样也就形成了最初的形式。
这里再看几个其它的API:
package com.example.helloworld
fun main() {
println(listOf("apple","banana","orange").map{it.uppercase()})
println(listOf("apple","banana","orange").filter{it.length > 5})
println(listOf("apple","banana","orange").any{it.length > 5})
println(listOf("apple","banana","orange").all{it.length > 5})
}
如果在Kotlin代码中调用了Java方法,并且该方法接收一个Java单抽象方法接口参数,就可以使用函数式API。Java单抽象方法接口指接口中只有一个待实现方法,如果接口中存在多个待实现方法,则无法使用函数式API。
比如Java原生API中有一个常见的单抽象方法接口Runnable,该接口存在一个待实现的run方法:
public interface Runnable {
void run();
}
而之前提到,对于任何一个Java方法,只要其接收Runnable参数,就可以使用函数式API。而Runnable主要是结合线程一块使用的,这里通过Java的线程类Thread来看一下:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread is running!")
}
}).start();
上面采用了匿名类的写法,创建了Runnable接口的匿名类实例,并将之传递给Thread类的构造方法,然后调用Thread类的start方法执行该线程。
而写成Kotlin代码,则是:
package com.example.helloworld
fun main() {
Thread(object :Runnable {
override fun run() {
println("Thread if running")
}
}).start()
}
上面代码的形式类似,不过写法有些许差异,这是因为Kotlin舍弃了new关键字,因此创建匿名类就改用了object关键字。而符合Java函数式API使用条件,就可以对代码进行简化:
package com.example.helloworld
fun main() {
Thread(Runnable {
println("Thread if running")
}).start()
}
上面的代码,Kotlin会明白Runnable后面的Lambda表达式就是要在run方法中实现的内容,同时如果Java方法的参数列表有且仅有一个Java单抽象方法接口函数,便可以对接口名进行省略:
package com.example.helloworld
fun main() {
Thread{ println("Thread if running") }.start()
}
这样的话,Android中常用的点击事件接口OnClickListener,原始形式为:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
就可以简化为:
button.setOnClickListener{ };
先看一段之前的代码:
fun doStudy(study: Study) {
study.readBook()
study.doHomework()
}
上面的代码并没有进行判空处理。
实际开发中,Java代码的大部分的方法都需要首先进行判空处理才可以保证后续的处理正常进行。而这样的检查会导致代码编写较为繁琐。而Kotlin利用编译时判空检查几乎能够杜绝空指针异常,
还是上面的代码,虽然形式上和Java代码并没有什么区别,但实际上是没有空指针风险的,因为Kotlin默认所有的参数和变量都不可为空,即上面传入的参数study一定不为空,因此上面的函数便可以正常调用。
即Kotlin将空指针异常的检查提前到了编译时期,即在编译期间存在空指针异常风险,会导致编译报错,而不是将错误推迟到代码执行期间,这样能保证在程序运行期间不会出现空指针异常。
而如果所有的参数和变量都不可为空,那么确实需要某个参数或变量为空时,就需要Kotlin提供的另外一套可为空的类型系统。
这种类型系统就是在类名后加上一个问号,比如上面的代码中,在Study后加上问号就表示此时参数study可以为空,不过此时要做判空处理:
fun doStudy(study: Study?) {
if (study != null) {
study.readBook()
study.doHomework()
}
}
在判空处理中,如果使用if处理,就会使代码变得比较繁琐,因此Kotlin提供了一系列辅助工具。
首先最常用的就是?.操作符,该操作符是当对象不为空时正常调用相应的方法,而当对象为空时则什么都不做:
fun doStudy(study: Study?) {
study?.readBook()
study?.doHomework()
}
还有一个是?:操作符,该操作符左右两边都接收一个表达式,如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果:
fun getTextLength(text: String?):Int {
if (text != null) {
return text.length
} else {
return 0
}
}
如果使用辅助判空工具,就可以写成:
fun getTextLength(text: String?) = text?.length?:0
可以说是相当简洁了。
再比如下面的代码:
package com.example.helloworld
var content: String?="hello"
fun printUpperCase() {
val upperCase = content.toUpperCase()
println(upperCase)
}
fun main() {
if (content != null) {
printUpperCase()
}
}
虽然在main函数中已经进行了判空处理,但是在printUpperCase中并不清楚外部已经进行该处理,因此还是无法编译通过。
此时如果想要强行通过编译,可以使用非空断言工具,写法是在对象后面加上!!:
fun printUpperCase() {
val upperCase = content!!.toUpperCase()
println(upperCase)
}
最后一个辅助工具是let,let既不是操作符,也不是关键字,而是一个函数,该函数提供了函数式API的编程接口,并将原始调用对象作为对象传递到Lambda表达式中,如:
obj.let {obj2 -> func}
上面的示例中,调用了obj对象的let函数,然后Lambda表达式中的代码就会立即执行,并且该obj对象本身还会作为参数传递到Lambda表达式中。obj2只是更换了参数名,实际上和obj是同一个对象。
let函数和判空处理结合的操作为:
fun doStudy(study: Study?) {
study?.let {
it.readBook()
it.doHomework()
}
}
这样就将let函数和判空处理结合起来了,而不用每个接口方法调用都是用?.操作符。
同时let函数是可以处理全局变量的判空问题,而if则会由于全局变量的值虽然会被其它线程更改而报错。
Kotlin允许在字符串中嵌入${}这中语法结构的表达式,并在运行时使用表达式执行的结果替代该内容:
"hello,${obj.name}. nice to meet you"
而当表达式中仅有一个变量名时,还可将两侧的大括号省略:
"hello,$name. nice to meet you"
之前提到了次构造函数,但其实次构造函数很少用,因为Kotlin能够为函数设定参数默认值。
fun printParams(num:Int, str:String = "hello") {
println("num is $num, str is $str")
}
这样在调用的时候,就可以只传入num变量,而为str赋默认的值。
而如果调换默认值的位置:
fun printParams(num:Int = 10, str:String) {
println("num is $num, str is $str")
}
此时调用时便不可只写一个参数,而要使用键值对的形式进行匹配,即:
printParams(str = "hello")
使用键值对的形式,便不用在意参数顺序的问题,因为是通过键值对匹配的,自然不会出现类型不匹配的问题。
这样默认的参数值在某些场景下便能够代替次构造函数的功能。