CodeGym /Java Blog /무작위의 /Java의 직렬화 및 역직렬화
John Squirrels
레벨 41
San Francisco

Java의 직렬화 및 역직렬화

무작위의 그룹에 게시되었습니다
안녕! 오늘 수업에서는 Java의 직렬화 및 역직렬화에 대해 이야기하겠습니다. 간단한 예부터 시작하겠습니다. 당신이 컴퓨터 게임 개발자라고 상상해보십시오. 90년대에 자랐고 그 시대의 게임 콘솔을 기억한다면 아마도 오늘날 우리가 당연하게 여기는 것, 즉 게임을 저장하고 로드하는 기능이 부족하다는 것을 알고 있을 것입니다. :) 그렇지 않다면 상상해보세요!Java의 직렬화 및 역직렬화 - 1오늘날 이러한 능력이 없는 게임은 망할 것 같아 두렵습니다! 어쨌든 게임을 '저장'하고 '로드'한다는 것은 무엇을 의미합니까? 글쎄, 우리는 일상적인 의미를 이해합니다. 우리는 우리가 중단한 곳에서 게임을 계속하고 싶습니다. 이를 위해 나중에 게임을 로드하는 데 사용할 특정 '체크 포인트'를 만듭니다. 하지만 일반 게이머가 아닌 프로그래머에게 이것이 의미하는 바는 무엇입니까? 대답은 간단합니다. 프로그램의 상태를 저장합니다. 전략 게임에서 스페인을 플레이하고 있다고 가정해 보겠습니다. 게임에는 상태가 있습니다. 모든 사람이 소유한 영토, 모든 사람이 보유한 자원 수, 존재하는 동맹 및 대상, 전쟁 중인 사람 등. 프로그램의 상태인 이 정보는 데이터를 복원하고 게임을 계속하기 위해 어떻게든 저장되어야 합니다. 공교롭게도 Java의 직렬화는 개체의 상태를 일련의 바이트로 저장하는 프로세스입니다. Java의 역직렬화는 이러한 바이트에서 개체를 복원하는 프로세스입니다. 모든 Java 객체는 바이트 시퀀스로 변환될 수 있습니다. 이것이 왜 필요한가요? 프로그램은 그 자체로 존재하지 않는다고 반복해서 말했습니다. 대부분의 경우 서로 상호 작용하고 데이터를 교환하는 등의 작업을 수행합니다. 이를 위해서는 바이트 형식이 편리하고 효율적입니다. 예를 들어 SavedGame 의 개체를 변환할 수 있습니다.클래스를 일련의 바이트로 변환하고 이 바이트를 네트워크를 통해 다른 컴퓨터로 전송한 다음 다른 컴퓨터에서 이 바이트를 다시 Java 객체로 변환합니다! 어렵게 들리죠? 이 모든 것을 실현하기는 어려울 것 같습니다. / 다행스럽게도 그렇지 않습니다! :) Java에서 Serializable 인터페이스는 직렬화 프로세스를 담당합니다. 이 인터페이스는 매우 간단합니다. 사용하기 위해 단일 메서드를 구현할 필요가 없습니다! 게임 저장을 위한 클래스가 얼마나 간단한지 살펴보십시오.

import java.io.Serializable;
import java.util.Arrays;

public class SavedGame implements Serializable {

   private static final long serialVersionUID = 1L;

   private String[] territoryInfo;
   private String[] resourceInfo;
   private String[] diplomacyInfo;

   public SavedGame(String[] territoryInfo, String[] resourceInfo, String[] diplomacyInfo){
       this.territoryInfo = territoryInfo;
       this.resourceInfo = resourceInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   public String[] getTerritoryInfo() {
       return territoryInfo;
   }

   public void setTerritoryInfo(String[] territoryInfo) {
       this.territoryInfo = territoryInfo;
   }

   public String[] getResourceInfo() {
       return resourceInfo;
   }

   public void setResourceInfo(String[] resourceInfo) {
       this.resourceInfo = resourceInfo;
   }

   public String[] getDiplomacyInfo() {
       return diplomacyInfo;
   }

   public void setDiplomacyInfo(String[] diplomacyInfo) {
       this.diplomacyInfo = diplomacyInfo;
   }

   @Override
   public String toString() {
       return "SavedGame{" +
               "territoryInfo=" + Arrays.toString(territoryInfo) +
               ", resourceInfo=" + Arrays.toString(resourceInfo) +
               ", diplomacyInfo=" + Arrays.toString(diplomacyInfo) +
               '}';
   }
}
세 개의 배열은 영토, 자원 및 외교에 대한 정보를 담당하며 Serializable 인터페이스는 Java 시스템에 다음과 같이 알려줍니다. ' 이 클래스의 개체를 직렬화할 수 있으면 모든 것이 정상입니다 .' 단일 인터페이스가 없는 인터페이스는 이상해 보입니다./ 왜 필요한가요? 그 질문에 대한 답은 위에 나와 있습니다. 자바 머신에 필요한 정보를 제공하기 위해서만 필요합니다. 지난 수업에서 마커 인터페이스에 대해 간략하게 언급했습니다. 이들은 미래에 Java 머신에 유용할 추가 정보로 클래스를 간단히 표시하는 특별한 정보 인터페이스입니다. 구현해야 하는 메서드가 없습니다. 여기에 Serializable이 있습니다 . 그러한 인터페이스 중 하나입니다. 또 다른 중요한 점은 다음과 같습니다.우리가 클래스에서 정의한 private static final long serialVersionUID 변수? 이 필드에는 직렬화된 클래스의 고유한 버전 식별자가 포함됩니다. Serializable 인터페이스를 구현하는 모든 클래스 에는 버전 식별자가 있습니다. 클래스의 내용(필드 및 선언 순서, 메서드 및 선언 순서)에 따라 결정됩니다. 그리고 필드 유형 및/또는 클래스의 필드 수를 변경하면 버전 식별자가 즉시 변경됩니다. serialVersionUID 클래스가 직렬화될 때도 기록됩니다. deserialize(즉, 바이트 시퀀스에서 개체 복원)를 시도할 때 serialVersionUID 의 값이 serialVersionUID 의 값과 비교됩니다.우리 프로그램의 수업 중. 값이 일치하지 않으면 java.io.InvalidClassException이 발생합니다. 아래에서 이에 대한 예를 살펴보겠습니다. 이러한 상황을 피하기 위해 클래스의 버전 식별자를 수동으로 설정하기만 하면 됩니다. 우리의 경우에는 단순히 1과 같습니다(원하는 다른 숫자를 사용할 수 있음). 이제 SavedGame 개체를 직렬화하고 어떤 일이 발생하는지 확인할 시간입니다 !

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

public class Main {

   public static void main(String[] args) throws IOException {

       // Create our object
       String[] territoryInfo = {"Spain has 6 provinces", "Russia has 10 provinces", "France has 8 provinces"};
       String[] resourceInfo = {"Spain has 100 gold", "Russia has 80 gold", "France has 90 gold"};
       String[] diplomacyInfo = {"France is at war with Russia, Spain has taken a neutral position"};

       SavedGame savedGame = new SavedGame(territoryInfo, resourceInfo, diplomacyInfo);

       // Create 2 streams to serialize the object and save it to a file
       FileOutputStream outputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);

       // Save the game to a file
       objectOutputStream.writeObject(savedGame);

       // Close the stream and release resources
       objectOutputStream.close();
   }
}
보시다시피 FileOutputStreamObjectOutputStream 이라는 2개의 스트림을 생성했습니다 . 첫 번째는 파일에 데이터를 쓰는 방법을 알고 두 번째는 개체를 바이트로 변환합니다. 예를 들어 new BufferedReader(new InputStreamReader(...)) 와 같은 유사한 중첩 구조를 이전 학습에서 이미 보았 으므로 겁내지 않아도 됩니다. :) 이 두 스트림 체인을 생성하여 두 작업을 모두 수행합니다. SavedGame 개체를 일련의 바이트로 변환하고 writeObject() 메서드를 사용하여 파일에 저장합니다. 그건 그렇고, 우리는 우리가 얻은 것을 보지도 않았습니다! 이제 파일을 볼 시간입니다! *참고: 미리 파일을 만들 필요는 없습니다. 지정된 이름을 가진 파일이 존재하지 않으면 자동으로 생성됩니다.* 그리고 그 내용은 다음과 같습니다: ¬н sr SavedGame [ 외교 정보 [ Ljava/lang/문자열; 문자열;¬ТVзй{G xp t pФранция воюет СЃ Россией, Р~спания заняла позицию нейтралит етаuq ~ t "РЈ Р~спании 100 золотаt РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 Р· 러시아 »РѕС‚Р°uq ~ t &РЈ Р˜СЃРїР°РЅРёРё 6 провинцийt %РЈ Р Р РѕСЃСЃРёРё 10 провинцийt &РЈ Фр анции 8 провинций 오, 오 :( 우리 프로그램이 작동하지 않는 것 같습니다 : ( 사실, 작동했습니다. 단순히 객체나 텍스트가 아닌 일련의 바이트를 파일로 보낸 것을 기억하십니까? 이것은 저장된 게임입니다! 원래 개체를 복원하려면(즉, 중단한 게임을 시작하고 계속하려면 역직렬화 프로세스가 필요합니다. 우리에게는 다음과 같습니다.

import java.io.*;

public class Main {

   public static void main(String[] args) throws IOException, ClassNotFoundException {

       FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);

       SavedGame savedGame = (SavedGame) objectInputStream.readObject();

       System.out.println(savedGame);
   }
}
결과는 다음과 같습니다! SavedGame{territoryInfo=[스페인은 6개, 러시아는 10개, 프랑스는 8개], resourceInfo=[스페인은 100골드, 러시아는 80골드, 프랑스는 90골드], 외교정보=[프랑스는 러시아와 전쟁 중입니다. 스페인은 중립 입장을 취했습니다]} 훌륭합니다! 먼저 게임 상태를 파일에 저장한 다음 파일에서 복원했습니다. 이제 동일한 작업을 시도하되 SavedGame 클래스에서 버전 식별자를 제거하겠습니다 . 우리는 두 클래스를 모두 다시 작성하지 않을 것입니다. 코드는 동일합니다. SavedGame 클래스 에서 private static final long serialVersionUID를 제거하기만 하면 됩니다 . 다음은 직렬화 후 개체입니다. ¬н sr SavedGameі€MіuОm‰ [ 외교 정보 [Ljava/lang/문자열;[ 자원 정보 정보 ~ [ 영토 정보 ~ xpur [Ljava.lang.String; Р~спания заняла позицию нейтталитетаuq ~ t "РЈ Р~спании 100 золРㅁㅁ °t РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Р~спании 6 РїСЂРѕРІ инцийt %РЈ Р РѕСЃСЃРёРё 10 провинцийt &РЈ Франции 8 провинций 그러나 역직렬화를 시도할 때 어떤 일이 발생하는지 살펴보십시오. InvalidClassException: 로컬 클래스가 호환되지 않음: 스트림 클래스 c serialVersionUID = -196410440475012755, 로컬 클래스 serialVersionUID = -6675950253085108747 그건 그렇고, 우리는 중요한 것을 놓쳤습니다. 분명히 문자열과 프리미티브는 쉽게 직렬화됩니다. Java에는 확실히 이를 위한 내장 메커니즘이 있습니다. 그러나 직렬화 가능 클래스에 프리미티브가 아닌 다른 객체에 대한 참조가 있는 필드가 있다면 어떻게 될까요 ? 예를 들어 별도의 TerritoryInfo , ResourceInfoDiplomacyInfo 클래스를 만들어 SavedGame 클래스 와 함께 작동하도록 합시다 .

public class TerritoryInfo {

   private String info;

   public TerritoryInfo(String info) {
       this.info = info;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   @Override
   public String toString() {
       return "TerritoryInfo{" +
               "info='" + info + '\'' +
               '}';
   }
}

public class ResourceInfo {

   private String info;

   public ResourceInfo(String info) {
       this.info = info;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   @Override
   public String toString() {
       return "ResourceInfo{" +
               "info='" + info + '\'' +
               '}';
   }
}

public class DiplomacyInfo {

   private String info;

   public DiplomacyInfo(String info) {
       this.info = info;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   @Override
   public String toString() {
       return "DiplomacyInfo{" +
               "info='" + info + '\'' +
               '}';
   }
}
이제 우리는 질문에 직면합니다. SavedGame 클래스를 직렬화하려면 이 모든 클래스가 직렬화 가능 해야 합니까 ?

import java.io.Serializable;
import java.util.Arrays;

public class SavedGame implements Serializable {

   private TerritoryInfo territoryInfo;
   private ResourceInfo resourceInfo;
   private DiplomacyInfo diplomacyInfo;

   public SavedGame(TerritoryInfo territoryInfo, ResourceInfo resourceInfo, DiplomacyInfo diplomacyInfo) {
       this.territoryInfo = territoryInfo;
       this.resourceInfo = resourceInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   public TerritoryInfo getTerritoryInfo() {
       return territoryInfo;
   }

   public void setTerritoryInfo(TerritoryInfo territoryInfo) {
       this.territoryInfo = territoryInfo;
   }

   public ResourceInfo getResourceInfo() {
       return resourceInfo;
   }

   public void setResourceInfo(ResourceInfo resourceInfo) {
       this.resourceInfo = resourceInfo;
   }

   public DiplomacyInfo getDiplomacyInfo() {
       return diplomacyInfo;
   }

   public void setDiplomacyInfo(DiplomacyInfo diplomacyInfo) {
       this.diplomacyInfo = diplomacyInfo;
   }

   @Override
   public String toString() {
       return "SavedGame{" +
               "territoryInfo=" + territoryInfo +
               ", resourceInfo=" + resourceInfo +
               ", diplomacyInfo=" + diplomacyInfo +
               '}';
   }
}
좋아, 그럼! 그것을 테스트하자! 지금은 모든 것을 그대로 두고 SavedGame 개체를 직렬화하려고 합니다.

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

public class Main {

   public static void main(String[] args) throws IOException {

       // Create our object
       TerritoryInfo territoryInfo = new TerritoryInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
       ResourceInfo resourceInfo = new ResourceInfo("Spain has 100 gold, Russia has 80 gold, France has 90 gold");
       DiplomacyInfo diplomacyInfo =  new DiplomacyInfo("France is at war with Russia, Spain has taken a neutral position");


       SavedGame savedGame = new SavedGame(territoryInfo, resourceInfo, diplomacyInfo);

       FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

       objectOutputStream.writeObject(savedGame);

       objectOutputStream.close();
   }
}
결과: "main" 스레드에서 예외 java.io.NotSerializableException: DiplomacyInfo 작동하지 않았습니다! 여기 우리 질문에 대한 답이 있습니다. 개체가 직렬화되면 해당 인스턴스 변수가 참조하는 모든 개체가 직렬화됩니다. 그리고 해당 객체가 다른 객체도 참조하는 경우 직렬화됩니다. 그리고 영원히. 이 체인의 모든 클래스는 Serializable 이어야 합니다 . 그렇지 않으면 직렬화가 불가능하고 예외가 발생합니다. 그건 그렇고, 이것은 앞으로 문제를 일으킬 수 있습니다. 예를 들어 직렬화 중에 클래스의 일부가 필요하지 않으면 어떻게 해야 합니까? 또는 라이브러리의 일부로 '상속을 통해' TerritoryInfo 클래스를 가져온다면 어떨까요 ? 그리고 그것이 '따라서 변경할 수 없습니다. 이는 SavedGame 클래스 에 TerritoryInfo 필드를 추가할 수 없다는 것을 의미합니다 . 그러면 전체 SavedGame 클래스가 직렬화할 수 없게 되기 때문입니다! 그게 문제입니다. / Java에서 이런 종류의 문제는 transient 키워드 로 해결됩니다 . 클래스의 필드에 이 키워드를 추가하면 해당 필드가 직렬화되지 않습니다. SavedGame 클래스 transient 의 필드 중 하나를 만든 다음 단일 개체를 직렬화하고 복원해 보겠습니다. Java의 직렬화 및 역직렬화 - 2

import java.io.Serializable;

public class SavedGame implements Serializable {

   private transient TerritoryInfo territoryInfo;
   private ResourceInfo resourceInfo;
   private DiplomacyInfo diplomacyInfo;

   public SavedGame(TerritoryInfo territoryInfo, ResourceInfo resourceInfo, DiplomacyInfo diplomacyInfo) {
       this.territoryInfo = territoryInfo;
       this.resourceInfo = resourceInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   // ...getters, setters, toString()...
}



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

public class Main {

   public static void main(String[] args) throws IOException {

       // Create our object
       TerritoryInfo territoryInfo = new TerritoryInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
       ResourceInfo resourceInfo = new ResourceInfo("Spain has 100 gold, Russia has 80 gold, France has 90 gold");
       DiplomacyInfo diplomacyInfo =  new DiplomacyInfo("France is at war with Russia, Spain has taken a neutral position");


       SavedGame savedGame = new SavedGame(territoryInfo, resourceInfo, diplomacyInfo);

       FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

       objectOutputStream.writeObject(savedGame);

       objectOutputStream.close();
   }
}


import java.io.*;

public class Main {

   public static void main(String[] args) throws IOException, ClassNotFoundException {

       FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);

       SavedGame savedGame = (SavedGame) objectInputStream.readObject();

       System.out.println(savedGame);

       objectInputStream.close();


   }
}
그리고 결과는 다음과 같습니다: SavedGame{territoryInfo=null, resourceInfo=ResourceInfo{info='스페인은 100골드, 러시아는 80골드, 프랑스는 90골드'}, DiplocyInfo=DiplomacyInfo{info='프랑스는 러시아, 스페인과 전쟁 중입니다. 중립 입장을 취했습니다.'}} 즉, 일시적인 필드에 어떤 값을 할당할지에 대한 질문에 대한 답을 얻었습니다. 기본값이 할당됩니다. 개체의 경우 null 입니다 . 'Head-First Java' 책에서 이 주제에 대한 훌륭한 챕터를 읽을 수 있습니다. 주목하세요 :)
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION