Java枚举——高级用法与深入解读

}

//比较的是ordinal值

public final int compareTo(E o) {

Enum<?> other = (Enum<?>)o;

Enum 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 getDeclaringClass() {

//获取class对象引用,getClass()是Object的方法

Class<?> clazz = getClass();

//获取父类Class对象引用

Class<?> zuper = clazz.getSuperclass();

return (zuper == Enum.class) ? (Class)clazz : (Class)zuper;

}

public static <T extends Enum> T valueOf(Class 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对象中存在如下方法:

在这里插入图片描述

因此通过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方法,我们仍能一次性获取所有的枚举实例常量。

三、枚举与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引用。

四、枚举进阶

=====================================================================

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

1、向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的定义结束,我们也永远无法手动调用构造函数创建枚举实例,毕竟这事只能由编译器执行。

2、覆盖enum类方法


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:星期日

*/

}

3、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){ }

4、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;

}

}

}

五、枚举与单例模式

========================================================================

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

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

public class SingletonHungry {

private static SingletonHungry instance = new SingletonHungry();

private SingletonHungry() {

}

public static SingletonHungry getInstance() {

return instance;

}

}

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

//懒汉式单例模式(适合多线程安全)

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关键字。或许我们可以利用静态内部类来实现更安全的机制,静态内部类单例模式如下:

//静态内部类

public class SingletonInner {

private static class Holder {

private static SingletonInner singleton = new SingletonInner();

}

private SingletonInner(){}

public static SingletonInner getSingleton(){

return Holder.singleton;

}

}

正如上述代码所展示的,我们把Singleton实例放到一个静态内部类中,这样可以避免了静态实例在Singleton类的加载阶段就创建对象,毕竟静态变量初始化是在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;

读者福利

由于篇幅过长,就不展示所有面试题了,感兴趣的小伙伴

35K成功入职:蚂蚁金服面试Java后端经历!「含面试题+答案」

35K成功入职:蚂蚁金服面试Java后端经历!「含面试题+答案」

35K成功入职:蚂蚁金服面试Java后端经历!「含面试题+答案」

更多笔记分享

35K成功入职:蚂蚁金服面试Java后端经历!「含面试题+答案」

加入社区:https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0
ava.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;

读者福利

由于篇幅过长,就不展示所有面试题了,感兴趣的小伙伴

[外链图片转存中…(img-YdPMCdLJ-1725669934120)]

[外链图片转存中…(img-jGTJlVU0-1725669934121)]

[外链图片转存中…(img-qLCZNiZL-1725669934121)]

更多笔记分享

[外链图片转存中…(img-g4PDjYIy-1725669934122)]

加入社区:https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值