Kotlin 类型体系和基本操作符

本文整理自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) // 编译通过
}

 

©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值