最近在学习kotlin,记录下自己学习过程中的笔记。因为内容比较多,所以分为几篇文章。那么第一篇主要是以下的内容:
转载地址:https://zhuanlan.zhihu.com/p/417635072
原作者:啥都不懂的小白
在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"
可以将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 = 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
val money = 10
when (money) {
in 0..5 -> {
println("cheap")
}
in 5..10 -> {
println("mid")
}
else -> {
println("expensive")
}
}
$变量名
或 ${表达式}
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
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())
}
这里抛出一个问题,正常来讲函数是不能赋值给变量的,如下面这个例子
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关键字就不能用了
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
如果你学过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中的闭包就是用来解决作用域这问题的。
刚刚在上面也有提到一嘴,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还是要更加方便的。
本文转自 https://zhuanlan.zhihu.com/p/417635072,如有侵权,请联系删除。
在这里为了方便大家系统的学习Kotlin,这里特意联合了阿里P7架构师和谷歌技术团队共同整理了一份Kotlin全家桶学习资料,完整资料扫描下方二维码。
Kotlin 是一种新型的静态类型编程语言,有超过 60% 的专业 Android 开发者在使用,它有助于提高工作效率、开发者满意度和代码安全性。不仅可以减少常见代码错误,还可以轻松集成到现有应用中。
内容概要:Kotlin 入门教程指南、高级Kotlin强化实战和史上最详Android版kotlin协程入门进阶实战 。
内容特点:条理清晰,含图像化表示更加易懂。
由于文章内容比较多,篇幅有限,资料已经被整理成了PDF文档,有需要 Android中高级面试必知必会 完整文档的可扫描下方免费获取!
● 前言
● 使用 Kotlin 进行服务器端开发
● 使用 Kotlin 进行 Android 开发
● Kotlin JavaScript 概述
● Kotlin/Native 用于原生开发
● 用于异步编程等场景的协程
● Kotlin 1.1 的新特性
● Kotlin 1.2 的新特性
● Kotlin 1.3 的新特性
● 基本语法
● 习惯用法
● 编码规范
● 基本类型
● 包
● 控制流:if、when、for、while
● 返回和跳转
● 类与继承
● 属性与字段
● 接口
● 可见性修饰符
● 扩展
● 数据类
● 密封类
● 泛型
● 嵌套类与内部类
● 枚举类
● 对象表达式与对象声明
● Inline classes
● 委托
委托属性
● 函数
● 高阶函数与 lambda 表达式
● 内联函数
● 解构声明
● 集合:List、Set、Map
● 区间
● 类型的检查与转换“is”与“as”
● This 表达式
● 相等性
● 操作符重载
● 空安全
● 异常
● 注解
● 反射
● 类型安全的构建器
● 类型别名
● 多平台程序设计
● 关键字与操作符
● 在 Kotlin 中调用 Java 代码
● Java 中调用 Kotlin
● JavaScript 动态类型
● Kotlin 中调用 JavaScript
● JavaScript 中调用 Kotlin
● JavaScript 模块
● JavaScript 反射
● JavaScript DCE
● 协程基础
● 取消与超时
● 通道 (实验性的)
● 组合挂起函数
● 协程上下文与调度器
● 异常处理
● select 表达式(实验性的)
● 共享的可变状态与并发
● 编写 Kotlin 代码文档
● Kotlin 注解处理
● 使用 Gradle
● 使用 Maven
● 使用 Ant
● Kotlin 与 OSGi
● 编译器插件
● 不同组件的稳定性
● FAQ
● 与 Java 语言比较
● 与 Scala 比较【官方已删除】
● Kotlin 概述
● Kotlin 与 Java 比较
● 巧用 Android Studio
● 认识 Kotlin 基本类型
● 走进 Kotlin 的数组
● 走进 Kotlin 的集合
● 完整代码
● 基础语法
● 方法入参是常量,不可修改
● 不要 Companion、INSTANCE?
● Java 重载,在 Kotlin 中怎么巧妙过渡一下?
● Kotlin 中的判空姿势
● Kotlin 复写 Java 父类中的方法
● Kotlin “狠”起来,连TODO都不放过!
● is、as` 中的坑
● Kotlin 中的 Property 的理解
● also 关键字
● takeIf 关键字
● 单例模式的写法
● 从一个膜拜大神的 Demo 开始
● Kotlin 写 Gradle 脚本是一种什么体验?
● Kotlin 编程的三重境界
● Kotlin 高阶函数
● Kotlin 泛型
● Kotlin 扩展
● Kotlin 委托
● 协程“不为人知”的调试技巧
● 图解协程:suspend
● 协程是什么
● 什么是Job 、Deferred 、协程作用域
● Kotlin协程的基础用法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g14uGeVh-1678177930493)(https://docimg1.docs.qq.com/image/ME6Ygm_vFgAkpKvYo-TGIg.png?w=567&h=303)]
● 协程调度器
● 协程上下文
● 协程启动模式
● 协程作用域
● 挂起函数
● 协程异常的产生流程
● 协程的异常处理
● Android使用kotlin协程
● 在Activity与Framgent中使用协程
● ViewModel中使用协程
● 其他环境下使用协程
● 协程的常用环境
● 协程在网络请求下的封装及使用
● 高阶函数方式
● 多状态函数返回值方式
● suspend的花花肠子
● 藏在身后的-Continuation
● 村里的希望-SuspendLambda
● 协程的那些小秘密
● 协程的创建过程
● 协程的挂起与恢复
● 协程的执行与状态机
● 从一个膜拜大神的 Demo 开始
● Kotlin 写 Gradle 脚本是一种什么体验?
● Kotlin 编程的三重境界
● Kotlin 高阶函数
● Kotlin 泛型
● Kotlin 扩展
● Kotlin 委托
● 协程“不为人知”的调试技巧
● 图解协程原理
● 项目配置
● 实现思路
● 协程实现
● 协程 + ViewModel + LiveData实现
● 后续优化
● 异常处理
● 更新Retrofit 2.6.0
由于文章内容比较多,篇幅有限,资料已经被整理成了PDF文档,有需要 Android中高级面试必知必会 完整文档的可以扫描二维码获取)
更有更多资料,加微信免费领取