CodeGym /Kurslar /JAVA 25 SELF /Dövrü istinadlar problemi: aşkarlanması, yan keçmə

Dövrü istinadlar problemi: aşkarlanması, yan keçmə

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

1. Dövrü istinadlar nədir?

Dövrü istinad — obyektin (və ya kolleksiyanın) birbaşa və ya dolayı yolla özünə istinad saxlamasıdır. Kolleksiyalarda bu, göründüyündən daha çox rast gəlinir, xüsusilə mürəkkəb verilənlər strukturları qurduğunuzda və ya qraflarla işlədikdə.

Həyatdan nümunələr

  • İki obyekt bir-birinə istinad edir:
    Məsələn, sizdə sinif User var və onda Profile-a istinad var, Profile-da isə əks istiqamətdə User-ə istinad var.
  • Kolleksiya özünü ehtiva edir:
    Ən sadə və “maraqlı” nümunə:
List<Object> list = new ArrayList<>();
list.add(list); // Hop! list özünü ehtiva edir
  • Obyektlər qrafı:
    Bir-biri ilə əlaqəli obyektlər; məsələn, hər düyünün valideynə və övladlara istinadı ola bilən ağac düyünləri.

Vizualizasiya

graph LR
A[User] -- profile --> B[Profile]
B -- user --> A

Və ya kolleksiya üçün:

graph TD
L[List] -- add(self) --> L

Bu niyə problem ola bilər?

Əgər serializator dövrələri izləməyi bacarmırsa, yuvallanmış obyektləri yenidən-yenidən serializə etməyə çalışaraq “sonsuzluğa gedə” bilər və nəhayət stack-i doldurar (StackOverflowError). Yaxşı xəbər: Java-nın standart serializasiyası bu hiylələri tanıyır və onlardan yan keçməyi bacarır!

2. Java-nın standart serializasiyası dövrələrlə necə işləyir?

Siz obyekti ObjectOutputStream vasitəsilə serializə etdikdə, Java həmin axında artıq hansı obyektlərin serializə olunduğunu avtomatik izləyir. Serializator obyekti təkrar görərsə, onu yenidən serializə etmir, artıq serializə olunmuş obyektə xüsusi istinad yazır. Bu, dövrələri olan çox mürəkkəb strukturları belə düzgün serializə etməyə imkan verir.

Nümunə: özünü ehtiva edən kolleksiya

Gəlin, özünü ehtiva edən bir kolleksiyanı serializə etməyə cəhd edək. Bu, zarafat deyil — belə kod kompilyasiya olunur və hətta işləyir:

import java.io.*;
import java.util.*;

public class CyclicListDemo {
    public static void main(String[] args) throws Exception {
        List<Object> list = new ArrayList<>();
        list.add("Hello, cyclic world!");
        list.add(list); // Özünü əlavə edirik

        // Serializasiya
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cyclic_list.ser"))) {
            out.writeObject(list);
        }

        // Deserializasiya
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("cyclic_list.ser"))) {
            List<?> deserialized = (List<?>) in.readObject();

            System.out.println(deserialized.get(0)); // "Hello, cyclic world!"
            System.out.println(deserialized.get(1) == deserialized); // true!
        }
    }
}

Nəticə:
— Birinci element — adi sətirdir.
— İkinci element — bu... elə həmin kolleksiya! Yoxlama deserialized.get(1) == deserialized true qaytaracaq.
Java döngüyə düşmədi və çökmədi, istinadlar strukturunu düzgün bərpa etdi.

Bu daxildə necə işləyir?

ObjectOutputStream serializə olunmuş obyektlərin daxili “reyestr”ini saxlayır. Obyekt artıq serializə olunubsa, axına onun məzmunu deyil, ona xüsusi istinad (handle) yazılır. Deserializasiya zamanı ObjectInputStream həmin əlaqələri eyni qaydada bərpa edir.

3. Problemlər və məhdudiyyətlər

  • Təsadüfən nəhəng qrafı serializə etmisiniz.
    Verilənlər strukturunuz çox böyükdürsə və çoxlu çarpaz istinadlar ehtiva edirsə, serializasiya çox vaxt apara və nəhəng fayl yarada bilər.
  • Siniflərin strukturunun dəyişdirilməsi.
    Obyekti serializə etdikdən sonra onun sinfini dəyişsəniz (məsələn, sahə əlavə və ya silsəniz), deserializasiya zamanı InvalidClassException yarana bilər. Xüsusilə də dövrədə iştirak edən sahələr dəyişdirilirsə.
  • Kastom serializasiyada problemlər.
    Əgər siz writeObject və readObject metodlarını əl ilə reallaşdırırsınızsa, dövrələri düzgün emal etməlisiniz. Susmaya görə metodları (defaultWriteObject/defaultReadObject) çağırmağı unutsanız, serializator dövrələri izləyə bilməyəcək.
  • Başqa formatlara (məsələn, JSON) serializasiya.
    Java-nın standart serializasiyası (ObjectOutputStream) dövrələrlə bacarır, lakin obyektləri JSON-a serializə edirsinizsə (məsələn, Jackson və ya Gson ilə), dövrələr StackOverflowError-a və ya istisnalara səbəb ola bilər. Belə kitabxanalar susmaya görə dövrələrlə işləmirlər — açıq sazlamalar tələb olunur.

4. Dövrü istinadlardan yan keçmə

Java-nın standart serializasiyasında

Hər şey “qutudan çıxan kimi” işləyir! Xüsusi heç nə etməyə ehtiyac yoxdur — Java dövrələri özü aşkarlayacaq və istinadlar strukturunu qoruyacaq.

Əl ilə: başqa formatlara serializasiya

  • İstinadlar əvəzinə identifikatorlardan istifadə edin.
    Digər obyektlərə istinadları saxlamaq əvəzinə onların unikal identifikatorlarını saxlayın. Deserializasiyadan sonra əlaqələri həmin id-lərə görə bərpa edin.
  • Xüsusi annotasiyalar və ya sazlamalar.
    Jackson-da dövrələrin serializasiyasını idarə etmək üçün annotasiya @JsonIdentityInfo-dan və ya cütlük @JsonBackReference/@JsonManagedReference-dən istifadə etmək olar.
  • Serializasiyadan əvvəl dövrələri aradan qaldırın.
    Dövrə yaradan sahələri müvəqqəti olaraq null edin, onları transient və ya annotasiyalar vasitəsilə istisna edin.

Nümunə: dövrələri olan qrafın serializasiyası

Daha mürəkkəb bir struktur nümunəsinə baxaq — hər istifadəçinin başqa istifadəçinin dostu ola bildiyi istifadəçilər qrafı.

import java.io.*;
import java.util.*;

class User implements Serializable {
    String name;
    List<User> friends = new ArrayList<>();

    User(String name) { this.name = name; }

    public String toString() {
        return name + " (" + friends.size() + " friends)";
    }
}

public class CyclicGraphDemo {
    public static void main(String[] args) throws Exception {
        User alice = new User("Alice");
        User bob = new User("Bob");
        User charlie = new User("Charlie");

        // Dövrələri olan dostluqlar yaradırıq
        alice.friends.add(bob);
        bob.friends.add(charlie);
        charlie.friends.add(alice); // dövrə!

        // Serializasiya
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("users.ser"))) {
            out.writeObject(alice);
        }

        // Deserializasiya
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("users.ser"))) {
            User restoredAlice = (User) in.readObject();
            System.out.println(restoredAlice);
            System.out.println(restoredAlice.friends.get(0));
            System.out.println(restoredAlice.friends.get(0).friends.get(0));
            System.out.println(restoredAlice.friends.get(0).friends.get(0).friends.get(0) == restoredAlice); // true!
        }
    }
}

Nəticə:
— Dövrəsi olan struktur bərpa olunur: dostlar üzrə üç keçiddən sonra yenidən Alice-ə qayıdırıq.
— Java çaşmadı və döngüyə düşmədi.

5. Dövrü istinadlarla işləyərkən tipik səhvlər

Səhv №1: Dövrələr dəstəyi olmadan JSON-a serializasiya. Əgər dövrələri olan obyekti Jackson və ya Gson vasitəsilə sazlamasız JSON-a serializə etsəniz, çox güman ki, StackOverflowError alacaqsınız. Məsələn, sinif Node-da hər düyün valideynə və övladlara istinad edirsə, belə ağacın JSON-a serializasiyası sonsuz iç-içəliklə nəticələnəcək.

Səhv №2: Sinif strukturunun pozulması. Serializasiyadan sonra sinfin strukturunu dəyişsəniz (məsələn, sahə əlavə etsəniz), köhnə faylı deserializə edərkən uyğunsuzluq xətası yarana bilər. Bu, xüsusilə dövrələri olan mürəkkəb qraflar üçün kritikdir.

Səhv №3: Dövrələr nəzərə alınmadan “özəl” serializasiya. Əgər siz writeObject/readObject-i əl ilə reallaşdırır və defaultWriteObject-u çağırmırsınızsa, Java dövrələri izləyə bilməyəcək, nəticədə ya serializasiya döngüyə düşəcək, ya da deserializasiya zamanı istinadlar strukturu pozulacaq.

Səhv №4: Kolleksiyanın təsadüfən özünü ehtiva etməsi. Bəzən təcrübəsiz tərtibatçılar təsadüfən kolleksiyanı özünə əlavə edirlər (məsələn, elementləri kopyalayarkən) və bununla dövrə yaradırlar. Nəticədə serializasiya işləyəcək, lakin proqramın məntiqi qəribə və qeyri-proqnozlaşdırılan ola bilər.

1
Tapşırıq
JAVA 25 SELF, səviyyə, dərs
Bağlanıb
Paradoksal siyahı: özünü ehtiva edən kolleksiya 🤯
Paradoksal siyahı: özünü ehtiva edən kolleksiya 🤯
1
Tapşırıq
JAVA 25 SELF, səviyyə, dərs
Bağlanıb
Şəbəkə topologiyasının saxlanması: dövri marşrutlu qrafın seriyalaşdırılması 🌐
Şəbəkə topologiyasının saxlanması: dövri marşrutlu qrafın seriyalaşdırılması 🌐
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION