【问题标题】:Self referential enum with immutable parameters具有不可变参数的自引用枚举
【发布时间】:2013-10-17 14:22:11
【问题描述】:

考虑以下sscce

public enum Flippable 
  A (Z), B (Y), Y (B), Z (A);

  private final Flippable opposite;

  private Flippable(Flippable opposite) {
    this.opposite = opposite;
  }

  public Flippable flip() {
    return opposite;
  }
}

这不会编译,因为 ZY 没有被声明为允许作为 AB 的构造函数的参数。

可能的解决方案 1: 硬编码方法

public enum Flippable {
  A {
    public Flippable flip() { return Z; }
  }, B {
    public Flippable flip() { return Y; }
  }, Y {
    public Flippable flip() { return B; }
  }, Z {
    public Flippable flip() { return A; }
  };
  public abstract Flippable flip();
}

虽然实用,但从风格上看,这似乎很恶心。虽然我不知道为什么这会是一个真正的问题。

可能的解决方案 2: 静态加载

public enum Flippable {
  A, B, Y, Z;

  private Flippable opposite;

  static {
    for(Flippable f : Flippable.values()) {
      switch(f) {
      case A:
        f.opposite = Z;
        break;
      case B:
        f.opposite = Y;
        break;
      case Y:
        f.opposite = B;
        break;
      case Z:
        f.opposite = A;
        break;
      }
    }
  }

  public Flippable flip() {
    return opposite;
  }
}

这比第一个解决方案更严重,因为该字段不再是最终的,并且容易受到反射。归根结底,这是一个晦涩难懂的担忧,但暗示了一种糟糕的代码气味。

有没有办法做到这一点,与第一个示例基本相同,但编译正确?

【问题讨论】:

  • 你为什么要列举 Flippables?为什么不直接依次分配每个,因为无论如何您都必须在每个 case: 表达式中执行此操作!
  • @robert 因为我总是把事情复杂化 :)

标签: java enums self-reference


【解决方案1】:

也许没有你想要的那么漂亮......

public enum Flippable {
    A, B, Z, Y;

    static {
        A.opposite = Z;
        B.opposite = Y;
        Y.opposite = B;
        Z.opposite = A;
    }

    public Flippable flip() {
        return opposite;
    }

    private Flippable opposite;

    public static void main(String[] args) {         
        for(Flippable f : Flippable.values()) {
            System.out.println(f + " flips to " + f.flip());
        }
    }
}

【讨论】:

  • 到目前为止,我更喜欢这个解决方案(包括我的),但我觉得应该有某种保证,A.flip().flip() 应该总是返回A。这个解决方案(以及我的)很容易出现拼写错误。
  • 我认为没有办法解决循环引用问题
【解决方案2】:

正如您所见,这是不可能的,因为枚举常量是静态的,并且您无法初始化 A,直到 Z 未初始化。

所以这个技巧应该有效:

public enum Flippable { 
  A ("Z"), B ("Y"), Y ("B"), Z ("A");

  private final String opposite;

  private Flippable(String opposite) {
    this.opposite = opposite;
  }

  public Flippable flip() {
    return valueOf(opposite);
  }
}

【讨论】:

  • 您也可以将valueOf 放入构造函数中,以便存储实际值而不是字符串...
  • @robert 不,他不能,它不会运行。我不喜欢这个答案,因为它比较慢(每次都解析字符串)......除非 Hotspot 足够聪明来解决问题。
  • 如果它在构造函数中,它应该只在类加载时解析一次 - 但是,是的,我刚刚检查过它编译得很好但没有运行(:
  • 如果您查看Enum.valueOf(),它在内部使用Map<String, Enum> 并在那里通过键执行搜索,所以它非常快。无论如何,这取决于您的情况,选择什么。您没有指定您的标准是什么:速度、内存消耗等。这个答案绝对适用于您的问题:有没有办法做到这一点,与第一个示例基本相同,但编译正确?
  • 另一个公平点。好的,我已经投票给你了;实际上考虑到Enum.valueOf 的速度,这也解决了不变性问题,所以这实际上可能是最好的答案。
【解决方案3】:

只需映射对立面:

import java.util.*;

public enum Flippable 
{
  A, B, Y, Z;

  private static final Map<Flippable, Flippable> opposites;

  static
  {
    opposites = new EnumMap<Flippable, Flippable>(Flippable.class);
    opposites.put(A, Z);
    opposites.put(B, Y);
    opposites.put(Y, B);
    opposites.put(Z, A);

    // integrity check:
    for (Flippable f : Flippable.values())
    {
      if (f.flip().flip() != f)
      {
        throw new IllegalStateException("Flippable " + f + " inconsistent.");
      }
    }
  }

  public Flippable flip()
  {
    return opposites.get(this);
  }

  public static void main(String[] args)
  {
    System.out.println(Flippable.A.flip());
  }
}

编辑:切换到 EnumMap

【讨论】:

  • 这并不比方案2好,实际上更糟,因为它具有更高的内存消耗
  • 我想我完全是为了可读性。解决方案 2(像大多数开关一样)对我来说看起来很丑。所以我猜你对“更糟”的说法是非常主观的。恕我直言,在考虑地图与逻辑上相似的开关时,内存消耗似乎不是一个合理的论点。
  • 切换静态发生,并且只发生一次。
  • 是的,这里不需要HashMap,你可以直接分配给成员
  • 我同意使用地图对罗伯特的回答没有任何好处。但如果有人告诉你必须使用地图,EnumMap 会比 HashMap 更好。
【解决方案4】:

好问题。也许你会喜欢这个解决方案:

public class Test {
    public enum Flippable {
        A, B, Y, Z;

        private Flippable opposite;

        static {
            final Flippable[] a = Flippable.values();
            final int n = a.length;
            for (int i = 0; i < n; i++)
                a[i].opposite = a[n - i - 1];
        }

        public Flippable flip() {
            return opposite;
        }
    }

    public static void main(final String[] args) {
        for (final Flippable f: Flippable.values()) {
            System.out.println(f + " opposite: " + f.flip());
        }
    }
}

结果:

$ javac Test.java && java Test
A opposite: Z
B opposite: Y
Y opposite: B
Z opposite: A
$ 

如果你想保持实例字段“final”(这当然很好),你可以在运行时索引到数组中:

public class Test {
    public enum Flippable {
        A(3), B(2), Y(1), Z(0);

        private final int opposite;
        private Flippable(final int opposite) {
            this.opposite = opposite;
        }

        public Flippable flip() {
            return values()[opposite];
        }
    }

    public static void main(final String[] args) {
        for (final Flippable f: Flippable.values()) {
            System.out.println(f + " opposite: " + f.flip());
        }
    }
}

这也有效。

【讨论】:

  • 有趣的方法。为什么您认为这比其他答案更受欢迎?
  • 嗯,第一个解决方案会自动计算对立面,而不是对它们进行硬编码,这可以说比所需的(但不是有效的 Java)原版更干净! OTOH,它失去了不变性,这是一种耻辱。
  • 第二个解决方案基本上与所需的原始解决方案相同,只是用数字而不是名称来指定相反的部分 - 诚然,数组访问的运行时成本有点高。因此,不幸的是,两者都不是完美的,但我认为它们都以某种方式改进了其他答案......
  • 我在依赖枚举的索引值方面有过非常糟糕的经历。迟早你可能会不小心交换索引和diamonds become black, when they should be red
  • 好点西蒙。数字很​​脆弱……虽然硬编码的名字也很脆弱。所以这就是我更喜欢我的第一个解决方案的原因,它既不硬编码:)
【解决方案5】:

只要该字段对枚举仍然是私有的,我不确定它的最终状态是否真的值得关注。

也就是说,如果在构造函数中定义了对立面(它不一定是!),则条目将所述对立面放入自己的字段中,同时将自己分配给对立面的字段。这一切都应该通过决赛很容易解决。

【讨论】:

    【解决方案6】:

    我正在使用这个解决方案:

    public enum Flippable {
    
        A, B, Y, Z;
    
        public Flippable flip() {
            switch (this) {
                case A:
                    return Z;
                case B:
                    return Y;
                case Y:
                    return B;
                case Z:
                    return A;
                default:
                    return null;
            }
        }
    }
    

    测试:

    public class FlippableTest {
        @Test
        public void flip() throws Exception {
            for (Flippable flippable : Flippable.values()) {
                assertNotNull( flippable.flip() ); // ensure all values are mapped.
            }
            assertSame( Flippable.A.flip() , Flippable.Z);
            assertSame( Flippable.B.flip() , Flippable.Y);
            assertSame( Flippable.Y.flip() , Flippable.B);
            assertSame( Flippable.Z.flip() , Flippable.A);
        }
    }
    

    这个版本更短,但灵活性有限:

    public enum Flippable {
    
        A, B, Y, Z;
    
        public Flippable flip() {
                return values()[ values().length - 1 - ordinal() ];
        }
    
    }
    

    【讨论】:

    • 我有兴趣看到您的第一个答案与其他答案进行对比。
    【解决方案7】:

    下面的代码编译良好,并且满足所有 OP 的要求,但在运行时失败,Exception in thread "main" java.lang.ExceptionInInitializerError。它留在这里腐烂,作为所有偶然发现它的人不要做的一个例子。

    public enum Flippable {
    A, B, Y, Z;
    
    private final Flippable opposite;
    
    private Flippable() {
        this.opposite = getOpposite(this);
        verifyIntegrity();
    }
    
    private final Flippable getOpposite(Flippable f) {
        switch (f) {
            case A: return Z;
            case B: return Y;
            case Y: return B;
            case Z: return A;
            default:
                throw new IllegalStateException("Flippable not found.");
        }
    }
    
    private void verifyIntegrity() {
        // integrity check:
        Arrays.stream(Flippable.values())
        .forEach(f -> {
            if(!f.flip().flip().equals(f)) {
                throw new IllegalStateException("Flippable " + f + " is inconsistent.");
            }
        });
    }
    
    public Flippable flip() {
        return opposite;
    }
    

    }

    【讨论】:

    • 为什么每次都新建地图?
    • 我不需要。我可以声明它static final 并在静态块中初始化它。但是 Map 实例在程序执行期间一直存在,但在初始构造函数运行后不再需要。每次初始化它都是一种权衡,在这种情况下您会受到初始性能的影响,但此后会减少内存,因为地图不会永远存在。
    • 实际上看起来您根本不需要地图。只要使用单独的方法,您就可以摆脱开关吗?我已经更新了答案。
    • 有趣的是,它编译得很好,但会抛出一个Exception in thread "main" java.lang.ExceptionInInitializerError。我将把它留在这里作为不该做的例子。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-09-13
    • 1970-01-01
    • 2020-04-21
    • 1970-01-01
    • 1970-01-01
    • 2021-06-02
    • 1970-01-01
    相关资源
    最近更新 更多