R. Chavarria's Blog

Proud of developing software, proud of being an Engineer

Formatear y parsear enumerados en Java con valores personalizados

A partir de la versión 1.5 de Java, nos encontramos con un tipo de datos, un tipo especial de clases podríamos decir, los enumerados. Los tipos enumerados sirven para acotar los posibles valores que puede tomar una variable o tipo de objeto. Java proporciona un método para transformar un enumerado en cadena, name(), y un método estático para convertir una cadena en uno de los valores del enumerado, valueOf(String). Pero estos métodos son un poco limitados, ya que no nos permiten personalizar los valores a los cuales son transformados los enumerados.

Partiremos del siguiente código describiendo tres valores en un enumerado:

1
2
3
4
5
public enum States {
  EVERYTHING_IS_OK,
  SOMETHING_WENT_WRONG,
  TOTAL_FAILURE;
}

Así, sin más, podemos obtener la representación en String de States.TOTAL_FAILURE simplemente llamando al método States.TOTAL_FAILURE.name(). De la misma forma, usando la llamada States.valueOf("EVERYTHING_IS_OK") es fácil imaginar qué valor del enumerado obtendremos.

Pero, y si en lugar de esa representación a String, ¿queremos definir la nuestra propia?. Por ejemplo, siguiendo la siguiente tabla:

  • EVERYTHING_IS_OK → green
  • SOMETHING_WENT_WRONG → yellow
  • TOTAL_FAILURE → red

Formatear a cadena

Formatear es sencillo, y directo. Podemos declarar un campo value donde almacenar el valor al cual queremos transformar el enumerado, y podemos sobreescribir el método toString() de la siguiente manera:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public enum States {
  EVERYTHING_IS_OK("green"),
  SOMETHING_WENT_WRONG("yellow"),
  TOTAL_FAILURE("red");

  private String value;

  private States(String value) {
      this.value = value;
  }

  public String toString() {
      return value;
  }
}

Parsear desde una cadena

El problema viene cuando queremos obtener un valor del enumerado a partir de un String. Supongamos que la transformación la vamos a hacer en un método estático, que por contraste con el anterior método llamaremos fromString. Veamos qué opciones tenemos, de menos a más apropiadas:

Sentencias if

La primera solución que le viene a uno a la cabeza es usar sentencias if.

1
2
3
4
5
6
7
8
9
10
11
12
[...]
public static States fromString(String fromValue) {
  if("green".equals(fromValue)) {
      return EVERYTHING_IS_OK;
  } else if("yellow".equals(fromValue)) {
      return SOMETHING_WENT_WRONG;
  } else if("red".equals(fromValue)) {
      return TOTAL_FAILURE;
  }
  throw new IllegalArgumentException("Wrong value: " + fromValue);
}
[...]

No hay mucho que decir aquí, simplemente que se debería evitar a toda costa esta solución.

Bucle for

Otra solución, un poco más elegante, pero básicamente siguiendo la misma filosofía, podría ser utilizar un bucle para recorrer los posibles valores del enumerado:

1
2
3
4
5
6
7
8
9
10
[...]
public static States fromString(String fromValue) {
  for(States state : values()) {
      if(state.value.equals(fromValue)) {
          return state;
      }
  }
  throw new IllegalArgumentException("Wrong value: " + fromValue);
}
[...]

Esta solución es un poquito mejor, ya que si modificamos los valores posibles del enumerado, no es necesario que modifiquemos este método.

Prealmacenamiento de los valores en un Map

Existe una solución que no involucra el uso de sentencias if, lo cual es muy positivo, ya que estamos evitando la posibilidad de error, ya que no tendremos que formular una condición.

Esta solución la encontré en un hilo el de Stack Overflow how can I lookup a Java enum from its string value?. Lo que pretende esta solución es crear una estructura de datos, y cuando se quiera transformar un String en un valor del enumerado, simplemente se consulte esa estructura de datos.

La estructura de datos será un Map estático, que se rellenará en un bloque estático y consultaremos en nuestro método fromString.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[...]
private static Map<String, States> dictionary;
static {
  dictionary = new HashMap<String, States>();
  for(States state : values()) {
      dictionary.put(state.value; state);
  }
}
[...]
public static States fromString(String fromValue) {
  States state = dictionary.get(fromValue);
  if(state == null) {
      throw new IllegalArgumentException("Wrong value: " + fromValue);
  }
  return state;
}
[...]

Peligros de esta última solución

En Stack Overflow, un comentario a la respuesta donde encontré la solución sugiere que esta solución puede causar problemas debido al classloader.

Pero he investigado un poco el tema, y parece que el comentario es erróneo, al menos para el uso de los enumerados que estoy exponiendo aquí. Según el ejemplo 8.9.2-1 de la especificación del java, primero se inicializan los valores del enumerado, luego la variable estática y finalmente se ejecuta el bloque de código estático, estando ya todos los valores del enumerado inicializados.

Aquí os dejo unos cuantos enlaces a que confirman que la última solución es correcta:

Otras soluciones

Por supuesto que existen otras soluciones, quizá mejores que la última que propongo, pero no he querido hacer más largo este post y he querido centrarme en proponer una alternativa a los métodos .name() y .valueOf(), pertenecientes al propio enumerado.

Es por esto por lo que he descartado usar clases externas para formatear o parsear el enumerado, lo cual me habría hecho llegar a otras soluciones bien distintas. Pero si crees que tu solución es muchísimo mejor que ésta, déjame un comentario, será bienvenido.

Comments