如何在Java中根据索引获取枚举值?

111

我在Java中有一个枚举:

public enum Months
{
    JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC
}

我想通过索引访问枚举值,例如:

Months(1) = JAN;
Months(2) = FEB;
...

我该如何做到这一点?


18
在计算机科学中,索引从0开始,而不是从1开始;-) - fredoverflow
1
你确定你要这样做吗?通常情况下,除非实现低级数据结构(即使用备选机制,例如名称进行持久化),否则不应该触及序数。 - Tom Hawtin - tackline
您也可以使用java.util.Calendar类中的常量。它们是从0到11编号的,分别代表着1月到12月。要注意12这个数字,因为它代表着UnDecember(与农历有关)。但我很好奇为什么要重新发明已经包含在JRE中的月份常量呢? - Chris Aldrich
2FredOverflow: 同意,我使用了错误的索引。 2Tom Hawtin: 是的,我确定。我使用某个框架持久化数据,返回的是整数索引,而不是枚举类型。 2Chris Aldrich: 这只是一个不匹配实际情况的虚拟例子。 - jk_
顺便提一下,Java 8及更高版本内置了Month枚举。 - Basil Bourque
5个回答

273

试试这个

Months.values()[index]

46
请注意,每次调用此方法都会克隆一份值数组副本。因此,如果您在性能敏感的代码的内部循环中调用此方法,您可能需要制作一个静态副本并使用它。 - Christopher Barber
1
我感到困惑,那么为什么我不想使用数组呢? - Anudeep Samaiya
@AnudeepSamaiya 或许我们应该在代码中使用正确的枚举常量(Months.JAN),而不是到处使用 months[1]。 - Harry Joy
1
@Christopher Barber 这里有一个“制作静态副本”的一行代码:public static final ArrayList<Months> ALL = new ArrayList<Month>() {{ for (Months m : Months.values()) add(m); }};,然后你可以使用 Months i = ALL.get(index) 访问元素。 - muelleth
使用Months.values().clone()会更容易,或者如果您对可变性有疑虑,可以将其包装在不可变列表中(请参见Collections)。 - Christopher Barber

22

以下是三种方法来实现它。

public enum Months {
    JAN(1), FEB(2), MAR(3), APR(4), MAY(5), JUN(6), JUL(7), AUG(8), SEP(9), OCT(10), NOV(11), DEC(12);


    int monthOrdinal = 0;

    Months(int ord) {
        this.monthOrdinal = ord;
    }

    public static Months byOrdinal2ndWay(int ord) {
        return Months.values()[ord-1]; // less safe
    }

    public static Months byOrdinal(int ord) {
        for (Months m : Months.values()) {
            if (m.monthOrdinal == ord) {
                return m;
            }
        }
        return null;
    }
    public static Months[] MONTHS_INDEXED = new Months[] { null, JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC };

}




import static junit.framework.Assert.assertEquals;

import org.junit.Test;

public class MonthsTest {

@Test
public void test_indexed_access() {
    assertEquals(Months.MONTHS_INDEXED[1], Months.JAN);
    assertEquals(Months.MONTHS_INDEXED[2], Months.FEB);

    assertEquals(Months.byOrdinal(1), Months.JAN);
    assertEquals(Months.byOrdinal(2), Months.FEB);


    assertEquals(Months.byOrdinal2ndWay(1), Months.JAN);
    assertEquals(Months.byOrdinal2ndWay(2), Months.FEB);
}

}

5
public static 可变的(包括数组和非 final)。哎呀。抛出 IllegalArgumentException 比返回 null 更合理。 - Tom Hawtin - tackline

8

我刚刚尝试了同样的方法,并得出了以下解决方案:

public enum Countries {
    TEXAS,
    FLORIDA,
    OKLAHOMA,
    KENTUCKY;

    private static Countries[] list = Countries.values();

    public static Countries getCountry(int i) {
        return list[i];
    }

    public static int listGetLastIndex() {
        return list.length - 1;
    }
}

该类有自己的值保存在数组中,我使用数组来获取索引位置处的枚举。如上所述,数组从0开始计数,如果您希望索引从'1'开始,请只需更改这两个方法为:
public static String getCountry(int i) {
    return list[(i - 1)];
}

public static int listGetLastIndex() {
    return list.length;
}

在我的主程序中,我使用以下代码获取所需的countries对象:

public static void main(String[] args) {
   int i = Countries.listGetLastIndex();
   Countries currCountry = Countries.getCountry(i);
}

这段代码将把currCountry设置为最后一个国家,即Countries.KENTUCKY。

请记住,如果您使用硬编码索引来获取对象,则此代码非常容易受到ArrayOutOfBoundsExceptions的影响。


你确定这个静态块可以正常工作吗?.values()通常会提供内部(静态)数组的克隆,而不是仅仅引用。该数组可能在内部预先分配,但在static{}块的位置(在对象有机会运行任何构造函数之前),它将被填充为null。我可能会在第一次调用get(index)时使用.values(),它会将数组存储起来以供后续索引,或者在构造函数中逐个填充每个值。 - alife

3

我最近遇到了同样的问题,并使用了Harry Joy提供的解决方案。 但是,该解决方案仅适用于从零开始的枚举。 我认为它也不够安全,因为它无法处理超出范围的索引。

我最终使用的解决方案可能没有那么简单,但完全安全,并且即使对于大型枚举,也不会影响您的代码性能:

public enum Example {

    UNKNOWN(0, "unknown"), ENUM1(1, "enum1"), ENUM2(2, "enum2"), ENUM3(3, "enum3");

    private static HashMap<Integer, Example> enumById = new HashMap<>();
    static {
        Arrays.stream(values()).forEach(e -> enumById.put(e.getId(), e));
    }

    public static Example getById(int id) {
        return enumById.getOrDefault(id, UNKNOWN);
    }

    private int id;
    private String description;

    private Example(int id, String description) {
        this.id = id;
        this.description= description;
    }

    public String getDescription() {
        return description;
    }

    public int getId() {
        return id;
    }
}

如果您确定您的索引永远不会超出范围,并且不想像上面我所做的那样使用UNKNOWN,当然也可以这样做:

public static Example getById(int id) {
        return enumById.get(id);
}

我觉得这种复杂性是不必要的。为什么不在枚举内部保留一个包含所有元素的数组呢?这样做简单明了,而且你的 getById() 方法总是可以直接查找该数组,而不需要从 .values() 中克隆一个数组(大多数人都是这样做的),或者查找内部哈希表。 - alife

1
Java枚举的自然排序是根据枚举中声明的值的顺序。不幸的是,每次调用values()方法都会分配新的数组,从而导致性能问题。
静态初始化程序可以根据自然顺序为每个枚举成员分配序数。因此,我们不必维护顺序->id映射。此外,使用缓存的values()响应解决了常量分配的问题。
enum Months {
    JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC;

    int ordinal;     // ordinal will start from 1

    // cached instance used as optimization
    final static Months[] values = Months.values();

    static {
        // ordinal is initialized based on enum natural order
        int counter = 1;
        for (Months m: values)
            m.ordinal = counter++;  
    }

    public static Months getMonth(int ordinal) {
        return values[ordinal - 1];
    }
}

我在使用JDBC ResultSet时遇到了类似的问题。我想要使用ResultSet的索引方法来访问字段,但是由于列数很多,我不想停止使用列名。ResultSet的编号也是从1开始的,作为第一个元素。类似的枚举是这个问题的自然解决方案。

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