Java枚举详解

发布于:2025-05-22 ⋅ 阅读:(18) ⋅ 点赞:(0)

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());  // 0
        System.out.println(Season.SUMMER.ordinal());  // 1
        System.out.println(Season.AUTUMN.ordinal());  // 2
        System.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;
    }
    
    @Override
    public 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("+") {
        @Override
        public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-") {
        @Override
        public double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES("*") {
        @Override
        public double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDE("/") {
        @Override
        public 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);
    }
    
    @Override
    public 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 {
        @Override
        public double area(double... dimensions) {
            // 圆的面积:π * r²
            double radius = dimensions[0];
            return Math.PI * radius * radius;
        }
        
        @Override
        public String getDescription() {
            return "圆形";
        }
    },
    RECTANGLE {
        @Override
        public double area(double... dimensions) {
            // 矩形的面积:长 * 宽
            double length = dimensions[0];
            double width = dimensions[1];
            return length * width;
        }
        
        @Override
        public String getDescription() {
            return "矩形";
        }
    },
    TRIANGLE {
        @Override
        public double area(double... dimensions) {
            // 三角形的面积:0.5 * 底 * 高
            double base = dimensions[0];
            double height = dimensions[1];
            return 0.5 * base * height;
        }
        
        @Override
        public 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;
    }
    
    @Override
    public 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 {
        @Override
        public double calculatePayment(double amount) {
            // 信用卡支付,加收2%手续费
            return amount * 1.02;
        }
    },
    DEBIT_CARD {
        @Override
        public double calculatePayment(double amount) {
            // 借记卡支付,加收1%手续费
            return amount * 1.01;
        }
    },
    CASH {
        @Override
        public double calculatePayment(double amount) {
            // 现金支付,无手续费
            return amount;
        }
    },
    ALIPAY {
        @Override
        public double calculatePayment(double amount) {
            // 支付宝支付,满100减10
            if (amount >= 100) {
                return amount - 10;
            }
            return amount;
        }
    },
    WECHAT_PAY {
        @Override
        public 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枚举常量的EnumSet
        EnumSet<Day> allDays = EnumSet.allOf(Day.class);
        System.out.println("所有天:" + allDays);
        
        // 创建一个空的Day类型EnumSet
        EnumSet<Day> noDays = EnumSet.noneOf(Day.class);
        System.out.println("初始状态:" + noDays);
        
        // 添加元素
        noDays.add(Day.MONDAY);
        noDays.add(Day.WEDNESDAY);
        System.out.println("添加后:" + noDays);
        
        // 创建包含指定元素的EnumSet
        EnumSet<Day> weekend = EnumSet.of(Day.SATURDAY, Day.SUNDAY);
        System.out.println("周末:" + weekend);
        
        // 创建包含指定范围的EnumSet
        EnumSet<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);
    }
    
    @Override
    public 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的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, "滑雪、过年");
        
        // 遍历EnumMap
        for (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);
    }
    
    @Override
    public 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;
    }
    
    @Override
    public 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);
            
            // 验证identity
            System.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;
    
    // 显式地声明为private
    private 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 {
        @Override
        public OrderStatus next() {
            return PROCESSING;
        }
    },
    PROCESSING {
        @Override
        public OrderStatus next() {
            return SHIPPED;
        }
    },
    SHIPPED {
        @Override
        public OrderStatus next() {
            return DELIVERED;
        }
    },
    DELIVERED {
        @Override
        public OrderStatus next() {
            return this; // 终态
        }
    },
    CANCELLED {
        @Override
        public 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;
    }
    
    @Override
    public 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 -> PROCESSING
        System.out.println(order);
        
        order.proceed(); // PROCESSING -> SHIPPED
        System.out.println(order);
        
        order.proceed(); // SHIPPED -> DELIVERED
        System.out.println(order);
        
        order.proceed(); // DELIVERED -> DELIVERED(不变)
        System.out.println(order);
        
        // 创建一个新订单然后取消
        Order order2 = new Order("ORD-67890");
        System.out.println(order2);
        
        order2.cancel(); // NEW -> CANCELLED
        System.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 {
        @Override
        public void execute(TextEditor editor) {
            editor.copy();
        }
    },
    PASTE {
        @Override
        public void execute(TextEditor editor) {
            editor.paste();
        }
    },
    CUT {
        @Override
        public void execute(TextEditor editor) {
            editor.cut();
        }
    },
    UNDO {
        @Override
        public void execute(TextEditor editor) {
            editor.undo();
        }
    },
    REDO {
        @Override
        public 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 {
        @Override
        public double calculatePayment(double amount) {
            return amount * 1.02; // 2%手续费
        }
        
        @Override
        public String getDescription() {
            return "信用卡支付(2%手续费)";
        }
    },
    DEBIT_CARD {
        @Override
        public double calculatePayment(double amount) {
            return amount * 1.01; // 1%手续费
        }
        
        @Override
        public String getDescription() {
            return "借记卡支付(1%手续费)";
        }
    },
    PAYPAL {
        @Override
        public double calculatePayment(double amount) {
            return amount * 1.015; // 1.5%手续费
        }
        
        @Override
        public String getDescription() {
            return "PayPal支付(1.5%手续费)";
        }
    },
    CASH {
        @Override
        public double calculatePayment(double amount) {
            return amount; // 无手续费
        }
        
        @Override
        public 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常量的替代品,而是一种强大的类型,可以极大提高代码的可读性、类型安全性和维护性。


网站公告

今日签到

点亮在社区的每一天
去签到