kotlin常用语法点理解

语法点

空指针安全:可空类型修饰符

就是?,表明变量可能为null。

?及相关的elvis运算符(?:)的引入相比java是一大进步,可避免大量的NPE问题。

装箱类型?

kotlin无需装箱类型,因为它有?,比如:

kotlin 复制代码
val a: Int = 100; //自动用int
val b: Int? = 200;  //自动用Integer

字符串插值

不用String.format或StringBuilder,直接用${}将变量或表达式插入字符串里,很便捷:

kotlin 复制代码
fun getPrjWorkDir(project: Project): String {
        return "${project.basePath}/.backend_checker";
    }

当然,遇到循环里拼接字符串还是建议用StringBuilder

必须的override

java的@Override注解是可选的,而kotlin的override关键字是必须的。

对象表达式==匿名类实例

形如:

kotlin 复制代码
object: AbstractListener() {
	override fun func1() {
	...
	}
    
    override fun func2() {
	...
	}
}

等价于java里的创建匿名类实例:

java 复制代码
new AbstractListener() {
	@Override
	public void func1() {
	...
	}
    
    @Override
	public void func2() {
	...
	}
}

特别的,对于函数式接口(SAM,只有一个抽象方法),还可以使用"接口名+lambda"的简化写法:

kotlin 复制代码
ignoreAllItem = MenuItem("Ignore All")
// ActionListener为SAM, 只有一个actionPerformed接口,因此可以直接在lambda里写方法体
ignoreAllItem.addActionListener {
            val model = table.model
            ...
        }

双感叹号

强制保证引用不能为null,若为null,抛出NPE异常。个人不推荐使用,一般用于明确了绝不会为null的情况。

val+get()

等价于getter,但是作为属性而非成员函数存在,每次访问属性时都会重新计算,所以要注意使用中额外的性能开销。如果能确保一定的scope内值不变,使用时建议先用局部变量承载,避免每次都重新计算:

kotlin 复制代码
private fun buildNativeCmd(project: Project): String {
        // 这里用局部变量exePath承载,避免每次调用getter
        val exePath = exePath
        LOG.info("exe path:$exePath")

        if (!File(exePath).exists()) {
            LOG.error("exe:$exePath NOT found!")
            throw RuntimeException("No $EXE_NAME found!")
        }

        val fullCmd = exePath + " -d " + project.basePath
        LOG.warn("buildNativeCmd cmd:$fullCmd")
        return fullCmd
    }

    private val exePath: String
        get() = pluginRoot + File.separator + "bin" + File.separator + EXE_NAME

个人并不推荐这种写法,这会让使用者误认为属性调用是没有代价的。

static变量

kotlin没有static,等价的写法是定义在类的伴生对象里:

kotlin 复制代码
companion object {
    	// 等价于 static
        private val LOG = Logger.getInstance(AnalyzeTask::class.java)
		
    	// 伴生对象里的const val等价于static final
        const val EXE_NAME: String = "XXX"
    }

for循环

kotlin样例:

kotlin 复制代码
// 循环变量i无需提前声明,直接使用
for(i in 1..10)

全局函数与全局变量

kotlin并非纯OO,类似于scala,属于"FP+OO"的多范式语言,函数像类一样,在kotlin里是第一类对象。所以,支持全局函数或全局变量并不奇怪。

另外,kotlin里也不像java那样强制"单类单文件"。

属性赋值==setter

kotlin里的属性赋值就等价于调用setter函数(如果有的话)。

类似于val+get,我们也可以对类的可变属性用var+set来实现。

List和MutableList

kotlin在类型层面严格区分"只读"和"可变"集合,List是只读,不支持add、remove,MutableList是可变。但这只是类型层面的约束,并非运行层面的保证,因MutableList也是List,所以List底层依然可能可变,不能以此作为线程安全的考量。

与java互通

kotlin能100%与java互通,所以可以采用java/kotlin并存的方式逐步改造已有代码,改造完成后,去掉一些原本用于语言互通的注解(比如@JvmStatic、@JvmRecord、 @JvmField)即可。

继承

kotlin的继承都使用冒号,语法上不区分extends和implements,但语义上"只能extends一个类,可以implements多个接口"的约束仍在。

构造函数

kotlin里,主构造函数=类声明+init块,例如:

kotlin 复制代码
class LintResultPanel(private val project: Project, private val errors: MutableList<LintError>) :
    SimpleToolWindowPanel(false, true), DataProvider {
    private val table = JBTable()

    init {
        setContent(ScrollPaneFactory.createScrollPane(table))
        table.model = ErrorsTableModel()
        table.setShowGrid(false)

        ...
    }

上例中,第一行就是类声明,有2个属性:project和errors,在init块里做复杂的初始化工作。

次构造函数则用constructor指定,但次构造必须调用到主构造(通过this调用)。

takeIf/taskUnless/let

非集合类对象的takeIf和taskUnless 等价于 集合filter

非集合类对象的let 等价于 集合map。

见"对象链式写法"的例子。

apply和let的区别

两者都是对对象做映射处理,区别在于apply最终返回的仍是对象自身(即this),而let返回的是其函数体最后一行的值(一般肯定不是this)。因此,apply常用于链式配置,而let用于空安全或变换结果。

apply的例子:

kotlin 复制代码
private val table = JBTable().apply {
        // 这里相当于this.setRowSelectionAllowed(true)
        setRowSelectionAllowed(true)
        setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
    }

这里在创建Swing Table后,用apply设置其支持多选。用let肯定就不对了。

apply和also的区别

两者的功能很像,最终返回的都是对象自身。我们看声明:

kotlin 复制代码
public inline fun <T> T.also(block: (T) -> kotlin.Unit): T

public inline fun <T> T.apply(block: T.() -> kotlin.Unit): T

最终返回的都是T,但also是有参的(不写默认为it),而apply是无参的,且由于是T.(),说明apply紧跟的是T的无参成员函数,因此其内部用的是this(当然this一般会省略)。所以上一节的例子,如果用also改写,应为:

kotlin 复制代码
private val table = JBTable().also {
        it.setRowSelectionAllowed(true)
        it.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
    }

相比apply不那么直观。

因此,apply常用于对象创建后的链式配置,而also则用于其它有副作用的场景,比如日志或校验。当然,这只是习惯用法,并非绝对要求。

对象链式写法

替换if-else冗长的写法,比如:

kotlin 复制代码
var lineNo = err.lineNo?.let { err.lineNo - 1 } ?: 0

上述代码用到了kotlin的safe_navi+对象let映射+elvis三种能力,等价于:

kotlin 复制代码
        var lineNo = 0
        if (err.lineNo != null) {
            lineNo = err.lineNo - 1
        }

java里可以用optional的流式写法代替。

return居然是表达式

这点没想到啊,return不是语句,而是表达式,返回类型是Nothing,Nothing是任何类型的子类型,所以可以放在任何需要值的地方。比如:

kotlin 复制代码
override fun actionPerformed(e: AnActionEvent) {
        val project = e.project ?: return

        AnalyzeTask(project).queue()
    }

return要能用在elvis表达式里,就必须是一个表达式,而非语句。所以kotlin为了语法的需要,赋予了return表达式的特质。表达式的返回值可以是任意的类型,因此return的值也必须是任意类型的子类,所以只能是Nothing。

return:非局部返回

与java不同,kotlin里的return默认是非局部返回(Non-local Return, NLR),即return会退出最近的外层函数,而非包裹它的lambda。如只想退出lambda,而非整个外层函数,得用"return+@标签"的写法,比如下面的例子:

kotlin 复制代码
init {
        ...

        // 右键菜单
        val popup = JPopupMenu()
        val ignoreItem = JMenuItem("Ignore")
        popup.add(ignoreItem)

        ...

        ignoreItem.addActionListener {
            val selected = table.selectedRows
            if (selected.isEmpty()) {
                Messages.showInfoMessage(
                    project,
                    "No rows selected",
                    "Hint"
                )
                // 【注意!!】这里如只用return,会跳出整个init块,而非addActionListener的lambda块。所以必须加标签。
                // 且return和标签之间不能有空格!
                return@addActionListener
            }
            
            val model = table.model
            ...
        }
        ...
}

init块本质上是类的主构造函数(参见"构造函数"一节),如将上述代码里的return@addActionListener改为return,会跳出主构造函数。但我们这里addActionListener后的lambda其实是一个针对函数式接口(SAM)简化的对象表达式,是一个菜单响应处理事件,所以return应该只是从lambda里退出即可。如果仍用对象表达式,因为fun的存在,不会有问题,但语法简化之后就会触发return的NLR问题。好在,编译器层面能给出告警:

复制代码
return is not allowed here

Unit和Nothing

kotlin没有void,用Unit表示,Unit是一个单例(而非类型!),定义如下:

kotlin 复制代码
public object Unit {
    public open fun toString(): kotlin.String { /* compiled code */ }
}

Nothing则是一个类型(而非实例!),定义如下:

kotlin 复制代码
public final class Nothing private constructor() {
}

看到了吗,Nothing的构造器是私有的,意味着无法创建Nothing的实例。

Nothing主要用于表示return、throw的值。

with块

类似pascal的with,后跟receiver及指定函数块,函数块里可用省略属主的方式调用receiver的方法或属性。常用来简化代码,例如:

kotlin 复制代码
private fun hideColumn(table: JTable, colIndex: Int) {
    // 用with避免每次都要写table.columnModel.getColumn(colIndex)
        with(table.columnModel.getColumn(colIndex)) {
            minWidth = 0
            maxWidth = 0
            width = 0
            preferredWidth = 0
        }
    }

when

代替switch-case或多个if-else,例如:

kotlin 复制代码
return when (column) {
                0 -> e.filePath
                1 -> e.lineNo
                else -> e.message
            }

注意,when也是有值的。

异常声明?

想抛就抛,kotlin里无需异常声明。

之前写java的时候就觉得所谓的受检异常用起来很费劲,有时候干脆用RuntimeException来规避异常声明。kotlin完全废弃了这一套繁琐的东西。

资源自动释放

kotlin里对应try-with-resource的写法是use:

kotlin 复制代码
StringWriter().use { sw ->
        PrintWriter(sw).use {
            e.printStackTrace(it)
            log.info(sw.toString())
        }
    }

扩展函数

kotlin的扩展函数能为系统类添加成员函数,跟ruby的open class类似,是很强的能力了。比如为Int增加一个取模方法:

kotlin 复制代码
fun Int.mod(that: Int) = this % that

扩展函数的底层实现并非去修改系统类的字节码定义,而是直接将扩展函数变成静态方法,上述函数的字节码为:

复制代码
public static final int mod(int, int);
    descriptor: (II)I
    flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=2, locals=2, args_size=2
         0: iload_0
         1: iload_1
         2: irem
         3: ireturn

kotlin通过插入Metadata来帮助编译器识别扩展函数,字节码里同样含Metadata,以注解形式存在:

复制代码
RuntimeVisibleAnnotations:
  0: #12(#13=[I#14,I#15,I#15],#16=I#14,#17=I#18,#19=[s#20],#21=[s#5,s#22,s#9,s#22,s#23])
    kotlin.Metadata(
      mv=[2,0,0]
      k=2
      xi=48
      d1=["\u0000\u000e\n\u0000\n\u0002\u0010\b\n\u0000\n\u0002\u0010\t\n\u0000\u001a\u0015\u0010\u0000\u001a\u00020\u0001*\u00020\u00012\u0006\u0010\u0002\u001a\u00020\u0001H\u0086\u0004\u001a\u0015\u0010\u0000\u001a\u00020\u0003*\u00020\u00032\u0006\u0010\u0002\u001a\u00020\u0003H\u0086\u0004¨\u0006\u0004"]
      d2=["mod","","that","","el4k"]
    )

mv是kotlin编译器版本,我的是2.0.0

k=kind,k=2表示仅含顶层函数的 File 类,因我的mod方法是直接定义在顶层kt文件里。

d1是用protocol buffer压缩的信息,根据k的不同,取用不同的解压方法解出完整信息。

d2是方法签名。

Number类型

Number是一切数值类型的基类,它一般不直接使用。

协程

kotlin实现协程的机制是continuation(所谓剩余程序),当你写一个suspend函数时:

kotlin 复制代码
suspend fun co1() {
    println("A: ${Thread.currentThread().name}") 
    delay(100)                  // suspend点:非阻塞挂起100ms
    println("A done")
}

kotlin编译器会做一次cps变换,为挂起函数自动补一个continuation参数,比如co1变成:

复制代码
public static final java.lang.Object co1(kotlin.coroutines.Continuation<? super kotlin.Unit>);
    descriptor: (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;

kotlin编译器会在每个挂起点(比如上述代码的delay处)拆出标签L,并把局部变量塞进continuation对象,等到continuation对象resume时,跳转到标签L,用塞进continuation对象的局部变量继续运行,这样,整个执行流在挂起后就又接上了。

具体到delay这个suspend函数,其实现为:

kotlin 复制代码
public suspend fun delay(timeMillis: Long) {
    if (timeMillis <= 0) return // don't delay
    return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
        // if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
        if (timeMillis < Long.MAX_VALUE) {
            cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
        }
    }
}

suspendCancellableCoroutine所带的continuation参数,在上例中,可以粗略的认为是println("A done")这行代码。

DSL特性

infix函数

中缀调用函数可省略点号,更像自然语言。比如我们要做一个取模函数mod,希望能这样调用:

kotlin 复制代码
7 mod 3

可这样定义一个扩展函数:

kotlin 复制代码
infix fun Int.mod(that: Int) = this % that

kotlin的中缀函数要求加infix关键字,必须是类的成员函数或扩展函数,且只能有一个参数。

apply方法+lambda尾调用

apply方法可以省略this,达到"代码块就像写在类内部"的效果,有点ruby的instance_eval的味道。

lambda尾调用可以省略括号,只写大括号,更像DSL。

一般情况下,我们会做一个顶层的builder函数,传入一个lambda,像这样:

kotlin 复制代码
fun tasks(block: TaskMgr.() -> Unit) {
    TaskMgr().apply(block)
}

则脚本里可以这样写(例子是一个简单的任务依赖系统):

kotlin 复制代码
tasks {
    task { "go_to_work" to listOf("drink_coffee", "dress") }
    task { "drink_coffee" to listOf("make_coffee", "wash") }
    task { "dress" to listOf("wash") }
    call("go_to_work")
}

这样, tasks块里就可以省略this的方式调用TaskMgr类的成员方法了。上例中task是TaskMgr的成员方法,为了让它更像DSL,我们将其参数也设置为产生Pair对的lambda以便达到只使用大括号的效果:

kotlin 复制代码
class TaskMgr {
    ...

    fun task(block: () -> Pair<String, List<String>>) {
        val taskParam = block.invoke()
        val tsk = addTask(taskParam.first)
        tsk.addPreTasks(taskParam.second.map { addTask(it) })
    }

在kts脚本中支持自定义接口

使用ScriptEngine,要支持自定义接口有两种手段:

  • 动态eval
  • 插入对象

下面是一个例子:

kotlin 复制代码
object ScriptRunner {
    private fun dynInject(engine: ScriptEngine) {
        engine.eval(
            """
            import kotlin.math.pow
            infix fun Int.mod(that: Int) = this % that
            infix fun Long.mod(that: Long) = this % that
            infix fun Number.power(n: Int) = this.toDouble().pow(n)
        """.trimIndent()
        )
    }

    fun runScript(scriptFile: File) {
        check(scriptFile.exists()) { "script NOT exists:$scriptFile" }

        // 1. 获取 JSR-223 引擎
        val engine = ScriptEngineManager()
            .getEngineByExtension("kts")!!

        // 通过eval动态插入函数
        dynInject(engine)

        // 通过put插入对象
        val errCollector = ErrCollector()
        engine.put("errCollector", errCollector)

        // 2. 执行脚本
        println("start to run kts:$scriptFile")
        scriptFile.reader().use {
            val res = engine.eval(it)
            println("kts run result:${res}")
        }
    }

}
相关推荐
xixixi7777725 分钟前
解析常见的通信流量和流量分析
运维·开发语言·网络·安全·php·通信·流量
csdn_aspnet27 分钟前
用Python抓取ZLibrary元数据
开发语言·python·zlibrary
hazhanglvfang36 分钟前
使用curl测试java后端post接口
java·开发语言
杀死那个蝈坦36 分钟前
Lua核心认知
开发语言·lua
烂不烂问厨房41 分钟前
支付宝小程序camera录制视频超过30秒无法触发cameraContext.stopRecord回调,也没报错
android·小程序
杀死那个蝈坦41 分钟前
Redis 缓存预热
java·开发语言·青少年编程·kotlin·lua
秦jh_41 分钟前
【Qt】Qt 概述
开发语言·qt
稚辉君.MCA_P8_Java42 分钟前
在Java中,将`Short`(包装类)或`short`(基本类型)转换为`int`
java·开发语言