概述

在本文中,我们将会了解到什么是 Java 枚举,枚举解决了哪些问题以及如何在实践中使用 Java 枚举实现一些设计模式。

enum 这个关键字出现在 Java5 中,它继承 java.lang.Enum 类,有关枚举更多的内容,请参考 官方文档。

以这种方式定义的常量使得代码可读性更高,允许编译时异常检查,预先记录可接受值的列表,避免由于传入无效的值引发意外情况。

下面的代码定义了一个简单快捷的枚举示例,它定义了 pizza 订单的三种状态:ORDERED、READY 、DELIVERED。

public enum PizzaStatus {
    ORDERED,
    READY, 
    DELIVERED; 
}

另外,枚举还有好多有用的方法。如果你用传统的 public static final 常量,这些方法是需要你手动写的。

自定义方法

目前为止,我们对枚举是什么以及如何使用有了基本了解。让我们在之前的例子基础上再定义一些额外的方法,让我们之前的例子更好:

public class Pizza {
    private PizzaStatus status;
    public enum PizzaStatus {
        ORDERED,
        READY,
        DELIVERED;
    }
 
    public boolean isDeliverable() {
        if (getStatus() == PizzaStatus.READY) {
            return true;
        }
        return false;
    }
     
    // Methods that set and get the status variable.
}

使用 == 运算符比较枚举类型

由于枚举类型能够确保 JVM 中仅存在一个常量的实例,因此我们可以在上述例子中使用 == 运算符来比较两个变量。此外,== 运算符可提供编译时以及运行时的安全性。

让我们首先看下一下代码的运行时安全。其中 == 运算符用于比较两个状态,并且如果两个值均为 null 都不会引发 NullPointerException。相反,如果使用 equals() 方法,则会抛出 NullPointerException 的异常。

if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED)); 
if(testPz.getStatus() == Pizza.PizzaStatus.DELIVERED);

至于编译时安全,我们看另外一个例子,使用 equals() 方法比较结果为 true,因为 getStatus() 方法的枚举值和另一个和另一个枚举值相同,但逻辑上应该为 false。这个问题可以通过使用 == 运算符避免。

编译器会显示类型不兼容错误:

if(testPz.getStatus().equals(TestColor.GREEN));
if(testPz.getStatus() == TestColor.GREEN);

在 switch 语句中使用枚举类型

枚举类型同样可以被用在 switch 代码块中:

public int getDeliveryTimeInDays() {
    switch (status) {
        case ORDERED: return 5;
        case READY: return 2;
        case DELIVERED: return 0;
    }
    return 0;
}

枚举类型中的属性,方法,构造器

你可以在枚举类型中定义属性、方法、构造器让它功能更强大。

让我们接着拓展上面的示例,实现 pizza 从一个阶段到另一个阶段的过渡,并了解如何摆脱之前的 if 语句和 switch 语句。

public class Pizza {
 
    private PizzaStatus status;
    public enum PizzaStatus {
        ORDERED (5){
            @Override
            public boolean isOrdered() {
                return true;
            }
        },
        READY (2){
            @Override
            public boolean isReady() {
                return true;
            }
        },
        DELIVERED (0){
            @Override
            public boolean isDelivered() {
                return true;
            }
        };
 
        private int timeToDelivery;
 
        public boolean isOrdered() {return false;}
 
        public boolean isReady() {return false;}
 
        public boolean isDelivered(){return false;}
 
        public int getTimeToDelivery() {
            return timeToDelivery;
        }
 
        PizzaStatus (int timeToDelivery) {
            this.timeToDelivery = timeToDelivery;
        }
    }
 
    public boolean isDeliverable() {
        return this.status.isReady();
    }
 
    public void printTimeToDeliver() {
        System.out.println("Time to delivery is " + 
          this.getStatus().getTimeToDelivery());
    }
     
    // Methods that set and get the status variable.
}

下面的测试代码块展示了它如何工作:

@Test
public void givenPizaOrder_whenReady_thenDeliverable() {
    Pizza testPz = new Pizza();
    testPz.setStatus(Pizza.PizzaStatus.READY);
    assertTrue(testPz.isDeliverable());
}

EnumSet 和 EnumMap

EnumSet

EnumSet 是一种专门为枚举设计的 Set 类型。

由于使用了内部位向量表示,相对于 HashSet 来说,EnumSet 是特定的 Enum 常量集的有效紧凑的表示方式。并且它为传统的基于 int 的位标志提供了类型类型安全的替代方案,使我们能够编写更易读和易于维护的简洁代码。

EnumSet 是抽象类,有两个实现:RegularEnumSet 以及 JumboEnumSet。使用哪一个取决于实例化时枚举中常量的数量。

因此在我们想操作(取子集、添加、删除、containsAll 以及 removeAll)枚举常量集合的大部分情况下,使用 EnumSet 非常合适,如果仅仅需要遍历所有常量的值可以使用 Enum.values() 方法。

在下面的代码端中你可以看到 EnumSet 是如何创建常量子集以及它如何使用:

public class Pizza {
 
    private static EnumSet<PizzaStatus> undeliveredPizzaStatuses =
      EnumSet.of(PizzaStatus.ORDERED, PizzaStatus.READY);
 
    private PizzaStatus status;
 
    public enum PizzaStatus {
        ...
    }
 
    public boolean isDeliverable() {
        return this.status.isReady();
    }
 
    public void printTimeToDeliver() {
        System.out.println("Time to delivery is " + 
          this.getStatus().getTimeToDelivery() + " days");
    }
 
    public static List<Pizza> getAllUndeliveredPizzas(List<Pizza> input) {
        return input.stream().filter(
          (s) -> undeliveredPizzaStatuses.contains(s.getStatus()))
            .collect(Collectors.toList());
    }
 
    public void deliver() { 
        if (isDeliverable()) { 
            PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()
              .deliver(this); 
            this.setStatus(PizzaStatus.DELIVERED); 
        } 
    }
     
    // Methods that set and get the status variable.
}

执行下面的测试代码显示了实现了 Set 接口的 EnumSet 的强大功能:

@Test
public void givenPizaOrders_whenRetrievingUnDeliveredPzs_thenCorrectlyRetrieved() {
    List<Pizza> pzList = new ArrayList<>();
    Pizza pz1 = new Pizza();
    pz1.setStatus(Pizza.PizzaStatus.DELIVERED);
 
    Pizza pz2 = new Pizza();
    pz2.setStatus(Pizza.PizzaStatus.ORDERED);
 
    Pizza pz3 = new Pizza();
    pz3.setStatus(Pizza.PizzaStatus.ORDERED);
 
    Pizza pz4 = new Pizza();
    pz4.setStatus(Pizza.PizzaStatus.READY);
 
    pzList.add(pz1);
    pzList.add(pz2);
    pzList.add(pz3);
    pzList.add(pz4);
 
    List<Pizza> undeliveredPzs = Pizza.getAllUndeliveredPizzas(pzList); 
    assertTrue(undeliveredPzs.size() == 3); 
}

EnumMap

EnumMapMap 的一种特殊实现,用来将枚举常量用作键。与对应的 HashMap 相比,它是一个高效且紧凑的实现。内部用数组表示。

EnumMap<Pizza.PizzaStatus, Pizza> map;

让我们看一个它在实际中如何使用的例子:

public static EnumMap<PizzaStatus, List<Pizza>> 
  groupPizzaByStatus(List<Pizza> pizzaList) {
    EnumMap<PizzaStatus, List<Pizza>> pzByStatus = new EnumMap<PizzaStatus, List<Pizza>>(PizzaStatus.class);
     
    for (Pizza pz : pizzaList) {
        PizzaStatus status = pz.getStatus();
        if (pzByStatus.containsKey(status)) {
            pzByStatus.get(status).add(pz);
        } else {
            List<Pizza> newPzList = new ArrayList<Pizza>();
            newPzList.add(pz);
            pzByStatus.put(status, newPzList);
        }
    }
    return pzByStatus;
}

执行下面的测试代码证明了实现 Map 接口的 EnumMap 的强大功能:

@Test
public void givenPizaOrders_whenGroupByStatusCalled_thenCorrectlyGrouped() {
    List<Pizza> pzList = new ArrayList<>();
    Pizza pz1 = new Pizza();
    pz1.setStatus(Pizza.PizzaStatus.DELIVERED);
 
    Pizza pz2 = new Pizza();
    pz2.setStatus(Pizza.PizzaStatus.ORDERED);
 
    Pizza pz3 = new Pizza();
    pz3.setStatus(Pizza.PizzaStatus.ORDERED);
 
    Pizza pz4 = new Pizza();
    pz4.setStatus(Pizza.PizzaStatus.READY);
 
    pzList.add(pz1);
    pzList.add(pz2);
    pzList.add(pz3);
    pzList.add(pz4);
 
    EnumMap<Pizza.PizzaStatus,List<Pizza>> map = Pizza.groupPizzaByStatus(pzList);
    assertTrue(map.get(Pizza.PizzaStatus.DELIVERED).size() == 1);
    assertTrue(map.get(Pizza.PizzaStatus.ORDERED).size() == 2);
    assertTrue(map.get(Pizza.PizzaStatus.READY).size() == 1);
}

用枚举实现设计模式

单例模式

通常,使用类实现单例模式是不容易的,枚举提供了一种简单快速的方式实现枚举。

另外,由于枚举类在底层实现了 Serializable 接口,所以 JVM 会保证该类是单例的。这与传统的实现方式不同,在传统的实现方式中,我们必须确保在反序列化期间不创建任何新实例。

在下面的代码片展示了如何使用枚举实现单例模式:

public enum PizzaDeliverySystemConfiguration {
    INSTANCE;
    PizzaDeliverySystemConfiguration() {
        // Initialization configuration which involves
        // overriding defaults like delivery strategy
    }
 
    private PizzaDeliveryStrategy deliveryStrategy = PizzaDeliveryStrategy.NORMAL;
 
    public static PizzaDeliverySystemConfiguration getInstance() {
        return INSTANCE;
    }
 
    public PizzaDeliveryStrategy getDeliveryStrategy() {
        return deliveryStrategy;
    }
}

策略模式

通常,策略模式是通过具有由不同类实现的接口来编写的。

添加一个新的策略意味着添加一个新的实现类。有了枚举,可以很容易的达成这个目的。添加一个新的实现类意味着仅仅定义具有某种实现的另一个实例。

下面的代码展示了如何使用枚举实现策略模式:

public enum PizzaDeliveryStrategy {
    EXPRESS {
        @Override
        public void deliver(Pizza pz) {
            System.out.println("Pizza will be delivered in express mode");
        }
    },
    NORMAL {
        @Override
        public void deliver(Pizza pz) {
            System.out.println("Pizza will be delivered in normal mode");
        }
    };
 
    public abstract void deliver(Pizza pz);
}

给 Pizza 类添加如下方法:

public void deliver() {
    if (isDeliverable()) {
        PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()
          .deliver(this);
        this.setStatus(PizzaStatus.DELIVERED);
    }
}
@Test
public void givenPizaOrder_whenDelivered_thenPizzaGetsDeliveredAndStatusChanges() {
    Pizza pz = new Pizza();
    pz.setStatus(Pizza.PizzaStatus.READY);
    pz.deliver();
    assertTrue(pz.getStatus() == Pizza.PizzaStatus.DELIVERED);
}

Java 8 与枚举

Pizza 类可以使用 Java 8 重写,你可以看到用了 Java 8 中的 Lambda 和 Stream API 后 getAllUndeliveredPizzas()groupPizzaByStatus() 变得非常简洁。

public static List<Pizza> getAllUndeliveredPizzas(List<Pizza> input) {
    return input.stream().filter(
      (s) -> !deliveredPizzaStatuses.contains(s.getStatus()))
        .collect(Collectors.toList());
}
public static EnumMap<PizzaStatus, List<Pizza>> 
  groupPizzaByStatus(List<Pizza> pzList) {
    EnumMap<PizzaStatus, List<Pizza>> map = pzList.stream().collect(
      Collectors.groupingBy(Pizza::getStatus,
      () -> new EnumMap<>(PizzaStatus.class), Collectors.toList()));
    return map;
}

枚举类型的 JSON 表现形式

使用了 Jackson 库后,可以将枚举类型的 JSON 表示为 POJOs,下面的代码段展示了可以用于上述目的的 Jackson 注解:

@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum PizzaStatus {
    ORDERED (5){
        @Override
        public boolean isOrdered() {
            return true;
        }
    },
    READY (2){
        @Override
        public boolean isReady() {
            return true;
        }
    },
    DELIVERED (0){
        @Override
        public boolean isDelivered() {
            return true;
        }
    };
 
    private int timeToDelivery;
 
    public boolean isOrdered() {return false;}
 
    public boolean isReady() {return false;}
 
    public boolean isDelivered(){return false;}
 
    @JsonProperty("timeToDelivery")
    public int getTimeToDelivery() {
        return timeToDelivery;
    }
 
    private PizzaStatus (int timeToDelivery) {
        this.timeToDelivery = timeToDelivery;
    }
}

我们可以用以下的方式使用 Pizza 和 PizzaStatus:

Pizza pz = new Pizza();
pz.setStatus(Pizza.PizzaStatus.READY);
System.out.println(Pizza.getJsonString(pz));

Pizzas 状态的 JSON 表现形式:

{
  "status" : {
    "timeToDelivery" : 2,
    "ready" : true,
    "ordered" : false,
    "delivered" : false
  },
  "deliverable" : true
}

有关枚举类型的 JSON 序列化以及反序列化,请参考 Jackson-将枚举序列化为 JSON 对象

总结

在这篇文章中,我们从基础知识、高级用法以及实际场景分别探索了 Java 中的枚举使用。