Padrões de Enum: Corpos com Strategy, EnumSet e EnumMap
Objective
Vá além de enum como uma lista de constantes nomeadas: use corpos de método específicos por constante para dar a cada constante seu próprio comportamento (o padrão strategy-enum), e prefira EnumSet/EnumMap — coleções baseadas em vetor de bits e em array, construídas especificamente para chaves enum — em vez de flags de bits feitas à mão ou arrays indexados por ordinal().
Use Cases
- Modelar um conjunto fixo de operações (funções de calculadora, transições de máquina de estados, tipos de dia de folha de pagamento) em que cada constante precisa de uma lógica genuinamente diferente, não apenas de dados diferentes.
- Passar adiante uma combinação de flags/opções (estilos de texto, permissões, feature toggles) sem recorrer a aritmética bitwise sobre
int. - Agrupar ou tabular valores indexados por um enum (ervas por tipo de crescimento, requisições por status) onde um array simples ou um
HashMapfuncionaria, mas descartaria segurança de tipos e legibilidade. - Qualquer lugar em que
Enum.ordinal()esteja sendo usado como índice de array ou map — um sinal confiável de que umEnumMap(ouEnumSet) deveria ser usado no lugar.
Deep Dive
Corpos de método específicos por constante: o padrão strategy-enum
Um switch sobre this dentro de um método de enum compila, mas nada obriga que todas as constantes sejam cobertas — adicione uma constante, esqueça o case, e ela falha silenciosamente em tempo de execução:
java// Fragile: compiler won't catch a missing case
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
double apply(double x, double y) {
return switch (this) {
case PLUS -> x + y;
case MINUS -> x - y;
case TIMES -> x * y;
case DIVIDE -> x / y;
// add a new constant above and forget a branch here -> MatchException at runtime
};
}
}Declarar o método como abstract e dar a cada constante seu próprio corpo move essa checagem para o momento da compilação — o compilador se recusa a compilar um enum com uma constante que não sobrescreve um método abstrato:
javapublic enum Operation {
PLUS("+") { public double apply(double x, double y) { return x + y; } },
MINUS("-") { public double apply(double x, double y) { return x - y; } },
TIMES("*") { public double apply(double x, double y) { return x * y; } },
DIVIDE("/") { public double apply(double x, double y) { return x / y; } };
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return symbol; }
public abstract double apply(double x, double y);
}
for (Operation op : Operation.values()) {
System.out.printf("2 %s 4 = %f%n", op, op.apply(2, 4));
}Cada constante aqui é sua própria subclasse anônima de Operation, gerada pelo compilador — é isso que permite que ela sobrescreva apply de forma independente, continuando a ser uma Operation comum em todo o resto (values(), switch, EnumSet, EnumMap continuam funcionando sem mudanças).
A desvantagem aparece quando constantes precisam compartilhar lógica em vez de divergir. Duplicar código compartilhado em cada corpo de constante, ou recorrer a um switch, reintroduz o risco do case ausente. A correção é extrair a parte variável para um pequeno enum strategy aninhado e delegar a ele, em vez de colocar a lógica variável diretamente no enum externo:
javaenum PayrollDay {
MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY),
THURSDAY(WEEKDAY), FRIDAY(WEEKDAY),
SATURDAY(WEEKEND), SUNDAY(WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) { this.payType = payType; }
double pay(double hoursWorked, double payRate) {
return payType.pay(hoursWorked, payRate);
}
private enum PayType {
WEEKDAY {
double overtimePay(double hours, double payRate) {
return hours <= 8 ? 0 : (hours - 8) * payRate / 2;
}
},
WEEKEND {
double overtimePay(double hours, double payRate) {
return hours * payRate / 2;
}
};
abstract double overtimePay(double hours, double payRate);
double pay(double hoursWorked, double payRate) {
return hoursWorked * payRate + overtimePay(hoursWorked, payRate);
}
}
}Adicionar SATURDAY/SUNDAY obriga a escolher um PayType no local de chamada — não há um padrão para cair silenciosamente.
EnumSet em vez de campos de bits
O padrão antigo empacota cada flag em um bit de um int e as combina com |:
javapublic class Text {
public static final int STYLE_BOLD = 1 << 0;
public static final int STYLE_ITALIC = 1 << 1;
public static final int STYLE_UNDERLINE = 1 << 2;
public static final int STYLE_STRIKETHROUGH = 1 << 3;
public void applyStyles(int styles) { /* ... */ }
}
text.applyStyles(STYLE_BOLD | STYLE_ITALIC); // prints as an opaque number, no iterationEnumSet oferece a mesma combinação/união/interseção rápida via bitwise, mas como um Set<E> real e type-safe:
javapublic class Text {
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
public void applyStyles(Set<Style> styles) { /* ... */ }
}
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
text.applyStyles(EnumSet.range(Style.BOLD, Style.UNDERLINE)); // BOLD, ITALIC, UNDERLINE
text.applyStyles(EnumSet.noneOf(Style.class)); // empty setapplyStyles recebe Set<Style>, não EnumSet<Style> — aceite a interface, não a implementação, para que um chamador possa passar qualquer Set se tiver motivo para isso. Internamente, EnumSet é um único vetor de bits long para enums com 64 constantes ou menos (um long[] além disso), então testes de pertencimento e operações em lote (removeAll, retainAll) rodam como aritmética bitwise — comparável em velocidade a campos de bits feitos à mão, mas sem a saída impressa ilegível ou a falta de suporte a iteração.
EnumMap em vez de indexação por ordinal
Usar ordinal() para indexar um array funciona até o enum mudar — reordenar constantes reembaralha silenciosamente em qual posição cada uma cai, e nada sinaliza a quebra:
javaenum Type { ANNUAL, PERENNIAL, BIENNIAL }
Set<Herb>[] herbsByType = (Set<Herb>[]) new Set[Type.values().length]; // unchecked cast
for (int i = 0; i < herbsByType.length; i++) herbsByType[i] = new HashSet<>();
for (Herb h : garden) herbsByType[h.type().ordinal()].add(h); // wrong index -> wrong bucket, no errorReordene Type para { BIENNIAL, ANNUAL, PERENNIAL } e esse código ainda compila e ainda executa — só que arquiva cada erva sob o tipo errado, silenciosamente. EnumMap remove ordinal() da equação por completo:
javaMap<Type, Set<Herb>> herbsByType = new EnumMap<>(Type.class);
for (Type t : Type.values()) herbsByType.put(t, new HashSet<>());
for (Herb h : garden) herbsByType.get(h.type()).add(h);EnumMap é construído com o Class do tipo da chave (um type token limitado, necessário porque não existe new K[...] em Java) e é internamente sustentado por um array indexado por ordinal — comparável em velocidade à versão manual, mas o mapeamento de chave para posição é gerenciado pelo map, não por aritmética visível ao chamador. Reordenar as constantes de Type não muda mais o comportamento em nada. A ordem de iteração é a ordem natural (de declaração) do enum, independente da ordem de inserção — um benefício extra que HashMap e arrays indexados por ordinal não oferecem.
Trade-offs
EnumSetnão tem fábrica imutável — não existe um equivalente aEnumSet.of(...)que retorne um set não modificável comoSet.of(...)faz para coleções gerais; envolva-o comCollections.unmodifiableSet(...)se os chamadores não devem modificá-lo, aceitando a alocação extra.EnumMapitera na ordem de declaração do enum chave, não na ordem de inserção — geralmente é o comportamento desejado para exibição, mas é uma surpresa se o código foi portado de umLinkedHashMapque esperava ordem de inserção:javaMap<Type, String> m = new EnumMap<>(Type.class); m.put(Type.BIENNIAL, "b"); m.put(Type.ANNUAL, "a"); System.out.println(m); // {ANNUAL=a, BIENNIAL=b} — declaration order, not put orderCorpos de método específicos por constante transformam as constantes de um enum em subclasses anônimas distintas — isso é invisível em uso normal, mas significa que cada constante carrega sua própria classe compilada, e a lógica compartilhada entre constantes precisa ser extraída (método auxiliar, enum strategy aninhado) em vez de simplesmente escrita uma vez no método abstrato.
Enums extensíveis (uma interface implementada por dois tipos de enum separados) são o caso raro, não o padrão — recorra a isso só quando uma API genuinamente precisa que chamadores externos forneçam suas próprias constantes de enum (opcodes personalizados sobre um conjunto fixo); isso custa a capacidade de compartilhar uma implementação entre os dois tipos de enum, já que cada um precisa repetir qualquer lógica comum (por exemplo, armazenar um campo de símbolo) de forma independente.