Java进阶知识点1:白捡的扩展性 - 枚举值也是对象
一、背景
枚举经常被大家用来储存一组有限个数的候选常量。比如下面定义了一组常见数据库类型:
public enum DatabaseType { MYSQL, ORACLE, SQLSERVER }
当围绕这一组常量出现功能上的扩展点时,很多人的做法是为新的功能编写一个新类,新类中依赖该枚举类型。
比如要在界面上显示常见数据库类型的官方名称,可以用如下类实现这一功能:
public class DatabaseNameParser { public String getDatabaseName(DatabaseType databaseType) { if (DatabaseType.ORACLE.equals(databaseType)) { return "Oracle数据库"; } else if (DatabaseType.MYSQL.equals(databaseType)) { return "MySQL数据库"; } else if (DatabaseType.SQLSERVER.equals(databaseType)) { return "SQL Server数据库"; } else { throw new RuntimeException("不支持的数据库类型。"); } } public static void main(String[] args) { System.out.println(new DatabaseNameParser().getDatabaseName(DatabaseType.MYSQL)); } }
大量的if - else语句以及对其他类的过渡依赖(几乎每两行代码就会引用一次DatabaseType对象),让上面这段代码散发出浓浓的坏味道,你可能会想如果这些逻辑可以整合在枚举类中实现就好了。答案是当然可以。
我们只需意识到枚举值不只是常量值,枚举值也是一种对象,他几乎拥有面向对象编程的绝大部分功能 -- 封装、多态、继承(不支持,但一定程度上可以模拟类似效果)。
二、用枚举进行面向对象编程
2.1 封装
上面的例子中,数据库类型的官方名称应该封装到每个数据库类型枚举对象中,作为一个属性字段,每个枚举值提供get方法即可直接通过枚举对象获取其对应的官方名称。如下:
public enum DatabaseType { MYSQL("MySQL数据库"), ORACLE("Oracle数据库"), SQLSERVER("SQL Server数据库"); private final String name; DatabaseType(String name) { this.name = name; } public String getName() { return name; } public static void main(String[] args) { System.out.println(MYSQL.getName()); } }
2.2 多态
上面的例子中,比如新增一个需求,要在界面上提供数据库的连接检测功能。而不同数据库类型的连接检测逻辑是不同的。此时可以为枚举对象声明一个抽象方法,每个枚举值负责实现此方法。如下:
public enum DatabaseType { MYSQL { @Override public boolean detect(String ip, int port) { //为了简化举例环境,此处不真正实现功能 return false; } }, ORACLE { @Override public boolean detect(String ip, int port) { return false; } }, SQLSERVER { @Override public boolean detect(String ip, int port) { return false; } }; public abstract boolean detect(String ip, int port); public static void main(String[] args) { System.out.println(MYSQL.detect("127.0.0.1", 3306)); } }
这一特性也常被称之为:定义常量相关方法。
2.3 继承
枚举对象均是final对象,故不支持枚举对象的继承。但我们可以通过实现接口来模拟枚举类型的子类化,这也是子类化枚举类型的唯一方法。如下:
interface DatabaseType { public boolean detect(String ip, int port); enum RDBMS implements DatabaseType { MYSQL { @Override public boolean detect(String ip, int port) { return false; } }, ORACLE { @Override public boolean detect(String ip, int port) { return false; } }, SQLSERVER { @Override public boolean detect(String ip, int port) { return false; } } } enum NOSQL implements DatabaseType { REDIS { @Override public boolean detect(String ip, int port) { return false; } }, HBASE { @Override public boolean detect(String ip, int port) { return false; } }, MONGODB { @Override public boolean detect(String ip, int port) { return false; } } } }
这就模拟了DatabaseType派生出RDBMS和NOSQL两个抽象子类,每个子类下又派生出各自的具体实现类,这种继承结构。
三、用枚举更好地实现设计模式
当枚举值被赋予对象的强大能力后,再结合枚举类型本身的易用性,被聪明的Java开发者们发现了如下两类好用的模式:
3.1 使用枚举定义单例
—— 这是JDK5之后定义单例最好的方法,没有之一。
说起单例模式,常常让我们想起很多技巧或问题:
1、延迟初始化单例要Double Check,Double Check会失效需要使用volatile修饰符;
2、可以使用静态变量初始化单例;
3、单例对象可能可以被多次反序列化,就违背了单例模式的要求了;
... ...
现在不用纠结这些过时的套路了,就按如下方式定义单例,任何已知的问题都没有,一行代码搞定,而且引用起来更紧凑简洁。
public enum Singleton { INSTANCE; public void anyFunction() { } public static void main(String[] args) { Singleton.INSTANCE.anyFunction(); } }
3.2 使用枚举实现责任链或状态机
public enum StateMachine { BEGIN { @Override public StateMachine handleAndReturnNextState() { System.out.println("begin"); return PROCESS1; } }, PROCESS1 { @Override public StateMachine handleAndReturnNextState() { System.out.println("process1"); if (true/*some condition*/) { return PROCESS2; } return END; } }, PROCESS2 { @Override public StateMachine handleAndReturnNextState() { System.out.println("process2"); return END; } }, END { @Override public StateMachine handleAndReturnNextState() { System.out.println("end"); return null; } }; public abstract StateMachine handleAndReturnNextState(); public static void main(String[] args) { StateMachine state = BEGIN; while (state != null) { state = state.handleAndReturnNextState(); } } }
四、总结
1、枚举不只是常量,也是对象,当与枚举相关的功能遇到扩展需求时,可以考虑在枚举对象上扩展功能点,以获得更加简洁紧凑的代码。
2、枚举是实现单例的最好方式。
3、枚举可以用来实现状态机。