Kotlin数据类、密封类、嵌套类、内部类、枚举类、内联类
一、数据类
数据类标记为 data,类似java POJO。
data class User(val name: String, val age: Int)
为了确保生成的代码的⼀致性以及有意义的行为,数据类必须满足以下要求:
- 主构造函数需要至少有⼀个参数;
- 主构造函数的所有参数需要标记为 val 或 var ;
- 数据类不能是抽象、开放、密封或者内部的;
- 数据类只能实现接口。(在1.1之前,1.1之后可以扩展其它类)
编译器自动从住构造函数中声明的所有属性成员导出以下成员:
- equals() / hashCode() 对
- toString() 格式是 "User(name=John, age=42)"
- componentN() 函数 按声明顺序对应于所有属性
- copy() 函数
成员生成遵循关于成员继承的这些规则:
- 如果在数据类体中有显式实现 equals() 、hashCode() 或者 toString() ,或者这些函数在父类中有 final 实现,那么不会生成这些函数,而会使用现有函数;
- 如果超类型具有 open 的 componentN() 函数并且返回兼容的类型,那么会为数据类⽣成相应的函数,并 覆盖超类的实现。如果超类型的这些函数由于签名不兼容或者是 Mnal 而导致无法覆盖,那么会报错。
- 不允许从⼀个已具 copy(……) 函数且签名匹配的类型派生⼀个数据类。
- 不允许为 componentN() 以及 copy() 函数提供显式实现。
1.在类体中声明的属性
自动生成的函数,编译器只使用在主构造函数内部定义的属性。如需在生成的实现中排除⼀个属性,需要声明在类体中。
2.复制
需要复制⼀个对象改变它的⼀些属性,但其余部分保持不变
fun copy(name: String = this.name, age: Int = this.age) = User(name, age)
val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)
3.数据类与解构声明
为数据类生成的 Component 函数 使它们可在解构声明中使用:
val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age") // 输出 "Jane, 35 years of age"
4.标准数据类
标准库提供了 Pair 与 Triple 。尽管在很多情况下具名数据类是更好的设计选择,因为它们通过为属性提供有意义的名称使代码更具可读性。
二、密封类
密封类用来表式受限的类继承结构:当⼀个值为有限几种的类型、而不能有任何其他类型时。在某种意义上,是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在⼀个实例,而密封类的⼀个子类可以有可包含状态的多个实例。
- 声明一个密封类,在类名前⾯添加 sealed 修饰符。所有子类都必须在与密封类相同的文件中声明。
- ⼀个密封类是自身抽象的,它不能直接实例化,可以有抽象(abstract)成员。
- 密封类不允许有非-private 构造函数(其构造函数默认为 private)
- 扩展密封类子类的类(间接继承者)可以放在任何位置,无需在同⼀个⽂件中
sealed class Expr
data class Const(val number: Double) : Expr() //数据类继承密封类
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
使⽤密封类的关键好处在于使用 when 表达式 的时候,如果能够验证语句覆盖了所有情况,就不需要为该语句再添加⼀个 else 子句了。当然,这只有当你用 when 作为表达式而不是作为语句时才有用。
private fun sealedClassAndWhen(expr: Expr) = when (expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(null)
NotANumber -> Double.NaN // 不再需要 `else` ⼦句,因为我们已经覆盖了所有的情况 }
}
三、嵌套类与内部类
类可以嵌套在其他类中:
class Outer {
private val bar:Int =1
class Nested {
fun foo() =2
}
}
val demo = Outer.Nested().foo() // == 2,调用嵌套类方法和java内部类一样
1.内部类
标记为 inner 的嵌套类能够访问其外部类的成员。内部类会带有⼀个对外部类的对象的引用:
class Outer {
private val bar: Int = 1
inner class Inner {
fun foo() = bar
}
}
val demo = Outer().Inner().foo() // == 1
2.匿名内部类
使用对象表达式创建匿名内部类实例:
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
……
}
override fun mouseEntered(e: MouseEvent) {
……
}
})
对象是函数式接口(当个抽象方法的接口),可以使用拉姆达表达式进行SAM转换,使⽤带接口类型前缀的lambda表达式创建。
val listener = ActionListener { println("clicked") }
四、枚举类
枚举类的最基本的用法是实现类型安全的枚举。每个枚举常量都是⼀个对象。枚举常量用逗号分隔。
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
1.初始化
因为每⼀个枚举都是枚举类的实例,所以他们可以是这样初始化过的:
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
2.匿名类
枚举常量还可以声明其带有相应方法以及覆盖了基类方法的匿名类
enum class ProtocolState {
WAITING {
override fun signal() = TALKING
},
TALKING {
override fun signal() = WAITING
};
abstract fun signal(): ProtocolState
}
3.在枚举类中实现接口
⼀个枚举类可以实现接口(但不能从类继承),可以为所有条目提供统⼀的接口成员实现,也可以在相应匿名类中为每个条目提供各自的实现。只需将接口添加到枚举类声明中即可。
enum class IntArithmetics : BinaryOperator<Int>, 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)
}
4.使用枚举常量
Kotlin中的枚举类也有合成⽅法允许列出定义的枚举常量以及通过名称获取枚举常量:
EnumClass.valueOf(value: String): EnumClass
EnumClass.values(): Array<EnumClass>
可以使用 enumValues<T>() 与 enumValueOf<T>() 函数以泛型的方式访问枚举类中的常量 :
enum class RGB { RED, GREEN, BLUE }
inline fun <reified T : Enum<T>> printAllValues() {
print(enumValues<T>().joinToString { it.name })
}
printAllValues<RGB>()// 输出 RED, GREEN, BLUE
每个枚举常量都具有在枚举类声明中获取其名称与位置的属性:
val name: String
val ordinal: Int
枚举常量还实现了 Comparable 接口,其中⾃然顺序是它们在枚举类中定义的顺序。
五、内联类
围绕某种类型创建包装器,会由于额外的堆内存分配问题,引入运行时的性能开销。此外,如果被包装的类型是原生类型,性能的损失是很糟糕的,因为原生类型通常在运行时就进行了大量优化,然而他们的包装器却没有得到任何特殊的处理。为了解决这类问题,Kotlin 引⼊了⼀种被称为内联类的特殊类,它通过在类的前⾯定义⼀个 inline 修饰符来声明.
内联类必须含有唯⼀的⼀个属性在主构造函数中初始化。在运行时,将使用这个唯⼀属性来表示内联类的实例。这就是内联类的主要特性,类的数据被 “内联”到该类使用的地方。
1.成员
内联类支持普通类中的⼀些功能。特别是,内联类可以声明属性与函数:
inline class Name(val s: String) {
val length: Int get() = s.length
fun greet() {
println("Hello, $s")
}
}
fun main() {
val name = Name("Kotlin")
name.greet() // `greet` ⽅法会作为⼀个静态⽅法被调⽤
println(name.length) // 属性的 get ⽅法会作为⼀个静态⽅法被调⽤
}
内联类的成员也有⼀些限制:
- 内联类不能含有 init 代码块
- 内联类不能含有幕后字段,所以内联类只能含有简单的计算属性(不能含有延迟初始化/委托属性)
2.继承
内联类允许去继承接口。禁止内联类参与到类的继承关系结构中。这就意味着内联类不能继承其他的类而且必须是 final。
3.表示方式
Kotlin 编译器为每个内联类保留⼀个包装器。内联类的实例可以在运行时表示为包装器或者基础类型。这就类似于 Int 可以表示为原生类型 int 或者包装器Integer。内联类既可以表式为基础类型有可以表示为包装器,引用相等对于内联类而言毫⽆意义,因此这也是被禁止的。
interface I
inline 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) // 拆箱操作: ⽤作 Foo 本⾝
asGeneric(f) // 装箱操作: ⽤作泛型类型 T
asInterface(f) // 装箱操作: ⽤作类型 I
asNullable(f) // 装箱操作: ⽤作不同于 Foo 的可空类型 Foo?
// 在下⾯这⾥例⼦中,'f' ⾸先会被装箱(当它作为参数传递给 'id' 函数时)然后⼜被拆箱(当它从'id'函数中被返回 时)
// 最后, 'c' 中就包含了被拆箱后的内部表达(也就是 '42'), 和 'f' ⼀样
val c = id(f)
}
4.名字修饰
于内联类被编译为其基础类型,因此可能会导致各种模糊的错误,例如意想不到的平台签名冲突:
inline class UInt(val x: Int)
// 在 JVM 平台上被表⽰为'public final void compute(int x)'
fun compute(x: Int) {}
// 同理,在 JVM 平台上也被表⽰为'public final void compute(int x)'!
fun compute(x: UInt) {}
会通过在函数名后面拼接⼀些稳定的哈希码来重命名函数。因此,fun compute(x: UInt) 将会被表示为 public final void compute-<hashcode>(int x) ,以此来解决冲突的问题。“-””是⼀个无效的 符号,也就是说在 Java 中不能调用使用内联类作为形参的函数。
5.内联类与类型别名
内联类似乎与类型别名非常相似。关键的区别在于类型别名与其基础类型(以及具有相同基础类型的其他类型别名)是赋值兼容的,而内联类却不是这样。 换句话说,内联类引入了⼀个真实的新类型,与类型别名正好相反,类型别名仅仅是为现有的类型取了个新的替代名称(别名):
typealias NameTypeAlias = String
inline 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) // 正确: 传递别名类型的实参替代函数中基础类型的形参
acceptString(nameInlineClass) // 错误: 不能传递内联类的实参替代函数中基础类型的形参
// And vice versa:
acceptNameTypeAlias(string) // 正确: 传递基础类型的实参替代函数中别名类型的形参
acceptNameInlineClass(string) // 错误: 不能传递基础类型的实参替代函数中内联类类型的形参
}
6.内联类的 alpha 状态
处于alpha状态,使用会有警告,必须通过指定编译器参数 -Xinline-classes 来选择使用这项特性。