本文整理自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) // 編譯通過
}