美文网首页kotlin入门潜修
kotlin入门潜修之类和对象篇—枚举类及其原理

kotlin入门潜修之类和对象篇—枚举类及其原理

作者: 寒潇2018 | 来源:发表于2018-12-16 13:42 被阅读0次

    本文收录于 kotlin入门潜修专题系列,欢迎学习交流。

    创作不易,如有转载,还请备注。

    写在前面

    古之欲明明德于天下者,先治其国。欲治其国者,先齐其家;欲齐其家者,先修其身;欲修其身者,先正其心;欲正其心者,先诚其意;欲诚其意者;先致其知;致知在格物。——与君共勉。

    枚举类

    其实在本系列文章的前面已经接触过了枚举类,且和密封类进行了对比(见kotlin入门潜修之类和对象篇—密封类及其原理
    ),只不过那篇文章密封类是主角,而本篇文章枚举类是主角。下面来看下kotlin中的枚举类。

    枚举类的用处主要是保证了类型安全,kotlin中的枚举定义和java一样,如下所示:

    enum class Color {
        RED, GREEN, BLUE, BLACK
    }
    

    枚举类使用enum关键字修饰,其成员命名一般为大写(当然你也可以小写,只是不规范),多个成员使用英文逗号隔开。

    为什么枚举类的成员要大写呢?这是因为枚举类的成员实际上表达的是个常量,常量一般规范就是大写(下面原理章节会阐述为什么说枚举类成员是个常量)。

    初始化

    枚举类成员是可以在定义的时候进行初始化的,如下所示:

    enum class Color(val value: Int) {
        RED(1), GREEN(2), BLUE(3), BLACK(4)
    }
    

    初始化时需要注意的是,必须要为枚举类显示定义构造方法(可以有多个入参),因为枚举类默认是无参的私有构造方法。这同时也意味着,你无法自己构建枚举类实例。

    那么我们可以使用初始化值吗?当然可以,比如我们要打印上面的RED的值:

    class Main {
        companion object {
            @JvmStatic fun main(args: Array<String>) {
                println(Color.RED.value)//打印RED的值
            }
        }
    }
    

    上面代码执行完成后,会打印1。之所以通过Color.RED.value完成打印,是因为我们写构造方法的时候,定义的Color类的构造方法入参就是value,如果我们定义的时候改变value为value2,那么调用方法就变成了Color.RED.value2。

    匿名类

    枚举类成员可以定义自己的匿名类,如下所示:

    enum class Weather {
        SUN {//SUN作为Weather的成员,也可以定义自己的匿名类
            override fun sayTodayWeather() {
                print("today weather: sun")
            }
        },
    
        RAIN {//同上
            override fun sayTodayWeather() {
                print("today weather: rain")
            }
        };
    //枚举类的公有方法,枚举成员必须实现此方法
        abstract fun sayTodayWeather()
    }
    
    class Main {
        companion object {
            @JvmStatic fun main(args: Array<String>) {
                Weather.RAIN.sayTodayWeather()//打印today weather: rain
            }
        }
    }
    

    注意,kotlin中,枚举类成员只能定义匿名内部类,而无法定义嵌套类。

    实现接口

    同java一样,在kotlin中,枚举类可以实现接口。如下所示:

    interface ITest {//定义了一个接口ITest
        fun sayTest()
    }
    
    enum class Test : ITest {//枚举类Test实现了Itest接口
        TEST1 {//枚举类成员TEST1,复写了ITest中的方法
            override fun sayTest() {
                 println("sayTest in TEST1")
            }
        },
        TEST2;//注意这里,没有实现任何方法
    
        override fun sayTest() {//枚举类Test复写了ITest中的方法
            println("sayTest in Test")
        }
    }
    

    上面代码执行完成后打印结果如下:

    sayTest in TEST1
    sayTest in Test
    

    从打印结果可知,kotlin枚举类可以实现默认的接口方法,kotlin中的成员也可以实现自己的接口方法。如果成员没有实现,则调用枚举类默认的方法,如果成员自己有实现,则会覆盖枚举类默认的方法,调用自己实现的默认方法。

    枚举常量的使用

    前面实际上已经阐述过一部分枚举的使用方式了,本章节再来介绍枚举类常用的其他方法,如下所示:

    enum class Color {//枚举类Color,包含两个成员
        RED, GREEN
    }
    //测试类
    class Main {
        companion object {
            @JvmStatic fun main(args: Array<String>) {
                println(Color.valueOf("RED"))//正确,打印RED
                Color.values().map(::println)//正确,打印RED、GREEN
                println(Color.valueOf("red"))//!!!错误,这里是为了说明valueOf中的值必须是和枚举成员相匹配
            }
        }
    }
    

    上代码中使用到了枚举常用的两个方法:valueOf和values,valueOf返回特定的成员值,入参是个字符串,如果没有匹配到任何枚举类成员则会抛出异常。上面代码中的最后一句就会抛出异常。而values是返回了枚举类的所有成员,该返回值的类型是个数组。

    再来看一个枚举类的例子:

    enum class Color {//枚举类Color
        RED, GREEN
    }
    //测试类
    class Main {
        companion object {
            @JvmStatic fun main(args: Array<String>) {
                println(Color.GREEN.name)//打印'GREEN'
                println(Color.RED.ordinal)//打印'0'
    
                val list = listOf(Color.RED, Color.GREEN)
                list.sortedBy { it.name }.map(::println)//打印'GREEN RED'
            }
        }
    }
    

    上面代码又展示了枚举类的其他用法:

    1. 枚举类成员都可以通过name来打印其定义的字段名字符串。
    2. 枚举类成员都可以通过ordinal来打印枚举类成员的顺序值(该顺序从0开始,可以认为是索引index)。
    3. 从list.sortedBy调用可以发现,枚举类还实现了Comparable接口,这样才符合sortedBy方法的定义。

    最后,枚举类还可以结合泛型来使用,如下所示:

    enum class Color {
        RED, GREEN
    }
    
    inline fun <reified T : Enum<T>> printAllValues() {
        print(enumValues<T>().joinToString { it.name })
    }
    
    class Main {
        companion object {
            @JvmStatic
            fun main(args: Array<String>) {
                printAllValues<Color>()
            }
        }
    }
    

    这段代码展示了枚举结合泛型的使用,代码中涉及到的reified关键字会在下面文章中有专门篇幅介绍,这里暂时了解即可。

    枚举类原理

    前面讲述了枚举类的基本使用场景,本章节来看下枚举类的原理。

    先把我们要分析的的枚举类源码展示如下:

    enum class Color {
        RED, GREEN
    }
    

    其编译成的字节码文件如下所示:

    public final enum Color extends java/lang/Enum  {//Color类继承了java的Enum类
    //注意下面几个public final常量,正是对应于Color枚举类中的成员。
      // access flags 0x4019
      public final static enum LColor; RED
      // access flags 0x4019
      public final static enum LColor; GREEN
      // access flags 0x8
      static <clinit>()V//这里是静态类构造方法,这里完成了枚举类及其成员的初始化
        ICONST_2
        ANEWARRAY Color
        DUP
        DUP
        ICONST_0
        NEW Color
        DUP
        LDC "RED"
        ICONST_0
        INVOKESPECIAL Color.<init> (Ljava/lang/String;I)V
        DUP
        PUTSTATIC Color.RED : LColor;
        AASTORE
        DUP
        ICONST_1
        NEW Color
        DUP
        LDC "GREEN"
        ICONST_1
        INVOKESPECIAL Color.<init> (Ljava/lang/String;I)V
        DUP
        PUTSTATIC Color.GREEN : LColor;
        AASTORE
        PUTSTATIC Color.$VALUES : [LColor;
        RETURN
        MAXSTACK = 8
        MAXLOCALS = 0
      // access flags 0x101A
      private final static synthetic [LColor; $VALUES
      // access flags 0x4
      // signature ()V
      // declaration: void <init>()
      protected <init>(Ljava/lang/String;I)V
        @Ljava/lang/Synthetic;() // parameter 0
        @Ljava/lang/Synthetic;() // parameter 1
       L0
        LINENUMBER 1 L0
        ALOAD 0
        ALOAD 1
        ILOAD 2
        INVOKESPECIAL java/lang/Enum.<init> (Ljava/lang/String;I)V
        RETURN
       L1
        LOCALVARIABLE this LColor; L0 L1 0
        LOCALVARIABLE $enum_name_or_ordinal$0 Ljava/lang/String; L0 L1 1
        LOCALVARIABLE $enum_name_or_ordinal$1 I L0 L1 2
        MAXSTACK = 3
        MAXLOCALS = 3
    
      // access flags 0x9
      public static values()[LColor;//这里就是编译器为我们生成的values方法
        GETSTATIC Color.$VALUES : [LColor;
        INVOKEVIRTUAL [LColor;.clone ()Ljava/lang/Object;
        CHECKCAST [LColor;
        ARETURN
        MAXSTACK = 1
        MAXLOCALS = 0
    
      // access flags 0x9
      public static valueOf(Ljava/lang/String;)LColor;//这里是编译器为我们生成的valueOf方法
        LDC LColor;.class
        ALOAD 0
        INVOKESTATIC java/lang/Enum.valueOf (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
        CHECKCAST Color
        ARETURN
        MAXSTACK = 2
        MAXLOCALS = 1
    
      @Lkotlin/Metadata;(mv={1, 1, 7}, bv={1, 0, 2}, k=1, d1={"\u0000\u000c\n\u0002\u0018\u0002\n\u0002\u0010\u0010\n\u0002\u0008\u0004\u0008\u0086\u0001\u0018\u00002\u0008\u0012\u0004\u0012\u00020\u00000\u0001B\u0007\u0008\u0002\u00a2\u0006\u0002\u0010\u0002j\u0002\u0008\u0003j\u0002\u0008\u0004\u00a8\u0006\u0005"}, d2={"LColor;", "", "(Ljava/lang/String;I)V", "RED", "GREEN", "production sources for module Kotlin-demo"})
      // compiled from: Main.kt
    }
    

    从字节码文件我们可以总结如下:

    1. kotlin编译器会自动为枚举类添加了java.lang.Enum父类,字节码如下所示:
    public final enum Color extends java/lang/Enum  
    

    而我们查看java中的Enum类可知,该类实现了Comparable接口,这就是为什么枚举类天生也实现了Comparable接口的原因。此外,枚举类中的name、ordinal方法也是来自于此类。java中Enum类定义及部分方法摘录如下:

    public abstract class Enum<E extends Enum<E>>
            implements Comparable<E>, Serializable {//可以看出,Enum实现了Comparable接口,而且也实现了Serializable接口,这也说明枚举类天生支持序列化
        public final String name() {//name方法,这就是为什么枚举类有name方法的原因
            return name;
        }
        public final int ordinal() {//ordinal方法,这就是为什么枚举类有ordinal方法的原因
            return ordinal;
        }
    
    1. 枚举类中的成员实际上会被编译成public final static的枚举常量,这就是前面为什么说枚举类成员都是常量的原因。如下所示:
      public final static enum LColor; RED
      // access flags 0x4019
      public final static enum LColor; GREEN
    
    1. kotlin编译器会为枚举成员生成对应的字符串(字符串名为字段名)及其顺序(索引),这个是在枚举类类构造方法中进行的,枚举类类构造方法之后会调用枚举类实例构造方法,而在该实例方法中又会调用java.lang.Enum类的构造方法,进而完成字符串和顺序的初始化操作,而这个字符串和顺序正式对应于Enum中的name和ordinal字段。该段描述对应的字节码如下所示:
    //这段是静态类构造方法对应的字节码(我们这里只截取了一半,完整的可以参考上面的)
       static <clinit>()V
        ICONST_2
        ANEWARRAY Color
        DUP
        DUP
        ICONST_0
        NEW Color
        DUP
        LDC "RED"//这个就是产生的字符串,对应于RED这个成员
        ICONST_0
        INVOKESPECIAL Color.<init> (Ljava/lang/String;I)V//!!!在这里调用了Color的实例构造方法,并且传入了我们刚刚生成的"RED"字符串。
        DUP
        PUTSTATIC Color.RED : LColor;
        AASTORE
        DUP
    //下面代码是Color的实例构造方法
    protected <init>(Ljava/lang/String;I)V
        @Ljava/lang/Synthetic;() // parameter 0
        @Ljava/lang/Synthetic;() // parameter 1
       L0
        LINENUMBER 1 L0
        ALOAD 0
        ALOAD 1
        ILOAD 2
        INVOKESPECIAL java/lang/Enum.<init> (Ljava/lang/String;I)V//!!!注意这里,调用了java.lang.Enum的构造方法
    //传入了一个字符串(即name)以及一个整型顺序值(即ordinal)。
        RETURN
    //为了清晰对比,这里在将Enum的构造方法摘录出来,示例如下:
        protected Enum(String name, int ordinal) {//从Enum类的构造方法可以看出,入参确实是一个name和一个ordinal
            this.name = name;
            this.ordinal = ordinal;
        }
    
    1. kotlin编译器会为枚举类添加valueOf以及values方法,这就是为什么我们能使用这两个方法的原因,对应的字节码如下所示:
     // access flags 0x9
      public static values()[LColor;//values方法,返回了数组类型
        GETSTATIC Color.$VALUES : [LColor;
        INVOKEVIRTUAL [LColor;.clone ()Ljava/lang/Object;
        CHECKCAST [LColor;
        ARETURN
        MAXSTACK = 1
        MAXLOCALS = 0
    
      // access flags 0x9
      public static valueOf(Ljava/lang/String;)LColor;//valueOf方法,返回了枚举类型
        LDC LColor;.class
        ALOAD 0
        INVOKESTATIC java/lang/Enum.valueOf (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
        CHECKCAST Color
        ARETURN
        MAXSTACK = 2
        MAXLOCALS = 1
    

    至此,枚举相关已经阐述完毕。

    相关文章

      网友评论

        本文标题:kotlin入门潜修之类和对象篇—枚举类及其原理

        本文链接:https://www.haomeiwen.com/subject/brbtkqtx.html