本文整理自Chiclaim的博客:
https://chiclaim.blog.csdn.net/article/details/85575213
https://chiclaim.blog.csdn.net/article/details/88624808
一、 原始数据类型
我们知道,在 Java 中的数据类型分基本数据类型和基本数据类型对应的包装类型。如 Java 中的整型 int 和它对应的 Integer包装类型。
在 Kotlin 中是没有这样的区分的,例如对于整型来说只有 Int 这一个类型,Int 是一个类(姑且把它当装包装类型),我们可以说在 Kotlin 中在编译前只有包装类型,为什么说是编译前呢?因为编译时会根据情况把这个整型( Int )是编译成 Java 中的 int 还是 Integer。 那么是根据哪些情况来编译成基本类型还是包装类型呢,后面会讲到。我们先来看下 Kotlin和 Java 数据类型对比:
下面来分析下哪些情况编译成Java中的基本类型还是包装类型。下面以整型为例,其他的数据类型同理。
1. 如果变量可以为null(使用操作符?
),则编译后是包装类型
//因为可以为 null,所以编译后为 Integer
var width: Int? = 10
var width: Int? = null
//编译后的代码
@Nullable
private static Integer width = 10;
@Nullable
private static Integer width;
再来看看方法返回值为整型:
//返回值 Int 编译后变成基本类型 int
fun getAge(): Int {
return 0
}
//返回值 Int 编译后变成 Integer
fun getAge(): Int? {
return 0
}
所以声明变量后者方法返回值的时候,如果声明可以为 null,那么编译后时是包装类型,反之就是基本类型。
2. 如果使用了泛型则编译后是包装类型,如集合泛型、数组泛型等
//集合泛型
//集合里的元素都是 Integer 类型
fun getAge3(): List<Int> {
return listOf(22, 90, 50)
}
//数组泛型
//会编译成一个 Integer[]
fun getAge4(): Array<Int> {
return arrayOf(170, 180, 190)
}
//看下编译后的代码:
@NotNull
public static final List getAge3() {
return CollectionsKt.listOf(new Integer[]{22, 90, 50});
}
@NotNull
public static final Integer[] getAge4() {
return new Integer[]{170, 180, 190};
}
3. 如果想要声明的数组编译后是基本类型的数组,需要使用 xxxArrayOf(…),如 intArrayOf
从上面的例子中,关于集合泛型编译后是包装类型在 Java 中也是一样的。如果想要声明的数组编译后是基本类型的数组,需要使用 Kotlin 为我们提供的方法:
//会编译成一个int[]
fun getAge5(): IntArray {
return intArrayOf(170, 180, 190)
}
当然,除了intArrayOf,还有charArrayOf、floatArrayOf等等,就不一一列举了。
4. 为什么 Kotlin 要单独设计一套这样的数据类型,不共用 Java 的那一套呢?
我们都知道,Kotlin 是基于 JVM 的一款语言,编译后还是和 Java 一样。那么为什么不像集合那样直接使用 Java 那一套,要单独设计一套这样的数据类型呢?
Kotlin 中没有基本数据类型,都是用它自己的包装类型,包装类型是一个类,那么我们就可以使用这个类里面很多有用的方法。下面看下 Kotlin in Action 的一段代码:
fun showProgress(progress: Int) {
val percent = progress.coerceIn(0, 100)
println("We're $percent% done!")
}
编译后的代码为:
public static final void showProgress(int progress) {
int percent = RangesKt.coerceIn(progress, 0, 100);
String var2 = "We're " + percent + "% done!";
System.out.println(var2);
}
从中可以看出,在开发阶段我们可很方便地使用 Int 类扩展函数。编译后,依然编译成基本类型 int,使用到的扩展函数的逻辑也会包含在内。
二 、可空类型
可空类型 是 Kotlin 用来避免 NullPointException 异常的
例如下面的 Java 代码就可能会出现 空指针异常:
/*Java*/
int strLen(String s){
return s.length();
}
strLen(null); // throw NullPointException
如果上面的代码想要在 Kotlin 中避免空指针,可改成如下:
fun strLen(s: String) = s.length
strLen(null); // 编译报错
上面的函数参数声明表示参数不可为null,调用的时候杜绝了参数为空的情况
如果允许 strLen 函数可以传 null 怎么办呢?可以这样定义该函数:
fun strLenSafe(s: String?) = if (s != null) s.length else 0
在参数类型后面加上 ? ,表示该参数可以为 null
需要注意的是,可为空的变量不能赋值给不可为空的变量,如:
val x: String? = null
var y: String = x //编译报错
//ERROR: Type mismatch: inferred type is String? but String was expected
在为空性上,Kotlin 中有两种情况:可为空和不可为空;而 Java 都是可以为空的
三、安全调用操作符:?.
安全调用操作符(safe call operator): ?.
安全调用操作符 结合了 null 判断和函数调用,如:
fun test(s:String?){
s?.toUpperCase()
}
如果 s == null 那么 s?.toUpperCase() 返回 null,如果 s!=null 那就正常调用即可
如下图所示:
所以上面的代码不会出现空指针异常
安全调用操作符 ?.,不仅可以调用函数,还可以调用属性。
需要注意的是,使用了 ?. 需要注意其返回值类型:
val length = str?.length
if(length == 0){
//do something
}
这个时候如果 str == null 的话,那么 length 就是 null,它永远不等于0了
四、 Elvis操作符: ?:
Elvis操作符 用来为null提供默认值的,例如:
fun foo(s: String?) {
val t: String = s ?: ""
}
如果 s == null 则返回 “”,否则返回 s 本身,如下图所示:
上面介绍 可空性 时候的例子可以通过 Elvis操作符改造成更简洁:
fun strLenSafe(s: String?) = if (s != null) s.length else 0
//改成如下形式:
fun strLenSafe(s: String?) = s.length ?: 0
五、 安全强转操作符:as?
前面我们讲到了 Kotlin 的智能强转(smart casts),即通过 is 关键字来判断是否属于某个类型,然后编译器自动帮我们做强转操作
如果我们不想判断类型,直接强转呢?在 Java 中可能会出现 ClassCastException 异常
在 Kotlin 中我们可以通过 as? 操作符来避免类似这样的异常
as? 如果不能强转返回 null,反之返回强转之后的类型,如下图所示:
六 非空断言:!!
我们知道 Kotlin 中类型有可为空和不可为空两种
比如有一个函数的参数是不可空类型的,然后我们把一个可空的变量当做参数传递给该函数
此时Kotlin编译器肯定会报错的,这个时候可以使用非空断言。非空断言意思就是向编译器保证我这个变量肯定不会为空的
如下面伪代码:
var str:String?
// 参数不可为空
fun test(s: String) {
//...
}
// 非空断言
test(str!!)
注意:对于非空断言要谨慎使用,除非这个变量在实际情况真的不会为null,否则不要使用非空断言。虽然使用了非空断言编译器不报错了,但是如果使用非空断言的变量是空依然会出现空指针异常
非空断言的原理如下图所示:
七、延迟初始化属性
延迟初始化属性(Late-initialized properties),主要为了解决没必要的 非空断言 的出现
例如下面的代码:
class MyService {
fun performAction(): String = "foo"
}
class MyTest {
private var myService: MyService? = null
@Before fun setUp(){
myService = MyService()
}
@Test fun testAction(){
Assert.assertEquals("foo",myService!!.performAction())
}
}
我们知道属性 myService 肯定不会为空的,但是我们不得不为它加上 非空断言
这个时候可以使用 lateinit 关键字来对 myService 进行延迟初始化了
class MyTest {
private lateinit var myService: MyService
@Before fun setUp(){
myService = MyService()
}
@Test fun testAction(){
Assert.assertEquals("foo", myService.performAction())
}
}
这样就无需为 myService 加上非空断言了
八、 可空类型的扩展函数
在前面的章节我们已经介绍了扩展函数,那什么是 可空类型的扩展函数?
可空类型的扩展函数 就是在 Receive Type 后面加上问号(?)
如 Kotlin 内置的函数 isNullOrBlank:
public inline fun CharSequence?.isNullOrBlank(): Boolean
Kotlin 为我们提供了一些常用的 可空类型的扩展函数
如:isNullOrBlank、isNullOrEmpty
fun verifyUserInput(input: String?){
if (input.isNullOrBlank()) {
println("Please fill in the required fields")
}
}
verifyUserInput(null)
有些人可能会问 input==null,input.isNullOrBlank() 不会空指针吗?
根据上面对扩展函数的讲解,扩展函数编译后会变成静态调用
九、 数字类型转换
Kotlin 和 Java 另一个重要的不同点就是数字类型的转换上。
Kotlin 不会自动将数字从一个类型转换到另一个类型,例如:
val i = 1
val l: Long = i // 编译报错 Type mismatch
需要显示的将 Int 转成 Long:
val i = 1
val l: Long = i.toLong()
这些显式类型转换函数定义在每个原始类型上,除了 Boolean 类型
Kotlin 之所以在数字类型的转换上使用显示转换,是为了避免一些奇怪的问题。
例如,下面的 Java 例子 返回 false:
new Integer(42).equals(new Long(42)) //false
Integer 和 Long 使用 equals 函数比较,底层是先判断参数的类型:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
如果 Kotlin 也支持隐式类型转换的话,下面的代码也会返回 false ,因为底层也是通过 equals 函数来判断的:
val x = 1 // Int
val list = listOf(1L, 2L, 3L)
x in list
但是在Kotlin中上面的代码会编译报错,因为类型不匹配
上面的 val x = 1,没有写变量类型,Kotlin编译器会推导出它是个 Int
如果字面量是整数,那么类型就是 Int
如果字面量是小数,那么类型就是 Double
如果字面量是以 f 或 F 结尾,那么类型就是 Float
如果字面量是 L 结尾,那么类型就是 Long
如果字面量是十六进制(前缀是0x或0X),那么类型是 Long
如果字面量是二进制(前缀是0b或0B),那么类型是 Int
如果字面量是单引号中,那么类型就是 Char
需要注意的是,数字字面量当做函数参数或进行算术操作时,Kotlin会自动进行相应类型的转换
fun foo(l: Long) = println(l)
val y = 0
foo(0) // 数字字面量作为参数
foo(y) // 编译报错
val b: Byte = 1
val l = b + 1L // b 自动转成 long 类型
十、 Any类型
Any 类型 和 Java 中的 Object 类似,是Kotlin中所有类的父类
包括原始类型的包装类:Int、Float 等
Any 在编译后就是 Java 的 Object
Any 类也有 toString() , equals() , and hashCode() 函数
如果想要调用 wait 或 notify,需要把 Any 强转成 Object
十一、Unit 类型
Unit 类型和 Java 中的 void 是一个意思
下面介绍它们在使用过程的几个不同点:
1). 函数没有返回值,Unit可以省略
例如下面的函数可以省略 Unit:
fun f(): Unit { ... }
fun f() { ... } //省略 Unit
但是在 Java 中则不能省略 void 关键字
2) Unit 作为 Type Arguments
例如下面的例子:
interface Processor<T> {
fun process(): T
}
// Unit 作为 Type Arguments
class NoResultProcessor : Processor<Unit> {
override fun process() { // 省略 Unit
// do stuff
}
}
如果在 Java 中,则需要使用 Void 类:
class NoResultProcessor implements Processor<Void> {
@Override
public Void process() {
return null; //需要显式的 return null
}
}
十二、Nothing 类型
Nothing 类是一个 标记类
Nothing 不包含任何值,它是一个空类
public class Nothing private constructor()
Nothing 主要用于 函数的返回类型 或者 Type Argument
关于 Type Argument 的概念已经在前面的 Parameter和Argument的区别 章节介绍过了
下面介绍下 Nothing 用于函数的返回类型
对于有些 Kotlin 函数的返回值没有什么实际意义,特别是在程序异常中断的时候,例如:
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
你可能会问,既然返回值没有意义,使用Unit不就可以了吗?
但是如果使用Unit,当与 Elvis 操作符 结合使用的时候就不太方便:
fun fail(message: String) { // return Unit
throw IllegalStateException(message)
}
fun main() {
var address: String? = null
val result = address ?: fail("No address")
//编译器报错,因为result是Unit类型,所以result没有length属性
println(result.length)
}
这个时候使用 Nothing 类型作为 fail 函数的返回类型 就可以解决这个问题:
fun fail(message: String) : Nothing {
throw IllegalStateException(message)
}
fun main() {
var address: String? = null
val result = address ?: fail("No address")
println(result.length) // 编译通过
}