Hej! I dagens lektion pratar vi om serialisering och deserialisering i Java. Vi börjar med ett enkelt exempel. Låt oss säga att du har skapat ett datorspel. Om du växte upp på 90-talet och minns den tidens spelkonsoler, vet du förmodligen att de saknade något som vi tar för givet idag — möjligheten att spara och ladda spel :) Om inte, föreställ dig det!
Jag är rädd att idag skulle ett spel utan dessa förmågor vara dömt! Vad betyder det att "spara" och "ladda" ett spel ändå? Tja, vi förstår den vanliga innebörden: vi vill fortsätta spelet från den plats där vi slutade. För att göra detta skapar vi en slags "checkpoint", som vi sedan använder för att ladda spelet. Men vad betyder detta för en programmerare snarare än en casual gamer? Svaret är enkelt: vi. Låt oss säga att du spelar som Spanien i Strategium. Ditt spel har en stat: vem äger vilka territorier, vem har hur många resurser, vem är i en allians med vem, vem är i krig med vem och så vidare. Vi måste på något sätt spara denna information, vårt programs tillstånd, för att återställa den i framtiden och fortsätta spelet. För det är just detta som serialisering och deseralisering är till för. Serialisering är processen att lagra ett objekts tillstånd i en sekvens av byte. Deserialiseringär processen att återställa ett objekt från dessa bytes. Alla Java-objekt kan konverteras till en bytesekvens. Varför skulle vi behöva det? Vi har sagt mer än en gång att program inte existerar på egen hand. Oftast interagerar de med andra program, utbyter data etc. Och en bytesekvens är ett bekvämt och effektivt format. Till exempel kan vi förvandla vårt
I Java löses problem av det här slaget med hjälp av

SavedGame
objekt till en sekvens av bytes, skicka dessa bytes över nätverket till en annan dator och sedan på den andra datorn förvandla dessa bytes tillbaka till ett Java-objekt! Låter svårt, eller hur? Och att implementera denna process verkar vara jobbigt :/ Lyckligtvis är det inte så! :) I Java, denSerializable
gränssnittet ansvarar för serialiseringsprocessen. Det här gränssnittet är extremt enkelt: du behöver inte implementera en enda metod för att använda det! Så här enkelt ser vår spelsparande klass 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 arrayerna är ansvariga för information om territorier, resurser och diplomati. Det serialiserbara gränssnittet säger till den virtuella Java-maskinen: " Allt är OK - om det behövs kan objekt av denna klass serialiseras" . Ett gränssnitt utan ett enda gränssnitt ser konstigt ut :/ Varför är det nödvändigt? Svaret på denna fråga kan ses ovan: det tjänar bara till att tillhandahålla nödvändig information till den virtuella Java-maskinen. I en av våra tidigare lektioner nämnde vi kortfattat markörgränssnitt . Dessa är speciella informationsgränssnitt som helt enkelt markerar våra klasser med ytterligare information som kommer att vara användbar för Java-maskinen i framtiden. De har inga metoder som du måste implementera.Serializable
är ett av dessa gränssnitt. En annan viktig punkt: Varför behöver vi private static final long serialVersionUID
variabeln som vi definierade i klassen? Varför behövs det? Det här fältet innehåller en unik identifierare för versionen av den serialiserade klassen . Alla klasser som implementerar Serializable
gränssnittet har en version
identifierare. Den beräknas baserat på innehållet i klassen: dess fält, i vilken ordning de deklareras, metoder etc. Om vi ändrar typen av fält och/eller antalet fält i vår klass, ändras versionsidentifieraren omedelbart . serialVersionUID
skrivs också när klassen serialiseras. När vi försöker deserialisera, det vill säga återställa ett objekt från en uppsättning byte, serialVersionUID
jämförs den associerade med värdet avserialVersionUID
för klassen i vårt program. Om värdena inte matchar, då en java.io. InvalidClassException kommer att kastas. Vi kommer att se ett exempel på detta nedan. För att undvika detta ställer vi helt enkelt in versionsidentifieraren manuellt i vår klass. I vårt fall blir det helt enkelt lika med 1 (men du kan ersätta vilket annat nummer du vill). Tja, det är dags att försöka serialisera vårt SavedGame
objekt och se vad som händer!
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 skapade vi 2 strömmar: FileOutputStream
och ObjectOutputStream
. Den första kan skriva data till en fil, och den andra konverterar objekt till byte. Du har redan sett liknande "kapslade" konstruktioner, till exempel, , new BufferedReader(new InputStreamReader(...))
i tidigare lektioner, så de borde inte skrämma dig :) Genom att skapa en sådan "kedja" av två strömmar utför vi båda uppgifterna: vi konverterar objektet SavedGame
till en uppsättning av byte och spara den i en fil med writeObject()
metoden. Och, förresten, vi tittade inte ens på vad vi fick! Det är dags att titta på filen! *Obs: du behöver inte skapa filen i förväg. Om en fil med det namnet inte finns skapas den automatiskt* Och här är dess innehåll!
¬н 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 verkar som att vårt program inte fungerade :( Det fungerade faktiskt. Du minns att vi skickade en uppsättning byte, inte bara ett objekt eller text, till filen? Tja, det här är vad som uppsättning byte ser ut :) Detta är vårt sparade spel! Om vi vill återställa vårt ursprungliga objekt, dvs starta och fortsätta spelet där vi slutade, behöver vi den omvända processen: deserialisering. Så här kommer det att se ut i vår fall:
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);
}
}
Och här är 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]}
Excellent! Vi lyckades först spara vårt spels tillstånd till en fil och sedan återställa det från filen. Låt oss nu försöka göra samma sak, men utan versionsidentifieraren för vår SavedGame
klass. Vi kommer inte att skriva om båda våra klasser. Deras kod kommer att förbli densamma, men vi tar bort private static final long serialVersionUID
från SavedGame
klassen. Här är vårt objekt efter 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 titta på vad som händer när vi försöker deserialisera det:
InvalidClassException: local class incompatible: stream classdesc serialVersionUID = -196410440475012755, local class serialVersionUID = -6675950253085108747
Detta är själva undantaget som vi nämnde ovan. Förresten, vi missade något viktigt. Det är vettigt att strängar och primitiver lätt kan serialiseras: Java har förmodligen någon form av inbyggd mekanism för att göra detta. Men vad händer om vår serializable
klass har fält som inte är primitiva, utan snarare referenser till andra objekt? Låt oss till exempel skapa separata och klasser för att arbeta med vår klass TerritoriesInfo
.ResourcesInfo
DiplomacyInfo
SavedGame
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 + '\'' +
'}';
}
}
Och nu uppstår en fråga: behöver alla dessa klasser vara det Serializable
om vi vill serialisera vår förändrade SavedGame
klass?
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 +
'}';
}
}
Nåväl, låt oss testa det! Låt oss lämna allt som det är och försöka serialisera ett 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 fungerade inte! I grund och botten är det svaret på vår fråga. När ett objekt serialiseras serialiseras alla objekt som refereras till av dess instansvariabler. Och om dessa objekt också refererar till andra objekt, så är de också serialiserade. Och så vidare i det oändliga. Alla klasser i den här kedjan måste varaSerializable
, annars blir det omöjligt att serialisera dem och ett undantag kommer att kastas. Detta kan förresten skapa problem på vägen. Vad ska vi göra om vi till exempel inte behöver en del av en klass när vi serialiserar? Eller, till exempel, tänk om TerritoryInfo
klassen kom till oss som en del av något tredjepartsbibliotek. Och anta vidare att det inte är det Serializable
och att vi därför inte kan ändra det. Det visar sig att vi inte kan lägga till ett TerritoryInfo
fält till vårSavedGame
klass, för att göra det skulle göra hela SavedGame
klassen icke-serialiserbar! Det är ett problem :/ 
transient
nyckelordet. Om du lägger till det här nyckelordet i ett fält i din klass kommer det fältet inte att serialiseras. Låt oss försöka göra ett av SavedGame
klassens instansfält övergående. Sedan kommer vi att serialisera och återställa 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();
}
}
Och här är 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'}}
Dessutom fick vi svar på vår fråga om vilket värde som tilldelas ett transient
fält. Den tilldelas standardvärdet. För objekt är detta null
. Du kan läsa den här utmärkta artikeln om serialisering när du har några minuter över. Det nämner också Externalizable
gränssnittet, som vi kommer att prata om i nästa lektion. Dessutom har boken "Head-First Java" ett kapitel om detta ämne. Ge det lite uppmärksamhet :)
GO TO FULL VERSION