Hướng dẫn về Java Enums

1. Khái quát chung

Trong bài viết này, chúng ta sẽ xem Java enums là gì, chúng giải quyết những vấn đề gì và một số mẫu thiết kế chúng có thể được sử dụng trong thực tế như thế nào.

Các enum từ khóa đã được giới thiệu trong Java 5. Nó biểu thị một loại đặc biệt của lớp đó luôn mở rộng java.lang.Enum lớp. Để có tài liệu chính thức về cách sử dụng của chúng, hãy xem tài liệu.

Các hằng số được định nghĩa theo cách này làm cho mã dễ đọc hơn, cho phép kiểm tra thời gian biên dịch, ghi trước danh sách các giá trị được chấp nhận và tránh hành vi không mong muốn do các giá trị không hợp lệ được chuyển vào.

Dưới đây là một ví dụ nhanh chóng và đơn giản về enum xác định trạng thái của một đơn hàng pizza; trạng thái đơn hàng có thể là ĐÃ ĐẶT HÀNG , ĐÃ SN SÀNG hoặc ĐÃ GIAO HÀNG :

public enum PizzaStatus { ORDERED, READY, DELIVERED; }

Ngoài ra, chúng đi kèm với nhiều phương thức hữu ích, mà nếu không, bạn sẽ phải tự viết nếu bạn đang sử dụng các hằng số cuối cùng tĩnh công khai truyền thống.

2. Phương pháp Enum tùy chỉnh

Được rồi, bây giờ chúng ta đã hiểu cơ bản về enum là gì và cách bạn có thể sử dụng chúng, hãy đưa ví dụ trước của chúng ta lên cấp độ tiếp theo bằng cách xác định một số phương thức API bổ sung trên enum:

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. } 

3. So sánh các kiểu Enum bằng toán tử “==”

Vì các kiểu enum đảm bảo rằng chỉ có một trường hợp hằng số tồn tại trong JVM, chúng ta có thể sử dụng toán tử “==” một cách an toàn để so sánh hai biến như đã thấy trong ví dụ trên; hơn nữa, toán tử “==” cung cấp sự an toàn trong thời gian biên dịch và thời gian chạy.

Đầu tiên chúng ta hãy xem xét sự an toàn trong thời gian chạy trong đoạn mã sau, trong đó toán tử “==” được sử dụng để so sánh các trạng thái và NullPointerException sẽ không được ném nếu một trong hai giá trị là rỗng . Ngược lại, một NullPointerException sẽ được ném nếu phương thức bằng được sử dụng:

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

Đối với an toàn thời gian biên dịch , hãy xem một ví dụ khác trong đó một enum thuộc loại khác được so sánh bằng cách sử dụng phương thức bằng được xác định là đúng - bởi vì các giá trị của enum và phương thức getStatus trùng hợp là giống nhau, nhưng về mặt logic thì so sánh nên sai. Vấn đề này được tránh bằng cách sử dụng toán tử “==”.

Trình biên dịch sẽ gắn cờ so sánh là lỗi không tương thích:

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

4. Sử dụng các loại Enum trong các câu lệnh Switch

Các kiểu enum cũng có thể được sử dụng trong câu lệnh switch :

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

5. Trường, phương thức và hàm tạo trong Enums

Bạn có thể xác định các hàm tạo, phương thức và trường bên trong các kiểu enum làm cho nó rất mạnh mẽ.

Chúng ta hãy mở rộng ví dụ trên và thực hiện quá trình chuyển đổi từ một giai đoạn của một bánh pizza khác và xem làm thế nào chúng ta có thể thoát khỏi sự nếu tuyên bố và chuyển đổi tuyên bố sử dụng trước đó:

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. } 

Đoạn mã thử nghiệm bên dưới minh họa cách thức hoạt động của điều này:

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

6. EnumSetEnumMap

6.1. EnumSet

Các EnumSet là một chuyên Set thực hiện có nghĩa là để được sử dụng với Enum loại.

Nó là một biểu diễn rất hiệu quả và nhỏ gọn của một Tập hợp các hằng số Enum cụ thể khi so sánh với một HashSet , do Biểu diễn Vector Bit bên trong được sử dụng. Và nó cung cấp một loại an toàn thay thế cho truyền thống int dựa trên “cờ bit”, cho phép chúng ta viết mã ngắn gọn đó là dễ đọc hơn và duy trì.

Các EnumSet là một lớp trừu tượng có hai triển khai gọi RegularEnumSetJumboEnumSet , một trong số đó được chọn tùy thuộc vào số lượng các hằng số trong enum tại thời điểm instantiation.

Do đó, bạn nên sử dụng tập hợp này bất cứ khi nào chúng ta muốn làm việc với tập hợp các hằng số enum trong hầu hết các trường hợp (như tập hợp con, thêm, xóa và cho các hoạt động hàng loạt như containsAllremoveAll ) và dùng Enum.values ​​( ) nếu bạn chỉ muốn lặp qua tất cả các hằng số có thể.

Trong đoạn mã dưới đây, bạn có thể thấy cách EnumSet được sử dụng để tạo một tập hợp con các hằng số và cách sử dụng nó:

public class Pizza { private static EnumSet 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 getAllUndeliveredPizzas(List 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. } 

Việc thực hiện thử nghiệm sau đã chứng minh sức mạnh của việc triển khai EnumSet của giao diện Set :

@Test public void givenPizaOrders_whenRetrievingUnDeliveredPzs_thenCorrectlyRetrieved() { List 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 undeliveredPzs = Pizza.getAllUndeliveredPizzas(pzList); assertTrue(undeliveredPzs.size() == 3); }

6.2. EnumMap

EnumMap là một triển khai Bản đồ chuyên biệt có nghĩa là được sử dụng với các hằng số enum làm khóa. Nó là một triển khai hiệu quả và nhỏ gọn so với HashMap đối tác của nó và được biểu diễn bên trong dưới dạng một mảng:

EnumMap map; 

Hãy xem nhanh một ví dụ thực tế cho thấy nó có thể được sử dụng như thế nào trong thực tế:

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

Việc thực hiện thử nghiệm sau đây đã chứng minh sức mạnh của việc triển khai EnumMap của giao diện Bản đồ :

@Test public void givenPizaOrders_whenGroupByStatusCalled_thenCorrectlyGrouped() { List 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
    
      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); }
    

7. Triển khai các mẫu thiết kế bằng Enums

7.1. Mô hình Singleton

Thông thường, việc triển khai một lớp bằng cách sử dụng mẫu Singleton là khá không tầm thường. Enums cung cấp một cách dễ dàng và nhanh chóng để triển khai các singleton.

In addition to that, since the enum class implements the Serializable interface under the hood, the class is guaranteed to be a singleton by the JVM, which unlike the conventional implementation where we have to ensure that no new instances are created during deserialization.

In the code snippet below, we see how we can implement singleton pattern:

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; } }

7.2. Strategy Pattern

Conventionally the Strategy pattern is written by having an interface that is implemented by different classes.

Adding a new strategy meant adding a new implementation class. With enums, this is achieved with less effort, adding a new implementation means defining just another instance with some implementation.

The code snippet below shows how to implement the Strategy pattern:

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); }

Add the following method to the Pizza class:

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); }

8. Java 8 and Enums

The Pizza class can be rewritten in Java 8, and you can see how the methods getAllUndeliveredPizzas() and groupPizzaByStatus() become so concise with the use of lambdas and the Stream APIs:

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

9. JSON Representation of Enum

Using Jackson libraries, it is possible to have a JSON representation of enum types as if they are POJOs. The code snippet below shows the Jackson annotations that can be used for the same:

@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; } } 

We can use the Pizza and PizzaStatus as follows:

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

to generate the following JSON representation of the Pizzas status:

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

For more information on JSON serializing/deserializing (including customization) of enum types refer to the Jackson – Serialize Enums as JSON Objects.

10. Conclusion

Trong bài viết này, chúng ta đã khám phá Java enum, từ những kiến ​​thức cơ bản về ngôn ngữ cho đến những trường hợp sử dụng thực tế nâng cao và thú vị hơn.

Các đoạn mã từ bài viết này có thể được tìm thấy trong kho Github.