OCA Java imtahan mövzuları

Overriding a Method

Metodu override etmək – child classda parent classdakı metodla eyni signature (ad və parametr siyahısı) və geri dönüş tipində olan yeni metod yaratmaqdır. Metodu override etdikdən sonra artıq superthis açar sözlərindən istifadə edərək metodun cari yaxud parent versiyasından istifadə etmək istədiyimizi təyin edə bilərik:

class Dog {
    
    public int getAge() {
        return 8;
    }
}

class Husky extends Dog {
    
    public int getAge() {
        return super.getAge() + 3;  // əgər super`i silsək, sonsuz dövrə düşəcək  
    }
    
    public static void main(String[] args) {
        System.out.println(new Dog().getAge());
        System.out.println(new Husky().getAge());
    }
}

Output:

  8
  11

 

Düzgün override olunmuş metod aşağıdakı qaydalara riayət etməlidir:

  1. Child classdakı metod parent classdakı metod ilə eyni quruluşa (the same signature) malik olmalıdır. Yəni, metod adları və parametr siyahısı mütləq eyni olmalıdır;
  2. Child classdakı metodun access modifier`i parent classdakı metodla ən azı eyni olmalıdır, ya da ki, daha yüksək;
  3. Parent classdakı metodun fırlatdığı (throws) exceptionun tipi həmişə child classdakı metoddan daha geniş olmalıdır. Child classdakı metod maksimum halda eyni exception tipini fırlada, yaxud da kiçik tip, və yaxud da heç exception fırlatmaya da bilər. Amma burada söhbət checked exceptionlardan gedir, runtime exceptionlara bu məhdudiyyət tətbiq olunmur;
  4. Hər iki metodun geriyə döndürdüyü dəyər eyni tipdə olmalıdır və ya override edilən/parent classdakı metodun subclassı (covariant return types).

  private metodlar override olunmur.
**  Bu qaydalara nümunələr üzərində baxdıqda daha aydın olacaq.

 

Bu 4 qayda ilə bağlı nümunələrə keçməzdən öncə private metodların override edilməməsi məsələsinə bir az aydınlıq gətirək. Bəli, parent classdakı private metodlar override olunmur, hiding olur. Yəni əgər həmin metod parent classda çağırılırsa, parent classdakı metod işləyir, child classda çağırılırsa, child classdakı metod. Nümunə üzərindən baxaq:

public abstract class Bird {
    private void fly() {
        System.out.println("Bird is flying");
    }
    public static void main(String[] args) {
        Bird bird = new Pelican();
        bird.fly();
    }
}

class Pelican extends Bird {
    protected void fly() {
        System.out.println("Pelican is flying");
    }
}

Nümunənin override ilə bağlı izahına keçməzdən əvvəl kodla bağlı bir maraqlı məqamı qeyd edim ki, əgər biz Pelican classını run etsək həmin classın daxilində main metod olmamasına baxmayaraq class run olacaq. Səbəb isə varisliklə bağlıdır. Əgər Pelican`ın superclassının daxilində main metod varsa, biz Pelican classını run etdikdə o birbaşa gedib Bird classının daxilindəki main metodu çağırır.

İndi qayıdaq nümunəmizin override ilə bağlı hissəsinə. Hər iki classda fly() metodu var və override olunmanın demək olar ki, bütün qaydalarına riayət edilir. Amma burada az öncə qeyd etdiyimiz istisna var, parent classdakı metod private`dır, ona görə də bu override hesab olunmur, hiding hesab olunur. Kodu run etsək nəticə belə olacaqdır:

   Bird is flying

Çünki fly() metodu parent classda çağırılır. Biz əgər Pelican classındakı fly() metodunu commentə salsaq kod yenə compile olunacaq və run etdikdə eyni nəticəni alacağıq. Amma Bird classındakı fly() metodunu commentə salsaq kod compile olunmayacaqdır. Çünki bird referansı Bird classına aiddir və o compile vaxtı Bird classında belə bir metodun olmadığını aşkarlayır və xəta verir. Əgər Bird bird = new Pelican(); əvəzinə Pelican bird = new Pelican(); yazsaq kod compile olunacaq və nəticə  Pelican is flying  olacaqdır.

Əgər biz main metodunu Bird classından çıxarıb Pelican classında yazsaq, kod yenə compile olunmayacaqdır. Çünki Bird classındakı fly() metodu private`dır və subclass onu görə bilmir:

public abstract class Bird {
    private void fly() {
        System.out.println("Bird is flying");
    }
}

class Pelican extends Bird {
    protected void fly() {
        System.out.println("Pelican is flying");
    }
    public static void main(String[] args) {
        Bird bird = new Pelican();
        bird.fly();    // DOES NOT COMPILE
    }
}

4-cü bənddə “covariant return types” ifadəsi işlətdik, bunu biraz daha geniş izah edək. Deməli, bu xüsusiyyət Java`ya 1.5-ci versiyadan sonra gəlib. Anlamı isə odur ki, override edən (overriding) metodun return tipi override edilən (overridden) metodun return tipinin subclassı ola bilər. Yəni, əgər super classdakı metodun return tipi Number olarsa, subclassdakı metodun return tipi Integer (yaxud Number classından törənmiş hər hansı bir başqa class) ola bilər.

Amma bu qayda primitiv tiplər üçün keçərli deyil. Misal üçün, əgər override edən (overriding) metodun return tipi int olarsa, override edilən (overridden) metodun return tipi də mütləq int olmalıdır. short, long və yaxud Integer ola bilməz:

class Super {
    public Number getNumber() {
        return 5;
    }
    public int getInt() {
        return 6;
    }
}

class CovariantReturnTypes extends Super { 
    public Integer getNumber(){  // OK
        return 7;
    }
    public short getInt(){       // DOES NOT COMPILE 
        return 8;
    } 
}

Overriding ilə overloading bir-birinə çox bənzəyir, çünki hər ikisində də metod adları eynidir. Amma overriding`dən fərqli olaraq overloading həmişə fərqli metod signature istifadə edir:

class Bird {
    
    public void fly() {
        System.out.println("Bird is flying");
    }
    
    public int eat(int food) {
        System.out.println("Bird is eating " + food + " units of food");
        return food;
    }
}

public class Eagle extends Bird {
    
    public int fly(int height) {  // Everything is ok, because of overloading
        System.out.println("Bird is flying at " + height + " meters");
        return height;
    }

    public void eat(int food) {  // DOES NOT COMPILE, because of overriding, return type is different
        System.out.println("Bird is eating " + food + " units of food");
    }
}

Əgər imtahanda child və parent classda eyni adlı metod görsəniz, ilk növbədə onun override yaxud overload metod olduğunu təyin etmək, sualı tez tapmaq baxımından daha faydalı olacaqdır.

İndi isə qeyd etdiyimiz 4 qayda ilə bağlı nümunələrə baxaq.

Nümunə 1:

public class Camel {
    protected String getNumberOfHumps() {
        return "Undefined";
    }
}

class BactrianCamel extends Camel {
    private int getNumberOfHumps() {   // DOES NOT COMPILE
        return 2;
    }
}

2-ci4-cü qayda pozulur burada. Child classdakı metodun access modifier`i ancaq protectedpublic ola bilər. Geri dönüş tipi isə birində String`dir, digərindi int.

Nümunə 2:

class MyException extends Exception {}

class Reptile {

    protected boolean hasLegs() throws MyException {
        throw new MyException();
    }

    protected double getWeight() throws Exception {
        return 2;
    }
}

class Snake extends Reptile {

    protected boolean hasLegs() {
        return false;
    }

    protected double getWeight() throws MyException {
        return 2;
    }
}

Reptile classındakı hər iki metod Snake classında override edilib və bütün qaydalara riayət edilib, xüsusilə də 3-cü qaydanın tələbləri pozulmayıb. Əgər nümunə aşağıdakı kimi olsa idi, o zaman qaydalar pozulmuş olardı:

class MyException extends Exception {}

class Reptile {

    protected double getHeight() throws MyException {
        return 2;
    }

    protected int getLength() {
        return 5;
    }
}

class Snake extends Reptile {

    protected double getHeight() throws Exception {  // DOES NOT COMPILE
        return 2;
    }

    protected int getLength() throws MyException {   // DOES NOT COMPILE
        return 5;
    }
}

İndi isə daha qarışıq sual nümunələrinə baxaq. Aşağıda qeyd edəcəyim nümunə Enthuware testləri içərisində ən maraqlı və ən yaxşı suallardan biridir. Buna bənzər bir nümunəni Order of Initialization mövzusunda qeyd etmişəm, bu suala isə bu məqalədə ayrıca baxaq:

class A {

    A() { print(); }
    
    void print(){ System.out.println("A"); }
}

class B extends A {

    int i = 4;

    public static void main(String[] args) {
        A a = new B();
        a.print();
    }

    void print() { System.out.println(i); }
}

B classı run edildikdə output`un nə olacağı soruşulur və cavab variantları belədir:

A) It will print A, 4
B) It will print A, A
C) It will print 0, 4
D) It will print 4, 4
E) None of the above.

Cavab variantları da bir-birinə çox yaxındır, ona görə də sual maksimum həssaslıq və diqqət istəyir. O vaxtkı qeydlərimə baxıram, Standart Test`ləri edərkən özüm də bu sualı səhv cavablandırmışam 🙂 Mənim seçdiyim cavab A variantı idi, mən hesab edirdim ki, A classının constructoru çağırılarkən icra edilən print() metodu A classına məxsus print() metodu olacaq (hətta indi də suala çox baxdıqda bunu qarışdırıram 😀 ), amma override özəlliyindən dolayı B classının print() metodu icra edilir, buna diqqət etmək lazımdır.

Artıq bildik ki, A classının print() metodu çağırılmır, bu səbəbdən də AB variantlarını çıxara bilərik, geriyə qalır 3 variant. Kömək üçün qeyd edim ki, E variantını da çıxara bilərik, cavablar arasında düzgün variant var. Geriyə qalır CD variantları. Amma sualın ən maraqlı hissəsi hələ qabaqdadır, işi bitmiş hesab etmək olmaz 🙂

Yəqin ki, çoxumuz CD variantları arasında D-ni seçərdik. Amma D düzgün cavab deyil, düzgün cavab variantı C-dir. Maraqlı bir sual ortaya çıxır. Niyə? 🙂

Sualın bu hissəsi “order of initialization” ilə bağlıdır. Deməli sualda bütün hadisələr aşağıdakı ardıcıllıqla baş verir:

  1. Main metodda ilk sətirdə A classının referansı yaradılır və bu zaman B classının constructoru çağırılır (bu cür yazılış polimorfizm özəlliyindən dolayı düzgündür, növbəti məqalələrdə bu mövzuya toxunacağıq);
  2. B classının super classı varsa, ilk növbədə super classın constructoru çağırılmalıdır və bu nümunədə A classının constructoru çağırılır;
  3. A classının constructorunda print() metodu çağırılır. print() metodu B classında override edildiyindən, A classındakı print() metodu deyil, B classındakı print() metodu icra edilir;
  4. Bu addımda i dəyişəni çap edilir və işin ən maraqlı hissələrindən biri baş verir. i dəyişəni B classının instance dəyişənidir və instance dəyişənlərə dəyərlər super constructor icra edildikdən sonra mənimsədilir. Bu mərhələdə isə print() metodu super constructorun gövdəsində çağırılıb və bu səbəbdən super constructorun icrası hələ bitməyib. Məhz buna görə i dəyişəninə hələ dəyər mənimsədilməyib, ona görə də i dəyişəninin default dəyəri (sıfır) çap edilir;
  5. Super constructorun icrası bitir və i dəyişəninə 4 dəyəri mənimsədilir;
  6. Main metodda a referansı üzərindən print() metodu çağırılır. a referansı A classına aid olsa da “actual object” B classına məxsusdur. Ona görə də runtime vaxtı B classında print() metodunun override edilib edilməməsi yoxlanılır. B classında print() metodu override edilib, bu səbəbdən də həmin metod çağırılır və i`nin dəyəri (4) çap edilir.

Başqa bir nümunəyə baxaq. Tutaq ki, bizim 3 classımız var (A, B, C) və bu classlar zəncirvari şəkildə bir-birilərindən törəyiblər; C classı B classından, B classı da öz növbəsində A classından:

class A {
    public void print() {
        System.out.println("A");
    }
}

class B extends A {
    public void print() {
        System.out.println("B");
    }
}

class C extends B {
    public void print() {
        System.out.println("C");
    }
}

Bu kod nümunəsində siz C classının daxilində C classının instansını istifadə etməklə A classının print() metoduna heç cürə müraciət edə bilmirsiniz:

class C extends B {
    public void print() {
        System.out.println("C");
    }
    
    public void test(){
        A a = new C();
        a.print();            // C
        ((A)this).print();    // C
        
        this.print();         // C
        super.print();        // B
        super.super.print();  // DOES NOT COMPILE
    }
}

 

 

[topics lang=az]

About the author

Mushfiq Mammadov

Leave a Comment


The reCAPTCHA verification period has expired. Please reload the page.

 

This site uses Akismet to reduce spam. Learn how your comment data is processed.