本文收录于 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'
}
}
}
上面代码又展示了枚举类的其他用法:
- 枚举类成员都可以通过name来打印其定义的字段名字符串。
- 枚举类成员都可以通过ordinal来打印枚举类成员的顺序值(该顺序从0开始,可以认为是索引index)。
- 从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
}
从字节码文件我们可以总结如下:
- 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;
}
- 枚举类中的成员实际上会被编译成public final static的枚举常量,这就是前面为什么说枚举类成员都是常量的原因。如下所示:
public final static enum LColor; RED
// access flags 0x4019
public final static enum LColor; GREEN
- 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;
}
- 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
至此,枚举相关已经阐述完毕。