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) // 編譯通過
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章