Java枚举详解
文章目录
- 1. 引言
- 1.1 什么是枚举
- 1.2 为什么需要枚举
- 1.3 枚举的优势
- 2. 枚举基础
- 2.1 枚举的声明与使用
- 基本声明
- 在类中定义枚举
- 枚举的基本使用
- 2.2 枚举的常用方法
- 1. values()
- 2. valueOf(String name)
- 3. name()
- 4. ordinal()
- 5. toString()
- 6. compareTo(E o)
- 7. equals(Object other)
- 2.3 枚举与switch语句
- 3. 自定义枚举
- 3.1 带有字段的枚举
- 添加字段和构造函数
- 枚举构造函数的特点
- 3.2 枚举中的方法
- 添加实例方法
- 重写toString()方法
- 添加静态方法
- 3.3 枚举中的抽象方法
- 4. 枚举与接口
- 4.1 枚举实现接口
- 4.2 枚举与策略模式
- 5. 枚举的高级用法
- 5.1 EnumSet
- 创建EnumSet
- EnumSet的常用操作
- EnumSet的优势
- 5.2 EnumMap
- 创建EnumMap
- EnumMap的常用操作
- EnumMap的优势
- 5.3 枚举的序列化
- 6. 枚举的最佳实践
- 6.1 命名约定
- 6.2 何时使用枚举
- 6.3 枚举设计技巧
- 保持简单
- 使用私有构造函数
- 避免使用ordinal()
- 考虑使用EnumSet和EnumMap
- 优化switch语句
- 6.4 常见陷阱和注意事项
- 枚举的序列化考虑
- 避免在枚举中使用可变状态
- 避免过于复杂的枚举实现
- 7. 枚举的实际应用
- 7.1 状态机实现
- 7.2 单例模式实现
- 7.3 命令模式实现
- 7.4 策略模式实现
- 8. 总结
- 8.1 枚举的主要特点
- 8.2 枚举与设计模式
- 8.3 枚举的适用场景
1. 引言
1.1 什么是枚举
枚举(Enum)是Java 5(JDK 1.5)引入的一种特殊的数据类型,它允许变量成为一组预定义的常量。这些常量通常以大写字母表示,并且在Java程序中可以作为常规的值来使用。使用枚举可以更清晰地定义某些特定的值,并且保证这些值在编译时就已经固定下来了。
枚举类型的声明与类的声明类似,但是使用enum
关键字而不是class
关键字。枚举可以单独定义在一个文件中,也可以嵌套在另一个类中。
// 基本的枚举声明
public enum Season {SPRING, SUMMER, AUTUMN, WINTER
}
在这个简单的例子中,Season
是一个枚举类型,它有四个可能的值:SPRING
、SUMMER
、AUTUMN
和WINTER
。每个枚举常量都是Season
类型的实例。
1.2 为什么需要枚举
在Java 5引入枚举之前,表示一组固定常量的常见方式是使用接口或类中的public static final
字段。例如:
public class SeasonConstants {public static final int SPRING = 0;public static final int SUMMER = 1;public static final int AUTUMN = 2;public static final int WINTER = 3;
}
虽然这种方法可以工作,但它有几个严重的缺点:
-
类型不安全:这些常量只是整数值,可以轻松地与其他整数混淆。例如,你可以将一个表示操作码的整数常量误用为季节常量。
-
没有命名空间:除非使用长且可能笨拙的名称,否则常量名称会污染命名空间。
-
打印困难:当你打印一个整数常量时,你只看到一个数字,而不是有意义的名称。
-
编译时不检查:如果你添加、移除或重新排序常量,使用这些常量的代码可能会悄悄地失效。
枚举解决了所有这些问题,并提供了更多的功能:
public enum Season {SPRING, SUMMER, AUTUMN, WINTER
}// 使用示例
public class EnumTest {public static void main(String[] args) {Season season = Season.SPRING;System.out.println(season); // 输出:SPRING}
}
1.3 枚举的优势
使用Java枚举有以下几个主要优势:
- 类型安全:枚举提供了编译时类型安全。你不能将一个枚举类型赋值给另一个枚举类型,也不能使用非枚举值作为枚举类型。
Season season = Season.SPRING; // 有效
// Season season = 0; // 编译错误:不兼容的类型
// Season season = "SPRING"; // 编译错误:不兼容的类型
// Season season = Day.MONDAY; // 编译错误:不兼容的类型
- 命名空间:枚举常量位于其枚举类型的命名空间内,避免了命名冲突。
public enum Season { SPRING, SUMMER, AUTUMN, WINTER }
public enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY }// 使用时,命名空间清晰
Season season = Season.SPRING;
Day day = Day.MONDAY;
- 可读性:枚举值在打印时使用其名称,而不是一个可能没有意义的数字。
Season season = Season.SPRING;
System.out.println(season); // 输出:SPRING,而不是0或其他数字
- 内置方法:Java枚举自带了许多有用的方法,如
values()
(返回所有枚举常量的数组)和valueOf(String)
(将字符串转换为枚举常量)。
// 获取所有枚举常量
Season[] allSeasons = Season.values();
for (Season s : allSeasons) {System.out.println(s);
}// 字符串转换为枚举常量
Season summer = Season.valueOf("SUMMER");
System.out.println(summer); // 输出:SUMMER
- 可扩展性:枚举可以有构造函数、字段、方法和实现接口,这使它们比简单的常量声明更强大。
public enum Season {SPRING("温暖"),SUMMER("炎热"),AUTUMN("凉爽"),WINTER("寒冷");private final String description;Season(String description) {this.description = description;}public String getDescription() {return description;}
}// 使用扩展的枚举
Season summer = Season.SUMMER;
System.out.println(summer.getDescription()); // 输出:炎热
- 集合支持:枚举可以很容易地与Java集合框架(如EnumSet和EnumMap)一起使用,这些专为枚举设计的集合比一般的集合更高效。
// 使用EnumSet
EnumSet<Season> allSeasons = EnumSet.allOf(Season.class);
EnumSet<Season> warmSeasons = EnumSet.of(Season.SPRING, Season.SUMMER);// 使用EnumMap
EnumMap<Season, String> seasonActivities = new EnumMap<>(Season.class);
seasonActivities.put(Season.SPRING, "赏花");
seasonActivities.put(Season.SUMMER, "游泳");
seasonActivities.put(Season.AUTUMN, "赏枫");
seasonActivities.put(Season.WINTER, "滑雪");
通过以上优势,枚举为表示固定集合的常量提供了一种更安全、更灵活、更有表现力的方式。
2. 枚举基础
2.1 枚举的声明与使用
基本声明
定义枚举的基本语法如下:
public enum 枚举名 {常量1, 常量2, ..., 常量n
}
例如,定义一个表示星期几的枚举:
public enum Day {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
在类中定义枚举
枚举也可以定义在类的内部,作为该类的一个成员:
public class Calendar {public enum Day {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY}private Day today;public Calendar(Day today) {this.today = today;}public Day getToday() {return today;}
}// 使用内部枚举
Calendar calendar = new Calendar(Calendar.Day.MONDAY);
Calendar.Day today = calendar.getToday();
枚举的基本使用
使用枚举常量非常简单,只需通过枚举类型名称和点号访问:
public class EnumDemo {public static void main(String[] args) {// 使用枚举常量Day today = Day.MONDAY;// switch语句中使用枚举switch (today) {case MONDAY:System.out.println("星期一,工作日的开始");break;case TUESDAY:case WEDNESDAY:case THURSDAY:System.out.println("工作日");break;case FRIDAY:System.out.println("星期五,周末前夕");break;case SATURDAY:case SUNDAY:System.out.println("周末");break;}}
}
2.2 枚举的常用方法
所有枚举类型都隐式继承自java.lang.Enum
抽象类,该类提供了一些有用的方法。以下是最常用的方法:
1. values()
values()
方法返回一个包含所有枚举常量的数组,按照它们声明的顺序。
public class EnumMethodsDemo {public static void main(String[] args) {// 获取所有枚举常量Season[] seasons = Season.values();// 遍历所有枚举常量for (Season season : seasons) {System.out.println(season);}}
}
输出:
SPRING
SUMMER
AUTUMN
WINTER
2. valueOf(String name)
valueOf(String name)
方法返回带指定名称的枚举常量。如果没有找到匹配的常量,它会抛出IllegalArgumentException
。
public class EnumMethodsDemo {public static void main(String[] args) {// 字符串转换为枚举常量Season summer = Season.valueOf("SUMMER");System.out.println(summer);try {// 不存在的枚举常量名称Season unknown = Season.valueOf("SPRING_FESTIVAL");} catch (IllegalArgumentException e) {System.out.println("没有找到名为SPRING_FESTIVAL的Season枚举常量");}}
}
输出:
SUMMER
没有找到名为SPRING_FESTIVAL的Season枚举常量
3. name()
name()
方法返回此枚举常量的名称,与声明时完全相同。
public class EnumMethodsDemo {public static void main(String[] args) {Season spring = Season.SPRING;System.out.println("枚举常量的名称:" + spring.name());}
}
输出:
枚举常量的名称:SPRING
4. ordinal()
ordinal()
方法返回枚举常量的序数(位置),从0开始。
public class EnumMethodsDemo {public static void main(String[] args) {System.out.println(Season.SPRING.ordinal()); // 0System.out.println(Season.SUMMER.ordinal()); // 1System.out.println(Season.AUTUMN.ordinal()); // 2System.out.println(Season.WINTER.ordinal()); // 3}
}
5. toString()
toString()
方法返回枚举常量的名称,默认实现与name()
相同,但可以被重写以提供不同的字符串表示。
public class EnumMethodsDemo {public static void main(String[] args) {Season spring = Season.SPRING;System.out.println("默认的toString()输出:" + spring.toString());}
}
输出:
默认的toString()输出:SPRING
6. compareTo(E o)
compareTo(E o)
方法比较此枚举与指定对象的顺序,基于它们的序数值。
public class EnumMethodsDemo {public static void main(String[] args) {Season spring = Season.SPRING;Season winter = Season.WINTER;// 比较枚举常量的顺序int comparison = spring.compareTo(winter);if (comparison < 0) {System.out.println(spring + "在" + winter + "之前");} else if (comparison > 0) {System.out.println(spring + "在" + winter + "之后");} else {System.out.println(spring + "和" + winter + "是同一个常量");}}
}
输出:
SPRING在WINTER之前
7. equals(Object other)
equals(Object other)
方法检查指定的对象是否等于此枚举常量。
public class EnumMethodsDemo {public static void main(String[] args) {Season spring1 = Season.SPRING;Season spring2 = Season.SPRING;Season summer = Season.SUMMER;System.out.println("spring1.equals(spring2): " + spring1.equals(spring2));System.out.println("spring1.equals(summer): " + spring1.equals(summer));System.out.println("spring1 == spring2: " + (spring1 == spring2));}
}
输出:
spring1.equals(spring2): true
spring1.equals(summer): false
spring1 == spring2: true
2.3 枚举与switch语句
枚举在switch
语句中的使用特别方便。Java的switch
语句可以直接使用枚举常量,而无需指定枚举类型名称。
public class EnumSwitchDemo {public static void main(String[] args) {Season currentSeason = Season.SUMMER;switch (currentSeason) {case SPRING:System.out.println("春天,万物复苏的季节。");break;case SUMMER:System.out.println("夏天,炎热的季节。");break;case AUTUMN:System.out.println("秋天,收获的季节。");break;case WINTER:System.out.println("冬天,寒冷的季节。");break;}}
}
输出:
夏天,炎热的季节。
注意在switch
语句的case
子句中,我们直接使用SPRING
、SUMMER
等,而不是Season.SPRING
、Season.SUMMER
。这是因为switch
语句的表达式已经指定了枚举类型(Season currentSeason
),所以Java编译器知道这些常量属于Season
枚举。
3. 自定义枚举
3.1 带有字段的枚举
枚举不仅仅可以是简单的常量列表,还可以包含字段、构造函数和方法,就像普通的类一样。这使得枚举类型更加强大和灵活。
添加字段和构造函数
要为枚举添加字段,我们需要:
- 声明实例变量
- 创建构造函数
- 提供访问字段的方法
- 在枚举常量声明中传入参数
public enum Planet {MERCURY(3.303e+23, 2.4397e6),VENUS(4.869e+24, 6.0518e6),EARTH(5.976e+24, 6.37814e6),MARS(6.421e+23, 3.3972e6),JUPITER(1.9e+27, 7.1492e7),SATURN(5.688e+26, 6.0268e7),URANUS(8.686e+25, 2.5559e7),NEPTUNE(1.024e+26, 2.4746e7);private final double mass; // 质量,单位:千克private final double radius; // 半径,单位:米// 构造函数Planet(double mass, double radius) {this.mass = mass;this.radius = radius;}// 获取质量public double getMass() {return mass;}// 获取半径public double getRadius() {return radius;}// 计算表面重力public double surfaceGravity() {double G = 6.67300E-11; // 重力常数return G * mass / (radius * radius);}// 计算表面重量public double surfaceWeight(double otherMass) {return otherMass * surfaceGravity();}
}
使用带有字段的枚举:
public class PlanetDemo {public static void main(String[] args) {double earthWeight = 70.0; // 地球上的重量,单位:千克double mass = earthWeight / Planet.EARTH.surfaceGravity();for (Planet planet : Planet.values()) {System.out.printf("在%s上,一个重量为%.1f千克的物体的重量为%.1f千克。%n",planet, earthWeight, planet.surfaceWeight(mass));}}
}
输出:
在MERCURY上,一个重量为70.0千克的物体的重量为26.4千克。
在VENUS上,一个重量为70.0千克的物体的重量为63.4千克。
在EARTH上,一个重量为70.0千克的物体的重量为70.0千克。
在MARS上,一个重量为70.0千克的物体的重量为26.5千克。
在JUPITER上,一个重量为70.0千克的物体的重量为166.8千克。
在SATURN上,一个重量为70.0千克的物体的重量为74.4千克。
在URANUS上,一个重量为70.0千克的物体的重量为63.7千克。
在NEPTUNE上,一个重量为70.0千克的物体的重量为80.5千克。
枚举构造函数的特点
枚举的构造函数有一些特殊的规则:
- 构造函数总是私有的,即使你声明为
public
或protected
,Java也会自动将其视为private
。 - 枚举常量必须在任何字段或方法之前定义。
- 如果枚举声明中包含字段或方法,则枚举常量列表必须以分号结束。
// 错误:构造函数不能是public或protected
public enum Wrong {A, B;public Wrong() { // 编译错误:修饰符 'public' 对枚举构造函数无效// ...}
}// 错误:常量必须在字段和方法之前
public enum WrongOrder {private String name; // 编译错误:期望枚举常量WrongOrder(String name) {this.name = name;}A("a"), B("b"); // 编译错误:字段和方法必须在常量之后
}// 正确的声明
public enum Correct {A("a"), B("b"); // 注意这里的分号private final String name;Correct(String name) {this.name = name;}public String getName() {return name;}
}
3.2 枚举中的方法
除了字段和构造函数外,枚举还可以包含方法定义,包括静态方法、实例方法,甚至可以重写父类方法。
添加实例方法
public enum Day {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;// 判断是否为工作日public boolean isWeekday() {return this != SATURDAY && this != SUNDAY;}// 判断是否为周末public boolean isWeekend() {return this == SATURDAY || this == SUNDAY;}// 获取下一天public Day next() {// 使用(ordinal() + 1) % values().length获取下一个枚举常量的索引// 通过values()[index]获取对应的枚举常量return values()[(ordinal() + 1) % values().length];}
}
使用带有方法的枚举:
public class DayDemo {public static void main(String[] args) {Day today = Day.FRIDAY;System.out.println(today + "是工作日吗?" + today.isWeekday());System.out.println(today + "是周末吗?" + today.isWeekend());System.out.println(today + "的下一天是" + today.next());Day saturday = Day.SATURDAY;System.out.println(saturday + "是工作日吗?" + saturday.isWeekday());System.out.println(saturday + "是周末吗?" + saturday.isWeekend());}
}
输出:
FRIDAY是工作日吗?true
FRIDAY是周末吗?false
FRIDAY的下一天是SATURDAY
SATURDAY是工作日吗?false
SATURDAY是周末吗?true
重写toString()方法
默认情况下,枚举的toString()
方法返回枚举常量的名称,但你可以重写它以提供不同的字符串表示:
public enum Season {SPRING("春天"),SUMMER("夏天"),AUTUMN("秋天"),WINTER("冬天");private final String chineseName;Season(String chineseName) {this.chineseName = chineseName;}@Overridepublic String toString() {return chineseName;}
}
使用重写了toString()
的枚举:
public class SeasonToStringDemo {public static void main(String[] args) {for (Season season : Season.values()) {// 直接打印枚举常量会调用toString()方法System.out.println(season);}// 如果还需要获取枚举常量的名称,可以使用name()方法System.out.println(Season.SPRING.name() + ": " + Season.SPRING);}
}
输出:
春天
夏天
秋天
冬天
SPRING: 春天
添加静态方法
枚举也可以包含静态方法,这些方法与枚举类型有关,而不是与特定的枚举常量有关:
public enum Operation {PLUS("+") {@Overridepublic double apply(double x, double y) {return x + y;}},MINUS("-") {@Overridepublic double apply(double x, double y) {return x - y;}},TIMES("*") {@Overridepublic double apply(double x, double y) {return x * y;}},DIVIDE("/") {@Overridepublic double apply(double x, double y) {return x / y;}};private final String symbol;Operation(String symbol) {this.symbol = symbol;}// 抽象方法,每个枚举常量必须实现public abstract double apply(double x, double y);// 静态方法:根据符号查找操作public static Operation fromSymbol(String symbol) {for (Operation op : values()) {if (op.symbol.equals(symbol)) {return op;}}throw new IllegalArgumentException("未知的操作符号: " + symbol);}@Overridepublic String toString() {return symbol;}
}
使用带有静态方法的枚举:
public class OperationDemo {public static void main(String[] args) {double x = 10;double y = 5;for (Operation op : Operation.values()) {System.out.printf("%.1f %s %.1f = %.1f%n", x, op, y, op.apply(x, y));}// 使用静态方法try {Operation op = Operation.fromSymbol("+");System.out.println("找到的操作:" + op);System.out.printf("%.1f %s %.1f = %.1f%n", x, op, y, op.apply(x, y));// 尝试查找不存在的符号Operation unknown = Operation.fromSymbol("^");} catch (IllegalArgumentException e) {System.out.println(e.getMessage());}}
}
输出:
10.0 + 5.0 = 15.0
10.0 - 5.0 = 5.0
10.0 * 5.0 = 50.0
10.0 / 5.0 = 2.0
找到的操作:+
10.0 + 5.0 = 15.0
未知的操作符号: ^
3.3 枚举中的抽象方法
如上面的Operation
枚举所示,枚举还可以包含抽象方法,这要求每个枚举常量都必须提供该方法的实现。
这种模式在枚举常量之间的行为差异较大时非常有用:
public enum Shape {CIRCLE {@Overridepublic double area(double... dimensions) {// 圆的面积:π * r²double radius = dimensions[0];return Math.PI * radius * radius;}@Overridepublic String getDescription() {return "圆形";}},RECTANGLE {@Overridepublic double area(double... dimensions) {// 矩形的面积:长 * 宽double length = dimensions[0];double width = dimensions[1];return length * width;}@Overridepublic String getDescription() {return "矩形";}},TRIANGLE {@Overridepublic double area(double... dimensions) {// 三角形的面积:0.5 * 底 * 高double base = dimensions[0];double height = dimensions[1];return 0.5 * base * height;}@Overridepublic String getDescription() {return "三角形";}};// 抽象方法:计算面积public abstract double area(double... dimensions);// 抽象方法:获取形状描述public abstract String getDescription();
}
使用带有抽象方法的枚举:
public class ShapeDemo {public static void main(String[] args) {// 计算圆的面积double circleRadius = 5.0;double circleArea = Shape.CIRCLE.area(circleRadius);System.out.printf("%s的面积:%.2f%n", Shape.CIRCLE.getDescription(), circleArea);// 计算矩形的面积double rectLength = 4.0;double rectWidth = 6.0;double rectArea = Shape.RECTANGLE.area(rectLength, rectWidth);System.out.printf("%s的面积:%.2f%n", Shape.RECTANGLE.getDescription(), rectArea);// 计算三角形的面积double triangleBase = 8.0;double triangleHeight = 5.0;double triangleArea = Shape.TRIANGLE.area(triangleBase, triangleHeight);System.out.printf("%s的面积:%.2f%n", Shape.TRIANGLE.getDescription(), triangleArea);}
}
输出:
圆形的面积:78.54
矩形的面积:24.00
三角形的面积:20.00
抽象方法的好处是强制每个枚举常量都必须提供方法的实现,使得代码更加健壮。
4. 枚举与接口
4.1 枚举实现接口
枚举类型可以实现一个或多个接口,就像普通类一样。这为枚举提供了更大的灵活性,让它们能够融入到各种设计模式中。
// 定义一个接口
interface Describable {String getDescription();default void printDescription() {System.out.println(getDescription());}
}// 枚举实现接口
public enum Color implements Describable {RED("红色", "#FF0000"),GREEN("绿色", "#00FF00"),BLUE("蓝色", "#0000FF"),YELLOW("黄色", "#FFFF00"),BLACK("黑色", "#000000"),WHITE("白色", "#FFFFFF");private final String description;private final String hexCode;Color(String description, String hexCode) {this.description = description;this.hexCode = hexCode;}@Overridepublic String getDescription() {return description;}public String getHexCode() {return hexCode;}
}
使用实现了接口的枚举:
public class ColorDemo {public static void main(String[] args) {for (Color color : Color.values()) {System.out.printf("%s (%s)%n", color.getDescription(), color.getHexCode());// 通过接口调用方法Describable describable = color;describable.printDescription();System.out.println();}}
}
输出:
红色 (#FF0000)
红色绿色 (#00FF00)
绿色蓝色 (#0000FF)
蓝色黄色 (#FFFF00)
黄色黑色 (#000000)
黑色白色 (#FFFFFF)
白色
4.2 枚举与策略模式
接口的使用使得枚举能够很好地融入策略模式(Strategy Pattern)等设计模式中。
以下是使用枚举实现策略模式的例子:
// 定义付款策略接口
interface PaymentStrategy {double calculatePayment(double amount);
}// 使用枚举实现策略模式
public enum PaymentMethod implements PaymentStrategy {CREDIT_CARD {@Overridepublic double calculatePayment(double amount) {// 信用卡支付,加收2%手续费return amount * 1.02;}},DEBIT_CARD {@Overridepublic double calculatePayment(double amount) {// 借记卡支付,加收1%手续费return amount * 1.01;}},CASH {@Overridepublic double calculatePayment(double amount) {// 现金支付,无手续费return amount;}},ALIPAY {@Overridepublic double calculatePayment(double amount) {// 支付宝支付,满100减10if (amount >= 100) {return amount - 10;}return amount;}},WECHAT_PAY {@Overridepublic double calculatePayment(double amount) {// 微信支付,9折优惠return amount * 0.9;}};// 工厂方法,根据支付类型获取策略public static PaymentStrategy getStrategy(PaymentMethod method) {return method;}
}
使用枚举实现的策略模式:
public class PaymentDemo {public static void main(String[] args) {double purchaseAmount = 150.0;for (PaymentMethod method : PaymentMethod.values()) {// 获取对应的支付策略PaymentStrategy strategy = PaymentMethod.getStrategy(method);// 计算实际支付金额double finalAmount = strategy.calculatePayment(purchaseAmount);System.out.printf("使用%s支付%.2f元,实际支付:%.2f元%n", method, purchaseAmount, finalAmount);}// 直接使用枚举常量作为策略PaymentStrategy creditCardStrategy = PaymentMethod.CREDIT_CARD;double creditCardAmount = creditCardStrategy.calculatePayment(purchaseAmount);System.out.printf("使用信用卡策略支付%.2f元,实际支付:%.2f元%n", purchaseAmount, creditCardAmount);}
}
输出:
使用CREDIT_CARD支付150.00元,实际支付:153.00元
使用DEBIT_CARD支付150.00元,实际支付:151.50元
使用CASH支付150.00元,实际支付:150.00元
使用ALIPAY支付150.00元,实际支付:140.00元
使用WECHAT_PAY支付150.00元,实际支付:135.00元
使用信用卡策略支付150.00元,实际支付:153.00元
使用枚举实现策略模式的好处是:
- 代码更加紧凑,所有策略都集中在一个地方
- 枚举常量本身就是单例,不需要额外的单例实现
- 可以在客户端代码中直接使用枚举常量,提高可读性
- 通过
values()
方法可以轻松获取所有可用的策略
5. 枚举的高级用法
5.1 EnumSet
EnumSet
是Java集合框架中专门为枚举类型设计的高效Set
实现。它内部使用位向量(bit vector)表示,非常紧凑且高效。
创建EnumSet
EnumSet
提供了多种静态工厂方法来创建实例:
public class EnumSetDemo {public static void main(String[] args) {// 创建一个包含所有Day枚举常量的EnumSetEnumSet<Day> allDays = EnumSet.allOf(Day.class);System.out.println("所有天:" + allDays);// 创建一个空的Day类型EnumSetEnumSet<Day> noDays = EnumSet.noneOf(Day.class);System.out.println("初始状态:" + noDays);// 添加元素noDays.add(Day.MONDAY);noDays.add(Day.WEDNESDAY);System.out.println("添加后:" + noDays);// 创建包含指定元素的EnumSetEnumSet<Day> weekend = EnumSet.of(Day.SATURDAY, Day.SUNDAY);System.out.println("周末:" + weekend);// 创建包含指定范围的EnumSetEnumSet<Day> workdays = EnumSet.range(Day.MONDAY, Day.FRIDAY);System.out.println("工作日:" + workdays);// 创建互补的EnumSet(所有不在指定set中的元素)EnumSet<Day> notWeekend = EnumSet.complementOf(weekend);System.out.println("非周末:" + notWeekend);// 创建可变EnumSet的副本EnumSet<Day> weekendCopy = EnumSet.copyOf(weekend);System.out.println("周末副本:" + weekendCopy);}
}
输出:
所有天:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
初始状态:[]
添加后:[MONDAY, WEDNESDAY]
周末:[SATURDAY, SUNDAY]
工作日:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY]
非周末:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY]
周末副本:[SATURDAY, SUNDAY]
EnumSet的常用操作
EnumSet
实现了Set
接口,因此支持标准的集合操作:
public class EnumSetOperationsDemo {public static void main(String[] args) {EnumSet<Day> weekdays = EnumSet.range(Day.MONDAY, Day.FRIDAY);EnumSet<Day> weekend = EnumSet.of(Day.SATURDAY, Day.SUNDAY);// 并集:所有天EnumSet<Day> union = EnumSet.copyOf(weekdays);union.addAll(weekend);System.out.println("并集:" + union);// 交集:空集(因为weekdays和weekend没有共同元素)EnumSet<Day> intersection = EnumSet.copyOf(weekdays);intersection.retainAll(weekend);System.out.println("交集:" + intersection);// 差集:只保留工作日中不在周末中的元素(保持不变,因为没有重叠)EnumSet<Day> difference = EnumSet.copyOf(weekdays);difference.removeAll(weekend);System.out.println("差集:" + difference);// 检查是否包含特定元素boolean containsMonday = weekdays.contains(Day.MONDAY);boolean containsSaturday = weekdays.contains(Day.SATURDAY);System.out.println("工作日包含MONDAY:" + containsMonday);System.out.println("工作日包含SATURDAY:" + containsSaturday);// 清空EnumSet<Day> daysToRemove = EnumSet.copyOf(weekdays);System.out.println("清空前:" + daysToRemove);daysToRemove.clear();System.out.println("清空后:" + daysToRemove);}
}
输出:
并集:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
交集:[]
差集:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY]
工作日包含MONDAY:true
工作日包含SATURDAY:false
清空前:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY]
清空后:[]
EnumSet的优势
EnumSet
相比普通的Set
实现有以下优势:
- 性能:
EnumSet
的所有基本操作(如add
、remove
、contains
)都是常量时间复杂度。 - 空间效率:内部使用位向量,每个枚举常量只占用一个位。
- 类型安全:只能包含指定枚举类型的值。
- 迭代顺序:元素始终按照它们在枚举类型中的声明顺序进行迭代。
使用EnumSet
的实际应用示例:
// 使用EnumSet表示权限
public enum Permission {READ, WRITE, EXECUTE, DELETE
}public class FilePermissions {private EnumSet<Permission> permissions;private String fileName;public FilePermissions(String fileName) {this.fileName = fileName;// 默认只有读权限this.permissions = EnumSet.of(Permission.READ);}public void addPermission(Permission permission) {permissions.add(permission);}public void removePermission(Permission permission) {permissions.remove(permission);}public boolean hasPermission(Permission permission) {return permissions.contains(permission);}public void addPermissions(Permission... permissions) {this.permissions.addAll(EnumSet.of(permissions[0], permissions));}public void clearPermissions() {permissions.clear();}public EnumSet<Permission> getPermissions() {return EnumSet.copyOf(permissions);}@Overridepublic String toString() {return fileName + ": " + permissions;}
}
使用FilePermissions
类:
public class PermissionDemo {public static void main(String[] args) {FilePermissions file1 = new FilePermissions("document.txt");System.out.println("初始权限:" + file1);file1.addPermission(Permission.WRITE);System.out.println("添加写权限后:" + file1);file1.addPermissions(Permission.EXECUTE, Permission.DELETE);System.out.println("添加更多权限后:" + file1);System.out.println("有读权限吗?" + file1.hasPermission(Permission.READ));System.out.println("有执行权限吗?" + file1.hasPermission(Permission.EXECUTE));file1.removePermission(Permission.DELETE);System.out.println("移除删除权限后:" + file1);// 复制权限FilePermissions file2 = new FilePermissions("image.jpg");EnumSet<Permission> file1Permissions = file1.getPermissions();for (Permission permission : file1Permissions) {file2.addPermission(permission);}System.out.println("复制权限后的文件2:" + file2);}
}
输出:
初始权限:document.txt: [READ]
添加写权限后:document.txt: [READ, WRITE]
添加更多权限后:document.txt: [READ, WRITE, EXECUTE, DELETE]
有读权限吗?true
有执行权限吗?true
移除删除权限后:document.txt: [READ, WRITE, EXECUTE]
复制权限后的文件2:image.jpg: [READ, WRITE, EXECUTE]
5.2 EnumMap
EnumMap
是Java集合框架中专门为枚举类型键设计的高效Map
实现。与EnumSet
类似,它内部也使用数组实现,性能极高。
创建EnumMap
public class EnumMapDemo {public static void main(String[] args) {// 创建一个键类型为Season的EnumMapEnumMap<Season, String> seasonActivities = new EnumMap<>(Season.class);// 添加键值对seasonActivities.put(Season.SPRING, "赏花、踏青");seasonActivities.put(Season.SUMMER, "游泳、避暑");seasonActivities.put(Season.AUTUMN, "赏枫、收获");seasonActivities.put(Season.WINTER, "滑雪、过年");// 遍历EnumMapfor (Map.Entry<Season, String> entry : seasonActivities.entrySet()) {System.out.println(entry.getKey() + ": " + entry.getValue());}// 获取特定键的值String summerActivity = seasonActivities.get(Season.SUMMER);System.out.println("夏季活动:" + summerActivity);// 检查是否包含特定键boolean containsWinter = seasonActivities.containsKey(Season.WINTER);System.out.println("包含冬季吗?" + containsWinter);// 检查是否包含特定值boolean containsSkiing = seasonActivities.containsValue("滑雪、过年");System.out.println("包含滑雪活动吗?" + containsSkiing);// 移除键值对String removed = seasonActivities.remove(Season.AUTUMN);System.out.println("移除的秋季活动:" + removed);System.out.println("移除后的EnumMap:" + seasonActivities);}
}
输出:
SPRING: 赏花、踏青
SUMMER: 游泳、避暑
AUTUMN: 赏枫、收获
WINTER: 滑雪、过年
夏季活动:游泳、避暑
包含冬季吗?true
包含滑雪活动吗?true
移除的秋季活动:赏枫、收获
移除后的EnumMap:{SPRING=赏花、踏青, SUMMER=游泳、避暑, WINTER=滑雪、过年}
EnumMap的常用操作
EnumMap
实现了Map
接口,因此支持标准的映射操作:
public class EnumMapOperationsDemo {public static void main(String[] args) {EnumMap<Day, String> schedule = new EnumMap<>(Day.class);// 填充数据schedule.put(Day.MONDAY, "开周会");schedule.put(Day.TUESDAY, "项目开发");schedule.put(Day.WEDNESDAY, "代码评审");schedule.put(Day.THURSDAY, "项目测试");schedule.put(Day.FRIDAY, "周报总结");// 大小System.out.println("日程表大小:" + schedule.size());// 获取所有键Set<Day> days = schedule.keySet();System.out.println("所有工作日:" + days);// 获取所有值Collection<String> activities = schedule.values();System.out.println("所有活动:" + activities);// 获取键值对集合Set<Map.Entry<Day, String>> entries = schedule.entrySet();System.out.println("所有键值对:");for (Map.Entry<Day, String> entry : entries) {System.out.println(entry.getKey() + " -> " + entry.getValue());}// 替换值schedule.put(Day.FRIDAY, "团队建设");System.out.println("周五的新活动:" + schedule.get(Day.FRIDAY));// 获取默认值(如果键不存在)String saturdayActivity = schedule.getOrDefault(Day.SATURDAY, "休息");System.out.println("周六活动:" + saturdayActivity);// 仅当键不存在时才放入schedule.putIfAbsent(Day.SATURDAY, "加班");schedule.putIfAbsent(Day.MONDAY, "不会覆盖已有的值");System.out.println("添加后的日程表:" + schedule);// 清空schedule.clear();System.out.println("清空后的日程表:" + schedule);}
}
输出:
日程表大小:5
所有工作日:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY]
所有活动:[开周会, 项目开发, 代码评审, 项目测试, 周报总结]
所有键值对:
MONDAY -> 开周会
TUESDAY -> 项目开发
WEDNESDAY -> 代码评审
THURSDAY -> 项目测试
FRIDAY -> 周报总结
周五的新活动:团队建设
周六活动:休息
添加后的日程表:{MONDAY=开周会, TUESDAY=项目开发, WEDNESDAY=代码评审, THURSDAY=项目测试, FRIDAY=团队建设, SATURDAY=加班}
清空后的日程表:{}
EnumMap的优势
EnumMap
相比普通的Map
实现有以下优势:
- 性能:所有基本操作都是常量时间复杂度,且不涉及哈希计算和冲突解决。
- 内存效率:内部使用数组实现,比哈希表更节省空间。
- 类型安全:键只能是指定的枚举类型。
- 迭代顺序:元素始终按照枚举常量的声明顺序进行迭代。
- 空间紧凑:不会为空键分配空间。
使用EnumMap
的实际应用示例:
// 使用EnumMap实现简单的状态机
public enum TrafficLightState {RED, YELLOW, GREEN
}public class TrafficLight {private TrafficLightState currentState;// 使用EnumMap存储状态转换规则private EnumMap<TrafficLightState, TrafficLightState> transitions;// 使用EnumMap存储每个状态的持续时间private EnumMap<TrafficLightState, Integer> durations;public TrafficLight() {currentState = TrafficLightState.RED;// 初始化状态转换transitions = new EnumMap<>(TrafficLightState.class);transitions.put(TrafficLightState.RED, TrafficLightState.GREEN);transitions.put(TrafficLightState.GREEN, TrafficLightState.YELLOW);transitions.put(TrafficLightState.YELLOW, TrafficLightState.RED);// 初始化持续时间(秒)durations = new EnumMap<>(TrafficLightState.class);durations.put(TrafficLightState.RED, 30);durations.put(TrafficLightState.YELLOW, 5);durations.put(TrafficLightState.GREEN, 25);}public void changeState() {currentState = transitions.get(currentState);}public TrafficLightState getCurrentState() {return currentState;}public int getCurrentDuration() {return durations.get(currentState);}public void updateDuration(TrafficLightState state, int seconds) {durations.put(state, seconds);}@Overridepublic String toString() {return "当前状态:" + currentState + ",持续时间:" + getCurrentDuration() + "秒";}
}
使用TrafficLight
类:
public class TrafficLightDemo {public static void main(String[] args) {TrafficLight trafficLight = new TrafficLight();System.out.println("初始状态:" + trafficLight);// 模拟3次状态变化for (int i = 0; i < 3; i++) {trafficLight.changeState();System.out.println("变化后:" + trafficLight);}// 修改黄灯的持续时间trafficLight.updateDuration(TrafficLightState.YELLOW, 3);System.out.println("修改黄灯时间后:" + (trafficLight.getCurrentState() == TrafficLightState.YELLOW ? trafficLight : "当前不是黄灯"));// 再次变化trafficLight.changeState();System.out.println("再次变化后:" + trafficLight);}
}
输出:
初始状态:当前状态:RED,持续时间:30秒
变化后:当前状态:GREEN,持续时间:25秒
变化后:当前状态:YELLOW,持续时间:5秒
变化后:当前状态:RED,持续时间:30秒
修改黄灯时间后:当前不是黄灯
再次变化后:当前状态:GREEN,持续时间:25秒
5.3 枚举的序列化
枚举类型的序列化机制与普通Java类不同。枚举常量序列化时只保存其名称,而不保存字段值。这意味着即使枚举包含复杂的状态,在反序列化时也只会恢复到类加载时的状态。
public enum SerializableColor implements Serializable {RED("红色", 0xFF0000),GREEN("绿色", 0x00FF00),BLUE("蓝色", 0x0000FF);private final String name;private final int rgbValue;private transient String cachedHexValue; // transient字段不会被序列化SerializableColor(String name, int rgbValue) {this.name = name;this.rgbValue = rgbValue;updateHexValue();}private void updateHexValue() {this.cachedHexValue = "#" + Integer.toHexString(rgbValue).toUpperCase();}public String getName() {return name;}public int getRgbValue() {return rgbValue;}public String getHexValue() {// 懒加载,如果缓存值为null则重新计算if (cachedHexValue == null) {updateHexValue();}return cachedHexValue;}@Overridepublic String toString() {return name + " (" + getHexValue() + ")";}
}
序列化和反序列化示例:
public class EnumSerializationDemo {public static void main(String[] args) {// 原始枚举常量SerializableColor color = SerializableColor.RED;System.out.println("原始颜色:" + color);try {// 序列化ByteArrayOutputStream baos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(baos);oos.writeObject(color);oos.close();// 修改transient字段(显示为null)Field field = SerializableColor.class.getDeclaredField("cachedHexValue");field.setAccessible(true);field.set(color, null);System.out.println("修改后(被修改的transient字段为null):" + color);// 反序列化ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());ObjectInputStream ois = new ObjectInputStream(bais);SerializableColor deserializedColor = (SerializableColor) ois.readObject();ois.close();System.out.println("反序列化后的颜色:" + deserializedColor);// 验证identitySystem.out.println("是同一个对象?" + (color == deserializedColor));} catch (Exception e) {e.printStackTrace();}}
}
输出:
原始颜色:红色 (#FF0000)
修改后(被修改的transient字段为null):红色 (#FF0000)
反序列化后的颜色:红色 (#FF0000)
是同一个对象?true
关于枚举序列化的特点:
- 枚举序列化只保存枚举常量的名称(如"RED"),不保存字段值
- 反序列化时,通过名称查找已加载的枚举常量实例
- 由于枚举常量本质上是单例,所以反序列化总是返回同一个枚举实例
transient
修饰的字段不会被序列化,但在反序列化时会恢复到类初始化时的状态(因为返回的是同一个实例)- 这种机制确保了跨JVM的枚举实例相等性(使用
==
比较)
// … existing code …
6. 枚举的最佳实践
6.1 命名约定
对于枚举类型的命名,有以下几个建议:
- 枚举类名: 使用名词,单数形式,首字母大写,如
Season
、Color
、DayOfWeek
。 - 枚举常量: 全部大写,单词间用下划线分隔,如
SPRING
、SUMMER
、FIRST_QUARTER
。 - 方法和字段: 遵循与普通Java类相同的命名约定,如
getDescription()
、calculateValue()
。
// 良好的命名实践
public enum CurrencyUnit {US_DOLLAR, EURO, BRITISH_POUND, JAPANESE_YEN, CHINESE_YUAN;// 方法使用驼峰命名法public String getDisplayName() {// 实现return name().toLowerCase().replace('_', ' ');}
}
6.2 何时使用枚举
枚举在以下情况特别有用:
- 有限集合的常量: 当需要表示一组固定的值,如日期、状态、类型等。
- 编译时确定的值: 当值在编译时就已知并且不会动态变化。
- 需要类型安全: 当需要确保变量只能取特定的值,而不是任意值。
- 需要特殊行为: 当每个常量需要有自己的独特行为。
- 需要分组常量: 当想把相关的常量组织在一起时。
6.3 枚举设计技巧
保持简单
如果枚举只需要表示简单的值集合,不要添加不必要的复杂性:
// 简单的枚举足够了
public enum Direction {NORTH, EAST, SOUTH, WEST
}// 不要过度设计
public enum OverEngineeredDirection {NORTH("北", 0), EAST("东", 90), SOUTH("南", 180), WEST("西", 270);private final String chineseName;private final int degrees;// 除非真的需要这些字段,否则是不必要的复杂性OverEngineeredDirection(String chineseName, int degrees) {this.chineseName = chineseName;this.degrees = degrees;}// getters...
}
使用私有构造函数
枚举的构造函数默认是私有的,即使声明为public
,它也会被编译器视为private
。为了保持一致性和清晰度,建议显式地将构造函数声明为private
:
public enum Planet {MERCURY(3.303e+23, 2.4397e6),VENUS(4.869e+24, 6.0518e6);private final double mass;private final double radius;// 显式地声明为privateprivate Planet(double mass, double radius) {this.mass = mass;this.radius = radius;}// getters...
}
避免使用ordinal()
尽量避免依赖ordinal()
方法,因为如果枚举常量顺序改变,使用ordinal()
的代码可能会出现问题:
// 不好的做法
public enum BadPractice {A, B, C;public int getValue() {return ordinal() + 1; // 如果顺序变化,值也会变}
}// 好的做法
public enum GoodPractice {A(1), B(2), C(3);private final int value;private GoodPractice(int value) {this.value = value;}public int getValue() {return value;}
}
考虑使用EnumSet和EnumMap
在需要操作枚举集合时,优先使用EnumSet
和EnumMap
:
// 而不是这样
public void processDay(Day day) {if (day == Day.SATURDAY || day == Day.SUNDAY) {// 周末逻辑} else {// 工作日逻辑}
}// 更好的做法是使用EnumSet
private static final EnumSet<Day> WEEKENDS = EnumSet.of(Day.SATURDAY, Day.SUNDAY);public void processDayBetter(Day day) {if (WEEKENDS.contains(day)) {// 周末逻辑} else {// 工作日逻辑}
}
优化switch语句
当使用switch
语句处理枚举时,考虑以下优化:
- 不要在每个
case
中重复枚举类型名称。 - 如果每个枚举常量的行为差异很大,考虑使用抽象方法代替
switch
。 - 确保处理所有可能的枚举值,或有合理的默认处理。
// 不好的写法
public double calculate(Operation op, double x, double y) {switch (op) {case Operation.PLUS: // 错误,不需要类型名return x + y;case Operation.MINUS: // 错误,不需要类型名return x - y;// 遗漏了其他操作...}return 0; // 糟糕的默认返回
}// 好的写法
public double calculateBetter(Operation op, double x, double y) {switch (op) {case PLUS:return x + y;case MINUS:return x - y;case TIMES:return x * y;case DIVIDE:return x / y;default:throw new IllegalArgumentException("未知操作: " + op);}
}// 最好的写法(如果行为差异大)
public double calculateBest(Operation op, double x, double y) {return op.apply(x, y); // 使用枚举的抽象方法
}
6.4 常见陷阱和注意事项
枚举的序列化考虑
如前所述,枚举的序列化只存储名称,不存储状态。如果枚举中的字段值对于序列化/反序列化很重要,应该考虑这种影响。
避免在枚举中使用可变状态
枚举常量本质上是单例,因此不应包含可变状态,以避免并发问题:
// 不好的实践 - 可变状态
public enum Counter {INSTANCE;private int count = 0;public void increment() {count++; // 可变状态,在并发环境中可能有问题}public int getCount() {return count;}
}// 更好的做法是使用线程安全的方式或不可变设计
public enum SafeCounter {INSTANCE;private final AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet();}public int getCount() {return count.get();}
}
避免过于复杂的枚举实现
枚举应该保持相对简单。如果发现枚举变得过于复杂,可能应该考虑使用常规的类层次结构:
// 过于复杂的枚举
public enum ComplexEnum {INSTANCE_A {// 大量特定于A的逻辑...},INSTANCE_B {// 大量特定于B的逻辑...};// 大量共享方法和字段...
}// 可能更好的替代方案
public interface BetterDesign {// 共享接口方法
}public class ImplementationA implements BetterDesign {private static final ImplementationA INSTANCE = new ImplementationA();private ImplementationA() {}public static ImplementationA getInstance() {return INSTANCE;}// 实现接口方法,加上特定于A的逻辑
}public class ImplementationB implements BetterDesign {// 类似的实现...
}
7. 枚举的实际应用
7.1 状态机实现
枚举非常适合实现简单的状态机,每个枚举常量代表一个状态:
public enum OrderStatus {NEW {@Overridepublic OrderStatus next() {return PROCESSING;}},PROCESSING {@Overridepublic OrderStatus next() {return SHIPPED;}},SHIPPED {@Overridepublic OrderStatus next() {return DELIVERED;}},DELIVERED {@Overridepublic OrderStatus next() {return this; // 终态}},CANCELLED {@Overridepublic OrderStatus next() {return this; // 终态}};public abstract OrderStatus next();
}// 使用状态机
public class Order {private OrderStatus status;private String orderId;public Order(String orderId) {this.orderId = orderId;this.status = OrderStatus.NEW;}public void proceed() {status = status.next();}public void cancel() {status = OrderStatus.CANCELLED;}public OrderStatus getStatus() {return status;}@Overridepublic String toString() {return "Order " + orderId + ": " + status;}
}
使用示例:
public class OrderDemo {public static void main(String[] args) {Order order = new Order("ORD-12345");System.out.println(order);order.proceed(); // NEW -> PROCESSINGSystem.out.println(order);order.proceed(); // PROCESSING -> SHIPPEDSystem.out.println(order);order.proceed(); // SHIPPED -> DELIVEREDSystem.out.println(order);order.proceed(); // DELIVERED -> DELIVERED(不变)System.out.println(order);// 创建一个新订单然后取消Order order2 = new Order("ORD-67890");System.out.println(order2);order2.cancel(); // NEW -> CANCELLEDSystem.out.println(order2);}
}
输出:
Order ORD-12345: NEW
Order ORD-12345: PROCESSING
Order ORD-12345: SHIPPED
Order ORD-12345: DELIVERED
Order ORD-12345: DELIVERED
Order ORD-67890: NEW
Order ORD-67890: CANCELLED
7.2 单例模式实现
枚举提供了实现单例模式的最简单方法,可以防止序列化问题和反射攻击:
public enum DatabaseConnection {INSTANCE;private Connection connection;DatabaseConnection() {try {// 初始化数据库连接System.out.println("初始化数据库连接...");// 实际代码会连接到真实的数据库// connection = DriverManager.getConnection(url, user, password);} catch (Exception e) {throw new RuntimeException("无法连接到数据库", e);}}public void executeQuery(String sql) {System.out.println("执行查询: " + sql);// 实际代码会使用connection执行查询}public void close() {try {if (connection != null && !connection.isClosed()) {connection.close();System.out.println("数据库连接已关闭");}} catch (Exception e) {System.err.println("关闭数据库连接时出错: " + e.getMessage());}}
}
使用示例:
public class DatabaseSingletonDemo {public static void main(String[] args) {// 获取单例实例DatabaseConnection db = DatabaseConnection.INSTANCE;// 使用数据库连接db.executeQuery("SELECT * FROM users");db.executeQuery("UPDATE users SET active = true");// 在应用程序结束时关闭连接Runtime.getRuntime().addShutdownHook(new Thread(() -> {db.close();}));}
}
输出:
初始化数据库连接...
执行查询: SELECT * FROM users
执行查询: UPDATE users SET active = true
数据库连接已关闭
7.3 命令模式实现
枚举可以用来实现命令模式,每个枚举常量代表一个命令:
public enum TextCommand {COPY {@Overridepublic void execute(TextEditor editor) {editor.copy();}},PASTE {@Overridepublic void execute(TextEditor editor) {editor.paste();}},CUT {@Overridepublic void execute(TextEditor editor) {editor.cut();}},UNDO {@Overridepublic void execute(TextEditor editor) {editor.undo();}},REDO {@Overridepublic void execute(TextEditor editor) {editor.redo();}};public abstract void execute(TextEditor editor);
}// 文本编辑器类
class TextEditor {private String clipboard = "";private StringBuilder text = new StringBuilder();private Stack<String> undoStack = new Stack<>();private Stack<String> redoStack = new Stack<>();public void setText(String text) {saveForUndo();this.text = new StringBuilder(text);}public String getText() {return text.toString();}public void copy() {clipboard = text.toString();System.out.println("已复制文本到剪贴板");}public void paste() {saveForUndo();text.append(clipboard);System.out.println("已粘贴文本: " + clipboard);}public void cut() {saveForUndo();clipboard = text.toString();text = new StringBuilder();System.out.println("已剪切文本到剪贴板");}private void saveForUndo() {undoStack.push(text.toString());redoStack.clear();}public void undo() {if (!undoStack.isEmpty()) {redoStack.push(text.toString());text = new StringBuilder(undoStack.pop());System.out.println("撤销操作");} else {System.out.println("没有可撤销的操作");}}public void redo() {if (!redoStack.isEmpty()) {undoStack.push(text.toString());text = new StringBuilder(redoStack.pop());System.out.println("重做操作");} else {System.out.println("没有可重做的操作");}}
}
使用示例:
public class CommandPatternDemo {public static void main(String[] args) {TextEditor editor = new TextEditor();editor.setText("Hello World");System.out.println("初始文本: " + editor.getText());// 执行复制命令TextCommand.COPY.execute(editor);// 执行剪切命令TextCommand.CUT.execute(editor);System.out.println("剪切后文本: " + editor.getText());// 执行粘贴命令TextCommand.PASTE.execute(editor);System.out.println("粘贴后文本: " + editor.getText());// 执行撤销命令TextCommand.UNDO.execute(editor);System.out.println("撤销后文本: " + editor.getText());// 执行重做命令TextCommand.REDO.execute(editor);System.out.println("重做后文本: " + editor.getText());}
}
输出:
初始文本: Hello World
已复制文本到剪贴板
已剪切文本到剪贴板
剪切后文本:
已粘贴文本: Hello World
粘贴后文本: Hello World
撤销操作
撤销后文本:
重做操作
重做后文本: Hello World
7.4 策略模式实现
如前面示例所示,枚举也可以用来实现策略模式:
// 定义支付策略枚举
public enum PaymentStrategy {CREDIT_CARD {@Overridepublic double calculatePayment(double amount) {return amount * 1.02; // 2%手续费}@Overridepublic String getDescription() {return "信用卡支付(2%手续费)";}},DEBIT_CARD {@Overridepublic double calculatePayment(double amount) {return amount * 1.01; // 1%手续费}@Overridepublic String getDescription() {return "借记卡支付(1%手续费)";}},PAYPAL {@Overridepublic double calculatePayment(double amount) {return amount * 1.015; // 1.5%手续费}@Overridepublic String getDescription() {return "PayPal支付(1.5%手续费)";}},CASH {@Overridepublic double calculatePayment(double amount) {return amount; // 无手续费}@Overridepublic String getDescription() {return "现金支付(无手续费)";}};public abstract double calculatePayment(double amount);public abstract String getDescription();
}// 购物车类
class ShoppingCart {private List<Double> itemPrices;private PaymentStrategy paymentStrategy;public ShoppingCart() {itemPrices = new ArrayList<>();}public void addItem(double price) {itemPrices.add(price);}public void setPaymentStrategy(PaymentStrategy paymentStrategy) {this.paymentStrategy = paymentStrategy;}public double calculateTotal() {double sum = itemPrices.stream().mapToDouble(Double::doubleValue).sum();return paymentStrategy.calculatePayment(sum);}public void checkout() {double total = calculateTotal();double originalTotal = itemPrices.stream().mapToDouble(Double::doubleValue).sum();System.out.printf("使用%s付款%n", paymentStrategy.getDescription());System.out.printf("商品原价: ¥%.2f%n", originalTotal);System.out.printf("实际付款: ¥%.2f%n", total);System.out.println("支付成功!");}
}
使用示例:
public class PaymentStrategyDemo {public static void main(String[] args) {ShoppingCart cart = new ShoppingCart();cart.addItem(100.0);cart.addItem(50.0);cart.addItem(200.0);// 使用信用卡支付cart.setPaymentStrategy(PaymentStrategy.CREDIT_CARD);cart.checkout();System.out.println();// 使用现金支付cart.setPaymentStrategy(PaymentStrategy.CASH);cart.checkout();}
}
输出:
使用信用卡支付(2%手续费)付款
商品原价: ¥350.00
实际付款: ¥357.00
支付成功!使用现金支付(无手续费)付款
商品原价: ¥350.00
实际付款: ¥350.00
支付成功!
8. 总结
Java枚举是一种功能强大的特性,它不仅仅是简单的常量集合,还可以拥有字段、方法、构造函数,并且可以实现接口和抽象方法。
8.1 枚举的主要特点
- 类型安全:编译时类型检查,避免非法值。
- 单例性:枚举常量是单例的,可以使用
==
比较。 - 功能丰富:可以拥有字段、方法、构造函数。
- 多态性:可以实现接口或抽象方法,每个常量有不同实现。
- 序列化安全:特殊的序列化机制,防止伪造实例。
- 可扩展性:通过抽象方法可以轻松扩展行为。
8.2 枚举与设计模式
枚举在多种设计模式中都有应用,如:
- 单例模式:枚举提供了最简单的单例实现方式。
- 策略模式:每个枚举常量可以代表一种策略。
- 状态模式:枚举常量可以表示不同的状态。
- 命令模式:枚举常量可以封装不同的命令。
- 工厂模式:枚举可以作为简单的工厂,创建不同类型的对象。
8.3 枚举的适用场景
枚举适用于以下场景:
- 有限集合的常量:如日期、颜色、状态、类型等。
- 特定领域的常量集:如HTTP状态码、SQL类型、操作命令等。
- 常量关联行为:当常量需要具有相关联的行为时。
- 单例实现:当需要线程安全、序列化安全的单例时。
- 策略封装:当需要封装不同的行为策略时。
在Java编程中,枚举是表示固定集合常量的首选方式,它不仅仅是简单的int常量的替代品,而是一种强大的类型,可以极大提高代码的可读性、类型安全性和维护性。