Buscar..


Introducción

Java proporciona un mecanismo, denominado serialización de objetos, en el que un objeto se puede representar como una secuencia de bytes que incluye los datos del objeto, así como información sobre el tipo del objeto y los tipos de datos almacenados en el objeto.

Después de que un objeto serializado se haya escrito en un archivo, se puede leer del archivo y deserializar, es decir, la información de tipo y los bytes que representan el objeto y sus datos se pueden usar para recrear el objeto en la memoria.

Serialización básica en Java

¿Qué es la serialización?

La serialización es el proceso de convertir el estado de un objeto (incluidas sus referencias) en una secuencia de bytes, así como el proceso de reconstruir esos bytes en un objeto vivo en algún momento futuro. La serialización se utiliza cuando desea persistir el objeto. Java RMI también lo utiliza para pasar objetos entre JVM, ya sea como argumentos en una invocación de método de un cliente a un servidor o como valores de retorno de una invocación de método, o como excepciones lanzadas por métodos remotos. En general, la serialización se usa cuando queremos que el objeto exista más allá de la vida útil de la JVM.

java.io.Serializable es una interfaz de marcador (no tiene cuerpo). Solo se utiliza para "marcar" clases de Java como serializables.

El tiempo de ejecución de serialización asocia con cada clase serializable un número de versión, llamado serialVersionUID , que se usa durante la des- serialización para verificar que el remitente y el receptor de un objeto serializado hayan cargado clases para ese objeto que sean compatibles con respecto a la serialización. Si el receptor ha cargado una clase para el objeto que tiene un serialVersionUID diferente al de la clase del remitente correspondiente, la deserialización dará como resultado una InvalidClassException . Una clase serializable puede declarar su propio serialVersionUID declarando explícitamente un campo llamado serialVersionUID que debe ser static, final, y de tipo long :

ANY-ACCESS-MODIFIER static final long serialVersionUID = 1L;

Cómo hacer que una clase sea elegible para la serialización

Para conservar un objeto, la clase respectiva debe implementar la interfaz java.io.Serializable .

import java.io.Serializable;

public class SerialClass implements Serializable {

    private static final long serialVersionUID = 1L;  
    private Date currentTime;
    
    public SerialClass() {
        currentTime = Calendar.getInstance().getTime();
    }

    public Date getCurrentTime() {
        return currentTime;
    }
}

Cómo escribir un objeto en un archivo

Ahora necesitamos escribir este objeto en un sistema de archivos. Usamos java.io.ObjectOutputStream para este propósito.

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;

public class PersistSerialClass {

    public static void main(String [] args) {
        String filename = "time.ser";            
        SerialClass time = new SerialClass(); //We will write this object to file system.
        try {
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename));
            out.writeObject(time); //Write byte stream to file system.
            out.close();
        } catch(IOException ex){
            ex.printStackTrace();
        }
     }
 }

Cómo recrear un objeto desde su estado serializado

El objeto almacenado puede leerse desde el sistema de archivos más tarde usando java.io.ObjectInputStream como se muestra a continuación:

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.IOException;
import java.io.java.lang.ClassNotFoundException;

 public class ReadSerialClass {

    public static void main(String [] args) {
        String filename = "time.ser";    
        SerialClass time = null;

        try {
            ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename));
            time = (SerialClass)in.readObject();
            in.close();
        } catch(IOException ex){
            ex.printStackTrace();
        } catch(ClassNotFoundException cnfe){
            cnfe.printStackTrace();
        }
        // print out restored time
        System.out.println("Restored time: " + time.getTime());
     }
 }

La clase serializada está en forma binaria. La deserialización puede ser problemática si la definición de la clase cambia: consulte el capítulo Control de versiones de objetos serializados de la Especificación de serialización de Java para obtener más detalles.

Serializar un objeto serializa el gráfico de objeto completo del cual es la raíz, y funciona correctamente en presencia de gráficos cíclicos. Se proporciona un método reset() para forzar que ObjectOutputStream olvide de los objetos que ya se han serializado.

Campos transitorios - Serialización

Serialización con Gson

La serialización con Gson es fácil y producirá JSON correcto.

public class Employe {

    private String firstName;
    private String lastName;
    private int age;      
    private BigDecimal salary;
    private List<String> skills;

    //getters and setters
}

(Publicación por entregas)

//Skills 
List<String> skills = new LinkedList<String>();
skills.add("leadership");
skills.add("Java Experience");

//Employe
Employe obj = new Employe();
obj.setFirstName("Christian");
obj.setLastName("Lusardi");
obj.setAge(25);
obj.setSalary(new BigDecimal("10000"));
obj.setSkills(skills);

//Serialization process
Gson gson = new Gson();
String json = gson.toJson(obj); //{"firstName":"Christian","lastName":"Lusardi","age":25,"salary":10000,"skills":["leadership","Java Experience"]}

Tenga en cuenta que no puede serializar objetos con referencias circulares, ya que esto resultará en una recursión infinita.

(Deserialización)

//it's very simple...
//Assuming that json is the previous String object....

Employe obj2 = gson.fromJson(json, Employe.class); // obj2 is just like obj

Serialización con Jackson 2

La siguiente es una implementación que demuestra cómo un objeto puede ser serializado en su cadena JSON correspondiente.

class Test {

    private int idx;
    private String name;

    public int getIdx() {
        return idx;
    }

    public void setIdx(int idx) {
        this.idx = idx;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Publicación por entregas:

Test test = new Test();
test.setIdx(1);
test.setName("abc");
    
ObjectMapper mapper = new ObjectMapper();

String jsonString;
try {
    jsonString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(test);
    System.out.println(jsonString);
} catch (JsonProcessingException ex) {
    // Handle Exception
}

Salida:

{
  "idx" : 1,
  "name" : "abc"
}

Si no lo necesita, puede omitir la impresora bonita predeterminada.

La dependencia utilizada aquí es la siguiente:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.6.3</version>
</dependency>

Serialización personalizada

En este ejemplo, queremos crear una clase que genere y genere en la consola, un número aleatorio entre un rango de dos enteros que se pasan como argumentos durante la inicialización.

    public class SimpleRangeRandom implements Runnable {
    private int min;
    private int max;

    private Thread thread;

    public SimpleRangeRandom(int min, int max){
        this.min = min;
        this.max = max;
        thread = new Thread(this);
        thread.start();
    }

    @Override
 private void WriteObject(ObjectOutputStreamout) throws IO Exception;
    private void ReadObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
    public void run() {
        while(true) {
            Random rand = new Random();
            System.out.println("Thread: " + thread.getId() + " Random:" + rand.nextInt(max - min));
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    }

Ahora si queremos que esta clase sea serializable habrá algunos problemas. El subproceso es una de las ciertas clases de nivel de sistema que no son serializables. Así que tenemos que declarar el hilo como transitorio . Al hacer esto podremos serializar los objetos de esta clase pero aún tendremos un problema. Como puede ver en el constructor, establecemos los valores mínimo y máximo de nuestro aleatorizador y, a continuación, iniciamos el hilo que es responsable de generar e imprimir el valor aleatorio. Por lo tanto, al restaurar el objeto persistente llamando a readObject (), el constructor no se ejecutará nuevamente ya que no hay creación de un nuevo objeto. En ese caso, necesitamos desarrollar una serialización personalizada proporcionando dos métodos dentro de la clase. Esos métodos son:

private void writeObject(ObjectOutputStream out) throws IOException;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;

Por lo tanto, al agregar nuestra implementación en el readObject () podemos iniciar y comenzar nuestro hilo:

class RangeRandom implements Serializable, Runnable {

private int min;
private int max;

private transient Thread thread;
//transient should be any field that either cannot be serialized e.g Thread or any field you do not want serialized

public RangeRandom(int min, int max){
    this.min = min;
    this.max = max;
    thread = new Thread(this);
    thread.start();
}

@Override
public void run() {
    while(true) {
        Random rand = new Random();
        System.out.println("Thread: " + thread.getId() + " Random:" + rand.nextInt(max - min));
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

private void writeObject(ObjectOutputStream oos) throws IOException {
    oos.defaultWriteObject();
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    thread = new Thread(this);
    thread.start();
}
}

Aquí está el principal para nuestro ejemplo:

public class Main {
public static void main(String[] args) {
    System.out.println("Hello");
    RangeRandom rangeRandom = new RangeRandom(1,10);

    FileOutputStream fos = null;
    ObjectOutputStream out = null;
    try
    {
        fos = new FileOutputStream("test");
        out = new ObjectOutputStream(fos);
        out.writeObject(rangeRandom);
        out.close();
    }
    catch(IOException ex)
    {
        ex.printStackTrace();
    }


    RangeRandom rangeRandom2 = null;
       FileInputStream fis = null;
       ObjectInputStream in = null;
       try
       {
             fis = new FileInputStream("test");
             in = new ObjectInputStream(fis);
           rangeRandom2 = (RangeRandom)in.readObject();
             in.close();
           }
       catch(IOException ex)
       {
             ex.printStackTrace();
           }
       catch(ClassNotFoundException ex)
       {
             ex.printStackTrace();
           }

}
}

Si ejecuta main, verá que hay dos subprocesos en ejecución para cada instancia de RangeRandom y eso se debe a que el método Thread.start () ahora está en el constructor y en el readObject () .

Versiones y serialVersionUID

Cuando implementa la interfaz java.io.Serializable para hacer que una clase sea serializable, el compilador busca un campo static final llamado serialVersionUID de tipo long . Si la clase no tiene este campo declarado explícitamente, el compilador creará uno de estos campos y le asignará un valor que sale de un cálculo dependiente de la implementación de serialVersionUID . Este cálculo depende de varios aspectos de la clase y sigue las Especificaciones de serialización de objetos proporcionadas por Sun. Pero, no se garantiza que el valor sea el mismo en todas las implementaciones del compilador.

Este valor se usa para verificar la compatibilidad de las clases con respecto a la serialización y esto se hace al deserializar un objeto guardado. El Tiempo de ejecución de serialización verifica que serialVersionUID lea de los datos sin serializar y que serialVersionUID declarado en la clase sea exactamente el mismo. Si ese no es el caso, lanza una InvalidClassException .

Es altamente recomendable que declare e inicialice explícitamente el campo estático, final de tipo long y que se llame 'serialVersionUID' en todas sus clases que desee que sean serializables en lugar de confiar en el cálculo predeterminado del valor para este campo, incluso si no va a hacerlo. usar versionamiento El cálculo de 'serialVersionUID' es extremadamente sensible y puede variar de una implementación de compilador a otra y, por lo tanto, puede obtener la InvalidClassException incluso para la misma clase solo porque usó diferentes implementaciones de compilador en el remitente y el receptor finaliza el proceso de serialización.

public class Example implements Serializable {          
    static final long serialVersionUID = 1L /*or some other value*/;
    //...
}

Mientras que serialVersionUID sea ​​el mismo, la serialización de Java puede manejar diferentes versiones de una clase. Cambios compatibles e incompatibles son;

Cambios compatibles

  • Adición de campos: cuando la clase que se reconstituye tiene un campo que no aparece en el flujo, ese campo en el objeto se inicializará con el valor predeterminado para su tipo. Si se necesita una inicialización específica de la clase, la clase puede proporcionar un método readObject que puede inicializar el campo a valores no predeterminados.
  • Agregar clases: la secuencia contendrá la jerarquía de tipos de cada objeto en la secuencia. La comparación de esta jerarquía en la secuencia con la clase actual puede detectar clases adicionales. Como no hay información en la ruta desde la cual inicializar el objeto, los campos de la clase se inicializarán a los valores predeterminados.
  • Eliminación de clases: la comparación de la jerarquía de clases en la ruta con la de la clase actual puede detectar que una clase se ha eliminado. En este caso, los campos y objetos correspondientes a esa clase se leen de la secuencia. Los campos primitivos se descartan, pero los objetos a los que hace referencia la clase eliminada se crean, ya que se pueden consultar más adelante en la secuencia. Se recolectarán cuando el flujo se recolecta o se restablece.
  • Agregar métodos writeObject / readObject: si la versión que lee la secuencia tiene estos métodos, se espera que readObject, como de costumbre, lea los datos necesarios escritos en la secuencia por la serialización predeterminada. Debe llamar a defaultReadObject primero antes de leer cualquier dato opcional. Se espera que el método writeObject llame a defaultWriteObject para escribir los datos requeridos y luego puede escribir datos opcionales.
  • Agregar java.io.Serializable: Esto es equivalente a agregar tipos. No habrá valores en el flujo para esta clase, por lo que sus campos se inicializarán a los valores predeterminados. El soporte para la subclasificación de clases no serializables requiere que el supertipo de la clase tenga un constructor sin argumentos y la clase en sí misma se inicializará con los valores predeterminados. Si el constructor no-arg no está disponible, se lanza la excepción InvalidClassException.
  • Cambio del acceso a un campo: los modificadores de acceso público, paquete, protegido y privado no afectan la capacidad de serialización para asignar valores a los campos.
  • Cambiar un campo de estático a no estático o transitorio a no transitorio: cuando se confía en la serialización predeterminada para calcular los campos serializables, este cambio es equivalente a agregar un campo a la clase. El nuevo campo se escribirá en la secuencia, pero las clases anteriores ignorarán el valor, ya que la serialización no asignará valores a campos estáticos o transitorios.

Cambios incompatibles

  • Eliminar campos: si se elimina un campo en una clase, la secuencia escrita no contendrá su valor. Cuando una clase anterior lee el flujo, el valor del campo se establecerá en el valor predeterminado porque no hay ningún valor disponible en el flujo. Sin embargo, este valor predeterminado puede perjudicar la capacidad de la versión anterior para cumplir su contrato.
  • Mover clases hacia arriba o hacia abajo en la jerarquía: esto no se puede permitir ya que los datos en la secuencia aparecen en la secuencia incorrecta.
  • Cambiar un campo no estático a estático o un campo no transitorio a transitorio: cuando se confía en la serialización predeterminada, este cambio es equivalente a eliminar un campo de la clase. Esta versión de la clase no escribirá esos datos en el flujo, por lo que no estará disponible para ser leída por versiones anteriores de la clase. Al igual que al eliminar un campo, el campo de la versión anterior se inicializará con el valor predeterminado, lo que puede hacer que la clase falle de manera inesperada.
  • Cambio del tipo declarado de un campo primitivo: Cada versión de la clase escribe los datos con su tipo declarado. Las versiones anteriores de la clase que intenten leer el campo fallarán porque el tipo de datos en la secuencia no coincide con el tipo del campo.
  • Cambiar el método writeObject o readObject para que ya no escriba o lea los datos de campo predeterminados o cambiarlo para que intente escribirlo o leerlo cuando la versión anterior no lo hizo. Los datos de campo predeterminados deben aparecer constantemente o no aparecer en la transmisión.
  • Cambiar una clase de Serializable a Externalizable o viceversa es un cambio incompatible, ya que la transmisión contendrá datos que son incompatibles con la implementación de la clase disponible.
  • Cambiar una clase de un tipo sin enumeración a un tipo de enumeración o viceversa, ya que la secuencia contendrá datos que son incompatibles con la implementación de la clase disponible.
  • La eliminación de Serializable o Externalizable es un cambio incompatible, ya que cuando se escribe ya no proporcionará los campos necesarios para las versiones anteriores de la clase.
  • Agregar el método writeReplace o readResolve a una clase es incompatible si el comportamiento produce un objeto que es incompatible con cualquier versión anterior de la clase.

Deserialización JSON personalizada con Jackson

Consumimos la API de descanso como un formato JSON y luego la descomprimimos en un POJO. El org.codehaus.jackson.map.ObjectMapper de Jackson "simplemente funciona" fuera de la caja y realmente no hacemos nada en la mayoría de los casos. Pero a veces necesitamos deserializador personalizado para satisfacer nuestras necesidades personalizadas y este tutorial lo guiará a través del proceso de creación de su propio deserializador personalizado.

Digamos que tenemos las siguientes entidades.

public class User {
    private Long id;
    private String name;
    private String email;

    //getter setter are omitted for clarity 
}

Y

public class Program {
    private Long id;
    private String name;
    private User createdBy;
    private String contents;

    //getter setter are omitted for clarity
}

Primero serialicemos / ordenemos un objeto.

User user = new User();
user.setId(1L);
user.setEmail("[email protected]");
user.setName("Bazlur Rahman");

Program program = new Program();
program.setId(1L);
program.setName("Program @# 1");
program.setCreatedBy(user);
program.setContents("Some contents");

ObjectMapper objectMapper = new ObjectMapper();

String final json = objectMapper.writeValueAsString (programa); System.out.println (json);

El código anterior producirá siguiente JSON-

{
    "id": 1,
    "name": "Program @# 1",
    "createdBy": {
        "id": 1,
        "name": "Bazlur Rahman",
        "email": "[email protected]"
    },
    "contents": "Some contents"
}

Ahora puede hacer lo contrario muy fácilmente. Si tenemos este JSON, podemos unmarshal a un objeto de programa usando ObjectMapper de la siguiente manera:

Ahora, digamos, este no es el caso real, vamos a tener un JSON diferente de una API que no coincide con nuestra clase de Program .

{
"id": 1,
"name": "Program @# 1",
"ownerId": 1
"contents": "Some contents"
}

Mire la cadena JSON, como puede ver, tiene un campo diferente que es owenerId.

Ahora, si desea serializar este JSON como hicimos anteriormente, tendrá excepciones.

Hay dos formas de evitar excepciones y tener esto en serie:

Ignora los campos desconocidos.

Ignora el onwerId . Agregue la siguiente anotación en la clase Programa

@JsonIgnoreProperties(ignoreUnknown = true)
public class Program {}

Escribe deserializador personalizado

Pero hay casos en los que realmente necesita este campo owerId . Digamos que desea relacionarlo como un ID de la clase User .

En tal caso, necesita escribir un deserializador personalizado

Como puede ver, primero debe acceder a JsonNode desde JonsParser . Y luego puede extraer fácilmente información de un JsonNode usando el método get() . y tienes que asegurarte del nombre del campo. Debe ser el nombre exacto, error de ortografía causará excepciones.

Y, finalmente, debe registrar su ProgramDeserializer en el ObjectMapper .

ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Program.class, new ProgramDeserializer());
 
mapper.registerModule(module);
 
String newJsonString = "{\"id\":1,\"name\":\"Program @# 1\",\"ownerId\":1,\"contents\":\"Some contents\"}";
final Program program2 = mapper.readValue(newJsonString, Program.class);

Alternativamente, puede usar la anotación para registrar el deserializador directamente -

@JsonDeserialize(using = ProgramDeserializer.class)
public class Program {
}


Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow