Cuando trabajamos con ObjectInputStream y ObjectOutputStream en Java, podemos utilizarlos para guardar y leer información de formas que a veces resultan un poco peculiares. Por ejemplo, es posible almacenar cadenas de caracteres en formato UTF. Esto se hace para garantizar la máxima portabilidad de los datos, ya que el formato UTF asegura que la información se pueda interpretar correctamente en diferentes sistemas.
Si escribimos una cadena como hola mundo usando writeUTF, lo que realmente se guarda es la cadena literal, y podríamos incluso abrir el archivo resultante con un editor de texto para verla. Sin embargo, detrás de esa cadena hay más información almacenada en binario, como el número de caracteres que contiene la cadena. Esto es necesario porque los métodos writeUTF y readUTF funcionan escribiendo primero la longitud de la cadena y luego los caracteres que la componen. Por eso, aunque podamos ver la cadena en texto plano, no podemos simplemente usar estos métodos para leer un archivo de texto común, ya que el formato incluye datos adicionales que no son visibles.
Más allá de guardar cadenas, la verdadera potencia de ObjectOutputStream y ObjectInputStream está en la capacidad de escribir y leer objetos completos. No se trata solo de tipos primitivos como dobles o booleanos, sino de cualquier objeto que pueda ser serializado. Por ejemplo, si creamos un ArrayList con algunos elementos, como cadenas hola, adiós y buenas, podemos escribir ese objeto completo en un archivo usando writeObject.
ArrayList<String> elementos = new ArrayList<>();
elementos.add("hola");
elementos.add("adiós");
elementos.add("buenas");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("objetos"));
oos.writeObject(elementos);
oos.close();
Al abrir el archivo resultante con un editor de texto, veremos que no solo aparecen las cadenas, sino también información adicional como el nombre de la clase java.util.ArrayList, la palabra size y otros datos. Esto ocurre porque al llamar a writeObject, Java guarda todos los campos necesarios para reconstruir el objeto completo cuando lo leamos de nuevo. No es un formato pensado para ser leído por humanos, sino para que la máquina pueda recrear el objeto exactamente como estaba.
Cuando queramos leer ese objeto, usaremos readObject, que devuelve un Object genérico. Por eso, es necesario hacer un casteo para convertirlo al tipo original, en este caso un ArrayList<String>.
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("objetos"));
try {
ArrayList<String> elementosLeidos = (ArrayList<String>) ois.readObject();
System.out.println("Tamaño: " + elementosLeidos.size());
System.out.println("Contenido: " + elementosLeidos);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
ois.close();
Es importante manejar la excepción ClassNotFoundException, que puede ocurrir si intentamos leer un objeto cuya clase no está disponible en el entorno actual. Esto es raro con clases estándar como ArrayList, pero puede pasar si trabajamos con nuestras propias clases y el código que lee el objeto no tiene acceso a ellas.
Además, hay que tener cuidado con el casteo, porque si el objeto leído no es del tipo esperado, se lanzará una excepción. Por eso, siempre debemos asegurarnos de que el tipo coincide antes de hacer el casteo.
Finalmente, si queremos que nuestras propias clases puedan ser guardadas y leídas con estos streams, debemos cumplir ciertos requisitos, como implementar la interfaz Serializable. Esto permite que Java sepa cómo serializar y deserializar nuestros objetos correctamente, pero eso es algo que veremos con más detalle en otro momento. Por ahora, lo importante es entender que ObjectOutputStream y ObjectInputStream nos ofrecen una forma poderosa de persistir objetos completos, no solo datos primitivos o cadenas, y que detrás de esta funcionalidad hay un formato binario que almacena toda la información necesaria para reconstruir esos objetos.