Kotlin 数据类、枚举类、密封类

Kotlin 数据类

Kotlin 的数据类跟 Java 中的实体类,JavaBean 很类似;用来存放一些我们需要的数据。

数据类在声明时需要使用data 关键字:

  1. data class User(val name: String, val age: Int)

我们在声明完数据类之后,编译器还会帮我们做好以下的事情:

  1. 根据构造函数传入的参数生成 equals()hashCode()
  2. 根据构造函数传入的参数生成 toString() ;格式是 "User(name=John, age=42)"
  3. 根据构造函数传入的参数生成 componetN() 函数;一个参数就有 componet1(),两个就是 componet2(),一直到 componetN()
  4. 根据构造函数传入的参数生成 copy() 函数,一般的copy() 函数会根据传入的函数生成一个新的数据类对象。

如果上面的函数本类、父类、接口中已经定义,并且是可以重写的,则不会在生成。

除了在前面添加一个 data 关键字外,数据类还有以下的规则:

  1. 主构造函数需要至少有一个参数;
  2. 主构造函数的所有参数需要标记为 valvar
  3. 数据类不能是抽象、开放、密封或者内部的;
  4. 数据类是可以实现接口的,同时也可以继承其他类;
  5. 不能显式的声明 componentN() 以及 copy() 函数。

在 JVM 中,如果生成的类需要含有一个无参的构造函数,则所有的属性必须制定默认值:

  1. data class User(
  2. var name: String? = "123",
  3. var age: Int? = 123
  4. )

注意:在 toString()equals()hashCode() 以及 copy() 的实现中只会用到 name 属性;componetN() 函数生成的个数只会跟主构造函数中的定义的属性相关。

复制

数据类提供了一个 copy() 函数,我们可以通过它复制一个对象来改变一些属性,保持其它不变。

  1. /*数据类中使用了参数默认值*/
  2. data class User(
  3. var name: String? = "123",
  4. var age: Int? = 123
  5. )
  6. fun main() {
  7. val user = User()
  8. println("user :${user.hashCode()} ,$user")
  9. // 数据类中使用了参数默认值,如果仅仅是copy()并不改变它的参数
  10. val user2 = user.copy()
  11. println("user2:${user2.hashCode()} ,$user2")
  12. //数据类中使用了参数默认值,如果传入新的参数,copy()才不会创建新的对象
  13. val user3 = user.copy(name = "321", age = 21)
  14. println("user3:${user3.hashCode()} ,$user3")
  15. val user4 = user.copy(name = "Ricky")
  16. println("user4:${user4.hashCode()} ,$user4")
  17. }

上面的代码输出:

  1. user :1509513 ,User(name=123, age=123)
  2. user2:1509513 ,User(name=123, age=123)
  3. user3:1568931 ,User(name=321, age=21)
  4. user4:-1847351199 ,User(name=Ricky, age=123)

数据类中的 copy() 的操作,在Java是这样实现的,是一种浅拷贝:

  1. // 真实copy是这里操作的,会新 new 一个对象
  2. @NotNull
  3. public final User copy(@Nullable String name, @Nullable Integer age) {
  4. return new User(name, age);
  5. }
  6. // 当我们调用 object 类的 copy 函数后,调用的就是下面这个方法,
  7. // var1 代表的是 name, var2 代表的是 age
  8. // 如果我们在 copy 的时候没有修改其属性的值,这两个参数为 null
  9. // var3 是用来判断其参数个数的
  10. public static User copy$default(User var0, String var1, Integer var2, int var3, Object var4) {
  11. if ((var3 & 1) != 0) {
  12. var1 = var0.name;
  13. }
  14. if ((var3 & 2) != 0) {
  15. var2 = var0.age;
  16. }
  17. return var0.copy(var1, var2);
  18. }

跟Java实体类对比

Java 类:

  1. public class User {
  2. private String name;
  3. private int age;
  4. public User(String name, int age) {
  5. this.name = name;
  6. this.age = age;
  7. }
  8. public String getName() {
  9. return name;
  10. }
  11. public void setName(String name) {
  12. this.name = name;
  13. }
  14. public int getAge() {
  15. return age;
  16. }
  17. public void setAge(int age) {
  18. this.age = age;
  19. }
  20. @Override
  21. public String toString() {
  22. return "User{" +
  23. "name='" + name + '\'' +
  24. ", age=" + age +
  25. '}';
  26. }
  27. }

Kotlin 类:

  1. data class User(
  2. var name: String,
  3. var age: Int
  4. )

完成相同的事情, Kotlin 代码量比 Java 的代码少了很多更简洁了

总结:

  1. 数据类是特殊的类,用于类的特性,用来存放一些特定数据
  2. 数据类的主构造函数需要至少有一个参数,并且主构造函数的所有参数需要标记为 valvar
  3. 数据类在编译时会被编译器添加一些方法,比如:equals()hashCode()copy()componentN()
  4. 数据类是可以实现接口的,同时也可以继承其他类
  5. 不能显式的声明 componentN() 以及 copy() 函数
  6. 在 JVM 中,如果生成的类需要含有一个无参的构造函数,则所有的属性必须制定默认值

Kotlin 枚举类

将变量的值一一列出来,变量的值只限于列举出来的值的范围内,就是枚举。

枚举使用 enum 关键字定义:

  1. enum class 枚举类名{
  2. ...
  3. }

枚举常量

枚举中的值的都是确定的,所以每个值都是常量。在枚举中每个枚举常量都是一个对象,枚举常量用逗号分隔。每个枚举常量只存在一个实例。例如下面就是指的四个方向:

  1. enum class Direction {
  2. NORTH, SOUTH, WEST, EAST
  3. }

不需要实例化枚举类就可以访问枚举常量,我们可以通过 类名.常量名 调用我们定义的枚举常量:

  1. fun identifyDirection(direction: Direction) {
  2. when (direction) {
  3. Direction.NORTH -> println("当前为北方")
  4. Direction.SOUTH -> println("当前为南方")
  5. Direction.WEST -> println("当前为西方")
  6. Direction.EAST -> println("当前为东方")
  7. }
  8. }
  9. fun main() {
  10. val direction = Direction.EAST
  11. println(identifyDirection(direction))
  12. }

在枚举类中的每个常量都是一个对象,我们还可以获取这个枚举常量的名字和序号:

  1. val name = Direction.EAST.name
  2. val ordinal = Direction.EAST.ordinal

我们还可以通过准确的名字获取某个枚举常量:

  1. val east = Direction.valueOf("EAST")
  2. println(east.ordinal)

我们还可以获取一个包含了所有枚举常量的数组:

  1. Direction.values().forEach {
  2. println(it)
  3. }

枚举常量的初始化

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

  1. enum class Color(val rgb: Int) {
  2. RED(0xFF0000),
  3. GREEN(0x00FF00),
  4. BLUE(0x0000FF)
  5. }
  6. fun main() {
  7. val blue = Color.BLUE
  8. println("${blue.rgb} , ${blue.name} ,${blue.ordinal}")
  9. }

上面的代码中,我们给 Color 枚举类,添加了一个属性。实质上枚举还是一个类。

匿名类

枚举常量可以声明其带有相应方法以及覆盖了基类方法的匿名类。这句话有点难理解,下面我们一一解释。

枚举类实质就是一个特殊的类,里面的每个枚举常量都是枚举类的实例。所以枚举类中也可以有类中有的一些内容,比如:属性,方法。

  1. enum class State {
  2. WAITING {
  3. override val state: String? = "Waiting"
  4. fun waiting(){
  5. println("I am Waiting")
  6. }
  7. override fun currentState(state: State) {
  8. println(this.name)
  9. }
  10. },
  11. TALKING {
  12. override val state: String? = "Talking"
  13. override fun currentState(state: State) {
  14. println(this.name)
  15. }
  16. };
  17. open val state: String? = "State"
  18. open fun currentState(state: State) {
  19. println(state)
  20. }
  21. }

如上面的代码,我们在枚举类中声明的 属性和方法都放在 枚举常量的后面

在这个时候,枚举类中的每个常量都是其子类的实例。在 JVM 平台上,每个枚举常量都会在枚举类中生成一个内部的子类,这个子类以常量名为名。既然每个枚举常量都是其所在枚举类的子类的实例,当我们声明时就可以去重写或者调用其父类的属性和方法;同时每个枚举常量,还可以声明各自独有的方法和属性,就像上面的代码一样。

还有一个更重要的知识点是:枚举类中也可以有抽象方法,因为每个枚举常量都是其所在枚举类的子类的实例,那么我们在声明枚举常量的时候,就需要把枚举类中的抽象方法实现。

  1. enum class State {
  2. WAITING {
  3. override fun currentState() {
  4. println(this.name)
  5. }
  6. },
  7. TALKING {
  8. override fun currentState() {
  9. println(this.name)
  10. }
  11. };
  12. abstract fun currentState()
  13. }
  14. fun main() {
  15. println(State.WAITING.currentState())
  16. }

上面的代码中,我们并没有创建一个新的类,只是在声明常量的时候,实现了枚举类中的抽象方法。

上面的代码表明:

  1. 枚举常量可以声明独有的属性和方法;
  2. 枚举常量也可以重写枚举类中的方法和属性;
  3. 当枚举类存在抽象方法时,枚举常量必须实现枚举类的抽象方法。

枚举类实现接口

既然枚举类也是类,那么它去实现一个接口,也是可以的。枚举类实现一个接口,那么其中的每个枚举常量在声明的时候需要实现接口中的每个抽象方法。

  1. interface Math {
  2. fun apply(t: Int, u: Int): Int
  3. }
  4. enum class IntMath : Math {
  5. PLUS {
  6. override fun apply(t: Int, u: Int): Int {
  7. return t + u
  8. }
  9. },
  10. TIMES {
  11. override fun apply(t: Int, u: Int): Int = t * u
  12. };
  13. }
  14. fun main() {
  15. val plus = IntMath.PLUS.apply(1,2)
  16. println(plus)
  17. }

枚举类源码

  1. public abstract class Enum<E : Enum<E>>(name: String, ordinal: Int): Comparable<E> {
  2. companion object {}
  3. /**
  4. * Returns the name of this enum constant, exactly as declared in its enum declaration.
  5. */
  6. public final val name: String
  7. /**
  8. * Returns the ordinal of this enumeration constant (its position in its enum declaration, where the initial constant
  9. * is assigned an ordinal of zero).
  10. */
  11. public final val ordinal: Int
  12. public override final fun compareTo(other: E): Int
  13. /**
  14. * Throws an exception since enum constants cannot be cloned.
  15. * This method prevents enum classes from inheriting from `Cloneable`.
  16. */
  17. protected final fun clone(): Any
  18. public override final fun equals(other: Any?): Boolean
  19. public override final fun hashCode(): Int
  20. public override fun toString(): String
  21. }

枚举类 Enum 是抽象的,在 Enum 中只有两个变量都是 final 的,是不允许被重写。一个是 name ,用来获取枚举变量的名字;一个是 ordinal ,用来获取枚举变量的在枚举类中的序号,也就是位置 。

Enum 还实现了一个 Comparable 接口,这就是我们可以获取枚举常量的位置原因,它可以给每个常量排序,这样就可以很容易获取它们的位置了。

总结:

  1. 枚举类是一个特殊的类,拥有类的特性
  2. 枚举类中的每个常量都是枚举类的一个实例
  3. 每个枚举常量只存在一个实例
  4. 枚举类中的枚举常量可以看做是枚举类的子类的实例
  5. 枚举类中可以包含抽象方法,同时其中的枚举常量需要实现所有的抽象方法
  6. 枚举类可以实现接口,同时其中的枚举常量需要实现所有的抽象方法

密封类

密封类在某种意义上算是枚举类的扩展,它跟枚举类很相似;都表示一组已确定的某一类值的集合。但是密封类是可继承的,并且密封类的子类可以有多个实例,每个实例都可以保存自己状态。在密封类中,已经确定的值则是以类表示的,而不是枚举常量。

要声明一个密封类,需要在类名前面添加 sealed 修饰符:

  1. sealed class Expr
  2. data class Const(val number: Double) : Expr()
  3. data class Sum(val e1: Expr, val e2: Expr) : Expr()
  4. object NotANumber : Expr()

密封类本质是一个抽象类,它不能直接实例化,但是它可以有抽象的属性和方法。并且密封类的构造默认是 private 的。

密封类的直接子类必须跟密封类在一个 kt 文件中;但是子类的子类却可以放在任何位置。

密封类的使用场景

我们拿现实世界里的例子,比如按照狗的身高和体重来分类,可以分为超大型犬、大型犬、中型犬和小型犬。按照狗的品种可以分为哈士奇,藏獒,吉娃娃,博美等;植物可以分为树,草,花等;人按年龄阶段可以分为成年人,未成年人等。

上面的的例子中,它们都属于可以把一组的已知的确定的元素分几个确定的类中。我们拿植物为例:

  1. sealed class Botany() {
  2. object Tree : Botany()
  3. object Grass : Botany()
  4. object Flower : Botany()
  5. }
  6. fun isBotany(botany: Botany): Boolean {
  7. return when (botany) {
  8. Botany.Tree,
  9. Botany.Grass,
  10. Botany.Flower -> true
  11. }
  12. }

由上面的代码我们可以看出,它的作用跟枚举很像,只是它用类的方式替换枚举常量。还有在 使用 when 表达式时,如果能够验证 when 语句覆盖了所有情况,就不需要为该语句再添加一个 else 子句了。

总结:

  1. 密封类就是枚举的扩展
  2. 密封类实质上就是一个抽象类
  3. 密封类不能实例化对象,只能用它的子类实例化对象
  4. 密封类的直接子类必须跟密封类在一个 kt 文件中;但是子类的子类却可以放在任何位置
  5. 使用密封类时,使用 when 表达式,如果能够验证 when 语句覆盖了所有情况,就不需要为该语句再添加一个 else 子句了。
  6. 密封类功能更多在于限制继承,起到划分子类的作用。将抽象类定义为密封类,可以 禁止外部继承,对于一些只划分为固定类型的数据,可以保证安全。

参考自:

Kotlin 中文网

面向对象高级:密封类