17. Java学习之枚举
enum 的全称为 enumeration, 是 JDK 1.5 中引入的新特性,存放在 java.lang 包中。它是一个看似很小的特性,它使得我们在需要群组并使用枚举类型集时,可以很方便地处理。(note:可选值固定在某个范围时使用)。
一. 枚举的引入
1. 未引入枚举之前
假如有以下需求需要实现:
需求1:定义一年中的四季和对应的季度
需求2:打印当前所在的季节
需求3:获取下一个季节
需求4:得到各个季节所对应的季度
(1)需求1实现
1 package com.test.a; 2 3 public class Season { 4 5 public static final int SPRING = 1; 6 public static final int SUMMER = 2; 7 public static final int AUTUMN = 3; 8 public static final int WINTER = 4; 9 10 }
说明:如果想要引用上面的变量,直接类名.变量名就可以了,比如:Season.SPRING
(2)需求2实现
1 package com.test.a; 2 3 public class Season { 4 5 public static final Season SPRING = new Season(); 6 public static final Season SUMMER = new Season(); 7 public static final Season AUTUMN = new Season(); 8 public static final Season WINTER = new Season(); 9 10 private Season() { 11 12 } 13 14 public static void printSeason(Season seasonNow) { 15 if (seasonNow == SPRING) 16 System.out.println("Now is Spring"); 17 else if (seasonNow == SUMMER) 18 System.out.println("Now is Summer"); 19 else if (seasonNow == AUTUMN) 20 System.out.println("Now is Autumn"); 21 else 22 System.out.println("Now is Winter"); 23 } 24 25 }
说明:这次代码做了修改,引入了私有构造器,且创建了四个成员变量对象,代表了四个季节,这样有利于防止输入别的季节,且只能在该类中使用构造函数,外部类是不可以更改的。并且这个打印方法也做了实现,打印除了当前所在的季节。
(3)需求3实现
1 package com.test.a; 2 3 public class Season { 4 5 public static final Season SPRING = new Season(); 6 public static final Season SUMMER = new Season(); 7 public static final Season AUTUMN = new Season(); 8 public static final Season WINTER = new Season(); 9 10 private Season() { 11 12 } 13 14 public static void printSeason(Season seasonNow) { 15 if (seasonNow == SPRING) 16 System.out.println("Now is Spring"); 17 else if (seasonNow == SUMMER) 18 System.out.println("Now is Summer"); 19 else if (seasonNow == AUTUMN) 20 System.out.println("Now is Autumn"); 21 else 22 System.out.println("Now is Winter"); 23 } 24 25 public static Season getNextSeason(Season seasonNow) { 26 if (seasonNow == SPRING) 27 return SUMMER; 28 else if (seasonNow == SUMMER) 29 return AUTUMN; 30 else if (seasonNow == AUTUMN) 31 return WINTER; 32 else 33 return SPRING; 34 35 } 36 }
(4)需求4实现
1 package com.test.a; 2 3 public class Season { 4 5 public static final Season SPRING = new Season(); 6 public static final Season SUMMER = new Season(); 7 public static final Season AUTUMN = new Season(); 8 public static final Season WINTER = new Season(); 9 10 private Season() { 11 12 } 13 14 public static void printSeason(Season seasonNow) { 15 if (seasonNow == SPRING) 16 System.out.println("Now is Spring"); 17 else if (seasonNow == SUMMER) 18 System.out.println("Now is Summer"); 19 else if (seasonNow == AUTUMN) 20 System.out.println("Now is Autumn"); 21 else 22 System.out.println("Now is Winter"); 23 } 24 25 public static Season getNextSeason(Season seasonNow) { 26 if (seasonNow == SPRING) 27 return SUMMER; 28 else if (seasonNow == SUMMER) 29 return AUTUMN; 30 else if (seasonNow == AUTUMN) 31 return WINTER; 32 else 33 return SPRING; 34 35 } 36 37 public static int getQuarter(Season seasonNow) { 38 if (seasonNow == SPRING) 39 return 1; 40 else if (seasonNow == SUMMER) 41 return 2; 42 else if (seasonNow == AUTUMN) 43 return 3; 44 else 45 return 4; 46 47 } 48 49 }
总结:上面是没有采取枚举前的实现,每当需求变更,就会写很多的代码来适应需求,写了很多的If else,当然还有别的写法。不能简洁的实现需求。
2. 引入枚举
1 package com.test.a; 2 3 public enum Season { 4 SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4); 5 private int quater; 6 7 private Season(int quater) { 8 this.quater = quater; 9 } 10 11 public static Season getNextSeason(Season seasonNow) { 12 int nextSeasonQuater; 13 if (seasonNow.quater == 4) { 14 nextSeasonQuater = 1; 15 } 16 nextSeasonQuater = seasonNow.quater + 1; 17 return getSeasonByQuater(nextSeasonQuater); 18 } 19 20 // 得到季度对应的季节,采用枚举 21 private static Season getSeasonByQuater(int quater) { 22 for (Season season : Season.values()) { 23 if (season.quater == quater) { 24 return season; 25 } 26 } 27 return null; 28 } 29 30 }
说明:采用了枚举以后,if else减少,定义静态常量也非常简洁,并且我们可以直接调用values方法类获取对应的每个枚举实例值。整体能够支持的功能更加的多样化,且代码更加的简洁。枚举类也自定义了很多的final变量和方法,下面将会详细讲解的。
3. 为何引入枚举
根据上面的例子我们知道,在没有枚举类的时候,我们要定义一个有限的序列,春夏秋冬,一般会通过上面那种静态变量的形式,但是使用那样的形式如果需要一些其他的功能,就会出现很多风格不同的代码,且包含大量的if else。所以,枚举类的出现,就是为了简化这种操作。
二. Enum的源码分析
1. 类定义
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable
说明:这是一个抽象类,因此不能通过new来创建它的对象
翻阅thining in java,Enum类无法被继承,为什么呢?
1 public enum Season<E> extends Enum<Enum<E>>{ 2 } 3 4 public class Season extends Enum<Enum<E>>{ 5 } 6 7 public class Season extends Enum{ 8 }
说明:上面三种方式都试图来继承Enum,但是编译器都报错了。
2. 反编译
为了解释这种现象,通过javap命令来反编译Season类:
说明:1)上面是反编译的结果,发现上面的Season枚举类实际上是一个java类,且是final类型的,这就说明了这个枚举类型是不可以被继承的。并且Season继承了Enum类,因为java是单继承的,因此就意味着Season枚举类不可以继承其它的类了。(既不能再继承别人,又不能拥有子类了)。这个动作必须由编译器来完成,直接手动写成继承Enum编译报错。
2)其中定义的四个季节类型的枚举对象都被反编译成了public static final类型常量,说明可以直接访问它们。
3)上面还发现了多了一个Private类型的构造器,说明枚举类型是不允许被实例化的,即我们不可以用new来创建对象。并且,这个构造器再内部实际上调用了父类Enum的Protected类型的构造器protected Enum(String name, int ordinal) 。
倘若我们自定义了构造函数,编译器自动添加的构造函数也是不会冲突的,它将以有参构造函数的形式展示:private com.test.a.Season(int);
1 package com.test.a; 2 3 public enum Season { 4 SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4); 5 private int a; 6 7 Season(int a) { 8 this.a = a; 9 } 10 }
4)编译的时候还自动添加了static静态代码块,它用来初始化所有的枚举对象,并添加到自动生成的一个数组常量$VALUES中存储起来。
5)编译时还会自动生成values()方法,它用来返回所有枚举对象的数组。
6)编译时还会自动生成valueOf(String),它实际上调用了Enum.valueOf方法
7)注意点:values方法是编译器生成的,在父类Enum中没有;valueOf方法也是编译器生成的,它只有一个参数,但是父类中的valueOf是两个参数,但是最终还是调用了Enum类的valueOf方法。对比举例:
1 package com.test.a; 2 3 import java.util.Arrays; 4 5 public class Test { 6 public static void main(String args[]) { 7 Season seasons[] = Season.values();// Season类在编译时编译器默认提供了静态方法values。 8 System.out.println(Arrays.toString(seasons)); 9 Season season = Season.valueOf("SPRING"); 10 System.out.println(season); 11 12 } 13 } 14 15 16 [SPRING, SUMMER, AUTUMN, WINTER] 17 SPRING
说明:values()方法的作用就是获取枚举类中的所有变量,并且作为数组返回;valueOf(String name)同父类方法,只是它更简便。倘若将枚举实例向上转型成Enum,这两个方法都不可以被调用。比如:
1 package com.test.a; 2 3 import java.util.Arrays; 4 5 public class Test { 6 public static void main(String args[]) { 7 Enum enum1 = Season.SPRING; 8 enum1.valueOf("SPRING");// 只支持两个参数调用,编译错误 9 enum1.values();// 编译错误,没有这个values方法 10 11 } 12 }
总结:枚举类型它是一种特殊的数据类型,是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也让枚举变得相对简单和安全。
三. 基本用法
1. 变量
(1)name
相关的源码实现:
1 //对应的源码方法 2 public final String name() { 3 return name; 4 }
1 public String toString() { 2 return name; 3 }
说明:name()方法和toString()方法是一样的,唯一的区别是,你可以重写toString方法。name变量就是枚举变量的字符串形式。举例:
1 package com.test.a; 2 3 public class Test { 4 public static void main(String args[]) { 5 Season season = Season.SPRING; 6 String name = season.name(); 7 String name2 = season.toString(); 8 System.out.println(name); 9 System.out.println(name2); 10 11 } 12 }
输出:
SPRING
SPRING
(2)ordinal
1 //对应的源码方法 2 public final int ordinal() { 3 return ordinal; 4 }
说明:默认情况下,枚举类会给所有的枚举变量一个默认的次序,该次序从0开始,用ordinal变量表示。比如:
1 package com.test.a; 2 3 public class Test { 4 public static void main(String args[]) { 5 Season season = Season.SPRING; 6 int ordinal = season.ordinal(); 7 System.out.println(ordinal); 8 9 } 10 } 11 12 输出: 13 0
2. 方法
就介绍一些上面没有提到过的:
(1)compareTo
实际上就是比较的oridinal,如果为负,就代表前者小于后者。例如:
1 package com.test.a; 2 3 public class Test { 4 public static void main(String args[]) { 5 int i = Season.SPRING.compareTo(Season.WINTER); 6 System.out.println(i); 7 8 } 9 } 10 11 -3
(2)getDeclaringClass
说明:该方法是获取枚举实例变量的方法。针对上面向上转型以后是不可以调用values方法和valueOf方法的,就可以通过这个getDeclaringClass和反射来获取枚举实例。
在Class类中,有以下两个方法和枚举有关:
返回类型 | 方法名称 | 方法说明 |
---|---|---|
T[] |
getEnumConstants() |
返回该枚举类型的所有元素,如果Class对象不是枚举类型,则返回null。 |
boolean |
isEnum() |
当且仅当该类声明为enum类型时返回true |
简单例子:
1 package com.test.a; 2 3 import java.util.Arrays; 4 5 public enum Season { 6 SPRING, SUMMER, AUTUMN, WINTER; 7 public static void main(String args[]) { 8 System.out.println(Season.SPRING.getDeclaringClass()); 9 System.out.println(Season.class.isEnum()); 10 Season seasons[] = Season.class.getEnumConstants(); 11 System.out.println(Arrays.toString(seasons)); 12 } 13 } 14 15 16 class com.test.a.Season 17 true 18 [SPRING, SUMMER, AUTUMN, WINTER]
(3)getClass 和 getDeclaringClass的区别
Note:上面例子中的getClass可以提到getDeclaringClass。会得到相同的结果。但是并不是总是能够得到相同的结果。比如以下情况就不可以:
1 package com.test.a; 2 3 public enum Season { 4 5 SPRING { 6 int getOrdinal() { 7 return 0; 8 } 9 }, 10 SUMMER { 11 int getOrdinal() { 12 return 1; 13 } 14 }; 15 16 public static void main(String args[]) { 17 System.out.println(Season.SPRING.getClass()); 18 System.out.println(Season.SPRING.getDeclaringClass()); 19 } 20 21 abstract int getOrdinal(); 22 23 } 24 25 class com.test.a.Season$1 26 class com.test.a.Season
说明:上面的打印结果就不相同。SPRING和SUMMER相当于Season的内部类,并研究getDeclaringClass源码如下:
1 @SuppressWarnings("unchecked") 2 public final Class<E> getDeclaringClass() { 3 Class<?> clazz = getClass(); 4 Class<?> zuper = clazz.getSuperclass(); 5 return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper; 6 }
根据源码逻辑,我们知道父类是不是Enum,决定了最终返回的是何种类型,分类更加的清晰。因此,判断枚举类型时,使用getDeclaringClass更有保障。
四. 高级用法
1. 向enum类中添加变量和方法
enum其实和常规类的用法差不多,可以添加变量,方法,甚至是main方法。
1 package com.test.a; 2 3 public enum Season { 4 5 SPRING("春天"), SUMMER("夏天"), AUTUMN("秋天"), WINTER("冬天"); 6 7 private String name; 8 9 private Season(String name) { 10 this.name = name; 11 } 12 13 public String getName() { 14 return name; 15 } 16 17 public static void main(String[] args) { 18 for (Season day : Season.values()) { 19 System.out.println("name:" + day.name() + ",name:" + day.getName()); 20 } 21 } 22 23 }
1 name:SPRING,name:春天 2 name:SUMMER,name:夏天 3 name:AUTUMN,name:秋天 4 name:WINTER,name:冬天
说明:根据上面代码示例可以知道,enum和常规类用法差不多。但是enum中方法或者变量的定义必须要在enum实例对象定义之后,否则编译器会报错;不能手动创建enum类的对象,因为这必须且只能编译器自己做。
2. 复写父类中的方法
只支持toString方法的重写
3. enum中定义抽象方法
在enum中可以定义抽象方法,并且在每个枚举实例中重写这个抽象方法,这样就可以让不同的枚举实例拥有不同的行为。比如:
1 package com.test.a; 2 3 public enum Season { 4 SPRING { 5 6 @Override 7 public void f() { 8 System.out.println("this is spring"); 9 10 } 11 12 }, 13 WINTER { 14 @Override 15 public void f() { 16 System.out.println("this is winter"); 17 18 } 19 }; 20 21 public static void main(String[] args) { 22 Season.SPRING.f(); 23 Season.WINTER.f(); 24 } 25 26 public abstract void f(); 27 28 }
1 this is spring 2 this is winter
说明:enum类的实例似乎表现出了多态的特性,但是枚举类型的实例终究不能作为类型传递使用,一旦使用,编译器就会报错,比如
public void text(Season.SPRING instance){ } //编译错误,因为SEASON.SPRING是个实例对象
4. enum可以实现多个接口
1 public interface Base { 2 public void f(); 3 } 4 public interface Base2 { 5 public void g(); 6 7 }
1 package com.test.a; 2 3 public enum Season implements Base, Base2 { 4 SPRING, WINTER; 5 6 public static void main(String[] args) { 7 Season.SPRING.f(); 8 Season.SPRING.g(); 9 } 10 11 @Override 12 public void g() { 13 System.out.println("ggggg"); 14 15 } 16 17 @Override 18 public void f() { 19 System.out.println("fffffff"); 20 } 21 22 } 23 24 fffffff 25 ggggg
5. switch中可以支持枚举
六. EnumMap
EnumMap是专门针对枚举类型而创建的类。它的键依照ordinal的值作为put顺序。简单例子:
1 public enum Season{ 2 WINTER,AUTUMN,SPRING; 3 } 4 5 public class Test { 6 public static void main(String args[]) { 7 EnumMap enumMap=new EnumMap(Season.class); 8 enumMap.put(Season.AUTUMN, "秋天"); 9 enumMap.put(Season.WINTER, "冬天"); 10 enumMap.put(Season.SPRING, "春天"); 11 System.out.println(enumMap); 12 } 13 } 14 15 {WINTER=冬天, AUTUMN=秋天, SPRING=春天}
说明:根据输出结果可以知道和enum类定义中的ordinal顺序一致。
知识点:EnumMap的基本实现原理,内部有两个数组,长度相同,一个表示所有可能的键,一个表示对应的值,值为null表示没有该键值对,键都有一个对应的索引,根据索引可直接访问和操作其键和值,效率很高。(知识点摘自:https://www.cnblogs.com/swiftma/p/6044672.html)
note:更多关于enumMap和EnumSet的用法后面再补充。还有关于枚举单例模式等,后面补充。