秒懂Java枚举类型(enum)

理解枚举类型

枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。下面先来看看什么是枚举?如何定义枚举?

枚举的定义

  上述的常量定义常量的方式称为int枚举模式,这样的定义方式并没有什么错,但它存在许多不足,如在类型安全和使用方便性上并没有多少好处,如果存在定义int值相同的变量,混淆的几率还是很大的,编译器也不会提出任何警告,因此这种方式在枚举出现后并不提倡,现在我们利用枚举类型来重新定义上述的常量,同时也感受一把枚举定义的方式,如下定义周一到周日的常量

public class DayDemo {
 public static final int MONDAY =1;
 public static final int TUESDAY=2;
 public static final int WEDNESDAY=3;
 public static final int THURSDAY=4;
 public static final int FRIDAY=5;
 public static final int SATURDAY=6;
 public static final int SUNDAY=7;
}

相当简洁,在定义枚举类型时我们使用的关键字是enum,与class关键字类似,只不过前者是定义枚举类型,后者是定义类类型。枚举类型Day中分别定义了从周一到周日的值,这里要注意,值一般是大写的字母,多个值之间以逗号分隔。同时我们应该知道的是枚举类型可以像类(class)类型一样,定义为一个单独的文件,当然也可以定义在其他类内部,更重要的是枚举常量在类型安全性和便捷性都很有保证,如果出现类型问题编译器也会提示我们改进,但务必记住枚举表示的类型其取值是必须有限的,也就是说每个值都是可以枚举出来的,比如上述描述的一周共有七天。那么该如何使用呢?如下:

//枚举类型,使用关键字enum
enum Day {
 MONDAY, TUESDAY, WEDNESDAY,
 THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

就像上述代码那样,直接引用枚举的值即可,这便是枚举类型的最简单模型。

枚举实现的原理

我们大概了解了枚举类型的定义与简单使用后,现在有必要来了解一下枚举类型的基本实现原理。实际上在使用关键字enum创建枚举类型并编译后,编译器会为我们生成一个相关的类,这个类继承了Java API中的java.lang.Enum类,也就是说通过关键字enum创建枚举类型在编译后事实上也是一个类类型而且该类继承自java.lang.Enum类。下面我们编译前面定义的EnumDemo.java并查看生成的class文件来验证这个结论:

javac EnumDemo.java
Day.class EnumDemo.class EnumDemo.java

利用javac编译前面定义的EnumDemo.java文件后分别生成了Day.class和EnumDemo.class文件,而Day.class就是枚举类型,这也就验证前面所说的使用关键字enum定义枚举类型并编译后,编译器会自动帮助我们生成一个与枚举相关的类。我们再来看看反编译Day.class文件:

//反编译Day.class
final class Day extends Enum
{
 //编译器为我们添加的静态的values()方法
 public static Day[] values()
 {
 return (Day[])$VALUES.clone();
 }
 //编译器为我们添加的静态的valueOf()方法,注意间接调用了Enum也类的valueOf方法
 public static Day valueOf(String s)
 {
 return (Day)Enum.valueOf(com/zejian/enumdemo/Day, s);
 }
 //私有构造函数
 private Day(String s, int i)
 {
 super(s, i);
 }
 //前面定义的7种枚举实例
 public static final Day MONDAY;
 public static final Day TUESDAY;
 public static final Day WEDNESDAY;
 public static final Day THURSDAY;
 public static final Day FRIDAY;
 public static final Day SATURDAY;
 public static final Day SUNDAY;
 private static final Day $VALUES[];

 static
 {
 //实例化枚举实例
 MONDAY = new Day("MONDAY", 0);
 TUESDAY = new Day("TUESDAY", 1);
 WEDNESDAY = new Day("WEDNESDAY", 2);
 THURSDAY = new Day("THURSDAY", 3);
 FRIDAY = new Day("FRIDAY", 4);
 SATURDAY = new Day("SATURDAY", 5);
 SUNDAY = new Day("SUNDAY", 6);
 $VALUES = (new Day[] {
  MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
 });
 }
}

从反编译的代码可以看出编译器确实帮助我们生成了一个Day类(注意该类是final类型的,将无法被继承)而且该类继承自java.lang.Enum类,该类是一个抽象类(稍后我们会分析该类中的主要方法),除此之外,编译器还帮助我们生成了7个Day类型的实例对象分别对应枚举中定义的7个日期,这也充分说明了我们前面使用关键字enum定义的Day类型中的每种日期枚举常量也是实实在在的Day实例对象,只不过代表的内容不一样而已。注意编译器还为我们生成了两个静态方法,分别是values()和 valueOf(),稍后会分析它们的用法,到此我们也就明白了,使用关键字enum定义的枚举类型,在编译期后,也将转换成为一个实实在在的类,而在该类中,会存在每个在枚举类型中定义好变量的对应实例对象,如上述的MONDAY枚举类型对应public static final Day MONDAY;,同时编译器会为该类创建两个方法,分别是values()和valueOf()。ok~,到此相信我们对枚举的实现原理也比较清晰,下面我们深入了解一下java.lang.Enum类以及values()和valueOf()的用途。

枚举的常用方法

Enum抽象类常用方法

Enum是所有 Java 语言枚举类型的公共基本类(注意Enum是抽象类),以下是它的常见方法:

返回类型 方法名称 方法说明
int compareTo(E o) 比较此枚举与指定对象的顺序
boolean equals(Object other) 当指定对象等于此枚举常量时,返回 true。
Class<?> getDeclaringClass() 返回与此枚举常量的枚举类型相对应的 Class 对象
String name() 返回此枚举常量的名称,在其枚举声明中对其进行声明
int ordinal() 返回枚举常量的序数(它在枚举声明中的位置,其中初始常量序数为零)
String toString() 返回枚举常量的名称,它包含在声明中
static<T extends Enum<T>> T static valueOf(Class<T> enumType, String name)
返回带指定名称的指定枚举类型的枚举常量。

这里主要说明一下ordinal()方法,该方法获取的是枚举变量在枚举类中声明的顺序,下标从0开始,如日期中的MONDAY在第一个位置,那么MONDAY的ordinal值就是0,如果MONDAY的声明位置发生变化,那么ordinal方法获取到的值也随之变化,注意在大多数情况下我们都不应该首先使用该方法,毕竟它总是变幻莫测的。compareTo(E o)方法则是比较枚举的大小,注意其内部实现是根据每个枚举的ordinal值大小进行比较的。name()方法与toString()几乎是等同的,都是输出变量的字符串形式。至于valueOf(Class<T> enumType, String name)方法则是根据枚举类的Class对象和枚举名称获取枚举常量,注意该方法是静态的,后面在枚举单例时,我们还会详细分析该方法,下面的代码演示了上述方法:

public class EnumDemo {

 public static void main(String[] args){

 //创建枚举数组
 Day[] days=new Day[]{Day.MONDAY, Day.TUESDAY, Day.WEDNESDAY,
  Day.THURSDAY, Day.FRIDAY, Day.SATURDAY, Day.SUNDAY};

 for (int i = 0; i <days.length ; i++) {
  System.out.println("day["+i+"].ordinal():"+days[i].ordinal());
 }

 System.out.println("-------------------------------------");
 //通过compareTo方法比较,实际上其内部是通过ordinal()值比较的
 System.out.println("days[0].compareTo(days[1]):"+days[0].compareTo(days[1]));
 System.out.println("days[0].compareTo(days[1]):"+days[0].compareTo(days[2]));

 //获取该枚举对象的Class对象引用,当然也可以通过getClass方法
 Class<?> clazz = days[0].getDeclaringClass();
 System.out.println("clazz:"+clazz);

 System.out.println("-------------------------------------");

 //name()
 System.out.println("days[0].name():"+days[0].name());
 System.out.println("days[1].name():"+days[1].name());
 System.out.println("days[2].name():"+days[2].name());
 System.out.println("days[3].name():"+days[3].name());

 System.out.println("-------------------------------------");

 System.out.println("days[0].toString():"+days[0].toString());
 System.out.println("days[1].toString():"+days[1].toString());
 System.out.println("days[2].toString():"+days[2].toString());
 System.out.println("days[3].toString():"+days[3].toString());

 System.out.println("-------------------------------------");

 Day d=Enum.valueOf(Day.class,days[0].name());
 Day d2=Day.valueOf(Day.class,days[0].name());
 System.out.println("d:"+d);
 System.out.println("d2:"+d2);
 }
 /**
 执行结果:
 day[0].ordinal():0
 day[1].ordinal():1
 day[2].ordinal():2
 day[3].ordinal():3
 day[4].ordinal():4
 day[5].ordinal():5
 day[6].ordinal():6
 -------------------------------------
 days[0].compareTo(days[1]):-1
 days[0].compareTo(days[1]):-2
 clazz:class com.zejian.enumdemo.Day
 -------------------------------------
 days[0].name():MONDAY
 days[1].name():TUESDAY
 days[2].name():WEDNESDAY
 days[3].name():THURSDAY
 -------------------------------------
 days[0].toString():MONDAY
 days[1].toString():TUESDAY
 days[2].toString():WEDNESDAY
 days[3].toString():THURSDAY
 -------------------------------------
 d:MONDAY
 d2:MONDAY
 */

}
enum Day {
 MONDAY, TUESDAY, WEDNESDAY,
 THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

到此对于抽象类Enum类的基本内容就介绍完了,这里提醒大家一点,Enum类内部会有一个构造函数,该构造函数只能有编译器调用,我们是无法手动操作的,不妨看看Enum类的主要源码:

//实现了Comparable
public abstract class Enum<E extends Enum<E>>
 implements Comparable<E>, Serializable {

 private final String name; //枚举字符串名称

 public final String name() {
 return name;
 }

 private final int ordinal;//枚举顺序值

 public final int ordinal() {
 return ordinal;
 }

 //枚举的构造方法,只能由编译器调用
 protected Enum(String name, int ordinal) {
 this.name = name;
 this.ordinal = ordinal;
 }

 public String toString() {
 return name;
 }

 public final boolean equals(Object other) {
 return this==other;
 }

 //比较的是ordinal值
 public final int compareTo(E o) {
 Enum<?> other = (Enum<?>)o;
 Enum<E> self = this;
 if (self.getClass() != other.getClass() && // optimization
  self.getDeclaringClass() != other.getDeclaringClass())
  throw new ClassCastException();
 return self.ordinal - other.ordinal;//根据ordinal值比较大小
 }

 @SuppressWarnings("unchecked")
 public final Class<E> getDeclaringClass() {
 //获取class对象引用,getClass()是Object的方法
 Class<?> clazz = getClass();
 //获取父类Class对象引用
 Class<?> zuper = clazz.getSuperclass();
 return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
 }

 public static <T extends Enum<T>> T valueOf(Class<T> enumType,
      String name) {
 //enumType.enumConstantDirectory()获取到的是一个map集合,key值就是name值,value则是枚举变量值
 //enumConstantDirectory是class对象内部的方法,根据class对象获取一个map集合的值
 T result = enumType.enumConstantDirectory().get(name);
 if (result != null)
  return result;
 if (name == null)
  throw new NullPointerException("Name is null");
 throw new IllegalArgumentException(
  "No enum constant " + enumType.getCanonicalName() + "." + name);
 }

 //.....省略其他没用的方法
}

通过Enum源码,可以知道,Enum实现了Comparable接口,这也是可以使用compareTo比较的原因,当然Enum构造函数也是存在的,该函数只能由编译器调用,毕竟我们只能使用enum关键字定义枚举,其他事情就放心交给编译器吧。

//由编译器调用
protected Enum(String name, int ordinal) {
 this.name = name;
 this.ordinal = ordinal;
 }

编译器生成的Values方法与ValueOf方法

values()方法和valueOf(String name)方法是编译器生成的static方法,因此从前面的分析中,在Enum类中并没出现values()方法,但valueOf()方法还是有出现的,只不过编译器生成的valueOf()方法需传递一个name参数,而Enum自带的静态方法valueOf()则需要传递两个方法,从前面反编译后的代码可以看出,编译器生成的valueOf方法最终还是调用了Enum类的valueOf方法,下面通过代码来演示这两个方法的作用:

Day[] days2 = Day.values();
System.out.println("day2:"+Arrays.toString(days2));
Day day = Day.valueOf("MONDAY");
System.out.println("day:"+day);

/**
 输出结果:
 day2:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
 day:MONDAY
 */

从结果可知道,values()方法的作用就是获取枚举类中的所有变量,并作为数组返回,而valueOf(String name)方法与Enum类中的valueOf方法的作用类似根据名称获取枚举变量,只不过编译器生成的valueOf方法更简洁些只需传递一个参数。这里我们还必须注意到,由于values()方法是由编译器插入到枚举类中的static方法,所以如果我们将枚举实例向上转型为Enum,那么values()方法将无法被调用,因为Enum类中并没有values()方法,valueOf()方法也是同样的道理,注意是一个参数的。

//正常使用
Day[] ds=Day.values();
//向上转型Enum
Enum e = Day.MONDAY;
//无法调用,没有此方法
//e.values();

枚举与Class对象

上述我们提到当枚举实例向上转型为Enum类型后,values()方法将会失效,也就无法一次性获取所有枚举实例变量,但是由于Class对象的存在,即使不使用values()方法,还是有可能一次获取到所有枚举实例变量的,在Class对象中存在如下方法:

返回类型 方法名称 方法说明
T[] getEnumConstants() 返回该枚举类型的所有元素,如果Class对象不是枚举类型,则返回null。
boolean isEnum() 当且仅当该类声明为源代码中的枚举时返回 true

因此通过getEnumConstants()方法,同样可以轻而易举地获取所有枚举实例变量下面通过代码来演示这个功能:

//正常使用
Day[] ds=Day.values();
//向上转型Enum
Enum e = Day.MONDAY;
//无法调用,没有此方法
//e.values();
//获取class对象引用
Class<?> clasz = e.getDeclaringClass();
if(clasz.isEnum()) {
 Day[] dsz = (Day[]) clasz.getEnumConstants();
 System.out.println("dsz:"+Arrays.toString(dsz));
}

/**
 输出结果:
 dsz:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
 */

正如上述代码所展示,通过Enum的class对象的getEnumConstants方法,我们仍能一次性获取所有的枚举实例常量。

枚举的进阶用法

在前面的分析中,我们都是基于简单枚举类型的定义,也就是在定义枚举时只定义了枚举实例类型,并没定义方法或者成员变量,实际上使用关键字enum定义的枚举类,除了不能使用继承(因为编译器会自动为我们继承Enum抽象类而Java只支持单继承,因此枚举类是无法手动实现继承的),可以把enum类当成常规类,也就是说我们可以向enum类中添加方法和变量,甚至是mian方法,下面就来感受一把。

向enum类添加方法与自定义构造函数

重新定义一个日期枚举类,带有desc成员变量描述该日期的对于中文描述,同时定义一个getDesc方法,返回中文描述内容,自定义私有构造函数,在声明枚举实例时传入对应的中文描述,代码如下:

public enum Day2 {
MONDAY("星期一"),
TUESDAY("星期二"),
WEDNESDAY("星期三"),
THURSDAY("星期四"),
FRIDAY("星期五"),
SATURDAY("星期六"),
SUNDAY("星期日");//记住要用分号结束

private String desc;//中文描述

/**
* 私有构造,防止被外部调用
* @param desc
*/
private Day2(String desc){
this.desc=desc;
}

/**
* 定义方法,返回描述,跟常规类的定义没区别
* @return
*/
public String getDesc(){
return desc;
}

public static void main(String[] args){
for (Day2 day:Day2.values()) {
System.out.println("name:"+day.name()+
",desc:"+day.getDesc());
}
}

/**
输出结果:
name:MONDAY,desc:星期一
name:TUESDAY,desc:星期二
name:WEDNESDAY,desc:星期三
name:THURSDAY,desc:星期四
name:FRIDAY,desc:星期五
name:SATURDAY,desc:星期六
name:SUNDAY,desc:星期日
*/
}

从上述代码可知,在enum类中确实可以像定义常规类一样声明变量或者成员方法。但是我们必须注意到,如果打算在enum类中定义方法,务必在声明完枚举实例后使用分号分开,倘若在枚举实例前定义任何方法,编译器都将会报错,无法编译通过,同时即使自定义了构造函数且enum的定义结束,我们也永远无法手动调用构造函数创建枚举实例,毕竟这事只能由编译器执行。

关于覆盖enum类方法

既然enum类跟常规类的定义没什么区别(实际上enum还是有些约束的),那么覆盖父类的方法也不会是什么难说,可惜的是父类Enum中的定义的方法只有toString方法没有使用final修饰,因此只能覆盖toString方法,如下通过覆盖toString省去了getDesc方法:

public enum Day2 {
 MONDAY("星期一"),
 TUESDAY("星期二"),
 WEDNESDAY("星期三"),
 THURSDAY("星期四"),
 FRIDAY("星期五"),
 SATURDAY("星期六"),
 SUNDAY("星期日");//记住要用分号结束

 private String desc;//中文描述

 /**
 * 私有构造,防止被外部调用
 * @param desc
 */
 private Day2(String desc){
 this.desc=desc;
 }

 /**
 * 覆盖
 * @return
 */
 @Override
 public String toString() {
 return desc;
 }

 public static void main(String[] args){
 for (Day2 day:Day2.values()) {
  System.out.println("name:"+day.name()+
   ",desc:"+day.toString());
 }
 }

 /**
 输出结果:
 name:MONDAY,desc:星期一
 name:TUESDAY,desc:星期二
 name:WEDNESDAY,desc:星期三
 name:THURSDAY,desc:星期四
 name:FRIDAY,desc:星期五
 name:SATURDAY,desc:星期六
 name:SUNDAY,desc:星期日
 */
}

enum类中定义抽象方法

与常规抽象类一样,enum类允许我们为其定义抽象方法,然后使每个枚举实例都实现该方法,以便产生不同的行为方式,注意abstract关键字对于枚举类来说并不是必须的如下:

public enum EnumDemo3 {

 FIRST{
 @Override
 public String getInfo() {
  return "FIRST TIME";
 }
 },
 SECOND{
 @Override
 public String getInfo() {
  return "SECOND TIME";
 }
 }

 ;

 /**
 * 定义抽象方法
 * @return
 */
 public abstract String getInfo();

 //测试
 public static void main(String[] args){
 System.out.println("F:"+EnumDemo3.FIRST.getInfo());
 System.out.println("S:"+EnumDemo3.SECOND.getInfo());
 /**
  输出结果:
  F:FIRST TIME
  S:SECOND TIME
  */
 }
}

通过这种方式就可以轻而易举地定义每个枚举实例的不同行为方式。我们可能注意到,enum类的实例似乎表现出了多态的特性,可惜的是枚举类型的实例终究不能作为类型传递使用,就像下面的使用方式,编译器是不可能答应的:

//无法通过编译,毕竟EnumDemo3.FIRST是个实例对象
 public void text(EnumDemo3.FIRST instance){ }

在枚举实例常量中定义抽象方法

enum类与接口

由于Java单继承的原因,enum类并不能再继承其它类,但并不妨碍它实现接口,因此enum类同样是可以实现多接口的,如下:

interface food{
 void eat();
}

interface sport{
 void run();
}

public enum EnumDemo2 implements food ,sport{
 FOOD,
 SPORT,
 ; //分号分隔

 @Override
 public void eat() {
 System.out.println("eat.....");
 }

 @Override
 public void run() {
 System.out.println("run.....");
 }
}

有时候,我们可能需要对一组数据进行分类,比如进行食物菜单分类而且希望这些菜单都属于food类型,appetizer(开胃菜)、mainCourse(主菜)、dessert(点心)、Coffee等,每种分类下有多种具体的菜式或食品,此时可以利用接口来组织,如下(代码引用自Thinking in Java):

public interface Food {
 enum Appetizer implements Food {
 SALAD, SOUP, SPRING_ROLLS;
 }
 enum MainCourse implements Food {
 LASAGNE, BURRITO, PAD_THAI,
 LENTILS, HUMMOUS, VINDALOO;
 }
 enum Dessert implements Food {
 TIRAMISU, GELATO, BLACK_FOREST_CAKE,
 FRUIT, CREME_CARAMEL;
 }
 enum Coffee implements Food {
 BLACK_COFFEE, DECAF_COFFEE, ESPRESSO,
 LATTE, CAPPUCCINO, TEA, HERB_TEA;
 }
}

public class TypeOfFood {
 public static void main(String[] args) {
 Food food = Appetizer.SALAD;
 food = MainCourse.LASAGNE;
 food = Dessert.GELATO;
 food = Coffee.CAPPUCCINO;
 }
}

通过这种方式可以很方便组织上述的情景,同时确保每种具体类型的食物也属于Food,现在我们利用一个枚举嵌套枚举的方式,把前面定义的菜谱存放到一个Meal菜单中,通过这种方式就可以统一管理菜单的数据了。

public enum Meal{
 APPETIZER(Food.Appetizer.class),
 MAINCOURSE(Food.MainCourse.class),
 DESSERT(Food.Dessert.class),
 COFFEE(Food.Coffee.class);
 private Food[] values;
 private Meal(Class<? extends Food> kind) {
 //通过class对象获取枚举实例
 values = kind.getEnumConstants();
 }
 public interface Food {
 enum Appetizer implements Food {
 SALAD, SOUP, SPRING_ROLLS;
 }
 enum MainCourse implements Food {
 LASAGNE, BURRITO, PAD_THAI,
 LENTILS, HUMMOUS, VINDALOO;
 }
 enum Dessert implements Food {
 TIRAMISU, GELATO, BLACK_FOREST_CAKE,
 FRUIT, CREME_CARAMEL;
 }
 enum Coffee implements Food {
 BLACK_COFFEE, DECAF_COFFEE, ESPRESSO,
 LATTE, CAPPUCCINO, TEA, HERB_TEA;
 }
 }
}

枚举与switch

关于枚举与switch是个比较简单的话题,使用switch进行条件判断时,条件参数一般只能是整型,字符型。而枚举型确实也被switch所支持,在java 1.7后switch也对字符串进行了支持。这里我们简单看一下switch与枚举类型的使用:

enum Color {GREEN,RED,BLUE}

public class EnumDemo4 {

 public static void printName(Color color){
 switch (color){
  case BLUE: //无需使用Color进行引用
  System.out.println("蓝色");
  break;
  case RED:
  System.out.println("红色");
  break;
  case GREEN:
  System.out.println("绿色");
  break;
 }
 }

 public static void main(String[] args){
 printName(Color.BLUE);
 printName(Color.RED);
 printName(Color.GREEN);

 //蓝色
 //红色
 //绿色
 }
}

需要注意的是使用在于switch条件进行结合使用时,无需使用Color引用。

枚举与单例模式

单例模式可以说是最常使用的设计模式了,它的作用是确保某个类只有一个实例,自行实例化并向整个系统提供这个实例。在实际应用中,线程池、缓存、日志对象、对话框对象常被设计成单例,总之,选择单例模式就是为了避免不一致状态,下面我们将会简单说明单例模式的几种主要编写方式,从而对比出使用枚举实现单例模式的优点。首先看看饿汉式的单例模式:

/**
 * 饿汉式(基于classloder机制避免了多线程的同步问题)
 */
public class SingletonHungry {

 private static SingletonHungry instance = new SingletonHungry();

 private SingletonHungry() {
 }

 public static SingletonHungry getInstance() {
 return instance;
 }
}

显然这种写法比较简单,但问题是无法做到延迟创建对象,事实上如果该单例类涉及资源较多,创建比较耗时间时,我们更希望它可以尽可能地延迟加载,从而减小初始化的负载,于是便有了如下的懒汉式单例:

/**
 * Created by wuzejian on 2017/5/9..
 * 懒汉式单例模式(适合多线程安全)
 */
public class SingletonLazy {

 private static volatile SingletonLazy instance;

 private SingletonLazy() {
 }

 public static synchronized SingletonLazy getInstance() {
 if (instance == null) {
  instance = new SingletonLazy();
 }
 return instance;
 }
}

这种写法能够在多线程中很好的工作避免同步问题,同时也具备lazy loading机制,遗憾的是,由于synchronized的存在,效率很低,在单线程的情景下,完全可以去掉synchronized,为了兼顾效率与性能问题,改进后代码如下:

public class Singleton {
 private static volatile Singleton singleton = null;

 private Singleton(){}

 public static Singleton getSingleton(){
 if(singleton == null){
  synchronized (Singleton.class){
  if(singleton == null){
   singleton = new Singleton();
  }
  }
 }
 return singleton;
 }
}

这种编写方式被称为“双重检查锁”,主要在getSingleton()方法中,进行两次null检查。这样可以极大提升并发度,进而提升性能。毕竟在单例中new的情况非常少,绝大多数都是可以并行的读操作,因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,也就提高了执行效率。但是必须注意的是volatile关键字,该关键字有两层语义。第一层语义是可见性,可见性是指在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以其它线程会马上读取到已修改的值,关于工作内存和主内存可简单理解为高速缓存(直接与CPU打交道)和主存(日常所说的内存条),注意工作内存是线程独享的,主存是线程共享的。volatile的第二层语义是禁止指令重排序优化,我们写的代码(特别是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同,这在单线程并没什么问题,然而一旦引入多线程环境,这种乱序就可能导致严重问题。volatile关键字就可以从语义上解决这个问题,值得关注的是volatile的禁止指令重排序优化功能在Java 1.5后才得以实现,因此1.5前的版本仍然是不安全的,即使使用了volatile关键字。或许我们可以利用静态内部类来实现更安全的机制,静态内部类单例模式如下:

/**
 * Created by wuzejian on 2017/5/9.
 * 静态内部类
 */
public class SingletonInner {
 private static class Holder {
 private static SingletonInner singleton = new SingletonInner();
 }

 private SingletonInner(){}

 public static SingletonInner getSingleton(){
 return Holder.singleton;
 }
}

正如上述代码所展示的,我们把Singleton实例放到一个静态内部类中,这样可以避免了静态实例在Singleton类的加载阶段(类加载过程的其中一个阶段的,此时只创建了Class对象,关于Class对象可以看博主另外一篇博文,深入理解Java类型信息(Class对象)与反射机制)就创建对象,毕竟静态变量初始化是在SingletonInner类初始化时触发的,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的。从上述4种单例模式的写法中,似乎也解决了效率与懒加载的问题,但是它们都有两个共同的缺点:

序列化可能会破坏单例模式,比较每次反序列化一个序列化的对象实例时都会创建一个新的实例,解决方案如下:

//测试例子(四种写解决方式雷同)
public class Singleton implements java.io.Serializable {
 public static Singleton INSTANCE = new Singleton(); 

 protected Singleton() {
 } 

 //反序列时直接返回当前INSTANCE
 private Object readResolve() {
  return INSTANCE;
 }
}

使用反射强行调用私有构造器,解决方式可以修改构造器,让它在创建第二个实例的时候抛异常,如下:

public static Singleton INSTANCE = new Singleton();
private static volatile boolean flag = true;
private Singleton(){
 if(flag){
 flag = false;
 }else{
 throw new RuntimeException("The instance already exists !");
 }
}

如上所述,问题确实也得到了解决,但问题是我们为此付出了不少努力,即添加了不少代码,还应该注意到如果单例类维持了其他对象的状态时还需要使他们成为transient的对象,这种就更复杂了,那有没有更简单更高效的呢?当然是有的,那就是枚举单例了,先来看看如何实现:

/**
 * 枚举单利
 */
public enum SingletonEnum {
 INSTANCE;
 private String name;
 public String getName(){
 return name;
 }
 public void setName(String name){
 this.name = name;
 }
}

代码相当简洁,我们也可以像常规类一样编写enum类,为其添加变量和方法,访问方式也更简单,使用SingletonEnum.INSTANCE进行访问,这样也就避免调用getInstance方法,更重要的是使用枚举单例的写法,我们完全不用考虑序列化和反射的问题。枚举序列化是由jvm保证的,每一个枚举类型和定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定:在序列化时Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的唯一性,这里我们不妨再次看看Enum类的valueOf方法:

public static <T extends Enum<T>> T valueOf(Class<T> enumType,
      String name) {
 T result = enumType.enumConstantDirectory().get(name);
 if (result != null)
  return result;
 if (name == null)
  throw new NullPointerException("Name is null");
 throw new IllegalArgumentException(
  "No enum constant " + enumType.getCanonicalName() + "." + name);
 }

实际上通过调用enumType(Class对象的引用)的enumConstantDirectory方法获取到的是一个Map集合,在该集合中存放了以枚举name为key和以枚举实例变量为value的Key&Value数据,因此通过name的值就可以获取到枚举实例,看看enumConstantDirectory方法源码:

Map<String, T> enumConstantDirectory() {
 if (enumConstantDirectory == null) {
  //getEnumConstantsShared最终通过反射调用枚举类的values方法
  T[] universe = getEnumConstantsShared();
  if (universe == null)
  throw new IllegalArgumentException(
   getName() + " is not an enum type");
  Map<String, T> m = new HashMap<>(2 * universe.length);
  //map存放了当前enum类的所有枚举实例变量,以name为key值
  for (T constant : universe)
  m.put(((Enum<?>)constant).name(), constant);
  enumConstantDirectory = m;
 }
 return enumConstantDirectory;
 }
 private volatile transient Map<String, T> enumConstantDirectory = null;

到这里我们也就可以看出枚举序列化确实不会重新创建新实例,jvm保证了每个枚举实例变量的唯一性。再来看看反射到底能不能创建枚举,下面试图通过反射获取构造器并创建枚举

public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
 //获取枚举类的构造函数(前面的源码已分析过)
 Constructor<SingletonEnum> constructor=SingletonEnum.class.getDeclaredConstructor(String.class,int.class);
 constructor.setAccessible(true);
 //创建枚举
 SingletonEnum singleton=constructor.newInstance("otherInstance",9);
 }

执行报错

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
 at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
 at zejian.SingletonEnum.main(SingletonEnum.java:38)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:498)
 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

显然告诉我们不能使用反射创建枚举类,这是为什么呢?不妨看看newInstance方法源码:

public T newInstance(Object ... initargs)
 throws InstantiationException, IllegalAccessException,
  IllegalArgumentException, InvocationTargetException
 {
 if (!override) {
  if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
  Class<?> caller = Reflection.getCallerClass();
  checkAccess(caller, clazz, null, modifiers);
  }
 }
 //这里判断Modifier.ENUM是不是枚举修饰符,如果是就抛异常
 if ((clazz.getModifiers() & Modifier.ENUM) != 0)
  throw new IllegalArgumentException("Cannot reflectively create enum objects");
 ConstructorAccessor ca = constructorAccessor; // read volatile
 if (ca == null) {
  ca = acquireConstructorAccessor();
 }
 @SuppressWarnings("unchecked")
 T inst = (T) ca.newInstance(initargs);
 return inst;
 }

源码很了然,确实无法使用反射创建枚举实例,也就是说明了创建枚举实例只有编译器能够做到而已。显然枚举单例模式确实是很不错的选择,因此我们推荐使用它。但是这总不是万能的,对于android平台这个可能未必是最好的选择,在android开发中,内存优化是个大块头,而使用枚举时占用的内存常常是静态变量的两倍还多,因此android官方在内存优化方面给出的建议是尽量避免在android中使用enum。但是不管如何,关于单例,我们总是应该记住:线程安全,延迟加载,序列化与反序列化安全,反射安全是很重重要的。

EnumMap

EnumMap基本用法

先思考这样一个问题,现在我们有一堆size大小相同而颜色不同的数据,需要统计出每种颜色的数量是多少以便将数据录入仓库,定义如下枚举用于表示颜色Color:

enum Color {
 GREEN,RED,BLUE,YELLOW
}
import java.util.*;

public class EnumMapDemo {
 public static void main(String[] args){
 List<Clothes> list = new ArrayList<>();
 list.add(new Clothes("C001",Color.BLUE));
 list.add(new Clothes("C002",Color.YELLOW));
 list.add(new Clothes("C003",Color.RED));
 list.add(new Clothes("C004",Color.GREEN));
 list.add(new Clothes("C005",Color.BLUE));
 list.add(new Clothes("C006",Color.BLUE));
 list.add(new Clothes("C007",Color.RED));
 list.add(new Clothes("C008",Color.YELLOW));
 list.add(new Clothes("C009",Color.YELLOW));
 list.add(new Clothes("C010",Color.GREEN));
 //方案1:使用HashMap
 Map<String,Integer> map = new HashMap<>();
 for (Clothes clothes:list){
  String colorName=clothes.getColor().name();
  Integer count = map.get(colorName);
  if(count!=null){
  map.put(colorName,count+1);
  }else {
  map.put(colorName,1);
  }
 }

 System.out.println(map.toString());

 System.out.println("---------------");

 //方案2:使用EnumMap
 Map<Color,Integer> enumMap=new EnumMap<>(Color.class);

 for (Clothes clothes:list){
  Color color=clothes.getColor();
  Integer count = enumMap.get(color);
  if(count!=null){
  enumMap.put(color,count+1);
  }else {
  enumMap.put(color,1);
  }
 }

 System.out.println(enumMap.toString());
 }

 /**
 输出结果:
 {RED=2, BLUE=3, YELLOW=3, GREEN=2}
 ---------------
 {GREEN=2, RED=2, BLUE=3, YELLOW=3}
 */
}

代码比较简单,我们使用两种解决方案,一种是HashMap,一种EnumMap,虽然都统计出了正确的结果,但是EnumMap作为枚举的专属的集合,我们没有理由再去使用HashMap,毕竟EnumMap要求其Key必须为Enum类型,因而使用Color枚举实例作为key是最恰当不过了,也避免了获取name的步骤,更重要的是EnumMap效率更高,因为其内部是通过数组实现的(稍后分析),注意EnumMap的key值不能为null,虽说是枚举专属集合,但其操作与一般的Map差不多,概括性来说EnumMap是专门为枚举类型量身定做的Map实现,虽然使用其它的Map(如HashMap)也能完成相同的功能,但是使用EnumMap会更加高效,它只能接收同一枚举类型的实例作为键值且不能为null,由于枚举类型实例的数量相对固定并且有限,所以EnumMap使用数组来存放与枚举类型对应的值,毕竟数组是一段连续的内存空间,根据程序局部性原理,效率会相当高。下面我们来进一步了解EnumMap的用法,先看构造函数:

//创建一个具有指定键类型的空枚举映射。
EnumMap(Class<K> keyType)
//创建一个其键类型与指定枚举映射相同的枚举映射,最初包含相同的映射关系(如果有的话)。
EnumMap(EnumMap<K,? extends V> m)
//创建一个枚举映射,从指定映射对其初始化。
EnumMap(Map<K,? extends V> m)

与HashMap不同,它需要传递一个类型信息,即Class对象,通过这个参数EnumMap就可以根据类型信息初始化其内部数据结构,另外两只是初始化时传入一个Map集合,代码演示如下:

//使用第一种构造
Map<Color,Integer> enumMap=new EnumMap<>(Color.class);
//使用第二种构造
Map<Color,Integer> enumMap2=new EnumMap<>(enumMap);
//使用第三种构造
Map<Color,Integer> hashMap = new HashMap<>();
hashMap.put(Color.GREEN, 2);
hashMap.put(Color.BLUE, 3);
Map<Color, Integer> enumMap = new EnumMap<>(hashMap);

至于EnumMap的方法,跟普通的map几乎没有区别,注意与HashMap的主要不同在于构造方法需要传递类型参数和EnumMap保证Key顺序与枚举中的顺序一致,但请记住Key不能为null。

EnumMap实现原理剖析

EnumMap的源码有700多行,这里我们主要分析其内部存储结构,添加查找的实现,了解这几点,对应EnumMap内部实现原理也就比较清晰了,先看数据结构和构造函数

public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
 implements java.io.Serializable, Cloneable
{
 //Class对象引用
 private final Class<K> keyType;

 //存储Key值的数组
 private transient K[] keyUniverse;

 //存储Value值的数组
 private transient Object[] vals;

 //map的size
 private transient int size = 0;

 //空map
 private static final Enum<?>[] ZERO_LENGTH_ENUM_ARRAY = new Enum<?>[0];

 //构造函数
 public EnumMap(Class<K> keyType) {
 this.keyType = keyType;
 keyUniverse = getKeyUniverse(keyType);
 vals = new Object[keyUniverse.length];
 }

}

EnumMap继承了AbstractMap类,因此EnumMap具备一般map的使用方法,keyType表示类型信息,keyUniverse表示键数组,存储的是所有可能的枚举值,vals数组表示键对应的值,size表示键值对个数。在构造函数中通过keyUniverse = getKeyUniverse(keyType);初始化了keyUniverse数组的值,内部存储的是所有可能的枚举值,接着初始化了存在Value值得数组vals,其大小与枚举实例的个数相同,getKeyUniverse方法实现如下

//返回枚举数组
private static <K extends Enum<K>> K[] getKeyUniverse(Class<K> keyType) {
 //最终调用到枚举类型的values方法,values方法返回所有可能的枚举值
 return SharedSecrets.getJavaLangAccess()
     .getEnumConstantsShared(keyType);
 }

从方法的返回值来看,返回类型是枚举数组,事实也是如此,最终返回值正是枚举类型的values方法的返回值,前面我们分析过values方法返回所有可能的枚举值,因此keyUniverse数组存储就是枚举类型的所有可能的枚举值。接着看put方法的实现

public V put(K key, V value) {
 typeCheck(key);//检测key的类型
 //获取存放value值得数组下标
 int index = key.ordinal();
 //获取旧值
 Object oldValue = vals[index];
 //设置value值
 vals[index] = maskNull(value);
 if (oldValue == null)
  size++;
 return unmaskNull(oldValue);//返回旧值
 }

这里通过typeCheck方法进行了key类型检测,判断是否为枚举类型,如果类型不对,会抛出异常

private void typeCheck(K key) {
 Class<?> keyClass = key.getClass();//获取类型信息
 if (keyClass != keyType && keyClass.getSuperclass() != keyType)
 throw new ClassCastException(keyClass + " != " + keyType);
}

接着通过int index = key.ordinal()的方式获取到该枚举实例的顺序值,利用此值作为下标,把值存储在vals数组对应下标的元素中即vals[index],这也是为什么EnumMap能维持与枚举实例相同存储顺序的原因,我们发现在对vals[]中元素进行赋值和返回旧值时分别调用了maskNull方法和unmaskNull方法

//代表NULL值得空对象实例
 private static final Object NULL = new Object() {
 public int hashCode() {
  return 0;
 }

 public String toString() {
  return "java.util.EnumMap.NULL";
 }
 };

 private Object maskNull(Object value) {
 //如果值为空,返回NULL对象,否则返回value
 return (value == null ? NULL : value);
 }

 @SuppressWarnings("unchecked")
 private V unmaskNull(Object value) {
 //将NULL对象转换为null值
 return (V)(value == NULL ? null : value);
 }

由此看来EnumMap还是允许存放null值的,但key绝对不能为null,对于null值,EnumMap进行了特殊处理,将其包装为NULL对象,毕竟vals[]存的是Object,maskNull方法和unmaskNull方法正是用于null的包装和解包装的。这就是EnumMap集合的添加过程。下面接着看获取方法

public V get(Object key) {
 return (isValidKey(key) ?
  unmaskNull(vals[((Enum<?>)key).ordinal()]) : null);
 }

 //对Key值的有效性和类型信息进行判断
 private boolean isValidKey(Object key) {
 if (key == null)
  return false;

 // Cheaper than instanceof Enum followed by getDeclaringClass
 Class<?> keyClass = key.getClass();
 return keyClass == keyType || keyClass.getSuperclass() == keyType;
 }

话,直接通过ordinal方法取索引,然后在值数组vals里通过索引获取值返回。remove方法如下:

public V remove(Object key) {
 //判断key值是否有效
 if (!isValidKey(key))
  return null;
 //直接获取索引
 int index = ((Enum<?>)key).ordinal();

 Object oldValue = vals[index];
 //对应下标元素值设置为null
 vals[index] = null;
 if (oldValue != null)
  size--;//减size
 return unmaskNull(oldValue);
 }

非常简单,key值有效,通过key获取下标索引值,把vals[]对应下标值设置为null,size减一。查看是否包含某个值,

判断是否包含某value
public boolean containsValue(Object value) {
 value = maskNull(value);
 //遍历数组实现
 for (Object val : vals)
 if (value.equals(val))
  return true;

 return false;
}
//判断是否包含key
public boolean containsKey(Object key) {
 return isValidKey(key) && vals[((Enum<?>)key).ordinal()] != null;
}

判断value直接通过遍历数组实现,而判断key就更简单了,判断key是否有效和对应vals[]中是否存在该值。ok~,这就是EnumMap的主要实现原理,即内部有两个数组,长度相同,一个表示所有可能的键(枚举值),一个表示对应的值,不允许keynull,但允许value为null,键都有一个对应的索引,根据索引直接访问和操作其键数组和值数组,由于操作都是数组,因此效率很高。

EnumSet

EnumSet是与枚举类型一起使用的专用 Set 集合,EnumSet 中所有元素都必须是枚举类型。与其他Set接口的实现类HashSet/TreeSet(内部都是用对应的HashMap/TreeMap实现的)不同的是,EnumSet在内部实现是位向量(稍后分析),它是一种极为高效的位运算操作,由于直接存储和操作都是bit,因此EnumSet空间和时间性能都十分可观,足以媲美传统上基于 int 的“位标志”的运算,重要的是我们可像操作set集合一般来操作位运算,这样使用代码更简单易懂同时又具备类型安全的优势。注意EnumSet不允许使用 null 元素。试图插入 null 元素将抛出 NullPointerException,但试图测试判断是否存在null 元素或移除 null 元素则不会抛出异常,与大多数collection 实现一样,EnumSet不是线程安全的,因此在多线程环境下应该注意数据同步问题,ok~,下面先来简单看看EnumSet的使用方式。

EnumSet用法

创建EnumSet并不能使用new关键字,因为它是个抽象类,而应该使用其提供的静态工厂方法,EnumSet的静态工厂方法比较多,如下:

创建一个具有指定元素类型的空EnumSet。
EnumSet<E> noneOf(Class<E> elementType)
//创建一个指定元素类型并包含所有枚举值的EnumSet
<E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType)
// 创建一个包括枚举值中指定范围元素的EnumSet
<E extends Enum<E>> EnumSet<E> range(E from, E to)
// 初始集合包括指定集合的补集
<E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s)
// 创建一个包括参数中所有元素的EnumSet
<E extends Enum<E>> EnumSet<E> of(E e)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5)
<E extends Enum<E>> EnumSet<E> of(E first, E... rest)
//创建一个包含参数容器中的所有元素的EnumSet
<E extends Enum<E>> EnumSet<E> copyOf(EnumSet<E> s)
<E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c)

代码演示如下:

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;

/**
 * Created by wuzejian on 2017/5/12.
 *
 */
enum Color {
 GREEN , RED , BLUE , BLACK , YELLOW
}

public class EnumSetDemo {

 public static void main(String[] args){

 //空集合
 EnumSet<Color> enumSet= EnumSet.noneOf(Color.class);
 System.out.println("添加前:"+enumSet.toString());
 enumSet.add(Color.GREEN);
 enumSet.add(Color.RED);
 enumSet.add(Color.BLACK);
 enumSet.add(Color.BLUE);
 enumSet.add(Color.YELLOW);
 System.out.println("添加后:"+enumSet.toString());

 System.out.println("-----------------------------------");

 //使用allOf创建包含所有枚举类型的enumSet,其内部根据Class对象初始化了所有枚举实例
 EnumSet<Color> enumSet1= EnumSet.allOf(Color.class);
 System.out.println("allOf直接填充:"+enumSet1.toString());

 System.out.println("-----------------------------------");

 //初始集合包括枚举值中指定范围的元素
 EnumSet<Color> enumSet2= EnumSet.range(Color.BLACK,Color.YELLOW);
 System.out.println("指定初始化范围:"+enumSet2.toString());

 System.out.println("-----------------------------------");

 //指定补集,也就是从全部枚举类型中去除参数集合中的元素,如下去掉上述enumSet2的元素
 EnumSet<Color> enumSet3= EnumSet.complementOf(enumSet2);
 System.out.println("指定补集:"+enumSet3.toString());

 System.out.println("-----------------------------------");

 //初始化时直接指定元素
 EnumSet<Color> enumSet4= EnumSet.of(Color.BLACK);
 System.out.println("指定Color.BLACK元素:"+enumSet4.toString());
 EnumSet<Color> enumSet5= EnumSet.of(Color.BLACK,Color.GREEN);
 System.out.println("指定Color.BLACK和Color.GREEN元素:"+enumSet5.toString());

 System.out.println("-----------------------------------");

 //复制enumSet5容器的数据作为初始化数据
 EnumSet<Color> enumSet6= EnumSet.copyOf(enumSet5);
 System.out.println("enumSet6:"+enumSet6.toString());

 System.out.println("-----------------------------------");

 List<Color> list = new ArrayList<Color>();
 list.add(Color.BLACK);
 list.add(Color.BLACK);//重复元素
 list.add(Color.RED);
 list.add(Color.BLUE);
 System.out.println("list:"+list.toString());

 //使用copyOf(Collection<E> c)
 EnumSet enumSet7=EnumSet.copyOf(list);
 System.out.println("enumSet7:"+enumSet7.toString());

 /**
  输出结果:
  添加前:[]
  添加后:[GREEN, RED, BLUE, BLACK, YELLOW]
  -----------------------------------
  allOf直接填充:[GREEN, RED, BLUE, BLACK, YELLOW]
  -----------------------------------
  指定初始化范围:[BLACK, YELLOW]
  -----------------------------------
  指定补集:[GREEN, RED, BLUE]
  -----------------------------------
  指定Color.BLACK元素:[BLACK]
  指定Color.BLACK和Color.GREEN元素:[GREEN, BLACK]
  -----------------------------------
  enumSet6:[GREEN, BLACK]
  -----------------------------------
  list:[BLACK, BLACK, RED, BLUE]
  enumSet7:[RED, BLUE, BLACK]
  */
 }

}

到此这篇关于秒懂Java枚举类型(enum)的文章就介绍到这了,更多相关Java枚举类型内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入谈谈java的枚举(enum)类型

    前言 在大家日常编程中,往往存在着这样的"数据集",它们的数值在程序中是稳定的,而且"数据集"中的元素是有限的.例如星期一到星期日七个数据元素组成了一周的"数据集",春夏秋冬四个数据元素组成了四季的"数据集".在java中如何更好的使用这些"数据集"呢?因此枚举便派上了用场 枚举其实就是一种类型,跟int, char 这种差不多,就是定义变量时限制输入的,你只能够赋enum里面规定的值. 枚举(enum)实

  • 三分钟快速掌握Java中枚举(enum)

    什么是枚举? 枚举是JDK5引入的新特性.在某些情况下,一个类的对象是固定的,就可以定义为枚举.在实际使用中,枚举类型也可以作为一种规范,保障程序参数安全.枚举有以下特点: Java中枚举和类.接口的级别相同. 枚举和类一样,都有自己的属性.方法.构造方法,不同点是:枚举的构造方法只能是private修饰,也就无法从外部构造对象.构造方法只在构造枚举值时调用. 使用enum关键字声明一个枚举类型时,就默认继承自Java中的 java.lang.Enum类,并实现了java.lang.Seriab

  • 全面解读Java中的枚举类型enum的使用

    关于枚举 大多数地方写的枚举都是给一个枚举然后例子就开始switch,可是我想说,我代码里头来源的数据不太可能就是枚举,通常是字符串或数字,比如一个SQL我解析后首先判定SQL类型,通过截取SQL的token,截取出来可能是SELECT.DELETE.UPDATE.INSERT.ALTER等等,但是都是字符串,此时我想用枚举就不行了,我要将字符串转换成枚举怎么转呢,类似的情况还有从数据库取出数据根据一些类型做判定,从页面传入数据,根据不同的类型做不同的操作,但是都是字符串,不是枚举,悲剧的是我很

  • Java枚举类型enum的详解及使用

     Java枚举类型enum的详解及使用 最近跟同事讨论问题的时候,突然同事提到我们为什么Java 中定义的常量值不采用enmu 枚举类型,而采用public final static 类型来定义呢?以前我们都是采用这种方式定义的,很少采用enum 定义,所以也都没有注意过,面对突入起来的问题,还真有点不太清楚为什么有这样的定义.既然不明白就抽时间研究下吧. Java 中的枚举类型采用关键字enum 来定义,从jdk1.5才有的新类型,所有的枚举类型都是继承自Enum 类型.要了解枚举类型,建议大

  • Java(enum)枚举用法详解

    概念 enum的全称为 enumeration, 是 JDK 1.5 中引入的新特性. 在Java中,被 enum 关键字修饰的类型就是枚举类型.形式如下: enum Color { RED, GREEN, BLUE } 如果枚举不添加任何方法,枚举值默认为从0开始的有序数值.以 Color 枚举类型举例,它的枚举常量依次为RED:0,GREEN:1,BLUE:2 枚举的好处:可以将常量组织起来,统一进行管理. 枚举的典型应用场景:错误码.状态机等. 枚举类型的本质 尽管enum 看起来像是一种

  • 秒懂Java枚举类型(enum)

    理解枚举类型 枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性.安全性以及便捷性.下面先来看看什么是枚举?如何定义枚举? 枚举的定义 上述的常量定义常量的方式称为int枚举模式,这样的定义方式并没有什么错,但它存在许多不足,如在类型安全和使用方便性上并没有多少好处,如果存在定义int值相同的变量,混淆的几率还是很大的,编译器也不会提出任何警告,因此这种方式在枚举

  • java枚举类型-Enum

    目录 前言 应用 定义 基本Enum特性 Enum的静态导入 Enum中添加新方法 Switch语句中的Enum Enum的继承 EnumSet的使用 EnumMap的使用 常量相关方法 枚举值向枚举类型转换 前言 枚举是 Java1.5 引入的新特性,通过关键字 enum 来定义枚举类. 应用 定义 关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用. enum WeekEnum { Monday, Tuesday, Wednesday, T

  • 一文搞懂JAVA 枚举(enum)

    Java 枚举是一个特殊的类,一般表示一组常量,比如一年的 4 个季节,一个年的 12 个月份,一个星期的 7 天,方向有东南西北等. Java 枚举类使用 enum 关键字来定义,各个常量使用逗号 , 来分割. 例如定义一个颜色的枚举类. enum Color { RED, GREEN, BLUE; } 以上枚举类 Color 颜色常量有 RED, GREEN, BLUE,分别表示红色,绿色,蓝色. 使用实例: enum Color { RED, GREEN, BLUE; } public c

  • C++枚举类型enum与enum class的使用

    一.关于枚举类型 1. 什么是枚举类型? 答:如果一个变量只有几种可能的值,那么就可以定义为枚举类型,比如:性别只有男和女,那么就可以将性别定义为一种枚举类型,其中男和女就是性别所包含的变量.所谓"枚举"是指将变量的值一一列举出来,变量的值只能在列举出来的值的范围内.在C++中,枚举类型分为不限定作用域(enum)和限定作用域(enum class). 2. enum与enum class的区别? (为什么需要限定作用域?) 答:枚举作用域是指枚举类型成员名字的作用域,起自其声明之处,

  • c语言枚举类型enum的用法及应用实例

    目录 前言 一.枚举的概念 二.枚举的几种用法 1.直接定义枚举值,然后给普通变量赋值. 2.定义带名称的枚举 三.定义枚举别名 四.枚举有什么用,用在哪里? 最后总结: 前言 今天跟大家讲一下我在产品开发时,用枚举(enum)的一些骚操作. 我跟你保证,今天的内容绝对是你在书本和视频学不到的. 为什么要讲枚举呢? 因为我发现它是一个容易被遗忘,同时又非常重要的关键词,大家如果看那些大佬写的程序,真的会发现有非常非常多enum的应用. 让我惊奇的是市面上的一些C语言书籍以及视频课程并没有重视去讲

  • C#中的枚举类型(Enum)介绍

    枚举(Enum)定义 enum关键字用于声明枚举,即一种由一组称为枚举数列表的命名常量组成的独特类型.通常情况下,最好是在命名空间内直接定义枚举,以便该命名空间中的所有类都能够同样方便地访问它.但是,还可以将枚举嵌套在类或结构中.现在的有些电商网站根据购物的积分用到的,金牌会员,银牌会员,铜牌会员. enum MemberLevel { gold, silver, copper } 枚举值获取 一般获取的时候包括获取变量和变量值,默认情况下,第一个枚举数的值为 0,后面每个枚举数的值依次递增 1

  • Java枚举类enum介绍

    枚举类enum是JDK1.5引入的,之前都是用public static final int enum_value来代替枚举类的.枚举类enum是一种特殊的类,它默认继承了类java.lang.Enum.和其它普通类一样,enum同样可以有成员变量.方法.构造器,也可以实现一个或多个接口,区别是: 1.如果有构造器,必须用private修饰. 2.枚举类不能派生子类. 3.枚举类所有的实例必须在第一行显示定义.系统会自动给这些实例加上public static final修饰,无须程序员显示定义

  • Java枚举类型与泛型使用解读

    目录 一.枚举类型 1.使用枚举类型设置常量 2.枚举类型常用方法 3.枚举类型中的构造方法 二.泛型 1.泛型的常规用法 2.泛型的高级用法 总结 一.枚举类型 1.使用枚举类型设置常量 以往设置常量,通常将常量放置在接口中,这样在程序中就可以直接使用,并且该常量不能被修改,因为在接口中定义常量时,该常量的修饰符为final与static. 常规定义常量的代码如下所示. 例:在接口中定义常量的常规方式 public interface Constants{ public static fina

随机推荐