java 8 list对象属性判空_Kotlin漫谈(2)- 从Java转到Kotlin,你需要知道的

内容提要

很多同学刚从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)
}

对比它们的源码可以看到,withapply都传入了一个「带接收者的lambda」。所谓“接收者”就是实际调用lambda的对象。它们之间的区别在于,with函数的「接收者」是作为参数传入的,apply的「接收者」是通过扩展函数指定的。「带接收者的lambda」和「普通lambda」的区别可以类比「扩展函数」和「普通函数」的区别。

applylet都被声明为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语言层面对内联函数的支持,我们可以配合使用inlinereified关键字来优雅地处理这个问题:

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

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关键字除了类型转换,还可以用于处理命名冲突。
  • reifiedinline搭配使用可以在运行时安全地使用泛型参数做类型转换。
  • object关键字表示「对象」的概念,可以用来实现单例、伴生对象、匿名对象。伴生对象是Java中static变量的替代写法。
  • const关键字表示"值不变“的概念,val表示“引用不变”的概念。
  • 在Kotlin中,return默认从最近的一个使用fun关键字声明的函数返回,可以使用标签指定从lambda返回。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值