JavaRush /Blogue Java /Random-PT /Serialização como ela é. Parte 1
articles
Nível 15

Serialização como ela é. Parte 1

Publicado no grupo Random-PT
À primeira vista, a serialização parece um processo trivial. Realmente, o que poderia ser mais simples? Declarou a classe para implementar a interface java.io.Serializable- e é isso. Você pode serializar a classe sem problemas. Serialização como ela é.  Parte 1 - 1Teoricamente, isso é verdade. Na prática, existem muitas sutilezas. Eles estão relacionados ao desempenho, à desserialização, à segurança da classe. E com muitos mais aspectos. Essas sutilezas serão discutidas. Este artigo pode ser dividido nas seguintes partes:
  • Sutilezas de mecanismos
  • Por que é necessário?Externalizable
  • Desempenho
  • mas por outro lado
  • Segurança de dados
  • Serialização de objetosSingleton
Vamos para a primeira parte -

Sutilezas de mecanismos

Em primeiro lugar, uma pergunta rápida. Quantas maneiras existem de tornar um objeto serializável? A prática mostra que mais de 90% dos desenvolvedores respondem a essa pergunta aproximadamente da mesma maneira (até o texto) - só existe uma maneira. Enquanto isso, existem dois deles. Nem todo mundo se lembra do segundo, muito menos diz algo inteligível sobre suas características. Então, quais são esses métodos? Todo mundo se lembra do primeiro. Esta é a implementação já mencionada java.io.Serializablee não requer nenhum esforço. O segundo método também é a implementação de uma interface, mas diferente: java.io.Externalizable. Ao contrário java.io.Serializable, ele contém dois métodos que precisam ser implementados - writeExternal(ObjectOutput)e readExternal(ObjectInput). Esses métodos contêm a lógica de serialização/desserialização. Comente.SerializableA seguir , às vezes me referirei à serialização com implementação como padrão e implementação Externalizablecomo estendida. OutroComente. Eu deliberadamente não toco agora em opções de controle de serialização padrão como definição readObjecte writeObject, porque Acho que esses métodos estão um tanto incorretos. Esses métodos não são definidos na interface Serializablee são, na verdade, acessórios para contornar as limitações e tornar a serialização padrão flexível. ExternalizableMétodos que proporcionam flexibilidade são incorporados a eles desde o início . Vamos fazer mais uma pergunta. Como a serialização padrão realmente funciona usando java.io.Serializable? E funciona por meio da API Reflection. Aqueles. a classe é analisada como um conjunto de campos, cada um deles gravado no fluxo de saída. Acho que está claro que esta operação não é ideal em termos de desempenho. Descobriremos quanto exatamente mais tarde. Há outra grande diferença entre os dois métodos de serialização mencionados. Ou seja, no mecanismo de desserialização. Quando usada, Serializablea desserialização ocorre assim: a memória é alocada para um objeto, após o qual seus campos são preenchidos com valores do fluxo. O construtor do objeto não é chamado. Aqui precisamos considerar esta situação separadamente. Ok, nossa classe é serializável. E seu pai? Completamente opcional! Além disso, se você herdar uma classe de Object- o pai definitivamente NÃO é serializável. E mesmo que Objectnão saibamos nada sobre campos, eles podem existir em nossas próprias classes pai. Oque vai acontecer com eles? Eles não entrarão no fluxo de serialização. Que valores eles assumirão na desserialização? Vejamos este exemplo:
package ru.skipy.tests.io;

import java.io.*;

/**
 * ParentDeserializationTest
 *
 * @author Eugene Matyushkin aka Skipy
 * @since 05.08.2010
 */
public class ParentDeserializationTest {

    public static void main(String[] args){
        try {
            System.out.println("Creating...");
            Child c = new Child(1);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            c.field = 10;
            System.out.println("Serializing...");
            oos.writeObject(c);
            oos.flush();
            baos.flush();
            oos.close();
            baos.close();
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            System.out.println("Deserializing...");
            Child c1 = (Child)ois.readObject();
            System.out.println("c1.i="+c1.getI());
            System.out.println("c1.field="+c1.getField());
        } catch (IOException ex){
            ex.printStackTrace();
        } catch (ClassNotFoundException ex){
            ex.printStackTrace();
        }
    }

    public static class Parent {
        protected int field;
        protected Parent(){
            field = 5;
            System.out.println("Parent::Constructor");
        }
        public int getField() {
            return field;
        }
    }

    public static class Child extends Parent implements Serializable{
        protected int i;
        public Child(int i){
            this.i = i;
            System.out.println("Child::Constructor");
        }
        public int getI() {
            return i;
        }
    }
}
É transparente - temos uma classe pai não serializável e uma classe filha serializável. E é isso que acontece:
Creating...
Parent::Constructor
Child::Constructor
Serializing...
Deserializing...
Parent::Constructor
c1.i=1
c1.field=5
Ou seja, durante a desserialização, o construtor sem parâmetros da classe pai NÃO serializável é chamado . E se não existir tal construtor, ocorrerá um erro durante a desserialização. O construtor do objeto filho, aquele que estamos desserializando, não é chamado, como foi dito acima. É assim que os mecanismos padrão se comportam quando usados Serializable. Ao usá-lo, Externalizablea situação é diferente. Primeiro, o construtor sem parâmetros é chamado e, em seguida, o método readExternal é chamado no objeto criado, que na verdade lê todos os seus dados. Portanto, qualquer classe que implemente a interface Externalizável deve ter um construtor público sem parâmetros! Além disso, como todos os descendentes de tal classe também serão considerados para implementar a interface Externalizable, eles também devem ter um construtor sem parâmetros! Vamos mais longe. Existe um modificador de campo como transient. Isso significa que este campo não deve ser serializado. No entanto, como você mesmo entende, esta instrução afeta apenas o mecanismo de serialização padrão. Quando usado, Externalizableninguém se preocupa em serializar esse campo, bem como em subtraí-lo. Se um campo for declarado transitório, quando o objeto for desserializado, ele assumirá o valor padrão. Outro ponto bastante sutil. Com a serialização padrão, os campos que possuem o modificador staticnão são serializados. Assim, após a desserialização, este campo não altera seu valor. Claro, durante a implementação Externalizableninguém se preocupa em serializar e desserializar este campo, mas eu recomendo fortemente não fazer isso, porque isso pode levar a erros sutis. Os campos com um modificador finalsão serializados como os normais. Com uma exceção – eles não podem ser desserializados ao usar Externalizable. Porque final-поляeles devem ser inicializados no construtor, e depois disso será impossível alterar o valor deste campo em readExternal. Da mesma forma, se você precisar serializar um objeto que possui finalum campo -, você só terá que usar a serialização padrão. Outro ponto que muita gente não sabe. A serialização padrão leva em consideração a ordem em que os campos são declarados em uma classe. De qualquer forma, isso acontecia nas versões anteriores; na JVM versão 1.6 da implementação Oracle, a ordem não é mais importante, o tipo e o nome do campo são importantes. É muito provável que a composição dos métodos afete o mecanismo padrão, apesar do fato de que os campos geralmente permanecem os mesmos. Para evitar isso, existe o seguinte mecanismo. Para cada classe que implementa a interface Serializable, mais um campo é adicionado na fase de compilação -private static final long serialVersionUID. Este campo contém o identificador exclusivo da versão da classe serializada. É calculado com base no conteúdo da classe - campos, sua ordem de declaração, métodos, sua ordem de declaração. Assim, com qualquer alteração na classe, este campo mudará de valor. Este campo é gravado no fluxo quando a classe é serializada. A propósito, este é talvez o único caso que conheço quando staticum -field é serializado. Durante a desserialização, o valor deste campo é comparado com o da classe na máquina virtual. Se os valores não corresponderem, uma exceção como esta será lançada:
java.io.InvalidClassException: test.ser2.ChildExt;
    local class incompatible: stream classdesc serialVersionUID = 8218484765288926197,
                                   local class serialVersionUID = 1465687698753363969
Existe, no entanto, uma maneira de, se não ignorar, enganar essa verificação. Isto pode ser útil se o conjunto de campos da classe e sua ordem já estiverem definidos, mas os métodos da classe puderem mudar. Nesse caso, a serialização não corre risco, mas o mecanismo padrão não permitirá que os dados sejam desserializados usando o bytecode da classe modificada. Mas, como eu disse, ele pode ser enganado. Ou seja, defina manualmente o campo na classe private static final long serialVersionUID. Em princípio, o valor deste campo pode ser absolutamente qualquer. Algumas pessoas preferem defini-lo como igual à data em que o código foi modificado. Alguns até usam 1L. Para obter o valor padrão (aquele calculado internamente), você pode usar o utilitário serialver incluído no SDK. Uma vez definido desta forma, o valor do campo será fixo, portanto a desserialização sempre será permitida. Além disso, na versão 5.0, aproximadamente o seguinte apareceu na documentação: é altamente recomendado que todas as classes serializáveis ​​declarem este campo explicitamente, porque o cálculo padrão é muito sensível aos detalhes da estrutura da classe, que pode variar dependendo da implementação do compilador, e assim causar InvalidClassExceptionconsequências inesperadas. É melhor declarar este campo como private, porque refere-se apenas à classe em que é declarado. Embora o modificador não esteja especificado na especificação. Consideremos agora este aspecto. Digamos que temos esta estrutura de classes:
public class A{
    public int iPublic;
    protected int iProtected;
    int iPackage;
    private int iPrivate;
}

public class B extends A implements Serializable{}
Em outras palavras, temos uma classe herdada de um pai não serializável. É possível serializar esta classe e o que é necessário para isso? O que acontecerá com as variáveis ​​da classe pai? A resposta é esta. Sim, Bvocê pode serializar uma instância de uma classe. O que é necessário para isso? Mas a classe precisa Ater um construtor sem parâmetros, publicou protected. Então, durante a desserialização, todas as variáveis ​​de classe Aserão inicializadas usando este construtor. As variáveis ​​de classe Bserão inicializadas com os valores do fluxo de dados serializado. Teoricamente é possível definir em uma classe Bos métodos que falei no início - readObjecte writeObject, - no início dos quais realizar a (des)serialização das variáveis ​​da classe Batravés de in.defaultReadObject/out.defaultWriteObject, e depois a (des)serialização das variáveis ​​disponíveis da classe A(no nosso caso são iPublic, iProtectede iPackage, se Bestiver no mesmo pacote que A). No entanto, na minha opinião, é melhor usar a serialização estendida para isso. O próximo ponto que gostaria de abordar é a serialização de vários objetos. Digamos que temos a seguinte estrutura de classes:
public class A implements Serializable{
    private C c;
    private B b;
    public void setC(C c) {this.c = c;}
    public void setB(B b) {this.b = b;}
    public C getC() {return c;}
    public B getB() {return b;}
}
public class B implements Serializable{
    private C c;
    public void setC(C c) {this.c = c;}
    public C getC() {return c;}
}
public class C implements Serializable{
    private A a;
    private B b;
    public void setA(A a) {this.a = a;}
    public void setB(B b) {this.b = b;}
    public B getB() {return b;}
    public A getA() {return a;}
}
Serialização como ela é.  Parte 1 - 2O que acontece se você serializar uma instância da classe A? Ele arrastará uma instância da classe B, que, por sua vez, arrastará uma instância Cque tenha uma referência à instância A, a mesma com a qual tudo começou. Círculo vicioso e recursão infinita? Felizmente, não. Vejamos o seguinte código de teste:
// initiaizing
A a = new A();
B b = new B();
C c = new C();
// setting references
a.setB(b);
a.setC(c);
b.setC(c);
c.setA(a);
c.setB(b);
// serializing
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(a);
oos.writeObject(b);
oos.writeObject(c);
oos.flush();
oos.close();
// deserializing
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
A a1 = (A)ois.readObject();
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
// testing
System.out.println("a==a1: "+(a==a1));
System.out.println("b==b1: "+(b==b1));
System.out.println("c==c1: "+(c==c1));
System.out.println("a1.getB()==b1: "+(a1.getB()==b1));
System.out.println("a1.getC()==c1: "+(a1.getC()==c1));
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1.getA()==a1: "+(c1.getA()==a1));
System.out.println("c1.getB()==b1: "+(c1.getB()==b1));
O que estamos fazendo? Criamos uma instância das classes Ae B, Cfornecemos links entre elas e, em seguida, serializamos cada uma delas. Em seguida, desserializamos eles novamente e executamos uma série de verificações. O que acontecerá como resultado:
a==a1: false
b==b1: false
c==c1: false
a1.getB()==b1: true
a1.getC()==c1: true
b1.getC()==c1: true
c1.getA()==a1: true
c1.getB()==b1: true
Então, o que você pode aprender com este teste? Primeiro. As referências de objetos após a desserialização são diferentes das referências anteriores. Em outras palavras, durante a serialização/desserialização o objeto foi copiado. Este método às vezes é usado para clonar objetos. A segunda conclusão é mais significativa. Ao serializar/desserializar vários objetos que possuem referências cruzadas, essas referências permanecem válidas após a desserialização. Em outras palavras, se antes da serialização eles apontassem para um objeto, depois da desserialização eles também apontariam para um objeto. Outro pequeno teste para confirmar isso:
B b = new B();
C c = new C();
b.setC(c);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(b);
oos.writeObject(c);
oos.writeObject(c);
oos.writeObject(c);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
C c2 = (C)ois.readObject();
C c3 = (C)ois.readObject();
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1==c2: "+(c1==c2));
System.out.println("c1==c3: "+(c1==c3));
Um objeto de classe Btem uma referência a um objeto de classe C. Quando serializado, bele é serializado junto com uma instância da classe С, após a qual a mesma instância de c é serializada três vezes. O que acontece após a desserialização?
b1.getC()==c1: true
c1==c2: true
c1==c3: true
Como você pode ver, todos os quatro objetos desserializados representam, na verdade, um objeto - as referências a ele são iguais. Exatamente como era antes da serialização. Outro ponto interessante - o que acontecerá se implementarmos simultaneamente Externalizablee Serializable? Como naquela questão – elefante versus baleia – quem derrotará quem? Superará Externalizable. O mecanismo de serialização verifica primeiro a sua presença, e só depois a sua presença.Portanto, Serializablese a classe B, que implementa Serializable, herda da classe A, que implementa Externalizable, os campos da classe B não serão serializados. O último ponto é a herança. Ao herdar de uma classe que implementa Serializable, nenhuma ação adicional precisa ser executada. A serialização também se estenderá à classe filha. Ao herdar de uma classe que implementa Externalizable, você deve substituir os métodos readExternal e writeExternal da classe pai. Caso contrário, os campos da classe filha não serão serializados. Nesse caso, lembre-se de chamar os métodos pai, caso contrário, os campos pai não serão serializados. * * * Provavelmente já terminamos com os detalhes. No entanto, há uma questão que não abordámos, de natureza global. Ou seja -

Por que você precisa de externalizável?

Por que precisamos de serialização avançada? A resposta é simples. Em primeiro lugar, dá muito mais flexibilidade. Em segundo lugar, muitas vezes pode proporcionar ganhos significativos em termos de volume de dados serializados. Em terceiro lugar, existe um aspecto como o desempenho, sobre o qual falaremos a seguir . Tudo parece ficar claro com flexibilidade. Na verdade, podemos controlar os processos de serialização e desserialização como quisermos, o que nos torna independentes de quaisquer alterações na classe (como disse acima, alterações na classe podem afetar muito a desserialização). Portanto, quero dizer algumas palavras sobre o ganho de volume. Digamos que temos a seguinte classe:
public class DateAndTime{

  private short year;
  private byte month;
  private byte day;
  private byte hours;
  private byte minutes;
  private byte seconds;

}
O resto não é importante. Os campos poderiam ser do tipo int, mas isso apenas aumentaria o efeito do exemplo. Embora na realidade os campos possam ser digitados intpor motivos de desempenho. De qualquer forma, a questão é clara. A classe representa uma data e hora. É interessante para nós principalmente do ponto de vista da serialização. Talvez a coisa mais fácil a fazer seja armazenar um carimbo de data/hora simples. É do tipo longo, ou seja, quando serializado, levaria 8 bytes. Além disso, esta abordagem requer métodos para converter componentes em um valor e vice-versa, ou seja, – perda de produtividade. A vantagem dessa abordagem é uma data completamente maluca que cabe em 64 bits. Esta é uma enorme margem de segurança, muitas vezes desnecessária na realidade. A classe fornecida acima terá 2 + 5*1 = 7 bytes. Mais despesas gerais para a classe e 6 campos. Existe alguma maneira de compactar esses dados? Claro que sim. Segundos e minutos estão no intervalo de 0 a 59, ou seja, para representá-los, bastam 6 bits em vez de 8. Horas – 0-23 (5 bits), dias – 0-30 (5 bits), meses – 0-11 (4 bits). Total, tudo sem levar em conta o ano - 26 bits. Ainda restam 6 bits para o tamanho de int. Teoricamente, em alguns casos, isso pode ser suficiente para um ano. Caso contrário, adicionar outro byte aumenta o tamanho do campo de dados para 14 bits, o que dá um intervalo de 0 a 16383. Isso é mais que suficiente em aplicações reais. No total, reduzimos o tamanho dos dados necessários para armazenar as informações necessárias para 5 bytes. Se não for até 4. A desvantagem é a mesma do caso anterior - se você armazenar a data embalada, serão necessários métodos de conversão. Mas quero fazer desta forma: armazená-lo em campos separados e serializá-lo em formato empacotado. É aqui que faz sentido usar Externalizable:
// data is packed into 5 bytes:
//  3         2         1
// 10987654321098765432109876543210
// hhhhhmmmmmmssssssdddddMMMMyyyyyy yyyyyyyy
public void writeExternal(ObjectOutput out){
    int packed = 0;
    packed += ((int)hours) << 27;
    packed += ((int)minutes) << 21;
    packed += ((int)seconds) << 15;
    packed += ((int)day) << 10;
    packed += ((int)month) << 6;
    packed += (((int)year) >> 8) & 0x3F;
    out.writeInt(packed);
    out.writeByte((byte)year);
}

public void readExternal(ObjectInput in){
    int packed = in.readInt();
    year = in.readByte() & 0xFF;
    year += (packed & 0x3F) << 8;
    month = (packed >> 6) & 0x0F;
    day = (packed >> 10) & 0x1F;
    seconds = (packed >> 15) & 0x3F;
    minutes = (packed >> 21) & 0x3F;
    hours = (packed >> 27);
}
Na verdade, isso é tudo. Após a serialização, obtemos sobrecarga por classe, dois campos (em vez de 6) e 5 bytes de dados. O que já é significativamente melhor. Outras embalagens podem ser deixadas para bibliotecas especializadas. O exemplo dado é muito simples. Seu principal objetivo é mostrar como a serialização avançada pode ser usada. Embora o possível ganho no volume de dados serializados esteja longe de ser a principal vantagem, na minha opinião. A principal vantagem, além da flexibilidade... (passe suavemente para a próxima seção...) Link para a fonte: Serialização como ela é
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION