Enums
public enum Valor {
AS, DOS, TRES, CUATRO, CINCO, SEIS, SIETE, OCHO, NUEVE, DIEZ, J, Q, K;
}
public enum Palo {
TRÉBOLES, DIAMANTES, CORAZONES, PICAS;
}
public class Carta {
private Valor valor;
private Palo palo;
public Carta(Valor valor, Palo palo) {
this.valor = valor;
this.palo = palo;
}
public Valor valor() {
return valor;
}
public Palo palo() {
return palo;
}
}
public class Main {
public static void main(String[] args) {
var asCorazones = new Carta(Valor.AS, Palo.CORAZONES);
System.out.printf("%s %s", asCorazones.valor(), asCorazones.palo());
}
}
| AS CORAZONES |
El código 01 utiliza Enums para modelar el Valor y el Palo de una carta de una baraja. Un Enum es una forma especializada de una clase. Para crear un Enum en lugar de utilizar la palabra reservada <class>, utiliza la palabra reservada <enum>. Igual que con una clase necesitas definir un bloque para los Enum.
Hemos visto cómo puedes crear nuevos tipos de datos con clases. Los objetos que creamos a partir de esas clases tienen un estado y normalmente los atributos tienen muchos valores posibles, por lo que se puede decir que el número de posibles estados es muy grande. Pero ¿qué sucede cuando queremos modelar tipos de datos como el valor y el palo de una carta de una baraja, pero también el color de los ojos de una persona, o el tipo de documento de una persona, etc? Para estos tipos de datos donde el conjunto de valores es fácil de enumerar, lo recomendable es usar un Enum.
Las líneas 02 de Valor y 02 de Palo enumeran los posibles valores de su respectivo tipo de dato. Puedes elegir cualquier nombre siempre que se ajuste a las reglas para identificadores. Ni siquiera es necesario tener todos los identificadores en mayúsculas, pero es costumbre hacerlo ya que estás definiendo una constante.
Ahora con estos Enums puedes crear cartas sabiendo que el Valor y el Palo son valores válidos. La línea 04 de Main crea una carta con Valor.AS y Palo.CORAZONES. A diferencia de las clases, no creas valores de Enums usando el operador new, de los valores de un Enum no los creas simplemente los referencias de la misma manera que cualquier constante de clase.
Pero ahora puedes decir "Bueno, las enumeraciones parecen muy limitadas, ¿no?" Veamos más cosas que puedes hacer con ellas.
Metodos y Atributos
public enum Valor {
AS, DOS, TRES, CUATRO, CINCO, SEIS, SIETE, OCHO, DIEZ, J, Q, K;
public int puntaje() {
return ordinal() + 1;
}
public String toString() {
return switch(this) {
case DOS, TRES, CUATRO, CINCO, SEIS, SIETE, OCHO,NUEVE -> Integer.toString(value());
case AS-> "A";
case DIEZ-> "T";
case J, Q, K -> super.toString();
};
}
}
public enum Palo {
TREBOLES("♣"), DIAMANTES("♦"), CORAZONES("♥"), PICAS("♠");
private String símbolo;
private Suit(String símbolo) {
this.símbolo = símbolo;
}
public String toString() {
return símbolo;
}
}
La línea 04 de Palo define un atributo. Los Enum son también clases por lo que pueden tener atributos y métodos como cualquier otra clase. Sin embargo el orden de los elementos es importante, sólo puedes definir atributos después de la lista de valores (línea 02 en la misma clase). La línea 06 tiene un constructor. Los constructores en Enums siempre son privados porque no puedes llamarlos directamente. Lo que debes hacer es especificar los valores que usará cada constante cuando se llame al constructor. Ese constructor lo llama el ambiente de ejecución mucho antes de que tu programa comience a ejecutarse. Entonces, en la línea 02 verás que tienes una especie de invocación donde cada Palo define el valor del atributo.
Puedes definir cualquier cantidad de métodos y de cualquier tipo en un Enum. Las líneas 04 a 06 de Valor definen un método puntaje() ya que los valores en una carta generalmente están asociados con un valor numérico. T odos los tipos de Enum tienen algunos métodos ya implementados, uno de ellos es ordinal() que devuelve la posición actual de la constante en la lista. En este caso, dado que la posición en la lista está relacionada con su valor numérico podemos usar ordinal() para implementar puntaje(), aunque ordinal() devuelve un índice por lo que empieza en 0, razón por la cual debemos sumarle 1.
Las líneas 08-13 definimos el método toString(). Uno de los grandes beneficios de
Enums es que se combinan bastante bien con los switch. Una de las cosas buenas es
que si alguna vez agregas una nueva constante al Enum, el compilador dará un error
de compilación en el switch que usan ese tipo, hasta que agregues un
Misión Secundaria: Sobreescritura de Métodos por Constante
public enum Palo {
TREBOLES {
public String toString() {
return"♣";
}
},
DIAMANTES {
public String toString() {
return"♦";
}
},
CORAZONES {
public String toString() {
return"♥";
}
},
PICAS {
public String toString() {
return"♠";
}
};
}
Otra opción es que puedes proporcionar una implementación específica de un método para una constante. La sintaxis puede ser un poco extraña a la vista, pero mejora si utilizas un buen espacio vertical: para sobreescribir métodos debes agregar un bloque después del nombre de la constante y dentro de ese bloque escribir la implementación específica para esa constante del método.
Si el método no se hereda, debes declararlo fuera de las constantes, como por ejemplo:
public enum Palo {
TREBOLES {
public String toString() {
return"♣";
}
},
DIAMANTES {
public String toString() {
return"♦";
}
},
CORAZONES {
public String toString() {
return"♥";
}
},
PICAS {
public String toString() {
return"♠";
}
};
abstract String toString();
}
La línea 21 define toString() como un método abstracto. Esto no es necesario para toString() pero para métodos no-heredados, sí deberás hacerlo. El método abstracto es un tema más extenso relacionado con el concepto de herencia, que se sale del alcance de este material.
Ejercicios
E11.1
Escribe una función tipoNúmero() que reciba un número entero y retorne si un número es perfecto, abundante o deficiente.
Para este ejercicio apóyate de estas funciones:
public class Aritmética {
public static int sumaAlícuota(int n) {
var suma = 0;
for (var a = 1; a <= n / 2; ++a) {
if (esMúltiplo(n, a)) {
suma += a;
}
}
return suma;
}
public static boolean esMúltiplo(int a, int b) {
return a % b == 0;
}
}
Un número es:
- Perfecto: si es igual que su suma alícuota
- Deficiente si es menor que su suma alícuota
- Abundante si es mayor que su suma alícuota
Para esta función, crea un Enum:
TipoNúmero
| Constantes de Enum |
|---|
PERFECTO |
DEFICIENTE |
ABUNDANTE |
Asserts
|
E11.2
En muchos países, hay leyes que establecen que cuando la fecha de algunos días festivos no sea lunes, el día festivo se debe correr al siguiente lunes.
Java tiene una extensa lista de clases relacionadas con el tiempo, que incluye algunos Enums, como: Month y DayOfWeek.
LocalDate es un tipo de dato para representar una fecha sin tener en cuenta una zona horaria. Esta clase tiene un método getDayOfWeek() que retorna una variable de tipo DayOfWeek.
DayOfWeek por su lado tiene un método llamado getValue() que retorna un número entre 1 y 7, siendo 1 cuando el día de la semana es lunes y así sucesivamente hasta 7 cuando el día de la semana es domingo.
Escribe una función que se llame siguienteLunes(LocalDate) que retorne un LocalDate que representa el siguiente lunes del parámetro dado. Sigue este algoritmo:
- Si el día de la semana de la fecha ya es lunes, retorna el parámetro
- Sino, calcula la diferencia entre getValue() del domingo y el getValue() de la fecha dada. A este valor súmale 1, llamémoslo díasHastaLunes.
- Retorna el resultado de llamar plusDays(int) del parámetro dado y usa la variable díasHastaLunes como argumento.
Asserts
|
E11.3
En el juego de Risk, oponentes se enfrentan para controlar el mundo. Los combates se definen con dados: el atacante lanza 3 y el defensor lanza 2. El combate se resuelve así:
- Se ordenan los dados tanto del atacante como del defensor
- Se compara los dos dados mayores de cada oponente, si el dado del atacante es mayor que el del defensor, entonces el defensor pierde una tropa de lo contrario el atacante pierde una tropa
- Se comparan los siguientes dados mayores de la misma manera
Por lo tanto de un combate pueden surgir los siguientes resultados:
- El atacante pierde 2 tropas
- El atacante y el defensor pierden una tropa cada uno
- El defensor pierde 2 tropas
Escribe un Enum ResoluciónCombate que modele estos posibles resultados. El Enum tiene dos métodos modificadorAtacante() y modificadorDefensor() que regresan 0, -1 y -2 según el resultado que representan.
Luego escribe una función combate() que tome dos arreglos de ints. El primer arreglo tiene exactamente 3 números y el segundo arreglo tiene exactamente 2 números. En ambos casos los números están entre 1 y 6. La función retorna un Enum de tipo ResoluciónCombate según las reglas mencionadas antes.
| Constantes de Enum |
|---|
| ATACANTE_PIERDE_2 |
| DEFENSOR_PIERDE_2 |
| ATACANTE_Y_DEFENSOR_PIERDEN_1 |
| Métodos |
|---|
public int modificadorAtacante()Retorna la cantidad de tropas que pierde el atacante, 0, -1 o -2 según las reglas antes mencionadas. |
public int modificadorDefensores()Retorna la cantidad de tropas que pierde el defensor, 0, -1 o -2 según las reglas antes mencionadas. |
Asserts
|
|
|
|
|
|
E11.4
Escribe un Enum Dirección para representar direcciones dentro de una matriz, con ocho posibles valores: por ejemplo, la constante NORTE representa una dirección que significa subir una fila pero mantener la misma columna. Escoge cómo implementarías los métodos: con atributos / constructores, con switch o sobreescribiendo los métodos para cada constante.
Dirección
| Constantes de Enum | NORTE |
|---|
NOROESTE |
OESTE |
SUROESTE |
SUR |
SURESTE |
ESTE |
NORESTE |
| Metodos |
|---|
public int desplazamientoFila()devuelve el desplazamiento de la fila para esta dirección. Debería devolver -1 para NORTE, NOROESTE y NORESTE; 0 para OESTE y ESTE; 1 para SUR, SUROESTE y SURESTE. |
public int desplazamientoColumna()devuelve el desplazamiento de la fila para esta dirección. Deberia regresar.-1 para ESTE, NORESTE, SURESTE; 0 para SUR y NORTE; 1 para OESTE , NOROESTE y SUROESTE. |
Con este enum puedes verificar vecinos de una casilla en una matriz con este código como modelo:
var x = ...
var y = ...
for (var dirección : Dirección.values()) {
var nx = x + direccion.desplazamientoFila();
var ny = y + direccion.desplazamientoColumna();
// más código
}
E11.5
El juego de la vida de Conway es un juego sin jugadores, donde un mundo tiene un estado inicial y puedes ver cómo evoluciona. El mundo es una cuadrícula con células que pueden estar en dos estados: vivas y muertas. Después de cada generación algunas células cambian de estado mientras que otras no con estas simples reglas:
- Las células muertas cobran vida si tienen exactamente 3 vecinos vivos.
- Las células vivas mueren si tienen menos de 2 o más de 3 vecinos vivos.
- En todos los demás casos las células mantienen su estado.
Para este ejercicio desarrollarás un mundo que es finito. Escoge cómo implementarías los métodos: con atributos / constructores, con switch o sobreescribiendo los métodos para cada constante.
Celula
| Constantes de Enum |
|---|
VIVA |
MUERTA |
| Metodos |
|---|
public Célula siguienteGeneración(int vecinosVivos)Devuelve el estado de la célula para la próxima generación, según las reglas del Juego de la Vida de Conway. |
public String toString()retorna “#” para VIVA y “.” para MUERTA |
assertEquals(Célula.MUERTA, Célula.VIVA.siguienteGeneración(1)); |
Para la siguiente clase, reutiliza la clase Coordenada que escribiste anteriormente.
Mundo
| Constructores |
|---|
public Mundo(int filas, int columnas, Coordena[] célulasVivas)construye un mundo finito con las dimensiones dadas con todas sus célular inicialmente en estado MUERTA salvo en las coordenadas indicadas en célulasVivas |
| Métodos |
public Mundo siguienteGeneración()crea un nuevo mundo con las células en su estado actualizado después de llamar siguienteGeneración(int) en ellas. |
public String toString()devuelve un String de múltiples líneas con "." representando una célula muerta y "#" representando una célula viva. |
Asserts
// 01234 |
// 012345 |
© 2022 Carlos Obregón. This work is licensed under Creative Commons Attribution 4.0 International (CC BY 4.0)