变量、常量和类型
声明变量
在Java中声明变量
int a = 1;
在kotlin中声明变量
var a : Int = 1
可以看出在kotlin中声明变量的格式:
var 变量名 : 类型定义 = 赋值
只读变量
除了使用var
来声明变量,还可有使用val
来声明变量。但这二者的作用是不同的,val
是只读变量,使用了val
来定义变量后,该变量是不能改变的。其有些类似于Java中的final,但也有不同之处,后面会讲到。
var a : Int = 1
a = 10 //没问题
val b : Int = 1
b = 10 //报错
类型推断
对于已声明并赋值的变量,允许省略类型定义
var a : Int = 1
var s : String = "Hello World"
//可以直接写成
var a = 1
var s = "Hello World"
编译时常量
- 编译时常量只能在函数之外声明,因为编译时常量必须在编译时赋值,而函数都是在运行时才调用,函数内的变量也是在运行时赋值,编译时常量要在这些变量赋值前就已存在。
- 编译时常量只能是基本的八种数据类型,Int、Double、Float、Long……
可以将Kotlin代码转换成Java的代码来查看
可以发现kotlin中const val MAX
其实就等效于java中的final static
数据类型
- 在Java中有两种数据类型,除了八种基本数据类型,其余的都是引用类型
- 而在Kotlin中只有引用类型,出于更高性能的需要,Kotlin编译器会自动在Java字节码中改用基本数据类型
其实区别主要是装箱方面不同,拿整数类型的变量解释,java中我们定义一个整型变量可以通过int
(基本类型)或者Integer
(引用类型)来定义一个整数类型。
//java中
int a = 1;
Integer b = 2; //自动装箱
而在Kotlin中,只有Int
(引用类型)来定义整数类型。
var a : Int = 10;
但是Kotlin编译器会根据性能需要自动判断是否对该变量进行装箱。
var a: Int = 1 // unbox
var b: Int? = 2 // box
var list: List<Int> = listOf(1, 2) // box
条件语句
- if/else if 表达式(和Java一样)
- range表达式
- in A..B,in关键字用来检查某个值是否在指定范围之内
val money = 10
if(money in 0..5){
println("cheap")
}else if(money in 5..10){
println("mid")
}else{
println("expensive")
}
输出:
mid
- when表达式(其实就是java中的switch..case)
val money = 10
when (money) {
in 0..5 -> {
println("cheap")
}
in 5..10 -> {
println("mid")
}
else -> {
println("expensive")
}
}
string模板
- 模板支持在字符串的引号内放入变量值
- 还支持字符串里计算表达式的值并插入结果
- 格式
$变量名
或${表达式}
fun main(){
val a = 10
println("value = $a")
val flag = false
println("答案是${if(flag) "正确的" else "错误的"}")
}
输出
value = 10
答案是错误的
函数
函数声明
//格式
private fun 函数名(函数参数):返回类型 {}
fun main(){
println("value = ${getValue(100)}")
}
private fun getValue(money:Int):String{
var value = "";
value = when (money) {
in 0..5 -> {
"cheap"
}
in 5..10 -> {
"mid"
}
else -> {
"expensive"
}
}
return value
}
输出
value = expensive
函数参数
- 默认值参
- 如果不打算传入值参,可以预先给参数指定默认值
fun main(){
//如果所调用的方法参数已经给了默认值,可以不传参数
println("value = ${getValue()}")
}
//可以给参数设置默认值,若调用该方法不穿参数的话,就直接使用该默认值
private fun getValue(money:Int = 100):String{
var value = "";
value = when (money) {
in 0..5 -> {
"cheap"
}
in 5..10 -> {
"mid"
}
else -> {
"expensive"
}
}
return value
}
- 具名函数参数
- 如果使用命名值参,就可以不用管值参的顺序(参数多的时候用起来方便)
fun main(){
println("value = ${test(f = 20,d = 30,b = 100,a = "hhh",c = 1,e = 21)}")
}
private fun test(a:String,b:Int,c:Int,d:Int,e:Int,f:Int):String{
return a;
}
输出:
value = hhh
Unit函数
Kotlin中没有返回值的函数叫Unit函数,类似Java中的void。
fun main(){
getValue()
}
private fun getValue(money:Int = 100):Unit{
when (money) {
in 0..5 -> {
println("cheap")
}
in 5..10 -> {
println("mid")
}
else -> {
println("expensive")
}
}
}
输出:
expensive
匿名函数
定义时不取名字的函数,匿名函数通常整体传递给其他函数,或者从其它函数返回。通过匿名函数,能够在Kotlin中根据需要给标准函数指定特殊规则。
fun main(){
//统计字符个数
val num1 = "HAHAHAHAHDDC".count()
//统计字符串中`H`字符的个数
val numH = "HAHAHAHAHDDC".count({ letter ->
letter == 'H'
})
println("num1 = $num1,numH = $numH")
}
输出
num1 = 12,numH = 5
上面统计字符创中‘H’的个数中,count()
就是标准函数,括号中的{变量->函数体}
就是匿名函数,是我为count()
这个标准函数定义的一个规则,让count()
去统计字符串中‘H’的个数。
现在可能会看不太懂为什么这段代码可以这么写,下面会解释。
匿名函数类型与隐式返回
- 匿名函数也有类型,它可以当做变量进行赋值。
//格式
val 变量名:匿名函数 //变量类型是一个函数
fun main() {
//变量a是一个函数类型,该函数无参,返回值为Int类型
val a : () -> Int = {
val b = 10
val c = 20
b + c
}
println(a())
}
输出
30
- 和具名函数不一样,匿名函数一般不需要return关键字来返回数据,匿名函数会隐式或自动返回函数体最后一行语句的结果
这里抛出一个问题,正常来讲函数是不能赋值给变量的,如下面这个例子
fun main() {
val a = add() //这是函数调用,没有问题
val fun = add //这是将函数赋值给fun这个变量,编译会报错
}
fun add() : Int{
val b = 10
val c = 20
return b + c
}
那为什么匿名函数可以赋值给变量呢?匿名函数难道不是「函数」吗?继续往下看就会懂了。
匿名函数参数
和具名函数一样,匿名函数可以不带参数,也可以带一个或多个任何类型的参数,需要带参数时,参数的类型放在匿名函数的类型定义中,参数名则放在函数定义中
fun main() {
val a : (Int) -> Int = { num->
num + 20
}
println(a(10))
}
输出
30
it关键字
定义只有一个参数的匿名函数时,可以使用it关键字来表示参数名。但是当传入两个或两个以上的参数时,it关键字就不能用了
fun main() {
val a : (Int) -> Int = {
it + 20
}
println(a(10))
}
输出
30
类型推断
匿名函数类型也存在类型推断,当定义一个变量时,如果已把匿名函数作为变量赋值给它时,就不需要显示指明变量类型了
fun main() {
/*val a : () -> Int = {
val b = 10
val c = 20
b + c
}*/
//上面注释这段代码等效下面这段
val a = {
val b = 10
val c = 20
b + c
}
println(a())
}
输出
30
类型推断也支持带参数的匿名函数,但为了帮助编译器更准确地推断变量类型,匿名函数的参数名和参数类型必须要写
fun main() {
/*val a : (Int,Int) -> Int = {a,b ->
a + b
}*/
//上面注释这段代码等效下面这段
val a = {a:Int,b:Int ->
a + b
}
println(a(10,20))
}
输出
30
lambda
如果你学过Java,有没有发现上面Kotlin的匿名函数写法似曾相识,很像Java中的Lambda表达式。其实Kotlin中我们也将匿名函数称为lambda,将它定义为lambda表达式。
那Java和Kotlin的lambda有什么区别呢。刚刚上面也说了,在kotlin中,函数是可以作为变量的类型,也就是说函数是可以作为函数的参数来用的(有点绕口,后面详细讲下就明白了)。Java中我们是不能直接传递一个函数/方法的,当然借助接口可以实现类似的效果,但kotlin中直接支持传递函数无疑是方便了许多。
定义参数是函数的函数
在kotlin中支持函数的参数是另外一个函数,也就是说可以调用一个函数其参数可以是另外一个函数。
fun main() {
//定义一个函数类型的变量
val calStudentAge = {birYear:Int,year:Int ->
year - birYear
}
//传入的最后一个参数就是我们上面定义的函数类型变量
showStudentMsg("barry",2001,2021,calStudentAge)
}
//该函数的最后一个参数是一个名为getStudentAge的函数
fun showStudentMsg(name:String,birYear:Int,year:Int,getStudentAge : (Int,Int)->Int){
println("${name}同学${birYear}年出生,现在是${year}年")
println("所以${name}同学今年${getStudentAge(birYear,year)}岁了")
}
输出
barry同学2001年出生,现在是2021年
所以barry同学今年20岁了
简略写法
如果一个函数的lambda参数排在最后,或是唯一的参数,那么lambda参数的圆括号可以省略。
上面的调用showStudenMsg
代码我可以直接这样改写
showStudentMsg("barry",2001,2021,{birYear:Int,year:Int ->
year - birYear
})
此时我们的idea就会有以下的提示:
Lambda argument should be moved out of parentheses
也就是提示我们应将 Lambda 参数移出括号,因为此时就符合lambda参数排在最后的条件,所以正确的写法应该是:
showStudentMsg("barry",2001,2021) { birYear: Int, year: Int ->
year - birYear
}
我们在看回匿名函数这节中最开始的那个例子
val numH = "HAHAHAHAHDDC".count({ letter ->
letter == 'H'
})
现在可以理解为什么可以这么去写这段代码了吧,这是因为count这个函数参数支持我们传递一个函数。看下count函数的源码:
public inline fun CharSequence.count(predicate: (Char) -> Boolean): Int {
var count = 0
for (element in this) if (predicate(element)) ++count
return count
}
通过源码可以看到count函数中的参数,是一个返回类型为Boolean
,传递参数为Char
类型,名字为predicate
的函数。我把上面的调用改写一下可能会更好理解
val predicate = { letter:Char ->
letter == 'H'
}
val numH = "HAHAHAHAHDDC".count(predicate)
首先定义一个类型为函数的变量predicate,然后调用count时传入这个predicate变量,count函数就会通过我们这个匿名函数,对字符串进行遍历,如果element == 'H',那么count就+1。
上面的调用还可以简写,刚刚说了如果匿名函数中只有一个参数,那么可以用it代替
val predicate = {
it == 'H'
}
val numH = "HAHAHAHAHDDC".count(predicate)
并且count函数也符合lambda参数是唯一参数,所以可以写成
val numH = "HAHAHAHAHDDC".count{
it == 'H'
}
函数内联
我们在使用lambda
表达式时,它会被正常地编译成一个匿名类。说明每调用一次lambda
表达式,一个额外的类就会被创建,并且如果lambda
捕捉了某个变量,那么每次调用的时候都会创建一个新的对象,这会带来运行时的额外开销,导致使用lambda
比使用一个直接执行相同代码的函数效率更低。
所以kotlin中提供了一种优化机制——内联(inline
),有了内联,JVM就不需要使用lambda对象实例了。如果使用inline
修饰符标记一个函数,在函数被调用的时候编译器并不会生成函数调用的代码,而是 使用函数实现的真实代码替换每一次的函数调用。
可以来看下使用内联和不使用内联的字节码
未使用内联:
fun main() {
showStudentMsg("barry",2001,2021) { birYear: Int, year: Int ->
year - birYear
}
}
fun showStudentMsg(name:String,birYear:Int,year:Int,getStudentAge : (Int,Int)->Int){
println("${name}同学${birYear}年出生,现在是${year}年")
println("所以${name}同学今年${getStudentAge(birYear,year)}岁了")
}
可以main函数里看到未使用内联的是会直接调用showStudentMsg这个函数,调用后会产生一个Function2
类的对象getStudentAge
,然后通过invoke
方法来执行,这会增加额外的生成类和函数调用开销。
使用内联:
现在方法前加上inline
修饰符
fun main() {
showStudentMsg("barry",2001,2021) { birYear: Int, year: Int ->
year - birYear
}
}
inline fun showStudentMsg(name:String,birYear:Int,year:Int,getStudentAge : (Int,Int)->Int){
println("${name}同学${birYear}年出生,现在是${year}年")
println("所以${name}同学今年${getStudentAge(birYear,year)}岁了")
}
使用了inline
修饰后,也就是showStudentMsg成为了内联函数,那么此时我们再看main函数中,这时就不会再去调用showStudentMsg这个函数了,而是直接将showStudentMsg函数中的代码粘贴到了相应调用的位置。就等于是直接用函数的真实代码替换了函数调用,节省了额外的生成类和函数调用开销。
当然内联函数不是万能的,以下情况避免使用内联函数:
- 像上面使用内联后,会发现字节码数量相比没有内联是大大增多。所以尽量避免对具有大量函数体的函数进行内联,这样会导致过多的字节码数量。
- 由于JVM对普通函数已经能够根据实际情况智能地判断是否进行内联优化,所以并不需要对其使用Kotlin的inline语法,那只会让字节码变得更加复杂。
- 使用lambda的递归函数无法内联,因为会导致真实代码复制替换无限循环
在上面的字节码中,我们可以看到showStudent
这个函数中最后一个参数是一个Function类的引用,那就说明传递的参数是一个对象。可是我们在kotlin代码里面传递不是一个匿名函数吗,不应该是一个「函数」吗?下面就开始解密了
函数引用
调用带参数类型是函数的函数,除了传lambda表达式,kotlin还提供了其他方法,传递函数引用,函数引用可以把一个具名函数转换成一个值参,使用lambda表达式的地方,都可以使用函数引用。
fun main() {
//通过::将具名函数转换成一个对象进行传递
showStudentMsg("barry",2001,2021,::calStudentAge)
}
//将上面的匿名函数改造成一个具名函数
private fun calStudentAge(birYear:Int,year:Int) : Int{
return year - birYear
}
private fun showStudentMsg(name:String,birYear:Int,year:Int,getStudentAge : (Int,Int)->Int){
println("${name}同学${birYear}年出生,现在是${year}年")
println("所以${name}同学今年${getStudentAge(birYear,year)}岁了")
}
kotlin中函数可以作为参数进行传递,实际上传递还是一个对象。也就是说函数在Kotlin 里可以作为对象存在。那我们如何让具名函数变成一个对象呢,其实就是通过::
来实现的。只有在函数名前加上两个冒号,该函数才能变成一个对象进行传递。
在 Kotlin 里,一个函数名的前面加上两个冒号,它就不表示这个函数本身了,而表示一个对象,或者说一个指向对象的引用。
我们现在可以回到上面的一个问题,为什么匿名函数可以赋值给变量,而正常函数不可以呢?其实根据上面的讲述,大概也知道为什么了吧,现在我把之前的赋值代码换一下
fun main() {
val a = add() //这是函数调用,没有问题
//val fun = add //这是将函数赋值给fun这个变量,编译会报错
val fun = ::add //这是将一个函数类型的对象赋值给fun这个变量,没有问题
}
fun add() : Int{
val b = 10
val c = 20
return b + c
}
匿名函数/lambda 可以赋值给变量或者作为参数进行传递,是因为kotlin中匿名函数其实不是真正意义上的「函数」,而是一个对象,一个函数类型的对象。它和加了::
的具名函数是一个东西。
所以java中的lambda和kotlin中的lambda最大的区别就是,java中的lambda只是写法更加便捷,但并没有什么实质的变化。而kotlin中的lambda是实实在在的对象。
函数类型作为返回类型
既然kotlin中函数可以转变为一个对象,那么函数同样可以作为一个函数的返回类型
fun main() {
val studentMsg = getStudentMsg()
print(studentMsg("barry"))
}
//返回类型为函数的函数
fun getStudentMsg() : (String) -> String{
//返回匿名函数,也就是返回一个函数类型的对象
return {name:String->
val age = 20
"${name}同学今年${age}岁了"
}
}
在main函数中,通过getStudentMsg()
获取到一个类型为函数的对象,然后这个studentMsg("barry")
这个操作其实是kotlin的一个语法糖,它等效于下面的代码:
studentMsg.invoke("barry")
所以这个studentMsg其实就是一个引用类型,指向了getStudentMsg()
这个类型为函数的对象。
闭包
在kotlin中,匿名函数能修改并引用定义在自己的作用域之外的变量。匿名函数引用着定义自身的函数里的变量,一个函数引用着另一个函数声明的变量,这其实就是闭包。
像上面的代码可以改写成这样:
fun main() {
val studentMsg = getStudentMsg()
print(studentMsg("barry"))
}
//返回类型为函数的函数
fun getStudentMsg() : (String) -> String{
//age变量是在getStudentMsg()函数里定义的
val age = 20
//返回匿名函数,也就是返回一个函数类型的对象
return {name:String->
//在匿名函数里我们引用了getStudentMsg()函数里的age变量
"${name}同学今年${age}岁了"
}
}
上面return后面跟着的{ ... }
就是一个闭包
什么是闭包?
我们知道,变量分为全局变量和局部变量,全局变量,顾名思义,其作用域是当前文件甚至文件外的所有地方;而局部变量只能在其有限的作用域里获取。
那么,如何在外部调用局部变量呢?就是通过闭包,闭包就是能够读取其他函数内部变量的函数。
那kotlin中使用闭包的意义在哪呢,为什么在Java中好像没怎么听过闭包这个概念?
Java其实也是有闭包的,一般会在匿名内部类才能体现出来,但Java中的闭包是一种有“残缺”的闭包,为什么这么说,可以看下下面这篇文章。
在上面编写kotlin代码中,可以发现和编写Java代码时有些不同。在Java中,我们在编写代码时,必须要有包(Package)和类(Class),就比如一个简单地输出Hello World,在Java中:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
而在kotlin中:
fun main(){
println("Hello World")
}
那么Java中有包和类这两个东西,它会给我们带来作用域上的保护。我先拿kotlin举一个例子,比如我现在创建两个kotlin文件,然后在两个文件中都定义一个方法test,这是它就会报错
当然是用private修饰的话可以解决,但在Java中我创建两个Java文件,同样在两个文件中都定义一个方法test并用public
进行修饰,是不会报错的。这是因为这两个Java文件虽然在同一个包下,但它们是不同的类,所以并不会影响。
Kotlin中也含有类,但Kotlin也有脚本语言的特性,所以它可以让我们不用使用类也能进行函数的使用,所以一旦使用到了脚本语言的特性,就可能会出现作用域的问题,同名变量或者函数就会产生冲突。所以kotlin中的闭包就是用来解决作用域这问题的。
lambda与匿名内部类
刚刚在上面也有提到一嘴,java中也有方法实现kotlin中传递函数这种类似的效果,通过接口来实现。现在我们用Java代码来复现一下
public class Test {
public interface StudentAge{
int getStudentAge(int birYear,int year);
}
public static void showStudentAge(String name,int birYear,int year,StudentAge studentAge){
System.out.println(name + "同学" + birYear + "年出生,现在是" + year + "年");
System.out.println("所以" + name + "同学今年" + studentAge.getStudentAge(birYear,year) + "岁了");
}
public static void main(String[] args) {
showStudentAge("barry", 2001, 2021, new StudentAge() {
@Override
public int getStudentAge(int birYear, int year) {
return year - birYear;
}
});
}
}
上面在main函数中传递的最后一个参数就是一个匿名内部类,当然用java中的lambda表达式可以进一步的简化代码,如下
showStudentAge("barry", 2001, 2021, (birYear, year) -> year - birYear);
但java里面的lambda只是简化了书写,并没实质的改变,并不像kotlin中是可以实现一个函数类型的对象,在Java中必须借助接口才能实现kotlin中直接传递函数的效果。所以kotlin中的lambda还是要更加方便的。