内容提要
很多同学刚从Java切换到Kotlin时,写起来会有磕磕绊绊的感觉,最后写出来的代码很多都是Java风格的Kotlin。本文结合平时Java和Kotlin混合开发的一些实践,总结出笔者觉得比较重要的点供大家参考,也欢迎大家在评论区交流和补充。本文适合了解Kotlin基本语法的同学阅读。
指定Kotlin生成的类名
在Kotlin中定义顶层函数是非常方便的,例如:
//KotlinTest.kt
//...
fun doSomeThing() {
//do something
}
在Kotlin中调用是非常方便的,但在Java中默认需要添加Kt后缀:
//某Java业务类
//...
public class JavaTest {
public static void doSth() {
KotlinTestKt.doSomeThing();
}
}
看起来非常的别扭,说好的无缝互相调用呢?其实,Kotlin生成的Java类名是可以自定义的,方法就是在kt文件第一行加上@file:JvmName("KotlinTest")
,就可以在Java中直接调用KotlinTest.doSomeThing();
了
在Java代码中调用Kotlin高阶函数
Kotlin中lambda的用法是很方便的,我们在平时的开发中也经常会定义一些高阶函数,来抽象一些代码逻辑,例如:
val sList = arrayListOf("n", "e", "t", "e", "a", "s", "e")
fun doSthForEach(op: (String) -> Unit) {
sList.forEach {
op(it)
}
}
那么,如何在Java中调用这个方法呢?如果工程引入了Java8,可以直接传入一个lambda表达式;如果还在用Java6,也可以传一个匿名对象进去:
//Java8
KotlinTestKt.doSthForEach((s) -> {
//do something
return Unit.INSTANCE;
});
//Java6
KotlinTestKt.doSthForEach(new Function1<String, Unit>() {
@Override
public Unit invoke(String s) {
//do something
return Unit.INSTANCE;
}
});
需要注意的是,Kotlin中没有void
返回类型,如果不需要返回值,就需要返回一个Unit.INSTANCE
。
One more thing
上面例子中使用了Function1
,是因为这个lambda有1个参数,类似地,Kotlin还定义了其它形如FunctionN
的方法,可以根据需要选用。
注意Java方法参数的可空性
老工程里往往有大量的Java代码,引入Kotlin后不可避免地要在Kotlin中继承Java或实现Java接口。由于Java中很多老代码并没有指明可空性(显式添加@NonNull
或@Nullable
注解),Kotlin将判空的责任交给了开发者。例如:
//Java
public class AnimViewHolder {
//...
protected void render(Object meta, OnClickListener listener) {
//...
if (listener != null) {
listener.onClick();
}
//...
}
//...
}
假如我们需要使用Kotlin继承这个类,很容易写出下面的代码:
//Kotlin
class SubAnimViewHolder: AnimViewHolder() {
override fun render(meta: Any, listener: View.OnClickListener) {
super.render(meta, listener)
//以下代码没有用到listener
//...
}
}
乍一看似乎没什么问题,listener
只在基类调用,且调用时会做判空。然而运行时一旦listener
传了null进来,马上就会抛出NPE。如果测试时数据覆盖不全,很可能会把问题带到线上。
那么,问题的原因是什么呢?其实我们反编译一下这段Kotlin代码就很清楚了:
public final class SubAnimViewHolder extends AnimViewHolder {
protected void render(@NotNull Object meta, @NotNull OnClickListener listener) {
Intrinsics.checkParameterIsNotNull(meta, "meta");
Intrinsics.checkParameterIsNotNull(listener, "listener");
super.render(meta, listener);
}
}
//Intrinsics.java
public static void checkParameterIsNotNull(Object value, String paramName) {
if (value == null) {
throwParameterIsNullException(paramName);
}
}
注意,Kotlin中如果将参数声明为非空类型,在函数体开头就会做空安全检查,如果参数为空就会直接抛出异常了。所以,在Kotlin中覆盖Java方法,要尤其注意明确每个参数的可空性。
One more thing
类似的,还要谨慎使用!!
(强制非空声明)。无论后续使用过程中是否会产生NPE,一旦变量为null,会在使用!!的地方立即抛异常。总的来说,原则可以一句话概括:要确保Kotlin中非空变量在任何情况下都不会赋值为空。
一些常用的写法变更
很多同学在刚切到Java时会发现,不少在Java中很常用的写法发生了变化。例如:
- 获取类的Java Class属性
//Java
Intent intent = new Intent(this, MainActivity.class);
//Kotlin
val intent = Intent(this, MainActivity::class.java)
- 类型检查
//Java
apple instanceof Fruit
!(apple instanceof Fruit)
//Kotlin
apple is Fruit
apple !is Fruit
for
循环
//Java
List<String> list = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
//do something
}
//Kotlin 一般写法
for (element in sList) {
//do something
}
//Kotlin 不需要下标
list.forEach {
//do something
}
//Kotlin 需要下标
list.forEachIndexed { element, index ->
//do something
}
- 消失的
switch
语句
//Java
int timeout = 0;
switch (getABGroup()) {
case GROUP_T1:
timeout = 850;
break;
case GROUP_T2:
timeout = 1000;
break;
default:
timeout = 500;
break;
}
在Kotlin中,switch
语句正式退出了历史舞台,取而代之的是更为强大的when
表达式。注意语句(statement)和表达式(expression)的区别。通俗来讲,他们最大的区别是语句没有值,而表达式有值。因此在Kotlin中可以这样用:
fun getTimeout(): Int = when (getABGroup()) {
GROUP_T1 -> 850
GROUP_T2 -> 1000
else -> 500
}
One more thing
类似的,在Kotlin中,if
也变成了表达式,这也成为了Java中三目运算符的替代写法:
fun getTimeout(): Int = if (getABTestGroup() == GROUP_C) 1000 else 500
Kotlin标准库中几个方便的函数
apply let with
这三个函数的作用都是在某个对象上执行一系列操作,例如:
//apply返回this对象,可以直接链式调用其他方法
sList.apply {
add("hello")
add("world")
}.forEach {
println(it)
}
//with返回传入的lambda表达式的值
with(sList) {
add("hello")
add("world")
}
//let返回传入的lambda表达式的值
sList.let {
it.add("hello")
it.add("world")
}
one more thing
从它们的源码实现中可以很明显地看出来它们之间的区别:
public inline fun <T> T.apply(block: T.() -> Unit): T {
//...
block()
return this
}
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
//...
return receiver.block()
}
public inline fun <T, R> T.let(block: (T) -> R): R {
//...
return block(this)
}
对比它们的源码可以看到,with
和apply
都传入了一个「带接收者的lambda」。所谓“接收者”就是实际调用lambda的对象。它们之间的区别在于,with
函数的「接收者」是作为参数传入的,apply
的「接收者」是通过扩展函数指定的。「带接收者的lambda」和「普通lambda」的区别可以类比「扩展函数」和「普通函数」的区别。
apply
和let
都被声明为T类型的「扩展函数」,let
的实现方式是将调用let
的对象作为参数传入到lambda中。
isNullOrEmpty
很多同学刚切到Kotlin时还在这样写判空代码:
fun manipulateList(list: List<Int>?) {
if (list == null || list.isEmpty()) {
return
}
}
其实,得益于明确的空类型和扩展函数的特性,Kotlin标准库为我们提供了一系列方便的语法糖:
fun manipulateList(list: List<Int>?) {
//等价于(list == null || list.isEmpty())
if (list.isNullOrEmpty()) {
return
}
//安全地将可空类型的列表转为非空类型的列表,当list为null时返回empty列表
val notNullList: List<Int> = list.orEmpty()
//若列表为empty,返回一个默认列表
val listWithDefaultElements: List<Int> = notNullList.ifEmpty { arrayListOf(0, 3, 2, 1, 6, 5, 4) }
}
谈谈Kotlin语言的几个关键字
as
类型转换
as
最常用的就是类型转换了。在Java中,我们可能会这样写:
Apple apple = null;
Fruit fruit = getFruit();
if (fruit instanceof Apple) {
apple = (Apple) fruit;
}
而在Kotlin中,运用安全转换操作符as?
我们只需要一行代码就能达到上面的效果:
//apple是Apple?类型,当转换失败时为null
val apple = getFruit() as? Apple
处理顶层函数和顶层属性的冲突
随着项目规模的扩大,Kotlin中的顶层函数和顶层属性很容易出现重名的情况。如果我们需要在同一个Kotlin文件中使用两个重名的顶层函数或属性,就可以使用as为每个函数或属性指定别名:
import me.zq.another.MAX_TIMEOUT as MAX_OPERATION_TIMEOUT
import me.zq.MAX_TIMEOUT as MAX_NETWORK_TIMEOUT
reified和inline
这是两个Java中没有的关键字,首先说说reified
,它的用途与泛型相关。众所周知,Java的泛型是“伪泛型”,编译时会做类型擦除,因此在运行时是无法拿到泛型类型的。其实Kotlin也是一样的。例如,我们写出下面的代码:
fun <T> Bundle.getDataOrNullWithoutReified(): T? {
return getSerializable(KEY_DATA) as? T
}
IDE会在类型转换的地方给出warning:Unchecked cast,就是因为类型擦除的存在,无法在运行时保证类型安全。对此,Kotlin官方文档中给出了类似解释:
As said above, type erasure makes checking actual type arguments of a generic type instance impossible at runtime, and generic types in the code might be connected to each other not closely enough for the compiler to ensure type safety.
那么,Kotlin是如何解决这个问题的呢?得益于Kotlin语言层面对内联函数的支持,我们可以配合使用inline
和reified
关键字来优雅地处理这个问题:
inline fun <reified T> Bundle.getDataOrNull(): T? {
return getSerializable(KEY_DATA) as? T
}
fun reifiedTest(bundle: Bundle) {
val s: String? = bundle.getDataOrNull<String>()
val s2: String? = bundle.getDataOrNullWithoutReified<String>()
}
这样,就可以消除warning了。需要注意的是,reified只能用于内联函数。其实,reified
的作用并不是阻止类型擦除,而是告诉编译器“这个类型可以在运行时拿到”,泛型的实际类型在运行时仍然会被擦除,所以函数必须是内联的。上面的方法反编译后的结果如下:
@Nullable
public static final Object getDataOrNullWithoutReified(@NotNull Bundle $this$getDataOrNullWithoutReified) {
Intrinsics.checkParameterIsNotNull($this$getDataOrNullWithoutReified, "$this$getDataOrNullWithoutReified");
Serializable var10000 = $this$getDataOrNullWithoutReified.getSerializable("");
if (!(var10000 instanceof Object)) {
var10000 = null;
}
return (Object)var10000;
}
public static final void reifiedTest(@NotNull Bundle bundle) {
Intrinsics.checkParameterIsNotNull(bundle, "bundle");
int $i$f$getDataOrNull = false;
Serializable var10000 = bundle.getSerializable("");
if (!(var10000 instanceof String)) {
var10000 = null;
}
String s = (String)var10000;
String s2 = (String)getDataOrNullWithoutReified(bundle);
}
可以看到,非内联函数中的类型转换是Object
,而内联后类型正确转换为了String
。
object
熟悉Java的同学都知道,在Java中所有类都有一个公共基类:Object
。而在Kotlin中,公共基类变成了Any
,准确地说,是Any?
。而object
(注意o是小写的),在Kotlin中的含义就是「对象」。结合它的不同使用场景可以更好地理解这个概念:
单例对象
「单例」的含义其实就是“在当前进程中最多只存在一个对象实例”。在Kotlin中实现单例模式非常简单,只需要在声明类的时候使用object
代替class
:
object SingletonTest {
//...
}
通过这种方式实现的单例,是饿汉式、线程安全的。
伴生对象
Kotlin中去掉了Java中static
的概念,相对的,提供了「伴生对象」这一替代方案:
class CompanionTest {
companion object {
val TIMEOUT = 5000L
//伴生对象也可以声明为public
public var customTimeout = 0L
}
}
顾名思义,「伴生对象」就是“与类伴生的对象”,其实跟Java中static
的概念非常类似。
匿名对象
val listener = object : View.OnClickListener {
override fun onClick(v: View?) {
//do something
}
}
One more thing
Kotlin中可以声明「常量」(本节的「常量」特指“使用const
关键字修饰的字段”)的地方有三个位置:文件顶层、单例对象中、伴生对象中:
const val TIMEOUT = 5000L
object SingletonTest {
const val TIMEOUT = 5000L
}
class CompanionTest {
companion object {
const val TIMEOUT = 5000L
}
}
究其原因,Kotlin中的「常量」指的是“可以在编译时确定值的字段”,显然它必须是“静态”的,也就是说,不应该在不同的对象中存在不同的实例。符合这个要求的只有上述三个位置,因此「常量」只能在这三个位置定义。另外,Kotlin中只有基本类型和String可以被const
修饰,因为自定义类型可以通过改变其中的成员变量间接地改变它的“值”。
![0d12c1f3f9ca9f86c6aeba38c5608171.png](https://i-blog.csdnimg.cn/blog_migrate/a339e6b2a4cf2d3992cda1020b68f190.png)
return
return
关键字相信大家都不会陌生了。但是在Kotlin中,由于lambda的引入,return语句有一些需要注意的地方。例如下面的代码:
fun main() {
val sList = arrayListOf("n", "e", "t", "e", "a", "s", "e")
//在元素不是's'的时候做一些事情
sList.forEachIndexed { index, it ->
if (it == "s") {
println("find s, index: $index")
return
}
//do something
}
println("hello world")
}
/*===============
output:
find s, index: 5
================*/
但这样的写法是不符合预期的,最后会发现main
函数提前返回,forEachIndexed
之后的语句都不会执行了。这也是刚上手Kotlin的同学经常困惑的地方:如何从lambda中返回?
在Kotlin中,return
默认从最近的一个使用fun
关键字声明的函数返回,可以使用标签指定从lambda返回。
所以我们可以这样写:
//...
sList.forEachIndexed { index, it ->
if (it == "s") {
println("find s, index: $index")
return@forEachIndexed
}
//do something
}
println("hello world")
//...
//也可以指定标签名
//...
sList.forEachIndexed anotherLabel@{ index, it ->
if (it == "s") {
println("find s, index: $index")
return@anotherLabel
}
//do something
}
//...
/*===============
output:
find s, index: 5
hello world
================*/
One more thing
Kotlin中还有匿名函数的用法,例如:
//...
sList.forEachIndexed(fun (index, it) {
if (it == "s") {
println("find s, index: $index")
return
}
//do something
})
println("hello world")
//...
/*===============
output:
find s, index: 5
hello world
================*/
因为匿名函数使用fun
关键字声明的,所以return
也会从匿名函数中返回。
总结
- 在kt文件第一行加上
@file:JvmName("KotlinTest")
,可以指定生成的Java类名。 - 在Java中也可以调用Kotlin的高阶函数,根据Java版本的不同有两种方法。
- 使用Kotlin覆盖Java方法时,要注意Java方法参数的可空性。
- Kotlin中
for
循环等写法与Java中有些不同。 - 有些Java语句在Kotlin中变成了表达式,要善加利用。
as
关键字除了类型转换,还可以用于处理命名冲突。reified
和inline
搭配使用可以在运行时安全地使用泛型参数做类型转换。object
关键字表示「对象」的概念,可以用来实现单例、伴生对象、匿名对象。伴生对象是Java中static变量的替代写法。const
关键字表示"值不变“的概念,val
表示“引用不变”的概念。- 在Kotlin中,
return
默认从最近的一个使用fun
关键字声明的函数返回,可以使用标签指定从lambda返回。