Distintas formas de serialización en Java: Comparación de rendimiento

Hace poco, he tenido que serializar/deserializar unos datos en Java, hacía mucho que no lo hacía en formato binario directamente (ultimamente he utilizado JSON o XML). Recordaba haber serializado objetos implementando el interfaz Serializable, pero había visto por Internet otra forma implementando el interfaz Externalizable, ¿cuál interfaz utilizo? Pues depende lo que quieras y necesites, como todo.

Serializable

  • Cuando quieras serializar algo programando poco, es la forma fácil.
  • Pero tiene algunas restricciones: El objeto a serializar debe implementar el constructor por defecto. Debe hacerse responsable de los atributos no accesibles de la clase padre.
  • Cuando no te importe mucho el rendimiento, como veremos en la sección Pruebas de Rendimiento (Serializable vs Externalizable).

Externalizable

  • Vas a tener que implementar tú mismo los métodos de serialización.
  • Cuando no puedas utilizar Serializable.
  • Cuando quieras obtener un mejor rendimiento, como veremos en la sección Pruebas de Rendimiento (Serializable vs Externalizable)
  • Si tienes que encargarte de la serialización de los atributos de la clase padre, te recomiendo utilizar Externalizable, porque evitamos una sobrescritura extraña de métodos privados.
private void writeObject(ObjectOutputStream oos)
private void readObject(ObjectInputStream ois)

Pruebas de Rendimiento (Serializable vs Externalizable)

El interfaz Serializable utilizando la introspección de Java, adivina los tipos de los atributos de las clases que va a serializar/deserializar, pero esta "magia" tiene una penalización en el rendimiento.

Al utilizar el interfaz Externalizable somos nosotros los que decidimos como serializar/deserializar los objetos, es decir, tenemos que escribir nosotros el código que hace esto. Hemos perdido comodidad, pero evitamos que Java haga algunas tareas y, si sobrescribimos correctamente los métodos del interfaz Externalizable, conseguiremos una mejora de rendimiento.

Para saber cuánto es la diferencia de rendimiento, he escrito un pequeño ejemplo en el que se serializa un objeto con dos colecciones con 100000 emails y 100000 teléfonos. He contemplado 3 casos:

Utilizando el interfaz Serializable

Como dijimos más arriba, utilizando el interfaz Serializable Java tiene que adivinar ciertas cosas, en este proceso sacrifica algo de rendimiento (es el que más tarda), a cambio ĺo único que tenemos que hacer es que nuestra clase implemente el interfaz Serializable.

public class Contacts implements Serializable {
  • 1133 milisegundos en serializar
  • 506 milisegundos en deserializar

Utilizando el interfaz Externalizable (mal)

En este caso somos nosotros los encargados de "decir" a Java cómo debe serializar/deserializar, pero debemos ser cuidadosos, de lo contrario podemos quedarnos con lo peor de los dos mundos, por ejemplo: Si simplemente serializamos/deserializamos los atributos de la clase, y estos son atributos complejos (como colecciones), Java también tiene que adivinar bastantes cosas y también sacrificamos algo de rendimiento y además hemos tenido que escribir más código.

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
  setEmails((Set<String>) in.readObject());
  setPhones((Set<String>) in.readObject());
}

@Override
public void writeExternal(ObjectOutput out) throws IOException {
  out.writeObject(emails);
  out.writeObject(phones);
}
  • 737 milisegundos en serializar
  • 367 milisegundos en deserializar

Utilizando el interfaz Externalizable (bien)

Si serializamos/deserializamos uno por uno los elementos de las colecciones, ahorramos aún más tiempo, porque Java está serializando tipos más simples (en este caso java.lang.String).

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
  emails.clear();
  phones.clear();
  int nEmails = in.readInt();
  for (int i = 0; i < nEmails; i++) {
    emails.add(in.readUTF());
  }
  int nPhones = in.readInt();
  for (int i = 0; i < nPhones; i++) {
    phones.add(in.readUTF());
  }
}

@Override
public void writeExternal(ObjectOutput out) throws IOException {
  out.writeInt(emails.size());
  for (String e : emails) {
    out.writeUTF(e);
  }
  out.writeInt(phones.size());
  for (String p : phones) {
    out.writeUTF(p);
  }
}
  • 204 milisegundos en serializar
  • 92 milisegundos en deserializar

Hemos ganado rendimiento a costa de escribir un poco más de código.

Analizando los resultados

¡Precaución!

Por el hecho de utilizar un interfaz u otro, no ganamos rendimiento.

Consejo

Ganamos rendimiento porque el interfaz Externalizable nos "obliga" a implementar parte de la serialización y quitamos esta carga a Java.

Aunque como hemos visto en Utilizando el interfaz Externalizable (bien), si no tenemos cuidado, conseguiremos una mejora muy pequeña.

A continuación podéis ver un gráfico con los resultados de los tests.

Gráfico con los resultados

Abrir el gráfico interactivo | Abrir imagen

A continuación os dejo los enlaces a:

Comentarios