Java ile Nesne Yönelimli Programlama · Ders Notu
Bölüm 4.5: Soyut Sınıflar ve Arayüzler
Kalıtım ortak kodu üst sınıfta toplar, çok biçimlilik de aynı çağrının her nesnede kendi sınıfının metodunu çalıştırmasını sağlar. Bu iki aracın bir eksiği kalır: üst sınıf bazen bir metodun gövdesini yazamaz. Genel bir “şekil”in alanı hesaplanamaz; formülü yalnızca kare, çember ya da üçgen bilir. Java bu durum için iki araç sunar: soyut sınıf ve arayüz. İkisi de “ne yapılacağını” söyler, “nasıl yapılacağını” alt tiplere bırakır. Not önce soyut sınıfı ve soyut metodu, ardından arayüzü, arayüzün gövdeli metotlarını ve mühürlü tipleri işler. Tek metotlu arayüzlerin kısa yazımı olan lambda ifadeleri, tip parametreli sınıflar (jenerikler) ve koda üst veri ekleyen etiketler de notun konuları arasındadır. Son kesim, arayüzlere dayanan beş tasarım desenini (Tekil, Fabrika, Gözlemci, Strateji ve MVC) küçük konsol programlarıyla anlatır. Bütün programlar JDK 21 ile derlenip çalıştırılmıştır; çıktılar, izleme tablolarındaki son değerler ve derleyici iletileri bu çalıştırmalardan alınmıştır.
Öğrenme çıktıları
- Soyut sınıf
abstractsınıf ve metot tanımlamak; soyut sınıftan neden nesne oluşturulamadığını ve somut alt sınıfın hangi metotları yazmak zorunda olduğunu açıklamak.- Şablon metot
- Soyut sınıfın somut bir metodunun, alt sınıfta yazılan soyut metodu çağırdığını kodu adım adım izleyerek göstermek.
- Arayüz
interfacetanımlamak;implementsile bir ya da birden çok arayüzü uygulamak;default,staticveprivatearayüz metotlarını kullanmak.- Lambda
- Fonksiyonel arayüzü tanımak; tek metotluk bir davranışı lambda ifadesiyle yazmak ve lambdanın ne zaman çalıştığını izlemek.
- Tasarım kararı
- Mühürlü tipleri tanımak; bir problem için arayüz mü soyut sınıf mı (ya da ikisi birden) gerektiğine karar vermek; jenerik tipleri ve etiketleri kullanmak.
- Tasarım desenleri
- Tekil, Fabrika, Gözlemci, Strateji ve MVC desenlerinin hangi sorunu çözdüğünü ve arayüze nasıl dayandığını açıklamak.
4.5.1Soyutlama
Araba kullanan biri direksiyonu, vitesi ve pedalları bilir. Motorun içinde yakıtın nasıl yandığını, şanzımanın dişlilerini nasıl değiştirdiğini bilmek zorunda değildir. Arabanın sürücüye sunduğu yüz basittir; karmaşık ayrıntı bu yüzün arkasında kalır. Üstelik aynı yüz farklı arabalarda da aynıdır. Benzinli bir arabayı kullanabilen biri elektrikli bir arabayı da kullanabilir, çünkü ikisi de direksiyon ve pedal sözleşmesine uyar.
Soyutlama (abstraction), karmaşık ayrıntıları gizleyip yalnızca temel özellikleri göstermektir. Programlamada soyutlama, bir işin “ne” yaptığını onu “nasıl” yaptığından ayırır.
Programda da aynı ayrım işe yarar. Bir çizim programı ekrandaki şekillerin toplam alanını hesaplamak istesin. Her şeklin bir alanı vardır; bu, “şekil” kavramının temel özelliğidir. Ama genel bir şeklin alanı hesaplanamaz. Kare için formül a · a, çember için π · r², üçgen için t · h / 2'dir. Formülü yalnızca somut şekil bilir. Şekil 4.5.1 bu durumu çizer: yukarıda ortak kavram, aşağıda ayrıntı durur.
Toplam alanı hesaplayan kod yalnızca “her şeklin bir alan() metodu vardır” bilgisine dayanmalıdır. Bu kod kare mi çember mi olduğunu sormaz, her şekilden alanını ister. Yarın programa altıgen eklenirse toplama kodu değişmez; yalnızca yeni sınıf yazılır. Soyutlamanın kazancı budur: kullanan kod ayrıntıya değil kavrama bağlanır.
Java'da soyutlamanın iki aracı vardır: soyut sınıflar (abstract class) ve arayüzler (interface). İkisi de ne yapılacağını söyler, nasıl yapılacağını alt sınıflara bırakır. Aralarındaki fark kısaca şudur. Soyut sınıf yakın akraba sınıfların ortak atasıdır; alanları, yapıcısı ve hazır metotları olabilir. Arayüz ise birbiriyle akraba olmayan sınıflara ortak bir yetenek kazandıran sözleşmedir.
4.5.2Soyut sınıflar
Kalıtımda üst sınıf, alt sınıfların ortak alanlarını ve metotlarını taşır. Sekil sınıfı bütün şekillerin ortak isim alanını ve tasi metodunu yazabilir. alan() metodunda ise takılır. Gövdeye ne yazılacaktır? return 0; yazmak yanlıştır. Hiçbir şeklin alanı gerçekten 0 değildir. Bir alt sınıf metodu geçersiz kılmayı unutursa program sessizce yanlış sonuç üretir. Doğru çözüm, gövdeyi hiç yazmamak ve bunu derleyiciye açıkça söylemektir.
4.5.2.1Soyut sınıf tanımlamak
Soyut sınıf (abstract class), abstract anahtar kelimesiyle tanımlanan ve nesnesi oluşturulamayan sınıftır. Soyut sınıf hem soyut metotlar (gövdesiz) hem de somut metotlar (concrete, gövdeli) içerebilir. Soyut metotların hepsini yazan alt sınıfa somut sınıf denir; nesnesi ancak böyle bir sınıftan oluşturulur.
public abstract class Sekil {
// Soyut olmayan (somut) metot
void tasi(int x, int y) {
System.out.println("(" + x + ", " + y + ") noktasına taşındı");
}
// Soyut metot (alt sınıflarca uygulanmalı)
abstract void ciz();
}Bu Sekil sınıfında iki metot vardır. tasi somuttur: gövdesi hazırdır. Alt sınıflar onu olduğu gibi kalıtımla alır, isterse geçersiz kılar (override). Şekli bir noktaya taşımak her şekil için aynı işi gerektirdiği için gövdeyi üst sınıfta bir kez yazmak doğrudur. ciz ise soyuttur: yalnızca imzası vardır. Bir kareyi çizmekle bir çemberi çizmek farklı işlerdir; bu yüzden her somut alt sınıf kendi ciz() gövdesini yazmak zorundadır.
Soyut sınıf yarım kalmış bir plana benzer. Bazı odalar bitmiştir, bazılarının ise yalnızca yeri bellidir. Planı devralan mimar bitmiş odalara dokunmadan boş yerleri doldurur. Aşağıdaki küçük program sınıfı bir Kare ile tamamlar ve iki metodu da çağırır:
abstract class Sekil {
// Soyut olmayan (somut) metot
void tasi(int x, int y) {
System.out.println("(" + x + ", " + y + ") noktasına taşındı");
}
// Soyut metot (alt sınıflarca uygulanmalı)
abstract void ciz();
}
class Kare extends Sekil {
void ciz() { System.out.println("Kare çiziliyor"); }
}
public class D03 {
public static void main(String[] args) {
Sekil s = new Kare();
s.tasi(3, 4);
s.ciz();
}
}(3, 4) noktasına taşındı
Kare çiziliyorKare yalnızca ciz()'i yazmıştır; tasi'yi Sekil'den alır. Değişkenin tipi soyut Sekil'dir ama gösterdiği nesne bir Kare'dir. s.ciz() çağrısında hangi gövdenin çalışacağı, çok biçimlilikte olduğu gibi, çalışma zamanında nesnenin sınıfına göre seçilir. Bu karara dinamik bağlama (dynamic binding) denir.
Soyut sınıfın öteki özellikleri sıradan bir sınıfınkiyle aynıdır. Alanları, yapıcıları, static üyeleri olabilir. Başka sınıflar onu extends ile genişletir. Şekil 4.5.2 dört metotlu bir Sekil sınıfını ve onu genişleten üç somut sınıfı çizer. Çizim UML (Unified Modeling Language, birleşik modelleme dili) gösterimindedir.
Sekil'in isim alanı, yapıcısı ve getIsim() metodu somuttur; alan() ve ciz() soyuttur. Kare, Cember ve Dikdortgen kendi alanlarını taşır ve alan()'ı kendi formülleriyle yazar.Şekildeki tasarımın getirdiği kazanç, ortak durumun (alanların) ve davranışın tek bir yerde toplanmasıdır. isim alanı ve onu okuyan getIsim() üç sınıfta ayrı ayrı yazılmaz. Alt sınıflar soyut metotları kendi ihtiyacına göre doldurur. Soyut sınıflar bu yüzden çoğu zaman kalıtım hiyerarşisinin tepesinde yer alır.
4.5.2.2Soyut sınıftan nesne oluşturulamaz
Soyut sınıfın en belirgin kuralı, ondan new ile nesne oluşturulamamasıdır. Aşağıdaki üç satırdan ilki derlenmez, öteki ikisi derlenir:
Sekil s1 = new Sekil("?"); // HATA
Sekil s2 = new Kare(3); // Doğru: değişkenin türü soyut olabilir
Sekil[] dizi = new Sekil[3]; // Doğru: dizi yalnızca referans tutarSoyutNew.java:8: error: Sekil is abstract; cannot be instantiated
Sekil s1 = new Sekil("?");
^
1 errorYasak keyfî değildir. Sekil'in alan() metodunun gövdesi yoktur. new Sekil("?") izinli olsaydı s1.alan() çağrısında çalıştırılacak kod bulunmazdı. Derleyici bu durumu baştan engeller: gövdesi eksik bir nesne hiç oluşmaz.
Soyut sınıfın adı ise referans tipi olarak serbestçe kullanılır. Değişken, parametre, dizi öğesi ve dönüş tipi Sekil olabilir. İkinci satırdaki Sekil s2 = new Kare(3); tam da soyutlamanın istediği yazımdır: değişkenin tipi kavram, nesnenin sınıfı ayrıntıdır. Üçüncü satır öğrencileri sık şaşırtır. new Sekil[3] bir Sekil nesnesi oluşturmaz; üç gözlü bir dizi oluşturur ve her göz null başlar. Dizi nesnesi referans tutar, soyut sınıfın nesnesini değil. Satırlar çalıştırılınca bu açıkça görülür:
abstract class Sekil {
private String isim;
Sekil(String isim) { this.isim = isim; }
abstract double alan();
}
class Kare extends Sekil {
private double kenar;
Kare(double kenar) { super("Kare"); this.kenar = kenar; }
double alan() { return kenar * kenar; }
}
public class D05 {
public static void main(String[] args) {
Sekil s2 = new Kare(3); // Doğru: değişkenin türü soyut olabilir
Sekil[] dizi = new Sekil[3]; // Doğru: dizi yalnızca referans tutar
System.out.println(s2.alan());
System.out.println(dizi.length + " " + dizi[0]);
}
}9.0
3 nullBir sınıf hem abstract hem final olamaz. final sınıfın genişletilmesini yasaklar; abstract ise genişletilmeyi gerektirir, çünkü nesne ancak bir alt sınıftan oluşturulabilir. İki belirteç birbiriyle çelişir:
abstract final class Sekil { }
public class SoyutFinal { }SoyutFinal.java:1: error: illegal combination of modifiers: abstract and final
abstract final class Sekil { }
^
1 error4.5.2.3Soyut metotlar ve kuralları
Soyut metot (abstract method), gövdesi olmadan, yalnızca imzasıyla tanımlanan metottur. Süslü parantez içermez, noktalı virgülle biter. Somut her alt sınıf onu uygulamak, yani gövdesini yazmak zorundadır.
public abstract void ciz();Soyut metodu olan sınıfın kendisi de soyut olmak zorundadır. Aşağıdaki iki dosyada Sekil soyut ciz() metodunu bildirir, Cember ise onu uygular:
public abstract class Sekil {
// Soyut metot
public abstract void ciz();
}
// Ayrı bir dosyada:
public class Cember extends Sekil {
// ciz metodunu uygulamak zorunlu
public void ciz() {
System.out.println("Çember çiziliyor");
}
}Çember çiziliyornew Cember().ciz(); çağrısı Cember'in yazdığı gövdeyi çalıştırır. Soyut metodun uygulanması, sıradan bir metodun geçersiz kılınmasıyla aynı kurallara uyar. İmza (ad ve parametre listesi) birebir aynı olmalı, erişim daraltılmamalıdır. Üst sınıftaki metot public olduğu için Cember'deki de public'tir.
Soyut metotlarla ilgili kurallar kısa bir tabloya sığar. Tablo 4.5.1 doğru ve yanlış yazımları gerçek derleyici iletileriyle birlikte verir.
| Yazılan | Sonuç |
|---|---|
abstract void ciz(); (soyut sınıfta) | Doğru |
abstract void ciz() { } | abstract methods cannot have a body |
class Sekil { abstract void ciz(); } | Sekil is not abstract and does not override abstract method ciz() in Sekil |
class Cember extends Sekil { } (ciz yok) | Cember is not abstract and does not override abstract method ciz() in Sekil |
private abstract, static abstract, final abstract | illegal combination of modifiers: abstract and private (static ve final için de aynı ileti) |
abstract class Cember extends Sekil { } | Doğru: soyut alt sınıf gerçeklemeyi kendi alt sınıflarına bırakabilir |
abstract class A { void f() { } } | Doğru: hiç soyut metodu olmayan soyut sınıf da yazılabilir; yine de new A() yapılamaz |
Tablonun üçüncü satırındaki ileti ilk bakışta tuhaf görünür: “Sekil soyut değil ve Sekil'deki soyut ciz() metodunu geçersiz kılmıyor.” Derleyici, soyut metodu olan her sınıfa iki seçenek sunar: ya sınıf soyut olur ya da metodu yazar. Kendi bildirdiği metodu kendisinin yazmaması, aynı iletiyi doğurur. Dördüncü satır aynı kuralın alt sınıftaki karşılığıdır.
Beşinci satırdaki üç belirteç neden yasaktır? Üçü de metodun alt sınıfta geçersiz kılınmasını engeller. private metot alt sınıfa görünmez. static metot nesneye değil sınıfa bağlıdır ve dinamik bağlamaya girmez. final metot ise geçersiz kılınamaz. Soyut metot ise yalnızca geçersiz kılınmak için vardır. İletinin static sürümü şöyledir:
abstract class Sekil {
static abstract void ciz();
}
public class StaticSoyut { }StaticSoyut.java:2: error: illegal combination of modifiers: abstract and static
static abstract void ciz();
^
1 errorSon iki satır izin verilen ama az bilinen iki durumu gösterir. Soyut bir sınıfın alt sınıfı da soyut olabilir; o zaman metodu yazma yükümlülüğü bir alt kuşağa geçer. Hiç soyut metodu olmayan bir sınıf da abstract işaretlenebilir. Bu, “bu sınıftan doğrudan nesne oluşturulmasın, yalnızca genişletilsin” demenin yoludur. Aşağıdaki programda her iki durum da derlenip çalışır:
abstract class Sekil { abstract void ciz(); }
abstract class Cember extends Sekil { }
abstract class A { void f() { System.out.println("f"); } }
class Daire extends Cember { void ciz() { System.out.println("Daire çiziliyor"); } }
public class Main {
public static void main(String[] args) {
Sekil s = new Daire();
s.ciz();
A a = new A() { }; // adsız somut alt sınıf
a.f();
}
}Daire çiziliyor
fCember soyut kaldığı için ciz()'i yazmamıştır; onu Cember'i genişleten somut Daire yazar. new A() { } ifadesi ise A'nın nesnesini değil, A'yı genişleten adsız bir alt sınıfın nesnesini oluşturur. Bu yazıma anonim sınıf denir.
Soyut metodun imzasını yanlış yazmak. Sekil'deki soyut metot double alan() iken alt sınıfta double Alan() ya da double alan(int x) yazılırsa bu yeni bir metottur. Soyut alan() hâlâ eksiktir ve derleyici sınıfı reddeder:
ImzaYanlis.java:2: error: Kare is not abstract and does not override abstract method alan() in Sekil
class Kare extends Sekil {
^
1 errorİleti hatanın yerini değil, sonucunu gösterir; yazım hatası gözden kaçabilir. Metodun üstüne @Override yazılırsa derleyici asıl hatayı doğrudan söyler: method does not override or implement a method from a supertype.
4.5.2.4Soyut sınıfın yapıcısı
Soyut sınıftan nesne oluşturulamıyorsa yapıcısı neye yarar? Cevap, alt sınıf nesnesinin kuruluşundadır. Bir Kare nesnesi oluşturulurken nesnenin Sekil'den gelen kısmı da kurulmalıdır: isim alanına değer verilmelidir. Bu işi Sekil'in yapıcısı yapar ve alt sınıf onu super(...) ile çağırır. Aşağıdaki programda Sekil yapıcısı bir de ileti yazdırır; böylece çağrının ne zaman yapıldığı çıktıda görülür.
abstract class Sekil {
private String isim;
Sekil(String isim) {
this.isim = isim;
System.out.println(isim + " kuruldu");
}
abstract double alan();
}
class Kare extends Sekil {
private double kenar;
Kare(double kenar) {
super("Kare");
this.kenar = kenar;
}
double alan() { return kenar * kenar; }
}
// main içinde:
Sekil s = new Kare(3);
System.out.println(s.alan());Kare kuruldu
9.0Tablo 4.5.2 programın on adımını verir. Satır numaraları yukarıdaki kodun satırlarıdır. “Çağrı yığını” sütunu o anda yığında duran çerçeveleri soldan sağa, en alttakinden en üsttekine doğru sıralar.
| Adım | Satır | Çağrı yığını | Değişkenler | Yeni çıktı |
|---|---|---|---|---|
| 1 | 18: Sekil s = new Kare(3); | main() | s=— | — |
| 2 | 11: Kare(double kenar) { | main() → Kare(3) | kenar=3.0 | — |
| 3 | 12: super("Kare"); | main() → Kare(3) | kenar=3.0 | — |
| 4 | 3: Sekil(String isim) { | main() → Kare(3) → Sekil("Kare") | isim="Kare" | — |
| 5 | 4: this.isim = isim; | main() → Kare(3) → Sekil("Kare") | isim="Kare" | — |
| 6 | 5: System.out.println(isim + " kuruldu"); | main() → Kare(3) → Sekil("Kare") | isim="Kare" | Kare kuruldu |
| 7 | 13: this.kenar = kenar; | main() → Kare(3) | kenar=3.0 | — |
| 8 | 18: Sekil s = new Kare(3); | main() | s=→ Kare | — |
| 9 | 15: double alan() { return kenar * kenar; } | main() → Kare.alan() | kenar=3.0 | — |
| 10 | 19: System.out.println(s.alan()); | main() | s=→ Kare | 9.0 |
Birinci adımda new Kare(3) heap'te bir Kare nesnesi için yer ayırır. Nesnenin iki alanı vardır: Sekil'den gelen isim ve Kare'ye ait kenar. İkisi de varsayılan değerini alır: null ve 0.0. İkinci adımda Kare'nin yapıcısı çalışmaya başlar. Argüman int tipindeki 3'tür, parametre ise double'dır; değer otomatik olarak 3.0'a genişletilir.
new nesneyi ayırmış, alanlar varsayılan değerde. Ortada Sekil yapıcısı nesnenin Sekil'den gelen kısmını kurmuş: isim = "Kare". Sağda Kare yapıcısı da bitmiş ve referans s'ye atanmış.Üçüncü adımda Kare yapıcısının ilk satırı super("Kare") çalışır ve üst sınıfın yapıcısı çağrılır. Dördüncü adımda yığına Sekil("Kare") çerçevesi eklenir. Burada çok önemli bir ayrıntı vardır: ayrı bir Sekil nesnesi oluşmaz. Heap'te hâlâ tek bir nesne vardır. Sekil yapıcısı aynı nesnenin Sekil'den gelen kısmını kurar. Beşinci adımda this.isim = isim bu kısmın alanını "Kare" yapar, altıncı adımda ileti yazdırılır ve Sekil'in çerçevesi yığından çıkar.
Yedinci adımda denetim Kare'nin yapıcısına döner ve this.kenar = kenar çalışır. Nesne artık tamamen kurulmuştur. Sekizinci adımda new ifadesinin değeri olan referans s'ye atanır. Değişkenin tipi soyut Sekil'dir, gösterdiği nesne ise bir Kare'dir. Şekil 4.5.3'ün sağındaki çizim bu anı gösterir.
Dokuzuncu adım soyutlamanın asıl işini yapar. s.alan() derlenirken derleyici yalnızca Sekil'e bakar: Sekil'de alan() adlı, double döndüren bir metot vardır, bu yüzden çağrı geçerlidir. Çalışma zamanında ise JVM nesnenin gerçek sınıfına bakar ve Kare.alan()'ı çalıştırır: 3.0 · 3.0 = 9.0. Onuncu adımda bu değer yazdırılır.
Yapıcı zincirinin sırası kalıtımdaki sırayla aynıdır: önce üst sınıfın yapıcısı, sonra alt sınıfınki. Soyut sınıf bu sırayı değiştirmez; yalnızca zincirin en üst halkasından doğrudan nesne oluşturulmasını yasaklar. Sekil'in parametresiz yapıcısı olmadığı için Kare'nin yapıcısı super("Kare")'yi yazmak zorundadır. Yazmasaydı derleyici super() çağrısını kendisi ekler ve parametresiz yapıcıyı bulamadığı için hata verirdi.
4.5.2.5Şablon metot
Soyut sınıfın somut bir metodu, aynı sınıfın soyut bir metodunu çağırabilir. İlk duyuşta bu çelişkili görünür: gövdesi olmayan bir metot nasıl çağrılır? Çağrı derlenir, çünkü metodun imzası bellidir. Çalışırken ise this her zaman somut bir alt sınıfın nesnesini gösterir ve o nesnenin sınıfında metodun gövdesi vardır. Aşağıdaki Icecek sınıfı bir içeceğin hazırlanma sırasını belirler:
abstract class Icecek {
final void hazirla() {
suyuKaynat();
demle();
System.out.println("Servis edildi");
}
void suyuKaynat() {
System.out.println("Su kaynadı");
}
abstract void demle();
}
class Cay extends Icecek {
void demle() {
System.out.println("Çay demlendi");
}
}
// main içinde:
Icecek i = new Cay();
i.hazirla();Su kaynadı
Çay demlendi
Servis edildihazirla() üç adımı sırayla çağırır: suyu kaynatmak, demlemek, servis etmek. Su kaynatmak her içecekte aynıdır; suyuKaynat() somut olarak Icecek'te yazılmıştır. Demleme içeceğe göre değişir; demle() soyuttur ve Cay onu kendi gövdesiyle doldurur. hazirla() ise final'dır: alt sınıflar onu geçersiz kılamaz, yani hazırlama sırasını bozamaz.
| Adım | Satır | Çağrı yığını | Değişkenler | Yeni çıktı |
|---|---|---|---|---|
| 1 | 18: Icecek i = new Cay(); | main() | i=→ Cay | — |
| 2 | 2: final void hazirla() { | main() → hazirla() | this=→ Cay | — |
| 3 | 3: suyuKaynat(); | main() → hazirla() | this=→ Cay | — |
| 4 | 8: System.out.println("Su kaynadı"); | main() → hazirla() → suyuKaynat() | this=→ Cay | Su kaynadı |
| 5 | 4: demle(); | main() → hazirla() | this=→ Cay | — |
| 6 | 14: System.out.println("Çay demlendi"); | main() → hazirla() → Cay.demle() | this=→ Cay | Çay demlendi |
| 7 | 5: System.out.println("Servis edildi"); | main() → hazirla() | this=→ Cay | Servis edildi |
| 8 | 19: i.hazirla(); | main() | i=→ Cay | — |
Tablo 4.5.3 çağrıyı adım adım verir. Birinci adımda değişkenin tipi soyut Icecek, nesnesi Cay'dir. Cay tek soyut metodu demle()'yi uyguladığı için somuttur ve nesnesi oluşturulabilir. İkinci adımda i.hazirla() çağrılır. Cay'de hazirla() yoktur; kalıtımla alınan Icecek sürümü çalışır. Üçüncü ve dördüncü adımda suyuKaynat(), yani this.suyuKaynat() çağrılır ve Icecek'teki somut gövde “Su kaynadı” yazar.
hazirla()'nın içinde demle() çağrısına gelinmiş; Icecek'te bu metodun gövdesi yoktur. Sağdaki an: this bir Cay nesnesi olduğu için çağrı Cay.demle()'ye gider.Beşinci adım kritik andır. hazirla() demle()'yi çağırır. Icecek'te bu metodun gövdesi yoktur, ama this bir Cay nesnesidir. Dinamik bağlama çağrıyı Cay.demle()'ye yönlendirir ve altıncı adımda “Çay demlendi” yazılır (Şekil 4.5.4). Soyut sınıftaki kod, kendisi yazıldığında henüz var olmayan bir alt sınıfın kodunu çağırmıştır. Yedinci adımda servis iletisi yazdırılır.
Şablon metot (template method), bir algoritmanın iskeletini üst sınıfta tanımlayan, değişen adımları ise alt sınıflara bırakan tasarımdır. İskeleti kuran metot genellikle final'dır; değişen adımlar soyut metotlardır.
Şablon metodun gücü yeni bir içecek eklenince görülür. Bir Kahve sınıfı yalnızca demle()'yi yazar; su kaynatma ve servis adımlarını, sıralarıyla birlikte, hazır alır:
abstract class Icecek {
final void hazirla() {
suyuKaynat();
demle();
System.out.println("Servis edildi");
}
void suyuKaynat() {
System.out.println("Su kaynadı");
}
abstract void demle();
}
class Cay extends Icecek {
void demle() {
System.out.println("Çay demlendi");
}
}
class Kahve extends Icecek {
void demle() {
System.out.println("Kahve süzüldü");
}
}
public class Main {
public static void main(String[] args) {
Icecek[] siparis = { new Cay(), new Kahve() };
for (Icecek i : siparis) {
i.hazirla();
}
}
}Su kaynadı
Çay demlendi
Servis edildi
Su kaynadı
Kahve süzüldü
Servis edildiDöngü iki nesnede de aynı hazirla()'yı çağırır. İskelet değişmez; ortadaki satır her nesnede kendi sınıfının metodunu çalıştırır. Hazırlama sırası tek bir yerde yazılı olduğu için, örneğin servis öncesine bir “bardağı ısıt” adımı eklemek bütün içecekleri birlikte değiştirir.
final şablon metodu alt sınıfta geçersiz kılmaya çalışmak. Şablon metot iskeleti korumak için final yazılır. Alt sınıf onu yeniden yazmak isterse derleyici reddeder:
FinalHazirla.java:7: error: hazirla() in Cay cannot override hazirla() in Icecek
void hazirla() { }
^
overridden method is final
1 errorSıranın bir kısmı gerçekten değişebilir olmalıysa şablon bozulmaz. İskelete gövdeli ama geçersiz kılınabilen bir “kanca” metot eklenir (örneğin boolean sut() { return false; }).
4.5.3Arayüzler
Soyut sınıf, akraba sınıfların ortak atası olarak iyi çalışır. Kare de çember de birer şekildir. Ama bazı ortak noktalar akrabalıktan doğmaz. Bir kuş, bir uçak ve bir insansız hava aracı uçabilir; üçünün ortak bir atası yoktur ve olmamalıdır. Bir ördek hem uçar hem yüzer; tek bir üst sınıfı olduğu için iki ayrı soyut sınıftan aynı anda türeyemez. Java bu ihtiyaç için ikinci bir soyutlama aracı sunar: arayüz.
4.5.3.1Arayüz bir sözleşmedir
Arayüz (interface), sınıfların uygulaması gereken metotları belirleyen bir tiptir. interface anahtar kelimesiyle tanımlanır. Bir sınıf implements ile arayüzü uygular (implement), yani arayüzdeki soyut metotların gövdesini yazar.
Arayüz bir sözleşmedir (contract). Onu uygulayan her sınıfın belirli metotları sunacağını garanti eder. İki yazılım parçası arasındaki buluşma noktasıdır: kullanan taraf yalnızca arayüzü bilir, sözleşmenin arkasındaki sınıfı tanımaz. Şekil 4.5.5 bu ilişkiyi çizer.
GeometrikSekil arayüzünü ve onun ciz() metodunu bilir. Kare ve Cember aynı sözleşmeyi uygular (kesikli oklar).Gündelik bir benzetme arayüzü iyi anlatır: priz ve fiş. Priz bir sözleşmedir. Hangi cihaz takılırsa takılsın aynı gerilimi verir. Ütü de bilgisayar da prizin standardına uyar. Şebeke cihazın içini bilmez, cihaz da şebekenin nasıl kurulduğunu bilmez. Yeni bir cihaz üretmek için şebekeyi değiştirmek gerekmez; standarda uymak yeter.
Arayüzün kuralları sınıfınkinden farklıdır:
- Arayüzdeki metotlar,
publicyazılsın ya da yazılmasınpublic'tir. - Bir metot
static,defaultya daprivatedeğilse kendiliğindenabstract'tır. - Alan olarak yalnızca
public static finalsabitler bulunabilir. Örnek alanı (instance field) ve yapıcı yoktur.
Soyut sınıf ile arayüz arasındaki farkı iki ilişki adıyla özetlemek mümkündür. Soyut sınıf “bir türüdür” (is-a) ilişkisi kurar: kare bir şekildir. Arayüz ise “yapabilir” (can-do) yeteneği tanımlar: ördek uçabilir, ördek yüzebilir.
4.5.3.2Arayüz tanımlamak
Aşağıda solda programcının yazdığı arayüz, sağda derleyicinin onu nasıl gördüğü verilmiştir. Derleyici, yazılmayan belirteçleri kendisi ekler:
public interface GeometrikSekil {
// Soyut metot (public abstract)
void ciz();
// Sabit (public static final)
int KENAR_SAYISI = 4;
}public interface GeometrikSekil {
public abstract void ciz();
public static final int KENAR_SAYISI = 4;
}void ciz(); satırı public abstract void ciz(); olarak okunur. int KENAR_SAYISI = 4; satırı ise public static final bir sabittir. Sabitlere arayüzün adıyla erişilir: GeometrikSekil.KENAR_SAYISI 4 verir. Sabit adları geleneksel olarak BÜYÜK_HARF ile yazılır. Bu örtük belirteçlerin üç sonucu vardır, üçünü de derleyici denetler.
Birincisi, arayüzdeki bir sabite değer vermek zorunludur. Değer verilmemiş bir final alan sonradan da değer alamaz, çünkü arayüzün yapıcısı yoktur:
interface GeometrikSekil {
int deger;
}
public class SabitDegersiz { }SabitDegersiz.java:2: error: = expected
int deger;
^
1 errorİkincisi, sabit değiştirilemez. final bir alana ikinci kez değer atanamaz:
interface GeometrikSekil {
void ciz();
int KENAR_SAYISI = 4;
}
public class SabitAta {
public static void main(String[] args) {
GeometrikSekil.KENAR_SAYISI = 5;
}
}SabitAta.java:7: error: cannot assign a value to static final variable KENAR_SAYISI
GeometrikSekil.KENAR_SAYISI = 5;
^
1 errorÜçüncüsü, arayüzden de new ile nesne oluşturulamaz. İleti soyut sınıftakiyle aynıdır, çünkü arayüz de örtük olarak soyuttur:
ArayuzNew.java:6: error: GeometrikSekil is abstract; cannot be instantiated
GeometrikSekil g = new GeometrikSekil();
^
1 errorArayüzün örnek alanı olmaması, durum (state) tutamaması demektir. Bir arayüz, “bir sayaç değeri saklar” diyemez; yalnızca “deger() diye bir metot sunar” diyebilir. Değeri saklamak onu uygulayan sınıfın işidir. Arayüzde int deger = 0; yazılırsa bu bir sayaç değil, değişmez bir sabittir; onu artırmaya çalışan bir metot derlenmez:
interface Sayac {
int deger = 0;
default void arttir() { deger++; }
}
public class ArayuzAlan { }ArayuzAlan.java:3: error: cannot assign a value to static final variable deger
default void arttir() { deger++; }
^
1 error4.5.3.3Arayüzü uygulamak
Bir sınıf bir arayüzü implements anahtar kelimesiyle uygular. Sınıf, arayüzdeki bütün soyut metotları yazmalı ya da kendisi abstract olmalıdır. Aşağıdaki Kare, GeometrikSekil'in tek soyut metodu olan ciz()'i yazar:
public class Kare implements GeometrikSekil {
// ciz metodunu uygulamak zorunlu
public void ciz() {
System.out.println("Kare çiziliyor");
}
}
// main içinde:
GeometrikSekil g = new Kare();
g.ciz();
System.out.println(GeometrikSekil.KENAR_SAYISI);Kare çiziliyor
4GeometrikSekil g = new Kare(); satırında değişkenin tipi arayüzdür. Arayüz tipindeki bir değişken, o arayüzü uygulayan her sınıfın nesnesini gösterebilir. g.ciz() çağrısı derlenirken derleyici arayüze bakar; çalışırken Kare'nin metodu çalışır. Son satır sabite arayüz adıyla erişir ve 4 yazdırır.
Uygulayan metot public olmalıdır. Arayüzdeki metot örtük olarak public'tir ve geçersiz kılan bir metot erişimi daraltamaz. Sınıfta yalnızca void ciz() yazılırsa metot paket erişimli olur ve derleyici bunu erişimi zayıflatmak olarak görür:
interface GeometrikSekil {
void ciz();
}
class Kare implements GeometrikSekil {
void ciz() { System.out.println("Kare çiziliyor"); }
}
public class ArayuzZayif { }ArayuzZayif.java:5: error: ciz() in Kare cannot implement ciz() in GeometrikSekil
void ciz() { System.out.println("Kare çiziliyor"); }
^
attempting to assign weaker access privileges; was public
1 errorMetot hiç yazılmazsa ileti soyut sınıftakinin aynısıdır; tek fark, eksik metodun hangi tipte bildirildiğini söyleyen son kelimedir:
ArayuzEksik.java:4: error: Kare is not abstract and does not override abstract method ciz() in GeometrikSekil
class Kare implements GeometrikSekil { }
^
1 errorArayüz metodunu public yazmayı unutmak. Arayüzde public yazılmadığı için sınıfta da yazılmayabileceği düşünülür. Oysa arayüzdeki belirteç örtüktür, sınıftaki değildir. Sınıfta belirteç yazılmazsa metot paket erişimli olur ve attempting to assign weaker access privileges; was public iletisi gelir. Arayüz metodu sınıfta her zaman public yazılır.
4.5.3.4Bir sınıf, birden çok arayüz
Java'da bir sınıf en çok bir üst sınıfı genişletebilir. Buna karşılık sıfır ya da daha çok arayüzü uygulayabilir: implements A, B, …. İkisi birlikte yazılacaksa önce extends, sonra implements gelir. Aşağıdaki Ordek sınıfı Hayvan'dan türer ve iki yeteneği birden üstlenir:
class Hayvan {
String isim;
Hayvan(String isim) { this.isim = isim; }
}
interface Ucabilir { void uc(); }
interface Yuzebilir { void yuz(); }
class Ordek extends Hayvan implements Ucabilir, Yuzebilir {
Ordek(String isim) { super(isim); }
public void uc() { System.out.println(isim + " uçuyor"); }
public void yuz() { System.out.println(isim + " yüzüyor"); }
}Ordek sınıfı Hayvan'ı genişletir (düz çizgi, içi boş üçgen) ve Ucabilir ile Yuzebilir arayüzlerini uygular (kesikli çizgi).Ordek'in yapıcısı super(isim) ile Hayvan'ın yapıcısını çağırır ve isim alanını ondan alır. uc() ve yuz() metotları iki arayüzün sözleşmesini yerine getirir; ikisi de public'tir. Şekil 4.5.6 bu üç ilişkiyi UML'nin iki çizgi türüyle gösterir: kalıtım düz, arayüzü uygulama kesikli çizilir.
İki üst sınıf yazmak ise söz dizimi hatasıdır. Derleyici extends'ten sonra tek bir ad bekler; virgülü görünce sınıf gövdesinin başlamasını bekler:
class A { }
class B { }
class C extends A, B { }
public class IkiExtends { }IkiExtends.java:3: error: '{' expected
class C extends A, B { }
^
1 errorBir Ordek nesnesi artık üç farklı tipten görülebilir: Ordek, Ucabilir ve Yuzebilir. Aşağıdaki program tek bir nesneyi üç farklı referansla tutar:
Ordek o = new Ordek("Donald");
Ucabilir u = o;
Yuzebilir y = o;
u.uc();
y.yuz();
// u.yuz(); derleme hatası!
System.out.println(o.isim);
System.out.println(u == o);Donald uçuyor
Donald yüzüyor
Donald
true| Adım | Satır | Değişkenler | Yeni çıktı |
|---|---|---|---|
| 1 | 1: Ordek o = new Ordek("Donald"); | o=→ Ordek | — |
| 2 | 2: Ucabilir u = o; | o=→ Ordek, u=→ Ordek | — |
| 3 | 3: Yuzebilir y = o; | o=→ Ordek, u=→ Ordek, y=→ Ordek | — |
| 4 | 4: u.uc(); | o=→ Ordek, u=→ Ordek, y=→ Ordek | Donald uçuyor |
| 5 | 5: y.yuz(); | o=→ Ordek, u=→ Ordek, y=→ Ordek | Donald yüzüyor |
| 6 | 6: // u.yuz(); derleme hatası! | o=→ Ordek, u=→ Ordek, y=→ Ordek | — |
| 7 | 7: System.out.println(o.isim); | o=→ Ordek, u=→ Ordek, y=→ Ordek | Donald |
| 8 | 8: System.out.println(u == o); | o=→ Ordek, u=→ Ordek, y=→ Ordek | true |
Tablo 4.5.4'ün ilk üç adımı yalnızca atama yapar. Birinci adımda heap'te tek bir Ordek nesnesi oluşur ve o onu gösterir. İkinci adımdaki Ucabilir u = o; nesneyi kopyalamaz; u aynı nesneyi gösterir. Ördek bir Ucabilir olduğu için bu dönüşüm otomatiktir, açık dönüşüm (cast) gerekmez. Üçüncü adımda y da aynı nesneyi gösterir. Artık tek nesneye üç farklı tipten bakılır (Şekil 4.5.7).
o, u ve y değişkenlerinin tipleri farklıdır, ama üçü de heap'teki aynı Ordek nesnesini gösterir.Dördüncü adımda u.uc() çağrılır. u'nun tipi Ucabilir'dir ve bu tipten yalnızca uc() çağrılabilir. Çalışan, nesnenin kendi uc() metodudur: “Donald uçuyor”. Beşinci adımda y.yuz() çalışır; Yuzebilir tipi yuz()'ü bilir. Altıncı adım yorum satırındaki çağrıyı tartışır. u.yuz() yazılsaydı derleyici reddederdi:
RefTip.java:10: error: cannot find symbol
u.yuz();
^
symbol: method yuz()
location: variable u of type Ucabilir
1 errorNesnenin yuz() metodu vardır, ama derleyici nesneye değil referansın tipine bakar. Ucabilir sözleşmesinde yuz() yoktur. Hangi metotların çağrılabileceğini referansın tipi belirler; hangi gövdenin çalışacağını nesnenin sınıfı belirler. Bu iki kural çok biçimliliğin özüdür. Yedinci adımda o.isim okunur; o'nun tipi Ordek olduğu için alan dahil her üye erişilebilir. Son adımda u == o iki referansı karşılaştırır. İkisi aynı nesneyi gösterdiği için sonuç true'dur.
4.5.3.5Arayüzden kalıtım
Bir arayüz başka bir arayüzü extends ile genişletebilir. Arayüzler arasında çoklu kalıtım serbesttir: interface C extends A, B yazılabilir. Genişleten arayüz, üst arayüzün bütün metotlarını devralır ve yenilerini ekler. Aşağıdaki KareSekli, GeometrikSekil'in ciz() metoduna bir de ozelFonksiyon() ekler:
interface GeometrikSekil {
void ciz();
int KENAR_SAYISI = 4;
}
interface KareSekli extends GeometrikSekil {
// Ek metotlar veya sabitler eklenebilir
void ozelFonksiyon();
}
class OzelKare implements KareSekli {
public void ciz() {
System.out.println("Özel kare çiziliyor");
}
public void ozelFonksiyon() {
System.out.println("Köşegen: " + Math.sqrt(2));
}
}
public class Main {
public static void main(String[] args) {
KareSekli k = new OzelKare();
k.ciz();
k.ozelFonksiyon();
GeometrikSekil g = k; // üst arayüze atanabilir
System.out.println(g instanceof KareSekli);
}
}Özel kare çiziliyor
Köşegen: 1.4142135623730951
trueKareSekli arayüzü GeometrikSekil'i genişletir; OzelKare sınıfı KareSekli'ni uygular ve iki metodu da yazar.OzelKare yalnızca KareSekli'ni uyguladığını bildirir, ama sözleşme iki metodu da kapsar. Biri eksik kalsaydı sınıf derlenmezdi. OzelKare nesnesi dolaylı olarak bir GeometrikSekil'dir; bu yüzden GeometrikSekil g = k; dönüşümsüz derlenir (Şekil 4.5.8). Programın son satırı bir instanceof sınamasıdır ve true yazar: üst arayüz tipindeki referans hâlâ aynı nesneyi gösterir. Köşegen satırındaki 1.4142135623730951, kenarı 1 olan bir karenin köşegeni olan √2'nin double gösterimidir.
4.5.3.6Arayüz tipiyle programlama
Arayüzün asıl gücü, kullanan kodu somut sınıflardan bağımsız kılmasıdır. Bunun yolu, değişken, parametre ve dönüş tiplerini somut sınıf yerine arayüz olarak yazmaktır. Bir alışveriş sitesinin ödeme adımını düşünelim. Ödeme kartla ya da havaleyle yapılabilir; yarın kripto para da eklenebilir.
interface OdemeYontemi { void ode(double tutar); }
class KrediKarti implements OdemeYontemi {
public void ode(double tutar) {
System.out.println("Kart: " + tutar + " TL");
}
}
class Havale implements OdemeYontemi {
public void ode(double tutar) {
System.out.println("Havale: " + (tutar + 5) + " TL");
}
}
static void satinAl(OdemeYontemi y, double toplam) {
y.ode(toplam);
}
satinAl(new KrediKarti(), 250.0); // main
satinAl(new Havale(), 120.0);Kart: 250.0 TL
Havale: 125.0 TLsatinAl(OdemeYontemi y, double toplam) metodu ödemenin kartla mı havaleyle mi yapıldığını bilmez. Yalnızca y.ode(toplam) der; gerisini nesne yapar. Şekil 4.5.9 bu bağımlılığı çizer: satinAl'dan çıkan tek ok arayüze gider.
satinAl yalnızca OdemeYontemi arayüzünü bilir. KrediKarti, Havale ve sonradan eklenen Kripto sözleşmeyi uygular.| Adım | Satır | Değişkenler | Yeni çıktı |
|---|---|---|---|
| 1 | 15: satinAl(new KrediKarti(), 250.0); // main12: static void satinAl(OdemeYontemi y, double toplam) { | y=→KrediKarti #1, toplam=250.0 | — |
| 2 | 13: y.ode(toplam);3: public void ode(double tutar) {4: System.out.println("Kart: " + tutar + " TL"); | this=→KrediKarti #1, tutar=250.0 | Kart: 250.0 TL |
| 3 | 15: satinAl(new KrediKarti(), 250.0); // main | — | — |
| 4 | 16: satinAl(new Havale(), 120.0);12: static void satinAl(OdemeYontemi y, double toplam) { | y=→Havale #2, toplam=120.0 | — |
| 5 | 13: y.ode(toplam);8: public void ode(double tutar) {9: System.out.println("Havale: " + (tutar + 5) + " TL"); | this=→Havale #2, tutar=120.0 | Havale: 125.0 TL |
| 6 | 12: static void satinAl(OdemeYontemi y, double toplam) {13: y.ode(toplam); | — | — |
Tablo 4.5.5'nin birinci adımında 15. satır satinAl'ı yeni bir KrediKarti nesnesiyle çağırır. Parametre y'nin tipi arayüzdür; gelen nesne bir KrediKarti'dır. İkinci adımda 13. satırdaki y.ode(toplam) dinamik olarak bağlanır. Nesne kart olduğu için KrediKarti.ode() çalışır ve “Kart: 250.0 TL” yazılır. Üçüncü adımda çağrı biter. Kart nesnesini artık gösteren bir referans kalmamıştır; çöp toplayıcı onu silebilir.
Dördüncü adımda aynı satinAl bu kez bir Havale nesnesiyle çağrılır. Beşinci adımda aynı 13. satır bu sefer Havale.ode()'yi çalıştırır. Havale 5 TL masraf ekler: 120.0 + 5 = 125.0. Altıncı adımın vardığı sonuç önemlidir: satinAl iki farklı sınıfla çalıştı ama hiçbirinin adını bilmiyor. Yeni bir ödeme türü eklendiğinde de değişmeyecek.
Bu bağımsızlığa gevşek bağlılık (loose coupling) denir. İki somut yararı vardır. Birincisi, yeni bir ödeme türü yalnızca arayüzü uygulayan bir sınıf yazarak eklenir; satinAl ve onu çağıran kodlar hiç değişmez. İkincisi, kod kolay denenir. Gerçek banka yerine arayüzü uygulayan sahte bir sınıf verilir; bu sınıf para çekmez, yalnızca kendisine ne kadar ödeme geldiğini kaydeder:
interface OdemeYontemi { void ode(double tutar); }
class Kripto implements OdemeYontemi {
public void ode(double tutar) {
System.out.println("Kripto: " + tutar / 2000 + " birim");
}
}
class SahteOdeme implements OdemeYontemi {
double alinan = 0;
public void ode(double tutar) { alinan += tutar; }
}
public class Main {
static void satinAl(OdemeYontemi y, double toplam) {
y.ode(toplam);
}
public static void main(String[] args) {
satinAl(new Kripto(), 500.0);
SahteOdeme sahte = new SahteOdeme();
satinAl(sahte, 99.5);
satinAl(sahte, 0.5);
System.out.println(sahte.alinan);
}
}Kripto: 0.25 birim
100.0Kripto, sonradan eklenen bir ödeme türüdür; 500 TL'yi 2000 TL'lik bir birim fiyatıyla 0.25 birime çevirir. SahteOdeme ise iki ödemenin toplamını tutar: 99.5 + 0.5 = 100.0. satinAl'ın doğru tutarı doğru nesneye ilettiği, gerçek bir bankaya bağlanmadan böylece sınanmış olur. Java'nın kendi kütüphanesi de aynı düzende yazılmıştır: List<String> l = new ArrayList<>(); satırında değişkenin tipi List arayüzüdür. Program ileride ArrayList yerine LinkedList kullanmak isterse yalnızca bu satır değişir.
4.5.4Lambda ifadeleri
OdemeYontemi gibi arayüzlerin çoğunda tek bir soyut metot bulunur. Böyle bir arayüzü uygulamak için ayrı bir sınıf yazmak, ona ad vermek ve gövdeyi bu sınıfın içine yerleştirmek gerekir. Sınıf yalnızca tek bir yerde, tek bir kez kullanılacaksa bu tören gereksiz uzundur. Java 8 ile gelen lambda ifadeleri, tek metotluk bir davranışı doğrudan kullanılacağı yerde yazmayı sağlar.
4.5.4.1Adsız fonksiyon ve söz dizimi
Lambda ifadesi (lambda expression), parametre listesi, ok işareti (->) ve gövdeden oluşan adsız bir fonksiyondur: (ParametreListesi) -> İfade. Adsız (anonim) fonksiyon, ad yerine doğrudan kodun kendisini temsil eder.
Şekil 4.5.10'deki (int x, int y) -> x + y ifadesi iki tam sayı alır ve toplamlarını döndürür. Aynı işi yapan sıradan metot şudur:
static int toplama(int x, int y) {
return x + y;
}İki yazım arasındaki farklar lambdanın kısa yazımını açıklar. Lambdanın adı yoktur; dönüş tipi yazılmaz, ifadenin tipinden çıkarılır; tek ifadeli gövdede return ve süslü parantez de yazılmaz. Söz diziminin birkaç biçimi vardır:
- Parametre tipleri çoğu zaman yazılmaz, derleyici onları bağlamdan çıkarır:
(x, y) -> x + y. - Tek parametrede parantez de gerekmez:
s -> s * 2. - Parametresiz lambda boş parantezle yazılır:
() -> 4. - Gövde birden çok deyimden oluşuyorsa süslü parantez ve
returngerekir:(a, b) -> { int s = a + b; return s; }.
Aşağıdaki program bu biçimlerin hepsini aynı toplamla dener. Her lambda bir arayüz tipindeki değişkene atanmıştır. Lambdanın tipi her zaman böyle bir arayüzdür.
import java.util.function.IntBinaryOperator;
public class Main {
static int toplama(int x, int y) {
return x + y;
}
public static void main(String[] args) {
IntBinaryOperator t1 = (int x, int y) -> x + y;
IntBinaryOperator t2 = (x, y) -> x + y;
IntBinaryOperator t3 = (a, b) -> { int s = a + b; return s; };
java.util.function.IntUnaryOperator iki = s -> s * 2;
System.out.println(toplama(5, 3) + " " + t1.applyAsInt(5, 3) + " " + t2.applyAsInt(5, 3) + " "
+ t3.applyAsInt(5, 3) + " " + iki.applyAsInt(21));
}
}8 8 8 8 42Dört toplama da 8 verir; tek parametreli iki lambdası 21'i ikiye katlar. Yazım değişse de davranış aynıdır.
4.5.4.2Fonksiyonel arayüz
Java'da her değerin bir tipi vardır. Lambda ifadesinin tipi nedir? Java bu soruya yeni bir tip türü eklemeden cevap verir: lambda, tek soyut metodu olan bir arayüzün nesnesidir.
Fonksiyonel arayüz (functional interface), tek bir soyut metodu olan arayüzdür. Lambda ifadesi ancak böyle bir tipe atanabilir. Lambda o tek metodun gövdesini yazar; parametre ve dönüş tipleri metodun imzasından çıkarılır.
@FunctionalInterface
interface Selamlama {
void selamVer(String ad);
}
Selamlama s = ad -> System.out.println("Merhaba " + ad);
s.selamVer("Ayşe");Merhaba AyşeSelamlama arayüzünün tek soyut metodu selamVer(String ad)'dır. ad -> System.out.println("Merhaba " + ad) lambdası bu metodun gövdesidir. Derleyici ad'ın tipini metodun parametresinden String olarak çıkarır. Dönüş tipi void'dir; println çağrısı da bir değer üretmez. Şekil 4.5.11 eşleşmeyi gösterir.
@FunctionalInterface etiketi isteğe bağlıdır. Yazılırsa derleyici arayüzün gerçekten fonksiyonel olduğunu denetler ve ikinci bir soyut metot eklenmesine izin vermez. Etiket yazılmasa da tek soyut metotlu her arayüz lambdaya hedef olabilir. Etiket, arayüzü ileride değiştirecek programcıya karşı bir güvencedir. Biri yanlışlıkla ikinci bir metot eklerse arayüzü kullanan bütün lambdalar bozulur. Etiket bu hatayı arayüzün kendisinde yakalar.
Lambda ile ilgili en önemli kural, onun ne zaman çalıştığıdır. Lambda tanımlandığında çalışmaz. Tanım yalnızca bir arayüz nesnesi oluşturur ve onu değişkene atar. Gövde, arayüzün metodu çağrıldığında, yani s.selamVer(...) satırında çalışır. Bunu görmek için programa numaralı iletiler ekleyelim:
@FunctionalInterface
interface Selamlama {
void selamVer(String ad);
}
public class Main {
public static void main(String[] args) {
System.out.println("1: lambda tanımlanıyor");
Selamlama s = ad -> System.out.println("3: Merhaba " + ad);
System.out.println("2: henüz çağrılmadı");
s.selamVer("Ayşe");
s.selamVer("Ali");
}
}1: lambda tanımlanıyor
2: henüz çağrılmadı
3: Merhaba Ayşe
3: Merhaba AliÇıktıdaki sıra kuralı açıkça gösterir. Lambda 2. iletiden önce tanımlanmıştır ama gövdesi 2. iletiden sonra çalışır. Aynı lambda iki kez çağrıldığı için gövdesi de iki kez çalışır. Lambda bir değer gibi saklanan, taşınan ve istenince çağrılan bir davranıştır.
Lambdanın fonksiyonel bir arayüz dışında bir tipe atanması derleme hatasıdır. Object bir sınıftır, tek soyut metodu yoktur:
public class LambdaObject {
public static void main(String[] args) {
Object o = () -> System.out.println("?");
}
}LambdaObject.java:3: error: incompatible types: Object is not a functional interface
Object o = () -> System.out.println("?");
^
1 errorİki soyut metodu olan bir arayüz de lambdaya hedef olamaz. Derleyici lambdanın hangi metodun gövdesi olduğunu bilemez:
IkiSoyutLambda.java:7: error: incompatible types: Islem2 is not a functional interface
Islem2 i = (a, b) -> a + b;
^
multiple non-overriding abstract methods found in interface Islem2
1 error4.5.4.3Hazır fonksiyonel arayüzler
Her lambda için yeni bir arayüz yazmak gerekmez. java.util.function paketinde sık kullanılan biçimler hazırdır. Tablo 4.5.6 en sık kullanılan dördünü verir.
| Arayüz | Girdi | Çıktı | Metot | Örnek kullanım |
|---|---|---|---|---|
Consumer<T> | bir değer | yok | accept | yazdırma, kaydetme |
Supplier<T> | yok | bir değer | get | değer üretme |
Function<T, R> | bir değer | bir değer | apply | dönüştürme |
Predicate<T> | bir değer | boolean | test | süzme, koşul |
Arayüz adlarının yanındaki <T> ve <T, R>, tip parametreleridir. Function<String, Integer>, String alıp Integer döndüren bir fonksiyon demektir. Köşeli parantezin içindeki tipler, girdinin ve çıktının tipidir.
Consumer<String> yaz = m -> System.out.println(m);
yaz.accept("Consumer çalıştı");
Supplier<Integer> zar = () -> 4;
System.out.println(zar.get());
Function<String, Integer> uzunluk = m -> m.length();
System.out.println(uzunluk.apply("Giresun"));
Predicate<Integer> pozitifMi = x -> x > 0;
System.out.println(pozitifMi.test(-7));Consumer çalıştı
4
7
falseProgram dört arayüzü birer kez kullanır. Consumer aldığı iletiyi yazar ve bir şey döndürmez. Supplier parametresizdir; her get() çağrısında 4 üretir. Function “Giresun” dizgisini uzunluğuna dönüştürür: 7. Predicate −7 için “pozitif mi?” sorusunu cevaplar: false. Her arayüzün metodunun adı farklıdır (accept, get, apply, test); lambda o adı bilmez, çağıran kod bilir.
İlkel tipler için ayrı arayüzler de vardır. IntBinaryOperator iki int alıp int döndürür, metodu applyAsInt'tir. IntUnaryOperator tek int alır. Bu arayüzler kutulama ve kutudan çıkarma maliyetini ortadan kaldırır.
4.5.4.4Lambda ne zaman çalışır?
Lambdanın bir değer gibi saklanıp sonradan çağrıldığını bir liste süzme örneğinde adım adım izleyelim. ciftMi lambdası bir sayının çift olup olmadığını söyler; döngü bu lambdayla bir listeyi süzer.
Predicate<Integer> ciftMi = x -> x % 2 == 0;
List<Integer> sayilar = Arrays.asList(3, 8, 5, 12);
List<Integer> sonuc = new ArrayList<>();
for (int s : sayilar) {
if (ciftMi.test(s)) {
sonuc.add(s);
}
}
System.out.println(sonuc);[8, 12]| Adım | Satır | Değişkenler | Yeni çıktı |
|---|---|---|---|
| 1 | 1: Predicate<Integer> ciftMi = x -> x % 2 == 0; | sonuc=- | — |
| 2 | 2: List<Integer> sayilar = Arrays.asList(3, 8, 5, 12); | sonuc=- | — |
| 3 | 3: List<Integer> sonuc = new ArrayList<>(); | sonuc=[] | — |
| 4 | 4: for (int s : sayilar) { | s=3, sonuc=[] | — |
| 5 | 5: if (ciftMi.test(s)) { | s=3, test=false, sonuc=[] | — |
| 6 | 4: for (int s : sayilar) { | s=8, sonuc=[] | — |
| 7 | 5: if (ciftMi.test(s)) { | s=8, test=true, sonuc=[] | — |
| 8 | 6: sonuc.add(s); | s=8, test=true, sonuc=[8] | — |
| 9 | 4: for (int s : sayilar) { | s=5, sonuc=[8] | — |
| 10 | 5: if (ciftMi.test(s)) { | s=5, test=false, sonuc=[8] | — |
| 11 | 4: for (int s : sayilar) { | s=12, sonuc=[8] | — |
| 12 | 5: if (ciftMi.test(s)) { | s=12, test=true, sonuc=[8] | — |
| 13 | 6: sonuc.add(s); | s=12, test=true, sonuc=[8, 12] | — |
| 14 | 9: System.out.println(sonuc); | sonuc=[8, 12] | [8, 12] |
Tablo 4.5.7'nin birinci adımında 1. satır çalışır. Lambda bir Predicate<Integer> nesnesi olarak saklanır; gövdesi henüz çalışmamıştır. ciftMi, bu davranışı gösteren bir referanstır. İkinci adımda liste 3, 8, 5, 12 öğeleriyle kurulur, üçüncü adımda sonuç listesi boş başlar.
ciftMi.test(3) çağrısı lambdanın gövdesini çalıştırır ve false döner. Sağda döngü bitmiştir: sonuç listesinde 8 ve 12 vardır.Dördüncü adımda döngü ilk öğeyi alır: s = 3. Beşinci adımda ciftMi.test(3) çağrılır ve lambdanın gövdesi ilk kez çalışır: 3 % 2 == 0 yanlıştır, sonuç false'tur (Şekil 4.5.12'nin solu). 3 listeye eklenmez. Altıncı ve yedinci adımlarda aynı iş 8 için yapılır: 8 % 2 == 0 doğrudur. Sekizinci adımda 8 sonuç listesine eklenir. 5 için koşul yanlış, 12 için doğrudur; on üçüncü adımda 12 de eklenir. Son adımda döngü biter ve [8, 12] yazdırılır.
Lambda dört kez çalışmıştır: her test çağrısında bir kez. Tanım anında hiç çalışmamıştır. Bu sayımı doğrulamak için lambdanın gövdesine bir sayaç eklenebilir:
import java.util.*;
import java.util.function.*;
public class Main {
static int sayac = 0;
public static void main(String[] args) {
Predicate<Integer> ciftMi = x -> { sayac++; return x % 2 == 0; };
System.out.println("tanımdan sonra: " + sayac);
List<Integer> sayilar = Arrays.asList(3, 8, 5, 12);
List<Integer> sonuc = new ArrayList<>();
for (int s : sayilar) {
if (ciftMi.test(s)) {
sonuc.add(s);
}
}
System.out.println(sonuc + " " + sayac);
}
}tanımdan sonra: 0
[8, 12] 4Tanımdan hemen sonra sayaç 0'dır; döngüden sonra 4'tür. Sayacın yerel değişken değil static bir alan olduğuna dikkat edilmelidir. Bunun nedeni, lambdanın içinden yerel değişkenlerin değiştirilememesidir.
4.5.4.5Akışla süzme ve dönüştürme
Lambdanın en sık kullanıldığı yer, koleksiyonların işlenmesidir. Java 8 ile gelen akış (stream) arayüzü, bir listeyi bir dizi işlemden geçirmeyi sağlar ve her işlem bir lambda alır. Aşağıdaki kod import java.util.*;, java.util.function.IntBinaryOperator ve java.util.stream.Collectors içe aktarılmış bir main içinde çalışır:
IntBinaryOperator toplama = (int x, int y) -> x + y;
System.out.println(toplama.applyAsInt(5, 3));
List<Integer> sayilar = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
List<Integer> ciftSayilar = sayilar.stream().
filter(s -> s % 2 == 0).
collect(Collectors.toList());
System.out.println(ciftSayilar);
System.out.println(sayilar.stream().map(s -> s * s).toList());8
[2, 4, 6]
[1, 4, 9, 16, 25, 36, 49]İlk iki satır toplama lambdasını IntBinaryOperator tipine atar ve applyAsInt(5, 3) ile çağırır: 8. Ardından 1'den 7'ye kadar sayıların listesi kurulur. sayilar.stream() listeden bir akış üretir. filter(s -> s % 2 == 0) yalnızca lambdanın true döndürdüğü öğeleri, yani çift sayıları tutar. collect(Collectors.toList()) kalan öğeleri yeni bir listede toplar: [2, 4, 6]. Lambda burada açıkça yazılmış bir döngünün yerini almıştır; döngüyü filter'ın içinde Java kütüphanesi çalıştırır.
Son satırdaki map(s -> s * s) her öğeyi dönüştürür: her sayı karesiyle yer değiştirir. Sonuç [1, 4, 9, 16, 25, 36, 49]'dur. Kısaca filter öğe sayısını azaltır ama değerlere dokunmaz; map öğe sayısını korur ama değerleri değiştirir. Son satırdaki .toList() Java 16 ile gelmiştir. Daha eski sürümlerde aynı iş collect(Collectors.toList()) ile yapılır. Burada akışın yalnızca lambda alan yüzü kullanılmıştır.
4.5.4.6Lambda ve anonim sınıf
Lambdadan önce tek metotluk bir davranış anonim sınıfla (anonymous class) yazılırdı. Anonim sınıf, tanımlandığı yerde tek nesnesi oluşturulan adsız bir iç sınıftır. Aşağıdaki program aynı Runnable arayüzünü iki yolla uygular:
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Anonim sınıf");
}
};
Runnable r2 =
() -> System.out.println("Lambda");
r1.run();
r2.run();Anonim sınıf
Lambda
180r1 için yazılan anonim sınıf yedi satır tutar: new Runnable(), süslü parantez, @Override, tam metot imzası ve gövde. r2 için yazılan lambda aynı işi tek ifadede yapar. Derleyici lambdayı Runnable'ın tek metodu run()'ın gövdesi olarak yorumlar. İki nesnenin run() çağrıları beklenen iletileri yazar.
Lambdanın kısalığının bir bedeli vardır; üç sınırı bilinmelidir. Birincisi, lambda yalnızca fonksiyonel bir arayüz tipine atanabilir. Anonim sınıf ise birden çok metodu olan bir arayüzü ya da bir sınıfı da genişletebilir.
İkincisi, lambdanın kullandığı yerel değişkenler final ya da fiilen final (effectively final) olmalıdır. Fiilen final, final yazılmamış ama ilk değerinden sonra hiç değer atanmamış değişken demektir. Aşağıdaki lambda kdv yerel değişkenini okur ve 150 TL'lik bir fiyata %20 KDV ekler: 150 + 150 · 20 / 100 = 180.
int kdv = 20;
Function<Integer, Integer> vergili =
fiyat -> fiyat + fiyat * kdv / 100;
System.out.println(vergili.apply(150)); // 180
kdv = 18; // eklenirse: derleme hatasıSon satır yorumda kalırsa program derlenir ve 180 yazar. O satır eklenirse kdv artık fiilen final değildir ve derleyici lambdanın bulunduğu satırı işaret eder:
LambdaFinal.java:6: error: local variables referenced from a lambda expression must be final or effectively final
fiyat -> fiyat + fiyat * kdv / 100;
^
1 errorKuralın nedeni lambdanın sonradan çalışabilmesidir. Lambda, tanımlandığı metottan çok sonra, hatta o metot bittikten sonra çağrılabilir. Bu yüzden yerel değişkenin kendisini değil, değerinin bir kopyasını taşır. Değişken sonradan değişseydi lambdanın gördüğü değer ile metodun gördüğü değer birbirinden ayrılırdı. Java bu belirsizliği baştan yasaklar. Alanlar bu kurala girmez; lambda bir alanı okuyabilir ve değiştirebilir. Sayaç örneğinde sayacın static alan olarak yazılmasının nedeni budur.
Üçüncüsü, this anahtar kelimesinin anlamı farklıdır. Lambda içindeki this çevreleyen nesneyi, yani lambdayı yazan metodun nesnesini gösterir. Anonim sınıftaki this ise anonim nesnenin kendisini gösterir:
public class Main {
String ad = "dış nesne";
void dene() {
Runnable lam = () -> System.out.println("lambda: " + this.ad);
Runnable ano = new Runnable() {
String ad = "anonim nesne";
public void run() { System.out.println("anonim: " + this.ad); }
};
lam.run();
ano.run();
}
public static void main(String[] args) {
new Main().dene();
}
}lambda: dış nesne
anonim: anonim nesneİki gövde de this.ad okur. Lambdanın kendine ait bir nesnesi olmadığı için this dıştaki Main nesnesidir. Anonim sınıf ise kendi ad alanı olan ayrı bir nesnedir.
4.5.4.7Kullanım alanları
Lambda ifadeleri üç tür işte kodu belirgin biçimde kısaltır:
- Koleksiyon işlemleri. Liste süzme, sıralama, eşleştirme ve her öğe için bir iş yapma (
filter,sort,map,forEach). - İş parçacığı yönetimi. Paralel yürütülecek bir işin gövdesini kısaca vermek.
- Okunabilir kod. Tek yerde kullanılacak küçük bir işlem için ayrı bir metot ya da sınıf tanımlamak yerine işlemi kullanıldığı yerde yazmak.
List<String> isimler = new ArrayList<>(List.of("Zeynep", "Ali", "Mehmet"));
isimler.sort((a, b) -> a.length() - b.length());
System.out.println(isimler);
isimler.forEach(isim -> System.out.println("Merhaba " + isim));
Thread is = new Thread(() -> System.out.println("İş parçacığı çalıştı"));
is.start();[Ali, Zeynep, Mehmet]
Merhaba Ali
Merhaba Zeynep
Merhaba Mehmet
İş parçacığı çalıştıisimler.sort(...) çağrısı sıralama ölçütünü bir lambda olarak alır. (a, b) -> a.length() - b.length() iki ismin uzunluk farkını döndürür. Fark negatifse a önce gelir, pozitifse b önce gelir, sıfırsa ikisi eşit sayılır. “Ali” üç harfli olduğu için başa geçer. “Zeynep” ve “Mehmet” altışar harflidir; Java'nın liste sıralaması eşit öğelerin ilk sırasını korur, bu yüzden “Zeynep” “Mehmet”'ten önce kalır. Böyle sıralamaya kararlı sıralama denir.
forEach listenin her öğesi için verilen lambdayı çağırır ve üç selam yazar. Son iki satır bir iş parçacığı oluşturur. Thread'in yapıcısı bir Runnable bekler; () -> ... parametresiz bir lambdadır ve run()'ın gövdesi olur. is.start() bu gövdeyi ayrı bir iş parçacığında başlatır. Çalıştırılan programa is.join() satırı eklenmiştir; main'in iş parçacığının bitmesini beklemesini sağlar. Bu programda son ileti zaten en son yazılır, çünkü ondan önce başka iş kalmamıştır.
Sıralama ölçütünü değiştirmek yalnızca lambdayı değiştirmek demektir. Aşağıdaki program aynı listeyi üç ölçütle sıralar. Comparator.comparing(String::length) “uzunluğa göre” demenin hazır yoludur; String::length yazımı, yalnızca bir metodu çağıran s -> s.length() lambdasının kısaltmasıdır (metot referansı).
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> isimler = new ArrayList<>(List.of("Zeynep", "Ali", "Mehmet"));
isimler.sort(Comparator.comparing(String::length));
System.out.println(isimler);
isimler.sort((a, b) -> b.length() - a.length());
System.out.println(isimler);
isimler.sort(Comparator.comparing(String::length).thenComparing(Comparator.naturalOrder()));
System.out.println(isimler);
}
}[Ali, Zeynep, Mehmet]
[Zeynep, Mehmet, Ali]
[Ali, Mehmet, Zeynep]İlk satır kısa olandan uzuna, ikinci satır uzundan kısaya sıralar; ikincide de eşit uzunluktakiler ilk sıralarını korur. Üçüncü satır uzunluk eşitse alfabetik sıraya bakar ve “Mehmet”'i “Zeynep”'in önüne alır.
4.5.5Arayüzün gövdeli metotları
Java 8'e kadar bir arayüzde yalnızca soyut metotlar ve sabitler bulunabiliyordu. Bu katılık büyük bir sorun doğurdu. Java'nın kendi Collection ve List arayüzlerine lambda ile çalışan yeni metotlar (forEach, removeIf, stream) eklenmek istendi. Arayüze yeni bir soyut metot eklemek, onu uygulayan bütün sınıfları bozar: dünyadaki her liste sınıfı birden derlenmez hâle gelirdi. Çözüm, arayüzde gövdeli metot yazmaya izin vermek oldu. Bugün bir arayüzde üç tür gövdeli metot bulunabilir: varsayılan (default), static ve private.
4.5.5.1Varsayılan metotlar
Varsayılan metot (default method), arayüzde default anahtar kelimesiyle ve gövdesiyle yazılan metottur (Java 8+). Arayüzü uygulayan sınıf onu hazır olarak kalıtımla alır; isterse geçersiz kılar.
Aşağıdaki Collection arayüzü üç soyut metot ve bir varsayılan metot içerir. Bu Collection bizim yazdığımız küçük bir arayüzdür; java.util.Collection ile karıştırılmamalıdır.
interface Collection {
void add(Object o);
void remove(Object o);
int size();
default boolean isEmpty() {
return size() == 0;
}
}class Sepet implements Collection {
private int adet = 0;
public void add(Object o) { adet++; }
public void remove(Object o) { adet--; }
public int size() { return adet; }
// isEmpty() yazılmadı: default sürüm kullanılır
}
// main içinde:
Sepet sepet = new Sepet();
System.out.println(sepet.isEmpty());
sepet.add("elma");
sepet.add("armut");
System.out.println(sepet.size() + " " + sepet.isEmpty());true
2 falseisEmpty() soyut size()'ı çağırır. Bir koleksiyonun boş olup olmadığı, eleman sayısının sıfır olup olmadığıdır; bu bilgi her koleksiyon için aynı yoldan hesaplanır. Hangi size()'ın çalışacağı ise nesneye göre belirlenir. Sepet yalnızca üç soyut metodu yazmıştır; isEmpty()'yi hazır alır. Boş sepette true, iki öğe eklendikten sonra 2 false yazılır.
Varsayılan metodun çalışma biçimi şablon metoda benzer: arayüzdeki gövde, henüz yazılmamış bir sınıfın metodunu çağırır. Aşağıdaki kısa sürümü adım adım izleyelim.
interface Collection {
void add(Object o);
int size();
default boolean isEmpty() {
return size() == 0;
}
}
class Sepet implements Collection {
private int adet = 0;
public void add(Object o) { adet++; }
public int size() { return adet; }
}
Collection c = new Sepet(); // main
System.out.println(c.isEmpty());
c.add("elma");
System.out.println(c.isEmpty());true
false| Adım | Satır | Değişkenler | Yeni çıktı |
|---|---|---|---|
| 1 | 13: Collection c = new Sepet(); // main | c=→Sepet #1, adet=0 | — |
| 2 | 14: System.out.println(c.isEmpty());4: default boolean isEmpty() { | c=→Sepet #1 | — |
| 3 | 5: return size() == 0;11: public int size() { return adet; } | size()=0 | — |
| 4 | 5: return size() == 0; | size()=0, sonuç=true | true |
| 5 | 15: c.add("elma");10: public void add(Object o) { adet++; } | adet=1 | — |
| 6 | 16: System.out.println(c.isEmpty());5: return size() == 0;11: public int size() { return adet; } | size()=1, sonuç=false | false |
Tablo 4.5.8'in birinci adımında bir Sepet nesnesi oluşur ve arayüz tipindeki c'ye atanır. Sepet yalnızca iki soyut metodu, add ve size'ı yazmıştır. İkinci adımda c.isEmpty() çağrılır. JVM metodu önce nesnenin sınıfında arar; Sepet'te isEmpty() yoktur. Arama arayüze çıkar ve oradaki varsayılan gövde çalışır.
Sepet'te isEmpty() bulunmadığı için arayüzdeki varsayılan sürüm seçilir. Sağda varsayılan gövdenin çağırdığı size() dinamik olarak Sepet.size()'a bağlanır.Üçüncü adımda varsayılan gövde size()'ı çağırır. Bu çağrı this.size() demektir ve this bir Sepet'tir; hangi gövdenin çalışacağını nesne belirler: Sepet.size() 0 döndürür (Şekil 4.5.13). Dördüncü adımda 0 == 0 karşılaştırması true verir ve yazdırılır. Beşinci adımda c.add("elma") Sepet'in sürümünü çalıştırır ve adet 1 olur. Altıncı adımda aynı varsayılan gövde bu kez 1 görür ve false döndürür.
Örneğin dersi şudur: arayüz yazıldıktan sonra eklenen bir varsayılan metot, eski sınıfları bozmadan onlara da gelir. Sepet yazılırken isEmpty() hiç bilinmiyor olsaydı bile, arayüze varsayılan metot olarak eklendiği gün Sepet nesneleri onu kullanmaya başlardı. Java'nın forEach ve removeIf metotları bütün liste sınıflarına bu yolla eklenmiştir.
Varsayılan metotla ilgili bir öncelik kuralı daha vardır: sınıf kazanır. Bir sınıf aynı imzalı bir metodu hem üst sınıfından hem bir arayüzün varsayılan metodundan alıyorsa üst sınıftaki sürüm seçilir:
class Makine {
public void calis() { System.out.println("Makine çalışıyor"); }
}
interface Yazici {
default void calis() { System.out.println("Yazdırılıyor"); }
}
class Fotokopi extends Makine implements Yazici { }
public class Main {
public static void main(String[] args) {
Yazici y = new Fotokopi();
y.calis();
}
}Makine çalışıyorFotokopi calis()'ı yazmamıştır. Metodu hem Makine'den hem Yazici'nın varsayılan gövdesinden alabilir. Kural sınıf hiyerarşisini öne koyar ve “Makine çalışıyor” yazılır. Varsayılan metot, sınıfta zaten bulunan davranışı ezmez; yalnızca eksik olanı tamamlar.
4.5.5.2static metotlar
Arayüzde static metot da yazılabilir (Java 8+). Bu metotlar nesneye bağlı değildir; arayüzle ilgili yardımcı işlevleri bir arada tutar. Yalnızca arayüzün adıyla çağrılır.
interface Geometry {
static double circleArea(double r) { return Math.PI * r * r; }
static double squareArea(double s) { return s * s; }
static double perimeter(double s) { return s * 4.0; }
static double boxVolume(double w, double h, double d) {
return w * h * d;
}
}
// main içinde:
System.out.println(Geometry.circleArea(2));
System.out.println(Geometry.squareArea(3));
System.out.println(Geometry.perimeter(2.5));
System.out.println(Geometry.boxVolume(2, 3, 4));12.566370614359172
9.0
10.0
24.0Geometry arayüzü dört geometrik hesabı toplar. Yarıçapı 2 olan dairenin alanı π · 2 · 2 = 12.566370614359172'dir. Kenarı 3 olan karenin alanı 9.0, kenarı 2.5 olan karenin çevresi 10.0'dır. 2 × 3 × 4 boyutlu kutunun hacmi ise 24.0'dır. Çağrılar Geometry.circleArea(2) biçimindedir.
Arayüzün static metotları onu uygulayan sınıfa kalıtımla geçmez. Bu, sınıflardaki static metotlardan farklıdır; bir sınıfın static metodu alt sınıfın adıyla da çağrılabilir. Daire implements Geometry olsa bile Daire.squareArea(2) derlenmez:
interface Geometry {
static double squareArea(double s) { return s * s; }
}
class Daire implements Geometry { }
public class StaticKalitim {
public static void main(String[] args) {
System.out.println(Daire.squareArea(2));
}
}StaticKalitim.java:7: error: cannot find symbol
System.out.println(Daire.squareArea(2));
^
symbol: method squareArea(int)
location: class Daire
1 errorKuralın nedeni çoklu kalıtımdır. Bir sınıf birçok arayüzü uygulayabilir; iki arayüzde aynı adlı static metot bulunsaydı Daire.squareArea hangisini gösterecekti? Java metodu her zaman arayüz adıyla çağırtarak bu soruyu ortadan kaldırır.
Dairenin alanında kullanılan π sabiti Math sınıfındadır ve tam adıyla yazılmalıdır. Yalnızca PI yazılırsa derleyici onu arayüzde arar ve bulamaz:
PIYalniz.java:2: error: cannot find symbol
static double circleArea(double r) { return PI * r * r; }
^
symbol: variable PI
location: interface Geometry
1 error4.5.5.3Default çakışması
Bir sınıf iki arayüzü uyguladığında ve iki arayüz aynı imzalı birer varsayılan metot getirdiğinde sınıf hangisini alacağını bilemez. Java bu durumda tahmin yürütmez; sınıfı metodu kendisi yazmaya, yani geçersiz kılmaya zorlar. İstenen arayüzün sürümü ArayuzAdi.super.metot() yazımıyla çağrılabilir.
interface Selamlayan {
default void selam() { System.out.println("Merhaba"); }
}
interface Karsilayan {
default void selam() { System.out.println("Hoş geldiniz"); }
}
class Gorevli implements Selamlayan, Karsilayan {
public void selam() { // geçersiz kılmak zorunlu
Selamlayan.super.selam();
Karsilayan.super.selam();
}
}Merhaba
Hoş geldinizGorevli iki selamı da kullanmak istemiştir. Selamlayan.super.selam() “Merhaba”yı, Karsilayan.super.selam() “Hoş geldiniz”i yazar. super'in önündeki arayüz adı, hangi üst tipin varsayılan gövdesinin istendiğini söyler. Gorevli'de selam() hiç yazılmasaydı sınıf derlenmezdi:
DefaultCakisma.java:7: error: types Selamlayan and Karsilayan are incompatible;
class Gorevli implements Selamlayan, Karsilayan { }
^
class Gorevli inherits unrelated defaults for selam() from types Selamlayan and Karsilayan
1 errorAşağıdaki ofis makinesi örneği aynı durumu adım adım gösterir. Makine hem yazıcı hem tarayıcıdır ve iki arayüz de calis() için varsayılan gövde getirir.
interface Yazici {
default void calis() { System.out.println("Yazdırılıyor"); }
}
interface Tarayici {
default void calis() { System.out.println("Taranıyor"); }
}
class OfisMakinesi implements Yazici, Tarayici {
public void calis() {
Tarayici.super.calis();
Yazici.super.calis();
System.out.println("Kopya hazır");
}
}
Yazici y = new OfisMakinesi(); // main
y.calis();Taranıyor
Yazdırılıyor
Kopya hazır| Adım | Satır | Değişkenler | Yeni çıktı |
|---|---|---|---|
| 1 | 14: Yazici y = new OfisMakinesi(); // main | y=→OfisMakinesi #1 (tip: Yazici) | — |
| 2 | 15: y.calis();8: public void calis() { | this=→OfisMakinesi #1 | — |
| 3 | 9: Tarayici.super.calis();5: default void calis() { System.out.println("Taranıyor"); } | this=→OfisMakinesi #1 | Taranıyor |
| 4 | 10: Yazici.super.calis();2: default void calis() { System.out.println("Yazdırılıyor"); } | this=→OfisMakinesi #1 | Yazdırılıyor |
| 5 | 11: System.out.println("Kopya hazır"); | this=→OfisMakinesi #1 | Kopya hazır |
| 6 | 15: y.calis(); | y=→OfisMakinesi #1 | — |
Tarayici.super.calis() adı verilen arayüzün gövdesini çağırır.Tablo 4.5.9'in birinci adımında OfisMakinesi nesnesi oluşur ve Yazici tipindeki y'ye atanır. İkinci adımda y.calis() çağrılır. Referansın tipi Yazici'dır ama nesne OfisMakinesi'dir; sınıfın kendi calis() metodu çalışır. Referansın Yazici olması, Yazici'nın varsayılan gövdesinin seçileceği anlamına gelmez; sınıfın yazdığı sürüm her zaman önce gelir.
Üçüncü adımda Tarayici.super.calis() Tarayici'nın varsayılan gövdesini çağırır ve “Taranıyor” yazılır. Dördüncü adımda sıra Yazici'nınkine gelir: “Yazdırılıyor”. Hangi sürümlerin hangi sırayla kullanılacağına sınıf karar verir. Beşinci adımda sınıf kendi eklemesini yapar: “Kopya hazır”. Sonuç, iki yeteneği birleştiren yeni bir davranıştır. Altıncı adımda çağrı biter.
Arayüzün varsayılan metodunu super.metot() ile çağırmaya çalışmak. Yalın super üst sınıfı gösterir; arayüzün gövdesine ulaşmak için arayüzün adı yazılır: Tarayici.super.calis(). Bu yazım yalnızca sınıfın doğrudan uyguladığı arayüzler için geçerlidir.
4.5.5.4private metotlar
Varsayılan metotlar çoğaldıkça aralarında ortak kod birikir. Bu kodu ayrı bir metoda taşımak gerekir, ama metot default olursa arayüzü uygulayan her sınıfa açılır. Java 9 bu sorunu private arayüz metotlarıyla çözer.
interface Yazdirilabilir {
String icerik();
default void yazdir() {
System.out.println(baslik() + ": " + icerik());
}
private String baslik() { // Java 9+
return "Belge";
}
}
class Rapor implements Yazdirilabilir {
public String icerik() { return "Vize sonuçları"; }
}Belge: Vize sonuçlarıprivate metotlar, varsayılan ve static metotların ortak yardımcı kodunu dışarı açmadan paylaşır. Gövdeleri vardır ve yalnızca arayüzün içinden çağrılabilir. Akış şöyledir: yazdir() (varsayılan) önce baslik()'ı (private), sonra icerik()'i (Rapor'un uyguladığı soyut metot) çağırır ve ikisini birleştirir (Şekil 4.5.15).
yazdir() metodu, yalnızca arayüzün içinden görülen baslik()'ı ve Rapor'un yazdığı icerik()'i çağırır; sonuç “Belge: Vize sonuçları”dır.Uygulayan sınıf private metodu göremez. Rapor içinden baslik() çağrılırsa derleyici metodu Rapor'da arar ve bulamaz:
PrivateCagri.java:11: error: cannot find symbol
public String icerik() { return baslik(); }
^
symbol: method baslik()
location: class Rapor
1 error4.5.5.5Arayüz üyelerinin özeti
Arayüzde bulunabilecek bütün üyeler, örtük belirteçleri ve hangi Java sürümüyle geldikleri Tablo 4.5.10'de toplanmıştır.
| Üye | Örnek | Örtük belirteçler | Gövde | Java |
|---|---|---|---|---|
| Soyut metot | void ciz(); | public abstract | yok | 1.0 |
| Sabit | int KENAR_SAYISI = 4; | public static final | — | 1.0 |
| Varsayılan metot | default boolean isEmpty() {…} | public | var | 8 |
static metot | static double squareArea(…) {…} | public | var | 8 |
| Özel metot | private String baslik() {…} | yalnızca arayüz içinde | var | 9 |
| Örnek alanı, yapıcı | — | İzin verilmez: arayüzün nesnesi yoktur, durum tutamaz | ||
Uygulayan sınıf için kural kısadır: arayüzdeki bütün soyut metotlar public olarak yazılır. Varsayılan metotları yazmak isteğe bağlıdır; static ve private metotlar ise zaten kalıtımla geçmez. Tablonun son satırındaki yasak da derleyicide görülür. Arayüzün içine bir yapıcı yazılırsa derleyici onu yapıcı olarak tanımaz; dönüş tipi olmayan bir metot gibi okur ve tip adı bekler:
interface Sayac {
Sayac() { }
}
public class ArayuzYapici { }ArayuzYapici.java:2: error: <identifier> expected
Sayac() { }
^
1 error4.5.6Mühürlü tipler
Bir sınıf ya da arayüz yazıldığında, varsayılan olarak herkes onu genişletebilir ya da uygulayabilir. Bu açıklık çoğu zaman yararlıdır, ama bazen tasarımın gerçeğini yansıtmaz. Bir trafik ışığı yalnızca kırmızı, sarı ya da yeşil olabilir; bir ödeme yalnızca kart, havale ya da nakit olabilir. Böyle bir hiyerarşide yeni bir alt tipin habersizce eklenmesi, onu ele alan bütün kodu eksik bırakır. Java 17 ile gelen mühürleme, alt tiplerin listesini kapatmayı sağlar.
Mühürlü sınıf ya da arayüz (sealed class / interface), sealed anahtar kelimesiyle tanımlanan tiptir (Java 17+). Bu tipi yalnızca permits listesinde adı geçen alt tipler genişletebilir ya da uygulayabilir.
İzin verilen her alt tip, hiyerarşinin kendisinden sonra nasıl devam edeceğini seçmek zorundadır. Üç seçenek vardır:
final: hiyerarşi burada biter; bu tipten türetme yapılamaz.sealed: tip yeniden kısıtlanır; kendipermitslistesini verir.non-sealed: kısıt kaldırılır; herkes bu tipi genişletebilir.
Mühürlemenin asıl yararı, derleyicinin bütün alt tipleri bilmesidir. Böyle bir hiyerarşiye kapalı hiyerarşi denir. Aşağıdaki örnekte Hayvan arayüzü yalnızca Kedi ve Kopek'e izin verir:
public sealed interface Hayvan permits Kedi, Kopek {
void hareketEt();
}
final class Kedi implements Hayvan {
public void hareketEt() {
System.out.println("Kedi sessizce yürüyor");
}
}
non-sealed class Kopek implements Hayvan {
public void hareketEt() {
System.out.println("Köpek koşuyor");
}
}
class Kangal extends Kopek { // serbest
public void hareketEt() {
System.out.println("Kangal sürüyü koruyor");
}
}Hayvan arayüzü yalnızca Kedi (final) ve Kopek (non-sealed) tiplerine izin verir. Kangal, Kopek'i serbestçe genişletir; Kus'a izin yoktur.Kedi final olduğu için hiyerarşinin o kolu kapanmıştır. Kopek non-sealed'dir; bu yüzden Kangal hiçbir izin gerekmeden Kopek'i genişletir (Şekil 4.5.16). Kangal dolaylı olarak bir Hayvan'dır, ama Hayvan'ın doğrudan alt tipi değildir; bu yüzden permits listesinde yer alması gerekmez.
Mühürlü tipin derleyiciye kazandırdığı bilgi, switch ile birlikte kullanılınca görülür. Java 21'de switch bir nesnenin tipine göre dallanabilir ve derleyici bütün alt tiplerin ele alınıp alınmadığını denetler:
// Java 21: switch tüm alt türleri kapsar,
// default dalı gerekmez
static String tur(Hayvan h) {
return switch (h) {
case Kedi k -> "kedi";
case Kopek k -> "köpek";
};
}
// main içinde:
Hayvan[] hayvanlar =
{ new Kedi(), new Kopek(), new Kangal() };
for (Hayvan h : hayvanlar) {
h.hareketEt();
System.out.println(tur(h));
}Kedi sessizce yürüyor
kedi
Köpek koşuyor
köpek
Kangal sürüyü koruyor
köpektur metodundaki switch'in default dalı yoktur. Derleyici Hayvan'ın yalnızca Kedi ve Kopek olabileceğini bilir; bu yüzden iki dalın her durumu kapsadığını kanıtlayabilir. case Kedi k dalı, nesne bir Kedi ise onu k adıyla yakalar. Döngüdeki üçüncü nesne bir Kangal'dır; hareketEt() kendi sürümünü çalıştırır, switch ise onu bir Kopek olarak tanır ve “köpek” döndürür.
Bu denetimin değeri, hiyerarşi değiştiğinde ortaya çıkar. permits listesine yeni bir alt tip eklenirse onu ele almayan her switch derlenmez hâle gelir. Aşağıdaki switch yalnızca Kedi'yi ele alır:
sealed interface Hayvan permits Kedi, Kopek { }
final class Kedi implements Hayvan { }
final class Kopek implements Hayvan { }
public class SwitchEksik {
static String tur(Hayvan h) {
return switch (h) {
case Kedi k -> "kedi";
};
}
}SwitchEksik.java:6: error: the switch expression does not cover all possible input values
return switch (h) {
^
1 errorBir default dalı yazılsaydı bu yardım kaybolurdu: yeni tip sessizce default'a düşerdi. Mühürlü hiyerarşide default yazmamak bu yüzden bilinçli bir seçimdir.
Mühürleme kurallarının ihlali derleme hatasıdır. İzin listesinde olmayan bir sınıf mühürlü tipi uygulayamaz:
sealed interface Hayvan permits Kedi, Kopek { }
final class Kedi implements Hayvan { }
final class Kopek implements Hayvan { }
final class Kus implements Hayvan { }
public class MuhurIzinsiz { }MuhurIzinsiz.java:4: error: class is not allowed to extend sealed class: Hayvan (as it is not listed in its 'permits' clause)
final class Kus implements Hayvan { }
^
1 errorİletideki “sealed class” ifadesi arayüz için de kullanılır. İzinli bir alt tip üç belirteçten birini seçmezse de derleyici durur:
sealed interface Hayvan permits Kedi, Kopek { }
class Kedi implements Hayvan { }
final class Kopek implements Hayvan { }
public class MuhurBelirtecsiz { }MuhurBelirtecsiz.java:2: error: sealed, non-sealed or final modifiers expected
class Kedi implements Hayvan { }
^
1 errorBu hatanın üç düzeltmesi, üç farklı tasarım kararıdır. final class Kedi hiyerarşiyi orada bitirir. sealed class Kedi permits Tekir yeniden kısıtlar; o zaman Tekir de bir belirteç seçmelidir. non-sealed class Kedi ise Kedi'yi herkese açar.
4.5.7Arayüz mü, soyut sınıf mı?
Soyut sınıf ve arayüz birbirine benzer: ikisinden de nesne oluşturulamaz, ikisi de referans tipi olarak kullanılır, ikisi de alt tiplere metot yazdırır. Java 8'den sonra arayüzler de gövdeli metot taşıdığı için fark daha da incelmiştir. Tablo 4.5.11 iki aracı yan yana koyar.
| Özellik | Arayüz | Soyut sınıf |
|---|---|---|
| Tanım / kullanım | interface · implements | abstract class · extends |
| Metotlar | Soyut; Java 8+ varsayılan ve static, Java 9+ private | Hem soyut hem somut metotlar |
| Alanlar | Yalnızca public static final sabitler; örnek alanı olamaz | Her türlü alan (örnek alanları dahil) |
| Yapıcı | Yok | Olabilir; alt sınıf super(…) ile çağırır |
| Erişim | Üyeler örtük public (private yardımcılar hariç) | public, protected, private, paket |
| Kalıtım | Bir sınıf çok sayıda arayüz uygular; arayüz birden çok arayüzü genişletir | Bir sınıf yalnızca tek bir sınıftan türetilir |
| Amaç | İlgisiz sınıflara ortak yetenek (“yapabilir”) | Ortak alanlı yakın sınıfları gruplamak (“bir türüdür”) |
| Nesne | İkisinden de new ile nesne oluşturulamaz; ikisi de referans tipi olarak kullanılır | |
Tablodaki en belirleyici satırlar “Alanlar” ve “Kalıtım” satırlarıdır. Soyut sınıf durum taşır ama tek ebeveynlidir. Arayüz durum taşımaz ama bir sınıf istediği kadar arayüz alabilir. Seçim çoğu zaman bu iki ölçüte göre yapılır.
Soyut sınıf şu durumlarda uygundur:
- Alt sınıflar yakın akrabadır:
KareveCemberbirerSekil'dir. - Ortak alanlar ve yapıcı gerekir (
isimgibi). - Ortak kodun bir kısmı hazırdır (şablon metot gibi).
Arayüz şu durumlarda uygundur:
- Aynı yetenek ilgisiz sınıflara verilecektir:
Kus,UcakveDronehepsiUcabilir'dir. - Bir sınıfa birden çok yetenek kazandırılacaktır.
- Kullanan kod ile uygulayan kod arasında bir sözleşme kurulacaktır. Böyle bir sözleşmeye API (application programming interface, uygulama programlama arayüzü) denir.
Çoğu zaman ikisi birlikte kullanılır:
abstract class Hayvan implements Sesli { … }
class Ordek extends Hayvan implements Ucabilir, Yuzebilir { … }Bu yazımda Hayvan soyut sınıfı akrabalığı ve ortak durumu taşır; Sesli, Ucabilir ve Yuzebilir arayüzleri yetenekleri ekler. Ordek hem bir Hayvan'dır hem uçabilir hem yüzebilir.
Java'nın sınıflarda çoklu kalıtıma izin vermemesinin nedeni, iki üst sınıfın aynı alanı ya da metodu farklı tanımlaması durumunda hangisinin geçerli olacağının belirsizleşmesidir. Bu soruna, iki üst sınıfın ortak bir atadan geldiği çizimin biçimi yüzünden elmas problemi denir. Arayüzler durum taşımadığı için sorunun büyük kısmı ortadan kalkar. Kalan tek belirsizlik iki varsayılan metodun çakışmasıdır ve onu da derleyici sınıfa çözdürür.
Bir örnekle karar sürecini izleyelim. Plakası ve yapıcısı olan, soyut int tekerSayisi() metodu bulunan bir Tasit ile KAPASITE = 100 sabiti ve void sarjEt(int yuzde) metodu olan bir Sarj tasarlanacak. Tasit'in alanı ve yapıcısı vardır; bu yüzden soyut sınıf olmalıdır. Şarj olma yeteneği ise elektrikli arabaya da, telefona da, matkaba da verilebilir; bu yüzden Sarj bir arayüzdür. Elektrikli araba ikisini birlikte kullanır:
abstract class Tasit {
private final String plaka;
Tasit(String plaka) { this.plaka = plaka; }
abstract int tekerSayisi();
String getPlaka() { return plaka; }
}
interface Sarj {
int KAPASITE = 100;
void sarjEt(int yuzde);
}
class ElektrikliAraba extends Tasit implements Sarj {
private int doluluk = 20;
ElektrikliAraba(String plaka) { super(plaka); }
int tekerSayisi() { return 4; }
public void sarjEt(int yuzde) {
doluluk = Math.min(KAPASITE, doluluk + yuzde);
}
int getDoluluk() { return doluluk; }
}
public class Main {
public static void main(String[] args) {
ElektrikliAraba e = new ElektrikliAraba("28 ABC 61");
e.sarjEt(50);
System.out.println(e.getDoluluk());
e.sarjEt(50);
System.out.println(e.getDoluluk());
Tasit t = e; Sarj s = e;
System.out.println(t.tekerSayisi() + " " + (s == t));
}
}70
100
4 trueDoluluk %20'den başlar. İlk sarjEt(50) onu 70 yapar. İkinci çağrıda 70 + 50 = 120 hesaplanır ama Math.min(KAPASITE, …) sonucu kapasiteyle sınırlar: 100. KAPASITE sabiti arayüzden kalıtımla gelir ve sınıfın içinde adıyla kullanılır. Sınıf bildiriminde önce extends, sonra implements yazılmıştır ve sarjEt public'tir. Nesne hem Tasit hem Sarj tipindeki referanslara atanabilir; iki referans aynı nesneyi gösterdiği için s == t true verir.
4.5.8Jenerikler
Arayüzler ve soyut sınıflar, bir kodun farklı sınıflarla çalışmasını sağlar. Jenerikler aynı fikri bir adım öteye taşır: bir sınıfın ya da metodun farklı tiplerle çalışmasını, üstelik tip güvenliğini kaybetmeden sağlar. List<String> ve Function<String, Integer> yazımlarındaki köşeli parantezler jeneriklerin söz dizimidir.
4.5.8.1Neden jenerikler?
Her tipte değer saklayabilen bir kutu sınıfı yazmak isteyelim. Jenerikler olmadan akla gelen tek yol, her şeyin üst sınıfı olan Object'i kullanmaktır:
class KutuObj {
private Object icerik;
void koy(Object o) { icerik = o; }
Object al() { return icerik; }
}
KutuObj k = new KutuObj();
k.koy("Merhaba");
String s = (String) k.al(); // dönüşüm şart
k.koy(42); // itiraz yok
String t = (String) k.al(); // çalışırken hataException in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
at Main.main(Main.java:12)Bu tasarımın iki kusuru vardır. Birincisi, kutudan değer okurken her seferinde açık dönüşüm gerekir: (String) k.al(). Derleyici kutunun içinde ne olduğunu bilmez; programcının sözüne güvenir. İkincisi, yanlış tipte bir değer konulduğunda kimse itiraz etmez. k.koy(42) derlenir, çünkü 42 kutulanıp bir Integer nesnesi olur ve her nesne bir Object'tir. Hata ancak program çalışırken, 42'yi String'e dönüştürmeye çalışan satırda ortaya çıkar: ClassCastException. Hatanın yapıldığı yer ile fark edildiği yer birbirinden uzaktır.
class Kutu<T> {
private T icerik;
void koy(T x) { icerik = x; }
T al() { return icerik; }
}
Kutu<String> k = new Kutu<>();
k.koy("Merhaba");
String s = k.al(); // dönüşüm yok
k.koy(42); // derlenmezJenerikKoy.java:10: error: incompatible types: int cannot be converted to String
k.koy(42);
^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 errorJenerik sürümde kutunun tipi bir parametredir: Kutu<T>. Kutu<String> yazıldığında T'nin yerine String geçer. koy artık yalnızca String kabul eder, al da String döndürür. Okurken dönüşüm gerekmez. Yanlış tipteki k.koy(42) ise program çalışmadan, derlenirken reddedilir.
Jeneriklerin iki kazancı böylece özetlenir. Tip güvenliği: yanlış tipteki değer çalışırken değil, derlerken yakalanır. Yeniden kullanım: tek bir gerçekleme her tip için çalışır ve okurken açık dönüşüm gerekmez.
4.5.8.2Jenerik sınıf ve jenerik metot
Jenerikler (generics), sınıf ve metotların bir ya da birkaç tip parametresiyle yazılmasıdır. Tip parametresi (T), sınıf ya da metot kullanılırken verilen tip argümanıyla (String, Integer …) yer değiştiren bir yer tutucudur.
class Kutu<T> { // T: tip parametresi
private T icerik;
void koy(T x) { icerik = x; }
T al() { return icerik; }
}
static <T> void yazdir(T oge) { // jenerik metot
System.out.println("Öğe: " + oge);
}
// main içinde:
Kutu<String> k1 = new Kutu<>();
k1.koy("Merhaba");
Kutu<Integer> k2 = new Kutu<>();
k2.koy(42);
int n = k2.al() + 1; // kutudan çıkarma
System.out.println(k1.al() + " " + n);
yazdir(3.14);
yazdir('A');Merhaba 43
Öğe: 3.14
Öğe: AKutu<String> k1 ve Kutu<Integer> k2 aynı sınıfın iki farklı kullanımıdır. k2.al() bir Integer döndürür; + 1 işleminde bu değer kutudan çıkarılır ve sonuç 43 olur. new Kutu<>() yazımındaki boş köşeli parantez çiftine elmas (diamond) denir; derleyici tipi sol taraftaki bildirimden çıkarır.
yazdir bir jenerik metottur. Tip parametresini dönüş tipinden önce bildirir: static <T> void yazdir(T oge). Çağrılırken tip yazılmaz; derleyici onu argümandan çıkarır. yazdir(3.14)'te T Double, yazdir('A')'da Character'dır.
Tip argümanı bir referans tipi olmalıdır. Kutu<int> derlenmez; ilkel tipler için sarmalayıcı sınıflar kullanılır (Kutu<Integer>):
JenerikInt.java:6: error: unexpected type
Kutu<int> k = new Kutu<>();
^
required: reference
found: int
1 errorTip parametresi adları geleneksel olarak tek büyük harftir: T tip, E öğe (element), K anahtar (key), V değer (value), R sonuç (result). Function<T, R> adındaki iki harf, girdi tipini ve sonuç tipini anlatır.
4.5.8.3Sınırlı tip parametresi
Yalın bir T hakkında derleyici hiçbir şey bilmez; T tipindeki bir değer üzerinde ancak Object'in metotları çağrılabilir. Bir sayının iki katını hesaplayan jenerik bir metot yazmak istersek T'nin bir sayı olduğunu söylememiz gerekir.
Sınırlı tip parametresi (bounded type parameter), <T extends Ust> biçiminde yazılan tip parametresidir. T yalnızca Ust ya da onun bir alt tipi olabilir. Ust'e üst sınır denir. Karşılığında metot gövdesi Ust'ün metotlarını kullanabilir.
static <T extends Number> double ikiKati(T x) {
return x.doubleValue() * 2; // Number'ın metodu
}
// main içinde:
System.out.println(ikiKati(21)); // T = Integer
System.out.println(ikiKati(1.5)); // T = Double
System.out.println(ikiKati("on")); // derlenmez42.0
3.0<T extends Number> sınırı sayesinde x.doubleValue() çağrısı derlenir; bu metot Number sınıfındadır ve Integer, Double gibi bütün alt sınıflarda vardır. 21 için T = Integer, 1.5 için T = Double çıkarılır ve sonuçlar 42.0 ile 3.0'dır. Üçüncü satırda "on" bir String'dir ve Number'ın alt tipi değildir; derleyici çağrıyı reddeder:
IkiKatiOn.java:6: error: method ikiKati in class IkiKatiOn cannot be applied to given types;
System.out.println(ikiKati("on"));
^
required: T
found: String
reason: inference variable T has incompatible bounds
upper bounds: Number
lower bounds: String
where T is a type-variable:
T extends Number declared in method <T>ikiKati(T)
1 errorİletinin son kısmı derleyicinin akıl yürütmesini gösterir: T için üst sınır Number, argümandan gelen alt sınır ise String'dir. İkisini birden sağlayan bir tip yoktur.
Arayüz de sınır olabilir; sınır bir arayüz olsa da implements değil extends yazılır. Aşağıdaki metot her karşılaştırılabilir tipte (Integer, String …) en büyük öğeyi bulur. T extends Comparable<T> sınırı, T tipindeki her değerin başka bir T ile compareTo metoduyla karşılaştırılabileceğini garanti eder.
static <T extends Comparable<T>> T enBuyuk(T[] dizi) {
T eb = dizi[0];
for (int i = 1; i < dizi.length; i++) {
if (dizi[i].compareTo(eb) > 0) {
eb = dizi[i];
}
}
return eb;
}
// main içinde:
Integer[] sayilar = {7, 12, 5};
String[] adlar = {"elma", "kiraz", "armut"};
System.out.println(enBuyuk(sayilar));
System.out.println(enBuyuk(adlar));12
kiraz
1 -1 6 -10compareTo sözleşmesi şöyledir: a.compareTo(b) negatifse a küçüktür, sıfırsa eşittir, pozitifse büyüktür. Çıktının üçüncü satırı, izlemede kullanılan dört compareTo değerini doğrulamak için programa eklenmiştir.
| Adım | Satır | Değişkenler | Yeni çıktı |
|---|---|---|---|
| 1 | 11: Integer[] sayilar = {7, 12, 5};12: String[] adlar = {"elma", "kiraz", "armut"}; | sayilar=[7, 12, 5], adlar=[elma, kiraz, armut] | — |
| 2 | 13: System.out.println(enBuyuk(sayilar));1: static <T extends Comparable<T>> T enBuyuk(T[] dizi) {2: T eb = dizi[0]; | T=Integer, eb=7 | — |
| 3 | 3: for (int i = 1; i < dizi.length; i++) {4: if (dizi[i].compareTo(eb) > 0) {5: eb = dizi[i]; | T=Integer, i=1, compareTo=1, eb=12 | — |
| 4 | 3: for (int i = 1; i < dizi.length; i++) {4: if (dizi[i].compareTo(eb) > 0) { | T=Integer, i=2, compareTo=-1, eb=12 | — |
| 5 | 8: return eb;13: System.out.println(enBuyuk(sayilar)); | T=Integer, eb=12 | 12 |
| 6 | 14: System.out.println(enBuyuk(adlar));2: T eb = dizi[0]; | T=String, eb="elma" | — |
| 7 | 3: for (int i = 1; i < dizi.length; i++) {4: if (dizi[i].compareTo(eb) > 0) {5: eb = dizi[i]; | T=String, i=1, compareTo=6, eb="kiraz" | — |
| 8 | 3: for (int i = 1; i < dizi.length; i++) {4: if (dizi[i].compareTo(eb) > 0) { | T=String, i=2, compareTo=-10, eb="kiraz" | — |
| 9 | 8: return eb;14: System.out.println(enBuyuk(adlar)); | T=String, eb="kiraz" | kiraz |
Tablo 4.5.12'ün birinci adımında iki dizi kurulur. İkisi de uygundur: Integer ve String, Comparable'ı kendi tipleriyle uygular. Dizideki sayılar otomatik olarak kutulanır. İkinci adımda derleyici T'yi Integer olarak çıkarmıştır. İlk öğe en büyük aday olarak alınır: eb = 7.
Üçüncü adımda 12.compareTo(7) pozitiftir (1); 12 daha büyüktür ve eb = 12 olur. Dördüncü adımda 5.compareTo(12) negatiftir (−1); eb değişmez. Beşinci adımda döngü biter ve 12 döner. Dönüş tipi T, yani Integer'dır; dönüşüm gerekmez.
T = Integer: 12, 7'den büyük olduğu için yeni aday olur. Sağda T = String: “kiraz” sözlük sırasında “elma”dan sonra geldiği için yeni aday olur.Altıncı adımda aynı metot bu kez T = String ile çağrılır. compareTo artık dizgileri sözlük sırasıyla karşılaştırır: iki dizginin ilk farklı karakterlerinin kod farkını döndürür. Yedinci adımda “kiraz” ile “elma”nın ilk harfleri farklıdır: 'k' − 'e' = 107 − 101 = 6. Sonuç pozitiftir ve eb = "kiraz" olur (Şekil 4.5.17). Sekizinci adımda “armut” ile “kiraz” karşılaştırılır: 'a' − 'k' = 97 − 107 = −10; eb değişmez. Son adımda “kiraz” döner.
Tek bir gerçekleme iki farklı tipte doğru çalışmıştır. Üst sınır sayesinde compareTo çağrısı derleme zamanında güvence altındadır: compareTo'su olmayan bir tipin dizisiyle enBuyuk çağrılırsa program derlenmez. Sözlük sırasının harf kodlarına dayandığı unutulmamalıdır. Büyük harflerin kodları küçük harflerinkinden küçüktür; “Zeytin”, “armut”tan önce gelir.
4.5.8.4Joker tipler ve değişmezlik
Integer, Number'ın alt tipidir; bir Integer her yerde Number yerine kullanılabilir. O hâlde List<Integer> da List<Number>'ın alt tipi midir? İlk akla gelen cevap “evet”tir; Java'nın cevabı ise “hayır”dır:
import java.util.*;
public class Degismezlik {
public static void main(String[] args) {
List<Integer> tamlar = Arrays.asList(1, 2, 3);
List<Number> x = tamlar;
}
}Degismezlik.java:5: error: incompatible types: List<Integer> cannot be converted to List<Number>
List<Number> x = tamlar;
^
1 errorDeğişmezlik (invariance): A, B'nin alt tipi olsa bile List<A>, List<B>'nin alt tipi değildir. Jenerik tipler, tip argümanları arasındaki alt tip ilişkisini kendiliğinden devralmaz.
Kuralın nedeni yazma işlemidir. List<Integer> bir List<Number> değişkenine atanabilseydi, o değişken üzerinden listeye 2.5 eklenebilirdi; çünkü 2.5 bir Number'dır. Ama liste aslında tam sayı listesidir ve içine bir Double girmiş olurdu. Listeden okuyan ve Integer bekleyen kod çalışırken çökerdi. Java bu kapıyı derleme zamanında kapatır (Şekil 4.5.18).
Integer, Number'ın alt tipidir; List<Integer> ise List<Number>'ın alt tipi değildir. Bağlantı joker tiple kurulur: List<Integer>, List<? extends Number>'a atanabilir.Değişmezlik güvenlidir ama esnekliği azaltır. Bir sayı listesindeki değerleri toplayan bir metot List<Number> parametresi alırsa ona List<Integer> verilemez. Bu sorunu joker tipler (wildcards) çözer:
?: bilinmeyen herhangi bir tip.? extends T:Tya da onun bir alt tipi. Böyle bir liste değer üretir; ondanTolarak okunur.? super T:Tya da onun bir üst tipi. Böyle bir liste değer tüketir; içineTyazılır.
static double topla(List<? extends Number> liste) {
double t = 0;
for (Number n : liste) t += n.doubleValue(); // okuma
return t;
}
static void doldur(List<? super Integer> liste) {
liste.add(1); // yazma
liste.add(2);
}
// main içinde:
List<Integer> tamlar = Arrays.asList(1, 2, 3);
List<Number> sayilar = new ArrayList<>();
doldur(sayilar);
sayilar.add(2.5);
System.out.println(topla(tamlar) + " " + sayilar);
// List<Number> x = tamlar; derlenmez6.0 [1, 2, 2.5]
5.5topla listeden yalnızca okur; parametresi List<? extends Number>'dır. Bu yüzden List<Integer> da List<Double> da List<Number> da verilebilir. Okunan her öğe en azından bir Number'dır ve doubleValue() çağrılabilir. 1 + 2 + 3 = 6.0 bulunur. doldur ise listeye yalnızca yazar; parametresi List<? super Integer>'dır. Hangi liste gelirse gelsin bir Integer alabilir. Ona verilen List<Number>'a 1 ve 2 eklenir; programın kendisi de 2.5 ekler ve liste [1, 2, 2.5] olur. Çalıştırılan programa eklenen son satır, aynı topla'nın bu karışık listeyle de çalıştığını gösterir: 1 + 2 + 2.5 = 5.5.
? extends ile işaretlenmiş bir listeye yazmaya çalışmak derleme hatasıdır. Derleyici listenin gerçek tipini bilmez; belki bir List<Double>'dır ve içine 1 konamaz:
import java.util.*;
public class ExtendsYaz {
static void ekle(List<? extends Number> liste) {
liste.add(1);
}
}ExtendsYaz.java:4: error: incompatible types: int cannot be converted to CAP#1
liste.add(1);
^
where CAP#1 is a fresh type-variable:
CAP#1 extends Number from capture of ? extends Number
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 errorİletideki CAP#1, derleyicinin bilinmeyen tip için uydurduğu addır. Hangi jokerin seçileceğini hatırlatan kısa bir kural vardır: PECS (Producer Extends, Consumer Super). Değer üreten (okunan) yapı extends, değer tüketen (yazılan) yapı super ile yazılır.
4.5.8.5Tip silme
Jenerik tip bilgisi çoğunlukla derleyici içindir. Derleyici tip argümanlarını denetler, gereken dönüşümleri ekler ve sonra bu bilgiyi siler. Üretilen kodda T'nin yerinde Object (ya da sınırlı parametrede sınır tipi) kalır.
Tip silme (type erasure), jenerik tip argümanlarının derleme sırasında denetlendikten sonra üretilen bayt kodundan çıkarılmasıdır. Çalışma zamanında Kutu<String> ile Kutu<Integer> aynı sınıftır.
Kutu<String> ve dönüşümsüz okuma vardır. javac tipleri denetler ve siler; bayt kodda Kutu ile derleyicinin eklediği (String) dönüşümü kalır.class Kutu<T> {
private T icerik;
// T yeni() { return new T(); } derlenmez
}
// main içinde:
Kutu<String> a = new Kutu<>();
Kutu<Integer> b = new Kutu<>();
System.out.println(a.getClass() == b.getClass());
System.out.println(a.getClass().getName());true
Kutuİki kutunun getClass() sonucu aynı nesnedir: true. Sınıfın adı da yalnızca Kutu'dur; tip argümanı adın parçası değildir. Şekil 4.5.19 derleyicinin yaptığı dönüşümü gösterir: programcının yazmadığı (String) dönüşümünü bayt koda derleyici ekler. Tip denetimi geçtiği için bu dönüşüm hiçbir zaman başarısız olmaz.
Tip silmenin bir sonucu, T'nin çalışma zamanında bilinmemesidir. Bu yüzden new T() yazılamaz; JVM hangi sınıfın nesnesini oluşturacağını bilemez:
NewT.java:3: error: unexpected type
T yeni() { return new T(); }
^
required: class
found: type parameter T
where T is a type-variable:
T extends Object declared in class Kutu
1 errorAynı nedenle new T[10] ile jenerik dizi de oluşturulamaz. İleti bu kez farklıdır:
NewTDizi.java:2: error: generic array creation
T[] dizi = new T[10];
^
1 error4.5.9Etiketler
Koda yalnızca derleyicinin ya da bir aracın okuyacağı bilgiler eklemek bazen işe yarar: “Bu metot üst tipteki bir metodu geçersiz kılıyor”, “Bu sınıf artık kullanılmamalı”, “Bu metot bir testtir.” Java bu tür bilgiler için etiketleri kullanır.
Etiket (annotation), sınıf, metot, alan ya da parametreye @ ile eklenen üst veridir. Etiket kendi başına davranış değiştirmez. Onu derleyici, araçlar ya da çalışan program (yansıma ile) okur ve ona göre davranır.
| Etiket | Kim okur? | Ne yapar? |
|---|---|---|
@Override | derleyici | Üst tipte böyle bir metot yoksa hata verir |
@Deprecated | derleyici | Eskimiş öğe; onu kullanan kod uyarı alır |
@SuppressWarnings("unchecked") | derleyici | Belirtilen uyarıyı susturur |
@FunctionalInterface | derleyici | Arayüzde yalnızca bir soyut metot olduğunu denetler |
@Test (JUnit) | test aracı | Test metotlarını bulur ve çalıştırır |
Tablodaki etiketlerin dördünü derleyici okur. @Override bu notta birkaç kez işe yaradı: imzası yanlış yazılan bir metot, etiket sayesinde hatanın gerçek nedeniyle birlikte yakalanır. @FunctionalInterface'in denetimi ise şöyle görünür:
@FunctionalInterface
interface Islem {
int uygula(int a, int b);
int ters(int a); // 2. soyut metot
}FonkIki.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
Islem is not a functional interface
multiple non-overriding abstract methods found in interface Islem
1 errorEtiket yazılmasaydı arayüz sorunsuz derlenirdi; hata ancak biri bu arayüze bir lambda atamaya çalıştığında, başka bir dosyada ortaya çıkardı. Etiket, niyeti koda yazar ve derleyiciye denetletir.
Programcı kendi etiketini de tanımlayabilir. Etiket @interface ile tanımlanır; öğeleri metot gibi yazılır ve default ile varsayılan değer alabilir. Aşağıdaki @Gorev etiketi bir metodun yazarını ve önceliğini kaydeder. Program, çalışırken kendi sınıfını inceleyip etiketli metotları bulur ve çağırır:
@Retention(RetentionPolicy.RUNTIME) // çalışırken okunur
@Target(ElementType.METHOD) // yalnızca metotlara
@interface Gorev {
String yazar();
int oncelik() default 1;
}
class Isler {
@Gorev(yazar = "Ayşe", oncelik = 3)
public void yedekle() { System.out.println("Yedek alındı"); }
public void temizle() { System.out.println("Temizlendi"); }
}
// main içinde (yansıma, java.lang.reflect.Method):
Isler isler = new Isler();
for (Method m : Isler.class.getDeclaredMethods()) {
Gorev g = m.getAnnotation(Gorev.class);
if (g != null) {
System.out.println(m.getName() + ", "
+ g.yazar() + ", " + g.oncelik());
m.invoke(isler);
}
}yedekle, Ayşe, 3
Yedek alındıEtiketin üstündeki iki etiket, etiketin kendisi hakkında bilgi verir. @Target(ElementType.METHOD) etiketin yalnızca metotlara yazılabileceğini söyler. @Retention(RetentionPolicy.RUNTIME) ise etiketin çalışma zamanına kadar saklanacağını belirtir. Döngü Isler sınıfının bütün metotlarını dolaşır. getAnnotation(Gorev.class) metotta @Gorev yoksa null döndürür. Yalnızca yedekle etiketlidir; onun adı, yazarı ve önceliği yazdırılır, sonra m.invoke(isler) metodu çağırır ve “Yedek alındı” yazılır. temizle etiketsiz olduğu için hiç çağrılmaz.
@Retention | Etiket nerede kalır? |
|---|---|
SOURCE | Yalnızca kaynak kodda (@Override gibi); derleyici okuduktan sonra atılır |
CLASS | .class dosyasında; yansıma göremez (varsayılan değer) |
RUNTIME | Çalışırken yansımayla okunur |
Tablo 4.5.14'daki farkı denemek kolaydır. Aynı program etiketin @Retention değeri CLASS yapılarak çalıştırılırsa etiket .class dosyasında durur ama yansıma onu göremez:
import java.lang.annotation.*;
import java.lang.reflect.Method;
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
@interface Gorev {
String yazar();
int oncelik() default 1;
}
class Isler {
@Gorev(yazar = "Ayşe", oncelik = 3)
public void yedekle() { System.out.println("Yedek alındı"); }
}
public class Main {
public static void main(String[] args) throws Exception {
int bulunan = 0;
for (Method m : Isler.class.getDeclaredMethods()) {
if (m.getAnnotation(Gorev.class) != null) bulunan++;
}
System.out.println("bulunan etiket: " + bulunan);
}
}bulunan etiket: 0Yansıma (reflection), programın kendi sınıflarını, metotlarını, alanlarını ve etiketlerini çalışırken incelemesi ve kullanmasıdır. java.lang.reflect paketindeki Method, Field gibi sınıflarla yapılır.
Yansıma güçlü bir araçtır; test araçları ve büyük uygulama çerçeveleri etiketli metotları bu yolla bulur. Bedeli de vardır: m.invoke(...) gibi çağrılar derleyicinin denetiminden geçmez, yanlış bir çağrı ancak çalışırken fark edilir, kod da daha yavaş çalışır. Bu yüzden yansıma sıradan uygulama kodunda değil, gerçekten gerektiği yerlerde kullanılır.
4.5.10Tasarım desenleri
Arayüzler, soyut sınıflar ve lambdalar birer dil aracıdır. Bu araçların hangi problemde nasıl bir araya getirileceği ise ayrı bir bilgidir. Deneyimli programcılar aynı tasarım sorunlarıyla tekrar tekrar karşılaşmış ve işe yarayan çözümlere ad vermişlerdir. Aşağıdaki beş desenin hepsi küçük konsol programlarıyla anlatılmıştır; dördü doğrudan bir arayüze dayanır.
4.5.10.1Desen nedir?
Tasarım deseni (design pattern), sık karşılaşılan bir tasarım sorununa denenmiş ve yeniden kullanılabilir bir çözüm kalıbıdır. Desen kopyalanacak hazır bir kod değildir; sorunu, çözümü ve çözümün ödünleşimlerini adlandıran ortak bir sözlüktür.
Desenlerin ortak amacı sorumlulukları ayırmak ve sınıflar arasındaki bağımlılığı azaltmaktır. Bağımlılığı az olan bir parça, ötekilere dokunmadan değiştirilebilir ve tek başına denenebilir. Desenler geleneksel olarak üç gruba ayrılır (Şekil 4.5.20). Oluşturucu desenler nesnenin nasıl oluşturulacağıyla, yapısal desenler nesnelerin nasıl birleştirileceğiyle ilgilenir. Davranışsal desenler ise nesnelerin iş birliğini düzenler.
Desen ancak gerçek bir sorunu çözüyorsa yararlıdır. Gereksiz yere eklenen desen programı karmaşıklaştırır. Desenler, “bileşenler arasında az bağımlılık” ve “kalıtım yerine birleşim” gibi daha genel tasarım ilkelerinin somut uygulamalarıdır. Birleşim (composition), bir sınıfın davranışını kalıtımla devralmak yerine başka bir nesneyi alan olarak tutup işi ona yaptırmasıdır.
4.5.10.2Tekil (Singleton)
Bazı sınıfların programda tek bir nesnesi olmalıdır. Uygulamanın ayarları, bir günlük dosyası ya da bir donanım sürücüsü iki kopya hâlinde tutulursa kopyalar birbirinden farklılaşabilir. Tekil deseni sınıfın yalnızca bir nesnesi olmasını ve o nesneye tek bir yerden erişilmesini sağlar.
public final class Ayarlar {
private static final Ayarlar TEK = new Ayarlar();
private Ayarlar() {
System.out.println("Ayarlar oluşturuldu");
}
public static Ayarlar getInstance() { return TEK; }
}
// main:
System.out.println("main başladı");
Ayarlar a = Ayarlar.getInstance();
Ayarlar b = Ayarlar.getInstance();
System.out.println(a == b);main başladı
Ayarlar oluşturuldu
trueDesenin üç parçası vardır. Birincisi, yapıcı private'tır; sınıfın dışından new Ayarlar() yazılamaz:
TekilNew.java:8: error: Ayarlar() has private access in Ayarlar
Ayarlar a = new Ayarlar();
^
1 errorİkincisi, tek nesne sınıfın içinde private static final bir alanda tutulur. Üçüncüsü, bu nesneye public static bir metotla (getInstance()) erişilir. İki çağrı aynı nesneyi döndürdüğü için a == b true verir.
Çıktının sırası da öğreticidir: “main başladı” iletisi “Ayarlar oluşturuldu”dan önce yazılmıştır. JVM TEK alanını, Ayarlar sınıfı ilk kullanıldığında, yani ilk getInstance() çağrısında bir kez ilklendirir. JVM sınıf ilklendirmesini yalnızca bir kez yaptığı için bu yöntem birden çok iş parçacığı olduğunda da güvenlidir.
Tekil desenin ciddi bir sakıncası vardır: gizli bir küresel değişken gibi davranır. Ayarlar.getInstance()'ı içeriden çağıran her sınıf, o tek nesneye görünmez biçimde bağlanır. Bu sınıfları denerken gerçek ayarların yerine başka bir nesne koymak zorlaşır. Çoğu zaman daha iyi bir yol, nesneyi ona ihtiyaç duyan sınıfın yapıcısına parametre olarak vermektir. Bağımlılığın dışarıdan verilmesine bağımlılık enjeksiyonu (dependency injection) denir.
4.5.10.3Fabrika
Bir çizim programı, kullanıcının menüden seçtiği şekli oluşturacak olsun. Seçim bir dizgi olarak gelir: “kare”, “daire” … Her yerde if zinciriyle hangi sınıfın oluşturulacağına karar vermek, oluşturma kodunu programa dağıtır. Fabrika deseni nesne oluşturmayı tek bir yerde toplar.
interface Sekil { double alan(); }
class Kare implements Sekil { … } // alan: a * a
class Daire implements Sekil { … } // alan: π r²
class SekilFabrikasi {
static Sekil olustur(String tur, double olcu) {
switch (tur.toLowerCase(Locale.ROOT)) {
case "kare": return new Kare(olcu);
case "daire": return new Daire(olcu);
default: return null; // bilinmeyen tür
}
}
}
Sekil s = SekilFabrikasi.olustur("Kare", 3);
System.out.println(s.alan()); // 9.0Fabrika metodu (factory method), nesneyi üretip üst sınıf ya da arayüz tipinde döndüren metottur. Çağıran kod hangi somut sınıfın oluşturulduğunu bilmez.
SekilFabrikasi.olustur metodu türü ve ölçüyü alır, uygun nesneyi oluşturur ve onu Sekil arayüzü tipinde döndürür. Çağıran kod Kare'yi değil, yalnızca Sekil'i bilir. Kodda … ile kısaltılan sınıf gövdeleri, çalıştırılan programda tamamlanmıştır:
import java.util.Locale;
interface Sekil { double alan(); }
class Kare implements Sekil {
private final double a;
Kare(double a) { this.a = a; }
public double alan() { return a * a; }
}
class Daire implements Sekil {
private final double r;
Daire(double r) { this.r = r; }
public double alan() { return Math.PI * r * r; }
}
class SekilFabrikasi {
static Sekil olustur(String tur, double olcu) {
switch (tur.toLowerCase(Locale.ROOT)) {
case "kare": return new Kare(olcu);
case "daire": return new Daire(olcu);
default: return null; // bilinmeyen tür
}
}
}
public class Main {
public static void main(String[] args) {
Sekil s = SekilFabrikasi.olustur("Kare", 3);
System.out.println(s.alan()); // 9.0
Sekil u = SekilFabrikasi.olustur("üçgen", 2);
System.out.println(u == null); // true
System.out.println(SekilFabrikasi.olustur("DAIRE", 1).alan());
System.out.println("DAIRE".toLowerCase(Locale.forLanguageTag("tr-TR")) + " " + "DAIRE".toLowerCase(Locale.ROOT));
}
}9.0
true
3.141592653589793
daıre daire“Kare” ve 3 için alan 9.0'dır. Yeni bir şekil eklemek yalnızca fabrikayı ve yeni sınıfı değiştirir; şekil isteyen kodlar aynı kalır. Bilinmeyen bir tür için fabrika null döndürür ve çağıran kod sonucu denetler: “üçgen” için u == null true verir.
toLowerCase(Locale.ROOT) ayrıntısı Türkçe kullanıcılar için önemlidir. toLowerCase() parametresiz çağrılırsa bilgisayarın yerel ayarına göre çalışır. Türkçe yerel ayarda büyük “I” harfinin küçüğü noktasız “ı”dır; “DAIRE” “daıre” olur ve "daire" dalıyla eşleşmez. Programın son satırı bu farkı gösterir: Türkçe kurallarla “daıre”, dilden bağımsız Locale.ROOT ile “daire” elde edilir. Fabrika Locale.ROOT kullandığı için “DAIRE” isteği doğru şekli üretir ve alanı π · 1 · 1 = 3.141592653589793 yazdırılır.
Fabrika her yerde gerekmez. Zaten basit oluşturulan nesneler için ayrı bir fabrika yazmak gereksiz karmaşıklıktır; new Nokta(3, 4) kadar açık bir ifadeyi bir metodun arkasına saklamak okuru yalnızca yorar.
4.5.10.4Gözlemci
Bir hava istasyonu sıcaklığı ölçüyor. Yeni bir ölçüm geldiğinde ekran onu göstermeli, bir uyarı modülü sıcaklık 30 dereceyi aşınca uyarmalı, bir kayıtçı ölçümü saklamalıdır. İstasyon bu üç parçayı doğrudan çağırırsa her yeni parça istasyonun kodunu değiştirir. Gözlemci deseni bu bağımlılığı tersine çevirir.
Gözlemci deseni (observer), bir konu nesnesi ile ona kayıtlı gözlemciler arasında bire çok ilişki kurar. Konunun durumu değişince kayıtlı bütün gözlemcilere haber verilir. Konu, gözlemcilerin somut sınıfını bilmez; yalnızca ortak arayüzü çağırır.
interface Gozlemci { void guncelle(int derece); }
class Istasyon {
private final List<Gozlemci> liste = new ArrayList<>();
void ekle(Gozlemci g) { liste.add(g); }
void setSicaklik(int d) {
for (Gozlemci g : liste) g.guncelle(d);
}
}guncelle metodunu çağırır; gözlemcilerin ne yaptığını bilmez.Gozlemci tek metotlu bir arayüzdür. Istasyon gözlemcileri bir listede tutar. ekle yeni bir gözlemciyi kaydeder, setSicaklik ise listeyi dolaşıp her gözlemcinin guncelle metodunu çağırır. Arayüzün tek metodu olduğu için gözlemciler lambdayla da yazılabilir. Aşağıdaki programda iki lambda gözlemci kaydedilir:
interface Gozlemci { void guncelle(int derece); }
class Istasyon {
private final List<Gozlemci> liste = new ArrayList<>();
void ekle(Gozlemci g) { liste.add(g); }
void setSicaklik(int d) {
for (Gozlemci g : liste) g.guncelle(d);
}
}
// main:
Istasyon ist = new Istasyon();
ist.ekle(d -> System.out.println("Ekran: " + d + " °C"));
ist.ekle(d -> { if (d > 30) System.out.println("Uyarı: sıcak!"); });
ist.setSicaklik(25);
ist.setSicaklik(34);Ekran: 25 °C
Ekran: 34 °C
Uyarı: sıcak!| Adım | Satır | Değişkenler | Yeni çıktı |
|---|---|---|---|
| 1 | 10: Istasyon ist = new Istasyon(); | ist=Istasyon, liste=[] | — |
| 2 | 11: ist.ekle(d -> System.out.println("Ekran: " + d + " °C"));4: void ekle(Gozlemci g) { liste.add(g); } | liste=[ekran] | — |
| 3 | 12: ist.ekle(d -> { if (d > 30) System.out.println("Uyarı: sıcak!"); });4: void ekle(Gozlemci g) { liste.add(g); } | liste=[ekran, uyarı] | — |
| 4 | 13: ist.setSicaklik(25);6: for (Gozlemci g : liste) g.guncelle(d); | d=25 | — |
| 5 | 6: for (Gozlemci g : liste) g.guncelle(d);11: ist.ekle(d -> System.out.println("Ekran: " + d + " °C")); | d=25, g=ekran | Ekran: 25 °C |
| 6 | 6: for (Gozlemci g : liste) g.guncelle(d);12: ist.ekle(d -> { if (d > 30) System.out.println("Uyarı: sıcak!"); }); | d=25, g=uyarı | — |
| 7 | 14: ist.setSicaklik(34);6: for (Gozlemci g : liste) g.guncelle(d); | d=34 | — |
| 8 | 6: for (Gozlemci g : liste) g.guncelle(d);11: ist.ekle(d -> System.out.println("Ekran: " + d + " °C")); | d=34, g=ekran | Ekran: 34 °C |
| 9 | 6: for (Gozlemci g : liste) g.guncelle(d);12: ist.ekle(d -> { if (d > 30) System.out.println("Uyarı: sıcak!"); }); | d=34, g=uyarı | Uyarı: sıcak! |
Tablo 4.5.15'nin birinci adımında konu nesnesi oluşur; gözlemci listesi boştur. İkinci adımda ilk gözlemci eklenir. Bu bir lambdadır ve Gozlemci arayüzünün tek metodunu uygular. Lambda listeye eklenir ama çalışmaz. Üçüncü adımda ikinci gözlemci eklenir; bu gözlemci yalnızca 30 derecenin üstünde konuşur.
setSicaklik(34) listedeki ikinci gözlemciye ulaşmış; koşul doğru olduğu için uyarı yazılıyor.Dördüncü adımda setSicaklik(25) çağrılır ve döngü listeyi baştan dolaşır. Beşinci adımda ekran gözlemcisinin guncelle(25)'i çağrılır ve “Ekran: 25 °C” yazılır. Altıncı adımda uyarı gözlemcisi de haber alır, ama 25 > 30 yanlış olduğu için bir şey yazmaz. Yedinci adımda yeni ölçüm gelir: 34. Sekizinci adımda ekran yeni değeri yazar. Dokuzuncu adımda koşul bu kez doğrudur ve “Uyarı: sıcak!” yazılır (Şekil 4.5.22).
İstasyon, hiçbir gözlemcinin ne yaptığını bilmemiştir. Yeni bir gözlemci eklemek istasyonun kodunu değiştirmez. Gözlemci lambda olmak zorunda da değildir; arayüzü uygulayan sıradan bir sınıf da olabilir. Aşağıdaki kayıtçı her ölçümü bir listede saklar:
import java.util.*;
interface Gozlemci { void guncelle(int derece); }
class Istasyon {
private final List<Gozlemci> liste = new ArrayList<>();
void ekle(Gozlemci g) { liste.add(g); }
void setSicaklik(int d) {
for (Gozlemci g : liste) g.guncelle(d);
}
}
class Kayitci implements Gozlemci {
private final List<Integer> olcumler = new ArrayList<>();
public void guncelle(int derece) { olcumler.add(derece); }
List<Integer> getOlcumler() { return olcumler; }
}
public class Main {
public static void main(String[] args) {
Istasyon ist = new Istasyon();
Kayitci k = new Kayitci();
ist.ekle(k);
ist.setSicaklik(25);
ist.setSicaklik(34);
ist.setSicaklik(18);
System.out.println(k.getOlcumler());
}
}[25, 34, 18]Kayıtçı istasyona bir kez kaydedilmiş ve üç ölçümün hepsini almıştır. Bu örnek, gözlemcinin konudan bağımsız olarak denenebildiğini de gösterir: kayıtçının listesi, istasyonun gerçekten her ölçümü ilettiğinin kanıtıdır. Gözlemci deseni, grafik arayüzlerdeki düğme tıklamalarından tablo programlarındaki hücre güncellemelerine kadar, “bir şey olunca haber ver” gereken her yerde kullanılır.
4.5.10.5Strateji
Bir mağazanın kasası ödenecek tutarı hesaplıyor. Öğrenciye %10 indirim, kampanya döneminde 200 TL üstüne 50 TL indirim uygulanıyor; yarın başka bir kural gelebilir. Kasa bu kuralları if zinciriyle kendisi bilirse her yeni kural kasanın kodunu değiştirir.
Strateji deseni (strategy), birbirinin yerine geçebilen algoritmaları ortak bir arayüzün arkasına koyar. Algoritmayı kullanan nesne, işi kendisine verilen strateji nesnesine devreder; strateji değişince kodu değişmeden davranışı değişir.
interface Indirim { double uygula(double tutar); }
class OgrenciIndirimi implements Indirim {
public double uygula(double tutar) { return tutar * 0.90; }
}
class Kasa {
private final Indirim strateji;
Kasa(Indirim strateji) { this.strateji = strateji; }
double odenecek(double tutar) {
return strateji.uygula(tutar);
}
}
Kasa k = new Kasa(new OgrenciIndirimi());
System.out.println(k.odenecek(250)); // 225.0225.0Kasa indirimi kendisi hesaplamaz. Yapıcıda verilen Indirim nesnesini bir alanda tutar ve odenecek çağrıldığında işi ona devreder. OgrenciIndirimi bu arayüzün bir gerçeklemesidir: 250 · 0.90 = 225.0. Arayüzün tek metodu olduğu için strateji bir lambda da olabilir. Aşağıdaki program aynı Kasa sınıfını üç farklı stratejiyle kullanır:
interface Indirim { double uygula(double tutar); }
class Kasa {
private final Indirim strateji;
Kasa(Indirim strateji) { this.strateji = strateji; }
double odenecek(double tutar) { return strateji.uygula(tutar); }
}
// main:
Kasa ogrenci = new Kasa(t -> t * 0.90);
Kasa kampanya = new Kasa(t -> t >= 200 ? t - 50 : t);
System.out.println(ogrenci.odenecek(250));
System.out.println(kampanya.odenecek(250));
System.out.println(kampanya.odenecek(120));
// sabit strateji: Kasa tek başına denenir
System.out.println(new Kasa(t -> 7).odenecek(100));225.0
200.0
120.0
7.0| Adım | Satır | Değişkenler | Yeni çıktı |
|---|---|---|---|
| 1 | 8: Kasa ogrenci = new Kasa(t -> t * 0.90); | ogrenci=Kasa(%10) | — |
| 2 | 9: Kasa kampanya = new Kasa(t -> t >= 200 ? t - 50 : t); | kampanya=Kasa(200 ve üstü −50) | — |
| 3 | 10: System.out.println(ogrenci.odenecek(250));5: double odenecek(double tutar) { return strateji.uygula(tutar); } | tutar=250.0 | 225.0 |
| 4 | 11: System.out.println(kampanya.odenecek(250));5: double odenecek(double tutar) { return strateji.uygula(tutar); } | tutar=250.0 | 200.0 |
| 5 | 12: System.out.println(kampanya.odenecek(120));5: double odenecek(double tutar) { return strateji.uygula(tutar); } | tutar=120.0 | 120.0 |
| 6 | 14: System.out.println(new Kasa(t -> 7).odenecek(100)); | tutar=100.0 | 7.0 |
Tablo 4.5.16'nin birinci adımında ilk kasaya %10 indirim lambdası takılır. İkinci adımda aynı Kasa sınıfı başka bir stratejiyle kurulur: tutar 200 TL ya da üstündeyse 50 TL düşülür. Üçüncü adımda ogrenci.odenecek(250) işi stratejiye devreder: 250 × 0.90 = 225.0. Dördüncü adımda kampanya stratejisi çalışır: 250 ≥ 200 olduğu için 250 − 50 = 200.0. Beşinci adımda 120 < 200'dür ve indirim yoktur: 120.0. Kasanın kodu hiç değişmeden davranışı değişmiştir.
t × 0.90, t ≥ 200 ? t − 50 : t ve tutardan bağımsız olarak 7 döndüren sabit strateji.Altıncı adımın amacı farklıdır. Gerçek bir indirim kuralı yerine, tutar ne olursa olsun 7 döndüren sabit bir strateji takılır (Şekil 4.5.23). Sonuç 7.0'dır; t -> 7 lambdasının döndürdüğü int değer, metodun dönüş tipi double olduğu için genişletilmiştir. Bu satır indirim hesabını değil, yalnızca Kasa'nın kendisini dener: Kasa işi stratejiye doğru biçimde devrediyor mu? Sabit strateji takıldığında sonuç yalnızca stratejiden gelebilir; 7.0 görülüyorsa devretme doğrudur. Bir parçayı, bağımlı olduğu parçanın yerine basit bir sahte nesne koyarak tek başına denemek, stratejinin dışarıdan verilmesiyle mümkün olmuştur.
Java kütüphanesinde stratejinin en bilinen örneği sıralamadır. isimler.sort((a, b) -> a.length() - b.length()) çağrısındaki lambda bir Comparator stratejisidir; sıralama algoritması aynı kalır, karşılaştırma ölçütü değişir.
4.5.10.6MVC
Kullanıcıyla etkileşen programlarda üç farklı iş iç içe geçme eğilimindedir: verinin kendisi ve kuralları, verinin ekranda gösterilmesi ve kullanıcının girdilerine tepki verilmesi. Bu üç işi tek sınıfta toplamak, ekranda yapılan küçük bir değişikliğin veri kurallarını bozmasına yol açar.
MVC (Model–View–Controller, Model–Görünüm–Denetleyici), programı üç sorumluluğa ayıran mimari kalıptır. Model veriyi ve kuralları tutar, görünüm yalnızca gösterir, denetleyici girdiyi alır ve modeli ile görünümü yönetir.
Sorumlulukları tek tek ele alalım. Model (Ogrenci, getAd()) veriyi ve kuralları içerir. Ekrandan habersizdir; ekran olmadan da denenebilir. Görünüm (OgrenciGorunum) yalnızca gösterir; goster(ad) metodu konsola yazar. Denetleyici (OgrenciDenetleyici) girdiyi alır ve iki tarafı yönetir; guncelle() modeli okur, sonucu görünüme verir.
Ogrenci m = new Ogrenci("Ayşe");
OgrenciGorunum g = new OgrenciGorunum();
OgrenciDenetleyici d =
new OgrenciDenetleyici(m, g);
d.guncelle(); // g.goster(m.getAd())Bu kısa kesitte kullanılan üç sınıfın tam hâli aşağıdaki programdadır:
class Ogrenci {
private final String ad;
Ogrenci(String ad) { this.ad = ad; }
String getAd() { return ad; }
}
class OgrenciGorunum {
void goster(String ad) { System.out.println("Öğrenci: " + ad); }
}
class OgrenciDenetleyici {
private final Ogrenci model;
private final OgrenciGorunum gorunum;
OgrenciDenetleyici(Ogrenci model, OgrenciGorunum gorunum) {
this.model = model;
this.gorunum = gorunum;
}
void guncelle() { gorunum.goster(model.getAd()); }
}
public class Main {
public static void main(String[] args) {
Ogrenci m = new Ogrenci("Ayşe");
OgrenciGorunum g = new OgrenciGorunum();
OgrenciDenetleyici d =
new OgrenciDenetleyici(m, g);
d.guncelle(); // g.goster(m.getAd())
}
}Öğrenci: AyşeDenetleyici, model ve görünüm nesnelerini yapıcısında alır. Bu da bir bağımlılık enjeksiyonudur: denetleyici hangi model ve görünümle çalışacağını kendisi oluşturmaz, dışarıdan alır. Görünümü değiştirmek, örneğin adı konsol yerine bir dosyaya yazmak, yalnızca OgrenciGorunum'u etkiler; model hiç değişmez. Uygulama çerçeveleri MVC'yi farklı biçimlerde uygular. Tek bir “doğru” sınıf yapısı yoktur; ortak olan, üç sorumluluğun birbirinden ayrılmasıdır.
4.5.10.7Desen ne zaman kullanılmaz?
Desen öğrenmenin bir yan etkisi, onları gereksiz yerlerde kullanma isteğidir. Dört yaygın yanlış vardır:
- Tekil'i küresel değişken yapmak. Her yerden erişilen ve değiştirilebilen durum, programın parçalarını birbirine görünmez biçimde bağlar.
- Gereksiz fabrika.
new Nokta(3, 4)kadar basit bir oluşturma için ayrı bir sınıf yazmak. - Gereksiz gözlemci. Tek bir doğrudan metot çağrısı yeterken bir olay sistemi kurmak; programın akışını izlemek zorlaşır.
- Deseni ad olsun diye kullanmak. Önce sorun, sonra desen gelir. Bir desen eklenmeden önce iki soru sorulur: “Hangi değişikliği kolaylaştırıyor? Hangi bağımlılığı azaltıyor?”
Strateji ve bağımlılığın dışarıdan verilmesi, gerçek bir parçanın yerine basit bir sahte nesne koymayı sağlar. Kasaya sabit strateji takmak ve ödeme metoduna sahte ödeme nesnesi vermek bunun örnekleridir. Tersinden bakılırsa tek başına denenmesi zor bir sınıf, çoğu zaman tasarımının gözden geçirilmesi gerektiğini gösterir.
4.5.11Örnek problemler ve alıştırmalar
4.5.11.1Örnek problemler
Aşağıdaki problemler, soyut sınıf ve arayüz kurallarının bir arada uygulanmasını ister. Her problemin çözümü, kuralı gerekçesiyle birlikte verir; çıktılar ve derleyici iletileri çalıştırılarak doğrulanmıştır.
Aşağıdaki (a)–(e) parçalarının her biri için: parça derlenir mi, derlenmiyorsa hata nedir? Derlenen parçalardan hangisinin nesnesi new ile oluşturulabilir?
// (a)
abstract class A {
void f() { System.out.println("f"); }
}
// (b)
class B { abstract void g(); }
// (c)
interface I { int X = 1; }
... I.X++;
// (d)
interface J { void h(); }
class C implements J { void h() { } }
// (e)
abstract class D { abstract void k(); }
abstract class E extends D { }(a) derlenir. A'nın hiç soyut metodu yoktur ama soyut işaretlenmiştir; bu geçerlidir. (b) derlenmez. B'nin soyut bir metodu vardır, kendisi ise soyut değildir:
P57b.java:1: error: B is not abstract and does not override abstract method g() in B
class B { abstract void g(); }
^
1 error(c) derlenmez. X arayüzde tanımlandığı için örtük olarak public static final'dır; I.X++ ona yeni bir değer atamaya çalışır:
P57c.java:3: error: cannot assign a value to static final variable X
void f() { I.X++; }
^
1 error(d) derlenmez. J'deki h() örtük olarak public'tir; C'deki h() ise paket erişimlidir ve erişimi daraltır:
P57d.java:2: error: h() in C cannot implement h() in J
class C implements J { void h() { } }
^
attempting to assign weaker access privileges; was public
1 error(e) derlenir. E de soyut olduğu için k()'yı yazmak zorunda değildir; yükümlülük E'nin somut alt sınıflarına geçer. Sorunun ikinci kısmının cevabı “hiçbiri”dir. Derlenen iki parçadaki A ve E soyuttur; new A() ve new E() da derleme hatasıdır:
P57new.java:8: error: A is abstract; cannot be instantiated
A a = new A();
^
P57new.java:9: error: E is abstract; cannot be instantiated
E e = new E();
^
2 errorsAşağıdaki program ne yazdırır? Balik, ses()'i yazmadığı hâlde neden derlenir? Kedi'deki ses()'ten public silinirse ne olur? Sesli s = new Balik(); s.ad(); derlenir mi?
interface Sesli {
default String ses() { return "..."; }
}
abstract class Hayvan implements Sesli {
Hayvan() { System.out.print("H "); }
abstract String ad();
}
class Kedi extends Hayvan {
Kedi() { System.out.print("K "); }
String ad() { return "kedi"; }
public String ses() { return "miyav"; }
}
class Balik extends Hayvan {
String ad() { return "balık"; }
}
// main içinde:
Hayvan[] d = { new Kedi(), new Balik() };
System.out.println();
for (Hayvan h : d)
System.out.println(h.ad() + ": " + h.ses());H K H
kedi: miyav
balık: ...Dizi kurulurken iki nesne oluşturulur. new Kedi()'de önce üst sınıfın yapıcısı çalışır ve “H ” yazar, sonra Kedi'nin yapıcısı “K ” yazar. new Balik()'te Balik'in yazılmış bir yapıcısı yoktur; derleyicinin eklediği varsayılan yapıcı super()'i çağırır ve yalnızca “H ” yazılır. Böylece ilk satır “H K H ” olur; println() satırı bitirir. Döngüde ad() her nesnede kendi sınıfının sürümünü çalıştırır. Kedi ses()'i geçersiz kılmıştır: “miyav”. Balik kılmamıştır; arayüzün varsayılan gövdesi çalışır: “...”.
Balik'in derlenmesinin nedeni, ses()'in soyut değil varsayılan bir metot olmasıdır. Balik onu kalıtımla alır. Somut olmak için tek soyut metot olan ad()'ı yazması yeterlidir. Kedi'deki ses()'ten public silinirse metot arayüzdeki public sürümü daha dar erişimle geçersiz kılmaya çalışır:
P58public.java:9: error: ses() in Kedi cannot implement ses() in Sesli
String ses() { return "miyav"; }
^
attempting to assign weaker access privileges; was public
1 errorSon soru referans tipinin rolünü sınar. s'nin tipi Sesli'dir ve Sesli yalnızca ses()'i bilir; nesnenin ad() metodu olsa da çağrı derlenmez:
P58yuz.java:13: error: cannot find symbol
s.ad();
^
symbol: method ad()
location: variable s of type Sesli
1 errorSoyut sınıfın yapıcısı bir static sayacı artırıyor ve somut ozet() metodu soyut alan()'ı çağırıyor. Program ne yazar?
abstract class Sekil {
static int adet = 0;
String ad;
Sekil(String ad) { this.ad = ad; adet++; }
abstract double alan();
String ozet() { return ad + ":" + alan(); }
}
class Kare extends Sekil {
double k;
Kare(double k) { super("Kare"); this.k = k; }
double alan() { return k * k; }
}
class Daire extends Sekil {
double r;
Daire(double r) { super("Daire"); this.r = r; }
double alan() { return 3 * r * r; }
}
public class Main {
public static void main(String[] args) {
Sekil[] d = { new Kare(2), new Daire(1) };
for (Sekil s : d) System.out.println(s.ozet());
System.out.println(Sekil.adet);
}
}Kare:4.0
Daire:3.0
2Dizinin öğe tipi soyut Sekil'dir, gösterdiği nesneler somuttur. new Kare(2), super("Kare") ile Sekil'in yapıcısını çağırır; ayrı bir Sekil nesnesi oluşmaz, aynı nesnenin Sekil kısmı kurulur ve adet 1 olur. new Daire(1) aynı yoldan geçer ve adet 2 olur. Döngüde ozet() Sekil'den gelir; içindeki alan() çağrısı this'in sınıfına bağlanır. Kare için 2 · 2 = 4.0, Daire için 3 · 1 · 1 = 3.0 bulunur. Son satır, iki yapıcı çağrısını sayan static sayacı yazar: 2.
Birbiriyle ilgisi olmayan üç sınıf aynı Fiyatli arayüzünü uyguluyor. Döngü sonunda toplam ve en pahalı ürünün fiyatı nedir?
interface Fiyatli { double fiyat(); }
class Kitap implements Fiyatli {
public double fiyat() { return 40; }
}
class Kalem implements Fiyatli {
int adet;
Kalem(int adet) { this.adet = adet; }
public double fiyat() { return 7.5 * adet; }
}
class Bilet implements Fiyatli {
public double fiyat() { return 25; }
}
public class Main {
public static void main(String[] args) {
Fiyatli[] sepet = { new Kitap(), new Kalem(4),
new Bilet() };
double toplam = 0;
Fiyatli enPahali = sepet[0];
for (Fiyatli f : sepet) {
toplam += f.fiyat();
if (f.fiyat() > enPahali.fiyat()) enPahali = f;
}
System.out.println(toplam + " " + enPahali.fiyat());
}
}95.0 40.0Dizinin öğe tipi bir arayüzdür. Kitap, Kalem ve Bilet'in ortak noktası yalnızca Fiyatli sözleşmesidir. enPahali ilk öğeyle, yani kitapla başlar. Birinci turda f kitabı gösterir: fiyat 40.0, toplam 40.0; 40 > 40 yanlış olduğu için en pahalı değişmez. İkinci turda f kalemi gösterir: 7.5 × 4 = 30.0, toplam 70.0; 30 > 40 yanlış. Üçüncü turda bilet: 25.0, toplam 95.0; 25 > 40 yanlış. Çıktı “95.0 40.0”dır. Aynı f.fiyat() çağrısı her turda başka bir sınıfın metodunu çalıştırmıştır. Yeni bir Fiyatli sınıfı eklemek için döngü değişmez.
ikiKez() arayüzde bir kez yazılmış bir varsayılan metottur. İki nesnede kaç artış yapar? Program ne yazar?
interface Sayac {
int deger();
void arttir();
default boolean sifirMi() { return deger() == 0; }
default void ikiKez() { arttir(); arttir(); }
}
class Basit implements Sayac {
private int d;
public int deger() { return d; }
public void arttir() { d++; }
}
class Ciftli extends Basit {
@Override public void arttir() {
super.arttir(); super.arttir();
}
}
public class Main {
public static void main(String[] args) {
Sayac a = new Basit();
Sayac b = new Ciftli();
a.ikiKez();
b.ikiKez();
System.out.println(a.deger() + " " + b.deger()
+ " " + b.sifirMi());
}
}2 4 falsea bir Basit'tir. a.ikiKez()'de varsayılan gövde arttir()'ı iki kez çağırır; bu çağrı this.arttir()'dir ve Basit'in sürümüne bağlanır: d önce 1, sonra 2 olur. b bir Ciftli'dir. Aynı varsayılan gövde çalışır, ama this bu kez bir Ciftli'dir ve geçersiz kılınmış arttir() her çağrıda super.arttir()'ı iki kez yapar: d 2, sonra 4 olur. Ciftli'nin d alanı Basit'ten gelir; private olduğu için yalnızca Basit'in metotlarıyla değişir. Son olarak sifirMi() de varsayılan bir metottur; deger() 4'tür ve sonuç false'tur. Çıktı “2 4 false”dur. Tek bir varsayılan metot, onu çağıran sınıfın geçersiz kıldığı metotlar sayesinde iki nesnede farklı davranmıştır.
Gerekli içe aktarmalar yapılmıştır. Program ne yazdırır?
import java.util.*;
import java.util.function.*;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
IntBinaryOperator f = (a, b) -> a * b + 1;
IntUnaryOperator g = x -> x * x;
int t = f.applyAsInt(3, 4) + g.applyAsInt(5);
System.out.println(t);
List<Integer> l =
Arrays.asList(5, 8, 3, 12, 7);
System.out.println(l.stream()
.filter(s -> s > 5)
.map(s -> s / 2)
.collect(Collectors.toList()));
}
}38
[4, 6, 3]f.applyAsInt(3, 4) lambdayı a = 3, b = 4 ile çalıştırır: 3 · 4 + 1 = 13. g.applyAsInt(5) 5 · 5 = 25 verir. İki değer de int'tir ve toplamları 38'dir. Akışta filter(s -> s > 5) 5'ten büyük öğeleri, sıralarını koruyarak tutar: 8, 12, 7. map(s -> s / 2) her birini dönüştürür. Bölme tam sayı bölmesidir: 8 / 2 = 4, 12 / 2 = 6, 7 / 2 = 3. Sonuç [4, 6, 3]'tür.
4.5.11.2Alıştırmalar
Aşağıdaki alıştırmalar kod yazmayı gerektirir. Her birinin ardından olası bir çözüm ve sonucu verilmiştir.
Alıştırma 1. Soyut bir Calisan sınıfı: isim alanı, yapıcı, soyut double maas() ve adı ile maaşı yazdıran bilgi(). Alt sınıflar: aylık 30000 alan TamZamanli ve 120 saat × 200 TL alan SaatUcretli. Bir Calisan[] dizisinde bilgi() çağrılır. Aynı programda iki küçük soru daha cevaplanır: soyut double alan() ve "Alan: " + alan() döndüren varsayılan ozet() metodu olan Sekil2B arayüzünde Ucgen(6, 4) için ozet() ne döndürür? interface Hesap { static int topla(int a, int b) … } ve class Basit implements Hesap {} için Hesap.topla(3, 4) ve Basit.topla(3, 4) ne verir?
abstract class Calisan {
private final String isim;
Calisan(String isim) { this.isim = isim; }
abstract double maas();
void bilgi() { System.out.println(isim + ": " + maas()); }
}
class TamZamanli extends Calisan {
TamZamanli(String isim) { super(isim); }
double maas() { return 30000; }
}
class SaatUcretli extends Calisan {
private final int saat;
private final double ucret;
SaatUcretli(String isim, int saat, double ucret) { super(isim); this.saat = saat; this.ucret = ucret; }
double maas() { return saat * ucret; }
}
interface Sekil2B {
double alan();
default String ozet() { return "Alan: " + alan(); }
}
class Ucgen implements Sekil2B {
private final double taban, yukseklik;
Ucgen(double taban, double yukseklik) { this.taban = taban; this.yukseklik = yukseklik; }
public double alan() { return taban * yukseklik / 2; }
}
interface Hesap { static int topla(int a, int b) { return a + b; } }
class Basit implements Hesap { }
public class Main {
public static void main(String[] args) {
Calisan[] calisanlar = { new TamZamanli("Ayşe"), new SaatUcretli("Ali", 120, 200) };
for (Calisan c : calisanlar) c.bilgi();
System.out.println(new Ucgen(6, 4).ozet());
System.out.println(Hesap.topla(3, 4));
}
}Ayşe: 30000.0
Ali: 24000.0
Alan: 12.0
7bilgi() somuttur ve Calisan'da bir kez yazılmıştır; içindeki maas() her nesnede kendi sınıfının formülünü çalıştırır: 30000.0 ve 120 · 200 = 24000.0. Üçgenin alanı 6 · 4 / 2 = 12.0'dır ve varsayılan ozet() “Alan: 12.0” döndürür. Hesap.topla(3, 4) 7 verir. Basit.topla(3, 4) ise derlenmez, çünkü arayüzün static metotları kalıtımla geçmez:
BasitTopla.java:5: error: cannot find symbol
System.out.println(Basit.topla(3, 4));
^
symbol: method topla(int,int)
location: class Basit
1 errorAlıştırma 2. sealed abstract class Sekil permits Kare, Cember yazılır; Kare final, Cember non-sealed'dir. Elips extends Cember ve Ucgen extends Sekil derlenir mi? Aynı programda bir de “hem hayvan hem yüzebilen balık” tasarlanır.
sealed abstract class Sekil permits Kare, Cember { abstract double alan(); }
final class Kare extends Sekil {
double a = 2;
double alan() { return a * a; }
}
non-sealed class Cember extends Sekil {
double r = 1;
double alan() { return 3 * r * r; }
}
class Elips extends Cember { }
abstract class Hayvan { }
interface Yuzebilir { void yuz(); }
class Balik extends Hayvan implements Yuzebilir {
public void yuz() { System.out.println("Balık yüzüyor"); }
}
public class Main {
static String ad(Sekil s) {
return switch (s) {
case Kare k -> "kare";
case Cember c -> "çember";
};
}
public static void main(String[] args) {
Sekil[] dizi = { new Kare(), new Cember(), new Elips() };
for (Sekil s : dizi) System.out.println(ad(s) + " " + s.alan());
Yuzebilir y = new Balik();
y.yuz();
Hayvan h = (Hayvan) y;
System.out.println(h instanceof Yuzebilir);
}
}kare 4.0
çember 3.0
çember 3.0
Balık yüzüyor
trueElips derlenir: Cember non-sealed olduğu için onu herkes genişletebilir. switch bir Elips'i Cember olarak tanır ve “çember” der; alanı da Cember'den gelir. Ucgen ise izin listesinde yoktur:
MuhurSinif.java:4: error: class is not allowed to extend sealed class: Sekil (as it is not listed in its 'permits' clause)
class Ucgen extends Sekil { }
^
1 errorKare final olduğu için onu genişletmek de mümkün değildir; ileti bu kez mühürlemeden değil final'dan söz eder:
MuhurFinalAlt.java:4: error: cannot inherit from final Kare
class BuyukKare extends Kare { }
^
1 errorBalık tasarımında hem akrabalık hem yetenek vardır. Hayvan soyut sınıftır, yüzme yeteneği Yuzebilir arayüzüdür: class Balik extends Hayvan implements Yuzebilir. Aynı nesne Yuzebilir tipinde tutulup Hayvan'a dönüştürülebilir; ikisi de aynı nesnenin farklı tipleridir.
Alıştırma 3. class Kare implements GeometrikSekil içinde public void Ciz() { … } yazılmıştır. Ne olur? Metodun üstüne @Override eklenirse ne değişir?
Ciz ile ciz farklı adlardır; Java büyük ve küçük harfi ayırt eder. Ciz() yeni bir metottur ve ciz() hâlâ eksiktir: Kare is not abstract and does not override abstract method ciz() in GeometrikSekil. @Override eklenince derleyici ikinci bir ileti verir ve hatanın asıl yerini gösterir:
OverrideYok.java:4: error: Kare is not abstract and does not override abstract method ciz() in GeometrikSekil
class Kare implements GeometrikSekil {
^
OverrideYok.java:5: error: method does not override or implement a method from a supertype
@Override
^
2 errors4.5.12Sık yapılan hatalar
Soyut sınıf ve arayüz hatalarının neredeyse hepsini derleyici yakalar. İletiler İngilizcedir ve kalıplaşmıştır; bir kez tanınınca hatanın yeri hemen bulunur. Aşağıdaki kutular en sık görülenleri iletileriyle birlikte verir.
Soyut sınıftan nesne oluşturmak. new Sekil("?") → Sekil is abstract; cannot be instantiated. Nesne somut bir alt sınıftan oluşturulur; değişkenin tipi soyut kalabilir: Sekil s = new Kare(3);. Aynı ileti arayüzden nesne oluşturulmaya çalışıldığında da gelir.
Soyut metodu yazmayı unutmak. Cember is not abstract and does not override abstract method ciz() in Sekil. Ya metot yazılır ya da sınıf abstract yapılır. Arayüz uygulanırken ileti aynıdır; yalnızca son kelime arayüzün adıdır.
Soyut metoda gövde yazmak ya da sınıfı soyut yapmamak. abstract void ciz() { } → abstract methods cannot have a body. Metot ya soyuttur ya da gövdelidir. class Sekil { abstract void ciz(); } → Sekil is not abstract and does not override abstract method ciz() in Sekil.
Çelişen belirteçler. abstract final, private abstract, static abstract → illegal combination of modifiers. Üç belirteç de geçersiz kılmayı engeller; soyut metot ve soyut sınıf ise alt sınıf için vardır.
İki sınıftan türetmek. class C extends A, B → '{' expected. Bir sınıfın tek extends'i, çok sayıda implements'i olabilir. İletinin hatayı açıklamaması, onu tanımayı zorlaştırır.
Referansın tipinde olmayan metodu çağırmak. Ucabilir u = new Ordek(); u.yuz(); → cannot find symbol. Hangi metotların çağrılabileceğini nesne değil, referansın tipi belirler. Metot, onu bilen tipte bir referansla çağrılır (Yuzebilir ya da Ordek).
Çakışan varsayılan metotları çözmemek. class Gorevli inherits unrelated defaults for selam() from types Selamlayan and Karsilayan. Metot sınıfta geçersiz kılınır; istenen arayüzün sürümü ArayuzAdi.super.selam() ile çağrılabilir.
Mühürleme kurallarını unutmak. İzinsiz alt tip: class is not allowed to extend sealed class: Hayvan. İzinli alt tipte belirteç yok: sealed, non-sealed or final modifiers expected.
Lambdanın kullandığı yerel değişkeni değiştirmek. local variables referenced from a lambda expression must be final or effectively final. Lambda yerel değişkenin değerinin bir kopyasını taşır; değişken sonradan değiştirilemez. Değişmesi gereken bir değer alanda tutulur.
Jenerik tiplerin alt tip ilişkisini devraldığını sanmak. List<Number> x = tamlar; (tamlar bir List<Integer>) → incompatible types: List<Integer> cannot be converted to List<Number>. Okuyan metot parametresini List<? extends Number>, yazan metot List<? super Integer> olarak yazar.
4.5.13Özet
- Soyutlama, ayrıntıyı gizleyip “ne yapılır”ı tanımlamak, “nasıl”ı alt tiplere bırakmaktır. Java'da iki aracı vardır: soyut sınıf ve arayüz.
- Soyut sınıf
abstract classile tanımlanır; nesnesi oluşturulamaz. Alan, yapıcı, soyut ve somut metot içerebilir. Yapıcısı, alt sınıf nesnesinin kendisinden gelen kısmını kurar vesuper(...)ile çağrılır. - Soyut metot gövdesizdir,
;ile biter. Somut alt sınıf onu uygulamak zorundadır.private,staticvefinalile birlikte yazılamaz. - Şablon metot, algoritmanın iskeletini üst sınıfta (
finalbir metotta) tanımlar; değişen adımlar alt sınıflarda yazılan soyut metotlardır. - Arayüz bir sözleşmedir. Metotları örtük
public abstract, alanlarıpublic static final'dır; örnek alanı ve yapıcısı yoktur. Bir sınıf tekextends, çok sayıdaimplementsyazabilir; uygulayan metotpublicolmalıdır. - Hangi metotların çağrılabileceğini referansın tipi, hangi gövdenin çalışacağını nesnenin sınıfı belirler. Arayüz tipiyle programlamak gevşek bağlılık sağlar.
- Lambda ifadesi
(parametreler) -> ifadebiçiminde yazılan adsız fonksiyondur ve bir fonksiyonel arayüze (tek soyut metotlu arayüz) atanır (Java 8+). Tanımlandığında değil, arayüzün metodu çağrıldığında çalışır. Kullandığı yerel değişkenler fiilen final olmalıdır. - Arayüzde gövdeli metotlar: varsayılan (Java 8, kalıtımla gelir),
static(Java 8, yalnızca arayüz adıyla çağrılır) veprivate(Java 9, yalnızca arayüz içinde). Çakışan varsayılan metotları sınıf geçersiz kılar; üst sınıftaki metot varsayılan metoda üstün gelir. - Mühürlü tip (
sealed, Java 17+) alt tiplerinipermitsile sınırlar; her alt tipfinal,sealedya danon-sealed'dir. Kapalı hiyerarşideswitchdefaultdalı olmadan her durumu kapsayabilir. - Soyut sınıf yakın akraba sınıfları ortak durum ve kodla gruplar (“bir türüdür”); arayüz ilgisiz sınıflara yetenek kazandırır (“yapabilir”). Çoğu zaman ikisi birlikte kullanılır.
- Jenerikler sınıf ve metotları tip parametresiyle yazar (
Kutu<T>,<T extends Ust>). Tip hataları derlemede yakalanır. Jenerik tipler değişmezdir; esneklik joker tiplerle (? extends,? super) sağlanır. Tip bilgisi derlemeden sonra silinir. - Etiketler
@Overridegibi üst verilerdir;RUNTIMEetiketleri yansımayla okunur. - Tasarım desenleri denenmiş çözüm kalıplarıdır: Tekil (tek nesne), Fabrika (oluşturmayı toplamak), Gözlemci (değişikliği kayıtlı nesnelere bildirmek), Strateji (değiştirilebilir algoritma) ve MVC (veri, gösterim ve denetimi ayırmak). Desen ancak gerçek bir sorunu çözüyorsa kullanılır.
4.5.14Kavram listesi
Terimlerin İngilizce karşılıkları ve öteki bölümlerdeki kullanımları terim sözlüğünde bulunur.
- soyutlama abstraction
- soyut sınıf abstract class
- soyut metot abstract method
- somut sınıf concrete class
- şablon metot template method
- yapıcı zinciri constructor chaining
- arayüz interface
- uygulamak implement
- çoklu kalıtım multiple inheritance
- dinamik bağlama dynamic binding
- bildirilen tip / gerçek tip declared type / actual type
- gevşek bağlılık loose coupling
- lambda ifadesi lambda expression
- fonksiyonel arayüz functional interface
- anonim sınıf anonymous class
- fiilen final effectively final
- varsayılan metot default method
- mühürlü sınıf / arayüz sealed class / interface
- jenerikler generics
- sınırlı tip parametresi bounded type parameter
- joker tip wildcard
- tip silme type erasure
- etiket annotation
- yansıma reflection
- tasarım deseni design pattern
- fabrika metodu factory method
- bağımlılık enjeksiyonu dependency injection
- MVC Model–View–Controller
4.5.15Kendini Dene
Soyut
Belgesınıfının yapıcısı"B ", alt sınıflarMektupveKitap'ın yapıcıları sırasıyla"M "ve"K "yazdırıyor.Belge'ninfinal String etiket() { return ad + "(" + sayfa() + ")"; }metodu soyutsayfa()'yı çağırıyor;Mektup1,Kitap(int bolum)bolum * 20döndürüyor.Belge[] b = { new Mektup(), new Kitap(3) };satırı ne yazdırır? Ardından her öğeninetiket()'i nedir?Cevap
Her nesnede önce soyut üst sınıfın yapıcısı, sonra alt sınıfınki çalışır:
B M B K. Ayrı birBelgenesnesi oluşmaz.etiket()Belge'de bir kez yazılmıştır amasayfa()her nesnede kendi sınıfına bağlanır:mektup(1)ve 3 · 20 = 60 ilekitap(60).Sayiciarayüzünde soyutint say()vedefault int ikiKati() { return 2 * say(); };Olcerarayüzündedefault int ikiKati() { return 100; }var.Ikili implements Sayici, Olcersınıfısay()'da 7 döndürüyor veikiKati()'yiSayici.super.ikiKati() + Olcer.super.ikiKati()olarak yazıyor.new Ikili().ikiKati()kaçtır?IkiliikiKati()'yi yazmasaydı ne olurdu?Cevap
Sayici'nin varsayılan gövdesithis.say()'i çağırır ve 2 · 7 = 14 bulur;Olcer'inki 100 döndürür. Sonuç 114'tür. Metot yazılmasaydı iki arayüz aynı imzalı iki varsayılan gövde getirdiği için sınıf derlenmezdi:inherits unrelated defaults for ikiKati().Function<Integer, Integer> f = x -> x + 1;veFunction<Integer, Integer> g = x -> x * 3;tanımlarından sonraf.apply(g.apply(4))veg.apply(f.apply(4))kaçtır? Tanım satırları çalıştığında lambdalardan herhangi biri çalışmış olur mu?Cevap
İçteki çağrı önce çalışır.
g.apply(4) = 12,f.apply(12) = 13.f.apply(4) = 5,g.apply(5) = 15. Sonuç13 15'tir. Tanım satırları yalnızca iki arayüz nesnesi oluşturur; gövdeler ancakapplyçağrıldığında çalışır.List<String> l = new ArrayList<>(List.of("ayva", "elma", "kivi", "nar", "armut"));listesinel.removeIf(s -> s.length() == 4);uygulanıyor. Liste ne olur?removeIf'in parametresi hangi fonksiyonel arayüzdür?Cevap
Dört harfli “ayva”, “elma” ve “kivi” silinir; kalan liste
[nar, armut]'tur ve kalanların sırası korunur.removeIfbirPredicatealır: her öğe içinbooleandöndürentestmetodu.removeIf, Java 8'deCollectionarayüzüne varsayılan metot olarak eklenmiştir.Bu bölümdeki
enBuyukjenerik metoduInteger[] d = {3, 9, 4}veString[] a = {"Zeytin", "armut", "Elma"}ile çağrılıyor. Sonuçlar nedir? İkinci sonuç neden ilk bakışta şaşırtıcıdır?Cevap
Sayılarda 9 bulunur. Dizgilerde
compareToharf kodlarını karşılaştırır ve büyük harflerin kodları (Z = 90, E = 69) küçük harflerinkinden (a = 97) küçüktür. Bu yüzden sözlük sırasında en sonda gelen, yani “en büyük” dizgiarmut'tur. Sonuç9 armut'tur.Mühürlü
Hayvanarayüzü yalnızcaKediveKopek'e izin veriyor veturmetodundakiswitch'in iki dalı var,defaultdalı yok. Bir günpermitslistesineKuseklenipfinal class Kus implements Hayvanyazılıyor. Ne olur?switch'te baştan birdefaultdalı olsaydı ne olurdu?Cevap
turmetodu derlenmez hâle gelir:the switch expression does not cover all possible input values. Derleyici, yeni tipin ele alınmadığı her yeri gösterir.defaultdalı olsaydı program derlenir veKussessizcedefault'a düşerdi; eksik dal ancak çalışırken, belki hiç fark edilmezdi.Makinesınıfındapublic void calis()“Makine çalışıyor” yazıyor.Yaziciarayüzündedefault void calis()“Yazdırılıyor” yazıyor.class Fotokopi extends Makine implements Yazici { }içinYazici y = new Fotokopi(); y.calis();ne yazdırır? Sınıf neden derlenir?Cevap
“Makine çalışıyor” yazılır. Bir sınıf aynı imzalı metodu hem üst sınıftan hem bir arayüzün varsayılan gövdesinden alıyorsa sınıf hiyerarşisindeki metot kazanır. Bu durumda çakışma yoktur, bu yüzden sınıf derlenir. Referansın
Yazicitipinde olması sonucu değiştirmez.Predicate<Integer> p = x -> { sayac.add(x); return x > 2; };(sayacboş birArrayList<Integer>) tanımlanıyor.List.of(1, 5, 2, 8)üzerinde dolaşan bir döngüp.test(x)doğruysan'yi artırıyor. Döngüden sonranvesayacnedir?sayacbirintyerel değişkeni olsaydı ve lambda onusayac++ile artırsaydı ne olurdu?Cevap
Lambda her
testçağrısında bir kez çalışır ve gördüğü sayıyı listeye ekler. 5 ve 8 koşulu sağlar:n = 2,sayac = [1, 5, 2, 8]. Listeye öğe eklemeksayacdeğişkenine yeni bir değer atamak değildir; değişken hep aynı listeyi gösterir ve fiilen final kalır. Birintyerel değişkeninisayac++ile artırmak ise ona yeni değer atamaktır ve derlenmez:local variables referenced from a lambda expression must be final or effectively final.
4.5.16İleri okuma
- Oracle, The Java Tutorials, “Interfaces and Inheritance”, “Lambda Expressions” ve “Generics” dersleri: soyut sınıf, arayüz, varsayılan metot, lambda ve jenerikleri kısa, çalıştırılabilir örneklerle işler; çevrim içi ve ücretsizdir.
- J. Bloch, Effective Java (3. baskı, Addison-Wesley, 2018): 20. madde (“Prefer interfaces to abstract classes”) arayüz ile soyut sınıf seçimini, 31. madde joker tiplerin PECS kuralını, 42. madde ise lambdaları anonim sınıflara tercih etmenin gerekçelerini tartışır.
- E. Freeman ve E. Robson, Head First Design Patterns (2. baskı, O'Reilly, 2020): Strateji, Gözlemci, Fabrika ve Tekil desenlerini Java örnekleri ve bol çizimle, yeni başlayanlara göre anlatır.
- E. Gamma, R. Helm, R. Johnson ve J. Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994): tasarım desenleri sözlüğünü kuran temel kitap; her desenin amacını, yapısını ve ödünleşimlerini tanımlar.
4.5.17Kaynaklar
- Oracle, The Java Tutorials: “Abstract Methods and Classes”, “Interfaces”, “Default Methods”, “Lambda Expressions”, “Generics”, “Annotations”.
- J. Gosling ve diğ., The Java Language Specification, Java SE 21 Edition (8.1.1.1 abstract Classes, 8.1.1.2 sealed, non-sealed, and final Classes, Bölüm 9: Interfaces, 15.27 Lambda Expressions).
- Oracle, JEP 409: Sealed Classes (Java 17) ve JEP 441: Pattern Matching for switch (Java 21).
- P. Deitel ve H. Deitel, Java: How to Program, 11. baskı, Pearson (Bölüm 10: Object-Oriented Programming: Polymorphism and Interfaces).
- K. Sierra ve B. Bates, Head First Java, 3. baskı, O'Reilly (Bölüm 8: Interfaces and Abstract Classes).
- C. S. Horstmann, Core Java, Volume I: Fundamentals, 12. baskı, Pearson (Bölüm 5–6).
- E. Gamma, R. Helm, R. Johnson ve J. Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1994.
- J. Bloch, Effective Java, 3. baskı, Addison-Wesley, 2018 (20., 31. ve 42. maddeler).
- E. Freeman ve E. Robson, Head First Design Patterns, 2. baskı, O'Reilly, 2020.