Java切换到Kotlin,Crash率上升了?

news2024/11/28 8:41:13

前言

最近对一个Java写的老项目进行了部分重构,测试过程中波澜不惊,顺利上线后几天通过APM平台查看发现Crash率上升了,查看堆栈定位到NPE类型的Crash,大部分发生在Java调用Kotlin的函数里,本篇将会分析具体的场景以及规避方式。
通过本篇文章,你将了解到:

  1. NPE(空指针 NullPointerException)的本质
  2. Java 如何预防NPE?
  3. Kotlin NPE检测
  4. Java/Kotlin 混合调用
  5. 常见的Java/Kotlin互调场景

1. NPE(空指针 NullPointerException)的本质

变量的本质

    val name: String = "fish"

name是什么?
对此问题你可能嗤之以鼻:

不就是变量吗?更进一步说如果是在对象里声明,那就是成员变量(属性),如果在方法(函数)里声明,那就是局部变量,如果是静态声明的,那就是全局变量。

回答没问题很稳当。
那再问为什么通过变量就能找到对应的值呢?

答案:变量就是地址,通过该地址即可寻址到内存里真正的值

无法访问的地址

在这里插入图片描述

如上图,若是name=“fish”,表示的是name所指向的内存地址里存放着"fish"的字符串。
若是name=null,则说明name没有指向任何地址,当然无法通过它访问任何有用的信息了。

无论C/C++亦或是Java/Kotlin,如果一个引用=null,那么这个引用将毫无意义,无法通过它访问任何内存信息,因此这些语言在设计的过程中会将通过null访问变量/方法的行为都会显式(抛出异常)提醒开发者。

2. Java 如何预防NPE?

运行时规避

先看Demo:

public class TestJava {
   public static void main(String args[]) {
      (new TestJava()).test();
   }

   void test() {
      String str = getString();
      System.out.println(str.length());
   }

   String getString() {
      return null;
   }
}

执行上述代码将会抛出异常,导致程序Crash:
在这里插入图片描述
我们有两种解决方式:

  1. try…catch
  2. 对象判空

try…catch 方式

public class TestJava {
    public static void main(String args[]) {
        (new TestJava()).testTryCatch();
    }

    void testTryCatch() {
        try {
            String str = getString();
            System.out.println(str.length());
        } catch (Exception e) {
        }
    }

    String getString() {
        return null;
    }
}

NPE被捕获,程序没有Crash。

对象判空

public class TestJava {
    public static void main(String args[]) {
        (new TestJava()).testJudgeNull();
    }

    void testJudgeNull() {
        String str = getString();
        if (str != null) {
            System.out.println(str.length());
        }
    }

    String getString() {
        return null;
    }
}

因为提前判空,所以程序没有Crash。

编译时检测

在运行时再去做判断的缺点:

无法提前发现NPE问题,想要覆盖大部分的场景需要随时try…catch或是判空
总有忘记遗漏的时候,发布到线上就是个生产事故

那能否在编译时进行检测呢?
答案是使用注解。

public class TestJava {
    public static void main(String args[]) {
        (new TestJava()).test();
    }

    void test() {
        String str = getString();
        System.out.println(str.length());
    }

    @Nullable String getString() {
        return null;
    }
}

在编写getString()方法时发现其可能为空,于是给方法加上一个"可能为空"的注解:@Nullable

当调用getString()方法时,编译器给出如下提示:
在这里插入图片描述
意思是访问的getString()可能为空,最后访问String.length()时可能会抛出NPE。
看到编译器的提示我们就知道此处有NPE的隐患,因此可以针对性的进行处理(try…catch或是判空)。

当然此处的注解仅仅只是个"弱提示",你即使没有进行针对性的处理也能编译通过,只是问题最后都流转到运行时更难挽回了。

有"可空"的注解,当然也有"非空"的注解:
在这里插入图片描述
@Nonnull 注解修饰了方法后,若是检测到方法返回null,则会提示修改,当然也是"弱提示"。

3. Kotlin NPE检测

编译时检测

Kotlin 核心优势之一:

空安全检测,变量分为可空型/非空型,能够在编译期检测潜在的NPE,并强制开发者确保类型一致,将问题在编译期暴露并解决

先看非空类型的变量声明:

class TestKotlin {

    fun test() {
        val str = getString()
        println("${str.length}")
    }

    private fun getString():String {
        return "fish"
    }
}

fun main() {
    TestKotlin().test()
}

此种场景下,我们能确保getString()函数的返回一定非空,因此在调用该函数时无需进行判空也无需try…catch。

你可能会说,你这里写死了"fish",那我写成null如何?
在这里插入图片描述
编译期直接提示不能这么写,因为我们声明getString()的返回值为String,是非空的String类型,既然声明了非空,那么就需要言行一致,返回的也是非空的。

有非空场景,那也得有空的场景啊:

class TestKotlin {

    fun test() {
        val str = getString()
        println("${str.length}")
    }

    private fun getString():String? {
        return null
    }
}

fun main() {
    TestKotlin().test()
}

此时将getString()声明为非空,因此可以在函数里返回null。
然而调用之处就无法编译通过了:
在这里插入图片描述
意思是既然getString()可能返回null,那么就不能直接通过String.length访问,需要改为可空方式的访问:

class TestKotlin {

    fun test() {
        val str = getString()
        println("${str?.length}")
    }

    private fun getString():String? {
        return null
    }
}

str?.length 意思是:如果str==null,就不去访问其成员变量/函数,若不为空则可以访问,于是就避免了NPE问题。

由此可以看出:

Kotlin 通过检测声明与实现,确保了函数一定要言行一致(声明与实现),也确保了调用者与被调用者的言行一致

因此,若是用Kotlin编写代码,我们无需花太多时间去预防和排查NPE问题,在编译期都会有强提示。

4. Java/Kotlin 混合调用

回到最初的问题:既然Kotlin都能在编译期避免了NPE,那为啥使用Kotlin重构后的代码反而导致Crash率上升呢?

原因是:项目里同时存在了Java和Kotlin代码,由上可知两者在NPE的检测上有所差异导致了一些兼容问题。

Kotlin 调用 Java

调用无返回值的函数

Kotlin虽然有空安全检测,但是Java并没有,因此对于Java方法来说,不论你传入空还是非空,在编译期我都没法检测出来。

public class TestJava {
    void invokeFromKotlin(String str) {
        System.out.println(str.length());
    }
}
class TestKotlin {

    fun test() {
        TestJava().invokeFromKotlin(null)
    }
}

fun main() {
    TestKotlin().test()
}

如上无论是Kotlin调用Java还是Java之间互调,都没法确保空安全,只能由被调用者(Java)自己处理可能的异常情况。

调用有返回值的函数

public class TestJava {
    public String getStr() {
        return null;
    }
}
class TestKotlin {
    fun testReturn() {
        println(TestJava().str.length)
    }
}

fun main() {
    TestKotlin().testReturn()
}

如上,Kotlin调用Java的方法获取返回值,由于在编译期Kotlin无法确定返回值,因此默认把它当做非空处理,若是Java返回了null,那么将会Crash。

Java 调用 Kotlin

调用无返回值的函数

先定义Kotlin类:

class TestKotlin {

    fun testWithoutNull(str: String) {
        println("len:${str.length}")
    }

    fun testWithNull(str: String?) {
        println("len:${str?.length}")
    }
}

有两个函数,分别接收可空/非空参数。

在Java里调用,先调用可空函数:

public class TestJava {
    public static void main(String args[]) {
        (new TestKotlin()).testWithNull(null);
    }
}

因为被调用方是Kotlin的可空函数,因此即使Java传入了null,也不会有Crash。

再换个方式,在Java里调用非空函数:

public class TestJava {
    public static void main(String args[]) {
        (new TestKotlin()).testWithoutNull(null);
    }
}

却发现Crash了!
在这里插入图片描述
为什么会Crash呢?反编译查看Kotlin代码:

public final class TestKotlin {
   public final void testWithoutNull(@NotNull String str) {
      Intrinsics.checkNotNullParameter(str, "str");
      String var2 = "len:" + str.length();
      System.out.println(var2);
   }

   public final void testWithNull(@Nullable String str) {
      String var2 = "len:" + (str != null ? str.length() : null);
      System.out.println(var2);
   }
}

对于非空的函数来说,会有检测代码:
Intrinsics.checkNotNullParameter(str, “str”):

    public static void checkNotNullParameter(Object value, String paramName) {
        if (value == null) {
            throwParameterIsNullNPE(paramName);
        }
    }
    private static void throwParameterIsNullNPE(String paramName) {
        throw sanitizeStackTrace(new NullPointerException(createParameterIsNullExceptionMessage(paramName)));
    }

可以看出:

  1. Kotlin对于非空的函数参数,先判断其是否为空,若是为空则直接抛出NPE
  2. Kotlin对于可空的函数参数,没有强制检测是否为空

调用有返回值的函数

Java 本身就没有空安全,只能在运行时进行处理。

小结

很容看出来:

  1. Java 调用Kotlin的非空函数有Crash的风险,编译器无法检查到传入的参数是否为空
  2. Java 调用Kotlin的可空函数没有Crash风险,Kotlin编译期检查空安全
  3. Kotlin 调用Java的函数有Crash风险,由Java代码规避风险
  4. Kotlin 调用Java有返回值的函数有Crash风险,编译器无法检查到返回值是否为空

回到文章的标题,我们已经大致知道了Java切换到Kotlin,为啥Crash就升上了的原因了,接下来再详细分析。

5. 常见的Java/Kotlin互调场景

Android里的Java代码分布

在这里插入图片描述
在Kotlin出现之前,Java就是Android开发的唯一语言,Android Framework、Androidx很多是Java代码编写的,因此现在依然有很多API是Java编写的。

而不少的第三方SDK因为稳定性、迁移代价的考虑依然使用的是Java代码。

我们自身项目里也因为一些历史原因存在Java代码。

以下讨论的前提是假设现有Java代码我们都无法更改。

Kotlin 调用Java获取返回值

由于编译期无法判定Java返回的值是空还是非空,因此若是确认Java函数可能返回空,则可以通过在Kotlin里使用可空的变量接收Java的返回值。

class TestKotlin {
    fun testReturn() {
        val str: String? = TestJava().str
        println(str?.length)
    }
}

fun main() {
    TestKotlin().testReturn()
}

Java 调用Kotlin函数

LiveData Crash的原因与预防

之前已经假设过我们无法改动Java代码,那么Java调用Kotlin函数的场景只有一个了:回调。
上面的有返回值场景还是比较容易防备,回调的场景就比较难发现,尤其是层层封装之后的代码。
这也是特别常见的场景,典型的例子如LiveData。

Crash原因

class TestKotlin(val lifecycleOwner: LifecycleOwner) {
    val liveData: MutableLiveData<String> = MutableLiveData<String>()
    fun testLiveData() {
        liveData.observe(lifecycleOwner) {
            println(it.length)
        }
    }

    init {
        testLiveData()
    }
}

如上,使用Kotlin声明LiveData,其类型是非空的,并监听LiveData的变化。

在另一个地方给LiveData赋值:

TestKotlin(this@MainActivity).liveData.value = null

虽然LiveData的监听和赋值的都是使用Kotlin编写的,但不幸的是还是Crash了。
发送和接收都是用Kotlin编写的,为啥还会Crash呢?
看看打印:
在这里插入图片描述
意思是接收到的字符串是空值(null),看看编译器提示:
在这里插入图片描述
原来此处的回调传入的值被认为是非空的,因此当使用it.length访问的时候编译器不会有空安全提示。

再看看调用的地方:
在这里插入图片描述
可以看出,这回调是Java触发的。

Crash 预防

第一种方式:
我们看到LiveData的数据类型是泛型,因此可以考虑在声明数据的时候定为非空:

class TestKotlin(val lifecycleOwner: LifecycleOwner) {
    val liveData = MutableLiveData<String?>()
    fun testLiveData() {
        liveData.observe(lifecycleOwner) {
            println(it?.length)
        }
    }

    init {
        testLiveData()
    }
}

如此一来,当访问it.length时编译器就会提示可空调用。

第二种方式:
不修改数据类型,但在接收的地方使用可空类型接收:

class TestKotlin(val lifecycleOwner: LifecycleOwner) {
    val liveData = MutableLiveData<String>()
    fun testLiveData() {
        liveData.observe(lifecycleOwner) {
            val dataStr:String? = it
            println(dataStr?.length)
        }
    }

    init {
        testLiveData()
    }
}

第三种方式:
使用Flow替换LiveData。

LiveData 修改建议:

  1. 若是新写的API,建议使用第三种方式
  2. 若是修改老的代码,建议使用第一种方式,因为可能有多个地方监听LiveData值的变化,如果第一种方式的话需要写好几个地方。

其它场景的Crash预防:

与后端交互的数据结构
比如与后端交互声明的类,后端有可能返回null,此时在客户端接收时若是使用了非空类型的字段去接收,那么会发生Crash。
通常来说,我们会使用网络框架(如retrofit)接收数据,数据的转换并不是由我们控制,因此无法使用针对LivedData的第二种方式。
有两种方式解决:

  1. 与后端约定,不能返回null(等于白说)
  2. 客户端声明的类的字段声明为可空(类似针对LivedData的第一种方式)

Json序列化/反序列化
Json字符串转换为对象时,有些字段可能为空,也需要声明为可空字段。

小结

在这里插入图片描述

您若喜欢,请点赞、关注、收藏,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android/Kotlin

1、Android各种Context的前世今生
2、Android DecorView 必知必会
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分发全套服务
6、Android invalidate/postInvalidate/requestLayout 彻底厘清
7、Android Window 如何确定大小/onMeasure()多次执行原因
8、Android事件驱动Handler-Message-Looper解析
9、Android 键盘一招搞定
10、Android 各种坐标彻底明了
11、Android Activity/Window/View 的background
12、Android Activity创建到View的显示过
13、Android IPC 系列
14、Android 存储系列
15、Java 并发系列不再疑惑
16、Java 线程池系列
17、Android Jetpack 前置基础系列
18、Android Jetpack 易学易懂系列
19、Kotlin 轻松入门系列
20、Kotlin 协程系列全面解读

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/968277.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ORB-SLAM3复现过程中遇到的问题及解决办法

在复现过程中遇到的问题的解决过程 1. 版本检查1.1 Opencv版本的检测1.2 Eigen版本的检测1.3 查看Python版本1.4 其他 2. 编译过程中遇到的问题及解决办法2.1 ./build.sh遇到的问题2.2 ./build_ros.sh遇到的问题 因为环境比较干净&#xff0c;所以遇到的问题相对少一些&#xf…

ARM 汇编基础知识

1.为什么学习汇编&#xff1f; 我们在进行嵌入式 Linux 开发的时候是绝对要掌握基本的 ARM 汇编&#xff0c;因为 Cortex-A 芯片一 上电 SP 指针还没初始化&#xff0c; C 环境还没准备好&#xff0c;所以肯定不能运行 C 代码&#xff0c;必须先用汇编语言设置好 C 环境…

【python】实现积分

借助sympy.integrate() 符号运算库&#xff0c;所以里面的exp()&#xff0c;sin()等都要使用sympy库中的函数&#xff0c;如果使用numpy库中的函数时没用的。 import sympy as sp import numpy as np x sp.symbols("x") print(sp.integrate(sp.exp(-x**2), (x, -s…

企业数据加密软件——「天锐绿盾」

「天锐绿盾」是一款企业数据加密软件&#xff0c;主要用于防止企业计算机信息被破坏、丢失和泄密。该软件采用文件过滤驱动实现透明加解密&#xff0c;对用户完全透明&#xff0c;不影响用户操作习惯。 PC访问地址&#xff1a; isite.baidu.com/site/wjz012xr/2eae091d-1b97-4…

makefile开发应用程序的一个通用模板

下面是一个通用的 Makefile 模板&#xff0c;用于开发 C 语言应用程序&#xff1a; # 编译器设置 CC gcc CFLAGS -Wall -Wextra -stdc99# 可执行文件名 TARGET your_program# 源文件和对象文件 SRCS main.c file1.c file2.c OBJS $(SRCS:.c.o)# 默认目标 all: $(TARGET)#…

buildroot修改内核防止清理重新加载办法

当你使用 Buildroot 构建 Linux 内核时&#xff0c;如果对内核文件进行了手动修改&#xff0c;重新执行 Buildroot 的构建过程将会覆盖你所做的修改。这是因为 Buildroot会根据配置重新下载、提取和编译内核。 为了避免在重新构建时覆盖你的修改&#xff0c;可以采取以下两种方…

减量时代下,伊利金领冠DTB新模式凭什么成为母婴行业“破局之道”?

穷则变&#xff0c;变则通&#xff0c;通则久。 当下&#xff0c;许多行业正在穿越不确定性周期&#xff0c;增长放缓成为常态。如何通过创新应对变局&#xff0c;进而实现长期主义&#xff0c;成为摆在所有行业和企业面前的课题。 众多行业中&#xff0c;婴配粉市场的不确定…

lv3 嵌入式开发-3 linux shell命令(文件搜索、文件处理、压缩)

目录 1 查看文件相关命令 1.1 常用命令 1.2 硬链接和软链接 2 文件搜索相关命令 2.1 查找文件命令 2.2 查找文件内容命令 2.3 其他相关命令 3 文件处理相关命令 3.1 cut 3.2 sed 过滤 3.3 awk 匹配 4 解压缩相关命令 4.1 解压缩文件的意义 4.2 解压缩相关命令 1 …

推荐个一行代码的Python可视化神器

学过Python数据分析的朋友都知道&#xff0c;在可视化的工具中&#xff0c;有很多优秀的三方库&#xff0c;比如matplotlib&#xff0c;seaborn&#xff0c;plotly&#xff0c;Boken&#xff0c;pyecharts等等。这些可视化库都有自己的特点&#xff0c;在实际应用中也广为大家使…

数据结构(Java实现)-反射、枚举以及lambda表达式

Java的反射&#xff08;reflection&#xff09;机制是在运行状态中&#xff0c;对于任意一个类&#xff0c;都能够知道这个类的所有属性和方法&#xff1b;对于任意一个对象&#xff0c;都能够调用它的任意方法和属性&#xff0c;既然能拿到那么&#xff0c;我们就可以修改部分…

内网隧道代理技术(二十一)之 CS工具自带中转技术上线不出网机器

CS工具自带上线不出网机器 如图A区域存在一台中转机器,这台机器可以出网,这种是最常见的情况。我们在渗透测试的过程中经常是拿下一台边缘机器,其有多块网卡,边缘机器可以访问内网机器,内网机器都不出网。这种情况下拿这个边缘机器做中转,就可以使用CS工具自带上线不出网…

shiro550漏洞分析

准备工作 启动该项目 可以看到没有登录时候&#xff0c;cookie中没有rememberme字段 登录时候 当账号密码输入正确时候 登录后存在该字段 shiro特征&#xff1a; 未登陆的情况下&#xff0c;请求包的cookie中没有rememberMe字段&#xff0c;返回包set-Cookie⾥也没有del…

JMeter测试工具

JMeter测试工具 1、下载地址&#xff1a; https://downloads.apache.org/jmeter/binaries/ https://downloads.apache.org/jmeter/binaries/2、启动 解压&#xff1a; 点击bin目录下的jmeter.bat就可以运行 jmeter.bat3、流控规则测试

蓝牙发展现状

目录 一、产品分类1、Bluetooth经典2、Bluetooth低能耗(LE)3、二者差异 二、出货量三、未来需要加强的方向四、技术行业细分五、学习资料1、蓝牙官网2、大神博客——于忠军 一、产品分类 1、Bluetooth经典 Bluetooth Classic无线电&#xff0c;也被称为Bluetooth 基本速率/增强…

EasyExcel导出模板实现下拉选(解决下拉超过50个限制)

学习地址&#xff1a;https://d9bp4nr5ye.feishu.cn/wiki/O3obweIbgi2Rk1ksXJncpClTnAfB站视频&#xff1a;https://www.bilibili.com/video/BV1H34y1T7Lm 先来看看最终实现效果&#xff0c;如果效果是你想要的&#xff0c;再看看实现逻辑。 EasyExcel本身是支持设置下拉校验的…

大学物理 之 安培环路定理

文章目录 前言什么是安培环路定理安培环路定理有什么作用 深入了解深入学习 前言 什么是安培环路定理 安培环路定理的物理意义在于描述了电流和磁场之间的相互作用&#xff0c;以及如何在一个封闭的回路中分析这种相互作用。 简单的来说 , 用环路定理来解决在磁场中B对任意封…

教你如何让iPhone电池更健康,不容错过的10个技巧

iPhone是一款功能强大的设备,但与许多电子产品一样,它需要一些维护才能确保正常工作。就像一艘可以永远航行的船,只要人们愿意维护它,只要你保持电池健康,你的iPhone就会继续工作。 以下是为什么维护iPhone电池至关重要,以及如何做到这一点,让你的设备使用更长时间。 …

数据结构 -作用及基本概念

为什么要使用数据结构 学习数据结构是计算机科学和软件工程领域中非常重要的一门课程。以下是学习数据结构的几个重要原因&#xff1a; 组织和管理数据&#xff1a;数据结构提供了一种组织和管理数据的方式。通过学习不同的数据结构&#xff0c;你可以了解如何有效地存储和操作…

【项目经验】:elementui表格中表头的多选框换成文字

一.项目需求 表格可以多选&#xff0c;表头都是汉字。。。。类似于这种 二.实现功能 用到的方法 Table Attributes 参数说明类型可选值默认值header-cell-class-name表头单元格的 className 的回调方法&#xff0c;也可以使用字符串为所有表头单元格设置一个固定的 className。…

在STS里使用Gradle编译Apache POI5.0.0

1、到官方下面地址下载Gradle最新的版本 Gradle Distributions 2、解压后拷贝到D盘下D:\gradle-8.3-rc-4里 3、配置环境变量 新建系统变量 GRADLE_HOME &#xff0c;值为 路径 4、在 Path 中添加上面目录的 bin 文件路径 &#xff08;可以用 %GRADLE_HOME%\bin&#xff0c…