变量、常量和类型

声明变量

在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……

java 读取变量私有属性值 java 只读变量_java 读取变量私有属性值

可以将Kotlin代码转换成Java的代码来查看

java 读取变量私有属性值 java 只读变量_匿名函数_02

可以发现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)}岁了")
}

java 读取变量私有属性值 java 只读变量_匿名函数_03

可以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)}岁了")
}

java 读取变量私有属性值 java 只读变量_赋值_04

使用了inline修饰后,也就是showStudentMsg成为了内联函数,那么此时我们再看main函数中,这时就不会再去调用showStudentMsg这个函数了,而是直接将showStudentMsg函数中的代码粘贴到了相应调用的位置。就等于是直接用函数的真实代码替换了函数调用,节省了额外的生成类和函数调用开销。

当然内联函数不是万能的,以下情况避免使用内联函数:

  1. 像上面使用内联后,会发现字节码数量相比没有内联是大大增多。所以尽量避免对具有大量函数体的函数进行内联,这样会导致过多的字节码数量。
  2. 由于JVM对普通函数已经能够根据实际情况智能地判断是否进行内联优化,所以并不需要对其使用Kotlin的inline语法,那只会让字节码变得更加复杂。
  3. 使用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,这是它就会报错

java 读取变量私有属性值 java 只读变量_java 读取变量私有属性值_05

java 读取变量私有属性值 java 只读变量_匿名函数_06

当然是用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还是要更加方便的。