深入构造器
构造器用于在创建实例时执行初始化。构造器是创建对象的重要途径(即使使用工厂模式、反射等方式创建对象,其实质也依然是依赖于构造器),因此 Kotlin类必须包含一个或一个以上的构造器。
主构造器和初始化块
前面己经提到, Kotlin 类可以定义0~1个主构造器和0~N 个次构造器。如果主构造器没有任何注解或可见性修饰符,则可以省略 constructor关键字。
主构造器作为类头的一部分,可以声明形参,但它自己并没有执行体。那么主构造器的形参有什么用呢 ?其作用主要有两点。
- 初始化块可以使用主构造器定义的形参。
- 在声明属性时可以使用主构造器定义的形参 。
由此可见, Kotlin 的主构造器并不是传统意义上的构造器,它更像 Java 的初始化块,或者说是对初始化块的增强,Java的初始化块不能传入参数: Kotlin通过主构造器的设计,允许为初始化块传入参数。
初始化块的语法格式如下:
init {
//初始化块中的可执行代码,可以使用主构造器定义的参数
}
初始化块中的代码可以使用主构造器定义的参数,也可以包含任何可执行语句,包括定义局部变量、调用其他对象的方法,以及使用分支、循环语句等。如下所示:
class Person(name:String){
//下面定义一个初始化块
init {
var a =6
if(a>4){
println ("Person初始化块:局部变量a的值大于4")
}
println("Person的初始化块")
println("name参数为:${ name}")
}
//定义第二个初始化块
init {
println("Person 的第二个初始化块")
}
}
fun main(args: Array<String>) {
Person("sy")
//输出:Person初始化块:局部变量a的值大于4
//Person的初始化块
//name参数为:sy
//Person 的第二个初始化块
}
从上面可以看出来当程序通过主构造器创建对象时,系统其实就是调用该类里定义的初始化块,如果一个类里定义了两个普通初始化块,则前面定义的初始化块先执行,后面定义的初始化块后执行。
Kotlin中允许一个类里定义两个初始化块,但这没有任何意义。因为初始化块是在创建对象时隐式执行的,而且它们总是全部执行,因此完全可以把多个普通初始化块合并成一个初始化块,从而让程序更加简洁,可读性更强。
从上面结果不难看出,程序调用主构造器创建对象,实际上就是执行初始化块。由此可见,主构造器的主要作用就是为初始化块定义参数,因此主构造器更像是初始化块的一部分。也可以说,初始化块就是主构造器的执行体 。
构造器最大的用处就是在创建对象时执行初始化,但由于初始化块就是主构造器的执行体, 因此,如果希望为对象的属性显式指定初始值,则也可以通过初始化块来指定。如果程序员没有为 Kotlin类提供任何构造器(主构造器和次构造器),则系统会为这个类提供一个无参数的主构造器,这个构造器的执行体为空,不做任何事情。
//提供主构造器,该构造器包含两个参数
class ConstructorTest(name:String,count:Int){
var name:String
var count:Int
//定义初始化块,它相当于主构造器的执行体
init {
//初始化块中的 this 代表其进行初始化的对象
//下面两行代码将传入的两个参数赋给 this 所代表的对象的 name 和 count 属性
this.name = name
this.count = count
}
}
fun main(args: Array<String>) {
//使用向定义的构造器来创建对象
//系统将会对该对象执行自定义的初始化
var tc =ConstructorTest("cc",28)
//输出ConstructorTest 对象的 name属性
println(tc.name)
}
次构造器和构造器重载
Kotlin 允许使用 constructor 关键字定义 N 个次构造器,而且Kotlin的主构造器其实属于初始化块(或者说,初始化块其实是主构造器的执行体),因此 Kotlin 要求所有的次构造器必须委托调用主构造器,可以直接委托或通过别的次构造器间接委托。所谓“委托”,其实就是要先调用主构造器(执行初始化块中的代码),然后才执行次构造器代码。
如果两个构造器中有相同的初始化代码,就可以把它们放在初始化块中定义。如果这些初始化代码需要参数,则可将参数放在主构造器中定义 。通过把多个构造器中的相同代码提取到初始块中定义,能更好地提高初始化代码的复用性,提高整个应用的可维护性。
同一个类里具有多个构造器 , 多个构造器的形参列表不同,即被称为构造器重载。程序可通过不同的构造器来创建对象,但不管使用哪个构造器,首先都要先调用主构造器(执行初始化块代码)。
下面程序示范了构造器重载,利用构造器重载就可以通过不同的构造器来创建对象。
class ConstructorOverload {
var name: String?
var count: Int
init {
println("初始化块!")
}
//提供无参数的构造器
constructor() {
name = null
count = 0
}
constructor(name:String,count:Int){
this.name = name
this.count = count
}
}
fun main(args: Array<String>) {
//通过无参数的构造器创建 ConstructorOverload 对象
var oc1 = ConstructorOverload()
//通过有参数的构造器创建 ConstructorOverload 对象
var oc2 = ConstructorOverload("xq",111)
//输出初始化块!
//初始化块!
//xq 111
println(oc2.name + " " + oc2.count)
}
从上面的运行结果可以看出,不管调用哪个构造器创建对象,系统总会先执行初始化块,也就是说,初始化块总会在所有次构造器之前执行。用Kotlin的专业术语来说,叫作 : 所有的次构造器都要委托调用初始化块。
上面的 ConstrctorOverload没有定义主构造器,因此次构造器不需要委托主构造器。下面再定义一个带主构造器的类 。
class User1(name: String) {
var name: String
var age: Int
var info: String? = null
init {
println("User 的初始化块”")
this.name = name
this.age = 0
}
//委托给主构造器
constructor(name: String, age: Int) : this(name)
//委托给( String, Int)构造器
constructor(name: String, age: Int, info: String) : this(name, age) {
this.info = info
}
}
fun main(args: Array<String>) {
//调用主构造器
var user1 = User1("xq")
println("${user1.name} => ${user1.age} => ${user1.info}")
//调用( String, Int)构造器
var user2 = User1("cq",21)
println("${user2.name} => ${user2.age} => ${user2.info}")
//调用( String, Int, String)构造器
var user3 = User1("xy",20,"tsss")
println("${user3.name} => ${user3.age} => ${user3.info}")
}
从上面代码可以看出, Kotiin 使用“:this(参数)”的语法委托另 一个构造器,到底委托哪个构造器则取决于传入的参数。系统会根据传入的参数来推断委托了哪个构造器。
主构造器声明属性
Kotlin允许在主构造器上声明属性,直接在参数之前使用var或val即可声明属性,使用 var声明的是读写属性,使用 val声明的是只读属性。当程序调用这种方式声明的主构造器创建对象时,传给该构造器的参数将会赋值给对象的属性。
例如如下程序:
class Item2(var name:String,val num:Int){
}
fun main(args: Array<String>) {
var item2 = Item2("xq",1)
println(item2.name + " " + item2.num)
}
如果主构造器的所有参数都有默认值,程序能以构造参数的默认值来调用该构造器(即不需要为构造参数传入值),此时看上去就像调用无参数的构造器。
class Item2(var name:String= "xq",val num:Int = 0){
}
fun main(args: Array<String>) {
var item = Item2()
println(item.name + " " + item.num)
var item2 = Item2("xqs",2)
println(item2.name + " " + item2.num)
}
类的继承
继承是面向对象的三大特征之一,也是实现软件复用的重要手段。 Kotlin的继承同样是单继承:每个子类最多只有一个直接父类。
继承的语法
Kotiin 的子类继承父类的语法格式如下:
修饰符 class SubClass : Superclass{
//类定义部分
}
从上面的语法格式来看,定义子类的语法非常简单 ,只需在原来的类定义中添加SuperClass,即可表明该子类继承了 SuperClass类。
从子类的角度来看, 子类扩展了父类; 但从父类的角度来看,父类派生出子类。也就是说,扩展和派生所描述的是同一个动作,只是观察的角度不同而己。 如果在定义一个 Kotlin类时并未显式指定这个类的直接父类,则这个类默认扩展 Any类 。因此Any类是所有类的父类,要么是其直接父类,要么是其间接父类 。需要说明的是 ,Any类不是 java.lang.Object类 ,Any类只有 equals()、hashCode()和 toString()。这 3个方法。
还有一点需要说明的是, Kotlin的类默认就有 final修饰,因此Kotlin的类默认是不能派生子类的。 为了让一个类能派生子类,需要使用open修饰该类。
open 就是 final 的反义词,用于取消 Kotlin 自动添加的 final修饰符 。
下面我们尝试定义如下父子类 。
open class BaseClass {
}
class SubClass : BaseClass {
}
如果尝试编译上面两个类 , 就会发现程序无法通过编译,这是为什么呢?
java子类构造器总要调用父类构造器一次,子类构造器调用父类构造器分如下几种情况:
- 子类构造器执行体的第一行使用 super(参数)显式调用父类构造器,系统将根据 super(参数)调用中传入的实参列表调用父类对应的构造器 。
- 子类构造器执行体的第一行代码使用 this(参数)显式调用本类中重载的构造器,系统将根据 this(参数)调用中传入的实参列表调用本类中的另一个构造器。 调用本类中的另一个构造器最终还是要调用父类构造器。
- 子类构造器执行体中既没有super(参数)调用,也没有 this(参数)调用,系统将会在执行子类构造器之前 , 隐式调父类无参数的构造器 。
Kotlin的构造器同样要遵守这个规则,只不过 Kotlin为之换了个新说法 :委托父类构造器 。而且由于 Kotlin 的构造器分为主构造器和次构造器,因此情况略微复杂一些。 下面分主构造器和次构造器进行详细说明 。
1. 子类的主构造器
如果子类定义了主构造器,由于主构造器属于类头部分(如果不定义初始化块,它就没有执行体),为了让主构造器能调用父类构造器,因此主构造器必须在继承父类的同时委托调用父类构造器 。 例如如下代码 :
open class BaseClass {
var name:String
constructor(name:String){
this.name = name
}
}
//子类没有显式声明主构造器
//子类默认有一个主构造器,因此要在声明继承时委托调用父类构造器
class SubClass1 : BaseClass("foo1") {
}
//子类显式声明主构造器
//主构造楼必须在声明继承时委托调用父类构造器
class SubClass2(name: String):BaseClass(name){
}
其中 SubClass1没有显式声明主构造器,系统会为该类自动生成一个无参数的主构造器,因此程序在继承 BaseClass时必须立即调用父类构造器
为 SubClass2 显式定义了一个带参数的主构造器,因此程序同样需要在继承 BaseClass时必须立即调用父类构造器。
2. 子类的次构造器
次构造器同样需要委托调用父类构造器。
如果子类定义了主构造器,由于子类的次构造器总会委托调用子类的主构造器(直接或间接),而主构造器一定会委托调用父类构造器,因此子类的所有次构造器最终也调用了父类构造器。
如果子类没有定义主构造器,则此时次构造器委托调用父类构造器可分为 3 种方式。
- 子类构造器显式使用:this(参数)显式调用本类中重载的构造器,系统将根据 this(参数)调用中传入的实参列表调用本类中的另一个构造器。调用本类中的另一个构造器最终还是要调用父类构造器。
- 子类构造器显式使用:super(参数)委托调用父类构造器,系统将根据 super(参数)调用中传入的实参列表调用父类对应的构造器。
- 子类构造器既没有super(参数)调用,也没有this(参数)调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。
这段描述和Java 子类构造器调用父类构造器的述相似吗?相似就对了。正如前面所介绍的, Kotiin 的次构造器相当于 Java 的构造器,因此 Kotlin 的次构造器委托调用父类构造器的 3 种方式,正好对应于 Java 构造器调用父类构造器的 3 种方式。它们唯一的区别只是写法不同 : Java 将调用父类构造器写在方法体内, Kotlin 将委托父类构造器写在构造器声明中。
如下代码示范了没有主构造器的子类的次构造器是如何调用父类构造器的
open class BaseClass {
constructor() {
println("Base 的无参数的构造器")
}
constructor(name: String) {
println("Base 的带一个 String参数:${name}的构造器")
}
}
class Sub : BaseClass {
//构造器没有显式委托
//因此该次构造器将会隐式委托调用父类无参数的构造器
constructor() {
println("Sub 的无参数的构造器")
}
//构造器用 super(name)显式委托父类带 String 参数的构造器
constructor(name: String) : super(name) {
println("Sub 的 String 构造器, String 参数为:${name}")
}
//构造器用 this(name)显式委托本类中带 String参数的构造器
constructor(name: String, age: Int) : this(name) {
println("Sub 的 String Int构造器, Int 参数为:${age}")
}
}
fun main(args: Array<String>) {
Sub()
Sub("Sub")
Sub("子类",12)
}
上面的 Sub 类没有定义主构造器,类体中 3 行粗体字代码定义了3个次构造器 。
- 第一个次构造器既没有 super(参数)委托 ,也没有(this)委托,因此该构造器会隐式委托调用父类无参数的构造器 。
- 第二个次构造器使用:super(name)委托调用, 其中 name是一个 String类型的参数,因此该构造器属于显式委托调用父类带一个 String参数的构造器。
- 第三个次构造器使用:this(name)委托调用,其中 name是一个 String类型的参数,因此该构造器属于显式委托调用该类中带一个 String参数的构造器,即调用前一个构造器:而前一个构造器委托调用了父类构造器, 因此该次构造器最终也调用了父类构造器一次。
当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行;不仅如此,在执行父类构造器时,系统会再次上溯执行其父类构造器 ...... 依此类推,创建任何 Kotlin对象,最先执行的总是 Any 类的构造器 。
重写父类方法
子类继承父类,将可以获得父类的全部属性和方法 。
open class Fruit(var weight: Double) {
fun info() {
println("我是一个水果,重${weight}克")
}
}
class Apple : Fruit(0.0)
fun main(args: Array<String>) {
//创建Apple对象
var apple = Apple()
// Apple 对象本身没有 weight 属性
//因为 Apple 的父类有 weight 属性,所以也可以访问 Apple 对象的 weight 属性
apple.weight = 5.6
//调用 Apple 对象的 info ()方法
apple.info()
}
上面的 Apple类只是一个空类,但程序中创建了Apple对象之后,可以访问该 Apple对象的weight属性和info()方法,这表明 Apple对象也具有了weight属性和info()方法,这就是继承的作用。
子类继承了父类,子类是一个特殊的父类。大部分时候子类总是以父类为基础额外增加新的属性和方法。但有一种情况例外 : 子类需要重写父类的方法。例如鸟类都包含了飞翔方法,其中驼鸟是一种特殊的鸟类,因此驼鸟应该是鸟的子类, 它也将从鸟类获得飞翔方法, 但这个飞翔方法明显不适合驼鸟,为此 ,驼鸟需要重写鸟类的方法。
open class Bird {
open fun fly() {
println("我在天空里自由自在地飞翔...")
}
}
class Ostrich:Bird(){
override fun fly() {
println("我只能在地上奔跑...")
}
}
fun main(args: Array<String>) {
var os =Ostrich()
//执行Ostrich对象的 fly()方法,将输出”我只能在地上奔跑...”
os.fly()
}
留意上面的 fly()方法,该方法同样使用了 open 修饰符,Kotlin 默认为所有方法添加 final修饰符,阻止该方法被重写,添加 open 关键字用于阻止 Kotlin 自动添加 final 修饰符。
kotlin类重写父类的方法必须添加 override修饰符, 就像 Java 的@Override 注解,只不过 Java 的@Override是可选的 , 而 Kotlin的override 修饰符是强制的。
这种子类包含与父类同名方法的现象被称为方法重写( Override),也被称为方法覆盖 。可以说子类重写了父类的方法,也可以说子类覆盖了父类的方法 。 方法的重写要遵循“两同两小一大”规则,“两同”即方法名相同、形参列表相同;“两小”指的是子类方法的返回值类型应比父类方法的返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;“一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。
当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但可以在子类方法中调用父类中被覆盖的方法。如果需要在子类方法中调用父类中被覆盖的方法,则可以使用 super作为调用者来调用父类中被覆盖的方法 。
如果父类方法具有 private 访问权限,则该方法对其子类是隐藏的,因此其子类无法访问该方法,也就是无法重写该方法。如果子类中定义了一个与父类 private 方法具有相同的方法名、相同的形参列表、相同的返回值类型的方法,那么这不是重写,只是在子类中重新定义了 一个新方法 。
重写父类的属性
重写父类的属性与重写父类的方法大致相似:父类被重写的属性必须使用 open 修饰,子类重写的属性必须使用 override修饰。此外,属性重写还有如下两个限制:
- 重写的子类属性的类型与父类属性的类型要兼容。
- 重写的子类属性要提供更大的访问权限 。此处包含两方面的含义 : 1.在访问权限方面, 子类属性的访问权限应比父类属性的访问权限更大或相等。2.只读属性可被读写属性重写,但读写属性不能被只读属性重写。
open class Item {
open protected var price: Double = 5.9
open val name: String = ""
open var validDays: Int = 0
}
class Book : Item {
//正确重写了父类属性,类型兼容,访问权限更大
override public var price: Double
//正确重写了父类属性,读写属性重写只读属性
override var name = "图书"
//重写错误 ,只读属性不能重写读写属性
//override val validDays :Int =2
constructor() {
price = 3.1
}
}
super 限定
如果需要在子类方法中调用父类中被覆盖的法或属性,则可使用 super限定。例如,为上面的 Ostrich 类添加一个方法,在这个方法中调用 Bird类中被覆盖的的()方法。
fun callOverridedMethod() {
//在子类方法中通过 super 显式调用父类中被覆盖 的方法
super.fly()
}
借助 callOverrided.Method()方法,就可以让 Ostrich对象既可以调用自己重写的 fly()方法, 也可以调用 Bird类中被覆盖的 fly()方法(调用 callOverridedMethod()方法即可)。
super是 Kotlin提供的一个关键字,用于限定该对象调用它从父类继承得到的属性或方法。
如果子类重写了父类的属性,那么子类中定义的方法直接访问该属性默认会访问到子类中定义的属性,无法访问到父类中被重写的属性。在子类定义的方法中可以通过 super来访问父类中被重写的属性。
open class BaseClass1 {
open var a: Int = 5
}
class Subclass : BaseClass1() {
override var a: Int = 7
fun accessOwner() {
println(a)
}
fun accessBase() {
//通过 super 限定访问从父类继承得到的 a 属性
println(super.a)
}
}
强制重写
如果子类从多个直接超类型(接口或类)继承了同名的成员,那么 Kotlin 要求子类必须重写该成员。如果需要在子类中使用super来引用超类型中的成员,则可使用尖括号超类型名限定的 super进行引用,如 super<Bar>。
open class Foo{
open fun test(){
println("Foo的test()")
}
fun foo(){
println("foo")
}
}
interface Bar{
//接口中成员默认是 open 的
fun test(){
println("Bar的test")
}
fun bar(){
println("bar")
}
}
class Wow:Foo(),Bar{
//编译器要求必须重写 test()
override fun test() {
//调用父接口 Bar 的 test()
super<Bar>.test()
//调用父类 Foo的 test()
super<Foo>.test()
}
}
fun main(args: Array<String>) {
var w = Wow()
w.test()
}
上面程序在 Foo类中定义了一个可被重写的 test()方法,在 Bar接口中也定义了一个可被重写的 test()方法(接口中的方法默认有 open修饰符)。
当子类 Wow 同时继承 Foo、 Bar时, 它会获得两个直接父类中定义的 test()方法,此时编译器要求子类 Wow 必须重写 test()方法,如上面程序的 Wow 类中的 test()方法所示。不仅如此, 如果程序希望在子类中调用父类 Foo 的 test()方法,则可通过 super<Foo>.test()进行调用:如果希望在子类中调用父接口 Bar 的 test()方法,则可通过 super<Bar>.test()进行调用。
多态
与Java类似, kotlin的变量也有两个类型: 一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态( Polymorphism)。
多态性
open class BaseClass2 {
open var book = 6
fun base() {
println("父类的普通方法 ")
}
open fun test() {
println("父类的被覆盖的方法")
}
}
class SubClass : BaseClass2() {
//重写父类的属性
override var book = 60
override fun test() {
println("子类的覆盖父类的方法")
}
fun sub() {
println("”子类的普通方法")
}
}
fun main(args: Array<String>) {
//下面编译时类型和运行时类型完全一样,因此不存在多态
var bc:BaseClass2 = BaseClass2()
//输出 6
println(bc.book)
//下面两次调用将执行 BaseClass 的方法
bc.test()
bc.base()
//下面编译时类型和运行时类型完全一样,因此不存在多态
var sub:SubClass = SubClass()
//输出 60
println(sub.book)
//下面调用将执行从父类继承的 base ()方法
sub.base()
//下面调用将执行当前类的 test ()方法
sub.test()
//下面编译时类型和运行时类型不一样,多态发生
var ploymophicBc:BaseClass2 = SubClass()
//输出 60 一一表明访问的依然是子类对象的属性
println(ploymophicBc.book)
//下面调用将执行从父类继承的 base ()方法
ploymophicBc.base()
//下面调用将执行当前类的 test ()方法
ploymophicBc.test()
//因为 ploymophicBc 的编译时类型是 BaseClass
//BaseClass 类没有提供 sub ()方法,所以下面代码编译时会出现错误
//ploymophicBc.sub()
}
上面程序的 main()函数中显式创建了3个变量,对于前两个变量 bc 和sub,它们的编译时类型和运行时类型完全相同,因此调用它们的属性和方法没有任何问题。但第三个变量 ploymophicBc则比较特殊,它的编译时类型是 BaseClass,而运行时类型是 SubClass, 当调用该变量的 test()方法(BaseClass类中定义了该方法,子类 Subclass覆盖了父类的该方法) 时,实际执行的是 SubClass类中覆盖后的 test()方法,这就可能出现多态。
因为子类其实是一种特殊的父类,因此 Kotlin 允许把一个子类对象直接赋给一个父类变量,无须任何类型转换,或者被称为向上转型( upcasting),向上转型由系统自动完成。
当把一个子类对象直接赋给父类变量时,例如上面的 var ploymophicBc: BaseCalss2 = new Subclass(),这个 ploymophicBc 变量的编译时类型是 BaseClass,而运行时类型是 Subclass,当运行时调用该变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量,调用同一个方法时呈现出多种不同子类的行为特征,这就是多态。
上面的 main()函数中注释掉了 ploymophicBc.sub(),这行代码会在编译时引发错误。虽然ploymophicBc 引用的对象实际上确实包含 sub()方法(例如,可以通过反射来执行该方法),但
因为它的编译时类型为 BaseClass, 因此编译时无法调用 sub()方法。 与方法类似的是,对象的属性同样具有多态性。比如上面通过 ploymophicBc输出它的 book属性变量时,依然是输出 Subclass类中定义的属性,而不是输出 BaseClass父类中定义的属性。
使用 is检查类型
变量只能调用其编译时类型的方法,而不能调用其运行时类型的方法,即使它实际所引用的对象确实包含该方法。如果要让这个变量调用其运行时类型的方法,就需要把它强制转换成运行时类型,强制转换需要借助于强制转型运算符。 Kotlin的类型转换运算符包含 as和 as?两个。
由于向上转型可由 Kotlin 自动转换,因此强制转型通常总是向下转型。为了保证类型转换不会出错,Kotlin 提供了类型检查运算符: is 和!is。
is 运算符的前一个操作数通常是一个变量,后一个操作数通常是一个类(也可以是接口,可以把接口理解成一种特殊的类),它用于判断前面的变量是否引用后面的类,或者其子类、 实现类的实例。如果是,则返回 true,否则返回 false。
此外, Kotlin的 is和!is都非常智能,当程序使用 is或!is判断之后,只要 Kotlin能推断出变量属于某种类型, Kotlin就 会自动将该变量的编译时类型转换为指定类型。
import java.util.*
fun main(args: Array<String>) {
//声明 hello 时使用 Any 类,则 hello 的编译时类型是 Any
//hello变量的实际类型是 String
var hello :Any = "Hello"
println("字符串是否是String类的实例:${hello is String}")
//Date与Any类存在继承关系, 可以进行is运算
println("”字符串是否是 Date 类的实例 :${hello is Date}")
//由于 hello 的编译时类型是 Any,因此编译时它没有 length 属性
//println(hello.length)
//先判断 hello 为 String, 在 if 条件体中 hello 被自动转换为 String 类型
if(hello is String){
println(hello.length)
}
}
使用 as 运算符转型
除使用 is 自动转型之外, Kotlin 也支持使用 as 运算符进行强制转型 。 Kotlin 提供了两个向下转型运算符。
as:不安全的强制转型运算符,如果转型失败,程序将会引发 ClassCastException 异常
as?: 安全的强制转型运算符,如果转型失败,程序不会引发异常,而是返回 null。
强制向下转型只能在具有继承关系的两种类型之间进行,如果是两种没有任何继承关系的类型 ,则无法进行类型转换,编译时就会发出警告:转换不可能成功。如果试图把一个父类变量转换成子类类型,则该变量实际引用的实例必须是子类实例才行(即编译时类型为父类类型, 而运行时类是子类类型),否则就会转换失败。
fun main(args: Array<String>) {
val obj: Any = "Hello"
//obj 变量的编译时类理为 Any, Any 与 String 存在继承关系,可以进行转换
//而且 obj 实际引用的实例是 String 类型,所以运行时也可通过
val objString: String = obj as String
println(objString)
//定义一个 objPri 变量,编译时类型为 Any,实际类型为 Int
val objPri: Any = 5
//objPri 变量的编译时类型为 Any, objPri的运行时类型为 Int
//Any 与 String 存在继承关系,可以进行转换,编译通过
//但 objPri 变量实际引用的实例是 Int 类型 , 所以转换失败
val str: String = objPri as String //转换失败,引发 ClassCastException
}