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()); // 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
子句中,我们直接使用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;
}
@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元
使用枚举实现策略模式的好处是:
- 代码更加紧凑,所有策略都集中在一个地方
- 枚举常量本身就是单例,不需要额外的单例实现
- 可以在客户端代码中直接使用枚举常量,提高可读性
- 通过
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
实现有以下优势:
- 性能:
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);
}
@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
实现有以下优势:
- 性能:所有基本操作都是常量时间复杂度,且不涉及哈希计算和冲突解决。
- 内存效率:内部使用数组实现,比哈希表更节省空间。
- 类型安全:键只能是指定的枚举类型。
- 迭代顺序:元素始终按照枚举常量的声明顺序进行迭代。
- 空间紧凑:不会为空键分配空间。
使用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
关于枚举序列化的特点:
- 枚举序列化只保存枚举常量的名称(如"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;
// 显式地声明为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
在需要操作枚举集合时,优先使用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 {
@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 枚举的主要特点
- 类型安全:编译时类型检查,避免非法值。
- 单例性:枚举常量是单例的,可以使用
==
比较。 - 功能丰富:可以拥有字段、方法、构造函数。
- 多态性:可以实现接口或抽象方法,每个常量有不同实现。
- 序列化安全:特殊的序列化机制,防止伪造实例。
- 可扩展性:通过抽象方法可以轻松扩展行为。
8.2 枚举与设计模式
枚举在多种设计模式中都有应用,如:
- 单例模式:枚举提供了最简单的单例实现方式。
- 策略模式:每个枚举常量可以代表一种策略。
- 状态模式:枚举常量可以表示不同的状态。
- 命令模式:枚举常量可以封装不同的命令。
- 工厂模式:枚举可以作为简单的工厂,创建不同类型的对象。
8.3 枚举的适用场景
枚举适用于以下场景:
- 有限集合的常量:如日期、颜色、状态、类型等。
- 特定领域的常量集:如HTTP状态码、SQL类型、操作命令等。
- 常量关联行为:当常量需要具有相关联的行为时。
- 单例实现:当需要线程安全、序列化安全的单例时。
- 策略封装:当需要封装不同的行为策略时。
在Java编程中,枚举是表示固定集合常量的首选方式,它不仅仅是简单的int常量的替代品,而是一种强大的类型,可以极大提高代码的可读性、类型安全性和维护性。