Java 8中是否可以扩展枚举?

15

玩闹间,我找到了一种为Java中的enum添加功能的甜美方法,即使用Java Enum toString()方法这个答案

进一步的尝试让我 几乎 成功地添加了一个整洁(即不会抛出异常)的反向查找,但是有一个问题。它报告:

error: valueOf(String) in X cannot implement valueOf(String) in HasValue
public enum X implements PoliteEnum, ReverseLookup {
overriding method is static

有办法吗?

这里的目标是通过实现一个界面(类似于我在链接回答中添加的politeName)默默地添加一个lookup方法,该方法可以执行valueOf函数而不会引发异常。 这是可能的吗?现在显然可以扩展枚举-这是我之前在Java中遇到的主要问题之一。

这是我的失败尝试:

public interface HasName {

    public String name();
}

public interface PoliteEnum extends HasName {

    default String politeName() {
        return name().replace("_", " ");
    }
}

public interface Lookup<P, Q> {

    public Q lookup(P p);
}

public interface HasValue {
    HasValue valueOf(String name);
}

public interface ReverseLookup extends HasValue, Lookup<String, HasValue> {

    @Override
    default HasValue lookup(String from) {
        try {
            return valueOf(from);
        } catch (IllegalArgumentException e) {
            return null;
        }
    }

}

public enum X implements PoliteEnum/* NOT ALLOWED :( , ReverseLookup*/ {

    A_For_Ism, B_For_Mutton, C_Forth_Highlanders;
}

public void test() {
    // Test the politeName
    for (X x : X.values()) {
        System.out.println(x.politeName());
    }
    // ToDo: Test lookup
}

2
@millimoose - 这是我的最初想法 - 不幸的是问题在于 valueOfstatic,因此无法被覆盖。我猜需要完全不同的方法。 - OldCurmudgeon
2
你将在一个实例上调用 lookup 吗? - Sotirios Delimanolis
2
说实话,我会把上述功能做成一个实用方法,而不是需要3-4个新接口的混乱代码。 - millimoose
试一下这个。有时候它可能对你有帮助。https://dev59.com/nnE85IYBdhLWcg3wtV_1 - realProgrammer
3
我相信大部分人都像我一样,更关心完成任务,而不是追求“优美的技巧”来挑战极限。对我来说,用最小的功夫干净利落地完成工作比不管如何都把它变成一个实例方法调用更有价值。请理解,Java 是一种历史上故意回避聪明才智、高阶编程等设计的语言。如果你想使用“魔法”,可能其他平台更适合满足你的需求。 - millimoose
显示剩余6条评论
4个回答

17

你的设计过于复杂。如果你可以接受只能在实例上调用default方法,那么整个代码可能如下所示:

interface ReverseLookupSupport<E extends Enum<E>> {
    Class<E> getDeclaringClass();
    default E lookup(String name) {
        try {
            return Enum.valueOf(getDeclaringClass(), name);
        } catch(IllegalArgumentException ex) { return null; }
    }
}
enum Test implements ReverseLookupSupport<Test> {
    FOO, BAR
}

您可以使用以下方法进行测试:

Test foo=Test.FOO;
Test bar=foo.lookup("BAR"), baz=foo.lookup("BAZ");
System.out.println(bar+"  "+baz);

一种不需要抛出/捕获异常的替代方案是:

interface ReverseLookupSupport<E extends Enum<E>> {
    Class<E> getDeclaringClass();
    default Optional<E> lookup(String name) {
        return Stream.of(getDeclaringClass().getEnumConstants())
          .filter(e->e.name().equals(name)).findFirst();
}

如何使用:

Test foo=Test.FOO;
Test bar=foo.lookup("BAR").orElse(null), baz=foo.lookup("BAZ").orElse(null);
System.out.println(bar+"  "+baz);

2
如果您不喜欢反射代码 / 临时数组,可以将 Stream.of(getDeclaringClass().getEnumConstants()) 替换为 EnumSet.allOf(getDeclaringClass()).stream() - Holger
现在代码看起来比我的整洁多了!我喜欢你使用getDeclaringClass的方式,并将其一次性地应用于枚举。聪明地使用流 - 我可能会将它们流入一个Set,但那只是个人偏好。感谢您的见解。我期待着认真使用Java 8。 - OldCurmudgeon
你可以使用 stream.collect(Collectors.toMap(Enum::name, Function.identity())) 将它们流式转换为 Map<String,Enum>。但是我认为对于典型的 enum 大小,哈希查找并不比线性搜索更快... - Holger
1
真的很烦人,你不能在那里使用静态方法。使用任意值来访问类方法相当奇怪。 - flaschenpost
@flaschenpost:别担心,我不想为这个答案辩护。我只是感觉你有一个完全不同的问题,可能你认为Java中存在限制,但实际上并没有。这个答案没有涉及静态方法,因为问题不是关于它的。你可以在基本接口中声明static方法,但是它们需要一个Class参数才能提供正确的返回类型。或者,您可以在每个enum类型中提供一个存根方法,以委托到基本方法。 - Holger
显示剩余3条评论

0
情况相同,即在接口中无法创建默认的 toString()。枚举已包含静态 valueOf(String) 方法的签名,因此您无法覆盖它。
枚举是编译时常量,因此它们很难以后扩展。
如果您想通过名称获取常量,可以使用以下代码:
public static <E extends Enum<E>> Optional<E> valueFor(Class<E> type, String name) {

       return Arrays.stream(type.getEnumConstants()).filter( x ->  x.name().equals(name)).findFirst();

    }

我相信你是正确的,但在我的问题帖子中,我通过添加一个politeName方法来回避了这个问题。如果有类似的创意想法,我会很高兴的。我仍然需要调用enumvaluesvalueOf方法才能使查找工作正常运行。 - OldCurmudgeon
我不明白。在 HasName 中,您有一个名为 name 的公共非静态方法,因此它可以正常工作。在 HasValue 中,您声明了一个枚举中的 valueOf(String) 方法,该方法是静态的。这就是为什么会出现错误的原因。我不明白的是什么? - Damian Leszczyński - Vash
我觉得你都明白了 - 有没有一种可行的方法?也许我们可以通过反射调用values并创建一个单例Map - 我现在还不清楚。 - OldCurmudgeon
你不需要调用它。已经有东西可以获取常量。 - Damian Leszczyński - Vash

0

这里基本上有两个要点。具体原因是它无法编译的8.4.8.1

如果实例方法覆盖静态方法,则会出现编译时错误。

换句话说,枚举类型无法实现HasValue,因为名称冲突。

然后我们还有一个更普遍的问题,即静态方法无法“覆盖”。由于valueOf是由编译器在派生自Enum的类本身上插入的静态方法,因此无法更改它。我们也不能使用接口来解决它,因为它们没有静态方法。

在这种特定情况下,组合可以使这种事情变得不那么重复,例如:

public class ValueOfHelper<E extends Enum<E>> {
    private final Map<String, E> map = new HashMap<String, E>();

    public ValueOfHelper(Class<E> cls) {
        for(E e : EnumSet.allOf(cls))
            map.put(e.name(), e);
    }

    public E valueOfOrNull(String name) {
        return map.get(name);
    }
}

public enum Composed {
    A, B, C;

    private static final ValueOfHelper<Composed> HELPER = (
        new ValueOfHelper<Composed>(Composed.class)
    );

    public static Composed valueOfOrNull(String name) {
        return HELPER.valueOfOrNull(name);
    }
}

此外,我建议使用这种方法而不是捕获异常。

我知道“你不能这样做”并不是一个理想的答案,但由于静态方面的限制,我看不到其他解决方法。


@Vash 你的意思是什么?我认为OP正在尝试规避一些人不喜欢的IllegalArgumentException异常。 这确实做与valueOf相同的事情,只是不会抛出异常。 此示例更多地旨在说明组合方面:可以以此方式添加其他静态实用程序。 - Radiodef
好的,我在想OP在忙些什么。但我认为你已经理解得很正确了。 - Damian Leszczyński - Vash
只是为了确保你走在正确的轨道上,但Java 8中的接口确实有选择使用静态方法。不确定你是否已经意识到了这一点。 - skiwi
@skiwi 他们确实有,但似乎静态方法并没有被实现类“继承”。或者至少我尝试过了,它不起作用,因此它们似乎遵循与类上的静态方法不同的规则。 - Radiodef
据我所记,静态方法始终是类特定的。也就是说,在扩展/实现时,它们不会对其他类可用。但是,您仍然可以通过ClassYouWantToCall.staticMethod()或在某些情况下通过super.staticMethod()来调用它们。 - skiwi
@skiwi 对,这就是为什么我在引号中加入了“继承”的原因。重点是似乎你不能调用接口上的静态方法,除非使用接口类的名称。所以在这里它没有用处。 - Radiodef

0

我认为我有一个答案 - 它很巧妙,使用了反射,但似乎符合要求 - 即在枚举中没有方法且不抛出异常的情况下进行反向查找。

public interface HasName {

    public String name();
}

public interface PoliteEnum extends HasName {

    default String politeName() {
        return name().replace("_", " ");
    }
}

public interface Lookup<P, Q> {

    public Q lookup(P p);
}

public interface ReverseLookup<T extends Enum<T>> extends Lookup<String, T> {

    @Override
    default T lookup(String s) {
        return (T) useMap(this, s);
    }

}

// Probably do somethiong better than this in the final version.
static final Map<String, Enum> theMap = new HashMap<>();

static Enum useMap(Object o, String s) {
    if (theMap.isEmpty()) {
        try {
            // Yukk!!
            Enum it = (Enum)o;
            Class c = it.getDeclaringClass();
            // Reflect to call the static method.
            Method method = c.getMethod("values");
            // Yukk!!
            Enum[] enums = (Enum[])method.invoke(null);
            // Walk the enums.
            for ( Enum e : enums) {
                theMap.put(e.name(), e);
            }
        } catch (Exception ex) {
            // Ewwww
        }
    }
    return theMap.get(s);
}

public enum X implements PoliteEnum, ReverseLookup<X> {

    A_For_Ism,
    B_For_Mutton,
    C_Forth_Highlanders;
}

public void test() {
    for (X x : X.values()) {
        System.out.println(x.politeName());
    }
    for (X x : X.values()) {
        System.out.println(x.lookup(x.name()));
    }
}

打印

A For Ism
B For Mutton
C Forth Highlanders
A_For_Ism
B_For_Mutton
C_Forth_Highlanders

已添加

受 @Holger 启发 - 这是我感觉最像我所寻找的东西:

public interface ReverseLookup<E extends Enum<E>> extends Lookup<String, E> {

  // Map of all classes that have lookups.
  Map<Class, Map<String, Enum>> lookups = new ConcurrentHashMap<>();

  // What I need from the Enum.
  Class<E> getDeclaringClass();

  @Override
  default E lookup(String name) throws InterruptedException, ExecutionException {
    // What class.
    Class<E> c = getDeclaringClass();
    // Get the map.
    final Map<String, Enum> lookup = lookups.computeIfAbsent(c, 
              k -> Stream.of(c.getEnumConstants())
              // Roll each enum into the lookup.
              .collect(Collectors.toMap(Enum::name, Function.identity())));
    // Look it up.
    return c.cast(lookup.get(name));
  }

}

// Use the above interfaces to add to the enum.
public enum X implements PoliteName, ReverseLookup<X> {

    A_For_Ism,
    B_For_Mutton,
    C_Forth_Highlanders;
}

问题在于接口只是实例化的。因此,您必须执行类似于 EnumClass.KnownConstant.lookup(unknownName); 的操作。 - Radiodef
@Radiodef - 是的 - 它并不完全像它试图模拟的静态方法一样运作,但你必须承认它是一个改进。 - OldCurmudgeon
太过复杂了。看看我的答案... - Holger
你可以将 (E) lookup.get(name) 改为 c.cast(lookup.get(name)),以消除类型安全警告。 - Holger
1
关于多线程,ConcurrentHashMap.computeIfAbsent(…)看起来是一个完美的应用场景。这甚至可以简化代码并提供另一个“为什么Java 8如此酷”的例子... 另外注意,collect返回一个新的映射而不是填充您的HashMap<>;在这里不需要手动创建HashMap - Holger

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