Hei! I dagens leksjon snakker vi om serialisering og deserialisering i Java. Vi starter med et enkelt eksempel. La oss si at du har laget et dataspill. Hvis du vokste opp på 90-tallet og husker spillkonsollene fra den tiden, vet du sannsynligvis at de manglet noe vi tar for gitt i dag – muligheten til å lagre og laste inn spill :) Hvis ikke, forestill deg det!
Jeg er redd for at i dag ville et spill uten disse evnene være dømt! Hva betyr det å "lagre" og "laste" et spill uansett? Vel, vi forstår den vanlige betydningen: vi ønsker å fortsette spillet fra stedet der vi slapp. For å gjøre dette lager vi et slags "sjekkpunkt", som vi så bruker for å laste spillet. Men hva betyr dette for en programmerer i stedet for en casual gamer? Svaret er enkelt: vi. La oss si at du spiller som Spania i Strategium. Spillet ditt har en stat: hvem eier hvilke territorier, hvem har hvor mange ressurser, hvem er i allianse med hvem, hvem er i krig med hvem, og så videre. Vi må på en eller annen måte lagre denne informasjonen, programmets tilstand, for å gjenopprette den i fremtiden og fortsette spillet. For dette er nettopp hva serialisering og deseralisering er for. Serialisering er prosessen med å lagre tilstanden til et objekt i en sekvens av byte. Deserialiseringer prosessen med å gjenopprette et objekt fra disse bytene. Ethvert Java-objekt kan konverteres til en bytesekvens. Hvorfor skulle vi trenge det? Vi har sagt mer enn en gang at programmer ikke eksisterer alene. Oftest samhandler de med andre programmer, utveksler data osv. Og en bytesekvens er et praktisk og effektivt format. For eksempel kan vi gjøre
I Java løses problemer av denne typen ved å bruke nøkkelordet

SavedGame
objektet vårt om til en sekvens av byte, sende disse bytene over nettverket til en annen datamaskin, og deretter på den andre datamaskinen gjøre disse bytene tilbake til et Java-objekt! Høres vanskelig ut, ikke sant? Og å implementere denne prosessen virker som en smerte :/ Heldigvis er det ikke slik! :) I Java, denSerializable
grensesnittet er ansvarlig for serialiseringsprosessen. Dette grensesnittet er ekstremt enkelt: du trenger ikke implementere en enkelt metode for å bruke det! Slik ser vår spillsparende klasse ut:
import java.io.Serializable;
import java.util.Arrays;
public class SavedGame implements Serializable {
private static final long serialVersionUID = 1L;
private String[] territoriesInfo;
private String[] resourcesInfo;
private String[] diplomacyInfo;
public SavedGame(String[] territoriesInfo, String[] resourcesInfo, String[] diplomacyInfo){
this.territoriesInfo = territoriesInfo;
this.resourcesInfo = resourcesInfo;
this.diplomacyInfo = diplomacyInfo;
}
public String[] getTerritoriesInfo() {
return territoriesInfo;
}
public void setTerritoriesInfo(String[] territoriesInfo) {
this.territoriesInfo = territoriesInfo;
}
public String[] getResourcesInfo() {
return resourcesInfo;
}
public void setResourcesInfo(String[] resourcesInfo) {
this.resourcesInfo = resourcesInfo;
}
public String[] getDiplomacyInfo() {
return diplomacyInfo;
}
public void setDiplomacyInfo(String[] diplomacyInfo) {
this.diplomacyInfo = diplomacyInfo;
}
@Override
public String toString() {
return "SavedGame{" +
"territoriesInfo=" + Arrays.toString(territoriesInfo) +
", resourcesInfo=" + Arrays.toString(resourcesInfo) +
", diplomacyInfo=" + Arrays.toString(diplomacyInfo) +
'}';
}
}
De tre matrisene er ansvarlige for informasjon om territorier, ressurser og diplomati. Det serialiserbare grensesnittet forteller den virtuelle Java-maskinen: " Alt er OK - om nødvendig kan objekter av denne klassen serialiseres ". Et grensesnitt uten et enkelt grensesnitt ser rart ut :/ Hvorfor er det nødvendig? Svaret på dette spørsmålet kan sees ovenfor: det tjener bare til å gi nødvendig informasjon til den virtuelle Java-maskinen. I en av våre tidligere leksjoner nevnte vi kort markørgrensesnitt . Dette er spesielle informasjonsgrensesnitt som ganske enkelt markerer klassene våre med tilleggsinformasjon som vil være nyttig for Java-maskinen i fremtiden. De har ingen metoder du må implementere.Serializable
er et av disse grensesnittene. Et annet viktig poeng: Hvorfor trenger vi variabelen private static final long serialVersionUID
som vi definerte i klassen? Hvorfor trengs det? Dette feltet inneholder en unik identifikator for versjonen av den serialiserte klassen . Enhver klasse som implementerer grensesnittet Serializable
har en version
identifikator. Den beregnes basert på innholdet i klassen: dens felter, rekkefølgen de er deklarert i, metoder osv. Hvis vi endrer felttype og/eller antall felt i klassen vår, endres versjonsidentifikatoren umiddelbart . serialVersionUID
skrives også når klassen er serialisert. Når vi prøver å deserialisere, det vil si gjenopprette et objekt fra et sett med byte, serialVersionUID
sammenlignes den assosierte med verdien avserialVersionUID
for klassen i programmet vårt. Hvis verdiene ikke stemmer overens, så en java.io. InvalidClassException vil bli kastet. Vi vil se et eksempel på dette nedenfor. For å unngå dette setter vi ganske enkelt versjonsidentifikatoren manuelt i klassen vår. I vårt tilfelle vil det ganske enkelt være lik 1 (men du kan erstatte et hvilket som helst annet tall du vil). Vel, det er på tide å prøve å serialisere SavedGame
objektet vårt og se hva som skjer!
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[] resourcesInfo = {"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, resourcesInfo, 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 free resources
objectOutputStream.close();
}
}
Som du kan se har vi laget 2 strømmer: FileOutputStream
og ObjectOutputStream
. Den første kan skrive data til en fil, og den andre konverterer objekter til byte. Du har allerede sett lignende "nestede" konstruksjoner, for eksempel , new BufferedReader(new InputStreamReader(...))
i tidligere leksjoner, så de burde ikke skremme deg :) Ved å lage en slik "kjede" av to strømmer, utfører vi begge oppgavene: vi konverterer objektet SavedGame
til et sett av byte og lagre den i en fil ved hjelp av writeObject()
metoden. Og forresten så vi ikke engang på hva vi fikk! Det er på tide å se på filen! *Merk: du trenger ikke opprette filen på forhånd. Hvis en fil med det navnet ikke eksisterer, vil den bli opprettet automatisk* Og her er innholdet!
¬н sr SavedGame [ diplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ territoriesInfoq ~ xpur [Ljava.lang.String;ТVзй{G xp t pФранция воюет СЃ Россией, Рспания заняла позицию нейтралитетаuq ~ t "РЈ Рспании 100 золотаt РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Рспании 6 провинцийt %РЈ Р РѕСЃСЃРёРё 10 провинцийt &РЈ Франции 8 провинций
Uh-oh :( Det ser ut til at programmet vårt ikke fungerte :( Faktisk fungerte det. Du husker at vi sendte et sett med byte, ikke bare et objekt eller tekst, til filen? Vel, dette er hva det sett med byte ser ut som :) Dette er det lagrede spillet vårt! Hvis vi vil gjenopprette det opprinnelige objektet vårt, dvs. starte og fortsette spillet der vi slapp, trenger vi den omvendte prosessen: deserialisering . Slik vil det se ut i vår sak:
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);
}
}
Og her er resultatet!
SavedGame{territoriesInfo=["Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces], resourcesInfo=[Spain has 100 gold, Russia has 80 gold, France has 90 gold], diplomacyInfo=[France is at war with Russia, Spain has taken a neutral position]}
Utmerket! Vi klarte først å lagre spillets tilstand til en fil, og deretter gjenopprette den fra filen. La oss nå prøve å gjøre det samme, men uten versjonsidentifikatoren for SavedGame
klassen vår. Vi vil ikke skrive om begge klassene våre. Koden deres forblir den samme, men vi fjerner private static final long serialVersionUID
fra SavedGame
klassen. Her er objektet vårt etter serialisering:
¬н sr SavedGameі€MіuОm‰ [ diplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ territoriesInfoq ~ xpur [Ljava.lang.String;ТVзй{G xp t pФранция воюет СЃ Россией, Рспания заняла позицию нейтралитетаuq ~ t "РЈ Рспании 100 золотаt РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Рспании 6 провинцийt %РЈ Р РѕСЃСЃРёРё 10 провинцийt &РЈ Франции 8 провинций
Men se på hva som skjer når vi prøver å deserialisere det:
InvalidClassException: local class incompatible: stream classdesc serialVersionUID = -196410440475012755, local class serialVersionUID = -6675950253085108747
Dette er selve unntaket vi nevnte ovenfor. Vi gikk forresten glipp av noe viktig. Det er fornuftig at strenger og primitiver lett kan serialiseres: Java har sannsynligvis en slags innebygd mekanisme for å gjøre dette. Men hva om serializable
klassen vår har felt som ikke er primitive, men heller referanser til andre objekter? La oss for eksempel lage separate TerritoriesInfo
, ResourcesInfo
og DiplomacyInfo
klasser for å jobbe med SavedGame
klassen vår.
public class TerritoriesInfo {
private String info;
public TerritoriesInfo(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override
public String toString() {
return "TerritoriesInfo{" +
"info='" + info + '\'' +
'}';
}
}
public class ResourcesInfo {
private String info;
public ResourcesInfo(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override
public String toString() {
return "ResourcesInfo{" +
"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 + '\'' +
'}';
}
}
Og nå oppstår et spørsmål: trenger alle disse klassene å være det Serializable
hvis vi ønsker å serialisere vår endrede SavedGame
klasse?
import java.io.Serializable;
import java.util.Arrays;
public class SavedGame implements Serializable {
private TerritoriesInfo territoriesInfo;
private ResourcesInfo resourcesInfo;
private DiplomacyInfo diplomacyInfo;
public SavedGame(TerritoriesInfo territoriesInfo, ResourcesInfo resourcesInfo, DiplomacyInfo diplomacyInfo) {
this.territoriesInfo = territoriesInfo;
this.resourcesInfo = resourcesInfo;
this.diplomacyInfo = diplomacyInfo;
}
public TerritoriesInfo getTerritoriesInfo() {
return territoriesInfo;
}
public void setTerritoriesInfo(TerritoriesInfo territoriesInfo) {
this.territoriesInfo = territoriesInfo;
}
public ResourcesInfo getResourcesInfo() {
return resourcesInfo;
}
public void setResourcesInfo(ResourcesInfo resourcesInfo) {
this.resourcesInfo = resourcesInfo;
}
public DiplomacyInfo getDiplomacyInfo() {
return diplomacyInfo;
}
public void setDiplomacyInfo(DiplomacyInfo diplomacyInfo) {
this.diplomacyInfo = diplomacyInfo;
}
@Override
public String toString() {
return "SavedGame{" +
"territoriesInfo=" + territoriesInfo +
", resourcesInfo=" + resourcesInfo +
", diplomacyInfo=" + diplomacyInfo +
'}';
}
}
Vel, la oss teste det! La oss la alt være som det er og prøve å serialisere et SavedGame
objekt:
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(territoriesInfo, resourcesInfo, diplomacyInfo);
FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(savedGame);
objectOutputStream.close();
}
}
Resultat:
Exception in thread "main" java.io.NotSerializableException: DiplomacyInfo
Det gikk ikke! I utgangspunktet er det svaret på spørsmålet vårt. Når et objekt serialiseres, serialiseres alle objektene det refereres til av forekomstvariablene. Og hvis disse objektene også refererer til andre objekter, blir de også serialisert. Og så videre i det uendelige. Alle klassene i denne kjeden må væreSerializable
, ellers vil det være umulig å serialisere dem og et unntak vil bli kastet. Dette kan forresten skape problemer nedover veien. Hva skal vi gjøre hvis vi for eksempel ikke trenger en del av en klasse når vi serialiserer? Eller, for eksempel, hva om TerritoryInfo
klassen kom til oss som en del av et tredjepartsbibliotek. Og anta videre at det ikke er det Serializable
, og at vi følgelig ikke kan endre det. Det viser seg at vi ikke kan legge til et TerritoryInfo
felt til vårSavedGame
klasse, fordi det ville gjøre hele SavedGame
klassen ikke-serialiserbar! Det er et problem :/ 
transient
. Hvis du legger til dette nøkkelordet i et felt i klassen din, blir ikke det feltet serialisert. La oss prøve å gjøre et av SavedGame
klassens forekomstfelt forbigående. Deretter skal vi serialisere og gjenopprette ett objekt.
import java.io.Serializable;
public class SavedGame implements Serializable {
private transient TerritoriesInfo territoriesInfo;
private ResourcesInfo resourcesInfo;
private DiplomacyInfo diplomacyInfo;
public SavedGame(TerritoriesInfo territoriesInfo, ResourcesInfo resourcesInfo, DiplomacyInfo diplomacyInfo) {
this.territoriesInfo = territoriesInfo;
this.resourcesInfo = resourcesInfo;
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(territoriesInfo, resourcesInfo, 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();
}
}
Og her er resultatet:
SavedGame{territoriesInfo=null, resourcesInfo=ResourcesInfo{info='Spain has 100 gold, Russia has 80 gold, France has 90 gold'}, diplomacyInfo=DiplomacyInfo{info='France is at war with Russia, Spain has taken a neutral position'}}
I tillegg fikk vi svar på spørsmålet vårt om hvilken verdi som blir tildelt et transient
felt. Den blir tildelt standardverdien. For objekter er dette null
. Du kan lese denne utmerkede artikkelen om serialisering når du har noen minutter til overs. Den nevner også Externalizable
grensesnittet, som vi skal snakke om i neste leksjon. I tillegg har boken "Head-First Java" et kapittel om dette emnet. Gi det litt oppmerksomhet :)
GO TO FULL VERSION