CodeGym /Kurslar /JAVA 25 SELF /Seriyalaşdırma zamanı uyğunluq və geriyə uyğunluq (backwa...

Seriyalaşdırma zamanı uyğunluq və geriyə uyğunluq (backward compatibility)

JAVA 25 SELF
Səviyyə , Dərs
Mövcuddur

1. Uyğunluq problemi

Təsəvvür edin: tətbiqinizin ilk versiyasını buraxdınız, istifadəçilər məlumatları saxlamağa başladılar (məsələn, istifadəçi profilləri və ya sazlamalar). Bir aydan sonra başa düşdünüz ki, UserProfile sinfində email sahəsi çatışmır və onu əlavə etdiniz. Hər şey əladır... ta ki, köhnə faylı yükləməyə çalışana qədər. Ən yaxşı halda yeni sahə boş qalacaq, ən pis halda — istisna alıb istifadəçini məyus edəcəksiniz.

Seriyalaşdırma uyğunluğu — proqramın əvvəlki sinif versiyaları ilə seriyalaşdırılmış məlumatları düzgün oxuya bilməsi və əksinə yazması deməkdir. Java-da (xüsusən Serializable vasitəsilə binar seriyalaşdırmada) bu mövzu xüsusilə vacibdir, çünki JVM siniflərin struktur dəyişikliyinə qarşı çox həssasdır.

Problemin yarandığı tipik ssenarilər:

  • Sinfə yeni sahə əlavə etdiniz.
  • Köhnə sahəni sildiniz.
  • Sahənin tipini dəyişdiniz (məsələn, intString).
  • Sinfə ad dəyişikliyi etdiniz və ya onu başqa paketə köçürdünüz.
  • Obyektləri seriyalaşdıran kitabxananı və ya framework-ü yenilədiniz.

Bu halların hamısında köhnə seriyalaşdırılmış məlumatlar yeni versiya üçün “oxunmayan” ola bilər.

2. serialVersionUID: seriyalaşdırılan sinfin “pasportu”

Java-da hər bir seriyalaşdırılan sinif (yəni Serializable interfeysini həyata keçirən) unikal versiya identifikatoruna — serialVersionUID — malikdir. Bu sahədən JVM obyektin həmin siniflə deserializasiya olunub-olunmayacağını yoxlamaq üçün istifadə edir. İdentifikatorlar uyğun gəlmirsə — InvalidClassException alırıq.

private static final long serialVersionUID = 1L;

Əgər bu sahəni açıq şəkildə elan etməmisinizsə, Java onu sinfin strukturu (sahələr, metodlar, modifikatorlar və s.) əsasında avtomatik generasiya edəcək. Amma sonradan sinfi (hətta cüzi də olsa) dəyişsəniz, avtomatik yaradılmış serialVersionUID dəyişəcək və köhnə məlumatlar uyğunsuz olacaq.

Yoxlama necə işləyir?

Obyekt seriyalaşdırılarkən, onun məlumatları ilə birlikdə serialVersionUID dəyəri də axına yazılır. Deserializasiya zamanı JVM bu identifikatoru cari sinifdə göstərilənlə tutuşdurur. Hər şey uyğun gəlirsə — obyekt sakitcə bərpa olunur. İdentifikatorlar fərqlidirsə, proses dərhal xəta ilə dayandırılır: JVM hesab edir ki, sinif o qədər dəyişib ki, köhnə məlumatlar ona artıq uyğun deyil.

serialVersionUID-i nə üçün açıq şəkildə elan etməli?

Əgər serialVersionUID dəyərini özünüz təyin edirsinizsə, sinifdə hansı dəyişikliklərin “məqbul” sayılacağını nəzarətdə saxlayırsınız. Məsələn, yeni sahə əlavə etmisiniz, amma köhnə obyektlərin hələ də yüklənməsini istəyirsiniz? İdentifikatoru əvvəlki kimi saxlayın — və deserializasiya problemsiz keçəcək. Avtomatik generasiyaya güvənsəniz, xoşagəlməz sürpriz ola bilər: koddakı ən kiçik dəyişiklik belə serialVersionUID-in dəyişməsinə və nəticədə köhnə saxlamaların açılmamasına gətirib çıxaracaq.

Nümunə:

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    // ... getter və setter-lər
}

Artıq yeni sahələr (məcburi deyilsə) əlavə edə bilərsiniz və köhnə obyektlərin deserializasiyası pozulmayacaq.

3. Sinif dəyişdikdə nə baş verir?

Yeni sahələrin əlavə edilməsi

Köhnə seriyalaşdırılmış obyekt → əlavə sahəsi olan yeni sinif

  • Yeni sahə defolt dəyər alacaq (null, 0, false).
  • Qalan hər şey düzgün deserializasiya olunacaq.

Nümunə:

// Əvvəl:
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
}

// Sonra:
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private String email; // yeni sahə
}

Nəticə: Köhnə obyektlər yüklənir, email == null.

Sahənin silinməsi

Köhnə seriyalaşdırılmış obyektdə sahə var, amma yeni sinifdə yoxdur

  • Bu sahə deserializasiya zamanı sadəcə nəzərə alınmır.
  • Əsas odur — serialVersionUID-i dəyişməyəsiniz.

Sahənin tipinin dəyişdirilməsi

Məsələn, əvvəl int age idi, indi String age oldu.

  • Bu, uyğunsuz dəyişiklikdir. Deserializasiya cəhdində xəta yaranacaq (adətən InvalidClassException və ya ClassCastException).
  • Belə dəyişikliklərdən qaçın və ya uyğunluğu fərdi seriyalaşdırma ilə təmin edin (aşağıya baxın).

Sinfin və ya paketin adının dəyişdirilməsi

Burada vəziyyət sərtdir: sinfin və ya paketin adını dəyişsəniz, deserializasiya baş tutmayacaq. Seriyalaşdırılmış axında sinfin tam adı saxlanılır və JVM məhz onu görməyi gözləyir. Ona görə istənilən ad dəyişikliyi kritik dəyişiklik sayılır. Layihə strukturunu dəyişmək lazımdırsa, əl ilə məlumat miqrasiyasından yan keçmək olmur.

4. transient və static: nə seriyalaşdırılır, nə isə yox?

  • static sahələr ümumiyyətlə seriyalaşdırılmır — onlar sinfə məxsusdur, obyektə yox.
  • transient sahələr müvəqqəti məlumatları işarələyir və seriyalaşdırmaya düşməməlidir (məsələn, keş, müvəqqəti tokenlər).

Nümunə:

public class Session implements Serializable {
    private static final long serialVersionUID = 1L;
    private String user;
    private transient String sessionToken; // seriyalaşdırılmır
}

Deserializasiya zamanı sessionToken null olacaq, hətta seriyalaşdırmadan əvvəl obyekt daxilində o doldurulmuş olsa belə.

5. Fərdi seriyalaşdırma: writeObject/readObject

Əgər daha mürəkkəb uyğunluq məntiqinə ehtiyacınız varsa (məsələn, köhnə sahələri yenilərinə çevirmək, dəyişmiş tipləri işləmək), xüsusi metodlar reallaşdıra bilərsiniz:

private void writeObject(ObjectOutputStream out) throws IOException {
    out.defaultWriteObject();
    // Lazım gələrsə əlavə məntiq
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    // Əlavə məntiq, məsələn, yeni sahəni köhnələrin əsasında doldurmaq
}

Təkamül nümunəsi:

public class User implements Serializable {
    private static final long serialVersionUID = 2L;
    private String name;
    private int age; // əvvəllər String birthYear idi

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        // Əgər birthYear sahəsi mövcud idisə, onu age-ə çevirmək
        // (əgər birthYear-ı transient kimi saxlayırsınızsa, kod nümunəsi)
    }
}

6. XML və JSON-da uyğunluq: mətn formatlarının elastikliyi

Binar seriyalaşdırmadan fərqli olaraq, XML və JSON formatları sinif strukturundakı dəyişikliklərə xeyli dözümlüdür.

XML (JAXB) və JSON (Jackson, Gson)

Binar seriyalaşdırmadan fərqli olaraq, XML və ya JSON ilə işləyərkən deserializasiya xeyli yumşaq davranır. Əgər məlumatlarda sinfinizdə olmayan sahə rast gəlinərsə, o sadəcə ignor edilir. Sinfinizdə olan, amma mənbə məlumatlarında olmayan yeni sahələr isə defolt dəyərlər alır — adətən obyektlər üçün null və ya ədədlər üçün 0. Elementlərin ardıcıllığı əhəmiyyət daşımır, beləliklə teqləri və ya açarları yerini dəyişsəniz də, hər şey düzgün parse olunacaq.

Annotasiyalar tam nəzarət verir: faylda hansı adın istifadə olunacağını, hansı sahələrin məcburi, hansının isə ötürülə biləcəyini göstərə, hətta formatlaşdırmanı qura bilərsiniz. Məsələn, JAXB-də User sinfi belə görünə bilər:

public class User {
    @XmlElement(required = true)
    private String name;

    @XmlElement
    private String email; // yeni sahə, məcburi deyil
}

JSON üçün Jackson və ya Gson ilə təxminən belə:

public class User {
    @JsonProperty("name")
    private String name;

    @JsonProperty("email")
    private String email; // yeni sahə
}

Nəticə xoşdur: köhnə JSON və ya XML-fayllar sakitcə yüklənir, yeni sahələr sadəcə null alır, məlumatdakı artıq sahələr isə nəzərə alınmır. Sinfin strukturunu rahatlıqla dəyişə bilərsiniz, köhnə saxlamaları sındırmaqdan qorxmadan.

Nə vaxt əlavə nəzarət lazımdır?

Nəzarət xüsusən sahəni məcburi etdiyiniz zaman vacibdir. Əgər köhnə məlumatlarda bu sahə yoxdursa, deserializasiya xəta verəcək. Tip dəyişikliklərinə də eyni cür aiddir: əgər əvvəl sahə sətir idisə, siz onu ədədə çevirmisinizsə, köhnə məlumatlar parse olunmaya bilər. Buna görə belə dəyişikliklərdən əvvəl onların mövcud saxlamalara necə təsir edəcəyini yoxlamaq və zərurət olduqda miqrasiya hazırlamaq və ya defolt dəyərlər təyin etmək lazımdır.

7. Uyğunluğu təmin etmə strategiyaları

  • Mütləq açıq şəkildə elan edin serialVersionUID. Bu, binar seriyalaşdırma üçün uyğunluğa nəzarətin əsas yoludur.
  • Yalnız məcburi olmayan sahələr əlavə edin. Yeni sahələr ya null olmalı, ya da defolt dəyərə malik olmalıdır.
  • İstifadə edin transient müvəqqəti və ya önəmsiz məlumatlar üçün. Belə sahələr seriyalaşdırmaya düşməyəcək və sinfin təkamülü zamanı problem yaratmayacaq.
  • Dəyişiklikləri sənədləşdirin. Sinifin şərhində hansı sahələrin əlavə/silinməsi və bunun hansı versiyadan etibarən olduğu qeyd edilsin.
  • Mürəkkəb hallarda — writeObject/readObject. Məlumatların “uçuşda” miqrasiyasını həyata keçirməyə imkan verir.
  • Sxemlərdən istifadə edin (XML Schema, JSON Schema) kritik məlumatlar üçün. Bu, məlumat strukturunu açıq təsvir etməyə və yükləmə zamanı onu yoxlamağa kömək edir.

8. Təcrübə: uyğunsuzluq və təkamülün nümayişi

Uyğun gəlməyən serialVersionUID halında xəta nümayişi

// Əvvəlcə sinfin bir versiyası ilə obyekti seriyalaşdırırıq
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
}

// Sonra serialVersionUID-i dəyişirik (məsələn, 2L), kompilyasiya edirik və köhnə faylı yükləməyə cəhd edirik
public class User implements Serializable {
    private static final long serialVersionUID = 2L;
    private String name;
}

Nəticə:

java.io.InvalidClassException: User; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2

Sinfin uğurlu təkamülü nümunəsi

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    // yeni sahə
    private String email;
}

Əgər köhnə obyekti ( email olmadan) seriyalaşdırıb sonra sahəni əlavə etsəniz və serialVersionUID-i dəyişməsəniz, deserializasiya işləyəcək, email null olacaq.

9. Seriyalaşdırma uyğunluğu ilə işləyərkən tipik səhvlər

Səhv №1: serialVersionUID elan olunmayıb. Əgər serialVersionUID-i açıq şəkildə elan etməsəniz, JVM onu avtomatik generasiya edəcək. Sinfin ən kiçik dəyişiklikləri (məsələn, yeni metod əlavə etdiniz və ya sahənin modifikatorunu dəyişdiniz) serialVersionUID-in dəyişməsinə və nəticədə köhnə məlumatların deserializasiya olunmamasına gətirib çıxaracaq. Bu, backward compatibility-ni “sındırmağın” klassik yoludur.

Səhv №2: Sahənin tipini dəyişmək. Sahənin tipini dəyişdiniz (məsələn, int-dən String-ə) — istisna və ya qeyri-düzgün məlumat alacaqsınız. Belə dəyişikliklər xüsusi ehtiyat tələb edir, ən yaxşısı — əl miqrasiyası ilə writeObject/readObject.

Səhv №3: Sinfin/paketin silinməsi və ya adının dəyişdirilməsi. Sinfin adını dəyişmək və ya paketi dəyişmək köhnə obyektlərin deserializasiya olunmamasına gətirib çıxarır. Sinfin adı və paket seriyalaşdırılmış axında saxlanılır və JVM onları uyğunlaşdıra bilməyəcək.

Səhv №4: transient-dən sui-istifadə. Əgər vacib sahəni transient etsəniz (məsələn, istifadəçi id-si), o seriyalaşdırılmayacaq və obyekt bərpa olunanda dəyər itəcək.

Səhv №5: Kolleksiyaların uyğunsuz dəyişdirilməsi. Yeni kolleksiya-sahə əlavə etdiniz və ya kolleksiyanın tipini dəyişdiniz (məsələn, List-dən Set-ə) — köhnə məlumatlar səhv deserializasiya oluna və ya xəta yarada bilər.

Səhv №6: XML/JSON-da həddən artıq sərt məhdudiyyətlər. Əgər XML/JSON-sxemdə sahəni məcburi (required = true) kimi işarələsəniz, amma köhnə məlumatlarda o yoxdursa, yükləmə xəta ilə bitəcək. Annotasiyalar və sxemlərlə diqqətli olun!

Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION