当前位置: 首页 > news >正文

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是一个枚举类型,它有四个可能的值:SPRINGSUMMERAUTUMNWINTER。每个枚举常量都是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;
}

虽然这种方法可以工作,但它有几个严重的缺点:

  1. 类型不安全:这些常量只是整数值,可以轻松地与其他整数混淆。例如,你可以将一个表示操作码的整数常量误用为季节常量。

  2. 没有命名空间:除非使用长且可能笨拙的名称,否则常量名称会污染命名空间。

  3. 打印困难:当你打印一个整数常量时,你只看到一个数字,而不是有意义的名称。

  4. 编译时不检查:如果你添加、移除或重新排序常量,使用这些常量的代码可能会悄悄地失效。

枚举解决了所有这些问题,并提供了更多的功能:

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枚举有以下几个主要优势:

  1. 类型安全:枚举提供了编译时类型安全。你不能将一个枚举类型赋值给另一个枚举类型,也不能使用非枚举值作为枚举类型。
Season season = Season.SPRING; // 有效
// Season season = 0; // 编译错误:不兼容的类型
// Season season = "SPRING"; // 编译错误:不兼容的类型
// Season season = Day.MONDAY; // 编译错误:不兼容的类型
  1. 命名空间:枚举常量位于其枚举类型的命名空间内,避免了命名冲突。
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;
  1. 可读性:枚举值在打印时使用其名称,而不是一个可能没有意义的数字。
Season season = Season.SPRING;
System.out.println(season); // 输出:SPRING,而不是0或其他数字
  1. 内置方法: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
  1. 可扩展性:枚举可以有构造函数、字段、方法和实现接口,这使它们比简单的常量声明更强大。
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()); // 输出:炎热
  1. 集合支持:枚举可以很容易地与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子句中,我们直接使用SPRINGSUMMER等,而不是Season.SPRINGSeason.SUMMER。这是因为switch语句的表达式已经指定了枚举类型(Season currentSeason),所以Java编译器知道这些常量属于Season枚举。

3. 自定义枚举

3.1 带有字段的枚举

枚举不仅仅可以是简单的常量列表,还可以包含字段、构造函数和方法,就像普通的类一样。这使得枚举类型更加强大和灵活。

添加字段和构造函数

要为枚举添加字段,我们需要:

  1. 声明实例变量
  2. 创建构造函数
  3. 提供访问字段的方法
  4. 在枚举常量声明中传入参数
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千克。
枚举构造函数的特点

枚举的构造函数有一些特殊的规则:

  1. 构造函数总是私有的,即使你声明为publicprotected,Java也会自动将其视为private
  2. 枚举常量必须在任何字段或方法之前定义。
  3. 如果枚举声明中包含字段或方法,则枚举常量列表必须以分号结束。
// 错误:构造函数不能是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元

使用枚举实现策略模式的好处是:

  1. 代码更加紧凑,所有策略都集中在一个地方
  2. 枚举常量本身就是单例,不需要额外的单例实现
  3. 可以在客户端代码中直接使用枚举常量,提高可读性
  4. 通过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实现有以下优势:

  1. 性能EnumSet的所有基本操作(如addremovecontains)都是常量时间复杂度。
  2. 空间效率:内部使用位向量,每个枚举常量只占用一个位。
  3. 类型安全:只能包含指定枚举类型的值。
  4. 迭代顺序:元素始终按照它们在枚举类型中的声明顺序进行迭代。

使用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实现有以下优势:

  1. 性能:所有基本操作都是常量时间复杂度,且不涉及哈希计算和冲突解决。
  2. 内存效率:内部使用数组实现,比哈希表更节省空间。
  3. 类型安全:键只能是指定的枚举类型。
  4. 迭代顺序:元素始终按照枚举常量的声明顺序进行迭代。
  5. 空间紧凑:不会为空键分配空间。

使用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

关于枚举序列化的特点:

  1. 枚举序列化只保存枚举常量的名称(如"RED"),不保存字段值
  2. 反序列化时,通过名称查找已加载的枚举常量实例
  3. 由于枚举常量本质上是单例,所以反序列化总是返回同一个枚举实例
  4. transient修饰的字段不会被序列化,但在反序列化时会恢复到类初始化时的状态(因为返回的是同一个实例)
  5. 这种机制确保了跨JVM的枚举实例相等性(使用==比较)

// … existing code …

6. 枚举的最佳实践

6.1 命名约定

对于枚举类型的命名,有以下几个建议:

  1. 枚举类名: 使用名词,单数形式,首字母大写,如SeasonColorDayOfWeek
  2. 枚举常量: 全部大写,单词间用下划线分隔,如SPRINGSUMMERFIRST_QUARTER
  3. 方法和字段: 遵循与普通Java类相同的命名约定,如getDescription()calculateValue()
// 良好的命名实践
public enum CurrencyUnit {US_DOLLAR, EURO, BRITISH_POUND, JAPANESE_YEN, CHINESE_YUAN;// 方法使用驼峰命名法public String getDisplayName() {// 实现return name().toLowerCase().replace('_', ' ');}
}

6.2 何时使用枚举

枚举在以下情况特别有用:

  1. 有限集合的常量: 当需要表示一组固定的值,如日期、状态、类型等。
  2. 编译时确定的值: 当值在编译时就已知并且不会动态变化。
  3. 需要类型安全: 当需要确保变量只能取特定的值,而不是任意值。
  4. 需要特殊行为: 当每个常量需要有自己的独特行为。
  5. 需要分组常量: 当想把相关的常量组织在一起时。

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

在需要操作枚举集合时,优先使用EnumSetEnumMap

// 而不是这样
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语句处理枚举时,考虑以下优化:

  1. 不要在每个case中重复枚举类型名称。
  2. 如果每个枚举常量的行为差异很大,考虑使用抽象方法代替switch
  3. 确保处理所有可能的枚举值,或有合理的默认处理。
// 不好的写法
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 枚举的主要特点

  1. 类型安全:编译时类型检查,避免非法值。
  2. 单例性:枚举常量是单例的,可以使用==比较。
  3. 功能丰富:可以拥有字段、方法、构造函数。
  4. 多态性:可以实现接口或抽象方法,每个常量有不同实现。
  5. 序列化安全:特殊的序列化机制,防止伪造实例。
  6. 可扩展性:通过抽象方法可以轻松扩展行为。

8.2 枚举与设计模式

枚举在多种设计模式中都有应用,如:

  1. 单例模式:枚举提供了最简单的单例实现方式。
  2. 策略模式:每个枚举常量可以代表一种策略。
  3. 状态模式:枚举常量可以表示不同的状态。
  4. 命令模式:枚举常量可以封装不同的命令。
  5. 工厂模式:枚举可以作为简单的工厂,创建不同类型的对象。

8.3 枚举的适用场景

枚举适用于以下场景:

  1. 有限集合的常量:如日期、颜色、状态、类型等。
  2. 特定领域的常量集:如HTTP状态码、SQL类型、操作命令等。
  3. 常量关联行为:当常量需要具有相关联的行为时。
  4. 单例实现:当需要线程安全、序列化安全的单例时。
  5. 策略封装:当需要封装不同的行为策略时。

在Java编程中,枚举是表示固定集合常量的首选方式,它不仅仅是简单的int常量的替代品,而是一种强大的类型,可以极大提高代码的可读性、类型安全性和维护性。

相关文章:

  • 抽象:C++命名作用域与函数调用
  • IO pin的transition约束从哪来?
  • 高级认知型Agent
  • dedecms织梦全局变量调用方法总结
  • 如何在电脑上登录多个抖音账号?多开不同IP技巧分解
  • 广东省省考备考(第十六天5.21)—言语:语句排序题(听课后强化)
  • React中 lazy与 Suspense懒加载的组件
  • git合并多次commit提交
  • CentOS:搭建国内软件repository,以实现自动yum网络安装
  • JUC高并发编程
  • 自动化软件如何确保高可用性和容错性?
  • 云蝠智能大模型呼叫动态情感共情能力上线!
  • 大语言模型 17 - MCP Model Context Protocol 介绍对比分析 基本环境配置
  • 双活数据中心解决方案
  • 如何在Java中处理PDF文档(教程)
  • :-1: error: msvc-version.conf loaded but QMAKE_MSC_VER isn‘t set错误原因及解决方法
  • WSD3043 MOSFET 在吸黑头仪中的应用
  • #Redis缓存篇#(七)分布式缓存
  • 基于大模型的全面惊厥性癫痫持续状态技术方案
  • 1.4 C++之运算符与表达式