本小节是Kotlin基本语法的一个重点章节,介绍了Kotlin中的类型体系和空安全这个重要特性,最后分析了空安全在与Java互操作过程中存在的问题。
类型体系
在Java中Object是所有引用类型的基类,而在Kotlin的类型系统中对应的为Any类,另外java存在int/long等等基本数据类型,而在Kotlin中没有,统一使用Int/Long等引用类型。
数字类型
Kotlin中的数字类型与Java基本一致。
类型 | 宽度(Bit) |
---|---|
Double | 64 |
Float | 32 |
Long | 64 |
Int | 32 |
Short | 16 |
Byte | 8 |
这些数字类统一继承Number类,其提供了不同类型间显式转换的方法。
val x: Int = 1
//toXXX转换函数
val y: Long = x.toLong()
这里需要注意一个问题,如果Kotlin中没有Java中的基本类型,所有对象都是引用类型,那对于最最常用的数字会不会产生巨大的性能开销?
事实上,虽然Kotlin中没有基本类型,但它编译成字节码时会做一步优化:将不可空类型(比如Int)优化为Java中的基本类型,将可空对象(比如Int?)转为包装(比如Integer)类型,因为可空类型可赋值为null,Java基本类型是不够用的,所以选择包装类型,此过程可通过反编译kotlin字节码验证。
//测试代码
var i: Int = 1
var j: Int? = 1
println(i)
println(j)
//反编译结果
int i = 1;
Integer j = 1;
System.out.println(i);
System.out.println(j);
字符串类型String
Kotlin中的String比Java更为强大,支持一系列的扩展函数(后面会讲到),在日常的开发过程中非常实用,举个栗子。
//使用filter扩展函数 过滤掉'c'字符
val result = "abcddddface".filter { it != 'c' }
print("result:$result")
输出:
result:abddddfae
上面的打印中使用了$来引用一个变量,我们称之为字符串模板。这种用法已经和现阶段的脚本语言完全一致了。
如果想引用一个表达式需要$后面跟花括号。
val name = "jenny"
print("size:${name.length}")
访问字符串中的元素可以像访问数组一样。
val name = "jenny"
//访问首字母
print("size:${name[0]}")
如果像打印字符串中原始内容而不受转义字符、空格、回车的影响可以使用三引号(""")实现。
val s: String = """
for (a in "abc")
print(a)
"""
数组类型
数组类为Array,可通过arrayOf方法创建一个数组。与Java不同的是,arrayOf可接收不同类型的元素,如果类型不同相当于Java中的Object类型的数组。
//Int类型数组
val array1 = arrayOf(1, 4, 5)
//Any类型数组
val array2 = arrayOf("1", 1, null)
使用arrayOfNulls可创建指定大小的所有元素都为null的数组,但使用时需声明类型。
//声明Int类型
val xx = arrayOfNulls<Int>(9)
默认数组的打印是数组类型,若想打印所有元素可以使用joinToString方法
val array1 = arrayOf(1, 4, 5)
println(array1.joinToString())
//输出
1, 4, 5
获取类型
若想获取一个Kotlin对象的类型可以使用符号::
,如果想获取其Java类型则继续使用.java
方法。
由于获取类型使用Kotlin的反射,需额外依赖反射库。
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
来个栗子:
val any = Any()
println(any) //打印java.lang.Object
println(any::class) //打印class kotlin.Any
println(any::class.java) //打印class java.lang.Object
//输出
java.lang.Object@27c170f0
class kotlin.Any
class java.lang.Object
另外::
还能获取到方法的引用。
//引用顶层函数println,依次打印数组元素
arrayOf("a","b","c").forEach(::println)
类型检查与转换
使用is
关键字检查类型,与之相反是!is
,对应Java中的instanceof。不同的是,如果检查类型相符则不需要再进行强制转换。
open class Person {
fun work() {
}
}
class Student: Person() {
fun study() {
}
}
val p: Person = Student()
if(p is Student) {
//类型检查通过后不需强转,并且可直接使用对应类型的方法
p.study()
}
显式强制转换类型需使用as
关键字,如果想避免类型转换异常需使用as?
,如果强转失败会返回null。
val p = Person()
val student = p as Student //java.lang.ClassCastException
//val student = p as? Student// 返回null
println(student)
可空类型
Kotlin使用可空类型实现空安全,类型后跟?
表示可空类型。
//声明一个可空的Int类型变量
val x: Int? = null
反编译为Java代码可以发现,Kotlin中的Any变量,到Java中会使用@NotNull修饰,而Any?会被@Nullable修饰。
我们先来看看null到底是什么类型。
fun main(args: Array<String>) {
println(null == null) //打印true
println(null != null) //打印false
println(null is Any) //打印false
println(null is Any?) //打印true
}
可见null不是Any类型,而是Any?类型。
空安全
有了类型具体的可空/非空性,可大大缩减空指针出现的几率。
Kotlin编译器在编译阶段对可空类型进行检查,防止程序在运行时发生空指针异常。
我们访问可空类型对象的方法时需加?
,如果此对象确实是null,则方法表达式结果最终返回null。
val x: Int? = null
val y = x?.toLong()
println(y) //输出null
在调用函数过程中可进行验空判断使用?:
操作符,后面可以跟对象或表达式。
val x: Int? = null
val y = x?.toLong() ?: 0
println(y)//打印0
x?.toLong() ?: println("null ex")//打印 null ex
有了这个操作符,原本Java语言中的验空代码就可以一行完成,更加简洁。
//java验空
String str = ...
if(str != null) {
str.toUpperCase();
}
//kotlin验空
val str: String? = ...
str?.toUpperCase()
如果确认访问的对象一定不是空可使用!!
操作符告诉编译器此处空指针的检查。
val x: Int? = 3
val y = x!!.toLong()//x 此时一定不是null
println(y)
真的就没有空指针了吗?
在Kotlin的体系下看上去确实解决了空指针问题,但实际场景是项目中存在Kotlin和Java代码相互调用的场景。
- Java模块使用Kotlin开发的library,反之同理。
- 同一个模块同时混编java和Kotlin代码。
一旦出现与Java的互操作则情况就变得复杂了。
Java调用Kotlin
我们先来看Java调用Kotlin代码。
//java类
public class TestMethod {
public static void test() {
Person p = new Person();
p.work(null);//传入空 编译通过
}
}
//kotlin类
open class Person {
//调用参数为不可空类型
fun work(detail: String) {
println("my work is $detail")
}
}
@JvmStatic
fun main(args: Array<String>) {
//Exception in thread "main" java.lang.IllegalArgumentException: Parameter specified as non-null is null
TestMethod.test()
}
可见在运行抛出了IllegalArgumentException异常。因为在Java环境并不会做Kotlin实参的空类型检查,而进入kotlin代码后会检查形参的可空性,当检查失败时抛出非法参数异常。
为了弄清这个异常时如何抛出的,我们反编译了Person类的Kotlin代码。
# 反编译后的java代码
public class Person {
public final void work(@NotNull String detail) {
//参数检查
Intrinsics.checkParameterIsNotNull(detail, "detail");
String var2 = "my work is " + detail;
System.out.println(var2);
}
}
public static void checkParameterIsNotNull(Object value, String paramName) {
if (value == null) {
//若参数为空 抛出异常
throwParameterIsNullException(paramName);
}
}
private static void throwParameterIsNullException(String paramName) {
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
...
//非法参数异常在这里创建并最终抛出
IllegalArgumentException exception =
new IllegalArgumentException("Parameter specified as non-null is null: " +
"method " + className + "." + methodName +
", parameter " + paramName);
throw sanitizeStackTrace(exception);
}
Kotlin调用Java
反过来,我们再来看看Kotlin调用Java类的例子。
# Utils是一个Java工具类
public class Utils {
static String format(String text) {
return text.isEmpty() ? null : text;
}
}
使用Kotlin测试Utils类的format方法。
@JvmStatic
fun main(args: Array<String>) {
doSomething("")
}
fun doSomething(text: String) {
//call java method
val f: String = Utils.format(text) //(1) IllegalStateException
println ("f.len : " + f.length)
}
运行main函数会发现,代码(1)处会抛出IllegalStateException异常。因为这里试图将一个空类型赋值给一个不可空类型,然而这个异常在编译阶段是不会被检查的。
解决办法是我们将f声明为可空类型。
val f: String? = Utils.format(text)//运行正常
但实际场景是我们经常使用类型推断,因而根本不会显式声明f的类型。
fun doSomething(text: String) {
//call java method
val f = Utils.format(text)
println ("f.len : " + f.length) //(2) NullPointerException
}
再次运行main函数,会发现出现我们最不想看到的空指针异常,异常发生在代码(2)处。由于没有显式声明f的类型,Kotlin通过format方法的返回值推断为String类型,且不会做参数的检查,当执行到f.length时便触发了空指针异常。
我们看看反编译的Java代码加深理解。
public final void doSomething(@NotNull String text) {
Intrinsics.checkParameterIsNotNull(text, "text");
String f = Utils.format(text);
String var3 = "f.len : " + f.length();
System.out.println(var3);
}
总结一下,Java与Kotlin的相互调用出现异常的原因。
- Java调用Kotlin方法时并不检查实参的可空性。
- Kotlin调用Java方法声明返回值类型时不具备类型推断能力。
总结
可见,当Java代码与Kotlin代码在项目中产生调用关系时,Kotlin的空安全特性可能引发一些不可期的异常。然而在现阶段这几乎不可避免,因为常见的第三方库几乎都是使用Java编写的,这也是Kotlin官方急于在新特性新功能方面优先支持Kotlin语言的一个重要原因。
网友评论