JavaRush /Java блогы /Random-KK /Сериализация сол күйінде. 1 бөлім
articles
Деңгей

Сериализация сол күйінде. 1 бөлім

Топта жарияланған
Бір қарағанда, сериализация тривиальды процесс сияқты көрінеді. Шынында да, қарапайым не болуы мүмкін? Интерфейсті жүзеге асыру үшін сыныпты жариялады 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сериядан шығару келесідей жүреді: жад an object үшін бөлінеді, содан кейін оның өрістері ағынның мәндерімен толтырылады. Нысанның конструкторы шақырылмайды. Бұл жерде біз бұл жағдайды бөлек қарастыруымыз керек. Жарайды, біздің сынып сериялануы мүмкін. Ал оның ата-анасы? Толығымен міндетті емес! Сонымен қатар, егер сіз сыныпты мұрагер болсаңыз 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;
        }
    }
}
Ол мөлдір - бізде серияланbyteын ата-аналық сынып және серияланатын еншілес сынып бар. Және бұл келесідей болады:
Creating...
Parent::Constructor
Child::Constructor
Serializing...
Deserializing...
Parent::Constructor
c1.i=1
c1.field=5
Яғни, сериядан шығару кезінде ата-аналық NON-серияланbyteын класстың параметрлері жоқ конструктор деп аталады . Ал егер мұндай конструктор болмаса, сериядан шығару кезінде қате пайда болады. Біз сериядан шығарып жатқан еншілес нысанның конструкторы жоғарыда айтылғандай шақырылмайды. Пайдаланылған кезде стандартты механизмдер осылай әрекет етеді Serializable. Оны пайдаланған кезде Externalizableжағдай басқаша. Біріншіден, параметрлері жоқ конструктор шақырылады, содан кейін оның барлық деректерін нақты оқитын құрылған an objectіге readExternal әдісі шақырылады. Сондықтан Externalizable интерфейсін жүзеге асыратын кез келген сыныпта параметрлері жоқ жалпы конструктор болуы керек! Сонымен қатар, мұндай класстың барлық ұрпақтары интерфейсті жүзеге асыру үшін де қарастырылатындықтан Externalizable, оларда параметрсіз конструктор болуы керек! Әрі қарай жүрейік. сияқты өріс модификаторы бар transient. Бұл өрісті сериялауға болмайды дегенді білдіреді. Дегенмен, өзіңіз түсінгендей, бұл нұсқаулық тек стандартты сериялау механизміне әсер етеді. Қолданылған кезде Externalizableешкім бұл өрісті сериялауға, сондай-ақ оны шегеруге алаңдамайды. Егер өріс өтпелі деп жарияланса, нысан сериядан шығарылған кезде ол әдепкі мәнді қабылдайды. Тағы бір өте нәзік нүкте. Стандартты сериялаумен модификаторы бар өрістер staticсерияланбайды. Сәйкесінше, сериядан шығарудан кейін бұл өріс өз мәнін өзгертпейді. Әрине, іске асыру кезінде Externalizableешкім бұл өрісті сериялауға және сериядан шығаруға алаңдамайды, бірақ мен мұны жасамауға кеңес беремін, өйткені бұл нәзік қателерге әкелуі мүмкін. Модификаторы бар өрістер finalкәдімгі өрістер сияқты серияланады. Бір ерекшелікпен - Externalizable пайдалану кезінде оларды сериядан шығару мүмкін емес. Өйткені final-поляолар конструкторда инициализациялануы керек, содан кейін readExternal ішінде бұл өрістің мәнін өзгерту мүмкін болмайды. Сәйкесінше, -өрісі бар нысанды сериялау қажет болса final, стандартты сериялауды ғана пайдалану керек болады. Көптеген адамдар білмейтін тағы бір мәселе. Стандартты сериялау сыныпта өрістердің жариялану ретін ескереді. Қалай болғанда да, бұл бұрынғы нұсқаларда болды; JVM Oracle іске асыруының 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
Дегенмен, бұл тексеруді айналып өтпесе, алдаудың жолы бар. Бұл сынып өрістерінің жиыны және олардың реті әлдеқашан анықталған болса, пайдалы болуы мүмкін, бірақ сынып әдістері өзгеруі мүмкін. Бұл жағдайда сериялау тәуекелге ұшырамайды, бірақ стандартты механизм өзгертілген сыныптың byte codeын пайдаланып деректерді сериядан шығаруға мүмкіндік бермейді. Бірақ, айтқанымдай, оны алдауға болады. Атап айтқанда, сыныптағы өрісті қолмен анықтаңыз private static final long serialVersionUID. Негізінде, бұл өрістің мәні мүлдем кез келген нәрсе болуы мүмкін. Кейбір адамдар оны code өзгертілген күнге теңестіруді жөн көреді. Кейбіреулер тіпті 1 л пайдаланады. Стандартты мәнді (ішкі түрде есептелетін) алу үшін SDK құрамына кіретін сериялық бағдарламаны пайдалануға болады. Осылайша анықталғаннан кейін өрістің мәні бекітіледі, сондықтан сериядан шығаруға әрқашан рұқсат етіледі. Сонымен қатар, 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{}
Басқаша айтқанда, бізде серияланbyteын ата-анадан мұраланған класс бар. Бұл сыныпты сериялау мүмкін бе және бұл үшін не қажет? Ата-аналық сыныптың айнымалыларымен не болады? Жауабы мынау. Иә, Bсынып данасын сериялауға болады. Бұл үшін не керек? Бірақ сыныпта Aпараметрлері жоқ конструктор болуы керек publicнемесе protected. Содан кейін сериядан шығару кезінде барлық класс айнымалылары Aосы конструктор арқылы инициализацияланады. Сынып айнымалылары Bсерияланған деректер ағынының мәндерімен инициализацияланады. BТеориялық тұрғыдан, сыныпта мен басында айтқан әдістерді анықтауға болады - readObjectжәне writeObject, - оның басында класс айнымалыларын сериядан шығару (де-), содан кейін қол жетімді айнымалыларды (де-)серияландыруды орындау Bкерек in.defaultReadObject/out.defaultWriteObject. сыныптан A(біздің жағдайда бұл iPublic, iProtectedжәне iPackage, егер Bол A. Дегенмен, менің ойымша, бұл үшін кеңейтілген сериализацияны қолданған дұрыс. Мен тоқталғым келетін келесі мәселе - бірнеше нысандарды сериялау. Бізде келесі сынып құрылымы бар делік:
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, ол өз кезегінде барлығы басталған Cданаға сілтемесі бар дананы бойымен сүйреп апарады. AТұйық шеңбер және шексіз рекурсия? Бақытымызға орай, жоқ. Келесі сынақ codeын қарастырайық:
// 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, оларға бір-біріне сілтемелер береміз, содан кейін олардың әрқайсысын сериялаймыз. Содан кейін біз оларды сериядан шығарып, бірқатар тексерулерді орындаймыз. Нәтижесінде не болады: BC
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
Сонымен, сіз бұл сынақтан не үйрене аласыз? Бірінші. Сериялизациядан кейінгі нысан сілтемелері оның алдындағы сілтемелерден ерекшеленеді. Басқаша айтқанда, сериялау/сериясыздандыру кезінде нысан көшірілді. Бұл әдіс кейде an objectілерді клондау үшін қолданылады. Екінші тұжырым маңыздырақ. Айқас сілтемелері бар бірнеше нысандарды сериялау/сериядан шығару кезінде сол сілтемелер сериядан шығарылғаннан кейін жарамды болып қалады. Басқаша айтқанда, егер сериялау алдында олар бір нысанды көрсетсе, сериядан шығарудан кейін олар да бір нысанды көрсетеді. Мұны растау үшін тағы бір шағын сынақ:
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
Көріп отырғаныңыздай, сериясыздандырылған төрт нысанның барлығы бір нысанды білдіреді - оған сілтемелер тең. Дәл сериализациядан бұрынғыдай. ExternalizableТағы бір қызық мәселе - егер біз бір уақытта іске асырсақ және не болады Serializable? Бұл сұрақтағыдай - піл мен кит - кім кімді жеңеді? Жеңеді Externalizable. Сериализация механизмі алдымен оның бар-жоғын тексереді, содан кейін ғана оның бар-жоғын тексереді.Сонымен, Serializableіске асыратын В класы Serializableіске асыратын А сыныбынан мұрагер болса Externalizable, В класының өрістері серияланбайды. Соңғы нүкте - мұрагерлік. іске асыратын сыныптан мұра алған кезде Serializableқосымша әрекеттерді орындау қажет емес. Сериализация балалар сыныбына да таралады. іске асыратын сыныптан мұра алған кезде Externalizable, ата-аналық сыныптың readExternal және writeExternal әдістерін қайта анықтау керек. Әйтпесе, еншілес сыныптың өрістері серияланбайды. Бұл жағдайда ата-аналық әдістерді шақыруды ұмытпау керек, әйтпесе ата-аналық өрістер серияланбайды. * * * Біз мәліметтерді аяқтаған шығармыз. Дегенмен, жаһандық сипаттағы біз қозғамаған бір мәселе бар. Атап айтқанда -

Сізге Externalizable не үшін қажет?

Неліктен бізге кеңейтілген сериализация қажет? Жауап қарапайым. Біріншіден, бұл әлдеқайда икемділік береді. Екіншіден, ол жиі серияланған деректер көлемі бойынша айтарлықтай табыстарды қамтамасыз ете алады. Үшіншіден, өнімділік сияқты аспект бар, ол туралы төменде айтатын боламыз . Икемділікпен бәрі түсінікті сияқты. Шынында да, біз сериялау және сериядан шығару процестерін өзіміз қалағандай басқара аламыз, бұл бізді сыныптағы кез келген өзгерістерден тәуелсіз етеді (жоғарыда айтқанымдай, сыныптағы өзгерістер сериядан шығаруға үлкен әсер етуі мүмкін). Сондықтан, мен көлемнің өсуі туралы бірнеше сөз айтқым келеді. Бізде келесі сынып бар делік:
public class DateAndTime{

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

}
Қалғаны маңызды емес. Өрістер int түрінде жасалуы мүмкін, бірақ бұл мысалдың әсерін күшейтеді. Шындығында өрістер intөнімділік себептері бойынша терілуі мүмкін. Қалай болғанда да, мәселе анық. Класс күн мен уақытты білдіреді. Бұл бізге ең алдымен сериализация тұрғысынан қызықты. Мүмкін, ең оңай нәрсе қарапайым уақыт белгісін сақтау болар еді. Ол ұзын типті, яғни. серияланған кезде ол 8 byte алады. Сонымен қатар, бұл тәсіл компоненттерді бір мәнге және кері түрлендіру әдістерін талап етеді, яғни. - өнімділіктің төмендеуі. Бұл тәсілдің артықшылығы - 64 битке сыйатын мүлдем ақылсыз күн. Бұл қауіпсіздіктің үлкен маржасы, көбінесе шын мәнінде қажет емес. Жоғарыда берілген класс 2 + 5*1 = 7 byte алады. Сыныпқа және 6 өріске қосымша шығындар. Бұл деректерді қысудың қандай да бір жолы бар ма? Әрине. Секундтар мен minutesтар 0-59 аралығында, яғни. оларды көрсету үшін 8 биттің орнына 6 бит жеткілікті. Сағат – 0-23 (5 бит), күн – 0-30 (5 бит), айлар – 0-11 (4 бит). Барлығы, жылды есепке алмағанда барлығы – 26 бит. int өлшеміне әлі 6 бит қалды. Теориялық тұрғыдан, кейбір жағдайларда бұл бір жылға жеткілікті болуы мүмкін. Олай болмаса, басқа byte қосу деректер өрісінің өлшемін 14 битке дейін арттырады, бұл 0-16383 ауқымын береді. Бұл нақты қолданбаларда жеткілікті. Жалпы, біз қажетті ақпаратты сақтау үшін қажетті деректердің көлемін 5 byteқа дейін азайттық. Егер 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);
}
Шындығында, бәрі осы. Сериализациядан кейін біз әр сыныпқа, екі өріске (6 орнына) және 5 byte деректерге қосымша шығындар аламыз. Бұл қазірдің өзінде айтарлықтай жақсы. Одан әрі қаптаманы мамандандырылған кітапханаларға қалдыруға болады. Келтірілген мысал өте қарапайым. Оның негізгі мақсаты - жетілдірілген сериялауды қалай қолдануға болатынын көрсету. Менің ойымша, серияланған деректер көлеміндегі мүмкін пайда негізгі артықшылықтан алыс болса да. Негізгі артықшылығы, икемділіктен басқа... (келесі бөлімге біркелкі өтіңіз...) Дереккөзге сілтеме: Серияландыру
Пікірлер
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION