Java: toString() genérico para todas las clases

Gracias a la reflexión en Java podemos acceder a los datos internos de nuestras clases. De esta forma podemos conocer los métodos y atributos declarados, entre otras cosas. Pues bien, hoy vamos a tener un método toString() que nos servirá para cualquier clase y que nos mostrará una cadena del tipo:

NombreClase {atributo1->valor1; atributo2->valor2; ... atributoN->valorN}

El código

Para lo anterior, tan sólo tenemos que tener un método toString() con la forma siguiente en nuestras clases:

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

...

/**
 * Obtiene una cadena con los valores del objeto.
 * @return Cadena con los valores del objeto.
 */
@Override
public String toString() {
  final StringBuilder objBuilder = new StringBuilder();
 
  // Inicio de cadena.
  objBuilder.append(getClass().getSimpleName());
  objBuilder.append(" {");
 
  try {
    // Comienza el análisis por la clase actual y recorre los elementos hasta
    // llegar a la clase base Object.
    Class<?> objClase = getClass();
    Field[] arCampos;
    String sTipo;

    while (null != objClase
           && !Object.class.getSimpleName().equals(objClase.getSimpleName())) {
      arCampos = objClase.getDeclaredFields();
      if (null != arCampos) {
        // Recorre los campos del objeto. Si es una constante omite su lectura.
        for (Field objCampo : arCampos) {
          sTipo = objCampo.getType().toString();
          if (!Modifier.isFinal(objCampo.getModifiers())
              || (sTipo.startsWith("class")
              && !sTipo.endsWith(String.class.getCanonicalName()))) {
            objCampo.setAccessible(true);
            objBuilder.append(objCampo.getName());
            objBuilder.append("->");
            objBuilder.append(objCampo.get(this));
            objBuilder.append("; ");
          }
        }
      }
 
      // Pasa a la siguiente clase padre.
      objClase = objClase.getSuperclass();
    }
  } catch (final IllegalArgumentException e) {
  } catch (final IllegalAccessException e) {
  }
 
  // Fin de cadena. Si hay datos elimina el último "; ".
  if (objBuilder.length() > getClass().getSimpleName().length() + 2) {
    objBuilder.setLength(objBuilder.length() - 2);
  }
  objBuilder.append('}');
 
  return objBuilder.toString();
}

Explicación

Lo primero de todo es saber de forma muy breve que es la reflexión en Java. De forma totalmente ajena a nosotros, cuando creamos una clase en Java, se crea una instancia de Class para dicha clase. Dicho objeto contiene toda la información de nuestra clase, métodos, atributos, anotaciones, constructores, etc.

En nuestro caso, lo que queremos saber es el contenido de las variables del objeto actual. Pues bien, veamos el código deteniéndonos en los pasos que más puedan interesar.

  objBuilder.append(getClass().getSimpleName());

Es la parte más sencilla, con eso obtenemos el nombre que le hemos dado a la clase. Si usamos, getName() nos mostraría el paquete y el nombre de la clase, por ejemplo es.jafs.ejemplo.MiClase.

while (null != objClase
       && !Object.class.getSimpleName().equals(objClase.getSimpleName())) {

Como sabemos, todas las clases, de forma transparente para nosotros heredan de la clase Object, por lo que es la clase padre de todas. Con este bucle, nos aseguramos de que salgamos al llegar a la clase Object.

arCampos = objClase.getDeclaredFields();
if (null != arCampos) {
  // Recorre los campos del objeto. Si es una constante omite su lectura.
  for (Field objCampo : arCampos) {
    sTipo = objCampo.getType().toString();
    if (!Modifier.isFinal(objCampo.getModifiers())
        || (sTipo.startsWith("class")
        && !sTipo.endsWith(String.class.getCanonicalName()))) {
      objCampo.setAccessible(true);
      objBuilder.append(objCampo.getName());
      objBuilder.append("->");
      objBuilder.append(objCampo.get(this));
      objBuilder.append("; ");
    }
  }
}

Aquí tenemos la parte principal del método. Lo primero que hacemos con getDeclaredFields() es obtener todos los atributos de la clase.

Posteriormente recorremos los atributos recibidos hay primero verificamos que no sean una constante y para ello utilizamos Modifier.isFinal(), en el caso de que sean una constante, omitiremos tanto los valores primitivos, como las cadenas. Pero el resto serán mostrados ya que el contenido de dichos objetos constantes puede variar.

Cuando el atributo pase nuestro filtro, lo que haremos es obtener su nombre y obtener su valor de toString() por defecto. Para obtener el valor de un campo lo que hacemos es llamar a los métodos get(), getString(), getInt(), etc, pasando la instancia del objeto actual.

Resultado

Tenemos dos clases, padre e hija cuyo código os mostramos a continuación. Ambas heredan de una clase ToString que contiene el método anterior.

public class Padre extends ToString {
  private int id;
  private String nombre;
  private final Hija objHija = new Hija(12.34);
  ...
}

public class Hija extends ToString {
  private double flotante;
  ...
}

Aquí podremos ver el resultado de ejecutar las dos sentencias inferiores:

  Padre objRadio = new Padre(123, "MiClase");
  System.out.println(objRadio.toString());
  ...

  Resultado:
  Padre {id->123; nombre->MiClase; objHija->Hija {flotante->12.34}}

Conclusión

Gracias a la reflexión hemos conseguido que cuando hagamos un toString() obtengamos cadenas con un formato parecido al que nos ofrece PHP. Si no queremos repetir código, lo cual no es deseable, lo mejor será crear una clase de la que hereden aquellas clases que queramos que implementen esta funcionalidad. El único problema es la herencia simple de Java, por lo que las clases que ya hereden de otra no podrán heredar de la que hayamos creado.

Todas las posibles actualizaciones de código las podréis seguir en GitHub.

También te podría gustar...

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

El tiempo límite ha expirado. Por favor, recarga el CAPTCHA.