CodeGym /Kurslar /JAVA 25 SELF /Generics-lə tanışlıq

Generics-lə tanışlıq

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

1. Giriş

Təsəvvür edin ki, nə istəsəniz içinə qoya biləcəyiniz universal bir qutunuz var: alma, kitab və ya hətta oyuncaq. Java-da belə “universal qutu” — ən ümumi tip olan Object tipində məlumat saxlayan bir sinifdir. Bu tip Java-dakı digər bütün siniflərin valideynidir, buna görə də Object tipli dəyişəndə istənilən obyekt saxlanıla bilər.

İndi bu cür qutularla dolu bir anbar təsəvvür edin. Qutuların üzərində etiket yoxdursa, ora hər nə istəyirsiniz qoya bilərsiniz, ancaq nəsə çıxarmaq lazım olanda — qutunu açıb içində nə olduğunu təxmin etməli olacaqsınız. Generiklərlə isə vəziyyət üzərində səliqəli yazılar olan bir anbar kimidir: “Yalnız almalar”, “Yalnız kitablar”, “Yalnız alətlər”. Artıq hər qutuda nə olduğunu hər zaman bilirsiniz və təsadüfən kitabı alma qutusuna qoya bilməzsiniz.

İlk baxışda Object-də saxlamaq rahat görünə bilər: müxtəlif verilən tipləri üçün ayrıca siniflər yaratmağa ehtiyac qalmır. Ancaq praktikada belə “universallıq” problemlərə səbəb olur:

  1. Səhv etmək asandır. Qutuya gözlədiyinizdən fərqli bir obyekti təsadüfən qoya bilərsiniz.
  2. Kompilyator problemi görməyəcək. Çünki Object tipi buna icazə verdiyindən, o sizə istənilən şeyi qoymağa icazə verəcək.
  3. Əl ilə “açıb-çıxarmaq” lazımdır. Belə qutudan obyekt çıxardığınızda, o yenə Object tipində olacaq və siz onu lazım olan tipə özünüz çevirməli olacaqsınız (bu tipə çevirmə və ya cast adlanır). Əgər tipi səhv seçsəniz, proqram sadəcə xəta ilə dayanacaq!

Gəlin bunu sadə bir nümunədə görək.

class Box {
    private Object value;

    public void set(Object value) {
        this.value = value;
    }

    public Object get() {
        return value;
    }
}

İndi bu qutudan istifadə edək:

Box box = new Box();
box.set("Salam"); // Sətir yerləşdirdik
String s = (String) box.get(); // Sətiri çıxartdıq, hər şey qaydasındadır

box.set(123); // Ədəd yerləşdirdik
// Kompilyator problemi görmür...
String t = (String) box.get(); // Proqramın icrası zamanı səhv!

Gördüyünüz kimi, kompilyator sonda xətaya səbəb olan kodu rahatlıqla buraxdı. Problemi yalnız proqram işə salınanda və “çökəndə” bildik.

2. Həll — generiklər

Generics (generiklər) bu problemi həll etməyin yoludur. Bu, sinfi və ya metodu daha kompilyasiya mərhələsində konkret verilən tipinə bağlamağa imkan verən xüsusi sintaksisdir. Başqa sözlə, bu, qutunun üzərində “Bu qutuda yalnız sətirlər ola bilər” və ya “Bu qutuda yalnız ədədlər ola bilər” deyən etiket kimidir.

Beləliklə, biz tip təhlükəsizliyi əldə edirik: kompilyator “qutu”ya səhv obyekt qoymağa imkan vermir. O, kodumuzu yoxlayır və proqramı işə salmadan əvvəl xətanı göstərir.

Eyni Box sinfi, amma indi generiklərlə:

class Box<Type> {
    private Type value;

    public void set(Type value) {
        this.value = value;
    }

    public Type get() {
        return value;
    }
}

Burada Typetip parametridir. Bu, özümüzün seçdiyimiz şərti addır (adətən T, E, K, V istifadə olunur) və belə deməkdir: “Box yaradanda onun hansı tiplə işləyəcəyini göstərəcəyik və kodda Type yazılan hər yerdə həmin tipdən istifadə edəcəyəm”.

3. Generics-dən istifadə

Generikləri olan bir sinifdən obyekt yaradanda, bucaqlı mötərizələrdə <...> konkret tipi göstəririk.

// Yalnız sətirlərlə işləyən bir qutu yaratdıq
Box<String> stringBox = new Box<>();
stringBox.set("Salam, dünya!"); // OK, sətir yerləşdirdik
String s = stringBox.get(); // Tip çevirməsi olmadan sətiri çıxartdıq

stringBox.set(123); // Kompilyasiya xətası! Kompilyator buna icazə verməyəcək.

Əgər Box<Integer> yaratsaq, kompilyator ora yalnız ədədlərin qoyulmasına nəzarət edəcək:

// Yalnız ədədlərlə işləyən bir qutu yaratdıq
Box<Integer> intBox = new Box<>();
intBox.set(42); // OK, ədəd yerləşdirdik
Integer number = intBox.get(); // Tip çevirməsi olmadan ədədi çıxartdıq

intBox.set("Salam"); // Kompilyasiya xətası!

İndi kompilyator hər qutuda hansı tipin olmalı olduğunu dəqiq bilir və bizi səhvlərdən etibarlı şəkildə qoruyur.

4. Bu necə işləyir: nümunələr

Generiklər təkcə siniflərdə deyil, metodlarda da istifadə oluna bilər. Bu, çox çevik və universal kod yazmağa imkan verir.

Nümunə 1 — generiklərlə sinif

Tutaq ki, eyni tipdən iki obyekt saxlayan Pair sinfinə ehtiyacımız var. Generiklərlə bu belə görünür:

class Pair<T> {
    private T first;
    private T second;

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public T getSecond() {
        return second;
    }
}

İstifadə:

Pair<String> greetings = new Pair<String>("Salam", "Dünya");
System.out.println(greetings.getFirst() + " " + greetings.getSecond());

Pair<Integer> numbers = new Pair<Integer>(10, 20);
System.out.println(numbers.getFirst() + numbers.getSecond());

Birinci halda kompilyator greetings-də sətirlərin, ikinci halda isə ədədlərin olduğunu bilir.

Nümunə 2 — universal metod

İstənilən verilən tipilə işləyən bir metod yaza bilərik.

class Utils {
    // <T> void-dən əvvəl bu metodun T tip parametri ilə işləyəcəyini göstərir
    public static <T> void printTwice(T value) {
        System.out.println(value);
        System.out.println(value);
    }
}

İndi bu metodu istənilən verilən tipilə çağıra bilərik və o, eyni cür işləyəcək:

Utils.printTwice("Java");
Utils.printTwice(123);
Utils.printTwice(3.14);

5. Generiklərin üstünlükləri

Generiklər sadəcə sintaktik “şəkər” deyil, real problemləri həll edən güclü bir alətdir. Gəlin üç əsas üstünlüyə baxaq.

Tip təhlükəsizliyi — generiklərin əsas üstünlüyüdür. Onlar olmadan kompilyator “universal” sinif və metodlarınızda verilən tiplərindən düzgün istifadə edib-etmədiyinizi yoxlaya bilmir. “Rəqəmlər üçün qutuya” sətir qoymaq kimi səhvlər yalnız proqram icra zamanı, o qəfil ClassCastException istisnası ilə “çökəndə” üzə çıxır. Generiklərlə kompilyator tiplərə ciddi nəzarət edir. O, Box<String>-ə yalnız sətirlərin, Box<Integer>-ə isə yalnız ədədlərin qoyulduğunu yoxlayır. Yanlış bir şey etməyə cəhd etsəniz, dərhal xətanı göstərəcək və siz onu proqramı işə salmazdan əvvəl düzəldə biləcəksiniz. Bu, kodunuzu xeyli daha etibarlı və proqnozlaşdırılan edir.

Təmiz kod (artıq tip çevirmələri olmadan). “Universal” qutu ilə generiklərsiz kodumuzun necə göründüyünü xatırlayın: Box box = new Box(); box.set("Salam"); String s = (String) box.get(); Qutudan obyekt çıxaranda hər dəfə (String), (Integer) və s. yazmalı idiniz. Generiklərlə bu zərurət aradan qalxır. Kompilyator artıq içəridə hansı tipin olduğunu bilir və sizin yerinizə avtomatik uyğunlaşdırır: Box<String> stringBox = new Box<>(); stringBox.set("Salam"); String s = stringBox.get(); Bu, kodu yalnız qısaltmır, həm də oxunaqlılığını və rahatlığını xeyli artırır.

Çeviklik və kodun təkrar istifadəsi. Generiklər, tip təhlükəsizliyini itirmədən müxtəlif verilən tipləri ilə işləyən həqiqətən universal sinif və metodlar yaratmağa imkan verir. Məsələn, Box<T> sinfini sətirləri, ədədləri, öz siniflərinizi (Student, Car) — nə istəyirsinizsə saxlamaq üçün istifadə edə bilərsiniz! Ayrı-ayrılıqda StringBox, IntegerBoxStudentBox yazmağa ehtiyac yoxdur. Bir universal Box<T> yazırsınız və obyekt yaradanda lazım olan tipi göstərirsiniz. Bu, kod miqdarını azaldır, təkrarı aradan qaldırır və proqramınızı daha modular və çevik edir.

6. Generiklərin məhdudiyyətləri

Bütün üstünlüklərinə baxmayaraq, generiklərin bilməli olduğunuz bir neçə vacib məhdudiyyəti var.

Primitivlər (primitives). int, double, boolean və s. kimi primitiv tipləri tip parametri kimi istifadə edə bilməzsiniz. Məsələn, Box<int> intBox = new Box<>(); kodu kompilyasiya xətası verəcək. Bunun əvəzinə onların “wrapper sinifləri”ni (Integer, Double, Boolean) istifadə etmək lazımdır: Box<Integer> intBox = new Box<>();. Java kompilyatoru primitivləri wrapper siniflərə və əksinə avtomatik çevirməyi bacarır (intInteger və əksinə) — bu mexanizm autoboxing/unboxing adlanır.

Tiplərin silinməsi (Type Erasure). Bu, Java-da generiklərin necə reallaşdırılmasının əsas xüsusiyyətidir. Fikir ondadır ki, Java kompilyatoru generiklər haqqında məlumatdan yalnız kompilyasiya zamanı istifadə edir. Kodu yoxlayıb tip təhlükəsizliyinə əmin olduqdan sonra tip parametrləri ilə bağlı bütün məlumatları silir. Məsələn, Box<String>Box<Integer> kompilyasiyadan sonra (bayt-kodda) sadəcə Box kimi görünəcək. Bu o deməkdir ki, Java Virtual Maşını (JVM) üçün onlar eyni tipə çevrilir.

Bu, sizin üçün nə deməkdir? Siz generiklərdən ibarət massiv yarada bilməzsiniz, məsələn new Box<String>[10], və generiklərlə instanceof istifadə edib obyektin instanceof Box<String> olub-olmadığını yoxlaya bilməzsiniz. Bu, daha mürəkkəb mövzudur, lakin Java-nı dərindən öyrənmək üçün vacibdir. Bu mərhələdə yadda saxlamaq kifayətdir ki, generiklər mahiyyətcə kompilyator üçün “etiket”dir: o, kodu yoxlamağa kömək edir, lakin proqram işə hazır olanda bu etiket çıxarılır.

1
Sorğu/viktorina
, səviyyə, dərs
Əlçatan deyil
Yerləşdirilmiş və daxili siniflər
Yerləşdirilmiş və daxili siniflər
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION