JavaRush /Java Blog /Random-KO /직렬화는 그대로입니다. 1 부
articles
레벨 15

직렬화는 그대로입니다. 1 부

Random-KO 그룹에 게시되었습니다
언뜻 보기에 직렬화는 사소한 프로세스처럼 보입니다. 사실, 이보다 더 간단한 게 어디 있겠습니까? 인터페이스를 구현하기 위해 클래스를 선언했습니다 java.io.Serializable. 그게 전부입니다. 문제없이 클래스를 직렬화할 수 있습니다. 직렬화는 그대로입니다.  파트 1 - 1이론적으로는 이것이 사실이다. 실제로는 많은 미묘함이 있습니다. 이는 성능, 역직렬화, 클래스 안전성과 관련이 있습니다. 그리고 더 많은 측면이 있습니다. 그러한 미묘함이 논의될 것입니다. 이 기사는 다음 부분으로 나눌 수 있습니다.
  • 메커니즘의 미묘함
  • 왜 필요한가요?Externalizable
  • 성능
  • 그러나 다른 일면에서는
  • 데이터 보안
  • 객체 직렬화Singleton
첫 번째 부분으로 넘어 갑시다 -

메커니즘의 미묘함

우선, 간단한 질문입니다. 객체를 직렬화할 수 있는 방법은 몇 가지입니까? 실습에 따르면 개발자 중 90% 이상이 이 질문에 대해 거의 동일한 방식(어휘까지)으로 답합니다. 방법은 단 하나뿐입니다. 그 사이에 두 개가 있습니다. 모든 사람이 두 번째 것을 기억하는 것은 아니며 그 기능에 대해 이해할 수 있는 말은 말할 것도 없습니다. 그렇다면 이러한 방법은 무엇입니까? 모두가 첫 번째를 기억합니다. 이는 이미 언급한 구현 java.io.Serializable이므로 어떠한 노력도 필요하지 않습니다. 두 번째 방법도 인터페이스를 구현하는 것이지만 다른 방법은 입니다 java.io.Externalizable. 와 달리 java.io.Serializable구현해야 하는 두 가지 메서드, 즉 writeExternal(ObjectOutput)와 가 포함되어 있습니다 readExternal(ObjectInput). 이러한 메서드에는 직렬화/역직렬화 논리가 포함되어 있습니다. 논평.다음에서는 구현을 표준으로, 구현을 확장으로 직렬화 Serializable하는 경우가 종종 있습니다 . Externalizable또 다른논평. readObject나는 의도적으로 및 정의와 같은 표준 직렬화 제어 옵션을 다루지 않습니다 writeObject. 나는 이러한 방법이 다소 올바르지 않다고 생각합니다. 이러한 메서드는 인터페이스에 정의되어 있지 않으며 Serializable실제로 제한 사항을 해결하고 표준 직렬화를 유연하게 만들기 위한 소품입니다. Externalizable유연성을 제공하는 방법은 처음부터 내장되어 있습니다 . 질문 하나 더 해보자. 표준 직렬화는 실제로 어떻게 작동합니까 java.io.Serializable? 그리고 Reflection API를 통해 작동합니다. 저것들. 클래스는 필드 집합으로 구문 분석되며 각 필드는 출력 스트림에 기록됩니다. 이 작업은 성능면에서 최적이 아니라는 것은 분명하다고 생각합니다. 정확히 얼마인지는 나중에 알아보겠습니다. 언급된 두 가지 직렬화 방법에는 또 다른 주요 차이점이 있습니다. 즉, 역직렬화 메커니즘에서입니다. 사용하면 Serializable다음과 같이 역직렬화가 발생합니다. 객체에 메모리가 할당된 후 해당 필드가 스트림의 값으로 채워집니다. 개체의 생성자가 호출되지 않습니다. 여기서는 이 상황을 별도로 고려해야 합니다. 좋습니다. 우리 클래스는 직렬화 가능합니다. 그리고 그의 부모는? 완전히 선택 사항입니다! 또한 클래스를 상속받는 경우 Object부모는 확실히 직렬화할 수 없습니다. Object그리고 우리가 필드에 대해 아무것도 모르 더라도 필드는 우리 부모 클래스에 존재할 수도 있습니다. 그들에게는 무슨 일이 일어날까요? 직렬화 스트림에 들어가지 않습니다. 역직렬화 시 어떤 값을 갖게 되나요? 이 예를 살펴보겠습니다.
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;
        }
    }
}
이는 투명합니다. 직렬화할 수 없는 상위 클래스와 직렬화할 수 있는 하위 클래스가 있습니다. 그리고 이런 일이 일어납니다:
Creating...
Parent::Constructor
Child::Constructor
Serializing...
Deserializing...
Parent::Constructor
c1.i=1
c1.field=5
즉, 역직렬화 중에 직렬화 가능하지 않은 상위 ​​클래스의 매개변수가 없는 생성자가 호출됩니다 . 그리고 해당 생성자가 없으면 deserialization 중에 오류가 발생합니다. 위에서 설명한 것처럼 역직렬화 중인 자식 개체의 생성자는 호출되지 않습니다. 이것이 표준 메커니즘이 사용될 때 작동하는 방식입니다 Serializable. 그것을 사용하면 Externalizable상황이 다릅니다. 먼저 매개 변수가 없는 생성자를 호출한 다음 생성된 객체에 대해 readExternal 메서드를 호출하여 실제로 모든 데이터를 읽습니다. 따라서 외부화 가능 인터페이스를 구현하는 모든 클래스에는 매개변수가 없는 공용 생성자가 있어야 합니다! 더욱이, 그러한 클래스의 모든 자손도 인터페이스를 구현하는 것으로 간주되므로 Externalizable매개변수가 없는 생성자도 있어야 합니다! 더 나아가자. 와 같은 필드 수정자가 있습니다 transient. 이는 이 필드가 직렬화되어서는 안 된다는 것을 의미합니다. 그러나 여러분도 이해하고 있듯이 이 명령은 표준 직렬화 메커니즘에만 영향을 미칩니다. 이 필드 를 사용하면 Externalizable아무도 이 필드를 직렬화하고 빼는 일을 하지 않습니다. 필드가 임시로 선언되면 개체가 역직렬화될 때 기본값을 사용합니다. 또 다른 미묘한 점. 표준 직렬화를 사용하면 수정자가 있는 필드는 static직렬화되지 않습니다. 따라서 역직렬화 후에도 이 필드는 값을 변경하지 않습니다. 물론 구현 중에는 Externalizable누구도 이 필드를 직렬화 및 역직렬화하려고 하지 않지만, 이렇게 하지 않는 것이 좋습니다. 이로 인해 미묘한 오류가 발생할 수 있습니다. 수정자가 있는 필드는 final일반 필드처럼 직렬화됩니다. 한 가지 예외를 제외하면 외부화 가능을 사용할 때 역직렬화할 수 없습니다. 생성자에서 초기화해야 하기 때문에 final-поля그 후에는 readExternal에서 이 필드의 값을 변경할 수 없습니다. 따라서 -필드가 있는 개체를 직렬화해야 하는 경우 final표준 직렬화만 사용해야 합니다. 많은 사람들이 모르는 또 하나의 포인트. 표준 직렬화는 클래스에서 필드가 선언되는 순서를 고려합니다. 어떤 경우든 이는 이전 버전의 경우였으며 Oracle 구현의 JVM 버전 1.6에서는 순서가 더 이상 중요하지 않으며 필드의 유형과 이름이 중요합니다. 필드가 일반적으로 동일하게 유지될 수 있다는 사실에도 불구하고 메서드의 구성은 표준 메커니즘에 영향을 미칠 가능성이 매우 높습니다. 이를 방지하기 위해 다음과 같은 메커니즘이 있습니다. 인터페이스를 구현하는 각 클래스에는 Serializable컴파일 단계에서 필드가 하나 더 추가됩니다.private static final long serialVersionUID. 이 필드에는 직렬화된 클래스의 고유 버전 식별자가 포함됩니다. 클래스의 내용(필드, 선언 순서, 메소드, 선언 순서)을 기준으로 계산됩니다. 따라서 클래스가 변경되면 이 필드의 값이 변경됩니다. 이 필드는 클래스가 직렬화될 때 스트림에 기록됩니다. 그건 그렇고, 이것은 아마도 static-필드가 직렬화될 때 나에게 알려진 유일한 경우일 것입니다. 역직렬화 중에 이 필드의 값은 가상 머신의 클래스 값과 비교됩니다. 값이 일치하지 않으면 다음과 같은 예외가 발생합니다.
java.io.InvalidClassException: test.ser2.ChildExt;
    local class incompatible: stream classdesc serialVersionUID = 8218484765288926197,
                                   local class serialVersionUID = 1465687698753363969
그러나 우회하지 않더라도 이 수표를 속이는 방법이 있습니다. 이는 클래스 필드 집합과 해당 순서가 이미 정의되어 있는 경우 유용할 수 있지만 클래스 메서드는 변경될 수 있습니다. 이 경우 직렬화는 위험하지 않지만 표준 메커니즘에서는 수정된 클래스의 바이트코드를 사용하여 데이터를 역직렬화하는 것을 허용하지 않습니다. 그러나 내가 말했듯이 그는 속을 수 있습니다. 즉, 클래스의 필드를 수동으로 정의하십시오 private static final long serialVersionUID. 원칙적으로 이 필드의 값은 무엇이든 될 수 있습니다. 어떤 사람들은 코드가 수정된 날짜와 동일하게 설정하는 것을 선호합니다. 1L를 사용하는 경우도 있습니다. 표준값(내부적으로 계산된 값)을 얻으려면 SDK에 포함된 serialver 유틸리티를 사용할 수 있습니다. 이러한 방식으로 정의되면 필드 값이 고정되므로 역직렬화가 항상 허용됩니다. 또한 버전 5.0에서는 대략 다음과 같은 내용이 문서에 나타났습니다. 기본 계산은 컴파일러 구현에 따라 달라질 수 있는 클래스 구조의 세부 사항에 매우 민감하기 때문에 모든 직렬화 가능 클래스가 이 필드를 명시적으로 선언하는 것이 좋습니다. 따라서 예상치 못한 InvalidClassException결과가 발생합니다. 이 필드를 로 선언하는 것이 더 좋습니다 private. 선언된 클래스만을 참조합니다. 사양에는 수정자가 지정되어 있지 않습니다. 이제 이 측면을 고려해 보겠습니다. 다음과 같은 클래스 구조가 있다고 가정해 보겠습니다.
public class A{
    public int iPublic;
    protected int iProtected;
    int iPackage;
    private int iPrivate;
}

public class B extends A implements Serializable{}
즉, 직렬화할 수 없는 부모로부터 상속받은 클래스가 있습니다. 이 클래스를 직렬화하는 것이 가능합니까? 이를 위해 필요한 것은 무엇입니까? 상위 클래스의 변수는 어떻게 되나요? 대답은 이것입니다. 예, B클래스의 인스턴스를 직렬화할 수 있습니다. 이를 위해 무엇이 필요합니까? 하지만 클래스에는 매개변수 가 A없는 생성자가 있어야 합니다 public. protected그런 다음 역직렬화 중에 모든 클래스 변수가 A이 생성자를 사용하여 초기화됩니다. 클래스 변수는 B직렬화된 데이터 스트림의 값으로 초기화됩니다. B이론적으로는 처음에 얘기했던 메소드들을 클래스에 정의하는 것이 가능하며 readObject, writeObject처음에는 를 B통해 클래스 변수의 (역)직렬화를 수행 in.defaultReadObject/out.defaultWriteObject하고, 사용 가능한 변수의 (역)직렬화를 수행합니다. 클래스에서 A(우리의 경우에는 iPublic, iProtected및 와 동일한 패키지에 있는 iPackage경우 ) 그러나 제 생각에는 확장 직렬화를 사용하는 것이 더 좋습니다. 다음으로 다루고 싶은 점은 다중 객체의 직렬화입니다. 다음과 같은 클래스 구조가 있다고 가정해 보겠습니다. BA
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;}
}
직렬화는 그대로입니다.  파트 1 - 2클래스의 인스턴스를 직렬화하면 어떻게 되나요 A? 이는 클래스의 인스턴스를 따라 드래그하고 , 클래스 는 모든 것이 시작된 것과 동일한 인스턴스에 대한 참조가 있는 B인스턴스를 따라 드래그합니다 . 악순환과 무한재귀? 다행히도 그렇지 않습니다. 다음 테스트 코드를 살펴보겠습니다. CA
// 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));
우리는 무엇을하고 있습니까? 클래스의 인스턴스를 생성 A하고 서로 링크를 제공한 다음 각각을 직렬화합니다 B. C그런 다음 다시 역직렬화하고 일련의 검사를 실행합니다. 결과적으로 어떤 일이 일어날까요?
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
그렇다면 이 테스트를 통해 무엇을 배울 수 있나요? 첫 번째. 역직렬화 후의 개체 참조는 역직렬화 이전의 참조와 다릅니다. 즉, 직렬화/역직렬화 중에 개체가 복사되었습니다. 이 방법은 때때로 객체를 복제하는 데 사용됩니다. 두 번째 결론이 더 중요하다. 상호 참조가 있는 여러 개체를 직렬화/역직렬화하는 경우 해당 참조는 역직렬화 후에도 유효한 상태로 유지됩니다. 즉, 직렬화 전에 하나의 개체를 가리켰다면 역직렬화 후에도 하나의 개체를 가리킵니다. 이를 확인하기 위한 또 다른 작은 테스트:
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));
클래스 객체에는 B클래스 객체에 대한 참조가 있습니다 C. 직렬화되면 b클래스의 인스턴스와 함께 직렬화되고 С, 그 후에는 c의 동일한 인스턴스가 세 번 직렬화됩니다. 역직렬화 후에는 어떻게 되나요?
b1.getC()==c1: true
c1==c2: true
c1==c3: true
보시다시피, 역직렬화된 객체 4개는 모두 실제로 하나의 객체를 나타냅니다. 해당 객체에 대한 참조는 동일합니다. 연재 전과 똑같습니다. Externalizable또 다른 흥미로운 점은 and 를 동시에 구현하면 어떻게 될까요 Serializable? 코끼리 대 고래라는 질문처럼 누가 누구를 이길까요? 극복할 것이다 Externalizable. 직렬화 메커니즘은 먼저 존재 여부를 확인한 다음 존재 여부를 확인하므로 를 Serializable구현하는 클래스 B가 를 Serializable구현하는 클래스 A에서 상속받는 경우 Externalizable클래스 B의 필드는 직렬화되지 않습니다. 마지막 요점은 상속입니다. 를 구현하는 클래스에서 상속하는 경우 Serializable추가 작업을 수행할 필요가 없습니다. 직렬화는 하위 클래스에도 확장됩니다. 를 구현하는 클래스에서 상속하는 경우 Externalizable상위 클래스의 readExternal 및 writeExternal 메서드를 재정의해야 합니다. 그렇지 않으면 하위 클래스의 필드가 직렬화되지 않습니다. 이 경우 상위 메소드를 호출해야 한다는 것을 기억해야 합니다. 그렇지 않으면 상위 필드가 직렬화되지 않습니다. * * * 세부사항은 이제 끝났을 것입니다. 그러나 우리가 다루지 않은 세계적인 문제가 하나 있습니다. 즉 -

왜 외부화 가능이 필요합니까?

왜 고급 직렬화가 필요한가요? 대답은 간단합니다. 첫째, 훨씬 더 많은 유연성을 제공합니다. 둘째, 직렬화된 데이터의 양 측면에서 상당한 이점을 제공할 수 있습니다. 셋째, 성능과 같은 측면이 있는데 이에 대해 아래에서 설명하겠습니다 . 유연성으로 모든 것이 명확 해 보입니다. 실제로 우리는 원하는 대로 직렬화 및 역직렬화 프로세스를 제어할 수 있으므로 클래스의 모든 변경 사항에 독립적이게 됩니다. 위에서 말했듯이 클래스의 변경 사항은 역직렬화에 큰 영향을 미칠 수 있습니다. 따라서 볼륨 증가에 대해 몇 마디 말하고 싶습니다. 다음과 같은 클래스가 있다고 가정해 보겠습니다.
public class DateAndTime{

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

}
나머지는 중요하지 않습니다. 필드는 int 유형으로 구성될 수 있지만 이는 예제의 효과만 향상시킬 뿐입니다. 실제로는 int성능상의 이유로 필드를 입력할 수 있습니다. 어쨌든 요점은 분명합니다. 클래스는 날짜와 시간을 나타냅니다. 이는 주로 직렬화의 관점에서 우리에게 흥미롭습니다. 아마도 가장 쉬운 방법은 간단한 타임스탬프를 저장하는 것일 것입니다. 긴 유형입니다. 직렬화되면 8바이트가 필요합니다. 또한 이 접근 방식에는 구성 요소를 하나의 값으로 변환하고 그 반대로 변환하는 방법이 필요합니다. – 생산성 손실. 이 접근 방식의 장점은 64비트에 들어갈 수 있는 완전히 미친 날짜입니다. 이는 실제로는 거의 필요하지 않은 엄청난 안전 여유입니다. 위에 제공된 클래스는 2 + 5*1 = 7바이트를 차지합니다. 클래스 및 6개 필드에 대한 오버헤드도 추가됩니다. 이 데이터를 압축할 수 있는 방법이 있나요? 물론이죠. 초와 분의 범위는 0~59입니다. 이를 표현하려면 8비트 대신 6비트이면 충분합니다. 시간 – 0-23(5비트), 일 – 0-30(5비트), 월 – 0-11(4비트). 총 연도를 고려하지 않은 모든 것 - 26비트. int 크기에는 아직 6비트가 남아 있습니다. 이론적으로 어떤 경우에는 1년이면 충분할 수도 있습니다. 그렇지 않은 경우 다른 바이트를 추가하면 데이터 필드의 크기가 14비트로 증가하여 범위가 0-16383이 됩니다. 실제 애플리케이션에서는 이 정도면 충분합니다. 필요한 정보를 저장하는 데 필요한 데이터 크기를 전체적으로 5바이트로 줄였습니다. 최대 4개가 아닌 경우 단점은 이전 경우와 동일합니다. 날짜를 압축하여 저장하는 경우 변환 방법이 필요합니다. 하지만 저는 이렇게 하고 싶습니다. 즉, 별도의 필드에 저장하고 패키지 형식으로 직렬화하는 것입니다. 여기에서 다음을 사용하는 것이 좋습니다 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);
}
사실 그게 전부입니다. 직렬화 후에는 클래스당 오버헤드, 2개의 필드(6개 대신) 및 5바이트의 데이터가 발생합니다. 이미 훨씬 나아졌습니다. 추가 패키징은 전문 라이브러리에 맡길 수 있습니다. 주어진 예는 매우 간단합니다. 주요 목적은 고급 직렬화를 사용할 수 있는 방법을 보여주는 것입니다. 직렬화된 데이터의 양을 늘릴 수 있다는 것이 주요 이점과는 거리가 멀다고 생각합니다. 유연성에 더해 주요 장점은... (부드럽게 다음 섹션으로 넘어가세요...) 소스 링크: 직렬화 있는 그대로
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION