Перечисления Java: два типа перечислений, каждый из которых содержит ссылки друг на друга? - PullRequest
30 голосов
/ 02 октября 2009

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

У меня есть два набора перечислений, 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, поэтому он начинает загрузку классов Bar. Он видит ссылку Foo.A в вызове конструктора Bar.Alpha, но (так как мы все еще в конструкторе Foo.A) Foo.A на этом этапе является нулевым, поэтому конструктор Bar.Alpha получает нулевое значение. Если я переверну два цикла for (или иным образом ссылаюсь на Bar перед Foo), выходные данные изменятся так, что все значения Bar будут правильными, а значения Foo - нет.

Есть ли способ обойти это? Я знаю, что могу создать статическую карту и статическую карту в третьем классе, но мне это кажется довольно хакерским. Я также мог бы создать методы Foo.getBar () и Bar.getFoo (), которые ссылаются на внешнюю карту, чтобы он даже не изменил мой интерфейс (реальные классы, в которых я использую инспекторы вместо открытых полей), но он все еще чувствует немного нечист для меня.

(Причина, по которой я делаю это в моей реальной системе: Foo и Bar представляют типы сообщений, которые 2 приложения посылают друг другу; поля Foo.b и Bar.f представляют ожидаемый тип ответа для данного сообщения - поэтому в моем примере кода, когда app_1 получает Foo.A, он должен ответить Bar.Alpha и наоборот.)

Заранее спасибо!

Ответы [ 5 ]

21 голосов
/ 23 декабря 2011

Один из лучших способов - использовать метод полиморфизма перечисления :

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

Смотри также:

11 голосов
/ 02 октября 2009

Проблема не столько в «двух перечислениях, ссылающихся друг на друга», сколько в том, что «два перечисления ссылаются друг на друга в своих конструкторах ». Эта круговая ссылка является хитрой частью.

Как насчет использования Foo.setResponse(Bar b) и Bar.setResponse(Foo f) методов? Вместо установки Foo's 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 - это два типа сообщений. Возможно ли объединить их в один тип? Из того, что я вижу, их поведение здесь то же самое. Это не исправляет круговую логику, но может дать вам другое представление о вашем дизайне ...

4 голосов
/ 02 октября 2009

Поскольку кажется, что вы все равно будете жестко программировать, почему бы не получить что-то вроде

public static Bar responseBar(Foo f) {
 switch(f) {
  case A: return Bar.Alpha;
  // ... etc
 }
}

для каждого перечисления? Похоже, что у вас есть несколько совпадающих ответов в вашем примере, так что вы даже можете воспользоваться преуспевающими случаями.

EDIT:

Мне нравится предложение Тома о 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); }
2 голосов
/ 02 октября 2009

Интересный дизайн. Я вижу ваши потребности, но что вы будете делать, когда требования слегка изменятся, так что в ответ на Foo.Epsilon app_1 должен отправить либо Bar.Gamma или Bar.Whatsit?

Решение, которое вы сочли хакерским и от которого отказались (поместив отношение в карту), кажется, дает вам гораздо больше гибкости и позволяет избежать круговой ссылки. Это также позволяет разделить ответственность: сами типы сообщений не должны отвечать за знание своего ответа, не так ли?

1 голос
/ 06 июля 2017

Вы можете использовать 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
...