Перечисление (Enumeration) в Java представляют собой класс, состоящий из констант. Появились в языке для удобной реализации перечисляемых типов данных. Например, светофор имеет три цвета: красный, желтый и зеленый. Вместо того, чтобы создавать отдельный статический класс с константами типа Color, у которых есть поле name в которое уже записано имя цвета.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class Color { private String name; Color(String name) { this.name = name; } @Override public String toString() { return "Color is " + name; } } public class NotEnum { static final class TrafficLight { static final Color RED = new Color("Red"); static final Color YELLOW = new Color("Yellow"); static final Color GREEN = new Color("Green"); } public static void main(String[] args) { Color currentColor = TrafficLight.RED; System.out.println(currentColor); } } |
Конструкция получилась очень неудобной. Ко всему прочему при такой реализации вы можете создать экземпляр класса TrafficLight, а это противоречит понятию перечисляемых типов. Давайте перепишем TrafficLight с помощью перечисляемых классов и реализуем функцию его переключения.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
enum TrafficLightColor { RED, YELLOW, GREEN } public class TLEnum { private TrafficLightColor color = TrafficLightColor.RED; void changeTLColor() { switch (color) { case RED: color = TrafficLightColor.GREEN; break; case GREEN: color = TrafficLightColor.YELLOW; break; case YELLOW: color = TrafficLightColor.RED; break; } } @Override public String toString() { return "TrafficLightColor is " + color.name(); } public static void main(String[] args) { TLEnum tlEnum = new TLEnum(); for (int i = 0; i < 5; i++) { tlEnum.changeTLColor(); System.out.println(tlEnum); } } } |
Вся логика первого примера уместилась в 5 строчек второго. Что происходит в этом примере? Мы создаем перечисление с помощью ключевого слова enum. По сути это обычный класс, который наследуется от java.lang.Enum, однако, для перечислений существует ряд ограничений. Во-первых, вы не можете создать экземпляр с перечисления с помощью конструктора. При компиляции будет создан приватный конструктор по умолчанию. Во-вторых, нельзя создать потомков перечисления. В остальном их можно использовать как обычный класс. Справедливости ради стоит заметить, что обычно структура перечислений остается простой.
Рассмотрим некоторые возможности перечислений.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public enum EnumExample { IGOR("Denis"), RENAT("Photo"), AVTO("Niva"); private String desc; //Вы можете добавить параметр к экземплярам перечисления EnumExample(String desc) { this.desc = desc; } //Можно переопределить метод toString() базового класса @Override public String toString() { return name() + " " + desc; } //Можно создавать свои методы public void getHashCode() { System.out.println("HashCode for " + name() +" "+ this.hashCode()); } //можно вызывать метод main public static void main(String[] args) { //метод values() возвращает массив перечислений for (EnumExample enm : EnumExample.values()) { //имя System.out.println("Имя: " + enm.name()); //порядковый номер System.out.println("Номер: " + enm.ordinal()); //выводим описание перечисления System.out.println("Описание: " + enm.desc); //вызываем toString() System.out.println("toString(): " + enm); //вызываем метод перечисления enm.getHashCode(); } } } |
- Для каждого перечисления можно добавить параметр, инициализировав его в конструкторе. Как видно из примера IGOR(“Denis”) имеет параметр desc=”Denis”, который задается в конструкторе. Конструктор не может быть public, потому что нельзя создать новый экземпляр перечисления, помимо объявленных. Если посмотреть на байткод скомпилированного класса, то мы увидим, что коснструктор принудительно будет объявлен как private, даже если мы оставили его с default.
1234567891011121314151617181920212223// access flags 0x2// signature (Ljava/lang/String;)V// declaration: void <init>(java.lang.String)private <init>(Ljava/lang/String;ILjava/lang/String;)VL0LINENUMBER 9 L0ALOAD 0ALOAD 1ILOAD 2INVOKESPECIAL java/lang/Enum.<init> (Ljava/lang/String;I)VL1LINENUMBER 10 L1ALOAD 0ALOAD 3PUTFIELD EnumExample.desc : Ljava/lang/String;L2LINENUMBER 11 L2RETURNL3LOCALVARIABLE this LEnumExample; L0 L3 0LOCALVARIABLE desc Ljava/lang/String; L0 L3 3MAXSTACK = 3MAXLOCALS = 4
В примере мы передали параметр String, но на деле можно передать любой объект и даже не один. - Есть возможность переопределить метод базового класса toString(). Какого базового класса? – спросите вы. java.lang.enum – отвечу я. Но ведь мы явно не указывали родителя? Это так. За нас это сделал компилятор.
123456789// class version 52.0 (52)// access flags 0x4031// signature Ljava/lang/Enum<LEnumExample;>;// declaration: EnumExample extends java.lang.Enum<EnumExample>public final enum EnumExample extends java/lang/Enum {// compiled from: EnumExample.java// access flags 0x4019
Мы видим ключевое слово extends и класс java.lang.enum. Любое созданое перечисление будет всегда наследоваться от него. Переопределить можно только метод toString(), потому что остальные методы базового класса являются final. - Можно создавать свои методы. Эта возможность открывает перед перечислениями целый спектр прикладных задач, в которых их можно использовать. Например, конечные автоматы – граф состояний, причем число состояний конечно. Яркий пример такого автомата – вендинговый автомат с ништяками. Его состояния: принять 100р, принять 50р, принять 10р, выдать товар, выдать сдачу, недостаточно средств, неверный код товара. Эти состояния можно заполнить в перечислении, добавив метод, который будет изменять состояния в зависимости от входных данных. Это лишь один из вариантов возможного использования перечислений.
- Метод ordinal() возвращает порядковый номер экземпляра перечисления (начиная с 0) в котором он был записан. Для IGOR – 0, для RENAT – 1, для AVTO – 2.
Подозрительный метод values()
В нашем примере есть вызов
1 |
for (EnumExample enm : EnumExample.values()) |
Понятно, что метод values() возвращает массив из всех экземпляров перечисления. В нашем примере этот метод мы явно нигде не указали. Но для нас это не проблема, ведь есть базовый класс, из которого мы вызываем, например, метод ordinal(), значит в нем и описан метод values(). Но ЭТОГО МЕТОДА В КЛАССЕ JAVA.LANG.ENUM НЕТ! Как такое может быть? Метод нигде явно не описан, но он есть. В очередной раз обратившись к байткоду мы увидим, что метод values() появится в нашем классе после компиляции.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public final enum EnumExample extends java/lang/Enum { // compiled from: EnumExample.java // access flags 0x4019 public final static enum LEnumExample; IGOR // access flags 0x4019 public final static enum LEnumExample; RENAT // access flags 0x4019 public final static enum LEnumExample; AVTO // access flags 0x2 private Ljava/lang/String; desc // access flags 0x101A private final static synthetic [LEnumExample; $VALUES // access flags 0x9 public static values()[LEnumExample; L0 LINENUMBER 1 L0 GETSTATIC EnumExample.$VALUES : [LEnumExample; INVOKEVIRTUAL [LEnumExample;.clone ()Ljava/lang/Object; CHECKCAST [LEnumExample; ARETURN MAXSTACK = 1 MAXLOCALS = 0 |
Так же видно, что помимо наших экземпляров создалась ссылка $VALUES как поле класса. Ниже мы видим скомпилированный метод values(). Согласитесь, это странно. Метод нигде не описан, но при компиляции он появляется! Не верите? Проверьте сами.