Перечисления Java: два типа перечислений, каждый из которых содержит ссылки друг на друга?
Есть ли способ обойти проблемы загрузки класса, вызванные наличием двух перечислений, которые ссылаются друг на друга?
У меня есть два набора перечислений, Foo и Bar, определенные так:
public class EnumTest {
public enum Foo {
A(Bar.Alpha),
B(Bar.Delta),
C(Bar.Alpha);
private Foo(Bar b) {
this.b = b;
}
public final Bar b;
}
public enum Bar {
Alpha(Foo.A),
Beta(Foo.C),
Delta(Foo.C);
private Bar(Foo f) {
this.f = f;
}
public final Foo f;
}
public static void main (String[] args) {
for (Foo f: Foo.values()) {
System.out.println(f + " bar " + f.b);
}
for (Bar b: Bar.values()) {
System.out.println(b + " foo " + b.f);
}
}
}
Вышеприведенный код выводит как результат:
A bar Alpha
B bar Delta
C bar Alpha
Alpha foo null
Beta foo null
Delta foo null
Я понимаю, почему это происходит - JVM начинает загрузку классов Foo; он видит Bar.Alpha в Foo.A-конструкторе, поэтому он запускает classloading Bar. Он видит ссылку Foo.A в вызове конструктора Bar.Alpha, но (поскольку мы все еще находимся в Foo.A-конструкторе) Foo.A на данный момент равно null, поэтому конструктор Bar.Alpha получает значение null. Если я отменил два для циклов (или в противном случае ссылку Bar перед Foo), выход изменится так, чтобы значения Bar были правильными, но значения Foo не являются.
Есть ли способ обойти это? Я знаю, что могу создать статическую карту и статическую карту в 3-м классе, но для меня это довольно грустно. Я мог бы также создавать методы Foo.getBar() и Bar.getFoo(), которые относятся к внешней карте, поэтому он даже не изменит мой интерфейс (фактические классы, в которых я использую инспекторов вместо публичных полей), но он все еще чувствует вроде нечистого для меня.
(Причина, по которой я делаю это в своей реальной системе: Foo и Bar представляют собой типы сообщений, которые 2 приложения отправляют друг другу, поля Foo.b и Bar.f представляют ожидаемый тип ответа для данного сообщения - поэтому в моем примере кода, когда app_1 получает Foo.A, ему нужно ответить с помощью Bar.Alpha и наоборот.)
Спасибо заранее!
Ответы
Ответ 1
Один из лучших способов - использование метода полиморфизма перечисления:
public class EnumTest {
public enum Foo {
A {
@Override
public Bar getBar() {
return Bar.Alpha;
}
},
B {
@Override
public Bar getBar() {
return Bar.Delta;
}
},
C {
@Override
public Bar getBar() {
return Bar.Alpha;
}
},
;
public abstract Bar getBar();
}
public enum Bar {
Alpha {
@Override
public Foo getFoo() {
return Foo.A;
}
},
Beta {
@Override
public Foo getFoo() {
return Foo.C;
}
},
Delta {
@Override
public Foo getFoo() {
return Foo.C;
}
},
;
public abstract Foo getFoo();
}
public static void main(String[] args) {
for (Foo f : Foo.values()) {
System.out.println(f + " bar " + f.getBar());
}
for (Bar b : Bar.values()) {
System.out.println(b + " foo " + b.getFoo());
}
}
}
Приведенный выше код выдает желаемый результат:
A bar Alpha
B bar Delta
C bar Alpha
Alpha foo A
Beta foo C
Delta foo C
См. также:
Ответ 2
Проблема заключается не столько в том, что "две перечисления ссылаются друг на друга", это больше "два перечисления ссылаются друг на друга в своих конструкторах". Эта круговая ссылка является сложной частью.
Как использовать методы Foo.setResponse(Bar b)
и Bar.setResponse(Foo f)
? Вместо того, чтобы устанавливать Foo Bar в конструкторе Foo (и аналогично Bar Foo в конструкторе Bar), вы выполняете инициализацию с помощью метода? Например.:
Foo:
public enum Foo {
A, B, C;
private void setResponse(Bar b) {
this.b = b;
}
private Bar b;
public Bar getB() {
return b;
}
static {
A.setResponse(Bar.Alpha);
B.setResponse(Bar.Delta);
C.setResponse(Bar.Alpha);
}
}
Бар:
public enum Bar {
Alpha, Beta, Delta;
private void setResponse(Foo f) {
this.f = f;
}
private Foo f;
public Foo getF() {
return f;
}
static {
Alpha.setResponse(Foo.A);
Beta.setResponse(Foo.C);
Delta.setResponse(Foo.C);
}
}
Кроме того, вы отмечаете, что Foo и Bar - это два типа сообщений. Можно ли объединить их в один тип? Из того, что я вижу, их поведение здесь одно и то же. Это не исправляет круговую логику, но это может дать вам другое представление о вашем дизайне...
Ответ 3
Поскольку, похоже, вы все равно будете жестко кодироваться, почему бы не что-то вроде
public static Bar responseBar(Foo f) {
switch(f) {
case A: return Bar.Alpha;
// ... etc
}
}
для каждого перечисления? Похоже, что у вас есть некоторые дублирующие ответы в вашем примере, так что вы даже можете воспользоваться случаями падения.
EDIT:
Мне нравится предложение Tom's EnumMap; Я думаю, что производительность, скорее всего, выше на EnumMap, но такая элегантная конструкция, описанная в Effective Java, по-видимому, не предоставляется этой конкретной проблемой - однако предлагаемое выше решение коммутатора было бы хорошим способом построить две статические EnumMaps, то ответ может быть примерно таким:
public static Bar response(Foo f) { return FooToBar.get(f); }
public static Foo response(Bar b) { return BarToFoo.get(b); }
Ответ 4
Интересный дизайн. Я вижу вашу потребность, но что вы собираетесь делать, когда требования слегка меняются, так что в ответ на Foo.Epsilon приложение_1 должно отправить либо Bar.Gamma, либо Bar.Whatsit?
Решение, которое вы рассмотрели и отбросило как хакерское (помещение отношения в карту), кажется, дает вам гораздо большую гибкость и позволяет избежать вашей круглой ссылки. Он также сохраняет ответственность разделенной: сами типы сообщений не должны нести ответственность за знание их ответа, не так ли?
Ответ 5
Вы можете использовать EnumMap и заполнить его в одном из перечислений.
private static EnumMap<Foo, LinkedList<Bar>> enumAMap;
public static void main(String[] args) throws Exception {
enumAMap = new EnumMap<Foo, LinkedList<Bar>>(Foo.class);
System.out.println(Bar.values().length); // initialize enums, prevents NPE
for (Foo a : Foo.values()) {
for (Bar b : enumAMap.get(a)) {
System.out.println(a + " -> " + b);
}
}
}
public enum Foo {
Foo1(1),
Foo2(2);
private int num;
private Foo(int num) {
this.num = num;
}
public int getNum() {
return num;
}
}
public enum Bar {
Bar1(1, Foo.Foo1),
Bar2(2, Foo.Foo1),
Bar3(3, Foo.Foo2),
Bar4(4, Foo.Foo2);
private int num;
private Foo foo;
private Bar(int num, Foo foo) {
this.num = num;
this.foo = foo;
if (!enumAMap.containsKey(foo)) {
enumAMap.put(foo, new LinkedList<Bar>());
}
enumAMap.get(foo).addLast(this);
}
public int getNum() {
return num;
}
public Foo getFoo() {
return foo;
}
}
Вывод:
4
Foo1 -> Bar1
Foo1 -> Bar2
Foo2 -> Bar3
Foo2 -> Bar4