密封类sealed class

密封类的具体作用如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 定义一个空接口
interface Result

class Success(val msg: String) : Result

class Failure(val error: Exception) : Result


fun getResultMsg(result: Result) = when (result) {
    is Success -> result.msg
    is Failure -> result.error.message
    else -> throw IllegalArgumentException()
}

上面定义了一个Result接口,用于表示某个操作的执行结果(接口中没有任何内容)。然后定义了两个类SuccessFailure实现Result接口。 再看getResultMsg方法,它接收一个Result参数,然后我们通过when语句判断result参数。我们知道,result只有为SuccessFailure两种情况,但是在when语句最后,我们不得不添加一个else条件,否则Kotlin编译器会认为这里缺少条件分支,会编译不过。

我们在这里添加一个else条件,仅仅只是为了让通过语法检查而已。使用密封类可以很好地解决这个问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
sealed class Result

class Success(val msg: String) : Result()

class Failure(val error: Exception) : Result()


fun getResultMsg(result: Result) = when (result) {
    is Success -> result.msg
    is Failure -> result.error.message
}

使用密封类之后,当在when语句中传入一个密封类变量作为条件时,Kotlin编译器会自动检查该密封类包含哪些子类,并强制要求你将每一个子类所对应的条件全部处理。这样就可以保证没有添加else分支的情况下,也不可能会漏写条件分支的情况。

注意:

密封类以及所有子类只能定义在同一个文件的顶层位置,不能嵌套在其他类中,这是被密封类底层的实现机制所限制的。

泛型

关于泛型部分的in, out, where关键字的使用,建议先阅读

https://rengwuxian.com/kotlin-generics/

Kotlin中的泛型与Java中的泛型有同有异,我们先来看相同的部分。

Kotlin泛型与Java泛型相同的部分

泛型的定义方式有两种: 一种是定义泛型类,另一种是定义泛型方法。

定义一个泛型类,泛型类中的方法允许使用T类型作为参数和返回值。

1
2
3
4
5
6
7
8
class MyClass <T> {
	fun method(param: T): T {
		return param
	}
}

val myClass = MyClass<Int>()
val result = myClass.method(123)

只定义一个泛型方法

1
2
3
4
5
6
7
8
9
class MyClass {
	fun <T> method(param: T): T {
		return param
	}
}

val myClass = Myclass()
val result = myClass.method<Int>(123)
val result2 = myClass.method(123) //利用推导机制,可以省略掉泛型的指定

通过限定上界,可以限制泛型的类型,下面的方法将限制泛型只能是数字类型。

1
2
3
4
5
class MyClass {
	fun <T: Number> method(param: T): T {
		return param
	}
}

默认情况下,所有的泛型类型都是可以指定成可空类型的,在没有指定上界的时候,泛型的上界默认是Any?。如果想让泛型的类型不可为空,可以将泛型的上界手动指定为Any类型。

Kotlin泛型独有的功能

对泛型实化

泛型实例化这个功能对于绝大多数Java程序员来讲是非常默认的,因为Java中完全没有这个概念。而如果我们要先解释一下泛型实化,就要先解释一下Java的擦除机制。

Java的泛型功能是通过类型擦除机制来实现的。什么意思呢?就是说泛型对于类型的约束只在编译时期存在,JVM是识别不出来我们在代码中指定的泛型类型的。假如我们创建了一个List<String>集合,虽然在编译时期只能想集合里添加String类型的元素,但是在运行时期JVM并不能知道它本来只打算包含哪种类型的元素,只能识别出来它是一个List.

所有基于JVM的语言,它们的泛型功能都是通过类型擦除机制来实现的,其中当然也包括Kotlin. 这种机制使得我们不可能使用a is T或者T::class.java这样的语法,因为T的实际类型在运行的时候已经被擦除了。

然而不同的是,Kotlin提供了一个内联函数的概念。内联函数中的代码会在编译的时候自动被替换到调用它的地方,这样的话也就不存在什么泛型擦除问题了,因为代码在编译后会直接使用实际的类型来替代内联函数中的泛型声明。

所以在Kotlin中泛型实化要有两个条件:

  1. 该函数必须是内联函数才行,也就是要用inline关键字来修饰该函数。
  2. 在声明泛型的地方必须加上reified关键字来表示该泛型要进行实化。

比如:

1
inline fun <reified T> getGenericType() = T::class.java

上面的函数直接返回了指定泛型的实际类型,T.class这样的语法在Java中是不合法的,而在Kotlin中,借助泛型实化功能就可以使用T::class.java这样的语法了。

1
2
3
4
5
6
7
8
fun main() {
    println(getGenericType<String>())
    println(getGenericType<Int>())
}

// 输出
// class java.lang.String
// class java.lang.Integer

泛型实化的应用

泛型实化功能允许我们在泛型函数中获得泛型实例的实际类型,这也就使得类似a is TT::class.java这样的语法成为了可能。

看一个启动Activity的代码,一般我们是这样写的

1
2
3
4
val intent = Intnet(context, TestActivity::class.java)
intent.putExtra("param1", "data")
intent.putExtra("param2", 123)
context.startActivity(intent)

利用泛型实化和高阶函数,我可以定义一个如下函数startActivity()

1
2
3
4
5
inline fun <reified T> startActivity(Context: context, block: Intent.() -> Unit) {
    val intent = Intent(context, T::class.java)
    intent.block()
    context.startActivity(intent)
}

然后调用的时候直接使用下面的形式就可以了

1
2
3
4
startActivity<TestActivity>(context, {
    putExtra("param1", "data")
    putExtra("param2", 123)
})

泛型的协变

假如定义了一个MyClass<T>的泛型类,其中AB的子类,同时MyClass<A>又是MyClass<B>的子类,那么我们就称MyClassT这个泛型上是协变的。

泛型的逆变

假如定义了一个MyClass<T>的泛型类,其中AB的子类,同时MyClass<B>又是MyClass<A>的子类,那么我们就称MyClassT这个泛型上是逆变的。

Java 的泛型本身是不支持协变和逆变的。

  • 可以使用泛型通配符 ? extends 来使泛型支持协变,但是「只能读取不能修改」,这里的修改仅指对泛型集合添加元素,如果是 remove(int index) 以及 clear 当然是可以的。
  • 可以使用泛型通配符 ? super 来使泛型支持逆变,但是「只能修改不能读取」,这里说的不能读取是指不能按照泛型类型读取,你如果按照 Object 读出来再强转当然也是可以的。

根据前面的说法,这被称为 PECS 法则:「Producer-Extends, Consumer-Super」。

说回 Kotlin 中的 outin

和 Java 泛型一样,Kolin 中的泛型本身也是不可变的。

  • 使用关键字 out 来支持协变,等同于 Java 中的上界通配符 ? extends
  • 使用关键字 in 来支持逆变,等同于 Java 中的下界通配符 ? super
1
2
var textViews: List<out TextView>
var textViews: List<in TextView>

Kotlin换了个写法,但作用是完全一样的。out 表示,我这个变量或者参数只用来输出,不用来输入,你只能读我不能写我;in 就反过来,表示它只用来输入,不用来输出,你只能写我不能读我。

嵌套类

kotlin中,类和接口可以任意组合嵌套,比如

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Outer {
    private val bar: Int = 1
    class Nested {
        fun foo() = 2
    }
}

val demo = Outer.Nested().foo() // == 2

// 接口和类直接也可以相互嵌套
interface OuterInterface {
    class InnerClass
    interface InnerInterface
}

class OuterClass {
    class InnerClass
    interface InnerInterface
}

Inner classes

如果一个嵌套类被标记为inner,那么这个类可以访问外部类的成员(没有标记时是不能直接访问的),它包含了一个外部类的引用

1
2
3
4
5
6
7
8
class Outer {
    private val bar: Int = 1
    inner class Inner {
        fun foo() = bar
    }
}

val demo = Outer().Inner().foo() // == 1

匿名内部类(Anonymous inner classes)

使用object关键字,可以创建匿名的内部类

1
2
3
4
5
6
window.addMouseListener(object : MouseAdapter() {

    override fun mouseClicked(e: MouseEvent) { ... }

    override fun mouseEntered(e: MouseEvent) { ... }
})

枚举类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
enum class Direction {
    NORTH, SOUTH, WEST, EAST
}

//包含初始值的枚举类
enum class Color(val rgb: Int) {
        RED(0xFF0000),
        GREEN(0x00FF00),
        BLUE(0x0000FF)
}

枚举类也可以匿名类的形式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
enum class ProtocolState {
    WAITING {
        override fun signal() = TALKING
    },

    TALKING {
        override fun signal() = WAITING
    };

    abstract fun signal(): ProtocolState
}

枚举类的常用方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
EnumClass.valueOf(value: String): EnumClass
// 获取枚举类的
EnumClass.values(): Array<EnumClass>

// 通过泛型方式方法
// enumValues<T>()
enum class RGB { RED, GREEN, BLUE }

inline fun <reified T : Enum<T>> printAllValues() {
    print(enumValues<T>().joinToString { it.name })
}

每个枚举类都默认有两个属性

1
2
val ordinal: Int // 按照声明顺序在枚举类中的位置
val name: String

Inline Class

类似data class, 在编写程序时,为了一些逻辑。我们经常会定义一些类来封装数据。这也做很方便,但是也会带来一些额外的运算开销。尤其是当只有一个属性时。因此,Kotlin中引起了Inline Class,可以把它看作为data class的子类

使用value关键字声明Inline Class

1
value class Password(private val s: String)

为了兼容JVM,还需要使用注解@JvmInline

1
2
3
// For JVM backends
@JvmInline
value class Password(private val s: String)

Inline Class必须在主构造函数里必须有一个属性,在代码运行的时候,编译器会将它作为原始数据来对待,而不会额外创建一个类来处理

1
2
3
// No actual instantiation of class 'Password' happens
// At runtime 'securePassword' contains just 'String'
val securePassword = Password("Don't try this in production")

Inline Class Members

Inline Class同样跟普通类一样,支持定义属性和一些方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@JvmInline
value class Name(val s: String) {
    init {
        require(s.length > 0) { }
    }

    val length: Int
        get() = s.length

    fun greet() {
        println("Hello, $s")
    }
}

fun main() {
    val name = Name("Kotlin")
    name.greet() // method `greet` is called as a static method
    println(name.length) // property getter is called as a static method
}

Inline Class 继承

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
interface Printable {
    fun prettyPrint(): String
}

@JvmInline
value class Name(val s: String) : Printable {
    override fun prettyPrint(): String = "Let's $s!"
}

fun main() {
    val name = Name("Kotlin")
    println(name.prettyPrint()) // Still called as a static method
}

根据不情况具有不同的表现形式

Inline Class并不是一直在运行时都做被当成原始数据,而是根据使用场景,优化了是否封包解包的操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface I

@JvmInline
value class Foo(val i: Int) : I

fun asInline(f: Foo) {}
fun <T> asGeneric(x: T) {}
fun asInterface(i: I) {}
fun asNullable(i: Foo?) {}

fun <T> id(x: T): T = x

fun main() {
    val f = Foo(42)

    asInline(f)    // unboxed: used as Foo itself
    asGeneric(f)   // boxed: used as generic type T
    asInterface(f) // boxed: used as type I
    asNullable(f)  // boxed: used as Foo?, which is different from Foo

    // below, 'f' first is boxed (while being passed to 'id') and then unboxed (when returned from 'id')
    // In the end, 'c' contains unboxed representation (just '42'), as 'f'
    val c = id(f)
}

Mangling

Since inline classes are compiled to their underlying type, it may lead to various obscure errors, for example unexpected platform signature clashes:

1
2
3
4
5
6
7
8
@JvmInline
value class UInt(val x: Int)

// Represented as 'public final void compute(int x)' on the JVM
fun compute(x: Int) { }

// Also represented as 'public final void compute(int x)' on the JVM!
fun compute(x: UInt) { }

To mitigate such issues, functions using inline classes are mangled by adding some stable hashcode to the function name. Therefore, fun compute(x: UInt) will be represented as public final void compute-<hashcode>(int x), which solves the clash problem.

Calling from Java code

You can call functions that accept inline classes from Java code. To do so, you should manually disable mangling: add the @JvmName annotation before the function declaration:

1
2
3
4
5
6
7
@JvmInline
value class UInt(val x: Int)

fun compute(x: Int) { }

@JvmName("computeUInt")
fun compute(x: UInt) { }

Inline classes vs type aliases

At first sight, inline classes seem very similar to type aliases. Indeed, both seem to introduce a new type and both will be represented as the underlying type at runtime.

However, the crucial difference is that type aliases are assignment-compatible with their underlying type (and with other type aliases with the same underlying type), while inline classes are not.

In other words, inline classes introduce a truly new type, contrary to type aliases which only introduce an alternative name (alias) for an existing type:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
typealias NameTypeAlias = String

@JvmInline
value class NameInlineClass(val s: String)

fun acceptString(s: String) {}
fun acceptNameTypeAlias(n: NameTypeAlias) {}
fun acceptNameInlineClass(p: NameInlineClass) {}

fun main() {
    val nameAlias: NameTypeAlias = ""
    val nameInlineClass: NameInlineClass = NameInlineClass("")
    val string: String = ""

    acceptString(nameAlias) // OK: pass alias instead of underlying type
    acceptString(nameInlineClass) // Not OK: can't pass inline class instead of underlying type

    // And vice versa:
    acceptNameTypeAlias(string) // OK: pass underlying type instead of alias
    acceptNameInlineClass(string) // Not OK: can't pass underlying type instead of inline class
}

Object expressions and declarations

有些时候,我们可能只需要修改修改类的一点东西而不想单独创建一个类,这个时候对象表达式就派上用场了。

对象表达式创建的类都不使用class关键字,一般情况下只会使用一次。

创建一个匿名类

1
2
3
4
5
6
val helloWorld = object {
    val hello = "Hello"
    val world = "World"
    // object expressions extend Any, so `override` is required on `toString()`
    override fun toString() = "$hello $world"
}

创建一个继承自其它类的匿名类

1
2
3
4
5
window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /*...*/ }

    override fun mouseEntered(e: MouseEvent) { /*...*/ }
})

如果继承的父类有构造函数,创建时也需要传递构造函数

1
2
3
4
5
6
7
8
9
open class A(x: Int) {
    public open val y: Int = x
}

interface B { /*...*/ }

val ab: A = object : A(1), B {
    override val y = 15
}

使用匿名类作为返回值

1
2
3
4
5
6
7
8
9
class C {
    private fun getObject() = object {
        val x: String = "x"
    }

    fun printX() {
        println(getObject().x)
    }
}

从匿名内部类可以访问外部的成员

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fun countClicks(window: JComponent) {
    var clickCount = 0
    var enterCount = 0

    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++
        }

        override fun mouseEntered(e: MouseEvent) {
            enterCount++
        }
    })
    // ...
}

单例类

在Kotlin中创建一个单例类的方法特别简单,只需要将class关键字改为object关键字即可。

1
2
3
4
5
object Manager {
    fun manager() {
        println("manager is called.")
    }
}

调用单例类中的函数也很简单,比较类似Java中静态方法的调用方式:

1
Manager.manager()

上面的写法看起来很像是静态方法的调用,但其实Kotlin背后帮我们自动创建了一个Manager类的实例,并保证全局只有一个Manager实例。

伴生类

使用companion object关键字,可以创建伴生类

1
2
3
4
5
class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

伴生类里面的成员可以直接通过外部类的类名访问

1
val instance = MyClass.create()

尽管伴生类看起来像是静态类,但是在使用时仍然是一个实际存在的对象,因此它也可以实现接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
interface Factory<T> {
    fun create(): T
}

class MyClass {
    companion object : Factory<MyClass> {
        override fun create(): MyClass = MyClass()
    }
}

val f: Factory<MyClass> = MyClass

静态方法

在Kotlin中极度弱化了静态方法这个概念。像工具类这种功能,在Kotlin中非常推荐使用单例的方式来实现。比如

1
2
3
4
5
object Util {
	fun doAction() {
		println("do some action")
	}
}

虽然这里的doAction()方法并不是静态方法,但是由于Util被声明为了单例的形式,我们仍然可以使用Util.doAction()的方式来调用,非常方便。

不过使用单例类的写法会将类中所有的方法都变成类似静态方法的调用方式,而如果我们只希望让类中的一个方法变成静态方法时,就需要使用companion object了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Util {
    fun doAction1() {
        println("do action1")
    }

    companion object {
        fun doAction2() {
            println("do action2")
        }
    }
}

fun main() {
    Util().doAction1() // doAction1必须先创建对象才能调用
    Util.doAction2()  // doAction2可以像静态方法一样直接调用
}

doAction2()方法其实也不是一个静态方法,companion object这个关键字实际上会在Util类的内部创建一个伴生类,而doAction2()就是定义在这个伴生类里的实例方法。 只是Kotlin会保证在Util类始终存在一个伴生类对象,因此调用Util.doAction2()实际上就是调用了Util类中伴生对象的doAction2方法。

由此可以看出,Kotlin确实没有直接定义静态方法的关键字,但是提供了一些语法特性来支持类似静态方法调用的写法。

如果确实需要定义真正的静态方法,Kotlin提供了两种实现方式:

  • 注解
  • 顶层方法

通过注解实现静态方法

先来看注解,前面使用单例类和companion object都只是在语法上模仿了静态方法的调用方式,实际上他们都不是真正的静态方法。因此如果你在Java中以静态方法的形式调用上面的方法的话,会发现这些方法根本不存在。而如果我们给单例类或者companion object中的方法加上@JvmStatic注解,那么Kotlin编译器就会将这些方法编译成真正的静态方法。如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Util {
    fun doAction1() {
        println("do action1")
    }

    companion object {
        @JvmStatic
        fun doAction2() {
            println("do action2")
        }
    }
}

注意: @JvmStatic只能加在单例类或者companion object中的方法上,如果你尝试加在一个普通方法上(如上面的doAction1()方法),会直接报错。

按照上面的方法添加注解之后,doAction2()方法就变成了真正的静态方法,不管是在Kotlin还是Java中,都可以使用Util.doAction2()的写法来调用。

通过顶层方法实现静态方法

顶层方法是指那些没有定义在任何类中的方法。Kotlin编译器会将所有的顶层方法全部编译成静态方法。因此只要你定义了一个顶层方法,那它一定是静态方法。

如在Helper.kt文件中定义一个方法

1
2
3
fun doSomething() {
	println("do something")
}

在Kotlin中调用顶层方法比较简单,直接使用方法名就可以调用了,但是在Java中,没有顶层方法这个概念,所有的方法都必须定义在类中,那么在Java中该如何调用这个方法呢?

由于我们刚刚创建的文件名是Helper.kt,于是Kotlin编译器会自动创建一个叫做HelperKt的Java类,doSomething就是以静态方法的形式定义在HelperKt类中的。 因此在Java中只需要通过HelperKt.doSomething()的写法来调用。