Archivo de la etiqueta: Java

El separador de decimales en Java

El Locale es la configuración regional que le dice a un programa informático cómo mostrar y formatear datos según un idioma, país y cultura específicos.

El Locale afecta a la forma de mostrar diversos elementos:

  • Números: separador decimal, miles
  • Fechas: DD/MM/YYYY vs MM/DD/YYYY
  • Moneda: €1.234,56 vs $1,234.56
  • Mayúsculas/minúsculas: Ñ → ñ (turco: i → İ)
  • Ordenación: ñ después de n en español

Por ejemplo, según el Locale seleccionado, estas serían distintas formas de mostrar el mismo número 1234.56:

🇺🇸 USA (en_US): 1,234.56
🇪🇸 España (es_ES): 1.234,56
🇩🇪 Alemania (de_DE): 1.234,56
🇫🇷 Francia (fr_FR): 1 234,56
🇯🇵 Japón (ja_JP): 1,234.56

El Locale consta de un idioma, un país y, opcionalmente, una variante. Por ejemplo:

Locale = Idioma + País [+ Variante]
ej: es_ES   → español (España)
    en_US   → inglés (Estados Unidos)
    pt_BR   → portugués (Brasil)

Al arrancar el sistema operativo (Windows, Linux, Mac,…) se carga un Locale por defecto que es el que usarán nuestros programas Java, salvo que indiquemos otra cosa.

Puedes comprobar el Locale que tienes cargado en el ordenador mediante la siguiente instrucción:

System.out.println(Locale.getDefault());

En mi caso, la salida ha sido:

es_ES

que corresponde al idioma español de España.

El Locale afecta a la forma de leer o escribir números double, ya sea en el terminal o en ficheros de texto.

Dentro de nuestros programas, podemos establecer el Locale de la siguiente forma:

Locale.setDefault(Locale.UK); // Locale del Reino Unido
Locale.setDefault(Locale.US); // Locale de Estados Unidos
Locale.setDefault(new Locale("es", "ES"));

Imprimir números double

Si se utilizan los métodos print() o println(), los números double se imprimen utilizando el punto como separador de decimales, independientemente del Locale activo en el ordenador:

System.out.println(1234.56); // Imprime 1234.56

El comportamiento es diferente cuando se usan métodos printf(). Con la configuración en español, cuando imprimimos un número double utilizando la instrucción printf(), el separador de decimales será la coma, no el punto:

System.out.printf("%.2f%n", 1234.56); // Imprime 1234,56

Si queremos que, además del separador de decimales, se imprima el separador de miles, hay que añadir al especificador de formato una coma antes del punto decimal:

System.out.printf("%,.2f%n", 1234.56); // Imprime 1.234,56

Si queremos forzar el uso del punto como separador de decimales podemos hacer:

Locale.setDefault(Locale.UK); // Locale del Reino Unido
System.out.printf("%.2f%n", 1234.56); // Imprime 1234.56
System.out.printf("%,.2f%n", 1234.56); // Imprime 1,234.56

Este comportamiento al escribir es el mismo cuando se escribe en el terminal o cuando se escribe en ficheros de texto.

¿Y por qué los métodos print() y println() utilizan siempre el punto como separador de decimales, independientemente del Locale que haya activo? La razón es que, internamente, estos métodos convierten los números double a cadena de texto utilizando el método toString() de la clase Double y dicho método no utiliza ningún formato regional, siempre utiliza el punto decimal en la conversión.

Hay que tener claras dos cosas:

  • Internamente, los números double no se ven afectados de ninguna manera por el Locale del ordenador. La representación interna del número utiliza otro tipo de evaluación, el Locale solo afecta a la representación textual del número al hacer entradas o salidas desde el terminal o desde ficheros de texto.
  • Al codificar, al escribir los programas, tenemos que utilizar siempre el punto como separador de decimales.

Leer números double

Con el ordenador usando el Locale español, si leemos un número double desde el terminal utilizando el método nextDouble() de la clase Scanner, deberá estar escrito con la coma como separador de decimales. Si el número está escrito con el punto, el programa lanzará una excepción y se interrumpirá abruptamente.

Prueba el siguiente programa:

package principal;

import java.util.Locale;
import java.util.Scanner;

public class Main {
   public static void main(String[] args) {
      System.out.println(Locale.getDefault());
	
      Scanner sc = new Scanner(System.in);
      System.out.print("Teclee un número con decimales: ");
      double x = sc.nextDouble();
      sc.close();
   }
}

He ejecutado el código en mi ordenador, configurado con Locale español y he tecleado el número utilizando el punto como separador de decimales. El resultado ha sido el siguiente:

Observa que el programa lanza una excepción del tipo InputMismatchException y se interrumpe.

He repetido la ejecución pero, en esta ocasión, he tecleado el número utilizando la coma como separador de decimales:

Observa que ahora el programa ha funcionado correctamente.

La clase Scanner ofrece el método setLocale(), que permite elegir un determinado Locale en las lecturas que haga. Por ejemplo, en el código siguiente, el primer double se lee utilizando el Locale del Reino Unido y, el segundo, utilizando el Locale español:

Scanner sc = new Scanner(System.in);

sc.useLocale(Locale.UK);
double x = sc.nextDouble(); // Espera leer 3.14

sc.useLocale(new Locale("es", "ES"));
double y = sc.nextDouble(); // Espera leer 3,14

Leer cadenas de texto y convertirlas en double

Otra técnica que puede ser útil al leer números con Scanner es leer una línea de texto con nextLine() y luego convertir la cadena leída a double utilizando el método estático parseDouble() de la clase Double.

El método Double.parseDouble(), al igual que sucede con el método toString(), siempre trabaja con el punto como separador de decimales. El siguiente ejemplo, leería un número double que debería estar escrito con punto decimal:

double z = Double.parseDouble(sc.nextLine());

Imponer un Locale para todo el programa

En programas profesionales, si no tomamos precauciones, puede resultar que la ejecución sea diferente según el Locale del ordenador en el que se esté ejecutando el programa. Por tanto, es necesario tomar medidas de forma que la ejecución siempre sea la misma, independientemente del Locale que esté configurado en el ordenador en el que se ejecute el programa.

Podemos imponer al principio del programa que se utilice un Locale determinado. Esta solución afectará tanto a las entradas que hagamos a través de objetos Scanner como a las salidas que hagamos con métodos printf(). Por ejemplo, podríamos imponer el Locale del Reino Unido o el de Estados Unidos y forzar a que las entradas y salidas de números decimales haya que hacerlas usando el punto como separador de decimales. La instrucción sería una de las dos siguientes:

Locale.setDefault(Locale.UK);
Locale.setDefault(Locale.US);

Otra opción sería imponer el Locale español de España. Para seleccionar el Locale de España no disponemos de una constante predefinida como en los casos anteriores y la instrucción que habría que poner al principio del programa sería:

Locale.setDefault(new Locale("es", "ES"));

También es posible imponer el Locale pasando un parámetro a la Máquina Virtual de Java al ejecutar el programa. Si ejecutamos desde el terminal y queremos imponer el Locale de Estados Unidos, habría que hacer:

java -Duser.language=en -Duser.country=US principal.Main

En Eclipse, podemos configurar la ejecución del programa y pasarle los argumentos a la JVM, como se muestra en la figura:

Cualquiera que sea la opción que uses, en programas profesionales en los que se realicen cálculos científicos con números decimales, es necesario tomar las precauciones necesarias para que el programa se ejecute de manera correcta en cualquier entorno de ejecución.

Codificación de ficheros fuente en Java

Al compilar ficheros .java, el compilador javac asume que los ficheros tienen una codificación por defecto. En los sistemas Linux o Mac, javac asume que los ficheros están codificados en UTF-8. En Windows, las versiones posteriores a Java 17 también asumen que los ficheros fuente están codificados en UTF-8, pero hasta la versión Java 17 inclusive, javac supone que los ficheros utilizan la codificación de Windows. En España, la codificación que utiliza Windows suele ser Windows-1252.

Esto es así, independientemente de la página de códigos que tengamos activa en el terminal.

Podemos indicar al compilador que utilice una codificación específica para los ficheros fuente .java, utilizando el parámetro -encoding:

javac -encoding UTF-8 *.java

Vamos a ver un ejemplo completo utilizando una variante de programa Hola Mundo, que utiliza caracteres especiales del español y servirá para entender el problema.

Vamos a compilar y ejecutar la siguiente clase Java:

public class Hola {
   public static void main(String[] args) {
      System.out.println("¡Hola, ¿qué tal?, ¡Vaya año llevamos!");
   }
}

Vamos a utilizar, en primer lugar, la siguiente configuración: Windows 10, Windows Terminal con la página de códigos activa en UTF-8, compilación y ejecución con Java 17 y ficheros .java codificados en UTF-8. La salida obtenida es la de la siguiente figura:

Se puede ver que no se reconocen los caracteres especiales del español. Lo que está pasando es que javac 17 asume que los ficheros fuente están codificados en Windows-1252. Y este comportamiento es independiente de la codificación que tengamos activa en el terminal.

Si compilamos con javac 17, utilizando el parámetro -encoding UTF-8, el fichero fuente se interpreta de manera adecuada:

Java proporciona un comando para poder ver el valor de las diferentes variables que usa:

java -XshowSettings:properties -version

La salida del comando anterior, en la configuración indicada, fue la siguiente:

Se puede observar que Java asume que los ficheros están codificados en Windows-1252.

Ahora vamos a utilizar la versión 21 de Java. En el terminal de Windows, vamos a dejar activa la página de código 850 y compilar y ejecutar el mismo programa. La salida es la siguiente:

Observa que ahora la interpretación de los caracteres especiales del español es correcta. Una vez más, independientemente de la codificación activa del terminal, que en este caso no era UTF-8.

Si consultamos las opciones que está utilizando Java 21:

Vemos que ahora Java 21 asume que los ficheros fuente están codificados en UTF-8.

Podemos hacer una última prueba: compilar con Java 21, pero diciéndole al compilador que el fichero Hola.java está codificado en Windows-1252:

Observa que la salida es la misma que obteníamos con Java 17, cuando interpretaba mal la codificación del fichero fuente.

Por tanto, al compilar ficheros fuente codificados en UTF-8, si utilizamos la versión Java 17 o una inferior, tendremos que utilizar el parámetro -encoding UTF-8 para obtener resultados correctos. Si usamos una versión de Java superior a la 17, ya se presupone que los ficheros fuente están codificados en UTF-8 y no será necesario utilizar el parámetro -encoding.

¿Y si estamos trabajando en Eclipse? Bueno, Eclipse proporciona una opción para indicar la codificación de los ficheros fuente y luego se encarga de pasarla al compilador o al entorno de ejecución. La opción la podemos configurar en: “Propiedades del proyecto -> Resource -> Text file encoding“, como se ve en la siguiente figura: