具有不可变参数的自引用枚举

13

请考虑以下 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;
  }
}

这个解决方案比第一种方案更加糟糕,因为这个字段不再是final,并且容易被反射攻击。虽然这个问题很少见,但是表明了代码存在潜在的问题。

有没有一种与第一个示例基本相同但可以正确编译的方法?


你为什么要枚举Flippables?既然你在每个case:表达式中都必须这样做,为什么不直接依次分配每个呢? - robert
@robert 因为我总是把事情搞得太复杂了 :) - durron597
7个回答

18

也许不像您期望的那么漂亮...

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。 这个解决方案(以及我的)容易因输入错误而出错。 - splungebob
我认为解决循环引用问题的方法是无法避免的。 - robert
1
@Admit 这个答案比你的更快。这是我的基准测试链接,结果在顶部有注释 - durron597

4

正如您所看到的,由于枚举常量是静态的,并且在Z没有初始化之前无法初始化A,因此这是不可能的。

所以这个技巧应该可以解决问题:

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
@robert 不行,它不能运行。我不喜欢这个答案,因为它比较慢(每次都需要进行字符串解析)... 除非Hotspot足够聪明,能够解决这个问题。 - durron597
如果在构造函数中,它应该只在类加载时解析一次 - 但是,是的,我刚刚检查过了,它可以编译通过,但无法运行 (: - robert
1
如果你查看 Enum.valueOf() 方法,它在内部使用了 Map<String, Enum> 并通过键值进行搜索,因此速度非常快。无论如何,选择哪种方式取决于你的具体情况,是追求速度还是内存消耗等方面。这个答案肯定适用于你的问题:“有没有一种方式与第一个示例基本相同,但能够正确编译?” - Admit
另一个公正的观点。好的,我已经给你点赞了;实际上,考虑到 Enum.valueOf 的速度,这也解决了不可变性问题,所以这可能是最好的答案。 - durron597
我最喜欢这个答案。简洁、不可变和高效。 - Jeshurun

2
只需映射相反的内容:
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更好,事实上它更糟糕,因为它具有更高的内存消耗。 - durron597
我猜我更偏爱可读性。像大多数switches一样的解决方案2,在我看来显得很丑陋。所以我猜你认为“更糟糕”的说法是高度主观的。在我看来,内存消耗似乎不是考虑maps与逻辑上类似的switches时的合理论据。 - splungebob
开关静态发生,仅一次。 - durron597
1
没必要在这里使用HashMap,你可以直接赋值给成员变量。 - robert
4
我同意使用地图与Robert的答案相比没有任何优势。但是如果有人告诉你必须使用地图,那么EnumMapHashMap更好。 - ajb
显示剩余3条评论

0
下面的代码编译良好,满足所有 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;
}

}


你为什么每次都要重新构建地图? - durron597
我不必这样做。我可以将其声明为static final并在静态块中初始化。但是,这个Map实例会一直存在于程序的执行期间,但在初始构造函数运行后就不再需要了。每次初始化都是一种权衡,在其中您会遇到初始性能损失,但之后内存减少,因为Map不会永远存在。 - Jeshurun
其实看起来你根本不需要使用Map。只要将它放在一个单独的方法中,你就可以用switch语句代替了。我已经更新了答案。 - Jeshurun
有趣的是,这个程序可以编译通过,但会抛出一个“Exception in thread "main" java.lang.ExceptionInInitializerError”的异常。我将把它留在这里作为不应该做的示例。 - Jeshurun

0

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

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());
        }
    }
}

这也可以工作。


有趣的方法。您认为这比其他答案更受欢迎的原因是什么? - durron597
首先的解决方案自动计算相反数,而不是硬编码它们,这可能比所需的(但不是有效的Java)原始代码更加简洁!然而,它失去了不可变性,这是一件遗憾的事情。 - Ed Price
第二种解决方案基本上与所需的原始方案相同,只是使用数字而不是名称来指定相反的内容 - 而且需要一些运行时成本来访问数组,不可否认。很遗憾,两者都不完美,但我认为它们在某些方面都比其他答案有所改进... - Ed Price
1
我曾经依赖于枚举的索引值,但是这给我带来了非常糟糕的经历。迟早有一天,你可能会意外地交换索引,导致方块变成黑色,而不是红色 - Simon Forsberg
好观点,西蒙。数字是脆弱的...硬编码的名称也很脆弱。这就是为什么我更喜欢我的第一个解决方案,它既不硬编码数字也不硬编码名称的原因 :) - Ed Price

0
只要该字段对枚举保持私有,我不确定它的最终状态是否真的很重要。
话虽如此,如果在构造函数中定义了相反的内容(不一定要这样做!),则该条目将把相反的内容带入自己的字段中,并将自己分配给相反的字段。这应该很容易通过finals解决。

0
我正在使用这个解决方案:
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() ];
    }

}

我很想看看你的第一个答案与其他人的对比情况。 - durron597

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接