Formatear cadenas en Java con printf(), format(), Formatter y MessageFormat

En este tutorial, formatearemos cadenas en Java usando printf(), System.format(), String.format(), las clases Formatter y MessageFormat.

Introducción

Hay varias formas de formatear cadenas en Java. Algunos de ellos son de la vieja escuela y están tomados directamente de los viejos clásicos (como printf de C), mientras que otros tienen más el espíritu de la programación orientada a objetos, como la clase MessageFormat.

En este artículo, pasaremos por alto varios de estos enfoques. Mostraremos algunos detalles de cómo se puede utilizar cada una de las técnicas y en qué circunstancias. Con este conocimiento, sabrá cómo abordar el formato de cadenas y cuál de las técnicas usar.

Sistema.out.printf()

Comencemos con el viejo clásico, printf(). Como se mencionó antes, printf() proviene del lenguaje de programación C y significa impresión formateada. Bajo el capó, printf() usa java.util.Formatter, del cual hablaremos más adelante.

La forma en que funciona printf() puede explicarse por sus argumentos. La forma más común de usar printf() es la siguiente:

1
System.out.printf(String format, String... arguments);

Podemos ver que el método espera un formato y un vararg argumentos. El argumento formato define la forma en que desea que se formatee la cadena: una plantilla para el resultado final.

Por ejemplo, es posible que desee imprimir un número decimal con exactamente siete lugares decimales o un número en representación hexadecimal. O bien, puede tener un mensaje predefinido para saludar a los usuarios, pero le gustaría formatearlo para incluir el nombre de usuario.

El vararg arguments espera convenientemente los argumentos (es decir, valores) para la plantilla String. Por ejemplo, si la plantilla tiene marcadores de posición para dos números, el método printf() también esperará dos números como argumentos:

1
System.out.printf("%d %d", 42, 23);

Hemos puesto dos símbolos %d en la cadena de plantilla. Estos dos símbolos representan marcadores de posición para un determinado tipo de valor. Por ejemplo, %d es un marcador de posición para un valor numérico decimal. Como tenemos dos, tenemos que pasar dos argumentos que se correspondan con valores numéricos, como 42 y 23.

Ejecutar este código producirá:

1
42 23

Especificadores de formato

Con printf(), puede imprimir valores como números, cadenas, fechas, etc. Para que el método sepa exactamente qué está tratando de imprimir, debe proporcionar un especificador de formato para cada uno de los valores . Veamos un ejemplo:

1
System.out.printf("Hello, %s!", "reader");

Si se ejecuta, este código imprimirá Hola, lector en la consola. El símbolo %s representa un especificador de formato para cadenas, similar a cómo %d representa un especificador de formato para números decimales.

Hay muchos especificadores de formato que podemos usar. Aquí hay algunos comunes:

  • %c - Personaje
  • %d - Número decimal (base 10)
  • %e - Número de punto flotante exponencial
  • %f - Número de punto flotante
  • %i - Número entero (base 10)
  • %o - Número octal (base 8)
  • %s - Cadena
  • %u - Número decimal (entero) sin signo
  • %x - Número hexadecimal (base 16)
  • %t - Fecha/hora
  • %n - Nueva línea

Si queremos imprimir, por ejemplo, un carácter y un número octal, usaríamos los especificadores %c y %o, respectivamente. Es posible que notes algo inusual: el especificador de nueva línea. Si no estás acostumbrado al comportamiento de printf() desde C, puede parecer un poco extraño tener que especificar cosas como esta.

Bueno, printf() no escribe una nueva línea por defecto. De hecho, no hace casi nada por defecto. Básicamente, si quieres que algo suceda, tienes que hacer que suceda tú mismo.

Es decir, si tenemos varias sentencias printf() sin un especificador de nueva línea:

1
2
3
System.out.printf("Hello, %s!", "Michael Scott");
System.out.printf("Hello, %s!", "Jim");
System.out.printf("Hello, %s!", "Dwight");

El resultado sería:

1
Hello, Michael Scott!Hello, Jim!Hello, Dwight!

Sin embargo, si incluimos el carácter de nueva línea:

1
2
3
System.out.printf("Hello, %s!%n", "Michael Scott");
System.out.printf("Hello, %s!%n", "Jim");
System.out.printf("Hello, %s!%n", "Dwight");

Entonces el resultado seria:

1
2
3
Hello, Michael Scott!
Hello, Jim!
Hello, Dwight!

Nota: %n es un formato especial que puede ser \r\n o simplemente \n. \n es el símbolo real de nueva línea, mientras que \r es el símbolo de retorno de carro. Por lo general, se recomienda usar \n ya que funciona como se espera en todos los sistemas, a diferencia de %n, que puede entenderse como cualquiera de los dos. Más sobre esto más adelante.

Caracteres de escape {#caracteres de escape}

Además de los especificadores de formato descritos anteriormente, hay otro tipo de símbolos de formato: Caracteres de escape.

Imaginemos que queremos imprimir un símbolo " usando printf(). Podemos intentar algo como:

1
System.out.printf(""");

Si intenta ejecutar esto, su compilador definitivamente generará una excepción. Si observa detenidamente, incluso el código que resalta el código en esta página resaltará ); como una cadena, y no como el corchete cerrado del método.

Lo que sucedió fue que intentamos imprimir un símbolo que tiene un significado reservado especial. Las comillas se utilizan para indicar el principio y el final de una cadena.

Comenzamos y terminamos un String "", después de lo cual abrimos otro " pero no lo cerramos. Esto hace que la impresión de caracteres reservados como este imposible, usando este enfoque.

La forma de eludir esto es escapando. Para imprimir caracteres especiales (como ") directamente, primero debemos escapar de sus efectos, y en Java eso significa anteponer una barra invertida (\). Para imprimir legalmente una comilla en Java, haríamos lo siguiente:

1
System.out.printf("\"");

La combinación de \ y " le dice específicamente al compilador que nos gustaría insertar el carácter " en ese lugar y que debe tratar " como un valor concreto, no como un símbolo reservado.

La aplicación del carácter de escape \ puede invocar diferentes efectos en función del siguiente. Pasar un carácter regular (no reservado) no hará nada y \ será tratado como un valor.

Sin embargo, ciertas combinaciones (también llamadas comandos) tienen un significado diferente para el compilador:

  • \b - Insertar retroceso
  • \f - El primer carácter de la siguiente línea comienza a la derecha del último carácter de la línea actual
  • \n - Insertar nueva línea
  • \r - Insertar retorno de carro
  • \t - Insertar pestaña
  • \\ - Insertar barra invertida
  • %% - Insertar signo de porcentaje

Por lo tanto, usaría \n para imprimir un separador de línea en la consola, comenzando efectivamente cualquier contenido nuevo desde el comienzo de la siguiente línea. De manera similar, para agregar pestañas, usaría el especificador \t.

Es posible que haya notado %% como la última combinación.

¿Por qué es esto? ¿Por qué no se usa \% simplemente?

El carácter % ya es un carácter de escape específicamente para el método printf(). Seguido de caracteres como d, i, f, etc., el formateador en tiempo de ejecución sabe cómo tratar estos valores.

Sin embargo, el carácter \ está destinado al compilador. Le dice dónde y qué insertar. El comando \% simplemente no está definido y usamos el carácter de escape % para escapar del efecto del carácter subsiguiente %, si eso tiene sentido.

Para el compilador, % no es un carácter especial, pero \ sí lo es. Además, es una convención que los caracteres especiales se escapen. \ escapa a \ y % escapa a %.

Uso básico

Vamos a formatear una Cadena con múltiples argumentos de diferentes tipos:

1
System.out.printf("The quick brown %s jumps %d times over the lazy %s.\n", "fox", 2, "dog");

La salida será:

1
The quick brown fox jumps 2 times over the lazy dog.

Flotación y doble precisión

Con printf(), podemos definir una precisión personalizada para los números de coma flotante:

1
2
3
4
double a = 35.55845;
double b = 40.1245414;

System.out.printf("a = %.2f b = %.4f", a, b);

Dado que %f se usa para flotantes, podemos usarlo para imprimir dobles. Sin embargo, al agregar un .n, donde n es el número de lugares decimales, podemos definir una precisión personalizada.

Ejecutar este código produce:

1
2
a = 35.56
b = 40.1245

Relleno de formato

También podemos agregar relleno, incluido el String pasado:

1
System.out.printf("%10s\n", "stack");

Aquí, después del carácter %, hemos pasado un número y un especificador de formato. Específicamente, queremos una cadena con 10 caracteres, seguida de una nueva línea. Dado que stack solo contiene 5 caracteres, se agregan 5 más como relleno para "llenar" la cadena hasta el carácter de destino:

1
     stack

También puede agregar relleno derecho en su lugar:

1
System.out.printf("%-10s\n", "stack");

Configuración regional

También podemos pasar un Locale como primer argumento, formateando el String de acuerdo con él:

1
2
System.out.printf(Locale.US, "%,d\n", 5000);
System.out.printf(Locale.ITALY, "%,d\n", 5000);

Esto produciría dos enteros con formato diferente:

1
2
5,000
5.000

Índice de argumentos

Si no se proporciona un índice de argumento, los argumentos simplemente seguirán el orden de presencia en la llamada al método:

1
System.out.printf("First argument is %d, second argument is %d", 2, 1);

Esto daría como resultado:

1
First argument is 2, argument number is 1

Sin embargo, después del carácter de escape % y antes del especificador de formato, podemos agregar otro comando. $n especificará el índice del argumento:

1
System.out.printf("First argument is %2$d, second argument is %1$d", 2, 1);

Aquí, 2$ está ubicado entre % y d. 2$ especifica que nos gustaría adjuntar el segundo argumento de la lista de argumentos a este especificador. De manera similar, 1$ especifica que nos gustaría adjuntar el primer argumento de la lista al otro especificador.

Ejecutar este código da como resultado:

1
First argument is 1, second argument is 2

Puede apuntar ambos especificadores al mismo argumento. En nuestro caso, eso significaría que solo usamos un único argumento proporcionado en la lista. Eso está perfectamente bien, aunque todavía tenemos que proporcionar todos los argumentos presentes en la cadena de plantilla:

1
System.out.printf("First argument is %2$d, second argument is %2$d", 2, 1);

Esto dará como resultado:

1
First argument is 1, second argument is 1

System.out.format()

Antes de hablar sobre System.out.format(), centrémonos brevemente en System.out.

Todos los sistemas UNIX tienen tres tuberías principales: tubería de entrada estándar (stdin), tubería de salida estándar (stdout) y tubería de error estándar (stderr). El campo out corresponde a la tubería stdout y es de tipo flujo de impresión.

Esta clase tiene muchos métodos diferentes para imprimir representaciones basadas en texto con formato en una secuencia, algunas de las cuales son format() y printf().

Según la documentación, ambos se comportan exactamente de la misma manera. Esto significa que no hay diferencia entre los dos y se puede utilizar para obtener los mismos resultados. Todo lo que hemos dicho hasta ahora sobre printf() también funciona para format().

Tanto printf() como System.out.format() se imprimen en la tubería stdout, que normalmente está dirigida a la consola/terminal.

Cadena.formato() {#formato de cadena}

Otra forma de formatear cadenas es con el método String.format() que internamente también usa java.util.Formatter, que exploraremos en la siguiente sección.

La principal ventaja de String.format() sobre printf() es su tipo de devolución: devuelve una String. En lugar de simplemente imprimir el contenido en la tubería de salida estándar y no tener un tipo de retorno (void) como lo hace printf(), String.format() se usa para formatear una cadena que se puede usar o reutilizar en el futuro:

1
String formattedString = String.format("Local time: %tT", Calendar.getInstance());

Ahora puede hacer lo que quiera con formattedString. Puede imprimirlo, puede guardarlo en un archivo, puede modificarlo o conservarlo en una base de datos. Imprimirlo daría como resultado:

1
Local time: 16:01:42

El método String.format() utiliza exactamente el mismo principio subyacente que el método printf(). Ambos utilizan internamente la clase Formatter para dar formato a las cadenas. Por lo tanto, todo lo dicho para printf() también se aplica al método String.format().

Usar printf(), String.format() o Formatter es esencialmente lo mismo. Lo único que difiere es el tipo de devolución: printf() imprime en el flujo de salida estándar (típicamente su consola) y String.format() devuelve una String formateada.

Dicho esto, String.format() es más versátil ya que puedes usar el resultado en más de una forma.

La clase de formateador

Dado que todos los métodos anteriores intrínsecamente llaman al Formatter, conocer solo uno significa que los conoce todos.

El uso de Formatter es bastante similar a otras técnicas mostradas anteriormente. La mayor diferencia es que para usarlo, uno necesita instanciar un objeto Formatter:

1
2
3
Formatter f = new Formatter();
f.format("There are %d planets in the Solar System. Sorry, Pluto", 8);
System.out.println(f);

Esto plantea la pregunta:

¿Por qué no usaría siempre los métodos anteriores, ya que son más concisos?

Hay una distinción más importante que hace que la clase Formatter sea bastante flexible:

1
2
3
4
StringBuilder sb = new StringBuilder();
Formatter formatter = new Formatter(sb);

formatter.format("%d, %d, %d...\n", 1, 2, 3);

En lugar de trabajar solo con Strings, Formatter también puede funcionar con StringBuilder, lo que hace posible (re)utilizar ambas clases de manera eficiente.

De hecho, Formatter puede trabajar con cualquier clase que implemente la interfaz Appendable. Uno de esos ejemplos es el mencionado StringBuilder, pero otros ejemplos incluyen clases como BufferedWriter, FileWriter, PrintStream, PrintWriter, StringBuffer, etc. La lista completa se puede encontrar en la [documentación] (https://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html).

Finalmente, todos los especificadores de formato, caracteres de escape, etc. también son válidos para la clase Formatter ya que esta es la lógica principal para formatear cadenas en los tres casos: String.format(), printf() y Formateador.

Formato del mensaje

Finalmente, mostremos una última técnica de formateo que no usa Formatter bajo el capó.

MessageFormat se creó para producir y proporcionar mensajes concatenados de una manera neutral en cuanto al idioma. Esto significa que el formato será el mismo, independientemente de si está utilizando Java, Python o algún otro lenguaje compatible con MessageFormat.

MessageFormat extiende la clase abstracta Format, tal como lo hacen DateFormat y NumberFormat. La clase Format está destinada a formatear objetos sensibles a la configuración regional en cadenas.

Veamos un buen ejemplo, cortesía de la [documentación] de MessageFormat (https://docs.oracle.com/javase/6/docs/api/java/text/MessageFormat.html).

1
2
3
4
5
6
7
int planet = 7;
String event = "a disturbance in the Force";

String result = MessageFormat.format(
    "At {1, time} on {1, date}, there was {2} on planet {0, number, integer}.",
    planet, new Date(), event
);

[Crédito del código: Documentos de Oracle]{.small}

La salida es:

1
At 11:52 PM on May 4, 2174, there was a disturbance in the Force on planet 7.

En lugar de los especificadores de porcentaje que hemos visto hasta ahora, aquí estamos usando corchetes para cada uno de los argumentos. Tomemos el primer argumento, {1, time}. El número 1 representa el índice del argumento que debe usarse en su lugar. En nuestro caso, los argumentos son planeta, nueva fecha() y evento.

La segunda parte, tiempo, se refiere al tipo del valor. Los tipos de formato de nivel superior son número, fecha, hora y elección. Para cada uno de los valores, se puede hacer una selección más específica, como {0, number, integer}, que dice que el valor debe tratarse no solo como un número, sino también como un número entero.

El conjunto completo de tipos y subtipos de formato se puede encontrar en la documentación.

Conclusión

En este artículo, hemos pasado por alto una buena cantidad de formas de formatear cadenas en el núcleo de Java.

Cada una de las técnicas que hemos mostrado tiene su propia razón de ser. printf(), por ejemplo, recuerda al método C de la vieja escuela del mismo nombre de.

Otros enfoques, como Formatter o MessageFormat ofrecen un enfoque más moderno que explota algunos beneficios de la programación orientada a objetos.

Cada técnica tiene casos de uso específicos, por lo que, con suerte, podrá saber cuándo usar cada una en el futuro.

Licensed under CC BY-NC-SA 4.0