Статьи

Трюки Enum: иерархическая структура данных

Перечисления Java обычно используются для хранения массивов данных. Этот совет показывает, как использовать enum для иерархических структур.

 

мотивация

Давным-давно я хотел создать enum, который содержит различные операционные системы, т.е.

public enum OsType {
WindowsNTWorkstation,
WindowsNTServer,
Windows2000Server,
Windows2000Workstation,
WindowsXp,
WindowsVista,
Windows7,
Windows95,
Windows98,
Fedora,
Ubuntu,
Knopix,
SunOs,
HpUx,

Мне не понравилась эта структура, потому что я хотел бы видеть группу WindowsNT, которая содержит WinNTWorkstation и сервер WindNT. Все версии Windows должны быть в супер группе «Windows». Fedora, Knopix и Ubuntu являются дистрибутивами Linux. Все дистрибутивы Linux вместе с SunO и HpUx являются системами Unix. Все системы Windows имеют общие свойства. То же самое касается систем Unix. И я ненавижу программирование копирования / вставки.


 

 

 

Решения

Как всегда, есть несколько решений.

Решение для класса на ОС

Очевидным решением здесь является создание отдельных классов для каждой операционной системы и абстрактных классов, которые представляют группы ОС. Например, класс Fedora расширяет класс Linux, который расширяет класс Unix, который расширяет класс OperatingSystem. Мы можем пользоваться всеми преимуществами наследования, поэтому все общие свойства ОС Windows хранятся в классе Windows и могут быть переопределены его подклассами.

Но теперь мы не можем видеть все операционные системы вместе, перебирать их и т. Д., Т. Е. Отсутствуют очень полезные функции перечисления Java.

Нет проблем! Теперь мы можем создать enum, как и предыдущий, который содержит настраиваемое поле типа Class:

public enum OsType {
WindowsNTWorkstation(WindowsNTWorkstation.class),
WindowsNTServer(WindowsNTServer.class),
Windows2000Server(Windows2000Server.class),
Windows2000Workstation(Windows2000Workstation.class),
WindowsXp(WindowsXp.class),
WindowsVista(WindowsVista.class),
Windows7(Windows7.class),
Windows95(Windows7.class),
Windows98(Windows98.class),
Fedora(Fedora.class),
Ubuntu(Ubuntu.class),
Knopix(Knopix.class),
SunOs(SunOs.class),
HpUx(HpUx.class),
;
private Class clazz;
OsType(Class clazz) {
this.clazz = clazz;
}
}

Это решение лучше, но оно все же имеет недостатки:

  1. Реализация метода, который извлекает всех «потомков» конкретной ОС (например, всех дистрибутивов Linux), сложна и неэффективна.
  2. Группировка отделена от enum.
  3. Решение очень многословно: каждая ОС представлена ​​своим собственным классом, даже если этому классу нечего переопределять.

Иерархический Enum

Для создания иерархии с помощью enum нам нужно настраиваемое поле «parent», которое инициализируется конструктором:

public enum OsType {
OS(null),
Windows(OS),
WindowsNT(Windows),
WindowsNTWorkstation(WindowsNT),
WindowsNTServer(WindowsNT),
Windows2000(Windows),
Windows2000Server(Windows2000),
Windows2000Workstation(Windows2000),
WindowsXp(Windows),
WindowsVista(Windows),
Windows7(Windows),
Windows95(Windows),
Windows98(Windows),
Unix(OS) {
@Override
public boolean supportsXWindows() {
return true;
}
},
Linux(Unix),
AIX(Unix),
HpUx(Unix),
SunOs(Unix),
;
private OsType parent = null;

private OsType(OsType parent) {
this.parent = parent;
}
}

Эта структура позволяет реализовать метод «is», который работает как оператор instanceof для классов и интерфейсов. Например, Windows2000 — это Windows, Fedora — это Linux, Windows — это не Unix и т. Д.


public boolean is(OsType other) {
if (other == null) {
return false;
}

for (OsType t = this; t != null; t = t.parent) {
if (other == t) {
return true;
}
}
return false;
}

Иногда нам нужен метод, который возвращает все «потомки» текущих узлов, например, все системы Linux или все варианты Windows2000. Самый простой способ реализовать это — сохранить коллекцию дочерних элементов для каждого элемента и заполнить ее из конструктора:

private List<OsType> children = new ArrayList<OsType>();
private OsType(OsType parent) {
this.parent = parent;
if (this.parent != null) {
this.parent.addChild(this);
}
}

Теперь метод children (), который возвращает потомков прямого узла, тривиален:

public OsType[] children() {
return children.toArray(new OsType[children.size()]);
}

Нетрудно реализовать рекурсивный метод «allChildren ()», который возвращает все дочерние элементы текущего узла (см.
Полный исходный код ). 

Но иерархический термин всегда сопровождается наследованием, что позволяет переопределять методы родителя. Это основная особенность классов во всех объектно-ориентированных языках. Можно ли реализовать некие наследственные отношения для элементов одного перечисления?

 

Переопределение родительского метода

Системы Unix поддерживают графическое окружение X Window. MS Windows нет. Мы хотели бы спросить у ОС, поддерживает ли она X Window.

Мы можем определить логический флаг «SupportX» и логический метод

public boolean supportsX() {return suppotsX;}

Теперь нам нужно добавить еще один аргумент в конструктор OsType и передать true / false для каждого элемента перечисления. Но это слишком многословно. Можно ли сказать, что Unix поддерживает X, Windows не поддерживает X и быть уверенным, что supportX () Fedora возвращает true, а supportX () Winddows95 возвращает false?

Реализация довольно проста. Сначала для простоты скажем, что X Window поддерживается всеми системами Unix и не поддерживается другими.

Итак, мы можем реализовать метод supportsXWindowSystem () на уровне перечисления следующим образом:

public boolean supportsXWindowSystem() {
return false;
}

Теперь мы должны переопределить его для всех систем Unix. Чтобы реализовать это, мы изменим реализацию по умолчанию на следующее:

public boolean supportsXWindowSystem() {
return parent == null ? false : parent.supportsXWindowSystem();
}

Будет использован метод первого родителя в иерархии, который реализует метод. Если никто из родителей и родителей родителей не реализует этот метод сам, мы называем метод корневого элемента. 
Теперь мы можем сказать следующее:


...
Unix(OS) {
@Override
public boolean supportsXWindowSystem() {
return true;
}
},
Linux(Unix),
AIX(Unix),
HpUx(Unix),
SunOs(Unix),
...


Метод переопределяется только для элемента Unix, и все его дочерние элементы будут использовать этот метод.

Проблема решена. Мы получили иерархическую полиморфную структуру на основе enum! Мы можем реализовать метод в базовом элементе (используя его как суперкласс), а затем переопределить его в любом элементе, который мы хотим.

Единственный недостаток этого решения состоит в том, что теперь нам нужно создать аналогичную реализацию для каждого метода, который мы добавляем к этому перечислению, и для всех других перечислений, которые содержат иерархическую структуру. 

Выводы

 
Хотя мы регулярно используем перечисления в качестве статических массивов, они также могут использоваться для представления иерархических древовидных структур данных, где каждый узел может найти своего родителя, своих потомков и даже наследовать и переопределять родительский метод почти точно так же, как мы делаем с классом. наследование.

 

Подтверждения

Первая версия этой статьи была написана в моем блоге. Метод поддерживает XWindowSystem (), реализованный с использованием отражения. 4 парня обсудили это решение и предложили мне его упростить. Я хотел бы поблагодарить Тодо, Антона Дамлера, Ника и Йоханнеса Шнайдера, которые помогли мне улучшить статью.