Kotlin学习之类与对象2

泛型

与 Java 类似,Kotlin 中的类也可以有类型参数:

class Box(t: T) {
    var value = t
}

一般来说,要创建这样类的实例,我们需要提供类型参数:

val box: Box = Box(1)

但是如果类型参数可以推断出来,例如从构造函数的参数或者从其他途径,允许省略类型参数:

val box = Box(1) // 1 具有类型 Int,所以编译器知道我们说的是 Box

型变

Java 类型系统中最棘手的部分之一是通配符类型(参⻅ Java Generics FAQ)。 而 Kotlin 中没有。 相反,
它有两个其他的东西:声明处型变(declaration-site variance)与类型投影(type projections)。

variance [ˈveəriəns] n. 变异;变化;不一致;分歧;[数] 方差
首先,让我们思考为什么 Java 需要那些神秘的通配符。在 《Effective Java》第三版 解释了该问题⸺第31 条:利用有限制通配符来提升 API 的灵活性 。 首先,Java 中的泛型是不型变的,这意味着List 并不是 List 的子类型。 为什么这样? 如果 List 不是不型变的,它就没比Java 的数组好到哪去,因为如下代码会通过编译然后导致运行时异常:

// Java
List strs = new ArrayList();
List objs = strs; // !!!即将来临的问题的原因就在这里。Java 禁止这样!
objs.add(1); // 这里我们把一个整数放入一个字符串列表
String s = strs.get(0); // !!! ClassCastException:无法将整数转换为字符串
 
 

因此,Java 禁止这样的事情以保证运行时的安全。但这样会有一些影响。例如,考虑 Collection 接口
中的 addAll() 方法。该方法的签名应该是什么?直觉上,我们会这样:

// Java
interface Collection ...... {
    void addAll(Collection items);
}

但随后,我们就无法做到以下简单的事情(这是完全安全):

// Java
void copyAll(Collection to, Collection from) {
        to.addAll(from);
        // !!!对于这种简单声明的 addAll 将不能编译:
        // Collection 不是 Collection 的子类型
}
 
 

(在 Java 中,我们艰难地学到了这个教训,参⻅《Effective Java》第三版,第 28 条:列表优先于数组)
这就是为什么 addAll() 的实际签名是以下这样:

// Java
interface Collection ...... {
      void addAll(Collection items);
}

通配符类型参数 ? extends E 表示此方法接受 E 或者 E 的 一些子类型对象的集合,而不只是 E 自身。 这意味着我们可以安全地从其中(该集合中的元素是 E 的子类的实例) 读取 E ,但不能写入, 因为我们不知道什么对象符合那个未知的 E 的子类型。 反过来,该限制可以让 Collection 表示为Collection 的子类型。 简而言之,带 extends 限定(上界)的通配符类型使得类型是协变的(covariant) 。
理解为什么这个技巧能够工作的关键相当简单:如果只能从集合中获取项目,那么使用 String 的集合, 并且从其中读取 Object 也没问题 。反过来,如果只能向集合中 放入 项目,就可以用 Object 集合并向其中放入 String :在 Java 中有 List 是 List 的一个超类
后者称为逆变性(contravariance) ,并且对于 List 你只能调用接受 String作为参数的方法 (例如,你可以调用 add(String) 或者 set(int, String) ),当然如果调用函数返回 List 中的 T ,你得到的并非一个 String 而是一个 Object.
Joshua Bloch 称那些你只能从中读取的对象为生产者,并称那些你只能写入的对象为消费者。他建
议:“为了灵活性最大化,在表示生产者或消费者的输入参数上使用通配符类型”,并提出了以下助记符:

PECS 代表生产者-Extens,消费者-Super(Producer-Extends, Consumer-Super)。
注意:如果你使用一个生产者对象,如 List ,在该对象上不允许调用 add() 或
set() 。
但这并不意味着该对象是不可变的:例如,没有什么阻止你调用 clear() 从列表中删除所有项目,因为 clear() 根本无需任何参数。通配符(或其他类型的型变)保证的唯一的事情是类型安全。不可变性完全是另一回事。

声明处型变

假设有一个泛型接口 Source ,该接口中不存在任何以 T 作为参数的方法,只是方法返回 T 类型值:

// Java
interface Source {
      T nextT();
}

那么,在 Source 类型的变量中存储 Source 实例的引用是极为安全的⸺没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:

// Java
void demo(Source strs) {
     Source objects = strs; // !!!在 Java 中不允许
     // ......
}
 
 

为了修正这一点,我们必须声明对象的类型为 Source ,这是毫无意义的,因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。但编译器并不知道。
在 Kotlin 中,有一种方法向编译器解释这种情况。这称为声明处型变:我们可以标注 Source 的类型参数 T 来确保它仅从 Source 成员中返回 (生产),并从不被消费。 为此,我们提供 out 修饰符:

interface Source {
    fun nextT(): T
}
fun demo(strs: Source) {
    val objects: Source = strs // 这个没问题,因为 T 是一个 out-参数
// ......
}

一般原则是:当一个类 C 的类型参数 T 被声明为 out 时,它就只能出现在 C 的成员的输出-位置,但回报是 C 可以安全地作为 C 的超类。
简而言之,他们说类 C 是在参数 T 上是协变的,或者说 T 是一个协变的类型参数。 你可以认为 C 是
T 的生产者,而不是 T 的消费者。
out修饰符称为型变注解,并且由于它在类型参数声明处提供,所以我们称之为声明处型变。 这与 Java
的使用处型变相反,其类型用途通配符使得类型协变。
另外除了 out,Kotlin 又补充了一个型变注释: in。它使得一个类型参数逆变:只可以被消费而不可以被
生产。逆变类型的一个很好的例子是 Comparable :

interface Comparable {
    operator fun compareTo(other: T): Int
}
fun demo(x: Comparable) {
    x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
    // 因此,我们可以将 e  的变量
    val y: Comparable = x // OK!
}

我们相信 in 和 out 两词是自解释的(因为它们已经在 C# 中成功使用很⻓时间了), 因此上面提到的助记符不是真正需要的,并且可以将其改写为更高的目标:
存在性(The Existential) 转换:消费者 in, 生产者 out! :-)

类型投影

使用处型变:类型投影

将类型参数 T 声明为 out 非常方便,并且能避免使用处子类型化的麻烦,但是有些类实际上不能限制为只返回 T ! 一个很好的例子是 Array:

class Array(val size: Int) {
      fun get(index: Int): T { ...... }
      fun set(index: Int, value: T) { ...... }
}

该类在 T 上既不能是协变的也不能是逆变的。这造成了一些不灵活性。考虑下述函数:

fun copy(from: Array, to: Array) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

这个函数应该将项目从一个数组复制到另一个数组。让我们尝试在实践中应用它:

val ints: Array = arrayOf(1, 2, 3)
val any = Array(3) { "" }
copy(ints, any)
//^ 其类型为 Array 但此处期望 Array

这里我们遇到同样熟悉的问题: Array 在 T 上是不型变的,因此 Array 和 Array 都不是另一个的子类型。为什么? 再次重复,因为 copy 可能做坏事,也就是说,例如它可能尝试写一个 String 到 from , 并且如果我们实际上传递一个 Int 的数组,一段时间后将会抛出一个ClassCastException 异常。
那么,我们唯一要确保的是 copy() 不会做任何坏事。我们想阻止它写到 from ,我们可以:

fun copy(from: Array, to: Array) { ...... }

这里发生的事情称为类型投影:我们说 from 不仅仅是一个数组,而是一个受限制的(投影的)数组:我们只可以调用返回类型为类型参数 T 的方法,如上,这意味着我们只能调用 get() 。这就是我们的使用处型变的用法,并且是对应于 Java 的 Array 、 但使用更简单些的方式。
你也可以使用 in 投影一个类型:

fun fill(dest: Array, value: String) { ...... }

Array 对应于 Java 的 Array ,也就是说,你可以传递一个CharSequence 数组或一个 Object 数组给 fill() 函数。

星投影

有时你想说,你对类型参数一无所知,但仍然希望以安全的方式使用它。 这里的安全方式是定义泛型类
型的这种投影,该泛型类型的每个具体实例化将是该投影的子类型。
Kotlin 为此提供了所谓的星投影语法:

  • 对于 Foo ,其中 T 是一个具有上界 TUpper 的协变类型参数,Foo <> 等价于 Foo 。 这意味着当 T 未知时,你可以安全地从 Foo <> 读取 TUpper的值。
  • 对于 Foo ,其中 T 是一个逆变类型参数,Foo <> 等价于 Foo 。 这意味着当 T 未知时,没有什么可以以安全的方式写入 Foo <> 。
  • 对于 Foo ,其中 T 是一个具有上界 TUpper 的不型变类型参数,Foo<*> 对于读取值时等价于 Foo 而对于写值时等价于 Foo
    如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。 例如,如果类型被声明为interface Function ,我们可以想象以下星投影:
  • Function<*, String> 表示 Function ;
  • Function 表示 Function ;
  • Function<*, *> 表示 Function
    注意:星投影非常像 Java 的原始类型,但是安全。

泛型函数

不仅类可以有类型参数。函数也可以有。类型参数要放在函数名称之前:

fun  singletonList(item: T): List {
    // ......
}
fun  T.basicToString(): String {  // 扩展函数
    // ......
}

要调用泛型函数,在调用处函数名之后指定类型参数即可:

val l = singletonList(1)

可以省略能够从上下文中推断出来的类型参数,所以以下示例同样适用:

val l = singletonList(1)

泛型约束

能够替换给定类型参数的所有可能类型的集合可以由泛型约束限制。

上界

最常⻅的约束类型是与 Java 的 extends 关键字对应的 上界:

fun > sort(list: List) {...... }

冒号之后指定的类型是上界:只有 Comparable 的子类型可以替代 T 。 例如:

sort(listOf(1, 2, 3)) // OK。Int 是 Comparable 的子类型
sort(listOf(HashMap())) // 错误:HashMap 不是
Comparable> 的子类型

默认的上界(如果没有声明)是 Any? 。在尖括号中只能指定一个上界。 如果同一类型参数需要多个上界,我们需要一个单独的 where-子句:

fun  copyWhenGreater(list: List, threshold: T): List
      where T : CharSequence,
                 T : Comparable {
      return list.filter { it > threshold }.map { it.toString() }
}

所传递的类型必须同时满足 where 子句的所有条件.在上述示例中,类型 T 必须既实现了CharSequence 也实现了 Comparable 。

类型擦除

Kotlin 为泛型声明用法执行的类型安全检测仅在编译期进行。 运行时泛型类型的实例不保留关于其类
型实参的任何信息。 其类型信息称为被擦除。例如,Foo 与 Foo 的实例都会被擦除为Foo<> 。
因此,并没有通用的方法在运行时检测一个泛型类型的实例是否通过指定类型参数所创建 ,并且编译器禁止这种 is 检测。
类型转换为带有具体类型参数的泛型类型,如 foo as List 无法在运行时检测。 当高级程序逻辑隐含了类型转换的类型安全而无法直接通过编译器推断时, 可以使用这种非受检类型转换。编译器会对非受检类型转换发出警告,并且在运行时只对非泛型部分检测(相当于 foo as List<
> )。
泛型函数调用的类型参数也同样只在编译期检测。在函数体内部, 类型参数不能用于类型检测,并且类
型转换为类型参数( foo as T )也是非受检的。然而, 内联函数的具体化的类型参数会由调用处内联函数体中的类型实参所代入,因此可以用于类型检测与转换, 与上述泛型类型的实例具有相同限制。

嵌套类与内部类

嵌套类

类可以嵌套在其他类中

nested ['nestɪd]
adj. 嵌套的,内装的
v. 筑巢;嵌入(nest的过去分词)

class Outer {
    private val bar: Int = 1
    class Nested {
        fun foo() = 2
    }
}
val demo = Outer.Nested().foo() // == 2

内部类

标记为 inner 的嵌套类能够访问其外部类的成员。内部类会带有一个对外部类的对象的引用:

class Outer {
    private val bar: Int = 1
    inner class Inner {
        fun foo() = bar
    }
}
val demo = Outer().Inner().foo() // == 1

参⻅限定的 this 表达式以了解内部类中的 this 的消歧义用法。

匿名内部类

使用对象表达式创建匿名内部类实例:

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

注:对于 JVM 平台, 如果对象是函数式 Java 接口(即具有单个抽象方法的 Java 接口)的实例, 你可以使
用带接口类型前缀的lambda表达式创建它:

val listener = ActionListener { println("clicked") }

枚举类

枚举类的最基本的用法是实现类型安全的枚举:

enum class Direction {
     NORTH, SOUTH, WEST, EAST
}

每个枚举常量都是一个对象。枚举常量用逗号分隔。

初始化

因为每一个枚举都是枚举类的实例,所以他们可以是这样初始化过的:

enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF)
}

匿名类

枚举常量还可以声明其带有相应方法以及覆盖了基类方法的匿名类。

enum class ProtocolState {
    WAITING {
        override fun signal() = TALKING
    },
    TALKING {
        override fun signal() = WAITING
    };
    abstract fun signal(): ProtocolState
}

如果枚举类定义任何成员,那么使用分号将成员定义中的枚举常量定义分隔开。
枚举条目不能包含内部类以外的嵌套类型(已在 Kotlin 1.2 中弃用)。

在枚举类中实现接口

一个枚举类可以实现接口(但不能从类继承),可以为所有条目提供统一的接口成员实现,也可以在相应匿名类中为每个条目提供各自的实现。只需将接口添加到枚举类声明中即可,如下所示:

enum class IntArithmetics : BinaryOperator, IntBinaryOperator {
    PLUS {
        override fun apply(t: Int, u: Int): Int = t + u
    },
    TIMES {
        override fun apply(t: Int, u: Int): Int = t * u
    };
    override fun applyAsInt(t: Int, u: Int) = apply(t, u)
}

使用枚举常量

Kotlin 中的枚举类也有合成方法允许列出定义的枚举常量以及通过名称获取枚举常量。这些方法的签
名如下(假设枚举类的名称是 EnumClass ):

EnumClass.valueOf(value: String): EnumClass
EnumClass.values(): Array

如果指定的名称与类中定义的任何枚举常量均不匹配,valueOf() 方法将抛出 IllegalArgumentException 异常。
自 Kotlin 1.1 起,可以使用 enumValues() 与 enumValueOf() 函数以泛型的方式访问枚举类中的常量 :

enum class RGB { RED, GREEN, BLUE }
inline fun > printAllValues() {
    print(enumValues().joinToString { it.name })
}
printAllValues() // 输出 RED, GREEN, BLUE

每个枚举常量都具有在枚举类声明中获取其名称与位置的属性:

val name: String
val ordinal: Int

枚举常量还实现了 Comparable 接口, 其中自然顺序是它们在枚举类中定义的顺序。

对象表达式与对象声明

有时候,我们需要创建一个对某个类做了轻微改动的类的对象,而不用为之显式声明新的子类。 Kotlin
用对象表达式和对象声明处理这种情况。

对象表达式

要创建一个继承自某个(或某些)类型的匿名类的对象,我们会这么写:

window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /*......*/ }
    override fun mouseEntered(e: MouseEvent) { /*......*/ }
})

如果超类型有一个构造函数,则必须传递适当的构造函数参数给它。 多个超类型可以由跟在冒号后面的
逗号分隔的列表指定:

open class A(x: Int) {
    public open val y: Int = x
}
interface B { /*......*/ }
val ab: A = object : A(1), B {
    override val y = 15
}

任何时候,如果我们只需要“一个对象而已”,并不需要特殊超类型,那么我们可以简单地写:

fun foo() {
    val adHoc = object {
        var x: Int = 0
        var y: Int = 0
    }
    print(adHoc.x + adHoc.y)
}

请注意,匿名对象可以用作只在本地和私有作用域中声明的类型。如果你使用匿名对象作为公有函数的返回类型或者用作公有属性的类型,那么该函数或属性的实际类型会是匿名对象声明的超类型,如果你没有声明任何超类型,就会是 Any 。在匿名对象中添加的成员将无法访问。

class C {
    // 私有函数,所以其返回类型是匿名对象类型
    private fun foo() = object {
        val x: String = "x"
    }
    // 公有函数,所以其返回类型是 Any
    fun publicFoo() = object {
        val x: String = "x"
    }
    fun bar() {
        val x1 = foo().x              // 没问题
        val x2 = publicFoo().x   // 错误:未能解析的引用“x”
    }
}

对象表达式中的代码可以访问来自包含它的作用域的变量。

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(继 Scala 之后)使单例声明变得很容易:

object DataProviderManager {
    fun registerDataProvider(provider: ContentProvider) {
// ......
    }
    val allDataProviders: Collection
        get() = // ......
}

这称为对象声明。并且它总是在 object 关键字后跟一个名称。 就像变量声明一样,对象声明不是一个表达式,不能用在赋值语句的右边。
对象声明的初始化过程是线程安全的。
如需引用该对象,我们直接使用其名称即可:

DataProviderManager.registerDataProvider(......)

这些对象可以有超类型:

object DefaultListener : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { ...... }
    override fun mouseEntered(e: MouseEvent) { ...... }
}

注意:对象声明不能在局部作用域(即直接嵌套在函数内部),但是它们可以嵌套到其他对象声明或非内部类中。

伴生对象

类内部的对象声明可以用 companion 关键字标记:

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

该伴生对象的成员可通过只使用类名作为限定符来调用:

val instance = MyClass.create()

可以省略伴生对象的名称,在这种情况下将使用名称 Companion :

class MyClass {
     companion object { }
}
val x = MyClass.Companion

其自身所用的类的名称(不是另一个名称的限定符)可用作对该类的伴生对象 (无论是否具名)的引用:

class MyClass1 {
    companion object Named { }
}
val x = MyClass1
class MyClass2 {
    companion object { }
}
val y = MyClass2

请注意,即使伴生对象的成员看起来像其他语言的静态成员,在运行时他们仍然是真实对象的实例成员,而且,例如还可以实现接口:

interface Factory {
    fun create(): T
}
class MyClass {
    companion object : Factory {
        override fun create(): MyClass = MyClass()
    }
}
val f: Factory = MyClass

当然,在 JVM 平台,如果使用 @JvmStatic 注解,你可以将伴生对象的成员生成为真正的静态方法和字段。更详细信息请参⻅Java 互操作性一节 。

对象表达式和对象声明之间的语义差异

对象表达式和对象声明之间有一个重要的语义差别:

  • 对象表达式是在使用他们的地方立即执行(及初始化)的;
  • 对象声明是在第一次被访问到时延迟初始化的;
  • 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配。

类型别名

类型别名为现有类型提供替代名称。 如果类型名称太⻓,你可以另外引入较短的名称,并使用新的名称替代原类型名。
它有助于缩短较⻓的泛型类型。 例如,通常缩减集合类型是很有吸引力的:

typealias NodeSet = Set
typealias FileTable = MutableMap>

你可以为函数类型提供另外的别名:

typealias MyHandler = (Int, String, Any) -> Unit
typealias Predicate = (T) -> Boolean

你可以为内部类和嵌套类创建新名称:

class A {
    inner class Inner
}
class B {
    inner class Inner
}
typealias AInner = A.Inner
typealias BInner = B.Inner

类型别名不会引入新类型。 它们等效于相应的底层类型。 当你在代码中添加 typealias Predicate 并使用 Predicate 时,Kotlin 编译器总是把它扩展为 (Int) ->Boolean 。 因此,当你需要泛型函数类型时,你可以传递该类型的变量,反之亦然:

内联类

實驗性的,1.3+ 中使用内联类时,将会得到一个警告,来表明此特性还是实验性的。

你可能感兴趣的:(Kotlin学习之类与对象2)