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

本文收录于 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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容