第3章 类型系统与可空类型 《Kotlin项目实战开发》

来源:互联网 发布:apache ab 中文参数 编辑:程序博客网 时间:2024/06/03 17:39

JavaCC ++ 一样, Kotlin也是静态类型编程语言



通常,编程语言中的类型系统中定义了

如何将数值和表达式归为不同的类型

如何操作这些类型

这些类型之间如何互相作用

我们在编程语言中使用类型的目的是为了让编译器能够确定类型所关联的对象需要分配多少空间。

在每一个编程语言中,都有一个特定的类型系统。静态类型在编译时期时,就能可靠地发现类型错误。因此通常能增进最终程序的可靠性。

类型系统在各种语言之间有非常大的不同,主要的差异存在于编译时期的语法,以及运行时期的操作实现方式。

本章我们简单介绍一下Kotlin的类型系统。

3.1  类型系统

我们知道在计算机中中,信息都是以01比特存储的,硬件无法区分存储器地址、脚本、字符、整数、以及浮点数。为了赋予01比特意义,于是有了类型。

3.1.1  类型系统的作用

使用类型系统,编译器可以检查无意义的、无效的、类型不匹配等错误代码。这也正是强类型语言能够提供更多的代码安全性保障的原因之一。

另外,静态类型检查还可以提供有用的信息给编译器。跟动态类型语言相比,由于有了类型的显式声明,静态类型的语言更加易读好懂。

有了类型我们还可以更好地做抽象化、模块化的工作。这使得我们可以在较高抽象层次思考并解决问题。例如,Java中的字符数组 char[] s = {'a','b', 'c'}和字符串类型 String str = "abc"就是最简单最典型的抽象封装实例。

字符数组

jshell> char[] s = {'a','b','c'}  // 声明并初始化一个字符数组

s ==> char[3] { 'a', 'b', 'c' } 

jshell> s[0] $3 ==> 'a' 

jshell> s[1] $4 ==> 'b' 

jshell> s[2] $5 ==> 'c'

 

字符串

jshell> String str = "abc" // 直接声明一个字符串类型的变量

str ==> "abc" 

jshell> str.toCharArray() // 字符串可以通过这个方法转为字符数组

$7 ==> char[3] { 'a', 'b', 'c' } 

3.1.2  Java的类型系统

Java的类型系统可以简单用下面的图来表示:

图3-1 Java的类型系统

 

关于Java中的null,有很多比较坑的地方。例如

int i = null; //类型不匹配,不能把null转换成int

short s = null; //类型不匹配,不能把null转换成 short

byte b = null: //类型不匹配,不能把null转换成 byte

double d = null; //类型不匹配,不能把null转换成double  

Integer io = null; // 这个可以编译通过

int j = io; //这个也可以编译通过, 但是在运行的时候会抛 NullPointerException ,这是Java的null比较坑的地方,在Kotlin中不会发生这样的情况,下文我们将会看到关于Kotlin的可空类型的介绍

基本数据类型与引用数据型在创建时,内存存储方式区别如下:

基本数据类型在被创建时,在栈上给其划分一块内存,将数值直接存储在栈上(性能高)。

引用数据型在被创建时,首先在栈上给其引用(句柄)分配一块内存,而对象的具体信息存储在堆内存上,然后由栈上面的引用指向堆中对象的地址。

3.1.3  Kotlin的类型系统

Java是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,为了能够将这些基本数据类型当成对象操作,Java为每一个基本数据类型都引入了对应的包装类型(wrapper class),例如int的包装类就是Integer。从Java 5开始引入了自动装箱/拆箱机制,使得二者可以相互转换。Java为每个原始类型提供了包装类型:

原始类型: booleancharbyteshortintlongfloatdouble

包装类型:BooleanCharacterByteShortIntegerLongFloatDouble

Kotlin中去掉了原始类型,只有包装类型编译器在编译代码的时候,会自动优化性能,把对应的包装类型拆箱为原始类型。

Kotlin系统类型分为可空类型和不可空类型。Kotlin中引入了可空类型,把有可能为null的值单独用可空类型来表示。这样就在可空引用与不可空引用之间划分出来一条明确的显式的界线

Kotlin类型层次结构如图3-2所示:

图3-2 Kotlin类型层次结构

 

通过这样显式地使用可空类型,并在编译器作类型检查,大大降低了出现空指针异常的概率。

对于Kotlin的数字类型而言,不可空类型与Java中的原始的数字类型对应。如表3-1所示

表3-1 Kotlin的不可空数字类型与Java的数字类型映射

Kotlin

Java

Int

int

Long

long

Float

float

Double

double

 

Kotlin中对应的可空数字类型就相当于Java中的装箱数字类型。如表3-2所示

表3-2 Kotlin的可空数字类型与Java的数字类型映射

Kotlin

Java

Int?

Integer

Long?

Long

Float?

Float

Double?

Double

 

Java中,从基本数字类型到引用数字类型的转换就是典型的装箱操作,例如int转为Integer。同理,在Kotlin中,非空数字类型到可空数组类型需要进行装箱操作。

>>> val a: Int = 1000 // 不可空Int

>>> val b:Int = 1000

>>> a===b

true

>>> a==b

true

上面返回的都是true,因为a,b它们都是以原始类型存储的,类似于Java中的基本数字类型。

>>> val a:Int? = 1000 // 可空Int

>>> val b:Int? = 1000

>>> a==b

true

>>> a===b

false 

我们可以看出,当 a, b 都为可空类型时, a b 的引用是不等的。

这里的等于号简单说明如表3-3所示:

表3-3 Kotlin中的“等于”号

等于符号

功能说明

=

赋值,在逻辑运算时也有效

==

等于运算,比较的是值,而不是引用

===

完全等于运算,不仅比较值,而且还比较引用,只有两者一致才为真

 

另外,Java中的数组也是一个较为特殊的类型。这个类型是 T[] ,这个方括号让我们觉得不大优雅。Kotlin中摒弃了这个数组类型声明的语法。Kotlin简单直接地使用Array类型代表数组类型。这个Array中定义了get set 算子函数,同时有一个size属性代表数组的长度,还有一个返回数组元素的迭代子 Iterator<T>的函数iterator()

Kotlin数组Array完整的定义如下:

public class Array<T> {

     public inlineconstructor(size: Int, init: (Int) -> T) // 构造函数

     public operator funget(index: Int): T // get操作符,等价于Array[index]

     public operator funset(index: Int, value: T): Unit // set操作符,等价于Array[index]=value

     public val size: Int // 数组长度,不可变Int

     public operator funiterator(): Iterator<T> // 数组元素迭代子

 }

其中,构造函数我们可以这么用

>>> val square = Array(5, { i -> i * i }) // 创建一个长度是5的数组,i对应数组的下标,值是i * i

>>> square.forEach(::println)

0 1 4 9 16 

我们在编程过程中常用的boolean[], char[]byte[]short[]int[]long[]float[]double[]Kotlin直接使用了8个新的类型来对应这样的编程场景:

BooleanArray

ByteArray

CharArray

DoubleArray

FloatArray

IntArray

LongArray

ShortArray

3.2  可空类型T

我想JavaAndroid开发者肯定早已厌倦了空指针异常(Null PointerException)。这个讨厌的空指针异常在运行时总会在某个你意想不到的地方忽然出现,让我们感到措手不及。

自然而然地,人们会想到为何不能在编译时就提前发现这类空指针异常,并大量修复这些问题?现代编程语言正是这么做的。

Kotlin自然也不例外。

Java 8中,我们可以使用 Optional类型来表达可空的类型。

package com.easy.kotlin;

import java.util.Optional;

import static java.lang.System.out; 

public class Java8OptionalDemo {

      public static void main(String[]args){

        out.println(strLength(Optional.of("abc")));// Optional.of("abc") 创建一个可空的Optional<String>类型的变量

        out.println(strLength(Optional.ofNullable(null)));

     }

      static IntegerstrLength(Optional<String> s) {

         returns.orElse("").length();

     }

 }

运行输出:

3 0

但是,这样的代码,依然不是那么地优雅。

Groovy中提供了一种安全的属性/方法访问操作符(?.)

user?.getUsername()?.toUpperCase();

Swift 也有类似的语法,只作用在 Optional的类型上。

Kotlin中使用了Groovy里面的安全调用符,并简化了 Optional类型的使用,直接通过在类型T后面加个?,这个类型 T? 就表达了Optional<T>的意义。

上面 Java 8的例子,用 Kotlin来写就显得更加简单优雅了:

package com.easy.kotlin

fun main(args: Array<String>) {

    println(strLength(null))

    println(strLength("abc"))

}

fun strLength(s: String?): Int {

    return s?.length ?: 0// 安全调用符和Elvis操作符

}

其中,我们使用 String?同样表达了Optional<String>的意思。相比之下,哪个更简单呢?想必是一目了然啦。

3.3  安全操作符

让我们赶紧扔掉Java中的一堆null的防御式样板代码吧!

当我们使用Java开发的时候,我们的代码大多是防御性的。如果我们不想遇到NullPointerException,我们就需要在使用它之前不停地去判断它是否为null

有了安全调用符(?.) , Elvis操作符(?:) let 函数,我们就可以完全抛弃掉 Java中冗长的 if-null if-not-null语句了。

本节我们来学习一下在Kotlin中如何优雅正确地使用安全操作符。

3.3.1  可空类型与null

Java中,如果我们访问一个空引用的成员变量或者方法,可能会导致空指针异常NullPointerException(NPE)

Kotlin正如很多现代编程语言一样——是空安全的。Kotlin中引入了可空类型。在Kotlin中,类型系统将可空类型和不可空类型进行了区分,例如,String为不可空类型,String?为可空类型,如果将不可空类型赋值为null将会编译不通过。

我们通过一个可空类型符号T?来明确地指定一个对象类型T是否能为空。

我们可以像这样去写:

>>> val str: String = null // null不能赋值给不可空的String类型

 error: null can not be avalue of a non-null type String val str: String = null                   ^ 

我们可以看到,这里不能通过编译, String类型不能是null

一个可以赋值为nullString类型的正确姿势是:String? ,代码如下所示

>>> var nullableStr: String? = null // null可以赋值给可空的字符串类型String?

>>> nullableStr

 null

我们再来看一下Kotlin中关于null的一些有趣的运算。

nullnull是相等的:

>>> null==null

 true

>>> null!=null

 false

null这个值比较特殊,null不是Any类型

>>> null is Any

 false

但是,nullAny?类型:

>>> null is Any?

 true

我们来看看null对应的类型是什么:

>>> var a=null // 隐式声明变量a,并初始化值为null

>>> a is Nothing? // a 是Nothing?类型

true

>>> a is Nothing // a 不是Nothing类型

false

>>> a

 null

>>> a=1 // 1 不能赋值给a, Nothing? 类型只有一个值 null

error: the integer literal does not conform to the expected typeNothing? a=1   ^

从报错信息我们可以看出,null的类型是Nothing?。关于Nothing?我们将会在下面的小节中介绍。

3.3.2  安全调用符

我们不能直接使用可空的nullableStr来调用其属性或者方法

>>> var nullableStr: String? = null

>>> nullableStr.length

error: only safe (?.) or non-null asserted (!!.) calls areallowed on a nullable receiver of type String? nullableStr.length            ^

上面的代码无法编译, nullableStr可能是null。我们需要使用安全调用符(?.)来调用

>>> nullableStr?.length //如果 nullableStr为空,则返回 null。这个表达式的类型是Int?

 null

>>> nullableStr = "abc"

>>> nullableStr?.length //如果 nullableStr非空,就返回 nullableStr.length。这个表达式的类型是 Int?

 3

只有在 nullableStr != null时才会去调用其length属性。

3.3.3  非空断言

如果我们想只有在确保 nullableStr不是null的情况下才能这么调用,否则抛出异常,我们可以使用断言操作符( !!

>>> nullableStr = null

>>> nullableStr!!.length

 kotlin.KotlinNullPointerException

非空断言符!!类似Java中 调用Assert语句。非空断言可以用在你需要抛出异常时(一般情况下,不建议使用)。

Kotlin中对于不可空类型的对象变量,我们可以直接调用它的成员变量或者函数。而对可空类型,直接调用成员变量或者函数将会直接编译不通过,直接在语法层面做出了限制。这样可以最大限度的去避免空指针异常。

 

3.3.4  Elvis运算符

Java 8 Optional提供了orElse方法,我们可以如下使用

s.orElse("").length();

这个东东,在 Kotlin 是最最常见不过的 Elvis 运算符( :? )了:

s?.length ?: 0

相比之下,还有什么理由继续用 Java 8 Optional 呢?

我们也可以使用Elvis操作符来给定一个在是null的情况下的替代值

>>> nullableStr

 null

>>> var s= nullableStr?:"NULL" // 如果nullableStr是null,表达式的值返回“NULL”

>>> s

 NULL

 

3.3.5  let函数

let函数是一个inline函数,它的定义如下

@kotlin.internal.InlineOnly

public inline fun <T, R> T.let(block: (T) -> R): R =block(this) //默认以当前这个对象T作为闭包的block参数

如果一个List中有空null元素和非空String元素,而只要对非空值执行某个操作,我们就可以使用let操作符

>>> val listWithNulls= listOf("A","B","C",null,null,"D")

>>> listWithNulls

[A, B, C, null, null, D]

>>> listWithNulls.forEach{ it?.let{ println(it) }  }

A

B

C

D

我们再举个简单的例子。例如,我们想要在TextViewtext都不为null时,在消息框(toast)中显示text消息,代码如下

textView?.text?.let { toast(it) }

是不是相当简洁、易懂?

3.4  特殊类型

本节我们介绍Kotlin中的特殊类型:UnitNothingAny以及其对应的可空类型Unit?,Nothing?,Any?

3.4.1  Unit类型

Kotlin也是面向表达式的语言。在Kotlin中所有控制流语句都是表达式(除了变量赋值、异常等)。

Kotlin中的Unit类型实现了与Java中的void一样的功能。

总的来说,这个Unit类型并没有什么特别之处。它的定义是:

package kotlin

public object Unit {

     override funtoString() = "kotlin.Unit" // object对象Unit的toString()函数固定返回"kotlin.Unit"这个字符串

 }

不同的是,当一个函数没有返回值的时候,我们用Unit来表示这个特征,而不是null

大多数时候,我们并不需要显式地返回Unit,或者声明一个函数的返回类型为Unit。编译器会推断出它。

代码示例:

>>> fun unitExample(){println("Hello,Unit")}

>>> val helloUnit = unitExample()

Hello,Unit

>>> helloUnit // 函数的返回是Unit, 调用Unit.toString()函数

kotlin.Unit

>>> println(helloUnit)

kotlin.Unit

>>> helloUnit is Unit // helloUnit是Unit类型

 true

我们可以看出,变量helloUnit的类型是 kotlin.Unit

下面几种写法是等价的:

@RunWith(JUnit4::class)

class UnitDemoTest {

     @Test funtestUnitDemo() {

         val ur1 =unitReturn1()

         println(ur1) //kotlin.Unit

         val ur2 =unitReturn2()

         println(ur2) //kotlin.Unit

         val ur3 =unitReturn3()

        println(ur3) //kotlin.Unit

     }

      fun unitReturn1() {}

      fun unitReturn2() {

         return Unit

     }

     fun unitReturn3():Unit {}

 }

跟任何其他类型一样,它的父类型是Any。如果是一个可空的Unit?,它的父类型是Any?

图3-3 Unit类型结构

 

3.4.2 Nothing类型

Java中,void不能是变量的类型。也不能被当做值打印输出。但是,在Java中有个包装类Void void 的自动装箱类型。如果你想让一个方法返回类型永远是 null的话, 可以把返回类型置为这个大写的VVoid类型。

代码示例:

    public Void voidDemo(){

        System.out.println("Hello,Void");

         return null; // 声明返回类型是Void的方法,只能返回null

     }

测试代码:

@RunWith(JUnit4.class)

 public class VoidDemoTest{

     @Test

     public voidtestVoid() {

         VoidDemo voidDemo= new VoidDemo();

         Void v =voidDemo.voidDemo(); // Hello,Void

        System.out.println(v); // null

     }

 }

这个Void对应Kotlin中的Nothing?。它的唯一可被访问到的返回值也是null

如上面小节的Kotlin类型层次结构图所示,在Kotlin类型层次结构的最底层就是类型Nothing

图3-4 Nothing的类型层次结构

它的定义如下:

public class Nothing private constructor()

这个Nothing不能被实例化

>>> Nothing() is Any // 不能调用Nothing()构造函数

error: cannot access '<init>': it is private in 'Nothing'Nothing() is Any ^

从上面代码示例,我们可以看出 Nothing()不可被访问。

如果一个函数的返回值是Nothing,这也就意味着这个函数永远不会有返回值。

但是,我们可以使用Nothing来表达一个从来不存在的返回值。例如EmptyList中的 get函数

 internal object EmptyList: List<Nothing>, Serializable, RandomAccess {

     override funget(index: Int): Nothing = throw IndexOutOfBoundsException("Empty listdoesn't contain element at index $index.")

     }

 }

一个空的List调用get函数,直接是抛出了IndexOutOfBoundsException,这个时候我们就可以使用Nothing作为这个get函数的返回类型,因为它永远不会返回某个值,而是直接抛出了异常。

再例如, Kotlin的标准库里面的exitProcess函数:

@file:kotlin.jvm.JvmName("ProcessKt")

@file:kotlin.jvm.JvmVersion

package kotlin.system

@kotlin.internal.InlineOnly

public inline fun exitProcess(status: Int): Nothing { // 该函数永远不会返回,在函数体内直接throw异常

     System.exit(status)

     throw RuntimeException("System.exitreturned normally, while it was supposed to halt JVM.")

 }

注意:UnitNothing之间的区别:Unit类型表达式计算结果的返回类型是UnitNothing类型的表达式计算结果是永远不会返回的(跟Java中的void相同)。

Nothing?可以只包含一个值:null。代码示例:

>>> var nul:Nothing?=null // Nothing?类型有且只有一个null值

>>> nul = 1

error: the integer literal does not conform to the expected typeNothing? nul = 1       ^

 >>> nul = true

error: the boolean literal does not conform to the expected typeNothing? nul = true       ^ 

>>> nul = null

>>> nul

null

从上面的代码示例,我们可以看出:Nothing?它唯一允许的值是null,被用作任何可空类型的空引用。

3.4.3  AnyAny类型

就像Any是在非空类型层次结构的根,Any?是可空类型层次的根。

Any?Any的超集,Any?Kotlin的类型层次结构的最顶端。

图3-5 Any与Any?类型

 

代码示例:

>>> 1 is Any

true

>>> 1 is Any?

true

>>> null is Any

false

>>> null is Any?

true

>>> Any() is Any?

true

3.5  类型检测与类型转换

我们知道,在动态类型语言中类型推导是一项基本功能。例如在JavaScript、Ruby、PHP、Python、Groovy等语言中,我们声明变量的时候不需要显式指定其类型。

而Java是一门强类型的静态类型语言。在 Java 中,我们定义变量的时候都必须声明类型。虽然Kotlin也是一门纯正的静态类型语言,但是Kotlin中也像Scala一样提供了类型推断的功能。在Kotlin中,多数情况下我们不需要显式地声明类型。例如val str =“abc” , 一个字符串字面量“abc”足以指明这是str变量是一个字符串类型。在Kotlin中像字符类型Char,整型Int,长整型Long,单浮点数Float,双浮点数Double,布尔值Boolean都是可以无需显性声明类型的,还有常用的集合类等。

代码示例

>>> val c = 'c'

>>> c::class

class kotlin.Char

>>> val i = 1

>>> i::class

class kotlin.Int

>>> val d = 1.0

>>> d::class

class kotlin.Double

>>> val map =mapOf(Pair("x","123"),Pair("y","abc"))

>>> map::class

class java.util.LinkedHashMap

 

3.5.1  is运算符

is运算符可以检查对象是否与特定的类型兼容(此对象是该类型,或者派生于该类型)。

is运算符用来检查一个对象(变量)是否属于某数据类型(如IntStringBoolean等)。C#里面也有这个运算符。

is运算符类似Javainstanceof:

jshell> "abc" instanceof String

$10 ==> true

Kotlin中,我们可以在运行时通过使用 is 操作符或其否定形式 !is 来检查对象是否符合给定类型:

>>> "abc" is String true

>>> "abc" !is String

false 

>>> null is Any

false

>>> null is Any?

true  

代码示例:

@RunWith(JUnit4::class)

class ISTest {

     @Test fun testIS() {

         val foo = Foo()

         val goo = Goo()

         println(foo is Foo) //true 自己

         println(goo isFoo)//子类 is 父类 = true

         println(foo isGoo)//父类 is 子类 = false

         println(goo isGoo)//true 自己

     }

 }  

open class Foo

class Goo : Foo()

3.5.2  类型自动转换

Java代码中,当我们使用str instanceofString来判断其值为true的时候,我们想使用str变量,还需要显式的强制转换类型:

@RunWith(JUnit4.class)

public class TypeSystemDemo {

    @org.junit.Test

    public void testVoid(){

        Object str ="abc";

        if (str instanceofString) {

            int len =((String)str).length();  // 显式的强制转换类型为String

            println(str +" is instanceof String");

           println("Length: " + len);

        } else {

            println(str +" is not instanceof String");

        }

 

        boolean is ="1" instanceof String;

        println(is);

    }

 

    void println(Objectobj) {

       System.out.println(obj);

    }

}

 

而大多数情况下,我们不需要在 Kotlin中使用显式转换操作符,因为编译器跟踪不可变值的 is检查,并在需要时自动插入(安全的)转换:

    @Test

    fun testIS() {

        val len =strlen("abc")

        println(len) // 3

        val lens =strlen(1)

        println(lens) // 1

    }

    fun strlen(ani: Any):Int {

        if (ani is String){

            returnani.length

        } else if (ani isNumber) {

            returnani.toString().length

        } else if (ani isChar) {

            return 1

        } else if (ani isBoolean) {

            return 1

        }

        print("Not AString")

        return -1

    }

 

3.5.3  as运算符

中缀操作符as用于执行引用类型的显式类型转换。如果要转换的类型与指定的类型兼容,转换就会成功进行;如果类型不兼容,使用as?运算符就会返回值null

代码示例:

>>> open class Foo //父类Foo

>>> class Goo:Foo() //子类Goo

>>> val foo = Foo()

>>> val goo = Goo()  

>>> foo as Goo // 父类不能转子类类型

java.lang.ClassCastException: Line69$Foo cannot be cast toLine71$Goo

 >>> foo as? Goo //使用as? 安全类型转换符,返回null

null 

>>> goo as Foo // 子类可以转型为父类类型

Line71$Goo@73dce0e6    

我们可以看出,在Kotlin中,子类是禁止转换为父类型的。

另外,如果我们对一个null对象调用as,会直接抛出空指针异常

>>> val nil = null

>>> val x: String = nil as String

kotlin.TypeCastException: null cannot be cast to non-null typekotlin.String

 

    这个时候,我们需要使用安全转换操作符as?,代码示例如下

 

>>> val x: String? = nil as? String

>>> x

null

这样转换,代码就会安静地输出null,不会抛出空指针异常。需要注意的是,当我们使用as?操作符计算表达式的值的时候,我们这里的变量x的类型也必须是String?可空类型。

 

本章小结

Kotlin通过引入可空类型,在编译时就大量清扫了空指针异常。同时,Kotlin中还引入了安全调用符(?.)以及Elvis操作符( ?: ) ,使得我们的代码写起来更加简洁。Kotlin的类型系统比Java更加简单一致,Java中的原始类型与数组类型在Kotlin中都统一表现为引用类型。Kotlin中还引入了UnitNothing等特殊类型,使得没有返回值的函数与永远不会返回的函数有了更加规范一致的签名。我们还可以使用is操作符来判断对象实例的类型,以及使用as操作符进行类型的转换。

原创粉丝点击