Polimorfizm
Polimorfizm, Java'da bir nesnenin farklı şekillerde davranabilme özelliğidir. Bu, farklı sınıfların aynı metodu farklı şekillerde uygulaması anlamına gelir. Java'da polimorfizm, yani çok biçimlilik, üç şekilde gerçekleştirilebilir: aşırı yükleme (overloading), aşırı yazma (overriding) ve arayüzler.
1. Aşırı Yükleme (Overloading)
Aşırı yükleme, aynı sınıfta aynı isme sahip farklı parametrelerle birden fazla metodun tanımlanmasına izin verir. Bu, aynı metodun farklı şekillerde çağrılmasını sağlar. Aşırı yükleme, Java'da statik bağlama olarak da bilinen bağlama türünden yararlanır.
public class Calculator {
public int add(int x, int y) {
return x + y;
}
public int add(int x, int y, int z) {
return x + y + z;
}
}
2. Aşırı Yazma (Overriding)
Aşırı yazma, bir alt sınıfta bir üst sınıfta tanımlanan metodu aynı isimle ve aynı parametrelerle yeniden tanımlama işlemidir. Aşırı yazma, Java'da dinamik bağlama olarak da bilinen bağlama türünden yararlanır.
public class Animal {
public void makeSound() {
System.out.println("Animal is making a sound");
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog is barking");
}
}
3. Arayüzler (Interfaces)
Arayüzler, bir sınıfın belirli bir işlevselliği uygulamasını zorunlu kılmak için kullanılır. Arayüzler, bir sınıfın birden fazla arayüzü uygulamasına olanak tanır ve çoklu kalıtımın bir alternatifi olarak kullanılabilir.
public interface Animal {
void makeSound();
}
public class Dog implements Animal {
@Override
public void makeSound() {
System.out.println("Dog is barking");
}
}
public class Cat implements Animal {
@Override
public void makeSound() {
System.out.println("Cat is meowing");
}
}
Polimorfizm, çoklu kalıtımdan farklıdır çünkü Java'da çoklu kalıtım desteklenmez. Ancak, bir sınıf birden fazla arayüzü uygulayabilir ve bu, aynı işlevselliğin farklı arayüzler üzerinden erişilebilmesini sağlar. Bu şekilde, Java'da çoklu kalıtımın alternatifi olarak polimorfizm kullanılabilir.
Çoklu Kalıtımın Riskleri
Çoklu kalıtımın en önemli riski, çakışmaya (ambiguity) ve karışıklığa (confusion) neden olabilmesidir. Çoklu kalıtım, bir sınıfın birden fazla üst sınıfı (parent class) olan alt sınıfların (child class) oluşturulmasına olanak tanır.
Örneğin, şöyle bir senaryoda çakışma yaşanabilir: Bir sınıfın bir üst sınıfı A ve bir başka üst sınıfı B var. Hem A hem de B sınıfları aynı isimde bir metodu veya özelliği tanımlıyor olsun. Bu durumda, alt sınıfın hangi üst sınıfın metodunu veya özelliğini kullanacağı belirsiz hale gelir.
Bir diğer risk de, çoklu kalıtımın karmaşıklığıdır. Birden fazla üst sınıfın özelliklerini bir alt sınıfa aktarmak, kodun okunabilirliğini, bakımını ve geliştirilmesini zorlaştırabilir.
Bu nedenlerden dolayı, Java gibi birçok modern programlama dili, çoklu kalıtımı desteklememekte veya sınırlamalar getirmektedir. Bunun yerine, arayüzler (interfaces) ve kalıtım (inheritance) gibi diğer mekanizmalar kullanılarak benzer işlevsellik sağlanmaktadır.
Geç Bağlama
Java'da geç bağlama, obje yönelimli programlama (OOP) prensiplerinden biridir ve dinamik bağlama (dynamic binding) olarak da bilinir. Geç bağlama, bir metodu veya bir özelliği çağırdığınızda, o metot veya özellik, o anki nesne tipine göre çalışacak şekilde otomatik olarak seçilir.
Java'da geç bağlama, çoğu zaman polimorfizm (polymorphism) ile birlikte kullanılır. Polimorfizm, aynı isme sahip farklı metotların veya özelliklerin farklı nesne tiplerinde farklı şekillerde davranmasına izin verir. Bu sayede, farklı sınıfların aynı arayüzü uygulamasına olanak tanır.
Geç bağlama, özellikle kalıtım (inheritance) ve arayüzler (interfaces) gibi OOP kavramlarının kullanıldığı yerlerde önemlidir.
Geç bağlama, "virtual method invocation" olarak da adlandırılır. Bu, JVM'in (Java Virtual Machine) çalışma zamanında, doğru metodun seçilmesini sağlamak için bir tablo kullanmasıdır. Bu tablo, nesne tipine göre hangi metodun çalıştırılacağını belirler. Bu sayede, çalışma zamanında metotların hangi nesne türüne bağlı olarak davranacağı dinamik olarak belirlenir.
public class Animal {
public void makeSound() {
System.out.println("The animal makes a sound");
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("The dog barks");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("The cat meows");
}
}
public class Main {
public static void main(String[] args) {
Animal animal1 = new Dog();
Animal animal2 = new Cat();
animal1.makeSound(); // Çıktı: The dog barks
animal2.makeSound(); // Çıktı: The cat meows
}
}
Yukarıdaki kodda, Animal
adında bir üst sınıf (parent class) ve Dog
ve Cat
adında iki alt sınıf (child class) var. Her iki alt sınıf da makeSound()
adında bir metodu ezmiş (override) ve kendi özel uygulamalarını sağlamıştır.
Main sınıfında, Animal
türünden animal1
ve animal2
nesneleri oluşturulmuştur. animal1
nesnesi Dog türünden oluşturulmuştur, animal2
nesnesi ise Cat türünden oluşturulmuştur. makeSound()
metodu her iki alt sınıfta farklı şekilde uygulandığı için, her iki nesnenin makeSound()
metodu çağrıldığında farklı çıktılar üretilir.
Burada, animal1.makeSound()
çağrıldığında Dog
alt sınıfındaki makeSound()
metodu çalışır ve "The dog barks" çıktısı üretilir. animal2.makeSound()
çağrıldığında ise Cat
alt sınıfındaki makeSound()
metodu çalışır ve "The cat meows" çıktısı üretilir. Bu, Java'nın geç bağlama özelliğinin bir örneğidir, çünkü hangi alt sınıfın metodu çağrılacağı, çalışma zamanında nesne türüne göre belirlenir.
Soyut Sınıflar
Java'da soyut sınıflar, tam olarak uygulanmamış (incomplete) sınıflardır ve diğer sınıfların genel tasarımlarını ve davranışlarını tanımlamak için kullanılırlar. Soyut sınıflar, en az bir soyut metoda sahip olmalıdır ve bir sınıfın soyut olarak tanımlanması için abstract
anahtar kelimesi kullanılır.
Soyut sınıflar, alt sınıflarında tamamlanması gereken metotların genel bir şablonunu tanımlarlar. Alt sınıflar, soyut sınıftan miras alır ve soyut metotları uygulayarak tamamlarlar. Bu, alt sınıfların özelliklerini ve davranışlarını belirler ve kalıtım hiyerarşisinde daha özelleştirilmiş (specialized) sınıflar oluşturulmasına izin verir.
Soyut sınıfların örnekleri oluşturulamaz, ancak bir referans olarak kullanılabilirler. Yani, soyut sınıf türünden bir değişken oluşturulabilir, ancak bu değişkenin değeri null veya bir alt sınıf örneği olmalıdır.
Örneğin, aşağıdaki kodda Animal
adında bir soyut sınıf tanımlanmıştır ve içinde makeSound()
adında bir soyut metot bulunmaktadır. Dog
adında bir alt sınıf da tanımlanmıştır ve makeSound()
metodu uygulanmıştır.
abstract class Animal {
public abstract void makeSound();
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("The dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal animal1 = new Dog();
animal1.makeSound(); // Çıktı: The dog barks
}
}
Yukarıdaki örnekte, Animal
soyut sınıfı makeSound()
adında bir soyut metot içerir. Dog
alt sınıfı, makeSound()
metotunu uygular ve Main
sınıfında bir Dog
örneği oluşturularak makeSound()
metodu çağrılır. Burada, Animal
soyut sınıfı, Dog
alt sınıfı tarafından uygulanan makeSound()
metodu aracılığıyla çağrılabilir.
Arayüzler
Java'da arayüzler, sınıflardan farklı olarak sadece metot tanımları ve sabitler içeren bir türdür. Arayüzler, belirli bir davranışı garanti altına almak için kullanılır ve sınıfların arayüzleri uygulaması beklenir. Arayüzler, benzer işlevleri olan sınıfların genişletilebilirliğini artırır ve daha kolay değiştirilebilir ve genişletilebilir kod yazmayı sağlar.
Arayüzler, interface
anahtar kelimesi kullanılarak tanımlanır ve içinde metot tanımları (sadece başlıkları) ve sabitler bulunur. Arayüzlerde herhangi bir metot gövdesi veya uygulaması yoktur. Bunun yerine, arayüzü uygulayan sınıflar, arayüzdeki tüm metotları uygulamak zorundadır. Arayüzdeki tüm metotlar varsayılan olarak public
ve abstract
olarak tanımlanır.
Bir sınıf birden fazla arayüzü uygulayabilir ve arayüzler arasında hiyerarşi oluşturabilir. Arayüzler arasındaki miras, extends
anahtar kelimesi kullanılarak gerçekleştirilir.
Aşağıdaki örnekte Animal
adında bir arayüz tanımlanmıştır ve içinde eat()
ve move()
adında iki metot tanımlanmıştır. Bird
adında bir sınıf, Animal
arayüzünü uygular ve eat()
ve move()
metotlarını uygular.
interface Animal {
public void eat();
public void move();
}
class Bird implements Animal {
public void eat() {
System.out.println("Bird eats.");
}
public void move() {
System.out.println("Bird flies.");
}
}
public class Main {
public static void main(String[] args) {
Bird bird1 = new Bird();
bird1.eat(); // Çıktı: Bird eats.
bird1.move(); // Çıktı: Bird flies.
}
}
Yukarıdaki örnekte, Animal
arayüzü içinde eat()
ve move()
adında iki metot tanımlanmıştır. Bird
sınıfı, Animal
arayüzünü uygular ve eat()
ve move()
metotlarını uygular. Main
sınıfında, Bird
sınıfından bir örnek oluşturulur ve eat()
ve move()
metotları çağrılır.
Yerel Sınıflar
Java'da yerel sınıflar, bir metot içinde tanımlanan ve sadece o metot içinde kullanılabilen sınıflardır. Yerel sınıflar, başka sınıfların içinde tanımlanabilen iç sınıfların bir alt kümesidir. Yerel sınıflar, bir metot içinde tanımlanmalarına rağmen, normal sınıflar gibi özelliklere sahip olabilirler. Yani, sınıf değişkenleri, özellikleri, kurucu metotları ve metotları tanımlayabilirler.
Yerel sınıflar, aynı zamanda metot içinde tanımlanmış olan değişkenlere erişebilirler. Ancak, erişebilecekleri değişkenlerin final
olarak tanımlanmış olması gerekmektedir. Bu, yerel sınıfların, metot içinde tanımlanmış olan değişkenlerin değerlerinin değişmesini önler ve yerel sınıfın ömrü boyunca bu değişkenlere erişebilmesini sağlar.
Yerel sınıfların en yaygın kullanımı, olay dinleyicileri gibi, bir sınıfın belirli bir özelliğinin veya işlevinin yürütülmesinde kullanılan geçici bir yardımcı sınıfın tanımlanmasıdır.
Aşağıdaki örnekte, bir printMessage()
metodu tanımlanmıştır ve bu metot içinde bir yerel sınıf olan MessagePrinter
tanımlanmıştır. Bu sınıf, bir print()
metodu içerir ve bu metot, printMessage()
metodu içindeki message
adlı değişkene erişebilir.
public class Main {
public void printMessage() {
final String message = "Hello, World!";
class MessagePrinter {
public void print() {
System.out.println(message);
}
}
MessagePrinter printer = new MessagePrinter();
printer.print();
}
public static void main(String[] args) {
Main main = new Main();
main.printMessage();
}
}
Yukarıdaki örnekte, printMessage()
metodu içinde MessagePrinter
adlı bir yerel sınıf tanımlanmıştır. Bu sınıf, print()
adında bir metot içerir ve bu metot, printMessage()
metodu içinde tanımlanan message
adlı değişkene erişebilir. main()
metodu içinde printMessage()
metodu çağrılır ve bu da MessagePrinter
sınıfının oluşturulmasına ve print()
metodu çağrılmasına neden olur.