Java ile Nesne Yönelimli Programlama · Ders Notu
Bölüm 4.2: Sarmalama
Bir sınıf yazmak, veriyi ve o veriyle çalışan metotları bir araya getirmekle başlar. Ama bu birliktelik tek başına yetmez. Alanlar herkese açıksa her kullanıcı nesneyi istediği gibi değiştirir; saniyesi 75 olan bir saat ya da bakiyesi eksiye düşmüş bir hesap ortaya çıkar. Sarmalama, nesnenin verisini dışarıya kapatıp ona yalnızca sınıfın kendi metotlarıyla ulaşılmasını sağlar. Böylece kurallar tek bir yerde, sınıfın içinde uygulanır. Not bu fikri adım adım kurar. İlk kesimler bilgi saklamayı, “ne / nasıl” ayrımını ve metodun ön ve son koşullarından oluşan sözleşmeyi anlatır. Ardından erişim belirleyiciler, erişimci ve değiştirici metotlar ve bir banka hesabı sınıfının ayrıntılı izlemesi gelir. Not daha sonra arayüz ile gerçeklemeyi ayırır, dışarı sızan referansları ve değiştirilemez sınıfları inceler. Son kesim sınıfları paketlerde gruplamayı ve import bildirimini 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ı
- Sarmalamayı açıklamak
- Veriyi ve onu işleyen metotları tek sınıfta toplamayı, iç ayrıntıları gizlemeyi (bilgi saklama) ve “ne / nasıl” ayrımını örnekle anlatmak.
- Metot sözleşmesi yazmak
- Bir metodun ön koşulunu ve son koşulunu Javadoc yorumuyla belirtmek; ön koşul bozulduğunda ne yapılacağını seçmek ve kodda uygulamak.
- Erişimi doğru seçmek
public,protected, paket erişimi veprivatedüzeylerinin kimlere açık olduğunu söylemek; derleyicinin verdiği erişim hatalarını okumak.- Erişimci ve değiştirici yazmak
- Alanları
privateyapıp doğrulama yapan erişim metotları yazmak;this.alan = alankalıbını ve gölgeleme tuzağını bilmek. - Arayüz ile gerçeklemeyi ayırmak
- Sınıfın dış yüzünü değiştirmeden iç yapısını değiştirmek;
interfaceveimplementsile tanışmak; sızan referansı savunmacı kopya ile kapatmak ve değiştirilemez sınıf tasarlamak. - Paket kullanmak
packageveimportile sınıfları gruplamak, paketli bir programı komut satırında derleyip çalıştırmak, sınıf adı çakışmalarını çözmek.
4.2.1Sarmalama ve bilgi saklama
Bir sınıfın alanları ve metotları tanımlandıktan sonra ilk tasarım sorusu şudur: bu alanlara kim dokunabilir? Sınıfın her alanı her yerden okunup yazılabiliyorsa, sınıfın kendi kuralları kâğıt üzerinde kalır. Bir saatin saniyesi 0 ile 59 arasında olmalıdır. Kuralı bilen sınıftır, ama alanı değiştiren her satır onu ayrı ayrı hatırlamak zorunda kalır. Programda yüzlerce satır varsa birinin unutması kaçınılmazdır. Sarmalama bu sorunu kökünden çözer: kuralı uygulama işini sınıfın kendisine bırakır.
4.2.1.1Sarmalama nedir?
Sarmalama (encapsulation), bir nesnenin verisini (alanlarını) ve bu veriyi işleyen metotları tek bir birimde, sınıfta toplamaktır. Veri dışarıya kapatılır (private); dışarıdan yalnızca sınıfın sunduğu metotlar çağrılabilir.
Tanımda iki ayrı fikir vardır. Birincisi bir araya getirmektir: veri ile onu işleyen kod aynı sınıfta durur. İkincisi kapatmaktır: veri dışarıdan doğrudan görülmez, değiştirilmez. İkinci fikir olmadan birincisi yarım kalır. Alanları public olan bir sınıf da veriyle metotları bir arada tutar, ama veriyi korumaz.
Sarmalamayı ilaç kapsülüne benzetmek mümkündür. Kapsülün içindeki toz görünmez; hasta yalnızca kapsülü yutar ve tozun nasıl hazırlandığıyla ilgilenmez. Sınıf da kendi verisinin bekçisidir. Saniyeyi 75 yapmak isteyen bir çağrı, sınıfın kapısından geçemez. Şekil 4.2.1 bu düzeni bir DijitalSaat nesnesi üzerinde gösterir.
DijitalSaat nesnesi. Özel veri (saat, dakika, saniye) nesnenin içindedir ve dışarıya kapalıdır. Başka bir sınıf yalnızca getSaniye(), setSaniye(int) ve goster() gibi açık metotları çağırabilir. Alana doğrudan yazma denemesi (s.saniye = 75;) derleme hatasıyla durdurulur.Şekildeki kesik kırmızı ok, sarmalamanın en somut kazancını gösterir. Alana doğrudan yazmak bir çalışma hatası değil, bir derleme hatasıdır. Program yanlış değeri hiçbir zaman göremez, çünkü böyle bir program derlenmez. Geçersiz değeri durdurma işi çalışma anına bırakılmaz; derleyici bunu kod yazılırken yakalar.
Sarmalama, nesne yönelimli programlamanın dört temel ilkesinden biridir. Ötekiler kalıtım, çok biçimlilik ve soyutlamadır. Dördü birbirini tamamlar: kalıtım ve çok biçimlilik sınıflar arasındaki ilişkiyi düzenler; sarmalama ve soyutlama ise tek bir sınıfın dışarıya nasıl göründüğünü belirler.
4.2.1.2Bilgi saklama: ne ve nasıl
Bilgi saklama (information hiding), bir sınıfın iç yapısını onu kullananlardan gizleme ilkesidir. Tasarım, bir işin ne yapıldığını nasıl yapıldığından ayırır. Kullanıcı yalnızca metodun ne işe yaradığını bilir; iç ayrıntıları bilmeden de metodu doğru kullanabilir.
Bilgi saklama, soyutlamayla (abstraction) iç içe bir kavramdır ve bazı kaynaklar ikisini aynı adla anar. Soyutlama ayrıntıyı bir kenara bırakıp yalnızca önemli özellikleri göstermektir. Bilgi saklama ise bu ayrıntıların kullanıcıya gerçekten ulaşmamasını sağlar. Biri bakış açısıdır, öteki o bakış açısını koruyan duvardır.
Günlük hayattaki en bilinen örnek arabadır. Sürücü direksiyonu, gaz pedalını ve freni görür ve kullanır. Direksiyon kutusunun, yakıt enjeksiyonunun ya da fren hidroliğinin nasıl çalıştığını bilmesi gerekmez. Üretici motoru değiştirse bile sürücü arabayı aynı biçimde kullanmaya devam eder. Şekil 4.2.2 bu ayrımı iki katman olarak çizer.
Aynı fikir Java'da da her gün kullanılır. System.out.println metodu sürekli çağrılır, ama karakterlerin ekrana nasıl gönderildiğini bilmek gerekmez. İşletim sistemiyle konuşan kod, karakter kodlaması, tampon bellek gibi ayrıntılar println'ın arkasında saklıdır. Kullanıcının bildiği tek şey, metodun ne yaptığıdır: verilen değeri yazar ve satır sonuna geçer.
4.2.1.3Tek çağrının arkası
Bilgi saklamayı kodda görmek için bir veri tabanına kayıt ekleyen küçük bir sınıf ele alalım. VeriIslemleri sınıfı, bağlantıyı açma, veriyi yazma ve bağlantıyı kapatma adımlarını tek bir metodun içine toplar:
public class VeriIslemleri {
private VeritabaniBaglantisi baglanti;
// metodun iç yapısı bilinmeden de kullanılabilir.
public void veriEkle(String veri) {
baglanti = new VeritabaniBaglantisi();
baglanti.baglan();
baglanti.veriEkle(veri);
baglanti.kapat();
}
}public class Ana {
public static void main(String[] args) {
VeriIslemleri islem = new VeriIslemleri();
islem.veriEkle("Ayşe");
}
}Bu programı derlemek için VeritabaniBaglantisi sınıfı da gerekir. Gerçek bir uygulamada bu sınıf bir veri tabanı sürücüsüyle konuşurdu. Notun örneğinde yalnızca ekrana yazan basit bir sınıftır:
public class VeritabaniBaglantisi {
public void baglan() { System.out.println("Bağlantı açıldı"); }
public void veriEkle(String veri) { System.out.println("Eklendi: " + veri); }
public void kapat() { System.out.println("Bağlantı kapatıldı"); }
}Bağlantı açıldı
Eklendi: Ayşe
Bağlantı kapatıldıAna sınıfının yaptığı tek şey islem.veriEkle("Ayşe") çağrısıdır. Bu satır ne istendiğini söyler: “Ayşe kaydını ekle.” Bağlantının açılması, verinin yazılması ve bağlantının kapatılması, yani işin nasıl yapıldığı, VeriIslemleri'nin içinde gizlidir. baglanti alanı private olduğu için Ana onu göremez de değiştiremez de. Yarın VeritabaniBaglantisi yerine gerçek bir sürücü kullanılsa, Ana'nın tek bir satırı bile değişmez.
Çağrının arkasında neler olduğu Tablo 4.2.1'de adım adım verilmiştir. Satır numaraları, iki sınıfın art arda yazıldığı izleme kodunun düzenine göredir: 1–10. satırlar VeriIslemleri, 11–16. satırlar Ana sınıfıdır.
| Adım | Satır | Çağrı yığını | Değişkenler | Yeni çıktı |
|---|---|---|---|---|
| 1 | 13: VeriIslemleri islem = new VeriIslemleri(); | main() | islem=→VeriIslemleri | — |
| 2 | 4: public void veriEkle(String veri) { | main() → islem.veriEkle("Ayşe") | veri="Ayşe" | — |
| 3 | 5: baglanti = new VeritabaniBaglantisi(); | main() → islem.veriEkle("Ayşe") | veri="Ayşe", baglanti=→nesne | — |
| 4 | 6: baglanti.baglan(); | main() → islem.veriEkle("Ayşe") → baglanti.baglan() | veri="Ayşe" | Bağlantı açıldı |
| 5 | 7: baglanti.veriEkle(veri); | main() → islem.veriEkle("Ayşe") → baglanti.veriEkle(veri) | veri="Ayşe" | Eklendi: Ayşe |
| 6 | 8: baglanti.kapat(); | main() → islem.veriEkle("Ayşe") → baglanti.kapat() | veri="Ayşe" | Bağlantı kapatıldı |
| 7 | 15: } | main() | islem=→VeriIslemleri | — |
İlk adımda new VeriIslemleri() heap'te bir nesne oluşturur. islem değişkeni bu nesneyi gösteren bir referanstır. Nesnenin baglanti alanı henüz null'dır, çünkü referans tipindeki alanların varsayılan değeri null'dır. İkinci adımda veriEkle çağrılır; yığına yeni bir çerçeve eklenir ve veri parametresi "Ayşe" değerini alır.
Üçüncü adımda gizli iş başlar. İkinci bir nesne oluşur ve private baglanti alanı onu gösterir. Şekil 4.2.3 bu anı ve çağrının sonunu yan yana verir. Dördüncü adımda baglan() çağrılır; yığında artık üç çerçeve vardır: main, veriEkle ve baglan. Ana bu çerçevelerin hiçbirinden haberdar değildir. Beşinci adımda veri, bağlantı nesnesinin veriEkle metoduna gider. İki metodun adı aynıdır, ama farklı sınıflara aittir; Java hangisinin çağrıldığını nesnenin tipinden bilir. Altıncı adımda bağlantı kapatılır.
veriEkle çalışıyor; VeriIslemleri nesnesinin özel baglanti alanı yeni oluşturulan VeritabaniBaglantisi nesnesini gösteriyor. Sağda çağrı bitmiş, yığında yalnızca main kalmış; bağlantı nesnesi kullanıcının hiç görmediği bir yerde duruyor.Yedinci adımda akış main'e döner. Kullanıcı tek satır yazdı ve üç ayrı işlemin hiçbirini bilmedi. Bilgi saklamanın anlamı tam olarak budur. Bu düzenin bir maliyeti yoktur: çağrı yığınına eklenen her çerçeve sabit zamanda açılıp kapanır. Gizlenen iş, gizlenmese de yapılacak işin aynısıdır.
Gizli ayrıntıyı kullanan kodu dışarıda yazmak. Ana içinde bağlantıyı kendisi açıp kapatan bir programcı, VeriIslemleri'nin işini ikinci kez yapmış olur. Bağlantı biçimi değiştiğinde iki yerin birden düzeltilmesi gerekir. Bir sınıfın bir işi tek çağrıyla sunduğu yerde, o işin adımları dışarıda tekrar yazılmaz.
4.2.2Ön ve son koşullar
Bilgi saklama, kullanıcının metodun içini bilmemesini ister. Ama kullanıcı bir şeyi mutlaka bilmelidir: metot hangi durumda doğru çalışır ve çalıştığında ne garanti eder? Bu bilgi kodun içinde değil, metodun dış yüzündedir. Bu kesim, metodun kullanıcıyla yaptığı anlaşmayı ve bu anlaşmanın kodda nasıl uygulandığını anlatır.
4.2.2.1Metodun sözleşmesi
Ön koşul (precondition), metot çağrılmadan önce doğru olduğu varsayılan koşuldur; onu sağlamak çağıranın sorumluluğudur. Son koşul (postcondition), ön koşul sağlandıysa metot bittikten sonra doğru olacağı garanti edilen durumdur; onu sağlamak metodun sorumluluğudur. İkisi birlikte metodun sözleşmesini (contract) oluşturur.
Sözleşme iki taraflıdır. Çağıran “Boş olmayan bir veri veriyorum” der, metot da “Ben de onu ekleyip true döndürüyorum” diye cevap verir. Kullanıcı yalnızca sözleşmeyi bilir, gövdeyi bilmez. Ön koşul bozulursa metot hiçbir şey garanti etmek zorunda değildir. Bu, sorumluluğun açıkça paylaşılması demektir: hata çağıranın verdiği değerdeyse çağıran düzeltir, metot doğru değerle yanlış sonuç veriyorsa metot düzeltilir.
Ön ve son koşul ilk adımda yorum satırı olarak yazılabilir. Sunumdaki iki küçük parça, aynı veriEkle metodunun önce ön koşulunu, sonra son koşulunu belirtir:
public class VeriIslemleri {
// Ön koşul: veri != null
public void veriEkle(String veri) {
// verinin null olup olmadığını kontrol et
// ...
}
}public class VeriIslemleri {
// Son koşul: veri tabanına veri eklendiyse true döner
public boolean veriEkle(String veri) {
// veriyi ekleyip eklemediğini kontrol et
// ...
return true;
}
}İkinci parçada metodun dönüş tipi de değişmiştir: void yerine boolean. Son koşul “eklendiyse true döner” dediği için metodun bir değer döndürmesi gerekir. Sözleşmeyi yazmak, metodun imzası üzerinde de düşünmeyi zorunlu kılar.
4.2.2.2Sözleşmeyi kodda uygulamak
Yorum satırı tek başına hiçbir şeyi denetlemez. Aşağıdaki sürüm sözleşmeyi hem yorumda yazar hem de gövdenin ilk satırında uygular:
public class VeriIslemleri {
private int kayitSayisi = 0;
/**
* Veri tabanına bir kayıt ekler.
* Ön koşul: veri null değil ve boş değil.
* Son koşul: eklendiyse true döner,
* kayitSayisi 1 artar.
*/
public boolean veriEkle(String veri) {
if (veri == null || veri.isEmpty()) {
return false; // ön koşul bozuk
}
// ... veriyi veri tabanına ekle ...
kayitSayisi++;
return true; // son koşul sağlandı
}
public int getKayitSayisi() {
return kayitSayisi;
}
}Metodun başındaki /** ... */ bir Javadoc yorumudur: metodun nasıl kullanılacağını anlatır ve araçlarla belgeye dönüştürülebilir. Gövdenin ilk işi ön koşulu denetlemektir. veri null ya da boşsa metot nesneye dokunmadan false döndürür. Ön koşul sağlanmışsa kayıt eklenir, kayitSayisi bir artar ve true döner; son koşul böylece yerine getirilir. Sunumdaki ilk main metodu üç çağrı yapar:
// main içinde:
VeriIslemleri islem = new VeriIslemleri();
System.out.println(islem.veriEkle("Ali"));
System.out.println(islem.veriEkle(""));
System.out.println(islem.veriEkle(null));
System.out.println(islem.getKayitSayisi());true
false
false
1Dört çağrının izlemesi Tablo 4.2.2'de verilmiştir. Bu kez sırayla "Ayşe", null, "" ve "Can" eklenmeye çalışılır. Satır numaraları izleme kodunun düzenine göredir: 1–7. satırlar veriEkle, 9–14. satırlar main içindedir.
| Adım | Satır | Değişkenler | Yeni çıktı |
|---|---|---|---|
| 1 | 9: VeriIslemleri islem = new VeriIslemleri(); | islem=→#1, kayitSayisi=0 | — |
| 2 | 10: System.out.println(islem.veriEkle("Ayşe"));2: if (veri == null || veri.isEmpty()) { | veri="Ayşe", kayitSayisi=0 | — |
| 3 | 5: kayitSayisi++;6: return true; // son koşul sağlandı | veri="Ayşe", kayitSayisi=1 | true |
| 4 | 11: System.out.println(islem.veriEkle(null));2: if (veri == null || veri.isEmpty()) { | veri=null, kayitSayisi=1 | — |
| 5 | 3: return false; // ön koşul bozuk | veri=null, kayitSayisi=1 | false |
| 6 | 12: System.out.println(islem.veriEkle(""));2: if (veri == null || veri.isEmpty()) {3: return false; // ön koşul bozuk | veri="", kayitSayisi=1 | false |
| 7 | 13: System.out.println(islem.veriEkle("Can"));5: kayitSayisi++;6: return true; // son koşul sağlandı | veri="Can", kayitSayisi=2 | true |
| 8 | 14: System.out.println(islem.getKayitSayisi()); | kayitSayisi=2 | 2 |
İkinci adımda "Ayşe" ne null ne de boştur. || işlecinin iki tarafı da yanlış çıkar, if bloğu atlanır. Üçüncü adımda sayaç 1 olur ve true döner. Dördüncü adım en öğretici olanıdır: veri == null doğrudur ve || kısa devre yapar. Sol taraf doğru olduğu için sağ taraf hiç değerlendirilmez; null.isEmpty() çağrısı yapılmaz ve NullPointerException oluşmaz. Şekil 4.2.4 bu durumu boş dizgi durumuyla yan yana gösterir.
veri null'dır; veri == null doğru çıkar ve veri.isEmpty() hiç değerlendirilmez. Sağda veri boş dizgidir; ilk koşul yanlış, ikinci koşul doğrudur. İki durumda da kayitSayisi 1'de kalır.Koşulların sırası bu yüzden önemlidir. veri.isEmpty() || veri == null biçiminde yazılsaydı, null argümanında önce isEmpty() çağrılır ve program NullPointerException ile dururdu. JDK 21 ile denendiğinde gerçekten de böyle olur. Önce null denetimi, sonra nesne üzerindeki çağrı: bu sıra Java'da her zaman korunur.
Altıncı adımda boş dizgi gelir. "" bir nesnedir, null değildir; ama uzunluğu 0 olduğu için isEmpty() doğru döndürür. Yedinci adımda "Can" eklenir ve sayaç 2 olur. Dört çağrıdan yalnızca sözleşmeye uyan ikisi kayıt ekledi. Nesnenin durumu hiçbir çağrıda bozulmadı.
Bir ayrıntı daha: " " (tek boşluk) isEmpty() sınamasından geçer, çünkü uzunluğu 1'dir. Aynı nesneye veriEkle(" ") gönderildiğinde metot true döndürür ve kaydı sayar. Boşluklardan oluşan veriyi de reddetmek istenirse koşul veri.trim().isEmpty() ya da Java 11'den beri veri.isBlank() ile yazılır. Ön koşulun tam olarak ne dediği, kodda neyin denetlendiğini belirler.
Ön koşul denetiminin maliyeti küçüktür: iki karşılaştırma ve en çok bir metot çağrısı, yani sabit zaman. Buna karşılık kazancı büyüktür; geçersiz bir kayıt nesneye hiç giremez.
4.2.2.3Ön koşul bozulunca
Ön koşul bozulduğunda metodun ne yapacağı da bir tasarım kararıdır. Tablo 4.2.3 üç yaygın yaklaşımı karşılaştırır.
| Yaklaşım | Örnek | Çağıran ne fark eder? |
|---|---|---|
| Sessizce yok saymak | setSaniye(75) hiçbir şey yapmaz | Hiçbir şey; hata gözden kaçabilir. |
| Durum döndürmek | veriEkle(null) → false | Dönüş değerine bakarsa fark eder. |
| İstisna fırlatmak | throw new IllegalArgumentException(...) | Yakalamazsa program durur; hata kaybolmaz. |
İlk yaklaşım en basit olanıdır, ama hatayı gizler. Çağıran 75 yazdığını sanır, oysa saat değişmemiştir. İkinci yaklaşım hatayı bildirir, fakat çağıranın dönüş değerine bakmasına bağlıdır. Dönüş değerini yok sayan bir çağrı, ilk yaklaşımdaki kadar sessizdir. Üçüncü yaklaşım hatayı görmezden gelinemez hâle getirir. Aşağıdaki setSaniye, geçersiz değerde IllegalArgumentException fırlatır:
public void setSaniye(int saniye) {
if (saniye < 0 || saniye > 59)
throw new IllegalArgumentException(
"saniye 0-59 olmalı: " + saniye);
this.saniye = saniye;
}
// main içinde:
DijitalSaat s = new DijitalSaat();
try {
s.setSaniye(30);
s.setSaniye(75);
System.out.println("buraya gelinmez");
} catch (IllegalArgumentException e) {
System.out.println("Hata: " + e.getMessage());
}
System.out.println(s.getSaniye());Hata: saniye 0-59 olmalı: 75
30s.setSaniye(30) başarılıdır ve saniye 30 olur. s.setSaniye(75) çağrısında koşul doğru çıkar ve throw deyimi istisnayı fırlatır. Atama satırı çalışmaz; metot o noktada biter. Akış try bloğunun geri kalanını atlar, bu yüzden "buraya gelinmez" yazılmaz. Kontrol catch bloğuna geçer ve istisnanın iletisi yazılır. Son satırda saniye hâlâ 30'dur; nesnenin durumu bozulmamıştır.
İstisna (exception), program çalışırken normal akışı bozan ve nesne olarak fırlatılan olaydır. throw deyimi bir istisna nesnesi fırlatır; try bloğunda fırlatılan istisna, tipine uyan catch bloğunda yakalanır.
İstisna yakalanmazsa ne olur? Aynı setSaniye(75) çağrısı try dışında yapıldığında JDK 21 şu çıktıyı verir:
Exception in thread "main" java.lang.IllegalArgumentException: saniye 0-59 olmalı: 75
at DijitalSaat.setSaniye(DijitalSaat.java:6)
at Yakalanmaz.main(Yakalanmaz.java:4)İletinin ilk satırı istisnanın tipini ve iletisini, sonraki satırlar istisnanın nerede doğduğunu gösterir. DijitalSaat.java:6 throw deyiminin bulunduğu satırdır; Yakalanmaz.java:4 ise hatalı çağrının yapıldığı satırdır. İstisnanın güçlü yanı budur: hatalı değer, çağrının yapıldığı yerde görünür hâle gelir.
public metotlarda argüman hatası için Java'nın alışılmış seçimi IllegalArgumentException'dır. Java kütüphanesi de aynı yolu izler. Örneğin Integer.parseInt("abc") bir NumberFormatException fırlatır ve bu sınıf IllegalArgumentException'ın bir alt türüdür. Hangi yaklaşım seçilirse seçilsin, seçim sözleşmenin parçası olarak belgelenmelidir. Kullanıcı metodun bozuk argümanda ne yapacağını belgeden okuyabilmelidir.
4.2.2.4Sözleşmeyi Javadoc ile yazmak
Javadoc yorumları, sözleşmeyi hem okunabilir hem de araçlarla işlenebilir biçimde yazmayı sağlar. Yorum /** ile başlar; ilk cümle metodun ne yaptığını özetler. Ardından gelen etiketler sözleşmenin parçalarını ayrı ayrı belirtir:
/**
* Hesaptan para çeker.
* @param miktar çekilecek tutar; {@code 0 < miktar <= bakiye} olmalı
* @return işlemden sonraki bakiye (eski bakiye - miktar)
* @throws IllegalArgumentException miktar bu aralıkta değilse
*/
public double paraCek(double miktar) {
if (miktar <= 0 || miktar > bakiye)
throw new IllegalArgumentException("Geçersiz miktar: " + miktar);
bakiye -= miktar;
return bakiye;
}| Etiket | Ne yazılır? | Örnekte |
|---|---|---|
@param | Her parametre ve onun ön koşulu | miktar için 0 < miktar <= bakiye |
@return | Dönüş değeri, yani son koşul | işlemden sonraki bakiye |
@throws | Sözleşme bozulunca ne olacağı | IllegalArgumentException |
Yorumdaki {@code ...} yazımı bir ayrıntı değildir. Javadoc, yorumların içini HTML olarak işler. Ham < işareti bir HTML etiketinin başlangıcı sanılır. Aynı yorum {@code} olmadan 0 < miktar <= bakiye diye yazıldığında JDK 21'in javadoc aracı iki malformed HTML hatası verir ve üretilen sayfada eşitsizliğin yerine invalid input uyarısı görünür. {@code ...} içindeki metin ise olduğu gibi, kod yazı tipiyle gösterilir.
javadoc -d belge BankaHesabi.java komutu bu yorumlardan belge klasörüne HTML sayfaları üretir. Java kütüphanesinin çevrim içi belgeleri de aynı yolla oluşturulur. String ya da Math sınıfının belgesinde okunan her açıklama, o sınıfların kaynak kodundaki Javadoc yorumlarından gelir.
Javadoc'a ne yazılacağı sorusunun cevabı bilgi saklamadan çıkar. Metodun ne yaptığı ve kullanım koşulları yazılır. Nasıl yaptığı yazılmaz; o, gizlenen gerçeklemedir ve değişebilir. Javadoc'un okuru, gövdeyi hiç görmeyecek olan kullanıcıdır. Bu bakışla sınıfın dış yüzü, public metotların imzaları ile bu yorumların toplamıdır.
Örnekteki paraCek, sözleşmeyi istisnayla uygular. JDK 21 ile 1000 TL bakiyeli bir hesapta denendiğinde paraCek(300.0) 700.0 döndürür. paraCek(5000.0) ve paraCek(-20) ise Geçersiz miktar: 5000.0 ve Geçersiz miktar: -20.0 iletileriyle istisna fırlatır. İkinci iletideki -20.0'a dikkat: int olarak verilen -20 argümanı double parametreye genişletilmiştir.
4.2.2.5Sınıf değişmezi ve assert
Ön ve son koşul tek bir metoda aittir. Bazı kurallar ise sınıfın bütün nesneleri için her an geçerli olmalıdır. Saatin saniyesi her zaman 0–59 arasında, banka hesabının bakiyesi her zaman sıfır ya da pozitif olmalıdır.
Sınıf değişmezi (class invariant), bir sınıfın her nesnesinin durumunun her zaman sağlaması gereken kuraldır. Değişmez, nesne kurulduğunda sağlanır ve her public metot bittiğinde yeniden doğru olmalıdır.
Değişmezi geliştirme sırasında denetlemek için Java'nın assert deyimi kullanılır. assert koşul : mesaj; deyimi koşulu hesaplar; koşul yanlışsa mesajla birlikte bir AssertionError fırlatır. Aşağıdaki Sayac sınıfında bilerek bir hata bırakılmıştır: değer 9'dan sonra 10 olur, oysa değişmez 0–9 aralığını ister.
class Sayac {
private int deger; // değişmez: 0 <= deger <= 9
void arttir() {
deger = deger + 1; // hata: 9'dan sonra 10
assert deger >= 0 && deger <= 9 : "deger=" + deger;
}
}
public class Ana {
public static void main(String[] args) {
Sayac s = new Sayac();
for (int i = 0; i < 10; i++) s.arttir();
System.out.println("bitti");
}
}Program iki kez çalıştırılır. Önce olağan biçimde, java Ana komutuyla:
bittiSonra doğrulamalar açılarak, java -ea Ana komutuyla:
Exception in thread "main" java.lang.AssertionError: deger=10
at Sayac.arttir(Ana.java:5)
at Ana.main(Ana.java:11)İki çıktının farkı assert'ün en önemli özelliğini gösterir. Doğrulamalar varsayılan olarak kapalıdır. -ea (enable assertions) seçeneği verilmezse assert satırı hiç çalışmaz; koşul hesaplanmaz bile. Hata sessizce geçer ve program bitti yazar. -ea ile ise onuncu artırmada değer 10 olur, koşul yanlış çıkar ve AssertionError: deger=10 fırlatılır. Yığın izinin ilk satırı hatanın Sayac.arttir içinde, Ana.java dosyasının 5. satırında doğduğunu gösterir.
Bu özellik assert'ün ne için kullanılacağını da belirler. assert, programcının “bu asla olmamalı” dediği iç varsayımları geliştirme sırasında denetler: sınıf değişmezleri, son koşullar, ulaşılmaması gereken kod dalları. public bir metodun argümanları ise assert ile denetlenmez. Doğrulamalar kapalıyken denetim hiç yapılmaz ve hatalı argüman nesneye girer. Kısaca: argüman için istisna, iç tutarlılık için assert.
Kullanıcı girdisini assert ile denetlemek. assert yas >= 0; satırı yalnızca -ea ile çalıştırılan programda işe yarar. Program olağan biçimde çalıştırıldığında negatif yaş hiçbir uyarı vermeden kabul edilir. Dışarıdan gelen değer için if ile denetim ve IllegalArgumentException kullanılır.
assert koşuluna yan etkili ifade yazmak. assert liste.remove(x); gibi bir satır, doğrulamalar kapalıyken hiç çalışmaz; eleman listeden silinmez. Programın davranışı -ea seçeneğine göre değişir. assert koşulu yalnızca okur, hiçbir şeyi değiştirmez.
4.2.3Erişim belirleyiciler
Sarmalamanın “kapatma” yarısını Java'da erişim belirleyiciler sağlar. Bir alanı ya da metodu kimlerin kullanabileceğini, tanımın başına yazılan tek bir anahtar kelime belirler. Kuralı derleyici uygular; programcının iyi niyetine bırakılmaz.
4.2.3.1public ve private
Erişim belirleyici (access modifier), bir sınıfa, alana ya da metoda kimin erişebileceğini belirleyen anahtar kelimedir. public üyeye diğer sınıflar doğrudan erişebilir. private üyeye yalnızca sınıfın içinden erişilebilir; dışarıdan erişimi derleyici engeller.
Sınıf yazarken izlenen alışkanlık üç maddeyle özetlenir. Sınıflar çoğu zaman public tanımlanır. Sınıfın alanları private tanımlanır. Alanlara erişmek ve onları değiştirmek için public metotlar yazılır. Kısaca: alanlar gizli, davranışlar açık.
public class Ogrenci { // herkese açık sınıf
private String ad; // gizli alan
private int not; // gizli alan
public int getNot() { // açık metot
return not;
}
}Ogrenci sınıfında ad ve not alanları private'tır. Başka bir sınıf o.not yazarak notu okuyamaz; okumak için o.getNot() çağrılır. Şekil 4.2.5 bu iki yolu UML gösterimiyle karşılaştırır. UML sınıf diyagramlarında - işareti private, + işareti public üye demektir.
Ogrenci'nin public metodu getNot()'u çağırabilir (yeşil ok). private alan not'a doğrudan erişme denemesi ise derlenmez (kırmızı kesik ok).4.2.3.2Dört erişim düzeyi
Java'da public ile private arasında iki ara düzey daha vardır. Dört düzeyin kimlere açık olduğu Tablo 4.2.5'de verilmiştir.
| Belirleyici | Aynı sınıf | Aynı paket | Alt sınıf (başka paket) | Her yer |
|---|---|---|---|---|
public | ✓ | ✓ | ✓ | ✓ |
protected | ✓ | ✓ | ✓ | ✗ |
| (yazılmazsa) paket erişimi | ✓ | ✓ | ✗ | ✗ |
private | ✓ | ✗ | ✗ | ✗ |
Belirleyici hiç yazılmazsa paket erişimi (package-private, bazı kaynaklarda “default”) geçerli olur: üyeye yalnızca aynı paketteki sınıflar erişir. protected ise alt sınıflar içindir. Aynı paketteki sınıflara ek olarak başka paketteki alt sınıflar da protected üyeye ulaşır. Paket, birbiriyle ilgili sınıfların aynı klasörde toplandığı gruptur. Alt sınıf ise extends ile başka bir sınıftan türetilen sınıftır. Düzeyler iç içe halkalar oluşturur (Şekil 4.2.6).
private (yalnız aynı sınıf), onu saran halkada paket erişimi (aynı paket) bulunur. Onun dışında protected (artı başka paketteki alt sınıflar), en dışta public (her yer) yer alır.Tablodaki her hücre JDK 21 ile sınanmıştır. p1 paketindeki A sınıfına dört alan konmuştur: public int acik, protected int korumali, belirleyicisiz int paket ve private int gizli. Dört alana dört farklı yerden yazılmaya çalışıldığında derleyici şu hataları verir:
p1/B.java:3: error: gizli has private access in A
void dene(A a) { a.acik = a.korumali = a.paket = 1; a.gizli = 1; }
^
p2/C.java:3: error: paket is not public in A; cannot be accessed from outside package
void dene() { acik = korumali = 1; paket = 1; gizli = 1; }
^
p2/C.java:3: error: gizli has private access in A
void dene() { acik = korumali = 1; paket = 1; gizli = 1; }
^
p2/D.java:3: error: korumali has protected access in A
void dene(p1.A a) { a.acik = 1; a.korumali = 1; a.paket = 1; a.gizli = 1; }
^
p2/D.java:3: error: paket is not public in A; cannot be accessed from outside package
void dene(p1.A a) { a.acik = 1; a.korumali = 1; a.paket = 1; a.gizli = 1; }
^
p2/D.java:3: error: gizli has private access in A
void dene(p1.A a) { a.acik = 1; a.korumali = 1; a.paket = 1; a.gizli = 1; }
^
6 errorsA'nın kendi metodu dört alana da yazabilir; hiç hata yoktur. Aynı paketteki B yalnızca gizli'ye ulaşamaz. Başka paketteki alt sınıf C, paket ve gizli'ye ulaşamaz. Başka paketteki ilgisiz sınıf D ise yalnızca acik'e yazabilir. Bu sonuç tablonun satırlarıyla birebir aynıdır. İletiler de düzeye göre değişir: has private access, is not public in A; cannot be accessed from outside package ve has protected access.
Doğru düzeyi seçerken kural basittir: en kısıtlayıcı düzey tercih edilir. Önce private düşünülür; gerçekten gerekirse daha açık bir düzeye geçilir. Bir üyeyi sonradan açmak kolaydır ve kimseyi etkilemez. Açık bir üyeyi sonradan kapatmak ise onu kullanan bütün kodu bozar.
Son bir kural da sınıfların kendisiyle ilgilidir. Üst düzey bir sınıf, yani başka bir sınıfın içinde olmayan sınıf, yalnızca public ya da paket erişimli olabilir. private class UstPrivate { } yazıldığında derleyici modifier private not allowed here iletisini verir. private ve protected sınıflar ancak başka bir sınıfın içinde, iç sınıf olarak tanımlanabilir.
4.2.3.3private alana erişim denemesi
DijitalSaat sınıfında saniye alanı private'tır. Başka bir sınıf bu alana doğrudan yazmaya çalışırsa ne olur?
public class Test {
public static void main(String[] args) {
DijitalSaat s = new DijitalSaat();
s.saniye = 75;
}
}Test.java:4: error: saniye has private access in DijitalSaat
s.saniye = 75;
^
1 errorİleti hatanın dosyasını ve satırını (Test.java:4), nedenini (saniye has private access in DijitalSaat) ve satırın içindeki yerini (^ işareti) gösterir. Program hiç çalışmaz; .class dosyası üretilmez. Alan public olsaydı s.saniye = 75; sessizce kabul edilir ve saat geçersiz bir duruma düşerdi. Çözüm, değişikliği sınıfın kendi metoduna yaptırmaktır: s.setSaniye(75);. Metot geçersiz değeri reddeder (Şekil 4.2.7).
Test sınıfının DijitalSaat'e iki yolu. Alana doğrudan yazmak (s.saniye = 75;) derleme hatasıdır. Değiştirici üzerinden gitmek (s.setSaniye(75);) derlenir ve çalışır; ama 0–59 dışındaki değer reddedilir, alan 0'da kalır.İki yol arasındaki fark, kuralın nerede denetlendiğidir. Birinci yolda denetimi derleyici yapar ve erişimi tamamen yasaklar. İkinci yolda erişime izin verilir, ama değer sınıfın kendi kuralından geçer. Sarmalama ikisini birlikte kullanır: alana giden doğrudan yol kapalı, metoda giden denetimli yol açıktır.
4.2.3.4private sınıf düzeyindedir
private sözcüğü “bu nesneye özel” anlamına gelmez. Erişim denetimi nesneye göre değil, sınıfa göre yapılır. Bir sınıfın kodu, o sınıftan oluşturulmuş her nesnenin private alanlarını okuyabilir.
class Kesir {
private int pay, payda;
Kesir(int pay, int payda) {
this.pay = pay;
this.payda = payda;
}
Kesir carp(Kesir diger) {
// diger'in private alanları: izinli
return new Kesir(pay * diger.pay,
payda * diger.payda);
}
@Override public String toString() {
return pay + "/" + payda;
}
}
// main içinde:
Kesir k = new Kesir(2, 3).carp(new Kesir(3, 4));
System.out.println(k);6/12carp metodu kendi alanlarını (pay, payda) ve parametre olarak gelen diger nesnesinin alanlarını (diger.pay, diger.payda) okur. İkinci nesne başka bir nesnedir, ama aynı sınıftandır; bu yüzden erişim serbesttir. 2/3 ile 3/4'ün çarpımı 6/12 bulunur. Kesir sadeleştirilmez, çünkü carp yalnızca pay ve paydaları çarpar.
carp, equals, compareTo gibi iki nesneyi birlikte işleyen metotlar bu sayede alan okumak için ayrı bir metoda gerek duymaz. Aynı satır başka bir sınıfta yazıldığında ise derlenmez:
Dis.java:3: error: pay has private access in Kesir
int p = new Kesir(1, 2).pay;
^
1 errorSarmalamanın sınırı sınıfın kaynak kodudur. Sınıfın içi tek bir yazarın sorumluluğundadır ve o yazar sınıfın bütün kurallarını bilir. Korunan, sınıfın dışındaki kullanıcılardır.
4.2.4Erişimci ve değiştirici metotlar
Alanlar private olunca onlara ulaşmanın tek yolu sınıfın metotlarıdır. Bu metotların en yaygın iki türü vardır: alanın değerini okuyan ve alanı denetimli biçimde değiştiren metotlar.
Erişimci (accessor, getter), bir alanın değerini döndüren metottur. Değiştirici (mutator, setter), bir alana yeni değeri önce denetleyip geçerliyse atayan metottur. Java geleneğinde adları get ve set ile başlar.
4.2.4.1DijitalSaat sınıfı
Sunumun ana örneği, saat, dakika ve saniyeyi tutan bir dijital saattir. İlk sürüm yalnızca saniye için erişimci ve değiştirici içerir:
public class DijitalSaat {
private int saat = 0;
private int dakika = 0;
private int saniye = 0;
public int getSaniye() {
return this.saniye;
}
public void setSaniye(int saniye) {
if (saniye >= 0 && saniye < 60) {
this.saniye = saniye;
}
}
}getSaniye() alanın değerini döndürür; hiçbir şeyi değiştirmez. setSaniye(int saniye) önce gelen değeri denetler. Değer 0 ile 59 arasındaysa atama yapılır, değilse metot hiçbir şey yapmadan biter. Bu değiştirici sessizce yok sayma yaklaşımını seçmiştir. Sınıfın UML diyagramı Şekil 4.2.8'te verilmiştir.
DijitalSaat sınıfının UML diyagramı. Üstte sınıfın adı, ortada üç private alan (-) ve ilk değerleri, altta iki public metot (+) ve dönüş tipleri.4.2.4.2Değiştiricinin korumasını izlemek
Değiştiricinin geçersiz değeri nasıl durdurduğunu görmek için aşağıdaki program adım adım izlenir:
public class DijitalSaat {
private int saniye = 0;
public int getSaniye() {
return this.saniye;
}
public void setSaniye(int saniye) {
if (saniye >= 0 && saniye < 60) {
this.saniye = saniye;
}
}
}
// SaatTest.main içinde:
DijitalSaat s = new DijitalSaat();
s.setSaniye(45);
s.setSaniye(75);
System.out.println(s.getSaniye());45| Adım | Satır | Çağrı yığını | Değişkenler | Yeni çıktı |
|---|---|---|---|---|
| 1 | 13: DijitalSaat s = new DijitalSaat(); | main() | s=→DijitalSaat | — |
| 2 | 7: if (saniye >= 0 && saniye < 60) { | main() → s.setSaniye(45) | this=→nesne, saniye=45 | — |
| 3 | 8: this.saniye = saniye; | main() → s.setSaniye(45) | this=→nesne, saniye=45 | — |
| 4 | 7: if (saniye >= 0 && saniye < 60) { | main() → s.setSaniye(75) | this=→nesne, saniye=75 | — |
| 5 | 4: return this.saniye; | main() → s.getSaniye() | this=→nesne, dönüş=45 | — |
| 6 | 16: System.out.println(s.getSaniye()); | main() | s=→DijitalSaat | 45 |
Birinci adımda nesne oluşturulur ve saniye alanı 0 ile başlar. İkinci adımda setSaniye(45) çağrılır. Yeni çerçevede iki şey vardır: saniye parametresi 45'tir ve this metodu çağıran nesneyi gösterir. Koşul doğru çıkar. Üçüncü adımda this.saniye = saniye; çalışır. Soldaki this.saniye nesnenin alanıdır, sağdaki saniye parametredir; alan 45 olur.
Dördüncü adım korumanın kendisidir. setSaniye(75) çağrısında koşul yanlış çıkar ve atama satırı atlanır. Geçersiz değer nesneye giremez. Şekil 4.2.9 bu iki çağrıyı yan yana gösterir. Beşinci adımda getSaniye() alanın değerini döndürür: 45. Son adımda ekrana 45 yazılır. Nesnenin durumu baştan sona geçerli kaldı.
setSaniye(45): koşul doğru, atama yapılacak. Sağda setSaniye(75): koşul yanlış; heap'teki nesnenin saniye alanı 45'te kalır.Erişimci ve değiştiricinin maliyeti sabittir: bir karşılaştırma, bir atama ya da bir okuma. Java sanal makinesi bu kadar küçük metotları çalışma sırasında çoğu zaman çağrı yerine yerleştirir; doğrudan alan erişimiyle aralarında ölçülebilir bir hız farkı kalmaz. Sarmalama, programı yavaşlatmanın bedeli olarak görülmemelidir.
4.2.4.3this ve gölgeleme
Değiştiricilerde parametrenin adı genellikle alanın adıyla aynı seçilir: setSaniye(int saniye). Bu seçim okunabilirliği artırır, ama bir kuralın bilinmesini gerektirir.
Gölgeleme (shadowing), iç kapsamdaki bir adın dış kapsamdaki aynı adı gizlemesidir. Parametrenin adı alanla aynıysa, metodun içinde o ad parametreyi gösterir; alan gölgede kalır. this, metodu çağıran nesnenin kendisine referanstır ve this.saniye her zaman alanı gösterir.
Doğru yazım this.saniye = saniye;'dir. this unutulduğunda ne olacağını Kutu sınıfı gösterir:
public class Kutu {
private int en;
public int getEn() {
return en;
}
public void setEn(int en) {
en = en;
}
}setEn metodunda en = en; satırının iki tarafı da parametredir. Parametre kendisine atanır, alan hiç değişmez. k.setEn(5) çağrısından sonra k.getEn() şunu döndürür:
0Alan, int alanların varsayılan değeri olan 0'da kalmıştır. Bu hatanın en tehlikeli yanı derleyicinin hata vermemesidir. en = en; geçerli bir Java deyimidir. Hata ancak program çalıştırılıp çıktıya bakıldığında fark edilir. Bu yüzden değiştiricilerde this. yazma alışkanlığı önemlidir. Bazı geliştirme ortamları bu satırı “kendine atama” uyarısıyla işaretler, ama javac varsayılan ayarlarıyla sessiz kalır.
final alanda this'i unutmak. Alan private final String ad; ise ve yapıcıda ad = ad; yazılırsa sonuç farklıdır. Atama yine parametreye yapılır; alana hiçbir değer verilmemiş olur. final alanın yapıcıda mutlaka atanması gerektiği için derleyici bu kez yapıcının sonunu gösteren variable ad might not have been initialized hatasını verir. final olmayan alanda ise hiçbir uyarı çıkmaz.
4.2.4.4Erişim metotları ve adlandırma
Saatin bütün alanları için erişimci ve değiştirici yazıldığında sınıf şu hâli alır:
public class DijitalSaat {
private int saat = 0; // 0..23
private int dakika = 0; // 0..59
private int saniye = 0; // 0..59
public int getSaat() { return saat; }
public int getDakika() { return dakika; }
public int getSaniye() { return saniye; }
public void setSaat(int saat) {
if (saat >= 0 && saat < 24) this.saat = saat;
}
public void setDakika(int dakika) {
if (dakika >= 0 && dakika < 60) this.dakika = dakika;
}
public void setSaniye(int saniye) {
if (saniye >= 0 && saniye < 60) this.saniye = saniye;
}
public void goster() {
System.out.printf("%02d:%02d:%02d%n",
saat, dakika, saniye);
}
}DijitalSaat s = new DijitalSaat();
s.setSaat(9);
s.setDakika(5);
s.setSaniye(30);
s.goster();
s.setSaat(24); // reddedilir
s.setDakika(-3); // reddedilir
s.goster();09:05:30
09:05:30İlk üç çağrı geçerlidir ve saat 09:05:30 olur. setSaat(24) çağrısı reddedilir, çünkü geçerli saatler 0–23'tür. setDakika(-3) de reddedilir. İkinci goster() çağrısı bu yüzden aynı değeri yazar. printf içindeki %02d biçimi sayıyı en az iki basamağa, gerekirse başına sıfır ekleyerek yazar: 9 değeri 09 olarak görünür.
Erişim metotlarının adları bir geleneğe uyar. dakika alanının erişimcisi getDakika(), değiştiricisi setDakika(int) adını alır. Alan adının ilk harfi büyütülüp başına get ya da set eklenir. boolean bir alanın erişimcisi ise is ile başlar: aktif alanı için isAktif(). Bu gelenek yalnızca okunabilirlik için değildir. Birçok Java kütüphanesi ve geliştirme aracı, bir sınıfın özelliklerini bu adlardan çıkarır.
4.2.4.5Neden public alan değil?
Bir alanı private yapıp iki metot yazmak, onu public yapmaktan daha zahmetlidir. Bu zahmetin karşılığı Tablo 4.2.7'de özetlenmiştir.
public class Saat {
public int saniye;
}
// başka bir sınıfta:
s.saniye = -5; // kabul edilir!public class Saat {
private int saniye;
public void setSaniye(int s) {
if (s >= 0 && s < 60) saniye = s;
}
}| Soru | public alan | private alan + metot |
|---|---|---|
| Kim, hangi değeri yazabilir? | Her yerden, her değer | Yalnızca metodun kabul ettiği değer |
| Kural (0–59) nerede denetlenir? | Her kullanıcıya bırakılır; biri unutursa nesne bozulur | Tek bir yerde, sınıfın içinde |
| Salt okunur alan mümkün mü? | Hayır (final dışında) | Evet; yalnızca erişimci yazılır |
| Alanın adı ya da tipi değişirse? | Onu kullanan bütün sınıflar değişir | Metotların imzası aynı kalabilir |
Tablonun son satırı en uzun vadeli kazancı gösterir: iç yapı değiştiğinde bile metotların imzası korunabilir. Saatin iki alanı tek bir toplamSaniye alanına indirildiğinde de kullanan kod hiç değişmez.
Sunumdaki etkileşimli deneme, bu karşılaştırmayı bir DijitalSaat nesnesi üzerinde yaptırır. Nesne 09:05:30 durumundadır. Saniyeye 75 yazılmak istendiğinde iki yol izlenebilir. Değiştirici yolu (s.setSaniye(75)) değeri reddeder ve saat 09:05:30'da kalır. Alan public olsaydı (s.saniye = 75) değer doğrudan yazılır ve saat 09:05:75 gibi anlamsız bir durumu gösterir. Geçerli aralıklar saat için 0–23, dakika ve saniye için 0–59'dur.
Yalnızca veri taşıyan ve hiç değişmeyecek sınıflar için Java 16'dan beri daha kısa bir yol vardır: kayıt (record). record Nokta(int x, int y) { } satırı, alanları otomatik olarak private final olan bir sınıf tanımlar. Erişimcilerin adı get ile başlamaz, alanın adını taşır: x() ve y(). Derleyici toString, equals ve hashCode metotlarını da kendisi yazar:
public class KayitTest {
public static void main(String[] args) {
Nokta n = new Nokta(3, 4);
System.out.println(n.x() + " " + n.y());
System.out.println(n);
System.out.println(n.equals(new Nokta(3, 4)));
}
}3 4
Nokta[x=3, y=4]
trueKaydın alanına dışarıdan yazma denemesi (n.x = 5;) x has private access in Nokta hatası verir. Alan hem private hem final olduğu için kayıt, değiştirilemez sınıfın hazır bir örneğidir.
4.2.5Örnek: banka hesabı
Şimdiye kadarki parçalar tek bir sınıfta birleştirilebilir. Banka hesabı bunun için iyi bir örnektir: verisi değerlidir, kuralları açıktır ve kuralların bozulması kolayca fark edilir. Bakiye hiçbir zaman eksiye düşmemeli, para yalnızca belirli işlemlerle girip çıkmalıdır. BankaHesabi sınıfı dört parça hâlinde incelenir.
4.2.5.1Alanlar ve yapıcı
public class BankaHesabi {
private int hesapNumarasi; // Hesap numarası
private double bakiye; // Hesap bakiyesi
private String hesapTuru; // "Vadesiz" / "Vadeli"
public BankaHesabi(int hesapNumarasi,
double bakiye, String hesapTuru) {
this.hesapNumarasi = hesapNumarasi;
this.bakiye = bakiye;
this.hesapTuru = hesapTuru;
}
public int getHesapNumarasi() {
return hesapNumarasi;
}
public double getBakiye() {
return bakiye;
}
public String getHesapTuru() {
return hesapTuru;
}
// ... para yatırma ve çekme metotlarıSınıfın üç alanı da private'tır: hesap numarası, bakiye ve hesap türü. Kimse hesap numarasını ya da bakiyeyi doğrudan değiştiremez. Yapıcı (constructor) nesneyi ilk değerleriyle kurar. Parametrelerin adları alanlarla aynı seçildiği için atamalar this.bakiye = bakiye; kalıbıyla yazılır.
Sınıfta yalnızca erişimciler vardır, değiştirici yoktur. setBakiye metodu bilinçli olarak yazılmamıştır. Bakiye yalnızca para yatırma, çekme, faiz ve transfer işlemleriyle değişir. Böylece bakiyeyi değiştiren her yol, kendi kuralını uygulayan bir metottan geçer. Hesap numarası ve hesap türü için de değiştirici yoktur; bu iki alan hesap açıldıktan sonra değişmez.
4.2.5.2Yatırma ve çekme
// ...
// Para yatırma işlemi
public void paraYatir(double miktar) {
if (miktar > 0) {
bakiye += miktar;
System.out.println(miktar
+ " TL yatırıldı. Yeni bakiye: " + bakiye);
} else {
System.out.println("Yatırılan miktar geçersiz.");
}
}
// Para çekme işlemi
public void paraCek(double miktar) {
if (miktar <= bakiye && miktar > 0) {
bakiye -= miktar;
System.out.println(miktar
+ " TL çekildi. Kalan bakiye: " + bakiye);
} else if (miktar > bakiye) {
System.out.println("Yetersiz bakiye.");
} else {
System.out.println("Çekilen miktar geçersiz.");
}
}Her işlem önce ön koşulu denetler. Para yatırmada koşul miktar > 0'dır. Para çekmede iki koşul vardır: miktar > 0 ve miktar ≤ bakiye. Koşul bozuksa bakiye değişmez, yalnızca bir mesaj yazılır. Bu sınıf ön koşul bozulduğunda “sessizce yok saymak” ile “bildirmek” arasında bir yol seçmiştir: istisna fırlatmaz, ama ekrana neyin yanlış gittiğini yazar.
paraCek'teki if–else if–else zincirinin sırası önemlidir. Önce işlemin geçerli olduğu durum denetlenir. Geçerli değilse ilk olası neden, miktarın bakiyeyi aşmasıdır. Geriye kalan tek durum miktarın sıfır ya da negatif olmasıdır. Bu düzen sayesinde her geçersiz çağrı tam olarak bir mesaj alır.
İki metot birlikte sınıfın değişmezini korur: bakiye hiçbir yoldan eksiye düşemez. Yatırma bakiyeyi yalnızca artırır; çekme ise ancak yeterli bakiye varsa azaltır. Değişmez nesne kurulurken doğruysa, bu iki metottan sonra da doğru kalır.
4.2.5.3Faiz
// ...
// Faiz ekleme (yalnızca vadeli hesap)
public void faizEkle(double oran) {
if (hesapTuru.equals("Vadeli") && oran > 0) {
double faiz = bakiye * oran / 100;
bakiye += faiz;
System.out.println("Faiz ekleniyor: " + faiz
+ " TL. Yeni bakiye: " + bakiye);
} else if (!hesapTuru.equals("Vadeli")) {
System.out.println(
"Faiz yalnızca vadeli hesaplarda uygulanabilir.");
} else {
System.out.println("Geçersiz faiz oranı.");
}
}Faiz yalnızca vadeli hesapta ve pozitif oranla eklenir. Hesap türü bir metin olduğu için karşılaştırma == ile değil, equals ile yapılır. == iki referansın aynı nesneyi gösterip göstermediğine bakar; equals ise metinlerin içeriğini karşılaştırır. İki ayrı yoldan oluşturulmuş "Vadeli" metinleri == ile farklı çıkabilir.
faiz bir yerel değişkendir. Yalnızca metot çalışırken vardır ve nesnenin parçası değildir. Faiz tutarı bir kez hesaplanır, bakiyeye eklenir ve metot bitince yok olur. Nesnenin durumunu oluşturan, yalnızca üç alandır.
4.2.5.4Transfer
// ...
// Başka bir hesaba transfer
public void transfer(BankaHesabi hedefHesap,
double miktar) {
if (miktar <= bakiye && miktar > 0) {
bakiye -= miktar;
hedefHesap.paraYatir(miktar);
System.out.println(miktar
+ " TL transfer edildi. Yeni bakiye: " + bakiye);
} else {
System.out.println(
"Yetersiz bakiye veya geçersiz miktar.");
}
}
}transfer metodu, aynı sınıftan başka bir nesnenin metodunu çağırır: hedefHesap.paraYatir(miktar). Kendi bakiyesinden düşer, hedef hesaba ise yatırma metoduyla ekler (Şekil 4.2.10).
hesap1 kendi bakiyesinden 300 düşer. Ardından hesap2 nesnesinin paraYatir metodunu çağırır; ekleme işini hesap2 kendi kuralıyla yapar.Hedef hesabın bakiye alanına doğrudan yazmak da mümkündü: hedefHesap.bakiye += miktar;. İki nesne aynı sınıftandır ve private sınıf düzeyindedir. Ama metot çağırmak kuralların tek yerde kalmasını sağlar. Yarın yatırma işlemine bir kural eklense, örneğin her yatırma kaydedilse, transfer de bu kuraldan otomatik olarak geçer. Alanı doğrudan değiştiren kod ise yeni kuralı atlardı.
4.2.5.5İki hesabı adım adım izlemek
Sınıfın bütün parçaları aşağıdaki main metoduyla birlikte çalıştırılır:
// main içinde:
BankaHesabi hesap1 =
new BankaHesabi(123456, 2000.0, "Vadeli");
BankaHesabi hesap2 =
new BankaHesabi(789012, 1500.0, "Vadesiz");
hesap1.paraYatir(500.0);
hesap2.paraCek(200.0);
hesap1.faizEkle(5.0);
hesap1.transfer(hesap2, 300.0);
System.out.println(hesap1.getBakiye());
System.out.println(hesap2.getBakiye());500.0 TL yatırıldı. Yeni bakiye: 2500.0
200.0 TL çekildi. Kalan bakiye: 1300.0
Faiz ekleniyor: 125.0 TL. Yeni bakiye: 2625.0
300.0 TL yatırıldı. Yeni bakiye: 1600.0
300.0 TL transfer edildi. Yeni bakiye: 2325.0
2325.0
1600.0| Adım | Satır | Çağrı yığını | Değişkenler | Yeni çıktı |
|---|---|---|---|---|
| 1 | 2: BankaHesabi hesap1 =3: new BankaHesabi(123456, 2000.0, "Vadeli"); | main() | hesap1=→BankaHesabi | — |
| 2 | 4: BankaHesabi hesap2 =5: new BankaHesabi(789012, 1500.0, "Vadesiz"); | main() | hesap1=→BankaHesabi, hesap2=→BankaHesabi | — |
| 3 | 6: hesap1.paraYatir(500.0); | main() → hesap1.paraYatir(500.0) | miktar=500.0 | 500.0 TL yatırıldı. Yeni bakiye: 2500.0 |
| 4 | 7: hesap2.paraCek(200.0); | main() → hesap2.paraCek(200.0) | miktar=200.0 | 200.0 TL çekildi. Kalan bakiye: 1300.0 |
| 5 | 8: hesap1.faizEkle(5.0); | main() → hesap1.faizEkle(5.0) | oran=5.0, faiz=125.0 | Faiz ekleniyor: 125.0 TL. Yeni bakiye: 2625.0 |
| 6 | 9: hesap1.transfer(hesap2, 300.0); | main() → hesap1.transfer(hesap2, 300.0) | miktar=300.0, hedefHesap=→hesap2 | — |
| 7 | 9: hesap1.transfer(hesap2, 300.0); | main() → hesap1.transfer(hesap2, 300.0) → hesap2.paraYatir(300.0) | miktar=300.0 | 300.0 TL yatırıldı. Yeni bakiye: 1600.0 |
| 8 | 9: hesap1.transfer(hesap2, 300.0); | main() → hesap1.transfer(hesap2, 300.0) | miktar=300.0 | 300.0 TL transfer edildi. Yeni bakiye: 2325.0 |
| 9 | 10: System.out.println(hesap1.getBakiye());11: System.out.println(hesap2.getBakiye()); | main() | hesap1=→BankaHesabi, hesap2=→BankaHesabi | 2325.0 ⏎ 1600.0 |
İlk iki adımda iki bağımsız nesne kurulur. Her nesnenin kendi bakiye alanı vardır: hesap1'inki 2000.0, hesap2'ninki 1500.0. Üçüncü adımda hesap1.paraYatir(500.0) çağrılır. Bu çağrıda this, hesap1'i gösterir; miktar > 0 doğrudur ve bakiye 2500.0 olur. Dördüncü adımda bu kez this, hesap2'dir. 200 ≤ 1500 olduğu için çekim yapılır ve bakiye 1300.0'a iner. hesap1 bu işlemden etkilenmez.
Beşinci adımda faiz hesaplanır. Hesap vadelidir ve oran pozitiftir: faiz = 2500.0 × 5 / 100 = 125.0. Bakiye 2625.0 olur. Altıncı adımda transfer başlar. Koşul doğrudur; hesap1 önce kendi bakiyesinden düşer: 2625.0 − 300.0 = 2325.0. Yedinci adımda hedefHesap.paraYatir(miktar) çağrılır. Yığında artık üç çerçeve vardır: main, transfer ve paraYatir. Bu en üstteki çerçevede this, hesap2'yi gösterir ve onun bakiyesi 1600.0 olur (Şekil 4.2.11).
transfer çalışıyor: this birinci hesabı, hedefHesap ikinci hesabı gösteriyor; birinci hesabın bakiyesi 2325.0'a inmiş. Sağda paraYatir çalışıyor: bu kez this ikinci hesap ve bakiyesi 1600.0.Sekizinci adımda paraYatir biter ve transfer kaldığı yerden devam eder. Çıktıdaki sıraya dikkat: önce “300.0 TL yatırıldı” (iç çağrının mesajı), sonra “300.0 TL transfer edildi” (dış metodun mesajı) yazılır. Son adımda erişimciler iki bakiyeyi okur. Toplam para korunmuştur: 2325 + 1600 = 3925 = 2625 + 1300. Transfer para üretmez, yalnızca yer değiştirir.
Sınıfın her metodu sabit sayıda işlem yapar; döngü yoktur. Bu yüzden her çağrının zaman maliyeti O(1)'dir. Bellekte ise her hesap nesnesi yalnızca üç alan tutar; nesne sayısı arttıkça bellek doğrusal olarak artar.
Çıktıdaki sayılar double değerlerin Java'daki varsayılan yazımıdır: 500.0, 2625.0. Bu örnekte bütün tutarlar tam olduğu için sorun çıkmaz. Ama double ondalık kesirleri her zaman tam tutamaz. Aynı sınıfla 0.1 TL bakiyeli bir hesaba 0.2 TL yatırıldığında JDK 21 Yeni bakiye: 0.30000000000000004 yazar. Gerçek bankacılık yazılımları bu yüzden parayı kuruş cinsinden bir tam sayıyla ya da BigDecimal sınıfıyla tutar.
4.2.5.6Yapıcı da doğrulamalı
BankaHesabi'nın işlem metotları bakiyeyi korur. Ama yapıcısı gelen değeri hiç denetlemeden atar. Bu boşluk iki yoldan kötüye kullanılabilir. new BankaHesabi(1, -500.0, "Vadesiz") eksi bakiyeli bir hesap kurar; değişmez daha nesne doğarken bozulmuştur. "vadeli" (küçük harfle) yazılırsa hesap hiçbir zaman faiz alamaz ve kimse bunu fark etmez. JDK 21 ile denendiğinde iki nesne de sorunsuz kurulur; küçük harfli hesaba faizEkle(5.0) çağrıldığında “Faiz yalnızca vadeli hesaplarda uygulanabilir.” mesajı çıkar.
Sınıf değişmezi nesnenin doğduğu anda sağlanmalıdır. Yapıcı da bir ön koşul denetleyicisidir:
public BankaHesabi(int hesapNumarasi,
double bakiye, String hesapTuru) {
if (bakiye < 0)
throw new IllegalArgumentException(
"Başlangıç bakiyesi negatif olamaz");
if (!hesapTuru.equals("Vadeli")
&& !hesapTuru.equals("Vadesiz"))
throw new IllegalArgumentException(
"Bilinmeyen hesap türü: " + hesapTuru);
this.hesapNumarasi = hesapNumarasi;
this.bakiye = bakiye;
this.hesapTuru = hesapTuru;
}Bu yapıcıyla new BankaHesabi(1, -500.0, "Vadesiz") çağrısı Başlangıç bakiyesi negatif olamaz, new BankaHesabi(2, 1000.0, "vadeli") çağrısı ise Bilinmeyen hesap türü: vadeli iletisiyle istisna fırlatır. Hesap türü olarak null verilirse hesapTuru.equals(...) çağrısı NullPointerException fırlatır; o da nesnenin kurulmasını engeller, ama iletisi daha az açıklayıcıdır.
Yapıcı istisna fırlatırsa new ifadesi hiçbir referans üretmez. Geçersiz nesne kullanıcının eline hiç geçmez. Bu davranışı küçük bir Sicaklik sınıfıyla izlemek mümkündür:
class Sicaklik {
private final double kelvin;
public Sicaklik(double kelvin) {
if (kelvin < 0)
throw new IllegalArgumentException(
"Kelvin negatif olamaz");
this.kelvin = kelvin;
}
public double getKelvin() { return kelvin; }
}
// main içinde:
Sicaklik a = new Sicaklik(300);
Sicaklik b = null;
try {
b = new Sicaklik(-5);
} catch (IllegalArgumentException e) {
System.out.println("Reddedildi: "
+ e.getMessage());
}
System.out.println(a.getKelvin() + " " + b);Reddedildi: Kelvin negatif olamaz
300.0 null| Adım | Satır | Çağrı yığını | Değişkenler | Yeni çıktı |
|---|---|---|---|---|
| 1 | 12: Sicaklik a = new Sicaklik(300); | main() | a=→#1 | — |
| 2 | 13: Sicaklik b = null; | main() | a=→#1, b=null | — |
| 3 | 15: b = new Sicaklik(-5);3: public Sicaklik(double kelvin) { | main() → Sicaklik(-5.0) | a=→#1, b=null, kelvin=-5.0 | — |
| 4 | 4: if (kelvin < 0)5: throw new IllegalArgumentException( | main() → Sicaklik(-5.0) | a=→#1, b=null, kelvin=-5.0 | — |
| 5 | 16: } catch (IllegalArgumentException e) {17: System.out.println("Reddedildi: "18: + e.getMessage()); | main() | a=→#1, b=null | Reddedildi: Kelvin negatif olamaz |
| 6 | 20: System.out.println(a.getKelvin() + " " + b); | main() | a=→#1, b=null | 300.0 null |
Birinci adımda 300 geçerlidir; koşul yanlış çıkar, kelvin atanır ve a yeni nesneyi gösterir. İkinci adımda b açıkça null ile başlatılır. Bu atama gereklidir: b catch bloğundan sonra okunacaktır. Java, değer verilmemiş olabilecek bir yerel değişkenin okunmasına izin vermez.
Üçüncü adımda new ikinci nesneye yer ayırır ve yapıcıyı çağırır. Yer ayrılmış nesnenin kelvin alanı varsayılan değer olan 0.0'dır; parametre ise −5.0'dır. Dördüncü adımda ön koşul bozuk bulunur ve throw yapıcıyı keser. Atama satırı hiç çalışmaz. Şekil 4.2.12 bu iki anı gösterir.
new ikinci nesneye yer ayırmış, yapıcı −5.0 parametresiyle çalışıyor; nesne yarım kurulmuştur. Sağda istisna yakalanmış, b = ... ataması hiç yapılmamış; b hâlâ null ve yarım nesneyi gösteren hiçbir referans yok.Beşinci adımda kontrol catch bloğuna geçer ve Reddedildi: Kelvin negatif olamaz yazılır. b = new Sicaklik(-5); ataması yarıda kaldığı için b değişmemiştir. Yarım kurulmuş nesneye ulaşan hiçbir referans yoktur; çöp toplayıcı onu bir süre sonra temizler. Son adımda 300.0 null yazılır. Programda yalnızca geçerli Sicaklik nesneleri kullanılabilir: değişmez (kelvin ≥ 0) her nesnede doğrudur.
Yapıcı, nesnenin kurulduğu tek kapıdır. Kurallar orada bir kez uygulanırsa sınıfın öteki metotları gelen nesnenin geçerli olduğuna güvenebilir. Değiştiricisi olan sınıflarda yapıcının, aynı denetimi tekrar yazmak yerine değiştiriciyi çağırması da yaygın bir yoldur. Böylece kural tek bir metotta kalır.
Değiştiricileri doğrulayıp yapıcıyı unutmak. setSaniye 75'i reddeder, ama yapıcı this.saniye = saniye; yazıyorsa new DijitalSaat(75) geçersiz bir saat kurar. Nesneye değer giren her yol, yapıcı dahil, aynı kuraldan geçmelidir.
4.2.6Sınıf tasarımı
Sarmalanmış bir sınıfın iki yüzü vardır: kullanıcının gördüğü dış yüz ve yalnızca yazarın bildiği iç yapı. Bu ayrım, sınıfı tasarlarken verilen kararların çoğunu yönlendirir. Bu kesim ayrımı kavram olarak tanımlar, Java'nın interface yapısıyla somutlaştırır ve bir sınıfın iç yapısını kullananları bozmadan nasıl değiştirdiğini gösterir.
4.2.6.1Arayüz ve gerçekleme
Bir sınıf tanımı iki kısma ayrılır. Arayüz (interface), sınıfın ne yaptığını ve nasıl kullanılacağını belirtir: public metotların adları, parametreleri, dönüş tipleri ve yorumları. Gerçekleme (implementation), sınıfın iç mekanik ayrıntılarını ve private alanlarını içerir; bunlar yalnızca sınıfın içinde önemlidir.
Gerçekleme değişse bile arayüz değişmeyebilir, çünkü özel alanlar sınıfın dışına gizlidir. Bazı kaynaklar bu ayrımı üç unsurla anlatır. Tanımlama (specification), yapıcı ve metotların imzalarıdır. Temsil (representation), veriyi tutan private alanlardır. Gerçekleme, yapıcı ve metotların gövdeleridir. Kullanıcı yalnızca tanımlamayı bilir. Temsil ve gerçekleme değişse de kullanıcının kodu bozulmaz.
Bir espresso makinesi bu ayrımı iyi gösterir (Şekil 4.2.13). Kullanıcı kontrol panelindeki “Başlat”, “Su miktarı” ve “Durdur” düğmelerini bilir. Pompa, kazan, valfler ve demleme grubu panelin arkasındadır. Üretici pompayı daha sessiz bir modelle değiştirse bile panel aynı kalır ve kullanıcı farkı yalnızca sesten anlar.
4.2.6.2interface ve implements
Java, arayüz fikrine bir dil yapısı ayırmıştır. interface anahtar kelimesiyle tanımlanan bir tip, bir sınıfın ne yapacağını söyleyen sözleşmedir. Metotların yalnızca imzası vardır, gövdesi yoktur:
public interface Araba {
// Sınıf arayüzü: NE yapılır?
public void hareketEt();
public void dur();
}Arayüzdeki metotlar örtük olarak public ve abstract'tır; örnekteki public sözcükleri yazılmasa da anlam değişmez. Arabayı kullanan kişi yalnızca hareketEt() ve dur() metotlarını bilir. Arayüzü uygulayan sınıf ise işin nasıl yapılacağını yazar:
public class SporAraba implements Araba {
// Sınıf gerçeklemesi: NASIL yapılır?
private String marka;
private int hiz;
public SporAraba(String marka) {
this.marka = marka;
}
public void hareketEt() {
hiz = 100;
System.out.println(marka + " hareket ediyor: "
+ hiz + " km/sa");
}
public void dur() {
hiz = 0;
System.out.println(marka + " durdu.");
}
}// main içinde:
Araba araba = new SporAraba("Rüzgar");
araba.hareketEt();
araba.dur();Rüzgar hareket ediyor: 100 km/sa
Rüzgar durdu.implements Araba ifadesi, SporAraba'nın Araba arayüzünü uyguladığını söyler. Bu bir sözdür ve derleyici sözün tutulup tutulmadığını denetler: sınıf, arayüzdeki her metodu yazmak zorundadır. Şekil 4.2.14 iki tip arasındaki ilişkiyi UML ile gösterir.
SporAraba sınıfı Araba arayüzünü uygular. Kesik çizgi ve içi boş üçgen “uygular” ilişkisidir. Arayüzde yalnızca iki public metot, sınıfta ise ek olarak iki private alan ve bir yapıcı vardır.Arayüz ile sınıf aynı pakette aynı adı taşıyamaz. Sınıfa da Araba adı verilmeye çalışılırsa derleyici duplicate class: Araba hatası verir; bu yüzden sınıfın adı SporAraba'dır. main metodu değişkenini Araba tipinde tanımlar: Araba araba = new SporAraba("Rüzgar");. Böylece bu kod yalnızca arayüze bağımlıdır. Yarın SporAraba yerine başka bir gerçekleme kullanılsa yalnızca new ifadesi değişir.
Bir metot yazılmazsa ne olur? SporAraba yalnızca hareketEt()'i yazıp dur()'u unuttuğunda JDK 21 şu iletiyi verir:
ArayuzEksik.java:5: error: SporAraba is not abstract and does not override abstract method dur() in Araba
class SporAraba implements Araba {
^
1 errorİleti iki çıkış yolu da söyler: sınıf ya eksik metodu yazar ya da kendisi abstract olarak işaretlenir. Burada önemli olan, arayüzün sarmalamadaki “dış yüz” fikrini derleyicinin denetlediği bir yapıya dönüştürmesidir.
4.2.6.3Gerçeklemeyi değiştirmek
Sarmalamanın en güçlü kazancı, sınıfın iç yapısını kullananları bozmadan değiştirebilmektir. DijitalSaat bunu göstermek için iki sürümle yazılmıştır. Eski sürüm dakikayı ve saniyeyi iki ayrı alanda tutar:
public class DijitalSaat {
private int dakika = 0;
private int saniye = 0;
public int getDakika() {
return dakika;
}
public int getSaniye() {
return saniye;
}
public void setDakika(int dakika) {
if (dakika >= 0 && dakika < 60) {
this.dakika = dakika;
}
}
public void setSaniye(int saniye) {
if (saniye >= 0 && saniye < 60) {
this.saniye = saniye;
}
}
}Yeni sürüm ise tek bir alan kullanır: gece yarısından beri geçen saniye sayısı. Dakika ve saniye artık saklanmaz, her istendiğinde bu sayıdan hesaplanır:
public class DijitalSaat {
// gece yarısından beri geçen saniye
private int toplamSaniye = 0;
public int getDakika() {
return toplamSaniye / 60 % 60;
}
public int getSaniye() {
return toplamSaniye % 60;
}
public void setDakika(int dakika) {
if (dakika >= 0 && dakika < 60) {
int fark = dakika - getDakika();
toplamSaniye += fark * 60;
}
}
public void setSaniye(int saniye) {
if (saniye >= 0 && saniye < 60) {
toplamSaniye += saniye - getSaniye();
}
}
}İki sürümün public metotları aynıdır: getDakika(), getSaniye(), setDakika(int), setSaniye(int). Kullanan kod hangisiyle çalıştığını bilemez. Yeni sürümdeki hesaplar biraz düşünmeyi gerektirir. toplamSaniye / 60 toplam dakika sayısını verir; % 60 bunun saat içindeki dakika kısmını ayırır. toplamSaniye % 60 ise saniye kısmıdır. Değiştiriciler yeni değeri doğrudan yazamaz, çünkü öteki kısmı korumak zorundadır. Bunun yerine eski değerle yeni değer arasındaki farkı ekler.
Aşağıdaki kullanım kodu yeni sürümle adım adım izlenir:
private int toplamSaniye = 0; // tek alan
public int getSaniye() {
return toplamSaniye % 60;
}
public void setSaniye(int saniye) {
if (saniye >= 0 && saniye < 60)
toplamSaniye += saniye - getSaniye();
}
// getDakika(): toplamSaniye / 60 % 60
DijitalSaat s = new DijitalSaat(); // main
s.setDakika(12);
s.setSaniye(45);
s.setSaniye(75);
s.setSaniye(9);
System.out.println(s.getDakika() + ":"
+ s.getSaniye());| Adım | Satır | Değişkenler | Yeni çıktı |
|---|---|---|---|
| 1 | 10: DijitalSaat s = new DijitalSaat(); // main | toplamSaniye=0 | — |
| 2 | 11: s.setDakika(12); | toplamSaniye=720 | — |
| 3 | 12: s.setSaniye(45);5: public void setSaniye(int saniye) {6: if (saniye >= 0 && saniye < 60) | saniye=45, toplamSaniye=720 | — |
| 4 | 7: toplamSaniye += saniye - getSaniye();2: public int getSaniye() {3: return toplamSaniye % 60; | saniye=45, getSaniye()=0, toplamSaniye=765 | — |
| 5 | 13: s.setSaniye(75);6: if (saniye >= 0 && saniye < 60) | saniye=75, toplamSaniye=765 | — |
| 6 | 14: s.setSaniye(9);7: toplamSaniye += saniye - getSaniye(); | saniye=9, getSaniye()=45, toplamSaniye=729 | — |
| 7 | 15: System.out.println(s.getDakika() + ":"16: + s.getSaniye()); | getDakika()=12, getSaniye()=9 | 12:9 |
Başta toplamSaniye 0'dır; dışarıdan bakan için saat 00:00'dır. setDakika(12) çağrısında getDakika() 0 döndürür, fark 12 dakikadır ve 12 · 60 = 720 saniye eklenir. Eski sürüm aynı çağrıda yalnızca dakika = 12 yazardı. setSaniye(45) çağrısında önce getSaniye() = 720 % 60 = 0 hesaplanır; fark 45'tir ve toplam 765 olur. Dışarıdan bakan için saat 12:45'tir.
setSaniye(75) çağrısı iki sürümde de reddedilir; kural (0–59) sınıfın içinde kalmıştır. setSaniye(9) çağrısında fark negatiftir: 9 − 45 = −36. Toplam 765 − 36 = 729 olur ve dakika kısmı yine 12'dir. Şekil 4.2.15 bu iki anı iki sürümle birlikte gösterir.
toplamSaniye) ve ondan hesaplanan değerler, alt kutusunda eski sürümün iki alanı vardır. Solda setSaniye(45) sonrası (765 ↔ 12 ve 45), sağda setSaniye(9) sonrası (729 ↔ 12 ve 9).Son satır 12:9 yazar; eski sürüm de aynısını verirdi. JDK 21 ile iki sürüm aynı çağrılarla yan yana çalıştırıldığında her adımda aynı dakika ve saniye değerleri bulunur: 12:45 ve 12:9. İç temsil değişti, main'in tek harfi değişmedi. Çıktıdaki 12:9 yazımı biçimlendirme yapılmadığı içindir; printf("%d:%02d", ...) ile 12:09 elde edilir.
Bu değişikliğin bir bedeli de vardır. Eski sürümde getDakika() yalnızca bir alan okur. Yeni sürümde bir bölme ve bir kalan işlemi yapar. İkisi de O(1)'dir; fark önemsizdir. Ama değiştiriciler artık önce erişimciyi çağırır. Tasarımcı bu tür takasları kullanıcıya hiç yansıtmadan yapabilir. Sarmalamanın sağladığı özgürlük budur.
4.2.6.4Tasarım önerileri
Sarmalanmış bir sınıf yazarken dört öneri izlenir:
- Sınıf içindeki bütün alanlar
privatetanımlanır. - Alanlara erişim için gerektiği kadar
publicmetot sağlanır. Her alana otomatik olarak değiştirici yazmak şart değildir. - Her metodun nasıl kullanılacağını belirten yorumlar yazılır: ne yapar, ön ve son koşulları nelerdir.
- Yalnızca sınıf içinde kullanılan yardımcı metotlar
privatetanımlanır.
Dördüncü öneri alanlar kadar metotlar için de sarmalamanın geçerli olduğunu hatırlatır:
public class Ogrenci {
private int not;
private boolean gecerliMi(int deger) {
return deger >= 0 && deger <= 100;
}
public void setNot(int not) {
if (gecerliMi(not)) this.not = not;
}
}gecerliMi sınıfın iç işidir. setNot onu kullanır, ama dışarıya açılması için bir neden yoktur. Başka bir sınıftan o.gecerliMi(50) çağrıldığında derleyici şu hatayı verir:
Dis.java:4: error: gecerliMi(int) has private access in Ogrenci
boolean b = o.gecerliMi(50);
^
1 errorYardımcı metodu private tutmak, sınıfın dış yüzünü küçük tutar. Dış yüzdeki her metot bir sözdür ve sonradan geri alınamaz. private metot ise yazarın istediği zaman değiştirebileceği, silebileceği bir ayrıntıdır.
4.2.6.5Sor değil, söyle
Her alana bir erişimci ve bir değiştirici yazmak sarmalama değildir. Kurallar yine sınıfın dışında uygulanıyorsa veri yalnızca bir adım uzağa taşınmıştır. Aşağıdaki iki kod parçası farkı gösterir. Birincisinde kullanan kod, nesneye verisini sorar ve kararı kendisi verir:
// her para çeken yerde aynı kontrol
if (h.getBakiye() >= 300) {
h.setBakiye(h.getBakiye() - 300);
}
// ...ve not ekleyen her yerde
o.setNot(Math.min(100, o.getNot() + 5));Bu yazımda kural, nesneyi kullanan her yerde tekrar eder. Biri unutursa nesne bozulur. Üstelik setBakiye yazıldığı anda herkes bakiyeyi istediği değere çekebilir; sınıfın değişmezi korumasız kalır. İkinci yazımda kullanan kod, nesneye ne istediğini söyler:
h.paraCek(300); // kural BankaHesabi'nda
o.notEkle(5); // 100 sınırı Ogrenci'deKararı, veriyi tutan nesne verir. 300 TL'lik çekimin yapılıp yapılmayacağına BankaHesabi karar verir; notun 100'ü aşmamasını Ogrenci sağlar. Değiştirici ancak alanın gerçekten serbestçe değişebildiği durumlarda yazılır. BankaHesabi'nda bu yüzden setBakiye yoktur.
Bir metodun nereye ait olduğunu bulmak için basit bir soru yeter: “Bu kararı vermek için gereken veri kimde?” Cevap nesnenin kendisiyse, karar da onun metodunda olmalıdır.
4.2.6.6Sarmalamanın kazançları
Notun buraya kadarki örnekleri sarmalamanın dört kazancını somut olarak göstermiştir:
- Soyutlama. Nesne ile kullanıcısı arasındaki ilişki sadeleşir; kullanıcı yalnızca metotları bilir.
islem.veriEkle("Ayşe")tek satırdır. - Koruma. Nesne, kullanıcının istenmeyen ya da hatalı erişimlerine karşı korunur.
s.saniye = 75;derlenmez,setSaniye(75)reddedilir. - Değiştirilebilirlik. Arayüz değiştirilmeden sınıfın iç yapısı değiştirilebilir; kullanan kod etkilenmez. İki alanlı saat, tek alanlı saate dönüştü ve
mainaynı kaldı. - Geçerli durum. Nesnenin durumu her zaman kurallara uyar. Yalnızca pozitif koordinatlı noktalar, 0–59 arasında saniye ve dakika, negatif olmayan saat ya da bakiye.
paraCekbakiyeyi asla eksiye düşürmez.
Bu kazançların büyük ölçekteki sonucu Şekil 4.2.16'de görülür. Bir programda BankaHesabi'nı kullanan birçok sınıf olabilir: banka işlemleri, raporlar, testler. Hepsi yalnızca arayüze bağlıdır.
Banka, Rapor ve Test sınıfları yalnızca arayüze (paraYatir(), paraCek(), getBakiye()) bağlıdır; arayüz kararlıdır. Gerçekleme (özel alanlar ve yardımcı metotlar) arayüzün arkasındadır ve değişebilir.Arayüzler gerçeklemeye göre çok daha seyrek değişir; bu, kodun kararlı olmasını sağlar. Gerçekleme değiştiğinde ona doğrudan bağımlı olan kodların da değişmesi gerekir. Gerçeklemeyi gizlemek bu bağımlılığı ortadan kaldırır ve değişiklik sınıfın içinde kalır. Bu duruma gevşek bağlılık (loose coupling) denir. Gevşek bağlı bir programda bir sınıfı değiştirmek, ötekilere dokunmayı gerektirmez.
4.2.7Değiştirilemez sınıflar
Alanları private yapmak ve değiştirici yazmamak, sarmalamanın tamamlandığı anlamına gelmez. Alan bir dizi ya da liste gibi değiştirilebilir bir nesneyi gösteriyorsa, o nesneye giden referans dışarı sızabilir. Bu kesim sızıntının nasıl oluştuğunu, nasıl kapatıldığını ve hiç değişmeyen nesnelerin neden bu sorunu baştan çözdüğünü anlatır.
4.2.7.1Sızan referans
Karne sınıfı notları bir dizide tutar. Alan private final'dır ve değiştirici yoktur. İlk bakışta notlar korunmuş görünür:
public class Karne {
private final int[] notlar;
public Karne(int[] notlar) {
this.notlar = notlar; // aynı dizi
}
public int[] getNotlar() {
return notlar; // iç dizi
}
public double ortalama() {
int t = 0;
for (int n : notlar) t += n;
return (double) t / notlar.length;
}
}int[] dizi = {70, 80, 90};
Karne k = new Karne(dizi);
dizi[0] = 0; // dışarıdaki dizi
k.getNotlar()[1] = -100; // getter'dan gelen
System.out.println(k.ortalama());-3.3333333333333335Notlar dışarıdan değişmiştir; üstelik −100 gibi geçersiz bir not içeri girmiştir. Bunun nedeni, dizinin bir nesne olmasıdır. Yapıcı ve erişimci diziyi değil, yalnızca referansını paylaşır. Aynı diziye üç ayrı yerden ulaşılabilir: main'deki dizi değişkeninden, Karne'nin alanından ve erişimcinin döndürdüğü referanstan. Tablo 4.2.11 bu durumun nasıl oluştuğunu adım adım gösterir.
| Adım | Satır | Çağrı yığını | Değişkenler | Yeni çıktı |
|---|---|---|---|---|
| 1 | 8: int[] dizi = {70, 80, 90}; | main() | dizi=→[70, 80, 90] | — |
| 2 | 9: Karne k = new Karne(dizi);2: this.notlar = notlar; | main() → Karne(notlar) | dizi=→[70, 80, 90], k=→Karne | — |
| 3 | 10: dizi[0] = 0; | main() | dizi=→[0, 80, 90], k=→Karne | — |
| 4 | 11: k.getNotlar()[1] = -100;5: return notlar; | main() | dizi=→[0, -100, 90], k=→Karne | — |
| 5 | 12: System.out.println(k.ortalama()); | main() | dizi=→[0, -100, 90], k=→Karne | -3.3333333333333335 |
Birinci adımda dizi heap'te oluşur; dizi değişkeni yalnızca onun referansını tutar. İkinci adımda yapıcı parametredeki referansı alana yazar. Artık Karne'nin private alanı ile main'in dizi değişkeni aynı diziyi gösterir. Yapıcı referansı kopyalamıştır, diziyi değil.
Üçüncü adımda dizi[0] = 0 çalışır. Bu satır Karne'nin hiçbir metodundan geçmez, ama Karne'nin gördüğü not değişir. Dördüncü adımda getNotlar() iç dizinin kendisini döndürür ve dönen referansla −100 yazılır. Geçersiz not hiçbir denetimden geçmeden nesneye girmiştir (Şekil 4.2.17).
notlar parametresi ve Karne'nin alanı, main'deki dizi ile aynı int[3] nesnesini gösteriyor. Sağda erişimci iç dizinin referansını vermiş ve [1] hücresine −100 yazılmış.Son adımda ortalama (0 − 100 + 90) / 3 ≈ −3,33 bulunur. Java bu değeri -3.3333333333333335 olarak yazar; son basamaktaki 5, double'ın ikili gösteriminden gelir. private yazmak yetmedi: iç nesneye giden referans dışarı sızdı.
final sözcüğü de bu durumu önlemez. final yalnızca referansın başka bir diziyi göstermesini engeller; dizinin içeriğini korumaz. Sınıfın içinde notlar[0] = 0; sorunsuz derlenir, notlar = new int[2]; ise cannot assign a value to final variable notlar hatası verir. Değişmeyen, kutudaki referanstır; referansın gösterdiği nesne değil.
4.2.7.2Savunmacı kopya
Savunmacı kopya (defensive copy), değiştirilebilir bir nesnenin sınıfa girerken ve sınıftan çıkarken kopyalanmasıdır. İç nesneye ulaşan tek referans, sınıfın kendi alanı olarak kalır.
public class Karne {
private final int[] notlar;
public Karne(int[] notlar) {
this.notlar = notlar.clone(); // girişte
}
public int[] getNotlar() {
return notlar.clone(); // çıkışta
}
// ortalama() aynı
}Aynı main metoduyla bu sürüm şunu yazar:
80.0Yapıcıdaki notlar.clone(), çağıranın elindeki diziyi sonradan değiştirmesine karşı korur. dizi[0] = 0 artık yalnızca main'in dizisini değiştirir; Karne'nin kopyası 70'te kalır. Erişimcideki kopya ise dönen dizinin değiştirilmesine karşı korur. k.getNotlar()[1] = -100 geçici bir kopyaya yazar ve o kopya hemen kaybolur. Ortalama (70 + 80 + 90) / 3 = 80.0'dır.
Listeler için iki seçenek vardır. new ArrayList<>(liste) bağımsız bir kopya üretir. Collections.unmodifiableList(liste) ise kopyalamaz; aynı listeye salt okunur bir görünüm verir. İkisinin farkı JDK 21 ile şöyle görülür:
import java.util.*;
public class Liste {
public static void main(String[] args) {
List<Integer> ic = new ArrayList<>(List.of(70, 80));
List<Integer> gorunum = Collections.unmodifiableList(ic);
List<Integer> kopya = new ArrayList<>(ic);
ic.add(90);
System.out.println(gorunum + " " + kopya);
try {
gorunum.add(100);
} catch (UnsupportedOperationException e) {
System.out.println(e.getClass().getName());
}
List<Integer> sabit = List.copyOf(ic);
ic.set(0, 0);
System.out.println(sabit + " " + gorunum);
}
}[70, 80, 90] [70, 80]
java.lang.UnsupportedOperationException
[70, 80, 90] [0, 80, 90]İç listeye 90 eklendikten sonra görünüm [70, 80, 90]'ı, kopya ise [70, 80]'i gösterir. Görünüm, iç listenin değişikliklerini yansıtır; yalnızca görünüm üzerinden değişiklik yapılamaz. gorunum.add(100) bir UnsupportedOperationException fırlatır. Java 10'dan beri bulunan List.copyOf ise değiştirilemez bir kopya üretir: iç listenin ilk elemanı 0 yapıldığında kopya etkilenmez.
String ve Integer gibi değiştirilemez nesneler kopyalanmaz. Onları değiştirmenin bir yolu olmadığı için paylaşılmaları zararsızdır. Kopyalamanın bir maliyeti de vardır: n elemanlı bir dizinin kopyası O(n) zaman ve O(n) ek bellek ister. Erişimci her çağrıldığında kopya üretildiği için sık çağrılan büyük dizilerde bu maliyet birikir. Böyle durumlarda salt okunur bir görünüm ya da baştan değiştirilemez bir tip seçmek daha ucuzdur.
4.2.7.3Değiştirilemez sınıf tasarlamak
Değiştirilemez (immutable) bir nesne, oluşturulduktan sonra durumu hiç değişmeyen nesnedir. String bunun en bilinen örneğidir.
Sızan referans sorununu kökünden çözmenin yolu, nesnenin hiç değişmemesidir. Aşağıdaki Para sınıfı tutarı kuruş cinsinden bir long ile tutar:
public final class Para { // alt sınıf yok
private final long kurus; // bir kez atanır
private final String birim;
public Para(long kurus, String birim) {
this.kurus = kurus;
this.birim = birim;
}
public Para ekle(Para diger) { // setter yerine
if (!birim.equals(diger.birim))
throw new IllegalArgumentException("Birim farklı");
return new Para(kurus + diger.kurus, birim);
}
@Override public String toString() {
return kurus / 100 + ","
+ String.format("%02d", kurus % 100) + " " + birim;
}
}Değiştirilemez bir sınıf beş kurala uyar:
- Değiştirici yoktur; durum yalnızca yapıcıda kurulur.
- Alanlar
private final'dır. - Sınıf
final'dır, böylece değiştirilebilir bir alt sınıf yazılamaz. - “Değiştiren” metotlar nesneyi değiştirmez, yeni bir nesne döndürür.
- Değiştirilebilir alanlar varsa savunmacı kopya yapılır.
Her kuralı derleyici de destekler. final bir alana yapıcı dışında değer atanmaya çalışıldığında, örneğin kurus += k; yazıldığında, cannot assign a value to final variable kurus hatası çıkar. final sınıftan türetme denemesi ise cannot inherit from final Para hatası verir. Üçüncü kural gereksiz görünebilir. Ama alt sınıf yazılabilseydi, o alt sınıf kendi değiştirilebilir alanlarını ekleyip Para yerine kullanılabilirdi.
Değiştirilemez sınıfın kazançları büyüktür. Değişmez yalnızca yapıcıda denetlenir; sonra hiç bozulamaz. Nesne kopyalanmadan paylaşılabilir; savunmacı kopya gerekmez. HashMap gibi yapılarda anahtar olarak güvenle kullanılabilir, çünkü anahtar değişip tablodaki yerini kaybetmez. İş parçacıkları arasında da güvenlidir; aynı nesneyi okuyan iki iş parçacığı birbirini rahatsız edemez. String ve record bu kuralların hazır örnekleridir.
toString metodu tutarı TL ve kuruş olarak yazar: kurus / 100 tam lira kısmını, kurus % 100 kuruş kısmını verir. String.format("%02d", ...) kuruşu iki basamağa tamamlar; 5 kuruş 0,05 TL olarak görünür. Bu yazım negatif tutarlarda bozulur: new Para(-50, "TL") 0,-50 TL yazar, çünkü −50 / 100 = 0 ve −50 % 100 = −50'dir. Sınıf negatif tutara izin verecekse toString'in bu durumu ayrıca ele alması gerekir.
4.2.7.4Para sınıfını izlemek
public Para ekle(Para diger) {
if (!birim.equals(diger.birim))
throw new IllegalArgumentException(
"Birim farklı");
return new Para(kurus + diger.kurus, birim);
}
// main içinde:
Para a = new Para(1250, "TL");
a.ekle(new Para(100, "TL")); // sonuç atıldı
Para b = a.ekle(new Para(375, "TL"));
System.out.println(a + " | " + b);| Adım | Satır | Çağrı yığını | Değişkenler | Yeni çıktı |
|---|---|---|---|---|
| 1 | 8: Para a = new Para(1250, "TL"); | main() | a=12,50 TL | — |
| 2 | 9: a.ekle(new Para(100, "TL")); // sonuç atıldı5: return new Para(kurus + diger.kurus, birim); | main() → a.ekle(diger) | a=12,50 TL | — |
| 3 | 9: a.ekle(new Para(100, "TL")); // sonuç atıldı | main() | a=12,50 TL | — |
| 4 | 10: Para b = a.ekle(new Para(375, "TL"));5: return new Para(kurus + diger.kurus, birim); | main() | a=12,50 TL, b=16,25 TL | — |
| 5 | 11: System.out.println(a + " | " + b); | main() | a=12,50 TL, b=16,25 TL | 12,50 TL | 16,25 TL |
Birinci adımda ilk nesne kurulur: 1250 kuruş, yani 12,50 TL. Alanları final olduğu için bundan sonra hiçbir metot onları değiştiremez. İkinci adımda a.ekle(new Para(100, "TL")) çağrılır. Argüman olarak ikinci bir nesne (#2) kurulur. ekle, a'yı değiştirmez; toplamı taşıyan yeni bir nesne (#3) kurup döndürür.
Üçüncü adım en sık yapılan hatayı gösterir. Dönen değer bir değişkene atanmamıştır; #3 kaybolur ve a değişmemiştir (Şekil 4.2.18). Derleyici bu satır için uyarı vermez; değer döndüren bir metodun sonucunu yok saymak Java'da serbesttir. Dördüncü adımda sonuç b'ye atanır: 1250 + 375 = 1625 kuruş. Argüman olarak kurulan #4 de artık kullanılmaz. Son adımda a ilk değerini korumuştur.
ekle'nin sonucu atılmış: #3 nesnesine (1350 kuruş) hiçbir değişken bağlı değil, a hâlâ 12,50 TL. Sağda programın sonu: a 1250 kuruşu, b 1625 kuruşu gösteriyor; iki ayrı, değişmeyen nesne.12,50 TL | 16,25 TLString'in metotları da aynı biçimde çalışır. Aşağıdaki deneme Para sınıfının öteki davranışlarını da gösterir:
public class ParaDeneme {
public static void main(String[] args) {
String s = "java";
s.toUpperCase();
System.out.println(s);
s = s.toUpperCase();
System.out.println(s);
Para a = new Para(1250, "TL");
try {
a.ekle(new Para(100, "USD"));
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
System.out.println(new Para(5, "TL") + " " + new Para(-50, "TL"));
}
}java
JAVA
Birim farklı
0,05 TL 0,-50 TLs.toUpperCase(); tek başına s'yi değiştirmez ve java yazılır; s = s.toUpperCase(); yazıldığında JAVA elde edilir. Farklı birimlerin toplanması Birim farklı iletisiyle reddedilir. Son satır, toString'in 5 kuruşu doğru, −50 kuruşu ise yanlış yazdığını gösterir. Doğru kalıp her zaman aynıdır: a = a.ekle(p);.
Değiştirilemez nesnenin metodunu değiştirici sanmak. a.ekle(p);, s.toUpperCase();, s.trim(); gibi satırlar tek başına hiçbir şeyi değiştirmez. Metot yeni bir nesne döndürür ve sonuç atanmazsa kaybolur. Derleyici uyarmaz; hata ancak çıktıda görülür.
4.2.8Paketler
Sarmalama tek bir sınıfın sınırında durmaz. Büyük bir program yüzlerce sınıftan oluşur ve bu sınıfların bir kısmı yalnızca birkaç komşu sınıfın işine yarar. Paketler sınıfları gruplar, ad çakışmalarını önler ve sınıf düzeyinin bir üstünde ikinci bir sarmalama katmanı kurar.
4.2.8.1Paket ve klasör
Paket (package), bir klasör içinde gruplanmış sınıflar topluluğudur; klasörün adı paketin adıdır. Bir dosyanın hangi pakete ait olduğu, dosyanın ilk satırındaki package paket_adi; bildirimiyle belirtilir.
Paketlerle çalışırken birkaç kural geçerlidir. Her public sınıf ayrı bir dosyada, sınıfla aynı adı taşıyan dosyada tanımlanır. package bildirimi dosyanın ilk satırıdır; ondan önce yalnızca yorum gelebilir. İç içe klasörlerde / ya da \ yerine nokta kullanılır: okul/yonetim klasörü okul.yonetim paketidir. Paket adları küçük harfle yazılır. Java'nın kendi paketleri de böyle adlandırılmıştır: java.lang, java.util, java.io.
Aşağıdaki proje yapısında iki paket vardır. banka paketi hesap işlerini, uygulama paketi programın giriş noktasını içerir. Kaynak dosyalar src/, derlenmiş dosyalar out/ altındadır:
proje/
├── src/
│ ├── banka/
│ │ ├── Hesap.java // package banka;
│ │ └── Yardimci.java // package banka;
│ └── uygulama/
│ └── Ana.java // package uygulama;
└── out/ // derleme çıktısı
├── banka/
│ ├── Hesap.class
│ └── Yardimci.class
└── uygulama/
└── Ana.classBir sınıfın tam adı paketini de içerir: banka.Hesap, uygulama.Ana. Derleyici ve Java sanal makinesi paketleri sınıf yolu (classpath) köklerinden başlayarak arar. Paket adı bu kökten itibaren klasör yolunu söyler: banka.Hesap sınıfı, kökün altındaki banka/Hesap.class dosyasıdır. Varsayılan sınıf yolu, komutun çalıştırıldığı klasördür (.).
public sınıfı başka adlı dosyaya yazmak. public class Hesap, Banka.java dosyasına yazıldığında derleyici class Hesap is public, should be declared in a file named Hesap.java hatası verir. Dosya adı, public sınıfın adıyla büyük-küçük harf dahil birebir aynı olmalıdır.
4.2.8.2Paketli programı derlemek ve çalıştırmak
İki paketli küçük bir program yazalım. banka.Hesap sınıfı yatırma ve bakiye okuma işlerini yapar; uygulama.Ana bu sınıfı kullanır:
package banka;
public class Hesap {
private double bakiye;
public void yatir(double miktar) {
if (miktar > 0) {
bakiye += miktar;
}
}
public double getBakiye() {
return bakiye;
}
}package uygulama;
import banka.Hesap;
public class Ana {
public static void main(String[] args) {
Hesap h = new Hesap();
h.yatir(250.0);
System.out.println("Bakiye: " + h.getBakiye());
}
}Ana, başka bir paketteki Hesap'ı kısa adıyla kullanabilmek için import banka.Hesap; satırını yazar. Proje klasöründe derleme ve çalıştırma komutları şöyledir:
> javac -d out src/banka/Hesap.java src/uygulama/Ana.java
> java -cp out uygulama.Ana
Bakiye: 250.0-d out seçeneği, .class dosyalarını paket klasörleriyle birlikte out/ altına yazar. JDK 21 ile çalıştırıldığında out/banka/Hesap.class ve out/uygulama/Ana.class dosyaları oluşur. Proje yapısında görünen Yardimci.class ancak Yardimci.java da derleme komutuna eklenirse oluşur. -cp out ise sınıf yolunu out klasörü yapar. Çalıştırırken sınıfın dosya yolu değil, tam adı verilir: uygulama.Ana.
Bu ayrım yeni başlayanların en çok takıldığı noktadır. Aynı projede yanlış komutlar denendiğinde JDK 21 şu iletileri verir:
| Komut | Sonuç | Neden |
|---|---|---|
java -cp out uygulama.Ana | Bakiye: 250.0 | Sınıf yolu ve tam ad doğru. |
java -cp out Ana | ClassNotFoundException: Ana | Paket adı eksik; kökte Ana.class yok. |
java -cp out/uygulama Ana | NoClassDefFoundError: Ana (wrong name: uygulama/Ana) | Dosya bulundu, ama içindeki sınıfın adı uygulama.Ana. |
java -cp out/uygulama Ana.class | ClassNotFoundException: Ana.class | Sınıf adı yerine dosya adı verilmiş. |
Üçüncü satırdaki ileti öğreticidir. Java Ana.class dosyasını bulur, ama dosyanın içindeki sınıf kendini uygulama.Ana olarak tanıtır. Paket adı klasör yapısıyla uyuşmadığı için sınıf yüklenmez. Paket, yalnızca bir klasör değil, sınıfın adının bir parçasıdır.
4.2.8.3İçe aktarma (import)
İçe aktarma (import), başka bir paketteki sınıfı kısa adıyla kullanmayı sağlayan bildirimdir. import paket_adi.SinifAdi; tek bir sınıfı, import paket_adi.*; paketteki bütün sınıfları içe aktarır.
import zorunlu değildir; bir sınıf her zaman tam adıyla da yazılabilir: java.util.Random. import yalnızca uzun adı kısaltır. Bazı sınıflar ise hiç import gerektirmez. java.lang paketindeki sınıflar (String, Math, System, Integer) ve aynı paketteki sınıflar her dosyada otomatik olarak görünür.
Bir kaynak dosyada sıra bellidir: önce package, sonra import'lar, en son sınıf. import static biçimi ise bir sınıfın static üyesini sınıf adı yazmadan kullandırır. Aşağıdaki program üç yolu bir arada gösterir:
import java.util.Scanner;
import static java.lang.Math.sqrt;
public class ImportOrnek {
public static void main(String[] args) {
Scanner giris = new Scanner(System.in);
int sayi = giris.nextInt();
java.util.Random rastgele =
new java.util.Random(42);
System.out.println(sqrt(sayi) + " "
+ Math.max(sayi, 10) + " "
+ rastgele.nextInt(100));
}
}Girdi olarak 49 verildiğinde çıktı şöyledir:
7.0 49 30Scanner, import java.util.Scanner; sayesinde kısa adıyla kullanılır. java.util.Random ise import edilmeden tam adıyla yazılmıştır. sqrt, import static java.lang.Math.sqrt; sayesinde Math. yazılmadan çağrılır; 49'un karekökü 7.0'dır. Math.max ise java.lang'tan geldiği için import gerektirmez ve 49 ile 10'un büyüğünü verir. Son sayı, 42 tohumuyla başlatılan rastgele sayı üretecinin ilk nextInt(100) değeridir. Aynı tohum her çalıştırmada aynı diziyi ürettiği için bu değer her zaman 30'dur.
import unutulduğunda derleyici sınıfı tanımaz. Scanner'ı içe aktarmadan kullanan bir dosyada JDK 21, satırdaki iki Scanner geçişi için ayrı ayrı hata verir:
ScannerYok.java:3: error: cannot find symbol
Scanner giris = new Scanner(System.in);
^
symbol: class Scanner
location: class ScannerYok
ScannerYok.java:3: error: cannot find symbol
Scanner giris = new Scanner(System.in);
^
symbol: class Scanner
location: class ScannerYok
2 errorsÇözüm dosyanın başına import java.util.Scanner; eklemektir. import yalnızca derleme zamanına ait bir kolaylıktır; programın çalışmasını yavaşlatmaz ve .class dosyasına ek kod koymaz. import java.util.*; yazmak da paketteki bütün sınıfları programa “yüklemez”; derleyiciye yalnızca adları nerede arayacağını söyler.
4.2.8.4Paket erişimi
Erişim belirleyicisi yazılmayan sınıf ve üyeler paket erişimlidir: yalnızca kendi paketlerinden görülürler. Bu kural sınıflar için de geçerlidir. banka paketindeki Yardimci sınıfı public değildir:
package banka;
class Yardimci { // public değil
static double kdv(double tutar) {
return tutar * 0.20;
}
}package uygulama;
import banka.Yardimci;
public class Ana2 {
}uygulama/Ana2.java:3: error: Yardimci is not public in banka; cannot be accessed from outside package
import banka.Yardimci;
^
1 errorYardimci, banka paketinin iç işidir. Aynı paketteki Hesap onu kullanabilir, uygulama paketi kullanamaz. Paket de böylece bir sarmalama düzeyi olur. Sınıflar dışarıya kapatılır; yalnızca public olanlar paketin dış yüzünü oluşturur. Bir sınıf private alanlarla kendi içini sakladığı gibi, bir paket de public olmayan sınıflarla kendi içini saklar.
Bir sınıfı paket dışından kullanılabilir yapmak için yalnızca sınıfı public yapmak yetmeyebilir. Kullanılan yapıcı ve metotlar da public olmalıdır. okul.Ogrenci sınıfı public yapıldığı hâlde getAd() metodu belirleyicisiz kalmışsa, uygulama paketinden yapılan o.getAd() çağrısı getAd() is not public in Ogrenci; cannot be accessed from outside package hatası verir.
4.2.8.5Paket adlandırma ve alt paketler
Dünyada milyonlarca Java sınıfı yazılmıştır ve banka gibi basit bir paket adı başkasıyla kolayca çakışabilir. Çakışmayı önlemek için kurumlar paket adına ters çevrilmiş alan adıyla başlar. Giresun Üniversitesi'nin alan adı giresun.edu.tr olduğu için paket adı tr.edu.giresun.banka olur. Noktalı adlar klasörlere karşılık gelir: tr/edu/giresun/banka/.
Adların iç içe görünmesi bir erişim hakkı vermez. Alt paket ayrı bir pakettir. tr.edu.giresun.banka.rapor paketi, tr.edu.giresun.banka paketinin paket erişimli üyelerini göremez:
// tr/edu/giresun/banka/Yardimci.java
package tr.edu.giresun.banka;
class Yardimci { // paket erişimli
static double kdv(double t) { return t * 0.20; }
}// tr/edu/giresun/banka/rapor/Rapor.java
package tr.edu.giresun.banka.rapor;
import tr.edu.giresun.banka.Yardimci;
public class Rapor {
double hesapla() { return Yardimci.kdv(100); }
}tr/edu/giresun/banka/rapor/Rapor.java:3: error: Yardimci is not public in tr.edu.giresun.banka; cannot be accessed from outside package
import tr.edu.giresun.banka.Yardimci;
^
tr/edu/giresun/banka/rapor/Rapor.java:5: error: Yardimci is not public in tr.edu.giresun.banka; cannot be accessed from outside package
double hesapla() { return Yardimci.kdv(100); }
^
2 errorsAynı kural import için de geçerlidir. import tr.edu.giresun.banka.*; alt paketleri içe aktarmaz. import java.util.*; da java.util.function paketindeki sınıfları getirmez. Function arayüzü kullanılmak istendiğinde derleyici sınıfı bulamaz:
FunctionYok.java:5: error: cannot find symbol
Function<Integer, Integer> f = x -> x * 2;
^
symbol: class Function
location: class FunctionYok
1 errorÇözüm import java.util.function.Function; satırını ayrıca yazmaktır. Paket adı ile klasör yolu birebir eşleşmezse derleyici ve sanal makine sınıfı bulamaz. Geliştirme ortamları bu yüzden klasörleri paket adından kendileri oluşturur.
4.2.8.6Sınıf adı çakışmaları
Farklı paketlerdeki iki sınıf aynı adı taşıyabilir. Java kütüphanesinde bunun bilinen örneği java.util.Date ile java.sql.Date'tir. İki paket de * ile içe aktarılırsa Date adı belirsiz kalır:
import java.util.*;
import java.sql.*;
public class Belirsiz {
public static void main(String[] args) {
Date tarih = null;
}
}Belirsiz.java:6: error: reference to Date is ambiguous
Date tarih = null;
^
both class java.sql.Date in java.sql and class java.util.Date in java.util match
1 errorDerleyici iki sınıfı da bulur ve hangisinin kastedildiğini seçemez. Belirsizliği gidermek için paket adı kullanılır. Biri tek sınıflık import ile içe aktarılır, öteki tam adıyla yazılır:
import java.util.Date;
public class TarihOrnegi {
public static void main(String[] args) {
Date baslangic = new Date(0); // java.util
java.sql.Date kayit = // tam ad
java.sql.Date.valueOf("2024-09-16");
System.out.println(baslangic.getTime());
System.out.println(kayit);
}
}0
2024-09-16Tek sınıflık import java.util.Date;, * ile yapılan içe aktarmalardan önce gelir. Date artık yalnızca java.util.Date anlamına gelir. new Date(0), 1 Ocak 1970 gece yarısını (UTC) gösteren nesneyi kurar; getTime() bu andan beri geçen milisaniyeyi verdiği için 0 yazılır. java.sql.Date tam adıyla kullanılmıştır; valueOf metodu yyyy-aa-gg biçimindeki metinden tarih üretir ve aynı biçimde yazdırılır.
4.2.9Örnek problemler
Bu kesimdeki problemler sunumun örnek problemleri ve alıştırmalarıdır. Her birinin cevabı JDK 21 ile çalıştırılarak doğrulanmıştır.
Erişim metotlarıyla yazılmış DijitalSaat sınıfı kullanılıyor (saat 0–23, dakika ve saniye 0–59). Aşağıdaki iki kod parçası ne yazdırır? İkinci parçada kaç DijitalSaat nesnesi oluşturulur?
DijitalSaat t = new DijitalSaat();
t.setSaat(25);
t.setDakika(30);
t.setSaniye(-1);
t.setSaniye(59);
t.goster();DijitalSaat a = new DijitalSaat();
DijitalSaat b = a;
a.setDakika(15);
b.setDakika(45);
b = new DijitalSaat();
b.setDakika(10);
System.out.println(a.getDakika() + " " + b.getDakika());00:30:59
45 10İlk parçada setSaat(25) ve setSaniye(-1) değiştiricilerce reddedilir. Saat başlangıç değeri olan 0'da kalır; dakika 30, saniye ise son geçerli çağrıyla 59 olur. Sonuç 00:30:59'dur.
İkinci parçada b = a ataması yeni nesne oluşturmaz; iki değişken aynı nesneyi gösterir. a.setDakika(15) ve b.setDakika(45) aynı nesneye uygulanır ve dakika 45 olur. Sonra b yeni bir nesneye bağlanır ve onun dakikası 10 yapılır. a ilk nesneyi göstermeye devam eder. Çıktı 45 10'dur. Toplam iki nesne oluşturulmuştur, çünkü new iki kez çalışmıştır.
Aşağıdaki A–E satırlarından hangileri derleme hatası verir? Neden?
public class Test {
public static void main(String[] args) {
DijitalSaat s = new DijitalSaat();
s.setSaniye(30); // A
int x = s.saniye; // B
System.out.println(s.getSaniye()); // C
s.getSaniye() = 10; // D
s.setSaniye("40"); // E
}
}Test.java:5: error: saniye has private access in DijitalSaat
int x = s.saniye; // B
^
Test.java:7: error: unexpected type
s.getSaniye() = 10; // D
^
required: variable
found: value
Test.java:8: error: incompatible types: String cannot be converted to int
s.setSaniye("40"); // E
^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
3 errorsB satırı private alanı okumaya çalışır; doğrusu s.getSaniye()'dir. D satırında bir metot çağrısının sonucuna atama yapılmak istenmiştir. Metot çağrısı bir değerdir, değişken değildir; iletideki required: variable, found: value bunu söyler. Doğrusu s.setSaniye(10)'dur. E satırında "40" bir String'dir ve int parametreye otomatik dönüştürülmez. A ve C doğrudur, çünkü public metotları çağırır.
BankaHesabi sınıfıyla aşağıdaki kod ne yazdırır? Son satırda hangi bakiyeler görülür?
BankaHesabi h = new BankaHesabi(111, 1000.0, "Vadesiz");
h.paraCek(5000.0);
h.paraYatir(-50.0);
h.faizEkle(3.0);
h.paraCek(250.0);
BankaHesabi v = new BankaHesabi(222, 100.0, "Vadeli");
h.transfer(v, 1000.0);
h.transfer(v, 750.0);
System.out.println(h.getBakiye() + " " + v.getBakiye());Yetersiz bakiye.
Yatırılan miktar geçersiz.
Faiz yalnızca vadeli hesaplarda uygulanabilir.
250.0 TL çekildi. Kalan bakiye: 750.0
Yetersiz bakiye veya geçersiz miktar.
750.0 TL yatırıldı. Yeni bakiye: 850.0
750.0 TL transfer edildi. Yeni bakiye: 0.0
0.0 850.0Bakiye 1000.0'dır. 5000.0 bakiyeyi aştığı için ilk çekim reddedilir. −50.0 geçersiz bir yatırma miktarıdır. Hesap vadesiz olduğu için faiz eklenmez. 250.0'lık çekim geçerlidir ve bakiye 750.0'a iner. İkinci hesap vadeli ve 100.0 bakiyelidir. 1000.0'lık transfer bakiyeyi aştığı için reddedilir. 750.0'lık transfer ise tam bakiye kadardır ve miktar ≤ bakiye koşulunu sağlar. Önce hedef hesabın mesajı (850.0), sonra transferin mesajı (0.0) yazılır. Son satır 0.0 850.0'dır.
(a) Kopyalamayan Karne ile aşağıdaki ilk kod ne yazdırır? Savunmacı kopyalı Karne ile ne yazdırır? (b) Para sınıfıyla ikinci kod ne yazdırır? (c) On kez arttir() çağıran Sayac programı -ea olmadan ve -ea ile çalıştırılırsa ne olur? (d) Kesir dışındaki bir sınıfta new Kesir(1, 2).pay derlenir mi?
int[] d = {50, 60};
Karne k = new Karne(d);
int[] g = k.getNotlar();
g[0] = 100;
d[1] = 100;
System.out.println(k.ortalama());Para p = new Para(100, "TL");
p.ekle(p);
p = p.ekle(p);
p.ekle(new Para(1, "TL"));
System.out.println(p);(a) Kopyalamayan sürümde d, g ve alan aynı diziyi gösterir. İki atama da nesneye ulaşır: dizi {100, 100} olur ve ortalama 100.0'dır. Savunmacı kopyalı sürümde iki atama da kopyalara yapılır; nesnenin dizisi {50, 60} kalır ve ortalama 55.0'dır. JDK 21 iki sürümde de bu değerleri verir.
(b) p.ekle(p) ve p.ekle(new Para(1, "TL")) çağrılarının sonuçları atılır. Yalnızca p = p.ekle(p) saklanır: 100 + 100 = 200 kuruş. Çıktı 2,00 TL'dir. Bir nesnenin kendisini argüman olarak alması da sorun çıkarmaz, çünkü ekle hiçbir nesneyi değiştirmez.
(c) -ea olmadan assert satırı çalışmaz ve program bitti yazar; hata görünmez. -ea ile onuncu artırmada değer 10 olur ve AssertionError: deger=10 fırlatılır. Hatayı yalnızca ikinci çalıştırma gösterir.
(d) Hayır. Derleyici pay has private access in Kesir hatası verir. private erişim yalnızca Kesir'in kendi kodunda serbesttir.
Dört küçük sınıf yazalım. Ogrenci: private ad ve not; not yalnızca 0–100 arasında kabul edilsin; gectiMi() not ≥ 50 ise true döndürsün. Nokta: yalnızca negatif olmayan koordinatları kabul etsin; uzaklik() orijine uzaklığı döndürsün. Termometre: santigrat değeri saklasın, −273,15'in altını reddetsin; getFahrenheit() ayrı bir alan olmadan hesaplasın. Sayac: yalnızca arttir(), sifirla() ve getDeger() metotları olsun.
public class Ogrenci {
private String ad;
private int not;
public Ogrenci(String ad) { this.ad = ad; }
public void setNot(int not) {
if (not >= 0 && not <= 100) this.not = not;
}
public int getNot() { return not; }
public boolean gectiMi() { return not >= 50; }
}public class Nokta {
private double x, y;
public Nokta(double x, double y) { setX(x); setY(y); }
public void setX(double x) { if (x >= 0) this.x = x; }
public void setY(double y) { if (y >= 0) this.y = y; }
public double getX() { return x; }
public double uzaklik() { return Math.sqrt(x * x + y * y); }
}public class Termometre {
private double santigrat;
public Termometre(double c) { setSantigrat(c); }
public void setSantigrat(double c) { if (c >= -273.15) santigrat = c; }
public double getSantigrat() { return santigrat; }
public double getFahrenheit() { return santigrat * 9 / 5 + 32; }
}public class Sayac {
private int deger;
public void arttir() { deger++; }
public void sifirla() { deger = 0; }
public int getDeger() { return deger; }
}public class Alistirma {
public static void main(String[] args) {
Ogrenci o = new Ogrenci("Ece");
o.setNot(120);
System.out.println(o.getNot() + " " + o.gectiMi());
o.setNot(75);
System.out.println(o.getNot() + " " + o.gectiMi());
Nokta n = new Nokta(3, 4);
System.out.println(n.uzaklik());
n.setX(-7);
System.out.println(n.getX());
Termometre t = new Termometre(25);
System.out.println(t.getFahrenheit());
t.setSantigrat(-300);
System.out.println(t.getSantigrat());
Sayac s = new Sayac();
s.arttir(); s.arttir(); s.arttir();
System.out.print(s.getDeger() + " ");
s.sifirla();
System.out.println(s.getDeger());
}
}0 false
75 true
5.0
3.0
77.0
25.0
3 0setNot(120) reddedilir ve not 0'da kalır: 0 false. setNot(75) sonrasında 75 true yazılır. Nokta'nın yapıcısı değiştiricileri çağırır; böylece kural tek yerde kalır. new Nokta(3, 4) için uzaklık √(9 + 16) = 5.0'dır. setX(-7) reddedilir ve x 3.0'da kalır. 25 °C, 25 × 9 / 5 + 32 = 77.0 °F'tır. Türetilen değer ayrı bir alanda tutulmaz; tutulsaydı iki alanın her değişiklikte uyumlu tutulması gerekirdi. setSantigrat(-300) sonrasında değer 25.0 kalır.
Sayac'ta değiştirici yoktur, çünkü sayacın değeri yalnızca birer birer artmalı ya da sıfırlanmalıdır. setDeger bu kuralı delerdi. Üç arttir() çağrısı değeri 3 yapar; sifirla() 0'a döndürür.
Yapıcının değiştirici çağırmasıyla ilgili küçük bir not: javac -Xlint:all ile derlendiğinde JDK 21 Nokta ve Termometre için possible 'this' escape uyarısı verir. Uyarı, bir alt sınıfın bu metotları değiştirip yarım kurulmuş nesneyi görebileceğini söyler. Sınıf final yapılırsa ya da çağrılan metotlar final ya da private olursa uyarı kaybolur.
(a) Ogrenci sınıfı okul paketine taşınıp uygulama.Ana'dan kullanılacak. Hangi satırlar eklenir; derleme ve çalıştırma komutları nelerdir? (b) BankaHesabi'na yalnızca erişimcisi olan bir islemSayisi alanı eklenecek; başarılı her yatırma ve çekmede bir artacak. İki hesaplı izleme örneğinin sonunda iki hesabın işlem sayısı kaçtır?
(a) Ogrenci.java dosyasının başına package okul;, Ana.java'nın başına package uygulama; ve import okul.Ogrenci; yazılır. Dosyalar src/okul/ ve src/uygulama/ klasörlerine konur. Komutlar: javac -d out src/okul/Ogrenci.java src/uygulama/Ana.java ve java -cp out uygulama.Ana. Ogrenci sınıfının ve kullanılan metotlarının public olması gerekir.
1 2(b) Sayaç paraYatir ve paraCek içinde, yalnızca başarılı dallarda artırılır. hesap1'in sayacı 1'dir: yalnızca paraYatir(500.0) sayılır. faizEkle bir yatırma değildir; transfer ise kendi bakiyesinden doğrudan düşer, kendi paraCek'ini çağırmaz. hesap2'nin sayacı 2'dir: paraCek(200.0) ve transferin içindeki paraYatir. Bu sonuç, transferin kendi tarafındaki işlemi paraCek üzerinden yapmamasının bir yan etkisini de gösterir. Kurallar tek yerde toplanmak isteniyorsa transfer da paraCek'i çağırmalıdır.
4.2.10Sık yapılan hatalar
Sarmalamayla ilgili hataların bir kısmını derleyici yakalar ve iletisiyle yerini gösterir. Ötekiler derlenen ama nesneyi sessizce bozan programlara yol açar. İkinci gruptakiler daha tehlikelidir.
private alana dışarıdan erişmek. İleti: saniye has private access in DijitalSaat. s.saniye = 75; yerine s.setSaniye(75); yazılır; okumak için s.getSaniye() kullanılır.
Değiştiricide this'i unutmak. en = en; derlenir, ama parametreyi kendisine atar; alan değişmez ve getEn() 0 döndürür. Doğrusu this.en = en;'dir.
Erişimciyi void yazmak. public void getEn() { return en; } satırı incompatible types: unexpected return value hatası verir. void metot değer döndüremez; dönüş tipi alanın tipi, yani int olmalıdır.
Değiştiriciyi doğrulamasız yazmak. public void setSaniye(int s) { saniye = s; } alanı public yapmaktan farksızdır. Geçerlilik kuralı değiştiricide denetlenir.
Metot adını parantezsiz yazmak. int d = s.getDakika; satırında derleyici getDakika adlı bir alan arar: cannot find symbol ... variable getDakika. Metot çağrısı her zaman parantezle yazılır.
Arayüz metodunu yazmamak. İleti: SporAraba is not abstract and does not override abstract method dur() in Araba. implements diyen sınıf arayüzdeki bütün metotları yazmalıdır.
İç nesneyi erişimciyle vermek. return notlar; değiştirilebilir diziyi ya da listeyi dışarı açar. Kopya ya da salt okunur görünüm döndürülür. Yapıcıda gelen dizi de kopyalanır.
final alanı değiştirilemez sanmak. private final int[] notlar yazıldığında sabit olan referanstır, dizinin elemanları değil. notlar[0] = 0; derlenir.
Yapıcıda doğrulamamak. Değiştiriciler denetlese de new BankaHesabi(1, -500.0, ...) geçersiz bir nesne kurar. Yapıcı da kuralı uygulamalıdır.
import'u unutmak ya da alt paketin içe aktarıldığını sanmak. İleti: cannot find symbol ... class Scanner. import java.util.*; sonrasında Function için de aynı hata çıkar; java.util.function ayrıca içe aktarılır.
Paket erişimli sınıfı dışarıdan kullanmak. İleti: Yardimci is not public in banka; cannot be accessed from outside package. Sınıf ya public yapılır ya da yalnız aynı paketten kullanılır. Kullanılan metotların da public olması gerekir.
Her alana değiştirici yazmak. Kural kullanan koda kayar. Önce davranış (paraCek, notEkle) tasarlanır; çoğu zaman değiştirici gerekmez.
Hatayı sessizce yutmak. Geçersiz değeri hiçbir iz bırakmadan yok saymak hatayı gizler. Davranış ya sözleşmede açıkça belirtilir ya da istisna fırlatılır.
4.2.11Özet
- Sarmalama, verinin ve onu işleyen metotların bir sınıfta toplanması ve verinin dışarıya kapatılmasıdır. Java'da alanlar
private, davranışlarpublicyazılır. - Bilgi saklama bir işin “ne”sini “nasıl”ından ayırır. Kullanıcı yalnızca metot imzasını ve sözleşmesini bilir;
islem.veriEkle("Ayşe")çağrısının arkasındaki adımları bilmez. - Ön koşul çağrıdan önce doğru olduğu varsayılan, son koşul çağrıdan sonra garanti edilen durumdur. İkisi metodun sözleşmesidir ve Javadoc'ta
@param,@return,@throwsetiketleriyle yazılır. Yorumdaki<işaretleri{@code ...}içine alınır. - Ön koşul bozulduğunda metot değeri yok sayabilir, durum döndürebilir ya da istisna fırlatabilir.
publicmetotlarda alışılmış seçimIllegalArgumentException'dır. - Sınıf değişmezi her nesnenin her an sağladığı kuraldır.
assertdeğişmezi geliştirme sırasında denetler, ama yalnızca-eaile çalışır; argüman denetimi için kullanılmaz. - Dört erişim düzeyi vardır:
public(her yer),protected(paket ve alt sınıflar), paket erişimi (yalnız paket) veprivate(yalnız sınıf). En kısıtlayıcı düzey tercih edilir.privatedenetimi nesneye göre değil, sınıfa göre yapılır. - Erişimci alanı döndürür, değiştirici yeni değeri denetleyip atar. Parametre alanı gölgelediği için atama
this.alan = alan;biçiminde yazılır. - Yapıcı da doğrulamalıdır. Yapıcı istisna fırlatırsa
newhiçbir referans üretmez ve geçersiz nesne kullanıcıya ulaşmaz. - Arayüz sınıfın dış yüzü, gerçekleme iç mekanizmasıdır.
interfaceveimplementsbu ayrımı derleyicinin denetlediği bir yapıya dönüştürür. Gerçekleme değişse de arayüz aynı kalabilir: iki alanlı ve tek alanlı saat aynı çıktıyı verir. - “Sor değil, söyle”: kararı veriyi tutan nesne verir; değiştirici yalnızca alan gerçekten serbestçe değişebiliyorsa yazılır.
- Değiştirilebilir bir iç nesnenin referansı dışarı sızabilir. Yapıcıda ve erişimcide savunmacı kopya yapılır ya da salt okunur görünüm verilir.
finalyalnızca referansı sabitler. - Değiştirilemez sınıfta değiştirici yoktur, alanlar
private final'dır, sınıffinal'dır ve “değiştiren” metotlar yeni nesne döndürür. Sonucu atamayı unutmak (a.ekle(p);) en sık yapılan hatadır. - Paket klasördeki sınıf grubudur; sınıfın tam adı paketini içerir. Paketli program
javac -d out ...ile derlenir,java -cp out paket.Sinifile çalıştırılır.importbaşka paketteki sınıfı kısa adıyla kullandırır; alt paketleri getirmez. Aynı adlı iki sınıftan biriimportedilir, öteki tam adıyla yazılır.
4.2.12Kavram listesi
Terimlerin İngilizce karşılıkları ve öteki bölümlerdeki kullanımları terim sözlüğünde bulunur.
- sarmalama encapsulation
- bilgi saklama information hiding
- veri gizleme data hiding
- soyutlama abstraction
- ön koşul / son koşul precondition / postcondition
- sınıf değişmezi class invariant
- doğrulama (assert) assertion
- istisna exception
- erişim belirleyici access modifier
- erişimci (getter) accessor
- değiştirici (setter) mutator
- gölgeleme shadowing
- this this keyword
- yapıcı constructor
- alan field
- referans reference
- arayüz interface
- arayüz / gerçekleme interface vs. implementation
- uygulamak implement
- savunmacı kopya defensive copy
- değiştirilemez immutable
- kayıt (record) record
- paket package
- içe aktarma (import) import
4.2.13Kendini Dene
Erişim metotlarıyla yazılmış
DijitalSaatiçin şu kod çalıştırılıyor:DijitalSaat a = new DijitalSaat(); DijitalSaat b = a; a.setSaat(23); b.setSaat(a.getSaat() + 1); a.setDakika(a.getDakika() - 1); b.setSaniye(59); a.goster();. Ne yazılır?Cevap
avebaynı nesneyi gösterir. Saat 23 olur.b.setSaat(24)reddedilir, çünkü geçerli saatler 0–23'tür.setDakika(-1)de reddedilir ve dakika 0'da kalır. Saniye 59 olur. Çıktı:23:00:59.p1paketindekipublic class Asınıfındapublic int acik,protected int korumali, belirleyicisizint paketveprivate int gizlialanları var.p2paketindeclass C extends p1.Asınıfının bir metodu dört alana da kendi adıyla değer atıyor. Hangi atamalar derlenmez?Cevap
acikvekorumaliatamaları derlenir:publicher yerden,protectedbaşka paketteki alt sınıftan erişilebilir.paketiçinpaket is not public in A; cannot be accessed from outside package,gizliiçingizli has private access in Ahatası çıkar.Tek alanlı (
toplamSaniye)DijitalSaatyeni oluşturulmuş. SıraylasetDakika(59),setSaniye(59)vesetDakika(0)çağrılıyor. Her çağrıdan sonratoplamSaniyekaçtır? SonundagetDakika() + ":" + getSaniye()ne verir?Cevap
İlk çağrıda fark 59 dakikadır: 59 · 60 = 3540. İkincide
getSaniye()0'dır, 59 eklenir: 3599. ÜçüncüdegetDakika()3599 / 60 % 60 = 59'dur; fark −59 dakika, yani −3540 saniyedir: 59. Sonuç0:59. Saniye kısmı dakika değişikliğinden etkilenmez.veriEklemetodundaki koşul yanlışlıklaif (veri.isEmpty() || veri == null)diye yazılmış.veriEkle(null)çağrısında ne olur? Koşulveri == null || veri.trim().isEmpty()yapılırsa" ",nullve" a b "argümanlarından kaçı kayıt ekler?Cevap
İlk yazımda
nullüzerindeisEmpty()çağrılır ve programNullPointerExceptionile durur;||'nin kısa devresi ancak sol taraf doğruysa işe yarar. İkinci yazımda" "kırpılınca boş kalır ve reddedilir,nullilk koşulda reddedilir; yalnızca" a b "eklenir. JDK 21 çıktısı:false false true 1.Bir Javadoc yorumunda
@param n 0 < n < 10 olmalıyazılmış.javadockomutu ne yapar? Yorum nasıl düzeltilir?Cevap
Javadoc yorumu HTML olarak işler ve ham
<işaretini bir etiketin başlangıcı sanır. JDK 21malformed HTMLhatası verir. Düzeltme:@param n {@code 0 < n < 10} olmalı.{@code ...}içindeki metin olduğu gibi gösterilir.<yazmak da çalışır, ama kaynakta okunması zordur.List<Integer> ic = new ArrayList<>(List.of(70, 80));listesindengorunum = Collections.unmodifiableList(ic)vekopya = new ArrayList<>(ic)üretiliyor; sonraic.add(90)çalışıyor.gorunumvekopyane içerir?gorunum.add(100)ne yapar?Cevap
Görünüm kopyalamaz, iç listeyi gösterir:
[70, 80, 90]. Kopya bağımsızdır:[70, 80].gorunum.add(100)UnsupportedOperationExceptionfırlatır. Görünüm dışarıdan değişikliği engeller, ama sınıfın kendi içindeki değişiklikleri yansıtır.ParasınıfıylaSystem.out.println(new Para(-50, "TL"));ne yazar? Bu bir hata mıdır; sınıfa hangi kural eklenebilir?Cevap
−50 / 100 = 0 ve −50 % 100 = −50 olduğu için
0,-50 TLyazılır. Tutar anlamsız görünür. Sınıf negatif tutara izin vermeyecekse yapıcıkurus < 0içinIllegalArgumentExceptionfırlatmalıdır. İzin vereceksetoStringişareti ayrı yazıp mutlak değerle çalışmalıdır.Proje klasöründe
javac -d out src/banka/Hesap.java src/uygulama/Ana.javaçalıştırıldı. Ardındanjava -cp out/uygulama Anadenendi. Ne olur, neden?Cevap
Java
out/uygulama/Ana.classdosyasını bulur, ama dosyadaki sınıfın tam adıuygulama.Ana'dır. Sonuç:NoClassDefFoundError: Ana (wrong name: uygulama/Ana). Doğru komutjava -cp out uygulama.Ana'dır: sınıf yolu paketlerin kökü, sınıf adı ise tam addır.Urunsınıfının yapıcısı fiyatısetFiyatile atıyor;setFiyatpozitif olmayan değeri sessizce yok sayıyor.new Urun("Kalem", -5).getFiyat()ne döndürür? Bu tasarımın sorunu nedir?Cevap
Atama yapılmaz ve alan varsayılan değer olan
0.0'da kalır. Nesne kurulmuştur, ama fiyatı 0 olan bir ürün “yalnızca pozitif fiyat” değişmezini bozar. Sessiz reddetme yapıcıda işe yaramaz. Yapıcının, geçersiz değerdeIllegalArgumentExceptionfırlatıp nesnenin hiç kurulmamasını sağlaması gerekir.
4.2.14İleri okuma
- J. Bloch, Effective Java (3. baskı, Addison-Wesley, 2018), Madde 15–17. Sınıfların ve üyelerin erişimini, değiştirilebilirliği en aza indirmeyi ve
publicalan yerine erişim metodu kullanmayı kısa, gerekçeli kurallarla anlatır. - Oracle, The Java Tutorials, “Controlling Access to Members of a Class” ve “Creating and Using Packages”: erişim düzeylerini ve paketli programların derlenmesini resmî örneklerle anlatır.
- A. B. Downey ve C. Mayfield, Think Java (2. baskı, O'Reilly, 2019), 11. bölüm “Designing Classes”: bir sınıfı alanları, yapıcıları, erişim metotları ve değişmezleriyle adım adım kurar; çevrim içi olarak ücretsiz okunabilir.
- J. Gosling ve diğ., The Java Language Specification, Java SE 21, §6.6 ve §7: erişim denetiminin ve paketlerin kesin kuralları; özellikle
protectederişimin ayrıntıları için başvuru kaynağı.
4.2.15Kaynaklar
- Oracle, The Java Tutorials: “Controlling Access to Members of a Class”, “Using the this Keyword”, “Packages”, “Interfaces”.
- J. Gosling ve diğ., The Java Language Specification, Java SE 21 Edition (6.6 Access Control, 7 Packages and Modules, 7.5 Import Declarations, 15.12.2 metot seçimi).
- P. Deitel ve H. Deitel, Java: How to Program, Early Objects, 11. baskı, Pearson, 2017 (Bölüm 3 ve 8: Classes and Objects).
- K. Sierra ve B. Bates, Head First Java, 3. baskı, O'Reilly, 2022 (Bölüm 4: encapsulation).
- C. S. Horstmann, Core Java, Volume I: Fundamentals, 12. baskı, 2022 (Bölüm 4: Objects and Classes).
- J. Bloch, Effective Java, 3. baskı, Addison-Wesley, 2018 (Madde 15–16: erişimi en aza indirmek,
publicalan yerine erişim metodu). - A. B. Downey ve C. Mayfield, Think Java, 2. baskı, O'Reilly, 2019 (Bölüm 11: Designing Classes).