Java - Sınıf Üyeleri ve Başlatma Sırası , Override-Overload , Generic Methodlar

Java - Sınıf Üyeleri ve Başlatma Sırası , Override-Overload , Generic Methodlar

Arkadaşlar merhaba,

Bu yazıda sınıf üyeleri , metot overriding ve metot overload konularına göz atacağız.

Bazen kod blokları bir method içindedir. Bunlar, method çağrıldığında çalıştırılır.

Diğer zamanlarda, kod blokları bir methodun dışında görünür.

Bunlara instance initializers denir.

Fields ve instance initializer blokları, dosyada göründükleri sırayla çalıştırılır.

Instance initializers sayarken bunların bir method içinde olamayacaklarını unutmayın.

Şimdi bu alanlar üzerinde okuma ve yazma işlemlerine bakalım.

public class Person{
    int  age;
    public static void main(String[] args) {
        Person person  = new Person();
        person.age = 29; // set variable
        System.out.println(person.age);  // read variable
    }

}
public class Name {

    String first = "Umut";
    String last = "Çakmak";
    String full = first + last;

    public static void main(String[] args) {
        Name n = new Name();
        System.out.println(n.full);
    }
}

Başlatma Sırası

  • Fields and instance initializer blokları, dosyada göründükleri sırayla çalıştırılır.
  • Kurucu methodları (constructor) tüm field'lar ve instance variable'lar çalıştırıldıktan sonra çalışır.
public class Chick {

      private String name = "Fluffy";

    {
        System.out.println(name + " : setting field"); //  instance initializer
    }

    public Chick() {
        name = "Tiny";
        System.out.println(name + " :setting constructor");

    }

 public static void main(String[] args) {
        Chick chick = new Chick();
        System.out.println(chick.name);

        // setting field
        //setting constructor
        //Tiny
    }

}

İlk adımda java main metod içerisinde kurucu çağrısını çağırarak Chick ile yeni bir nesne oluşturur. name alanını (field) "Fluffy" olarak başlatır.

Ardından, println() komutunu çalıştırır. System.out.println(name + " : setting field") satırını çalıştırdıktan sonra tüm field'lar ve instance initializer'lar çalıştıktan sonra, Java constructor'a geri döner.

Daha sonra name alanını "Tiny" olarak değiştirir. Constructor'daki işlem tamamlandıktan sonra main method içerisindeki System.out.println(chick.name); satırına geri döner ve yazdırılır.

Başka bir örneğe daha bakalım.

public class Egg {

    public Egg(){
          number = 5;
  }

    public static void main(String[] args) {
          Egg egg = new Egg();
          System.out.println(egg.number); //5
   }

    private int number = 3;
    { number = 4; } 

}

Field'lar ve bloklar önce çalıştırılır, sayı 3'e ve ardından 4'e ayarlanır.

Ardından kurucu method çalışır, sayıyı 5'e ayarlar.

Sonuç 5'dir.

Geçeriz Tanımlama

Field'lar ve bloklar için sıralama önemlidir. Tanımlanmadan önce bir değişkeni kullanamazsınız.

{ System.out.println(name); } // DOES NOT COMPILE

private String name = "Fluffy";

Sınıf Başlatma (Class Initialization)

Sınıf başlatma ile ilgili en önemli kural, her sınıf için en fazla bir kez gerçekleşmesidir. Ayrıca, programda kullanılmadığı takdirde sınıf hiçbir zaman yüklenmeyebilir. Bir sınıf için başlatma sırasını aşağıdaki gibi özetleyebiliriz.

Initialize Class X ;

Eğer X'in bir üst sınıfı Y varsa, o zaman önce Y sınıfını başlat.

Ayrıca tüm statik initializers'ları sınıfta göründükleri sırayla işlenir.

public class Animal {

    static {
        System.out.print("A");
    }
}

public class Lion extends Animal{

    static {
        System.out.print("B");
    }

    public static void main(String[] args) {
        System.out.print("C");
        new Lion();
        new Lion();
    }
}

ABC'yi tam olarak bir kez yazdırır. main() methodu Lion sınıfının içinde olduğundan, sınıf ilk olarak üst sınıftan başlayarak ve AB yazdırılarak başlatılacaktır.

Daha sonra main() methodu yürütülür ve C yazdırılır.main() methodu iki örnek oluştursa da sınıf yalnızca bir kez yüklenir.

Lion sınıfı, main() methodu yürütülmeden önce başlatıldı.

Bu, main() methodumuzun yürütülmekte olan sınıfın içinde olması nedeniyle oldu, bu nedenle başlangıçta yüklenmesi gerekiyordu.

public class ChildLion {

    public static void main(String[] args) {
        System.out.println("C");
        new Lion();
    }
}

Sınıfa başka hiçbir yerde referans verilmediğini varsayarsak, bu program CAB'yi yazdıracak ve Lion sınıfı main() methodunda ihtiyaç duyulana kadar yüklenmeyecektir.

Örnek Başlatma (Instance Initialization)

new anahtar sözcüğü her kullanıldığında bir örnek(instance) başlatılır. Önceki örneğimizde, iki Lion örneğinin başlatılmasıyla sonuçlanan iki yeni Lion() çağrısı vardı.

İlk olarak, new anahtar kelimenin kullanıldığı en düşük seviyeli kurucudan başlayın.

Unutmayın, her kurucunun ilk satırı this() veya super() için bir çağrıdır ve bu atlanırsa, derleyici otomatik olarak argümansız süper kurucuya bir çağrı ekler.

Ardından, yukarı doğru ilerleyin ve kurucuların sırasını not edin. Son olarak, üst sınıfla başlayarak her sınıfı başlatın, her instance initializer ve constructor çağrıldığı ters sırada işleyin.

Bir örnek için başlatma sırasını aşağıdaki gibi özetliyebiliriz.

  • X'in bir üst sınıfı Y var ise , ozaman önce Y instance'nı başlatın.
  • Tüm instance variable'ları sınıfta bildirikleri sıra ile işlenir.
  • Tüm statik initializers'ları sınıfta göründükleri sırayla işlenir.
  • this() ile başvurulan overloaded kurucu methodlar dahil tüm kurucuları başlatın.
public class Employee {

    private String name = "Employee";

    {
        System.out.print( name + "-");
    }

    private static int SALARY = 0;

    static {
        System.out.print(SALARY + "-");
    }

    public  Employee(){
        System.out.print("e-");
    }

    static {
        SALARY += 10;
        System.out.print(SALARY + "-");
    }

    public static void main(String[] args) {
        new Employee();
    }
}

// 0-10-Employee-e-

İlk önce sınıfı başlatmamız gerekiyor. Bildirilen bir üst sınıf olmadığı için, yani üst sınıf Object olduğu için, Employee'un statik bileşenleri ile başlayabiliriz.

Önce statikleri başlatıp daha sonra name alanını yazdı.Son olarak ise kurucu method çalıştı.

class Primate {
    public Primate() {
        System.out.print("Primate-");
    }
}

class Ape extends Primate {

    public Ape(int fur) {
        System.out.print("Ape1-");
    }

    public Ape() {
        System.out.print("Ape2-");
    }
}

public class Chimpanzee extends Ape {

    public Chimpanzee() {
        super(2);
        System.out.print("Chimpanzee-");
    }

    public static void main(String[] args) {
        new Chimpanzee();
    }
}

Compiler, hem Primate hem de Ape kurucularının ilk ifadesi olarak super() komutunu ekler. Kod, ilk çağrılan ana kurucularla yürütülür ve aşağıdaki çıktıyı verir:

Primate-Ape1-Chimpanzee-

İki Ape() kurucusundan yalnızca birinin çağrıldığına dikkat edin. Hangi kurucuların yürütüleceğini belirlemek için new Chimpanzee() sınıfını nasıl başlattığınıza göre sonuç değişecektir.

Örneğin;


public class Chimpanzee extends Ape {

    public Chimpanzee() {
        super();
        System.out.print("Chimpanzee-");
    }

    public static void main(String[] args) {
        new Chimpanzee();
    }
}

super() çağrısına herhangi bir argüman geçmediğimiz de argümansız yapıyıcı çağıracaktır ve böylelikle sonuç

Primate-Ape2-Chimpanzee-

olacaktır.

Unutmayın, kurucular aşağıdan yukarıya doğru yürütülür,ancak her kurucunun ilk satırı başka bir kurucuya yapılan bir çağrı olduğundan,akış aslında ana kurucunun alt kurucudan önce çalıştırılmasıyla sona erer.

Diğer bir örnek;

public class Anchovy {

      private String name = "swimmy";

      {
          System.out.println(name);
      }

      private static int COUNT = 0;

      static {
          System.out.println(COUNT);
      }

     {   
        COUNT++;
        System.out.println("COUNT : " + COUNT);
     }

      public Anchovy() {
        System.out.println("Constructor");
     }

    public static void main(String[] args) {
        System.out.println("READY ");
        new Anchovy();
    }
}

// 0
// READY 
// swimmy
// COUNT : 1
// Constructor

Bildirilen bir üst sınıf yoktur, bu nedenle kalıtımla ilgili tüm adımları atlayabiliriz.

İlk önce statik variables ve statik initializers işler ve 0 yazdırır.

Artık statik başlatıcılar kullanımdan kaldırıldığına göre, Ready'i yazdıran main() methodu çalışabilir.

name alanı ve daha sonra arttırılan Count yazdırılır.

Son olarak kurucu method çalıştırılır.

Diğer bir örneğe bakalım;

class MonkeyFamily{
    static {
        System.out.println("A");
      }


     {
        System.out.println("B");
     }

    public MonkeyFamily(String name) {
        this(1);
        System.out.println("C");
    }
    public MonkeyFamily() {
        System.out.println("D");
    }
    public MonkeyFamily(int stripes) {
        System.out.println("E");
    }
}

public class SquirrelMonkey extends MonkeyFamily{

    static {
        System.out.println("F");
    }

    public SquirrelMonkey(int stripes) {
        super("sugar");
        System.out.println("G");
    }

    {
        System.out.println("H");
    }

    public static void main(String[] args) {
        new SquirrelMonkey(1);
        System.out.println();
        new SquirrelMonkey(2);
    }
}

// AFBECHG
// BECHG
  • MonkeyFamily parent class olduğu için SquirrelMonkey çalıştığında static değerleri yükler ilk olarak A yazdırır.
  • Parent classdaki static değerini yazdırdıktan sonra SquirrelMonkey sınıfındaki static yüklenir ve F yazdırır.
  • Önce MonkeyFamily'nin üst sınıf örneği başlatılır.Daha sonra parent sınıf MonkeyFamily'deki instance initilazier çağrılır satırda B yazdırılır.
  • Kurucuları başlatıyoruz. Bu durumda, String name argümanı alan kurucunun çağrılmasını içerir. Bu da overloaded edilmiş kurucuyu çağırır.Sonuç olarak, kurucu gövdeleri çağrıldıkları sıranın tersi sırayla açıldıklarından EC yazdırılır.
  • EC'den sonra SquirrelMonkey'nin kendisini başlatmasıyla beraber H ve G yazdırılır.

MonkeyFamily üst sınıfındaki üç kurucudan yalnızca ikisi çağrıldığından, D'nin hiçbir zaman yazdırılmadığına dikkat edin.

Kurucu Method Kuralları (Constructor Rules)

  • Her kurucunun ilk ifadesi, this() aracılığıyla overloaded bir kurucuya veya super() aracılığıyla doğrudan bir ana kurucuya yapılan çağrıdır.
  • Bir kurucunun ilk ifadesi this() veya super() için bir çağrı değilse, o zaman derleyici, kurucunun ilk ifadesi olarak argümansız bir super() ekler.
  • Bir kurucunun ilk ifadesinden sonra this() ve super() öğesinin çağrılması derleyici hatasıyla sonuçlanır.
  • Parent sınıfında argümansız bir kurucu yoksa, alt sınıftaki her kurucu açık bir this() veya super() kurucu çağrısı ile başlamalıdır.
  • Parent sınıfın argümansız bir kurucusu yoksa ve alt sınıf herhangi bir kurucu tanımlamıyorsa, alt sınıf derlenmeyecektir.
  • Bir sınıf yalnızca private kurucuları tanımlarsa, üst düzey bir sınıf tarafından genişletilemez.
  • Tüm final instance variables, kurucunun sonuna kadar tam olarak bir kez bir değer atanmalıdır. Değer atanmamış herhangi bir final instance variables, kurucunun bildirildiği satırda bir derleyici hatası olacaktır.

Miras Alınan Üyeler (Inheriting Members)

Java sınıfları, methodlar, primitive tipler veya object reference'ları dahil olmak üzere ana sınıfın herhangi bir public veya protected üyesini kullanabilir.

Eğer parent ve child class aynı pakette ise child sınıf, parent sınıfta tanımlanan herhangi bir package-private üyeyi de kullanabilir.

Son olarak, bir child sınıf, en azından herhangi bir doğrudan referans yoluyla, üst sınıfın private bir üyesine asla erişemez.

class Fish {
    protected int size;
    private int age;

    public Fish(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }
}

public class Shark extends Fish {

    public Shark(int age) {
        super(age);
        this.size = 4;
    }

    private int numberOfFins = 8;

    public void displaySharkDetails() {
        System.out.print("Shark with age: " + getAge());
        System.out.print(" and " + this.size + " meters long");
        System.out.print(" with " + numberOfFins + " fins");
    }

    public  void displaySharkDetails1() {
        System.out.println("Shark with age: " + this.getAge());
        System.out.println("and " + super.size + " meters long");
        System.out.println("with " + this.numberOfFins + " fins");
    }

    public static void main(String[] args) {
        Shark s = new Shark(2);
        s.displaySharkDetails1();
    }
  • displaySharkDetails() methodunda alt sınıfta, üst sınıftaki değerlere erişmek için public getAge() methodunu ve protected üye olan size alanını kullanırız. Bunu, geçerli veya bir üst sınıfın görünür üyelerine erişmek için kullanabileceğinizi ve bir üst sınıfın görünür üyelerine erişmek için süper'i kullanabileceğinizi unutmayın.
  • displaySharkDetails1() methodunda getAge() ve size üst sınıfta tanımlandıkları için this veya super ile erişilebilirken, numberOfFins'e yalnızca this ile erişilebilir ve kalıtsal bir özellik olmadığı için super ile erişilemez.

Miras Alınan Methodlar (INHERITING METHODS)

Bir methodu geçersiz kılmak(override) için bir dizi kurala uymalısınız. Bir methodu geçersiz kıldığınızda derleyici aşağıdaki kontrolleri gerçekleştirir:

  • Alt sınıftaki method, üst sınıftaki method ile aynı imzaya sahip olmalıdır.
  • Alt sınıftaki method, en az üst sınıftaki method kadar erişilebilir olmalıdır.
  • Alt sınıf methodu, üst sınıf methodunda bildirilen herhangi bir exception sınıfından yeni veya daha geniş bir kontrol edilmiş istisna(checked exception) ilan etmeyebilir.
  • Method bir değer döndürürse, aynı veya üst sınıftaki method kovaryant dönüş türleri olarak bilinen bir alt türü olmalıdır.

X'i Y'nin bir alt türü olarak tanımlarsak, aşağıdakilerden biri doğrudur:

  • X ve Y sınıflardır ve X, Y'nin bir alt sınıfıdır.
  • X ve Y interface'dir ve X, Y'nin bir alt interface'dir.
  • X bir sınıftır ve Y bir interface'dir. X, Y'yi uygular (doğrudan veya miras alınan bir sınıf aracılığıyla).
  • Benzer şekilde, bir super tip , bir tipin diğerinin atası olduğu iki tip arasındaki karşılıklı ilişkidir. Unutmayın, bir subclass bir alt subtype,ancak tüm subtype, subclass değildir.

Ya aynı imzaya sahip hem parent hem de child sınıflarda tanımlanmış bir method varsa? Örneğin, methodun yeni bir sürümünü tanımlamak ve bu alt sınıf için farklı davranmasını sağlamak isteyebilirsiniz.

Çözüm, alt sınıftaki methodu override etmektir.

Java'da, bir methodu geçersiz kılmak(override), bir alt sınıf, aynı imzaya ve uyumlu dönüş türüne sahip devralınan bir method için yeniden bildirdiğinde gerçekleşir.

Bir method imzasının, methodun adını ve method parametre tiplerini içerdiğini unutmayın.

Bir method override edildiğinde , super anahtar sözcüğünü kullanarak methodun üst sürümüne başvurabilirsiniz. Bu şekilde, this ve super anahtar sözcükleri, sırasıyla bir methodun geçerli ve üst sürümleri arasında seçim yapmanıza olanak tanır.

public class Dog {
    public double getAverageWeight() {
        return 50;
    }
}

public class CoilDog extends Dog{

    public double getAverageWeight() {
        return super.getAverageWeight() + 20;
    }
}

Örneğin, super anahtar sözcüğünü kaldırırsak aşağıdaki kod ne olur?

  public double getAverageWeight() {
        return getAverageWeight() + 20; // // StackOverflowError
  }

Bu örnekte, compiler üst Dog methodunu çağırmaz; recursive bir method .ağrısı yürüttüğünüzü düşüneceğinden mevcut CoilDog methodunu çağırır.

Özyinelemeli (recursive) bir method, kendisini yürütmenin bir parçası olarak çağıran methotdur. Programlamada yaygındır, ancak bir noktada veya derinlikte özyinelemenin sonunu tetikleyen bir sonlandırma koşuluna sahip olmalıdır.Bu örnekte sonlandırma koşulu yoktur; bu nedenle, uygulama kendisini sonsuz olarak çağırmaya ve çalışma zamanında bir StackOverflowError üretmeye çalışacaktır.

    public static void main(String[] args) {
        System.out.println(new CoilDog().getAverageWeight());
        System.out.println(new Dog().getAverageWeight());
    }

CoilDog alt sınıfının Dog üst sınıfını geçersiz kıldığı (override) bu örnekte, getAverageWeight() methodu çalışır ve program 50.0 ve 70.0'i yazdırır.

OVERLOADING vs OVERRIDING

İki method aynı ada ancak farklı imzalara sahipse, methodlar override edilmez, overloading olur.

Overloaded bir methodun farklı bir method parametreleri listesi kullanması bakımından farklılık gösterirler.

Bu ayrım, overloaded methodlara sözdiziminde override edilen bir methoddan çok daha fazla özgürlük sağlar.

Overloaded methodlar bağımsız olarak kabul edilir ve override edilen methodlar ile aynı polimorfik özellikleri paylaşmaz.

public class Bird {

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

    public void eat(int food) {
        System.out.println("Bird is eating " + food + " unitof food");
    }
}
public class Eagle extends Bird {
    public int fly(int height) {
        System.out.println("Bird is flying at " + height + "meters");
        return height;
  }    
}

Fly() methodu, Eagle alt sınıfında overload edilmiştir, çünkü method imzası değişkensiz bir methoddan bir int değişkenli bir methoda dönüşür.

Method overload edildiğinden ve override edilmediğinden dolayı, dönüş türü void'den int'ye değiştirilebilir.

  public int eat(int food) { // DOES NOT COMPILE
       System.out.println("Bird is eating " + food + " unitsof food");
      return food;

  }

Bird üst sınıfındaki ile aynı olduğundan, Eagle alt sınıfında eat() methodu override edilir.Her ikisi de tek bir int argümanı alır.

Method override edildiğinden Eagle sınıfındaki methodun dönüş türü, Bird sınıfındaki methodun dönüş türü ile uyumlu olmalıdır.

Bu örnekte, dönüş türü int, void'in bir alt türü değildir.Bu nedenle, derleyici bu method tanımında bir istisna atar.

public class Camel {

    public String name = "ALİ";

    public int getNumberOfHumps() {
        return 1;
    }

}

public class BactrianCamel extends Camel {

    private int getNumberOfHumps() { // DOES NOT COMPILE return 2; }

    public int getNumberOfHumps() {
        return 2;
    }

    public String getNumberOfHumps(String name) {
        return super.name + name;
    }
}
public class Rider {

    public static void main(String[] args) {
        Camel c = new BactrianCamel();
        System.out.print(c.getNumberOfHumps());

        BactrianCamel c1 = new BactrianCamel();
        System.out.println(c1.getNumberOfHumps("Umut"));
    }

}
// 2
// ALİUmut

Bu örnekte BactrianCamel, üst sınıfta tanımlanan getNumberOfHumps() methodunu geçersiz kılmaya (override) çalışır, ancak private erişim değiştiricisi methodun üst sürümünde tanımlanandan daha kısıtlayıcı olduğundan başarısız olur.

Üçüncü kural, bir yöntemi geçersiz kılmanın (override), devralınan methoddan daha geniş checked exceptions bildiremeyeceğini söyler.

public class Reptile {

    protected void sleepInShell() throws IOException {
    }

    protected void hideInShell() throws NumberFormatException {
    }

    protected void exitShell() throws FileNotFoundException {
    }
}

public class Snake extends Reptile {

    public void sleepInShell() throws FileNotFoundException {}

    public void hideInShell() throws IllegalArgumentException {}

    public void exitShell() throws IOException {}

}

sleepInShell() : override edilen sleepInShell() methodu, devralınan method olan IOException'da bildirilen özel durumun bir alt sınıfı olan FileNotFoundException'ı bildirir.Override edilen methodlara ilişkin üçüncü kuralımıza göre, override edilen methodda istisna daha dar olduğu için bu başarılı bir override'dir.

hideInShell(): override edilen hideInShell() methodu, devralınan method olan NumberFormatException'da bildirilen istisnanın bir üst sınıfı olan bir IllegalArgumentException bildirir. Override edilen daha geniş bir istisna kullandığından bu geçersiz bir override gibi görünse de, bu istisnaların checked exception değildir.Bu nedenle üçüncü kural uygulanmaz.

exitShell(): override edilen üçüncü exitShell() methodu da , devralınan method FileNotFoundException'da bildirilen istisnanın bir üst sınıfı olan IOException'ı bildirir. Bunlar checked exception istisnalar olduğundan ve IOException daha geniş olduğundan, override edilen exitShell() methodu Snake sınıfında derlenmez.

Kovaryant'ı basitce anlatmak gerekirse... Miras alınan bir dönüş türü A ve override edilen bir dönüş türü B verildiğinde, bir B instance' ni A için bir referans değişkenine bir cast işlemi olmadan atayabilir misiniz ? Eğer öyleyse, o zaman kovayanttırlar. Bu kural, hem ilkel(primitive) türler hem de nesne türleri için geçerlidir. Dönüş türlerinden biri void ise, kendisi dışında hiçbir şey void ile kovaryant olmadığından, her ikisinin de void olması gerekir.

Kovaryant dönüş tipleri basitçe kendi sınıf referansını veya alt sınıf referansını döndürmek anlamına gelir.

JavanRhino alt sınıfı, Rhino'dan gelen iki methodu override etmeye çalışır. Override edilen her iki method da devralınan methodlar ile aynı isme ve imzaya sahiptir.

Override edilen methodlar ayrıca, devralınan methodlardan daha geniş bir erişim değiştiricisine sahiptir.

İkinci kurala göre daha geniş bir erişim değiştiricisi kabul edilebilir.

public class Donkey {

    protected CharSequence getName() {
        return "Donkey";
    }

    protected String getColor() {
        return "black, or white";
    }
}

    class CatalanDonkey extends Donkey {

        public String getName() {
            return " Catalan - donkey";
        }

    public CharSequence getColor() { // DOES NOT COMPILE 
          return "brown";  
      }

}

Öte yandan override edilen getColor() methodu CharSequence String'in bir alt türü olmadığı için derlenmez.

Tüm string değerleri bir CharSequence değerleridir, ancak tüm CharSequence değerleri String değerleri değildir.

Örneğin, bir StringBuilder bir CharSequence'tır, ancak bir String değildir.

Overloading Generic Method

public class LongTailAnimal {

    protected void chew(List<Object> input) {}

}

public class ThresherShark extends LongTailAnimal{

    protected void chew(ArrayList<Double> input) {}

}

ThresherShark sınıfı derlenir.Çünkü üst sınıfta tanımlanan aynı generic türü override edilen method da kullanır.

Ancak, imza aynı olmadığı için override edilen methodlar değil, aşırı yüklenmiş(overload) methodlar olarak kabul edilirler.

Tür silme, method değişkenlerinden birinin List ve diğerinin ArrayList olduğu gerçeğini değiştirmez.

Bir methodu generic parametrelerle override edebilirsiniz, ancak generic tür(generic type) dahil olmak üzere imzayı tam olarak eşleştirmeniz gerekir.

Bu örneklerin ikisi de tür silme nedeniyle derlenemez. Derleme zamanın da generic tür silinir ve geçersiz bir overload method olarak görünür.

protected void chew(List<String> input) {}

protected void chew(List<Double> input) {}

Generic Return Types

Genericleri döndüren override methodlar ile çalışırken dönüş değerleri kovaryant olmalıdır.

Genericler açısından bu override method da bildirilen sınıfın veya interface'in dönüş türünden üst sınıfta tanımlanan sınıfın bir alt türü olması gerektiği anlamına gelir.

Generic parametre türü , üst öğenin türü ile tam olarak eşleşmelidir.

public class Mammal {

    public List<CharSequence> play() {
        return null;
    }

    public CharSequence sleep() {
        return null;
    }

}

public class Sheep extends Mammal {

    public ArrayList<CharSequence> play() {
            return null;
    }

public class Goat extends Mammal {

      public List<String> play() {} // does not compile
}

ArrayList, List'in bir alt türü olduğu için Sheep sınıfı derlenir. Goat sınıfındaki play() methodu ise derlenmiyor.

Dönüş türlerinin kovaryant olması için, generic type parametresinin eşleşmesi gerekir. String, CharSequence'ın bir alt türü olsa da, Mammal sınıfında tanımlanan generic türle tam olarak eşleşmez. Bu nedenle, bu geçersiz bir override olarak kabul edilir.

public class Goat extends Mammal {

  public String sleep() {
        return null;
    }
}

String, CharSequence'ın bir alt türü olduğundan, Goat sınıfındaki sleep() methodunun derlendiğine dikkat edin. Bu örnek, kovaryantın generic parametre türü için değil, dönüş türü için geçerli olduğunu gösterir.

Ve unutmayın, generic methodlar yalnızca generic parametre türü değiştirilerek overload edilmez.

Private Method Tanımlama

Private bir methodu override etmeye çalışırsanız ne olur? Java'da, miras alınmadıkları için private methodlıar override edilemez.Bir alt sınıfın ana methoda erişimi olmaması, alt sınıfın kendi methodunu tanımlayamayacağı anlamına gelmez.

Java, alt sınıfta, üst sınıftaki method ile aynı veya değiştirilmiş imzayla yeni bir methodu yeniden bildirmenize izin verir.

Alt sınıftaki bu method,üst sınıfın methodu ile ilgisi olmayan ayrı ve bağımsız bir methoddur.Bu nedenle methodları override etme kurallarının hiç biri geçerli olmaz.

public class Camel {

    private String getNumberOfHumps() {
        return "Undefined";
    }
}

public class DromedaryCamel extends Camel {

    private int getNumberOfHumps() {
        return 1;
    }
}

Bu kod sorunsuz bir şekilde derlenir. Geri dönüş türünün alt method da String'den int'e değiştiğine dikkat edin.

Bu örnekte, üst sınıftaki getNumberOfHumps() methodu yeniden bildirilir, bu nedenle alt sınıftaki method, üst sınıftaki methodun override edilmesi değil yeni bir method olarak tanımlanmasıdır.

Üst sınıftaki method public veya protected olsaydı, alt sınıftaki method derlenemez çünkü override methodlarının kuralını ihlal ederdi.Bu örnekteki ebeveyn methodu private'dir,dolayısıyla böyle bir sorun yoktur.

Statik Methodları Gizleme (Hiding Static Methods)

Bir alt sınıf, bir üst sınıfta tanımlanan devralınan bir statik method ile aynı isme ve imzaya sahip bir statik method tanımladığında, gizli bir method oluşur.

Method gizleme benzerdir ancak method override etmek ile tam olarak aynı değildir.

Bir method gizlendiğinde, bir methodu override etmek için önceki dört kural izlenmelidir. Ayrıca, bir methodu gizlemek için yeni bir kural eklenir.

Alt sınıfta tanımlanan method, bir üst sınıfta statik olarak işaretlendiyse statik olarak işaretlenmelidir.

Basitçe söylemek gerekirse, iki method statik olarak işaretlenmişse method gizleme ve statik olarak işaretlenmemiş ise method override edilmiş demektir.

Biri statik olarak işaretlenmiş ve diğeri işaretlenmemiş ise sınıf derlenmeyecektir.

public class Bear {

    public static void eat() {
        System.out.println("Bear is eating");
    }

    public static void sneeze() {
        System.out.println("Bear is sneezing");
    }

    public void hibernate() {
        System.out.println("Bear is hibernating");
    }

    public static void laugh() {
        System.out.println("Bear is laughing");
    }

}

public class Panda extends Bear {

    public static void eat() {
        System.out.println("Panda is chewing");
    }

   public void sneeze() {} // DOES NOT COMPILE Parent class'da static burada değil compile olmaz.

   public static void hibernate() {} // DOES NOT COMPILE

   protected static void laugh() {}// DOES NOT COMPILE
}

hibernate() : Bu senaryoda, derleyici statik bir methodu gizlemeye çalıştığınızı düşünür.hibernate() override edilmesi gereken bir instance methodu olduğundan,derleyici bir hata verir.

laugh(): Methodun her iki kullanımı da de statik olarak işaretlenmiş olsa da, Panda'daki method, miras aldığından daha kısıtlayıcı bir erişim değiştiricisine sahiptir ve ikincisini bozar.

Final Methodlar

Bir methodu final olarak işaretleyerek, bir alt sınıfın bu methodu değiştirmesini yasaklamış olursunuz. Bu kural, hem bir methodu override ettiğinizde hem de bir methodu gizlediğinizde geçerlidir. Başka bir deyişle, üst sınıfta final olarak işaretlenmişse, bir alt sınıftaki statik bir yöntemi gizleyemezsiniz.

Methodları final olarak işaretlemek, override engellese de, pratikte avantajları vardır. Örneğin, bir üst sınıf tanımlarken bir methodu final olarak işaretlersiniz ve hangi alt sınıftan çağrıldığına bakılmaksızın üst sınıftaki bir methodun belirli davranışını garanti etmek istersiniz.

public class Bird {

    public final boolean hasFeathers() {
        return true;
    }

    public final static void flyAway() {}

}

Bird ile ilgili bu örnekte, üst sınıfın yazarı, çağrıldığı alt sınıf örneğinden bağımsız olarak hasFeathers() methodunun her zaman true döndürmesini sağlamak isteyebilir. Yazar, tüylerin bulunmadığı bir Bird örneğinin bulunmadığından emindir.

Bu nedenle, final değiştirici genellikle, üst sınıfın yazarı, polimorfizmi sınırlama pahasına belirli davranışları garanti etmek istediğinde kullanılır.

public class Penguin extends Bird {

    public final boolean hasFeathers() {} // DOES NOT COMPILE

}

Bu örnekte, hasFeathers() örnek methodu Bird üst sınıfında final olarak işaretlenmiştir, bu nedenle alt sınıf Penguin üst methodu override edemez. bu da bir derleyici hatasına neden olur.

public class Penguin extends Bird {
      public final static void flyAway() {}

}

FlyAway() statik methodu da de final olarak işaretlenmiştir, bu nedenle alt sınıfta gizlenemez. Bu örnekte, alt methodun final anahtar sözcüğünü kullanıp kullanmadığı önemsizdir; kod her iki şekilde de derlenmeyecektir.

Bu kural yalnızca devralınan (inherited) methodlar için geçerlidir.

Örneğin,iki method üst Bird sınıfında private olarak işaretlenmişse, tanımlandığı Penguin sınıfında derlenir. Bu durumda, private methodlar geçersiz (override) kılınmayacak veya gizlenmeyecek, yeniden bildirilecektir.

Değişkenleri Gizleme (Hiding Variables)

Bir alt sınıf, üst sınıfta tanımlanan devralınan bir değişkenle aynı ada sahip bir değişken tanımladığında, gizli bir değişken oluşur.

Bir örnek üst sınıfta tanımlanmış ve bir örnek alt sınıfta tanımlanmış olarak alt sınıfın bir instance'sinde değişkenin iki farklı kopyasını oluşturur.

class Carnivore {
    protected boolean hasFur = false;
}

public class Meerkat extends Carnivore {

    protected boolean hasFur = true;

    public static void main(String[] args) {
        Meerkat m = new Meerkat();
        Carnivore c = m;
        System.out.println(m.hasFur);
        System.out.println(c.hasFur);
    }
}

Bu sınıfların her ikisi de bir hasFur değişkeni tanımlar, ancak farklı değerlere sahiptir. main() methodu ile oluşturulan yalnızca bir nesne olsa da, her iki değişken de birbirinden bağımsız olarak var olur. Çıktı, kullanılan referans değişkene bağlı olarak değişir.

Umarım faydalı olmuştur.