Consideraciones de Java toString ()

Incluso los desarrolladores de Java principiantes son conscientes de la utilidad del método Object.toString () que está disponible para todas las instancias de clases de Java y se puede anular para proporcionar detalles útiles sobre cualquier instancia particular de una clase de Java. Desafortunadamente, incluso los desarrolladores experimentados de Java ocasionalmente no aprovechan al máximo esta poderosa característica de Java por diversas razones. En esta publicación de blog, miro el humilde Java toString()y describo los pasos sencillos que se pueden tomar para mejorar el utilitarismo de toString().

Implementar (anular) explícitamente toString ()

Quizás la consideración más importante relacionada con lograr el máximo valor de toString()es proporcionar implementaciones de ellos. Aunque la raíz de todas las jerarquías de clases de Java, Object, proporciona una implementación toString () que está disponible para todas las clases de Java, el comportamiento predeterminado de este método casi nunca es útil. El Javadoc para Object.toString () explica lo que se proporciona de forma predeterminada para toString () cuando no se proporciona una versión personalizada para una clase:

El método toString para la clase Object devuelve una cadena que consta del nombre de la clase de la que el objeto es una instancia, el carácter de signo de arroba "@" y la representación hexadecimal sin signo del código hash del objeto. En otras palabras, este método devuelve una cadena igual al valor de: getClass().getName() + '@' + Integer.toHexString(hashCode())

Es difícil llegar a una situación en la que el nombre de la clase y la representación hexadecimal del código hash del objeto separados por el signo @ sean útiles. En casi todos los casos, es mucho más útil proporcionar una descripción personalizada y explícita.

toString()

implementación en una clase para anular esta versión predeterminada.

El Javadoc para Object.toString () también nos dice qué toString()debería implicar una implementación en general y también hace la misma recomendación que estoy haciendo aquí: override toString():

En general, el  toString método devuelve una cadena que "representa textualmente" este objeto. El resultado debe ser una representación concisa pero informativa que sea fácil de leer para una persona. Se recomienda que todas las subclases anulen este método.

Siempre que escribo una nueva clase, hay varios métodos que considero agregar como parte del acto de creación de una nueva clase. Éstas incluyen

código hash()

y

es igual a (objeto)

si es apropiado. Sin embargo, en mi experiencia y en mi opinión, implementar explícitamente

toString()

siempre es apropiado.

Si la "recomendación" de Javadoc de que "todas las subclases anulan este método" no es suficiente (entonces no supongo que mi recomendación lo sea) para justificar a un desarrollador de Java la importancia y el valor de un toString()método explícito , entonces recomiendo revisar a Josh El elemento efectivo de Java de Bloch "Siempre anular toString" para obtener información adicional sobre la importancia de implementar toString(). En mi opinión, todos los desarrolladores de Java deberían tener una copia de Effective Java , pero afortunadamente el capítulo con este elemento toString()está disponible para aquellos que no tienen una copia: Métodos comunes a todos los objetos.

Mantener / actualizar toString ()

Es frustrante llamar explícita o implícitamente a un objeto toString()en una declaración de registro u otra herramienta de diagnóstico y obtener el nombre de clase predeterminado y el código hash hexadecimal del objeto devuelto en lugar de algo más útil y legible. Es casi tan frustrante tener una toString()implementación incompleta que no incluye partes significativas de las características y el estado actual del objeto. Trato de ser lo suficientemente disciplinado y generar y seguir el hábito de revisar siempre la toString()aplicación junto con la revisión de la equals(Object)e hashCode()implementaciones de cualquier trabajo de clase I en que es nuevo para mí o cada vez que estoy añadiendo o cambiando los atributos de una clase.

Solo los hechos (¡pero todos / la mayoría!)

En el capítulo de Effective Javamencionado anteriormente, escribe Bloch, "Cuando sea práctico, el método toString debería devolver toda la información interesante contenida en el objeto". Puede ser doloroso y tedioso agregar todos los atributos de una clase con muchos atributos a su implementación toString, pero el valor para quienes intentan depurar y diagnosticar problemas relacionados con esa clase valdrá la pena el esfuerzo. Por lo general, me esfuerzo por tener todos los atributos no nulos significativos de mi instancia en la representación de cadena generada (y algunas veces incluyo el hecho de que algunos atributos son nulos). Por lo general, también agrego un texto de identificación mínimo para los atributos. En muchos sentidos, es más un arte que una ciencia, pero trato de incluir suficiente texto para diferenciar los atributos sin presionar a los futuros desarrolladores con demasiados detalles. Lo más importante para mí es obtener los atributos 'valores y algún tipo de clave de identificación en su lugar.

Conozca a su audiencia

Uno de los errores más comunes que he visto cometer a los desarrolladores de Java que no son principiantes toString()es olvidar a qué y a quién toString()está destinado normalmente. En general, toString()es una herramienta de diagnóstico y depuración que facilita el registro de detalles en una instancia en particular en un momento determinado para depuración y diagnóstico posteriores. Por lo general, es un error que las interfaces de usuario muestren representaciones de cadenas generadas por toString()o tomar decisiones lógicas basadas en una toString()representación (de hecho, ¡tomar decisiones lógicas en cualquier cadena es frágil!). He visto desarrolladores bien intencionados que tienen toString()formato XML de retorno para usar en algún otro aspecto del código compatible con XML. Otro error importante es obligar a los clientes a analizar la cadena devuelta portoString()para acceder programáticamente a los miembros de datos. Probablemente sea mejor proporcionar un método de obtención / acceso público que confiar en que toString()nunca cambia. Todos estos son errores porque estos enfoques olvidan la intención de una toString()implementación. Esto es especialmente insidioso si el desarrollador elimina características importantes del toString()método (consulte el último elemento) para que se vea mejor en una interfaz de usuario.

Me gusta que las toString()implementaciones tengan todos los detalles pertinentes y que proporcionen un formato mínimo para que estos detalles sean más agradables. Este formato puede incluir caracteres de nueva línea seleccionados con criterio [ System.getProperty("line.seperator");] y tabulaciones, dos puntos, punto y coma, etc. No invierto la misma cantidad de tiempo que invertiría en un resultado presentado a un usuario final del software, pero lo intento para que el formato sea lo suficientemente agradable como para que sea más legible. Intento implementar toString()métodos que no sean demasiado complicados o costosos de mantener, pero que proporcionen un formato muy simple. Intento tratar a los futuros mantenedores de mi código como me gustaría que me trataran los desarrolladores cuyo código algún día mantendré.

En su artículo sobre toString()implementación, Bloch afirma que un desarrollador debe elegir si desea que la toString()devolución tenga o no un formato específico. Si se pretende un formato específico, debe documentarse en los comentarios de Javadoc y Bloch recomienda además que se proporcione un inicializador estático que pueda devolver el objeto a sus características de instancia en función de una cadena generada por toString(). Estoy de acuerdo con todo esto, pero creo que esto es más problemático de lo que la mayoría de los desarrolladores están dispuestos a aceptar. Bloch también señala que cualquier cambio en este formato en futuras versiones causará dolor y angustia a las personas que dependen de él (por eso no creo que sea una buena idea que la lógica dependa de una toString()salida). Con una disciplina significativa para escribir y mantener la documentación adecuada, tener un formato predefinido para untoString()podría ser plausible. Sin embargo, me parece un problema y es mejor simplemente crear un método nuevo y separado para tales usos y dejar los problemas toString()sin trabas.

No se toleran efectos secundarios

Por importante que sea la toString()implementación, generalmente es inaceptable (y ciertamente se considera de mala forma) tener la llamada explícita o implícita de toString()lógica de impacto o dar lugar a excepciones o problemas lógicos. El autor de un toString()método debe tener cuidado de asegurarse de que las referencias estén verificadas como nulas antes de acceder a ellas para evitar una NullPointerException. Muchas de las tácticas que describí en la publicación Effective Java NullPointerException Handling se pueden usar en la toString()implementación. Por ejemplo, String.valueOf (Object) proporciona un mecanismo sencillo para la seguridad nula en atributos de origen cuestionable.

Es igualmente importante que el toString()desarrollador verifique los tamaños de matriz y otros tamaños de colección antes de intentar acceder a elementos fuera de esa colección. En particular, es muy fácil encontrarse con una excepción StringIndexOutOfBoundsException cuando se intenta manipular valores String con String.substring.

Debido a que la toString()implementación de un objeto se puede invocar fácilmente sin que el desarrollador se dé cuenta conscientemente, este consejo para asegurarse de que no arroje excepciones o ejecute lógica (especialmente lógica de cambio de estado) es especialmente importante. Lo último que alguien quiere es que el acto de registrar el estado actual de una instancia conduzca a una excepción o cambio de estado y comportamiento. Una toString()implementación debería ser efectivamente una operación de solo lectura en la que se lee el estado del objeto para generar una cadena para su devolución. Si se cambia algún atributo en el proceso, es probable que sucedan cosas malas en momentos impredecibles.

Mi posición es que una toString()implementación solo debe incluir un estado en la Cadena generada que sea accesible en el mismo espacio de proceso en el momento de su generación. Para mí, no es defendible tener una toString()implementación que acceda a servicios remotos para construir un String de instancia. Quizás un poco menos obvio es que una instancia no debe completar atributos de datos porque toString()se llamó. La toString()implementación solo debe informar sobre cómo están las cosas en la instancia actual y no sobre cómo podrían ser o serán en el futuro si ocurren ciertos escenarios diferentes o si las cosas están cargadas. Para ser eficaz en la depuración y el diagnóstico, es toString()necesario mostrar cómo son las condiciones y no cómo podrían ser.

Se agradece sinceramente el formato simple

Como se describió anteriormente, el uso prudente de separadores de línea y tabulaciones puede ser útil para hacer que las instancias largas y complejas sean más agradables cuando se generan en formato String. Hay otros "trucos" que pueden mejorar las cosas. No solo String.valueOf(Object)proporciona cierta nullprotección, sino que también se presenta nullcomo la cadena "nulo" (que a menudo es la representación preferida de nulo en una cadena generada por toString (). Arrays.toString (Object) es útil para representar fácilmente arreglos como cadenas ( vea mi publicación Stringifying Java Arrays para obtener detalles adicionales).

Incluir el nombre de clase en la representación toString

Como se describió anteriormente, la implementación predeterminada de toString()proporciona el nombre de la clase como parte de la representación de la instancia. Cuando anulamos esto explícitamente, potencialmente perdemos este nombre de clase. Normalmente, esto no es un gran problema si se registra la cadena de una instancia porque el marco de registro incluirá el nombre de la clase. Sin embargo, prefiero estar seguro y tener siempre disponible el nombre de la clase. No me importa mantener la representación del código hash hexadecimal del valor predeterminado toString(), pero el nombre de la clase puede ser útil. Un buen ejemplo de esto es Throwable.toString (). Prefiero usar ese método en lugar de getMessage o getLocalizedMessage porque el primero ( toString()) incluye el Throwablenombre de la clase, mientras que los dos últimos métodos no.

toString () Alternativas

Actualmente no tenemos esto (al menos no es un enfoque estándar), pero se ha hablado de una clase Objects en Java que ayudaría mucho a preparar de manera segura y útil representaciones de cadenas de varios objetos, estructuras de datos y colecciones. No he oído hablar de ningún progreso reciente en JDK7 en esta clase. toString()Sería útil una clase estándar en el JDK que proporcionara una representación de cadena de los objetos, incluso cuando las definiciones de clase de los objetos no proporcionaran un explícito .

Apache Commons ToStringBuilder puede ser la solución más popular para construir implementaciones seguras de toString () con algunos controles de formato básicos. He escrito en un blog sobre ToStringBuilder anteriormente y hay muchos otros recursos en línea relacionados con el uso de ToStringBuilder.

El consejo técnico sobre tecnología Java de Glen McCluskey "Escribir métodos toString" proporciona detalles adicionales sobre cómo escribir un buen método toString (). En uno de los comentarios del lector, Giovanni Pelosi manifiesta su preferencia por delegar la producción de una representación de cadena de una instancia dentro de las jerarquías de herencia de toString()a una clase delegada construida para ese propósito.

Conclusión

Creo que la mayoría de los desarrolladores de Java reconocen el valor de las buenas toString()implementaciones. Desafortunadamente, estas implementaciones no siempre son tan buenas o útiles como podrían ser. En esta publicación he intentado esbozar algunas consideraciones para mejorar las toString()implementaciones. Aunque un toString()método no afectará (o al menos no debería) a la lógica como pueden hacerlo los métodos equals(Object)o hashCode(), puede mejorar la eficiencia de la depuración y el diagnóstico. Menos tiempo dedicado a averiguar cuál es el estado del objeto significa más tiempo para solucionar el problema, pasar a desafíos más interesantes y satisfacer más necesidades del cliente.

Esta historia, "Consideraciones de Java toString ()" fue publicada originalmente por JavaWorld.