Java ile Nesne Yönelimli Programlama · Ders Notu
Bölüm 4.1: Nesne ve Sınıflar
Şimdiye kadarki programlar static metotlardan ve bu metotlara aktarılan değişkenlerden oluşuyordu. Veri bir yerde, onu işleyen kod başka bir yerde duruyordu. Nesne yönelimli programlama bu ikisini bir araya getirir: bir öğrencinin adı, numarası ve not ortalaması ile bu verilerle çalışan metotlar aynı birimin içinde yaşar. Bu birimin taslağı sınıftır; taslaktan üretilen her somut örnek bir nesnedir. Not, bir sınıfın nasıl yazıldığını, new ile nesne oluşturulurken bellekte neler olduğunu ve değişkenlerin nesneyi değil, nesneye giden referansı tuttuğunu adım adım gösterir. Örneğe ait ve static üyeler, ilklendirme sırası, this ve yapıcı zinciri, == ile equals farkı, sarmalayıcı sınıflar, kayıtlar, enum ve iç sınıflar da notun konuları arasındadır. Not, bir kesir sınıfını baştan tasarlayarak biter. 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ı
- Sınıf ve nesne
- Sınıfı bir taslak, nesneyi o taslaktan üretilmiş somut bir örnek olarak açıklamak; alanları ve metotlarıyla bir sınıf yazmak.
- Nesne oluşturmak
newoperatörünü ve yapıcıları kullanmak; alanların varsayılan değerlerini ve derleyicinin eklediği varsayılan yapıcıyı bilmek.- Örnek ve sınıf üyeleri
- Örneğe ait üyelerle
staticüyeleri ayırt etmek; ilklendirici bloklarının ve yapıcıların çalışma sırasını izlemek. - Referanslar
- Değişkenin nesneyi değil referansı tuttuğunu bellek çizimiyle göstermek;
this,==veequalsfarkını açıklamak. - Yapıcı zinciri ve erişim
this(...)ile yapıcıları zincirlemek; erişim belirleyicilerin ve veri gizlemenin amacını açıklamak.- Özel sınıf türleri
- Sarmalayıcı sınıfları, kayıtları,
enumtürünü ve iç sınıfları tanımak;RasyonelSayiörneğiyle bir sınıfı baştan tasarlamak.
4.1.1Nesne yönelimli programlama
Bir öğrenci bilgi sistemini static metotlarla yazdığımızı düşünelim. Her öğrencinin adı bir dizide, numarası ikinci bir dizide, ortalaması üçüncü bir dizide tutulur. ortalamaHesapla, derseKaydet ve transkriptYaz gibi metotlar bu dizileri parametre olarak alır. Program büyüdükçe sorunlar belirir. Diziler birbirinden ayrı olduğu için 5. öğrencinin adı ile 5. öğrencinin ortalaması arasındaki bağı yalnızca indis kurar. Bir dizi sıralanıp öteki unutulursa adlar başka öğrencilerin notlarıyla eşleşir. Bir öğrenciye yeni bir bilgi eklemek, örneğin bölümünü tutmak, dördüncü bir dizi ve onu taşıyan yeni parametreler demektir. Bu dizilere dokunan her metodun değişmesi gerekir.
Nesne yönelimli programlama bu dağınıklığa başka bir düzenle cevap verir. Bir öğrencinin bütün bilgileri tek bir birimde toplanır. Bu bilgilerle çalışan kod da aynı birimin içine yazılır. Program, birbirine iş yaptıran bu birimlerden kurulur.
Nesne yönelimli programlama (object-oriented programming, OOP), programı birbiriyle etkileşen nesnelerden kuran programlama yaklaşımıdır. Her nesne kendi verisini taşır ve bu veri üzerinde çalışan işlemleri sunar. Nesneler birbirinin metotlarını çağırarak birlikte çalışır.
4.1.1.1Nesne: durum ve davranış
Gerçek dünyada açıkça ayırt edilebilen her varlık bir nesne olarak modellenebilir. Bir öğrenci, bir masa, bir çember, ekrandaki bir düğme, hatta bankadan çekilen bir kredi bile bir nesnedir. Bu nesnelerin her birinde iki şey bulunur. Birincisi nesnenin durumudur: o an taşıdığı veriler. Öğrencinin adı, numarası ve bölümü onun durumunu oluşturur. İkincisi nesnenin davranışıdır: nesnenin yapabildiği işler. Öğrenci bir derse kaydolabilir, notlarını görebilir. Tablo 4.1.1 beş nesnenin durum ve davranışlarını yan yana verir.
| Nesne | Alanlar (durum) | Davranışlar |
|---|---|---|
| Öğrenci | ad, numara, bölüm | derseKaydol(), notGor() |
| Masa | en, boy, malzeme | tasi() |
| Çember | merkez, yarıçap | alan(), cevre() |
| Düğme | yazı, renk, etkin mi | tikla() |
| Kredi | tutar, faiz, vade | taksitHesapla() |
Programlama dilinde durum ve davranışın karşılıkları bellidir. Nesnenin sakladığı veri parçalarına alan (field), nesnenin yapabildiği işlere metot (method) denir. Nesneler birbirinin metotlarını çağırarak iş görür. Bir okul programında öğrenci, öğretmen ve ders nesneleri birbirine böyle “mesaj gönderir”. Öğretmen nesnesi bir ders nesnesinin notGir metodunu çağırır; ders nesnesi de öğrencinin kaydını günceller. Mesaj göndermek, Java'da bir nesnenin metodunu çağırmak demektir.
ogrenci1 nesnesi. Üst bölümde alanlar ve o anki değerleri, alt bölümde nesnenin metotları vardır. Veri ve onu kullanan kod aynı kutunun içindedir.Şekil 4.1.1, Ogrenci tipindeki ogrenci1 nesnesini çizer. Kutunun üst bölümünde ad, numara ve bolum alanları ile değerleri, alt bölümünde ise selamVer(), dersEkle(ad, not) ve ortalama() metotları bulunur. ortalama() metodu bir dizi parametresi almaz; ihtiyaç duyduğu notlar zaten nesnenin içindedir. Veriyle kodun bu birlikteliği programı daha düzenli kılar. Kod yeniden kullanılabilir hâle gelir ve gerçek dünyadaki karşılığına benzediği için daha kolay anlaşılır.
4.1.1.2Prosedürel ve nesne yönelimli yaklaşım
Şimdiye kadarki programlar prosedürel (yordamsal, procedural) tarzda yazıldı. Bu tarzda temel birim fonksiyondur. Veri ayrı değişkenlerde durur ve onu isteyen her fonksiyona parametre olarak verilir. Nesne yönelimli tarzda temel birim nesnedir; veri nesnenin içinde saklanır ve dışarıya yalnızca nesnenin izin verdiği yollarla açılır. Tablo 4.1.2 iki yaklaşımı beş açıdan karşılaştırır.
| Prosedürel yaklaşım | Nesne yönelimli yaklaşım | |
|---|---|---|
| Temel birim | Fonksiyon (metot) | Nesne (veri + metotlar) |
| Veri | Ayrı değişkenlerde; her fonksiyon erişebilir | Nesnenin içinde; dışarıya kontrollü açılır |
| Örnek | alanHesapla(en, boy) | dikdortgen.alanHesapla() |
| Büyük programlar | Değişiklik birçok fonksiyonu etkiler | Değişiklik çoğunlukla bir sınıfın içinde kalır |
| Yeniden kullanım | Fonksiyonları kopyalayarak | Sınıfları kullanarak ve kalıtımla |
Tablonun “Örnek” satırı farkı en kısa yoldan gösterir. Prosedürel çağrıda alanHesapla metodu dikdörtgenin enini ve boyunu parametre olarak bekler. Çağıran, bu iki sayıyı bir yerde saklamak ve doğru sırayla vermek zorundadır. Nesne yönelimli çağrıda en ve boy dikdörtgen nesnesinin içindedir. Çağıran yalnızca hangi dikdörtgenin alanını istediğini söyler: dikdortgen.alanHesapla().
en, boy) ve bu veriyle çalışan metotları birlikte taşır.“Büyük programlar” satırı ise asıl kazancı söyler. Şekil 4.1.2'in solunda üç fonksiyon aynı veriye bağlıdır. Verinin biçimi değişirse, örneğin en ve boy yerine köşe koordinatları tutulmaya başlanırsa, üç fonksiyonun da değişmesi gerekir. Sağdaki düzende veri bir sınıfın içine kapanmıştır. Biçim değişikliği o sınıfın metotlarını etkiler; sınıfı kullanan kod alanHesapla() çağrısını aynen yazmaya devam eder.
Java'da her kod bir sınıfın içinde yazılır. Bu yüzden şimdiye kadarki programların da bir sınıfı vardı, ama o sınıflar yalnızca static metotları bir arada tutan kaplardı; onlardan hiç nesne üretilmedi. Nesne yönelimli programlamada sınıflar asıl görevini yapar: nesne üretir.
4.1.1.3Üç temel özellik
Nesne yönelimli dilleri prosedürel dillerden ayıran üç özellik vardır. Bu notun konusu sınıf ve nesnenin kendisidir. Yine de üç özelliğin neye hizmet ettiğini baştan bilmek, sınıf yazarken verilen kararları anlamayı kolaylaştırır.
- Sarmalama (encapsulation). Nesne, alanlarının ve davranışlarının ayrıntısını öteki nesnelerden gizleyebilir. Dışarıya karşı kapalı bir kutu gibidir; yalnızca izin verdiği metotlarla kullanılır. Bir televizyonun içindeki devreler kapalı bir kasadadır; kullanıcı yalnızca kumandanın düğmelerine erişir.
- Kalıtım (inheritance). Bir sınıf, başka bir sınıfın alanlarını ve metotlarını devralabilir. Böylece ortak kod bir kez yazılır ve sınıflar bir hiyerarşi içinde düzenlenir.
- Çok biçimlilik (polymorphism). Bir nesne birden çok tip olarak görülebilir. Aynı çağrı farklı nesnelerde farklı davranış üretir.
sekil.alan()çağrısı bir dairede πr², bir karede a² hesaplar.
Bu üçlüye çoğu zaman dördüncü olarak soyutlama (abstraction) eklenir. Soyutlama, gereksiz ayrıntıyı gizleyip yalnızca önemli özellikleri göstermektir. Bir BankaHesabi sınıfı gerçek bir hesabın bütün özelliklerini değil, programın işine yarayanları, örneğin sahip adını ve bakiyeyi modeller. Hesabın açıldığı şubenin kapı rengi modele girmez.
4.1.2Sınıf ve nesne
Bir fabrikada aynı modelden binlerce araba üretilir. Her arabanın bir rengi, bir şasi numarası, bir kilometre sayacı vardır ve bu değerler arabadan arabaya değişir. Ama hepsi aynı çizimden çıkar: hangi parçaların olacağını, motorun nasıl çalışacağını çizim belirler. Java'da bu çizimin karşılığı sınıf, üretilen her arabanın karşılığı nesnedir.
4.1.2.1Sınıf: nesnenin taslağı
Sınıf (class), bir nesnenin taslağıdır. Nesnelerin taşıyacağı verileri (alanları) ve yapabileceği işlemleri (metotları) tanımlar. Bir sınıftan farklı verileri taşıyan istenen sayıda nesne üretilebilir.
Nesne (object), bir sınıfın örneğidir (instance): sınıfın tarif ettiği yapının bellekte oluşturulmuş somut hâlidir. Her nesne, sınıfın tanımladığı alanların kendi kopyalarını taşır.
Sınıf ile nesne arasındaki ilişkiyi kurabiye kalıbı iyi anlatır. Kalıp bir kez yapılır ve kurabiyenin biçimini belirler. Kalıpla kesilen her kurabiye ayrı bir kurabiyedir: birine çikolata, ötekine fındık konabilir. Kalıbın kendisi yenmez. Sınıf da tek başına veri taşımaz; veriyi ondan üretilen nesneler taşır.
Araba sınıfı ve ondan new ile üretilen üç nesne. Sınıf alan adlarını ve metotları tanımlar; her nesne bu alanlara kendi değerlerini koyar.Şekil 4.1.3'ın solunda kesik çizgili kutu Araba sınıfıdır. Sınıf marka, model, yil ve renk alanlarını, ilerle(), dur() ve sinyalVer() metotlarını tanımlar. Sağdaki üç nesne aynı sınıftan üretilmiştir: Toyota Corolla, Ford Mustang ve Fiat Egea. Üçünde de aynı alan adları vardır, ama değerler farklıdır. Aynı sınıftan üretilen iki nesne aynı alan adlarını taşır; her birinin alan değerleri kendine aittir. Birinin rengini değiştirmek ötekinin rengini değiştirmez.
Sınıflar genellikle UML sınıf diyagramıyla (Unified Modeling Language, Birleşik Modelleme Dili) çizilir. Diyagramda sınıf üç bölmeli bir kutudur: en üstte sınıfın adı, ortada alanlar, altta metotlar. Her üyenin önündeki işaret erişim düzeyini gösterir: - private, + public demektir. Alanların yanında tipleri, metotların yanında dönüş tipleri yazılır. Nesneler de benzer bir kutuyla çizilir; ama başlıkta araba1 : Araba biçiminde nesnenin adı ve sınıfı bulunur ve başlığın altı çizilir.
Araba sınıfı (ad / alanlar / metotlar), sağda bu sınıfın iki nesnesi. Nesne kutularında alanların o anki değerleri yazılıdır; kesik oklar “örneği” ilişkisini gösterir.4.1.2.2Sınıf tanımlamak
Aşağıdaki kod Şekil 4.1.4'daki Araba sınıfının ilk Java sürümüdür. Diyagramda alanlar private işaretlidir; bu ilk sürümde ise hiçbir erişim belirleyici yazılmamıştır. Alanlar bu yüzden paket erişimlidir ve aynı klasördeki başka bir sınıf onlara doğrudan değer verebilir.
public class Araba {
// Alanlar (fields)
String marka;
String model;
int yil;
String renk;
// Metotlar (methods)
void ilerle() {
System.out.println(marka + " ilerliyor.");
}
void dur() {
System.out.println(marka + " durdu.");
}
void sinyalVer() {
System.out.println(marka + " sinyal veriyor.");
}
}Bir sınıf tanımı class anahtar kelimesi ve sınıfın adıyla başlar. Sınıfın gövdesi süslü parantezler arasındadır. Ad büyük harfle başlar ve birden çok sözcükten oluşuyorsa her sözcüğün ilk harfi büyük yazılır: Araba, BankaHesabi, RasyonelSayi. public bir sınıf, kendisiyle aynı adı taşıyan bir dosyada bulunmalıdır. public class Araba tanımı Araba.java dosyasına yazılır. Bir dosyada birden çok sınıf olabilir, ama bunlardan en çok biri public olabilir.
Alanlar sınıfın gövdesinde, metotların dışında tanımlanır. Tanım bir yerel değişken tanımına benzer: tip ve ad. String marka; satırı her Araba nesnesinde marka adlı bir String alanı bulunacağını söyler. Alanlar sınıfın her metodundan doğrudan kullanılabilir. ilerle() metodu marka'yı parametre olarak almaz; onu sınıfın alanı olarak tanır.
Metotların başlığında static yoktur. Şimdiye kadarki metotların hepsi static'ti ve sınıf adıyla, nesnesiz çağrılıyordu. Buradaki metotlar ise belirli bir arabanın metotlarıdır. ilerle() çağrıldığında hangi arabanın markasının yazılacağını, metodun hangi nesne üzerinden çağrıldığı belirler. Bu tür metotlara örneğe ait metot (instance method) denir.
Sınıfta main metodu yoktur. Bu yüzden Araba tek başına çalıştırılamaz; bir yapı taşıdır ve başka bir sınıf onu kullanır. Gerçek programlarda sınıfların çoğu böyledir: yalnızca bir sınıf main'i taşır, ötekiler onun kullandığı tipleri tanımlar.
4.1.2.3Sınıfı kullanmak
AracTesti sınıfı iki Araba nesnesi oluşturur, alanlarına değer verir ve metotlarını çağırır. İki dosya aynı klasördeyse javac Araba.java AracTesti.java ile derlenir, java AracTesti ile çalıştırılır.
public class AracTesti {
public static void main(String[] args) {
Araba araba1 = new Araba();
araba1.marka = "Toyota";
araba1.model = "Corolla";
araba1.yil = 2022;
araba1.renk = "Gri";
Araba araba2 = new Araba();
araba2.marka = "Ford";
araba2.model = "Mustang";
araba2.yil = 2023;
araba2.renk = "Mavi";
araba1.ilerle();
araba2.sinyalVer();
araba1.dur();
}
}Toyota ilerliyor.
Ford sinyal veriyor.
Toyota durdu.İlk satırdaki new Araba() bellekte yeni bir Araba nesnesi oluşturur. Araba araba1 tanımı ise Araba tipinde bir değişken açar. Atamadan sonra araba1 yeni oluşturulan nesneyi gösterir. Değişkenin tipi bir sınıf adıdır; Java'da her sınıf aynı zamanda yeni bir tiptir.
Sonraki dört satır nesnenin alanlarına değer verir. araba1.marka = "Toyota"; satırındaki nokta, “araba1'in gösterdiği nesnenin marka alanı” anlamına gelir. Bu işarete nokta operatörü (dot operator) denir. İkinci nesne için aynı alanlara başka değerler verilir. Artık bellekte iki ayrı marka alanı vardır: biri "Toyota", öteki "Ford" değerini taşır.
Son üç satır metot çağırır. araba1.ilerle() çağrısında metodun gövdesindeki marka, araba1'in markasıdır: Toyota ilerliyor. yazılır. araba2.sinyalVer() çağrısında aynı ad araba2'nin markasını gösterir: Ford sinyal veriyor. Metot kodu bir kez yazılmıştır; hangi nesnenin verisiyle çalışacağını, noktanın solundaki nesne belirler.
Alanlara dışarıdan böyle doğrudan değer vermek iyi bir alışkanlık değildir. araba1.yil = -5; satırı da derlenir ve nesneyi anlamsız bir duruma sokar. Bir nesnenin alanlarına ilk değerlerini vermenin doğru yolu yapıcılardır; değerleri sonradan değiştirmenin doğru yolu ise denetim yapan metotlardır. Buradaki yazım yalnızca nokta operatörünü tanıtmak içindir.
4.1.2.4Sınıfın üyeleri
Sınıfın gövdesinde tanımlanan her şeye sınıfın üyesi (member) denir. İki temel üye türü vardır:
- Alanlar, nesnelerin taşıyabileceği verileri temsil eder. Bir
Ogrencisınıfındaadvesoyadbirer alandır. - Metotlar, nesnelerin yapabileceği işlemleri tanımlar. Bir
Kopeksınıfındahavla()vekos()birer metottur.
Her üyenin iki ayrı özelliği daha vardır. Birincisi erişim düzeyidir: üye private, protected, public ya da hiçbiri yazılmadığında paket erişimli olabilir. Erişim düzeyi, üyeyi kimin kullanabileceğini belirler. İkincisi sahipliktir: üye ya her nesnede ayrı bulunur (örneğe ait, varsayılan durum) ya da static ile tanımlanıp sınıfın kendisine ait olur.
Sınıfın gövdesinde iki tür yapı daha bulunabilir. Yapıcılar sınıfın içinde yazılır ve metotlara benzer; ama yalnızca nesne oluşturulurken çalışır. Java dil tanımı onları metot değil, ayrı bir üye türü sayar. Bir sınıfın içinde başka sınıflar da tanımlanabilir; bunlara iç sınıf denir.
4.1.2.5Erişim belirleyiciler ve veri gizleme
Erişim belirleyici (access modifier), bir üyeye sınıfın dışından kimin erişebileceğini belirten anahtar kelimedir. Java'da dört erişim düzeyi vardır: public (herkes), protected (aynı paket ve alt sınıflar), yazılmadığında paket erişimi (aynı paketteki sınıflar) ve private (yalnızca sınıfın kendisi).
Genel kural basittir: alanlar private, dışarıya sunulan metotlar public tanımlanır. Alanın değeri dışarıdan bir erişimci (getter) ile okunur; adı geleneksel olarak get ile başlar. Değer bir değiştirici (setter) ile değiştirilir; adı set ile başlar. Değiştiricinin asıl görevi gelen değeri denetlemek ve geçersiz değeri reddetmektir.
public class BankaHesabi {
private double hesapBakiyesi; // gizli alan
public double getHesapBakiyesi() {
return hesapBakiyesi;
}
public void setHesapBakiyesi(double b) {
if (b >= 0) hesapBakiyesi = b; // geçersizi reddet
}
}
// başka bir sınıfta:
hesap.hesapBakiyesi = -500;hesapBakiyesi alanı private'tır. BankaHesabi sınıfının dışındaki bir sınıf bu alanı okuyamaz da değiştiremez de. Kodun son satırı başka bir sınıfa yazıldığında derleyici şu iletiyi verir:
class BankaHesabi {
private double hesapBakiyesi; // gizli alan
}
public class OzelErisim {
public static void main(String[] args) {
BankaHesabi hesap = new BankaHesabi();
hesap.hesapBakiyesi = -500;
}
}OzelErisim.java:7: error: hesapBakiyesi has private access in BankaHesabi
hesap.hesapBakiyesi = -500;
^
1 errorDışarıdan erişimin tek yolu iki public metottur. getHesapBakiyesi() değeri döndürür. setHesapBakiyesi(b) ise yeni değeri yazmadan önce denetler: b negatifse atama yapılmaz. Aşağıdaki deneme programı önce geçerli, sonra geçersiz bir bakiye vermeye çalışır:
public class HesapTesti {
public static void main(String[] args) {
BankaHesabi hesap = new BankaHesabi();
hesap.setHesapBakiyesi(1500);
hesap.setHesapBakiyesi(-500); // reddedilir
System.out.println(hesap.getHesapBakiyesi());
}
}1500.0-500 reddedildiği için bakiye 1500.0 kalır. Program bakiyeyi negatif yapamaz; bunu engelleyen kural tek bir yerde, setHesapBakiyesi'nin içinde yazılıdır. Alan public olsaydı aynı kuralı alanı değiştiren her satırın ayrıca denetlemesi gerekirdi ve bir tanesinin unutulması nesneyi bozardı.
Veri gizleme (data hiding), alanlara yalnızca kendi sınıfının metotlarının doğrudan erişebildiği düzendir. Alanlar private yapılır, dış dünya nesneyle yalnızca public metotlar üzerinden konuşur. Veri gizleme sarmalamanın temelidir.
Veri gizlemenin ikinci kazancı değişiklik özgürlüğüdür. Bakiyeyi double yerine kuruş cinsinden bir long alanda tutmaya karar verildiğini düşünelim. Alan private olduğu için onu dışarıda kullanan hiçbir satır yoktur. Yalnızca iki metodun gövdesi değişir; sınıfı kullanan kod hiç değişmez.
Her private alana düşünmeden bir get ve bir set metodu eklemek. Denetim yapmayan public void setBakiye(double b) { bakiye = b; } metodu alanı fiilen public yapar. Değiştirici yalnızca alanın dışarıdan değişmesi gerçekten gerekiyorsa yazılır ve geçersiz değerleri reddeder. Hiç değişmemesi gereken alanın değiştiricisi olmaz.
4.1.3Yapıcılar ve nesne oluşturma
AracTesti'nde her nesne beş satırda kuruldu: önce new Araba(), sonra dört ayrı atama. Bu düzende atamalardan biri unutulursa nesne yarım kalır ve hata ancak çalışma sırasında, belki çok sonra ortaya çıkar. Yapıcılar nesnenin oluşturulduğu anda geçerli bir duruma getirilmesini sağlar.
4.1.3.1Yapıcı nedir?
Yapıcı (constructor), nesne oluşturulurken çalışan özel bir kod bloğudur. new ile her nesne oluşturulduğunda kendiliğinden çağrılır. Adı sınıfın adıyla birebir aynıdır (büyük-küçük harf dahil) ve dönüş tipi yoktur; void bile yazılmaz.
Yapıcının görevi alanlara ilk değerlerini vermek ve nesnenin kullanılmaya hazır olması için gereken başka işleri yapmaktır. Aşağıdaki Ogrenci sınıfının yapıcısı iki parametre alır ve onları nesnenin alanlarına yazar.
public class Ogrenci {
private String ad;
private int numara;
// Yapıcı: sınıfla aynı ad, dönüş türü yok
public Ogrenci(String ad, int numara) {
this.ad = ad;
this.numara = numara;
}
}
// Kullanımı:
Ogrenci ogr = new Ogrenci("Ali", 2012);Yapıcının başlığı bir metot başlığına benzer, ama iki fark taşır. Ad, sınıfın adıdır: Ogrenci. Adın önünde dönüş tipi yoktur. Yapıcı bir değer döndürmez; new ifadesinin sonucu her zaman yeni nesnenin kendisidir. Gövdedeki this.ad = ad; satırı, ad parametresinin değerini nesnenin ad alanına yazar. Buradaki this, “oluşturulmakta olan nesne” anlamına gelir.
Yapıcılar genellikle public tanımlanır, çünkü nesneyi sınıfın dışındaki kod oluşturur. Bazen nesne oluşturmayı kısıtlamak istenir. Bu durumda yapıcı private yapılır ve sınıfın dışından new çağrılamaz. Tek nesnelik (singleton) sınıflar bu yolla kurulur. Bir sınıfın farklı parametre listeleriyle birden çok yapıcısı olabilir.
4.1.3.2new ifadesi
Nesne oluşturmanın genel biçimi şudur:
SinifAdi nesneAdi = new SinifAdi(parametreler);
Araba corolla = new Araba("Toyota", "Corolla", 2022);
Scanner klavye = new Scanner(System.in);
Kitap kitap = new Kitap();İlk satır kalıbı, sonraki üç satır üç örneği gösterir. Eşittir işaretinin solunda değişken tanımı, sağında new ifadesi vardır. Java'nın kendi sınıfları da aynı yolla kullanılır: klavyeden okumak için new Scanner(System.in), değiştirilebilir bir metin için new StringBuilder("Java") yazılır. Aşağıdaki program dört nesne oluşturur ve kullanır.
import java.util.Scanner;
public class Olusturma {
public static void main(String[] args) {
Scanner klavye = new Scanner(System.in);
StringBuilder metin = new StringBuilder("Java");
String baslik = "Nesneler"; // kısayol
String yazar = new String("Sercan"); // uzun yol
metin.append(" 21");
System.out.println(metin + " / " + baslik);
System.out.println(yazar.length());
System.out.println(klavye.nextInt() * 2);
}
}Klavyeden 21 girildiğinde program şunu yazar:
Java 21 / Nesneler
6
42new ifadesi üç adımda çalışır:
new, heap adı verilen bellek bölgesinde nesne için yer ayırır. Nesnenin bütün alanları önce varsayılan değerlerini alır: sayılar 0,booleanalanlarfalse, referans alanlarnullolur.- Argümanlara uyan yapıcı çağrılır ve alanlara ilk değerlerini verir.
newifadesinin sonucu yeni nesnenin referansıdır. Bu referans eşittir işaretinin solundaki değişkene atanır.
String'de bu kuralın tanıdık bir istisnası vardır: metin sabiti yazmak da bir String nesnesi verir. String baslik = "Nesneler"; satırı new kullanmadan bir String nesnesinin referansını baslik'a koyar. new String("Sercan") yazımı da geçerlidir, ama her çalıştığında gereksiz yere yeni bir nesne üretir. Yalnızca String baslik; yazmak ise hiçbir nesne oluşturmaz; yalnızca referans tutacak bir değişken tanımlar.
Çıktının ikinci satırındaki 6, yazar.length() çağrısının sonucudur: "Sercan" altı harflidir. Bu çağrı da nokta operatörüyle yapılmış bir örnek metodu çağrısıdır. Programcının kendi yazdığı sınıflarla Java'nın hazır sınıfları arasında kullanım açısından fark yoktur.
4.1.3.3Referanslar
Bir nesneye doğrudan dokunulamaz. Program bir nesneyle yalnızca ona giden referans aracılığıyla çalışır. Sınıf tipinden bir değişken nesnenin kendisini değil, bu referansı saklar.
Referans (reference), heap'teki bir nesnenin yerini gösteren değerdir. Nesnenin bellekteki adresi gibi düşünülebilir. Sınıf tipinden değişkenler (Araba, String, Scanner, diziler) referans tutar. Hiçbir nesneyi göstermeyen referansın değeri null'dır.
int, double, boolean gibi ilkel tipli değişkenler değeri kendileri tutar. int x = 5; satırından sonra x'in kutusunda 5 sayısı durur. Araba a = new Araba(...); satırından sonra ise a'nın kutusunda bir araba değil, arabanın yerini gösteren bir ok durur. Bu farkı akılda tutmanın kolay yolu bir benzetmedir: değişken bir televizyon kumandası, nesne ise televizyondur. Kumandayı kopyalamak ikinci bir televizyon yaratmaz; iki kumanda aynı televizyonu açıp kapatır.
sporAraba ve klasikAraba iki ayrı nesneyi gösterir; yeniAraba henüz hiçbir nesneyi göstermez (null).Bellek iki bölgeye ayrılır. Çalışan metotların çerçeveleri ve yerel değişkenleri yığında (stack) durur. Nesneler ise heap'te (heap) tutulur. Şekil 4.1.5'ta main'in çerçevesi soldadır. Çerçevedeki sporAraba ve klasikAraba kutularından çıkan oklar heap'teki iki nesneye uzanır. Üçüncü değişken yeniAraba tanımlanmış ama ona henüz bir nesne atanmamıştır; değeri null'dır. Bu çizim biçimi notun geri kalanında hep kullanılır: kutu değişkeni, ok referansı, heap'teki çerçeveli kutu nesneyi gösterir.
4.1.3.4Nesne oluşturmayı adım adım izlemek
Aşağıdaki Araba sınıfı iki alan, iki parametreli bir yapıcı ve bir metot içerir. main iki nesne oluşturup ikisinin bilgisini yazdırır.
public class Araba {
private String model;
private String renk;
public Araba(String model, String renk) {
this.model = model;
this.renk = renk;
}
public void bilgiYazdir() {
System.out.println(model + " - " + renk);
}
public static void main(String[] args) {
Araba sporAraba = new Araba("2022", "KIRMIZI");
Araba klasikAraba = new Araba("2023", "BEYAZ");
sporAraba.bilgiYazdir();
klasikAraba.bilgiYazdir();
}
}2022 - KIRMIZI
2023 - BEYAZTablo 4.1.3 programın sekiz adımını verir. Satır numaraları yukarıdaki kodun satırlarıdır. “Değişkenler” sütunu o anda en üstteki çerçevenin değişkenlerini gösterir; →#1 yazımı değişkenin heap'teki 1 numaralı nesneyi gösterdiğini, ? ise değişkene henüz değer verilmediğini belirtir.
| Adım | Satır | Çağrı yığını | Değişkenler | Yeni çıktı |
|---|---|---|---|---|
| 1 | 15: Araba sporAraba = new Araba("2022", "KIRMIZI"); | main() | sporAraba=?, klasikAraba=? | — |
| 2 | 5: public Araba(String model, String renk) { | main() → Araba(model, renk) | model="2022", renk="KIRMIZI" | — |
| 3 | 6: this.model = model; | main() → Araba(model, renk) | model="2022", renk="KIRMIZI" | — |
| 4 | 7: this.renk = renk; | main() → Araba(model, renk) | model="2022", renk="KIRMIZI" | — |
| 5 | 15: Araba sporAraba = new Araba("2022", "KIRMIZI"); | main() | sporAraba=→#1, klasikAraba=? | — |
| 6 | 16: Araba klasikAraba = new Araba("2023", "BEYAZ"); | main() | sporAraba=→#1, klasikAraba=→#2 | — |
| 7 | 17: sporAraba.bilgiYazdir();11: System.out.println(model + " - " + renk); | main() → bilgiYazdir() | sporAraba=→#1, klasikAraba=→#2 | 2022 - KIRMIZI |
| 8 | 18: klasikAraba.bilgiYazdir();11: System.out.println(model + " - " + renk); | main() → bilgiYazdir() | sporAraba=→#1, klasikAraba=→#2 | 2023 - BEYAZ |
Birinci adımda 15. satır çalışmaya başlar. new heap'te bir Araba nesnesi için yer ayırır. model ve renk String tipinde, yani referans tipinde alanlardır; varsayılan değerleri null'dır. Nesne bu anda vardır ama içi boştur.
new nesneyi ayırmış, alanlar null. Ortada yapıcı çalışıyor: yapıcının çerçevesindeki this yeni nesneyi gösteriyor ve iki alan dolmuş. Sağda ikinci bilgiYazdir() çağrısı: this bu kez 2 numaralı nesneyi gösteriyor.İkinci adımda yapıcı çağrılır ve yığında yeni bir çerçeve açılır. Parametreler argümanların kopyasını alır: model = "2022", renk = "KIRMIZI". Çerçevede bir değişken daha vardır: this. Java, yapıcıyı çağırırken yeni nesnenin referansını this adıyla gizlice verir. Üçüncü ve dördüncü adımda this.model = model; ve this.renk = renk; çalışır. Her satırda eşittir işaretinin solundaki this.model nesnenin alanı, sağındaki model ise parametredir. Bu iki satırdan sonra nesne hazırdır (Şekil 4.1.6, orta).
Beşinci adımda yapıcı biter ve çerçevesi silinir. Parametreler ve this onunla birlikte yok olur; ama nesne heap'te kalır. new ifadesinin değeri olan referans sporAraba'ya atanır. Altıncı adım aynı işleri ikinci nesne için yapar. Heap'te kendi model ve renk kopyalarıyla ayrı bir nesne oluşur ve klasikAraba onu gösterir.
Yedinci adımda sporAraba.bilgiYazdir() çağrılır. Metodun çerçevesinde de bir this vardır ve değeri noktanın solundaki referanstır: 1 numaralı nesne. Metodun gövdesindeki model ve renk adları this.model ve this.renk anlamına gelir. Bu yüzden 2022 - KIRMIZI yazılır. Sekizinci adımda aynı metot klasikAraba üzerinden çağrılır; this bu kez 2 numaralı nesneyi gösterir ve 2023 - BEYAZ yazılır. Kod tek, veriler nesne başına ayrıdır.
Bir nesnenin bellekte kapladığı yer, alanlarının toplamı kadardır. Metotlar her nesnede kopyalanmaz; sınıfın kodu bellekte bir kez bulunur. Bu yüzden bin Araba nesnesi oluşturmak bin kez iki referanslık yer tutar, ama bilgiYazdir'ın kodu yalnızca bir kez yer kaplar.
4.1.3.5Varsayılan yapıcı ve varsayılan değerler
İlk Araba sınıfında hiç yapıcı yoktu, ama new Araba() sorunsuz derlendi. Bunun nedeni derleyicinin yardımıdır.
Varsayılan yapıcı (default constructor), sınıfa hiç yapıcı yazılmamışsa derleyicinin eklediği parametresiz ve gövdesi boş yapıcıdır. Sınıfa tek bir yapıcı bile yazılırsa derleyici varsayılan yapıcıyı eklemez.
Nesne oluşturulurken her zaman bir yapıcı çağrılır; varsayılan yapıcı, programcı yazmadığında bu çağrının bir hedefi olmasını sağlar. Gövdesi boş olduğu için alanlara hiçbir değer vermez. Değer verilmeyen alanlar new'ün birinci adımında aldıkları varsayılan değerlerde kalır. Tablo 4.1.4 bu değerleri listeler.
| Alan tipi | Varsayılan değer |
|---|---|
byte, short, int, long | 0 |
float, double | 0.0 |
boolean | false |
char | '\u0000' (kod 0) |
Referans (String, diziler, nesneler) | null |
class Nokta3 {
int x;
double agirlik;
boolean gorunur;
char harf;
String ad;
}
public class Varsayilan {
public static void main(String[] args) {
Nokta3 n = new Nokta3(); // varsayılan yapıcı
System.out.println(n.x + " " + n.agirlik);
System.out.println(n.gorunur + " " + n.ad);
System.out.println((int) n.harf);
}
}0 0.0
false null
0Nokta3 sınıfında hiç yapıcı yoktur; new Nokta3() derleyicinin eklediği varsayılan yapıcıyı çağırır. Beş alanın hiçbirine değer verilmemiştir ve her biri tablodaki değeri taşır. char alanının değeri 0 kodlu karakterdir. Bu karakter ekranda görünmez, bu yüzden program onu (int) ile sayıya çevirerek yazar: 0.
Varsayılan değer kuralı yalnızca alanlar ve dizi öğeleri içindir. Bir metodun içindeki yerel değişken varsayılan değer almaz. Değer verilmeden okunan yerel değişkeni derleyici kabul etmez:
public class IlkDegersiz {
public static void main(String[] args) {
StringBuilder sb;
sb.append("x");
}
}IlkDegersiz.java:4: error: variable sb might not have been initialized
sb.append("x");
^
1 errorBu farkın nedeni güvenliktir. Derleyici bir metodun içindeki bütün yolları inceleyip yerel değişkenin okunmadan önce değer alıp almadığını görebilir. Alanlar için bunu yapamaz, çünkü bir alan herhangi bir metottan, herhangi bir sırayla okunabilir. Java bu yüzden alanları en azından tanımlı bir değerle başlatır.
4.1.3.6Parametresiz ve parametreli yapıcı
Bir sınıfa birden çok yapıcı yazılabilir. Aşağıdaki Arac sınıfının biri parametresiz, biri üç parametreli iki yapıcısı vardır.
public class Arac {
private String marka;
private String model;
private int yil;
public Arac() { // parametresiz yapıcı
marka = "Bilinmiyor";
model = "Bilinmiyor";
yil = 0;
}
public Arac(String marka, String model, int yil) {
this.marka = marka;
this.model = model;
this.yil = yil;
}
public void bilgiYazdir() {
System.out.println(marka + " " + model + " " + yil);
}
public static void main(String[] args) {
Arac a1 = new Arac();
Arac a2 = new Arac("Renault", "Clio", 2021);
a1.bilgiYazdir();
a2.bilgiYazdir();
}
}Bilinmiyor Bilinmiyor 0
Renault Clio 2021Arac() programcının kendi yazdığı parametresiz yapıcıdır. Varsayılan yapıcıdan farkı, alanlara anlamlı başlangıç değerleri vermesidir: marka ve model null yerine "Bilinmiyor" olur. İki yapıcı aynı adı taşır ve parametre listeleriyle ayrılır. Bu, metotlardaki aşırı yüklemenin yapıcılardaki karşılığıdır: yapıcı aşırı yükleme. Hangi yapıcının çalışacağını new ifadesindeki argümanlar belirler. new Arac() parametresiz yapıcıyı, new Arac("Renault", "Clio", 2021) üç parametreli yapıcıyı çağırır.
Sınıfa herhangi bir yapıcı yazıldığında derleyici artık varsayılan yapıcı eklemez. Yalnızca parametreli yapıcısı olan bir sınıfta new ile parametresiz nesne oluşturmak derleme hatasıdır:
public class AracVarsayilan {
private String marka;
public AracVarsayilan(String marka) {
this.marka = marka;
}
public static void main(String[] args) {
AracVarsayilan a = new AracVarsayilan();
}
}AracVarsayilan.java:7: error: constructor AracVarsayilan in class AracVarsayilan cannot be applied to given types;
AracVarsayilan a = new AracVarsayilan();
^
required: String
found: no arguments
reason: actual and formal argument lists differ in length
1 errorİletinin alt satırları neyin beklendiğini açıkça söyler: required: String, yani bir String argüman bekleniyor; found: no arguments, yani hiç argüman verilmemiş. Parametresiz nesne oluşturmak gerekiyorsa parametresiz yapıcı da elle yazılır.
Yapıcıya dönüş tipi yazmak. public void Hata1(String m) yapıcı değil, sınıfla aynı adı taşıyan sıradan bir metottur. Sınıfta başka yapıcı olmadığı için derleyici parametresiz varsayılan yapıcıyı ekler ve new Hata1("selam") şu iletiyle reddedilir:
Hata1.java:7: error: constructor Hata1 in class Hata1 cannot be applied to given types;
Hata1 h = new Hata1("selam");
^
required: no arguments
found: String
reason: actual and formal argument lists differ in length
1 errorİletide required: no arguments yazması, derleyicinin yalnızca parametresiz yapıcıyı gördüğünü gösterir. void silinince yapıcı yerine gelir.
4.1.4Alanlar
Alanlar sınıfın içinde, metotların dışında tanımlanan değişkenlerdir. Yerel değişkenlerden üç noktada ayrılırlar. Varsayılan değer alırlar. Bir metot bitince yok olmazlar, nesneyle birlikte yaşarlar. Sınıfın bütün metotlarından görünürler. Bir alanın kime ait olduğu ise static kelimesine bağlıdır.
4.1.4.1Örneğe ait ve sınıfa ait alanlar
Örneğe ait alan (instance field), static olmadan tanımlanan alandır; her nesnede ayrı bir kopyası bulunur. Sınıfa ait alan (static field), static ile tanımlanan alandır; sınıfta tek bir kopyası bulunur ve bütün nesneler bu kopyayı paylaşır.
| Örneğe ait alan | Sınıfa ait alan (static) | |
|---|---|---|
| Tanım | private String ad; | private static int toplamHesapSayisi; |
| Kopya sayısı | Her nesnede ayrı bir kopya | Sınıfta tek kopya, bütün nesneler paylaşır |
| Ne zaman oluşur? | Her new ile | Sınıf belleğe yüklenirken, bir kez |
| Erişim | nesne.ad | SinifAdi.toplamHesapSayisi |
| Örnek | Ogrenci: ad, soyad | BankaHesabi: toplamHesapSayisi |
Bir alanın örneğe mi sınıfa mı ait olacağını şu soru belirler: değer nesneden nesneye değişir mi? Her öğrencinin adı farklıdır; ad örneğe ait bir alandır. Bankada açılmış toplam hesap sayısı ise tek bir sayıdır. Her hesap nesnesinde ayrı bir sayaç tutmak hem gereksiz olurdu hem de sayaçlar birbirinden farklı değerler gösterirdi. Bu yüzden sayaç static tanımlanır.
4.1.4.2static sayacı izlemek
Aşağıdaki sınıfta her hesap kendi sahibinin adını tutar; bütün hesaplar ise tek bir sayacı paylaşır. Yapıcı her çalıştığında sayaç bir artar.
public class BankaHesabi {
private String hesapSahibi;
private static int toplamHesapSayisi = 0;
public BankaHesabi(String hesapSahibi) {
this.hesapSahibi = hesapSahibi;
toplamHesapSayisi++;
}
public static int getToplamHesapSayisi() {
return toplamHesapSayisi;
}
public static void main(String[] args) {
BankaHesabi h1 = new BankaHesabi("Ali");
BankaHesabi h2 = new BankaHesabi("Ayşe");
BankaHesabi h3 = new BankaHesabi("Can");
System.out.println(h2.hesapSahibi);
int sayi = BankaHesabi.getToplamHesapSayisi();
System.out.println(sayi);
}
}Ayşe
3| Adım | Satır | Çağrı yığını | Değişkenler | Yeni çıktı |
|---|---|---|---|---|
| 1 | 3: private static int toplamHesapSayisi = 0; | (sınıf yükleniyor) | toplamHesapSayisi=0 | — |
| 2 | 5: public BankaHesabi(String hesapSahibi) {6: this.hesapSahibi = hesapSahibi;7: toplamHesapSayisi++; | main() → BankaHesabi("Ali") | toplamHesapSayisi=1, h1=? | — |
| 3 | 15: BankaHesabi h1 = new BankaHesabi("Ali"); | main() | toplamHesapSayisi=1, h1=→#1 | — |
| 4 | 16: BankaHesabi h2 = new BankaHesabi("Ayşe"); | main() | toplamHesapSayisi=2, h1=→#1, h2=→#2 | — |
| 5 | 17: BankaHesabi h3 = new BankaHesabi("Can"); | main() | toplamHesapSayisi=3, h1=→#1, h2=→#2, h3=→#3 | — |
| 6 | 18: System.out.println(h2.hesapSahibi); | main() | toplamHesapSayisi=3 | Ayşe |
| 7 | 19: int sayi = BankaHesabi.getToplamHesapSayisi();10: public static int getToplamHesapSayisi() {11: return toplamHesapSayisi; | main() → getToplamHesapSayisi() | toplamHesapSayisi=3 | — |
| 8 | 20: System.out.println(sayi); | main() | toplamHesapSayisi=3, sayi=3 | 3 |
Birinci adım main'den önce gerçekleşir. BankaHesabi sınıfı belleğe yüklenirken static alan bir kez oluşur ve ilk değerini alır: 0. Bu anda heap'te hiçbir BankaHesabi nesnesi yoktur; ama sayaç vardır. static alan nesnelerin değil, sınıfın parçasıdır ve yaşamı sınıfın yüklenmesiyle başlar.
İkinci adımda new BankaHesabi("Ali") ilk nesneyi oluşturur. Yapıcının ilk satırı nesnenin kendi hesapSahibi alanına "Ali" yazar. İkinci satırdaki toplamHesapSayisi++ ise ortak sayacı 1'e çıkarır. Yapıcının içinden static alana sınıf adı yazmadan erişilebilir; derleyici adın bir sınıf alanı olduğunu bilir. Üçüncü adımda h1 bu nesneyi göstermeye başlar.
hesapSahibi alanı var, ama sayaç tek kutudur ve değeri 3'tür.Dördüncü ve beşinci adımlar aynı işi iki kez daha yapar. Her nesnenin ayrı bir hesapSahibi alanı oluşur, ama artırılan sayaç hep aynı kutudur: 2, sonra 3. Şekil 4.1.7'ın sağ parçası bu anı çizer. Heap'te üç nesne, üç ayrı hesapSahibi kutusu vardır; toplamHesapSayisi ise nesnelerin dışında, sınıfa ait bölgede tek bir kutudur.
Altıncı adımda h2.hesapSahibi okunur: örneğe ait alan nesne üzerinden okunur ve Ayşe yazılır. Yedinci adımda static metot getToplamHesapSayisi() sınıf adıyla çağrılır. Bu metodun çerçevesinde this yoktur, çünkü metot hiçbir nesne üzerinden çağrılmamıştır. Metot yalnızca static alanı okur ve 3 döndürür. Sekizinci adımda dönen değer sayi'ya atanır ve yazdırılır.
main metodu BankaHesabi sınıfının içinde olduğu için h2.hesapSahibi satırı derlenir, oysa alan private'tır. private, “yalnızca bu sınıfın içindeki kod” demektir; erişimi sınıf belirler, nesne belirlemez. Aynı satır başka bir sınıfın main'ine yazılsaydı has private access hatası alınırdı.
4.1.4.3Alan tanımları ve final
Bir alan tanımının genel biçimi şudur: [erişim] [static] [final] tip ad [= ilkDeğer];. Köşeli parantezdeki parçalar isteğe bağlıdır. Aşağıdaki iki sınıf farklı birleşimleri gösterir.
public class Kitap {
// Örneğe ait alanlar
private String baslik;
private String yazar;
private int sayfaSayisi;
// Sınıfa ait alan
private static int toplamKitapSayisi;
// final tanımlanmış bir alan (sabit)
public static final double PI = 3.14159265359;
}public class Alanlar {
public int a; // genel
int b = 1, c = 2; // varsayılan (paket)
private double x; // özel
private static int xx; // statik, özel
public static int y; // statik, genel
public final int CONST1 = 5; // değiştirilemez
private static final int CONST2 = 6; // statik sabit
}Kitap sınıfında üç örnek alanı, bir sınıf alanı ve bir sabit vardır. Alanlar sınıfı ise erişim belirleyicilerle static ve final'ın birleşimlerini sıralar. int b = 1, c = 2; satırı aynı tipte iki alanı tek satırda tanımlar ve ikisine de ilk değer verir. Erişim belirleyici yazılmadığı için bu iki alan paket erişimlidir.
final ile tanımlanan alana değer yalnızca bir kez verilir; sonra değiştirilemez. Değer ya tanım satırında ya da yapıcıda verilir. Sonradan değer atamaya çalışan satır derlenmez:
public class FinalAta {
public final int CONST1 = 5;
void degistir() {
CONST1 = 6;
}
}FinalAta.java:4: error: cannot assign a value to final variable CONST1
CONST1 = 6;
^
1 errorSınıf sabitleri genellikle static final tanımlanır ve adları alt çizgiyle ayrılmış büyük harflerle yazılır: MAX_DEGER, PI. static olduğu için sabitin tek bir kopyası vardır; final olduğu için değeri değişmez. Kitap sınıfındaki PI yalnızca örnek olarak tanımlanmıştır. Java'nın hazır bir sabiti zaten vardır ve daha duyarlıdır: Math.PI.
Alanlar sınıfındaki CONST1 final'dır ama static değildir. Bu yüzden her nesnede ayrı bir kopyası bulunur ve hepsinin değeri 5'tir. Değeri herkes için aynı olan bir sabiti her nesnede ayrıca saklamak bellek israfıdır; böyle sabitler static final yapılır.
4.1.4.4İlklendirici blokları ve ilklendirme sırası
Bir alana ilk değer vermenin üç yolu vardır: tanım satırında, yapıcıda ve ilklendirici bloğunda. Üçüncüsü, tek satıra sığmayan hazırlık işleri için kullanılır.
İlklendirici blok (initializer block), sınıfın gövdesine doğrudan yazılan bir kod bloğudur. Sınıfa ait ilklendirici (static { ... }), sınıf belleğe ilk kez yüklenirken yalnızca bir kez çalışır. Örneğe ait ilklendirici ({ ... }), her nesne oluşturulduğunda yapıcının gövdesinden önce çalışır.
Sınıfa ait ilklendirici, static alanlara karmaşık bir ilk değer vermek için kullanılır; örneğin bir tabloyu döngüyle doldurmak için. Örneğe ait ilklendirici ise bütün yapıcılarda ortak olan işleri tek bir yerde toplar. Bir nesne oluşturulurken adımların sırası şudur:
- Sınıf ilk kez kullanılıyorsa
staticalanların ilk değerleri vestaticbloklar, yazılış sırasıyla bir kez çalışır. newnesne için bellek ayırır; bütün alanlar varsayılan değer alır.- Alanların tanım satırındaki ilk değerleri ve örnek ilklendirici blokları, yazılış sırasıyla çalışır.
- Yapıcının gövdesi çalışır.
new'de çalışır.Üçüncü adımdaki “yazılış sırası” önemlidir. Tanım satırındaki ilk değerler ve { } blokları ayrı gruplar olarak değil, sınıfta yukarıdan aşağıya hangi sırayla yazılmışlarsa o sırayla çalışır. Bir ilklendirici bloğu bir alan tanımından önce yazılmışsa önce blok çalışır. Aşağıdaki Kopek sınıfında iki tür ilklendirici de vardır.
public class Kopek {
String adi; // alan
Kopek(String adi) { // yapıcı metot
this.adi = adi;
System.out.println(adi);
}
String havla() { // örneğe ait metot
return adi + " haw haw haw!";
}
static { // sınıfa ait ilklendirici
System.out.println("Köpekler!");
}
{ // örneğe ait ilklendirici
System.out.println("Köpek oluşturuluyor.");
}
public static void main(String... strings) {
var d1 = new Kopek("Kara");
var d2 = new Kopek("Gece");
d2.havla();
}
}Köpekler!
Köpek oluşturuluyor.
Kara
Köpek oluşturuluyor.
Gecemain'deki iki yazım yeni olabilir. var (Java 10'dan beri) yerel değişkenin tipini sağdaki ifadeden çıkarır: var d1 = new Kopek("Kara"); satırı Kopek d1 = ... ile aynıdır. String... strings ise String[] strings yerine geçen değişken sayıda parametre yazımıdır; main için ikisi eşdeğerdir.
| Adım | Satır | Çağrı yığını | Değişkenler | Yeni çıktı |
|---|---|---|---|---|
| 1 | 11: System.out.println("Köpekler!"); | static ilklendirici | — | Köpekler! |
| 2 | 17: var d1 = new Kopek("Kara"); | main() | d1=?, d2=? | — |
| 3 | 14: System.out.println("Köpek oluşturuluyor."); | main() → Kopek("Kara") | d1=?, d2=? | Köpek oluşturuluyor. |
| 4 | 4: this.adi = adi; | main() → Kopek("Kara") | adi="Kara" | — |
| 5 | 5: System.out.println(adi); | main() → Kopek("Kara") | adi="Kara" | Kara |
| 6 | 18: var d2 = new Kopek("Gece"); | main() | d1=→#1, d2=→#2 | Köpek oluşturuluyor. ⏎ Gece |
| 7 | 19: d2.havla();8: return adi + " haw haw haw!"; | main() → havla() | d1=→#1, d2=→#2 | — |
Birinci adımda main daha başlamadan Kopek sınıfı yüklenir, çünkü main bu sınıfın metodudur. Sınıf yüklenirken static blok çalışır ve Köpekler! yazılır. İkinci adımda new Kopek("Kara") nesne için yer ayırır; adi alanı null'dır. Üçüncü adımda yapıcının gövdesinden önce örnek ilklendirici çalışır: Köpek oluşturuluyor. Dördüncü ve beşinci adımlarda yapıcının gövdesi alanı doldurur ve adı yazar: Kara.
adi alanı henüz null. Sağda iki nesne de hazır; havla() 2 numaralı nesne üzerinde çalışıyor.Altıncı adım ikinci nesne için ilklendiriciyi ve yapıcıyı yeniden çalıştırır: Köpek oluşturuluyor. ve Gece. static blok bu kez çalışmaz, çünkü sınıf zaten yüklüdür. Yedinci adımda d2.havla() çağrılır. Metot "Gece haw haw haw!" metnini döndürür, ama main bu değeri ne bir değişkene atar ne de yazdırır. Döndürülen değer kullanılmadan kaybolur ve ekrana hiçbir şey çıkmaz. Son satırın görünmesi için System.out.println(d2.havla()); yazmak gerekirdi.
Aşağıdaki program ne yazdırır?
class A {
static { System.out.print("S"); }
{ System.out.print("I"); }
A() { System.out.print("C"); }
}
public class Problem3 {
public static void main(String[] args) {
System.out.print("M");
new A();
new A();
System.out.println();
}
}main önce M yazar. A sınıfı main'in sınıfı olmadığı için program başlarken yüklenmez; ilk new A() ifadesinde yüklenir. O anda static blok bir kez çalışır: S. Ardından ilk nesne için örnek ilklendirici (I) ve yapıcı (C) çalışır. İkinci new A()'da sınıf zaten yüklüdür; yalnızca I ve C yazılır. Sonuç:
MSICIC4.1.5Metotlar
Metotlar sınıfın davranışını tanımlar. Metot tanımlama, parametre, dönüş değeri ve çağrı yığını temel programlamadan bilinir. Burada asıl soru metotların nesnelerle ilişkisidir: bir metot hangi nesnenin verisiyle çalışır ve bunu nasıl bilir?
4.1.5.1Örneğe ait ve sınıfa ait metotlar
Alanlarda olduğu gibi metotlarda da iki tür vardır. Örneğe ait metot bir nesneyle ilişkilidir ve o nesnenin alanlarını kullanır: telefon.aramaYap(...). Sınıfa ait metot (static metot) sınıfın kendisiyle ilişkilidir ve sınıf adı üzerinden çağrılır: CepTelefonu.sistemiGuncelle(...).
public class CepTelefonu {
private String model;
private static String isletimSistemi = "Sürüm 1";
// Örneğe ait metot
public void aramaYap(String numara) {
System.out.println(model + " arıyor: " + numara);
}
// Örneğe ait metot
public void mesajGonder(String alici, String mesaj) {
System.out.println(alici + " <- " + mesaj);
}
// Sınıfa ait metot (static)
public static void sistemiGuncelle(String yeniSurum) {
isletimSistemi = yeniSurum;
}
public static void main(String[] args) {
CepTelefonu telefon = new CepTelefonu();
telefon.model = "A54";
telefon.aramaYap("5551234567");
telefon.mesajGonder("Ayşe", "Geliyorum");
CepTelefonu.sistemiGuncelle("Sürüm 2");
System.out.println(isletimSistemi);
}
}A54 arıyor: 5551234567
Ayşe <- Geliyorum
Sürüm 2aramaYap ve mesajGonder örneğe ait metotlardır. aramaYap'ın gövdesindeki model, metodu çağıran telefonun modelidir. sistemiGuncelle ise static'tir: bütün telefonların ortak işletim sistemi sürümünü değiştirir ve belirli bir telefona bağlı değildir. Bu yüzden static bir metot nesneye özgü verilere, örneğin model'e, doğrudan erişemez. Hangi telefonun modeli? Sorunun cevabı yoktur, çünkü metot bir telefon üzerinden çağrılmamıştır. main'in son satırı isletimSistemi'ni sınıf adı yazmadan okur; main de aynı sınıfın static metodu olduğu için bu yazım geçerlidir.
4.1.5.2Değer döndüren bir metot: Eposta
Bir metodun başlığı onun ne yaptığını özetler. Aşağıdaki gonder metodunun adı gonder'dir. Üç String parametresi vardır: alıcı, konu ve içerik. Dönüş tipi boolean'dır ve gönderimin başarılı olup olmadığını bildirir.
public class Eposta {
public boolean gonder(String alici, String konu,
String icerik) {
// Gerçek gönderim yerine bir benzetim
if (alici == null || !alici.contains("@")) {
return false; // başarısız
}
System.out.println(alici + " <- " + konu);
return true; // başarılı
}
public static void main(String[] args) {
Eposta e = new Eposta();
boolean s1 = e.gonder("ali@ornek.com", "Ödev", "Ek");
boolean s2 = e.gonder("ali.ornek.com", "Ödev", "Ek");
System.out.println(s1 + " " + s2);
}
}ali@ornek.com <- Ödev
true falseMetot gerçek bir e-posta göndermez; gönderimi bir benzetimle temsil eder. Alıcı adresi null'sa ya da @ içermiyorsa false döndürür. Koşuldaki || kısa devre işlecidir: alici == null doğruysa sağ taraf hiç hesaplanmaz. Sıra ters yazılsaydı null bir adres için alici.contains("@") çağrısı NullPointerException fırlatırdı. Adres geçerliyse metot gönderimi yazar ve true döndürür. main iki gönderim dener; ikinci adreste @ yerine nokta vardır, bu yüzden ikinci çağrı ekrana bir şey yazmadan false döndürür.
boolean döndüren bir metodun her yolu bir return ile bitmelidir. Gövdesi henüz yazılmamış, yalnızca yorum içeren bir metot bile derlenmez:
public class EksikReturn {
public boolean gonder(String alici, String konu, String icerik) {
// daha sonra yazılacak
}
}EksikReturn.java:4: error: missing return statement
}
^
1 error4.1.5.3Tek kod, birçok nesne
Örneğe ait alanlar ve metotlar sınıf içinde static kelimesi olmadan tanımlanır. Her nesne alanların kendi kopyasını taşır; bu yüzden nesneler farklı verilerle çalışabilir. Örneğe ait bir üyeyi kullanmak için önce bir nesne gerekir: önce new, sonra nesne.metot(). Metodun kodu ise bütün nesneler için tektir. Her çağrıda hangi nesnenin verisiyle çalışılacağını this belirler.
selamVer()'in kodu sınıfta tek kopyadır. ogrenci1.selamVer() çağrısında this birinci nesneyi, ogrenci2.selamVer() çağrısında ikinci nesneyi gösterir.| Örneğe ait | static | |
|---|---|---|
| Kime ait? | Nesneye | Sınıfa |
| Çağrı | nesne.metot() | Sinif.metot() |
| Nesne gerekir mi? | Evet | Hayır |
this var mı? | Evet | Hayır |
| Örnek | str.length() | Math.sqrt(x) |
Tablonun son satırı tanıdık iki çağrıyı karşılaştırır. str.length() bir metnin uzunluğunu verir; hangi metnin uzunluğu olduğunu str belirler. Math.sqrt(x) ise bir sayının karekökünü verir. Bu iş hiçbir Math nesnesine bağlı değildir; gerekli bütün bilgi x parametresindedir. Bir metodun static olup olmayacağına bu ölçüye bakarak karar verilir: metot nesnenin alanlarını kullanıyorsa örneğe ait, yalnızca parametreleriyle çalışıyorsa static olabilir.
4.1.5.4Nokta operatörü ve nesne parametresi
Nesne oluşturulduktan sonra alanlarına ve metotlarına nokta operatörüyle erişilir: nesne.metot(...), nesne.alan. Bir metot parametre olarak başka bir nesne de alabilir. Bu durumda metodun içinde iki nesne vardır: metodun çağrıldığı nesne ve parametre olarak gelen nesne.
public class Telefon {
String numara;
public Telefon(String numara) {
this.numara = numara;
}
public void aramaYap(Telefon telefon) {
System.out.println(numara + " arıyor: " + telefon.numara);
}
public void mesajGonder(Telefon telefon, String mesaj) {
System.out.println(telefon.numara + " <- " + mesaj);
}
public void kameraAc() {
System.out.println(numara + " kamera açtı.");
}
public static void main(String[] args) {
Telefon telefon1 = new Telefon("5551234567");
Telefon telefon2 = new Telefon("5051234567");
telefon1.aramaYap(telefon2);
telefon2.mesajGonder(telefon1, "Merhaba!");
telefon1.kameraAc();
}
}5551234567 arıyor: 5051234567
5551234567 <- Merhaba!
5551234567 kamera açtı.telefon1.aramaYap(telefon2) çağrısında metodun gövdesindeki yalın numara, metodun çağrıldığı nesnenin, yani telefon1'in numarasıdır. telefon.numara ise parametre olarak gelen nesnenin, yani telefon2'nin numarasıdır. Birinci satır bu yüzden “5551234567 arıyor: 5051234567” biçimindedir.
İkinci çağrı dikkatle okunmalıdır. telefon2.mesajGonder(telefon1, "Merhaba!") çağrısında metodu çağıran telefon2'dir, ama gövde yalnızca parametredeki telefonun numarasını kullanır: telefon.numara, yani telefon1'in numarası. Çıktıda 5551234567 görünmesinin nedeni budur. Metodun adı “mesaj gönder” olsa da yazdırılan numara alıcınındır; göndereni gösteren numara gövdede hiç kullanılmamıştır. Bir metodun gövdesinde yalın alan adı ile parametre üzerinden erişilen alan adı farklı nesnelere aittir; bu ayrım gözden kaçarsa yanlış nesnenin verisi okunur.
Parametre olarak gelen nesnenin private alanlarına da erişilebilir, yeter ki erişen kod aynı sınıfın içinde olsun. numara burada paket erişimlidir, ama private olsaydı telefon.numara yazımı yine derlenirdi. Erişim kuralı nesne başına değil, sınıf başınadır.
4.1.5.5Dikdörtgen sınıfı
Prosedürel ve nesne yönelimli yaklaşımın karşılaştırmasındaki dikdörtgen örneği şimdi tam bir sınıf olarak yazılabilir.
public class Dikdortgen {
private double uzunluk; // dikdörtgenin uzunluğu
private double genislik; // dikdörtgenin genişliği
// Yapıcı metot
public Dikdortgen(double uzunluk, double genislik) {
this.uzunluk = uzunluk;
this.genislik = genislik;
}
public double alanHesapla() { // alanı hesaplar
return uzunluk * genislik;
}
public double cevreHesapla() { // çevreyi hesaplar
return 2 * (uzunluk + genislik);
}
}Alanlar private'tır; dışarıdan yalnızca metotlarla kullanılır. Yapıcı iki kenarı alır ve nesneye kaydeder. alanHesapla() ve cevreHesapla() parametre almaz, çünkü ihtiyaç duydukları veriler nesnenin içindedir. Prosedürel karşılığı alanHesapla(5.0, 3.0) olurdu; nesneyle yazımı dikdortgen.alanHesapla() olur. Aşağıdaki test sınıfı iki dikdörtgen oluşturur.
public class DikdortgenTesti {
public static void main(String[] args) {
Dikdortgen dikdortgen = new Dikdortgen(5.0, 3.0);
Dikdortgen kare = new Dikdortgen(5.0, 5.0);
double alan = dikdortgen.alanHesapla();
double cevre = dikdortgen.cevreHesapla();
System.out.println("Alanı: " + alan
+ " Çevresi: " + cevre);
alan = kare.alanHesapla();
cevre = kare.cevreHesapla();
System.out.println("Alanı: " + alan
+ " Çevresi: " + cevre);
}
}Alanı: 15.0 Çevresi: 16.0
Alanı: 25.0 Çevresi: 20.0| Adım | Satır | Çağrı yığını | Değişkenler | Yeni çıktı |
|---|---|---|---|---|
| 1 | 3: Dikdortgen dikdortgen = new Dikdortgen(5.0, 3.0);4: Dikdortgen kare = new Dikdortgen(5.0, 5.0); | main() | dikdortgen=→#1, kare=→#2 | — |
| 2 | 5: double alan = dikdortgen.alanHesapla(); | main() → alanHesapla() | dikdortgen=→#1, kare=→#2 | — |
| 3 | 6: double cevre = dikdortgen.cevreHesapla(); | main() → cevreHesapla() | alan=15.0 | — |
| 4 | 7: System.out.println("Alanı: " + alan8: + " Çevresi: " + cevre); | main() | alan=15.0, cevre=16.0 | Alanı: 15.0 Çevresi: 16.0 |
| 5 | 9: alan = kare.alanHesapla();10: cevre = kare.cevreHesapla(); | main() → alanHesapla() | alan=25.0, cevre=20.0 | — |
| 6 | 11: System.out.println("Alanı: " + alan12: + " Çevresi: " + cevre); | main() | alan=25.0, cevre=20.0 | Alanı: 25.0 Çevresi: 20.0 |
Birinci adımda iki nesne oluşur; her birinin kendi uzunluk ve genislik değerleri vardır. İkinci adımda dikdortgen.alanHesapla() çağrılır. Metodun çerçevesinde this 1 numaralı nesneyi gösterir ve metot 5.0 × 3.0 = 15.0 döndürür. Üçüncü adımda cevreHesapla() aynı nesne için 2 × (5.0 + 3.0) = 16.0 bulur. Dördüncü adımda iki double değer metne eklenip yazılır; double değerler + ile birleştirilirken ondalık kısımla birlikte, 15.0 biçiminde yazılır.
dikdortgen.alanHesapla() çalışıyor: this 1 numaralı nesneyi gösteriyor. Sağda aynı metot kare üzerinden çağrılmış; this 2 numaralı nesneyi gösteriyor, alan ve cevre yeni değerlerini almış.Beşinci adımda aynı iki metot kare üzerinden çağrılır. this bu kez 2 numaralı nesneyi gösterir: 5.0 × 5.0 = 25.0 ve 2 × (5.0 + 5.0) = 20.0. Bu değerler alan ve cevre değişkenlerinin eski değerlerinin üzerine yazılır. Altıncı adımda ikinci satır yazdırılır. println çağrısının iki satıra bölünmüş olması bir sorun değildir: Java'da bir deyim noktalı virgül gelene kadar sürer.
Her iki metodun maliyeti sabittir: birkaç aritmetik işlem yapar ve ek bellek kullanmaz. Nesneye bir alan eklemek, örneğin dikdörtgenin rengini tutmak, bu metotları etkilemez. Prosedürel sürümde ise renk bilgisini taşımak için her çağrıya yeni bir parametre eklemek gerekirdi.
4.1.6static üyeler
static kelimesi alanlarda da metotlarda da karşımıza çıktı. Kuralları bir araya toplamak, en sık karşılaşılan derleme hatasının nedenini de açıklar.
4.1.6.1Nesnesiz erişim
static üye (static member), static anahtar kelimesiyle tanımlanan ve herhangi bir nesneye değil sınıfın kendisine ait olan alan ya da metottur. static üyeye erişmek için nesne oluşturmak gerekmez; sınıf adı ve nokta kullanılır: SinifAdi.alanAdi, SinifAdi.metotAdi(argumanlar).
public class Matematik {
// Statik toplama metodu
public static int toplama(int sayi1, int sayi2) {
return sayi1 + sayi2;
}
public static void main(String[] args) {
int sonuc = Matematik.toplama(5, 3);
System.out.println("Toplama Sonucu: " + sonuc);
System.out.println(Math.max(4, 9));
System.out.println(Math.sqrt(16.0));
}
}Toplama Sonucu: 8
9
4.0Matematik.toplama(5, 3) çağrısı hiçbir Matematik nesnesi oluşturmadan yapılır. main aynı sınıfın içinde olduğu için sınıf adı yazılmadan toplama(5, 3) da yazılabilirdi. Math.max ve Math.sqrt aynı türden çağrılardır: Math sınıfının static metotları. Java'nın hazır kütüphanesinde bunun çok örneği vardır: Math.PI, Integer.parseInt("12"), Integer.MAX_VALUE. Her programın başladığı main de static'tir. Program başladığında JVM (Java Sanal Makinesi) henüz hiçbir nesne oluşturmamıştır; main'i nesnesiz çağırabilmesi için metodun sınıfa ait olması gerekir.
static bir üyeye nesne üzerinden de erişilebilir: h1.toplamHesapSayisi. Java bu yazımı kabul eder, ama tercih edilmez. Kodu okuyan kişi üyenin nesneye ait olduğunu sanır ve h1'in sayacının h2'ninkinden farklı olabileceğini düşünür. Derleyici bu yazımı BankaHesabi.toplamHesapSayisi olarak işler; hangi nesnenin kullanıldığı önemsizdir. -Xlint seçeneğiyle derlenirse static variable should be qualified by type name uyarısı da verir.
4.1.6.2static bağlamın kuralları
Örneğe ait bir metot hem örnek üyelerini hem static üyeleri doğrudan kullanabilir. Böyle bir metot her zaman bir nesne üzerinden çağrılır; o nesne this'tir ve örnek alanları onun alanlarıdır. static sınıf alanı da zaten tektir. static bir metodun ise this'i yoktur. Bu metot örnek üyelerine yalnızca açıkça verilmiş bir nesne üzerinden ulaşabilir. Tablo 4.1.10 dört durumu özetler.
| Nereden | Örnek alanı / metodu | static alan / metot |
|---|---|---|
| Örneğe ait metottan | ✓ doğrudan (ad, this.ad) | ✓ doğrudan |
static metottan (main dahil) | ✗ ancak bir nesne üzerinden (ogr.ad) | ✓ doğrudan |
Kuralın ihlali, nesne yönelimli programlamaya yeni başlayanların en sık gördüğü derleme hatasıdır. main'den örneğe ait bir metodu doğrudan çağırmak:
public class Selam {
private String ad = "Ali";
public void selamVer() {
System.out.println("Merhaba " + ad);
}
public static void main(String[] args) {
selamVer();
}
}Selam.java:7: error: non-static method selamVer() cannot be referenced from a static context
selamVer();
^
1 errorstatic bir metottan örneğe ait bir alanı doğrudan okumak:
public class Selam2 {
private String ad = "Ali";
public static void yaz() {
System.out.println(ad);
}
}Selam2.java:4: error: non-static variable ad cannot be referenced from a static context
System.out.println(ad);
^
1 errorİki iletide de aynı ifade geçer: cannot be referenced from a static context. “static bağlam”, this'in olmadığı yer demektir. selamVer() çağrısı aslında this.selamVer()'dir, ad okuması da this.ad'dır. main'de this olmadığı için derleyici hangi nesnenin metodunun ya da alanının kastedildiğini bilemez. Açıkça this yazmak da bir şey değiştirmez; ileti bu kez non-static variable this cannot be referenced from a static context olur.
Çözümün iki yolu vardır ve hangisinin seçileceği tasarıma bağlıdır. Metot gerçekten bir nesnenin verisine ihtiyaç duyuyorsa çağrı bir nesne üzerinden yapılır: new Selam().selamVer();. Metot hiçbir nesnenin verisini kullanmıyorsa static yapılır. İkinci yol yalnızca metodun gerçekten nesneden bağımsız olduğu durumda doğrudur. Derleme hatasını susturmak için bütün metotları ve alanları static yapmak programı yeniden prosedürel tarza döndürür. Her nesnenin ayrı verisi olması gereken yerde tek bir ortak kopya kalır.
4.1.7Referanslar ve this
Değişkenin nesneyi değil referansı tuttuğu bilgisi, ilk bakışta yalnızca bir ayrıntı gibi görünür. Ama atamanın, karşılaştırmanın ve metotlara nesne aktarmanın nasıl davrandığını bu ayrıntı belirler.
4.1.7.1Referans atama ve takma ad
Java'da nesneler referanslar aracılığıyla işlenir. Bir referans değişkenini başka bir değişkene atamak nesneyi kopyalamaz; yalnızca referansı kopyalar. Atamadan sonra iki değişken aynı nesneyi gösterir.
Takma ad (alias), aynı nesneyi gösteren birden çok referans değişkeninin her biridir. Nesne bir tanedir; takma adlardan biri üzerinden yapılan değişiklik ötekilerden de görünür.
Gündelik hayatta da benzer durumlar vardır. Kütüphane rafındaki etiket kitabın kendisi değildir; kitabın yerini gösterir. Bir arabanın iki anahtarı olabilir; iki anahtar da aynı arabayı açar. Anahtarlardan biriyle arabaya binip koltuğu geri itmek, öteki anahtarla binen kişinin de geri itilmiş koltuk bulması demektir. Aşağıdaki program marka alanı private olan, erişimci ve değiştiricisi bulunan bir Araba sınıfıyla bu durumu gösterir.
public class Araba {
private String marka;
public Araba(String marka) {
this.marka = marka;
}
public String getMarka() {
return marka;
}
public void setMarka(String marka) {
this.marka = marka;
}
}public static void main(String[] args) {
Araba araba1 = new Araba("Toyota");
Araba araba2 = araba1;
System.out.println("Araba 1: " + araba1.getMarka());
System.out.println("Araba 2: " + araba2.getMarka());
araba2.setMarka("Ford");
System.out.println("Araba 1: " + araba1.getMarka());
araba1 = null;
System.out.println("Araba 2: " + araba2.getMarka());
}Araba 1: Toyota
Araba 2: Toyota
Araba 1: Ford
Araba 2: Ford| Adım | Satır | Çağrı yığını | Değişkenler | Yeni çıktı |
|---|---|---|---|---|
| 1 | 2: Araba araba1 = new Araba("Toyota"); | main() | araba1=→#1, araba2=? | — |
| 2 | 3: Araba araba2 = araba1; | main() | araba1=→#1, araba2=→#1 | — |
| 3 | 4: System.out.println("Araba 1: " + araba1.getMarka());5: System.out.println("Araba 2: " + araba2.getMarka()); | main() | araba1=→#1, araba2=→#1 | Araba 1: Toyota ⏎ Araba 2: Toyota |
| 4 | 6: araba2.setMarka("Ford"); | main() → setMarka("Ford") | araba1=→#1, araba2=→#1 | — |
| 5 | 7: System.out.println("Araba 1: " + araba1.getMarka()); | main() | araba1=→#1, araba2=→#1 | Araba 1: Ford |
| 6 | 8: araba1 = null; | main() | araba1=null, araba2=→#1 | — |
| 7 | 9: System.out.println("Araba 2: " + araba2.getMarka()); | main() | araba1=null, araba2=→#1 | Araba 2: Ford |
Birinci adımda heap'te tek bir nesne oluşur ve araba1 onu gösterir. İkinci adımdaki Araba araba2 = araba1; satırı yeni bir nesne oluşturmaz. araba1'in kutusundaki referans araba2'nin kutusuna kopyalanır. Heap'te hâlâ tek nesne vardır; artık ona giden iki ok bulunur. Üçüncü adımda iki değişken üzerinden de marka okunur ve ikisi de Toyota verir: okunan alan aynı alandır.
araba2 = araba1 sonrası iki ok tek nesneye gider; araba2.setMarka("Ford") ortak nesneyi değiştirir; araba1 = null yalnızca bir oku keser, nesne araba2 üzerinden yaşamaya devam eder.Dördüncü adımda araba2.setMarka("Ford") çağrılır. Metodun this'i araba2'nin gösterdiği nesnedir, yani tek nesne. Beşinci adımda araba1 üzerinden okunan marka da Ford'dur: değişiklik paylaşılmıştır. Bu davranış iki değişkenin aynı nesneyi gösterdiği bilinmezse şaşırtıcıdır. Program araba2'yi değiştirdiğini düşünür, ama aslında araba1'in gösterdiği nesneyi de değiştirmiştir.
Altıncı adımda araba1 = null; çalışır. Bu satır nesneyi silmez; yalnızca araba1 değişkeninin nesneyle bağını koparır. Nesne yerinde durur ve araba2 onu göstermeye devam eder. Yedinci adımda araba2 üzerinden okunan marka Ford'dur.
Atamanın maliyeti de bu yüzden sabittir. Nesnenin kaç alanı olursa olsun b = a yalnızca bir referans kopyalar. Nesnenin gerçekten kopyalanması gerekiyorsa yeni bir nesne oluşturulup alanlar tek tek aktarılır; Java bunu atamayla kendiliğinden yapmaz.
Aynı kural metotlara nesne aktarırken de geçerlidir. Java bütün argümanları değer ile aktarır. Argüman bir nesne referansıysa kopyalanan değer referanstır. Metot bu kopya üzerinden ortak nesneyi değiştirebilir, ama parametresine yeni bir nesne atamak çağıranın değişkenini etkilemez:
class Kutu { int deger; }
public class Problem4 {
static void degistir(Kutu k) {
k.deger = 100;
k = new Kutu();
k.deger = 7;
}
public static void main(String[] args) {
Kutu x = new Kutu();
x.deger = 1;
degistir(x);
System.out.println(x.deger);
}
}100degistir(x) çağrısında k, x'teki referansın kopyasını alır. k.deger = 100; ortak nesneye yazar; x de aynı nesneyi gösterdiği için değişiklik main'den görünür. k = new Kutu(); ise yalnızca yerel kopyayı yeni bir nesneye yöneltir. Bundan sonraki k.deger = 7; yeni nesneye yazar; x'in nesnesi etkilenmez. Program 100 yazar.
4.1.7.2null ve NullPointerException
null, bir referans değişkeninin hiçbir nesneyi göstermediği anlamına gelir. null bir referans üzerinden alan okumak ya da metot çağırmak, okunacak bir nesne olmadığı için çalışma zamanında hataya yol açar.
public class Bos {
String ad;
public static void main(String[] args) {
Bos b = null;
System.out.println(b.ad.length());
}
}Exception in thread "main" java.lang.NullPointerException: Cannot read field "ad" because "<local1>" is null
at Bos.main(Bos.java:6)Program derlenir, çünkü b'ye bir değer verilmiştir: null. Çalışırken 6. satırda b.ad okunmak istenir ve NullPointerException fırlatılır. JDK 15'ten beri ileti, hatanın yerini varsayılan olarak ayrıntılı söyler: ad alanı okunamamıştır, çünkü değişken null'dır. İletideki "<local1>" yazımı, main'in 1 numaralı yerel değişkenini, yani b'yi gösterir. Program hata ayıklama bilgisiyle (javac -g) derlenseydi değişkenin adı görünürdü: because "b" is null.
Yerel değişkene hiç değer verilmeden kullanılırsa durum farklıdır: program derlenmez bile (variable sb might not have been initialized). NullPointerException ise derleyicinin yakalayamadığı, çalışma sırasında ortaya çıkan bir hatadır. Bir alan bunun en sık kaynağıdır: String tipindeki bir alana değer verilmemişse varsayılan değeri null'dır ve üzerinden çağrılan ilk metot programı durdurur. Önlem, referansı kullanmadan önce denetlemektir:
public class Bos2 {
String ad;
public static void main(String[] args) {
Bos2 b = new Bos2();
if (b.ad != null) {
System.out.println(b.ad.length());
} else {
System.out.println("ad henüz verilmemiş");
}
System.out.println(b.ad.length());
}
}ad henüz verilmemiş
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local1>.ad" is null
at Bos2.main(Bos2.java:11)if bloğu ad'ın null olduğunu görür ve güvenli yolu seçer. Son satır ise aynı alanı denetimsiz kullanır. Bu kez ileti "<local1>.ad" der: b değil, b'nin ad alanı null'dır.
null referans hiçbir nesneyi göstermez; b.ad okunmak istenince NullPointerException fırlatılır. Sağda, hiçbir referansın göstermediği nesneyi çöp toplayıcı siler.Hiçbir referansın göstermediği nesneye programdan bir daha ulaşılamaz. Böyle nesnelerin belleğini JVM'in çöp toplayıcısı (garbage collector) zamanı gelince geri alır. Java'da nesneyi elle silmek gerekmez ve bunun bir yolu da yoktur. Referans atama izlemesinde araba2 de null yapılsaydı nesneyi gösteren son referans kaybolur ve nesne çöp toplamaya uygun hâle gelirdi. Çöp toplayıcının ne zaman çalışacağı önceden bilinmez; program yalnızca nesnenin artık ulaşılamaz olduğundan emin olabilir.
4.1.7.3this anahtar kelimesi
this, örneğe ait bir metodun ya da yapıcının içinde, metodun çağrıldığı (yapıcıda ise oluşturulmakta olan) nesneyi gösteren referanstır. static metotlarda this yoktur.
this en sık yapıcılarda, alanın adı ile parametrenin adı aynı olduğunda kullanılır: this.ad = ad;. Bu durumda yalın ad adı parametreyi gösterir, çünkü en yakın tanım parametredir. Parametre aynı adlı alanı gölgeler. Alana ulaşmak için this.ad yazılır. this unutulursa ne olur?
public class Golge {
private String ad;
public Golge(String ad) {
ad = ad; // this unutuldu!
}
public static void main(String[] args) {
Golge g = new Golge("Ali");
System.out.println(g.ad);
}
}nullad = ad; parametreyi kendine atar; nesnenin ad alanı null kalır. this.ad = ad; yazılsaydı "Ali" nesnenin alanına giderdi.ad = ad; satırının iki yanındaki ad da parametredir. Satır parametreyi kendine atar ve hiçbir şeyi değiştirmez. Nesnenin ad alanına hiç dokunulmaz; varsayılan değeri olan null'da kalır. Program null yazar. En tehlikelisi şudur: derleyici bu satır için hata vermez. Kod derlenir ve çalışır; hata ancak alan kullanıldığında, belki bambaşka bir yerde NullPointerException olarak ortaya çıkar.
Alan final olsaydı derleyici hatayı yakalardı. private final String ad; tanımında alan yapıcıda mutlaka değer almalıdır. ad = ad; alana değer vermediği için derleyici variable ad might not have been initialized iletisini verir. Hiç değişmeyecek alanları final yapmak bu yüzden yalnızca belgeleme değil, bir güvenlik önlemidir.
Gölgeleme yoksa this yazmak isteğe bağlıdır. Metot gövdesindeki yalın model, derleyici için this.model demektir. Aşağıdaki izleme bunu adım adım gösterir.
public class Ogrenci {
private String ad;
public Ogrenci(String ad) {
this.ad = ad;
}
public void selamVer() {
System.out.println("Merhaba, ben " + this.ad);
}
public static void main(String[] args) {
Ogrenci ogrenci1 = new Ogrenci("Ali");
Ogrenci ogrenci2 = new Ogrenci("Ayşe");
ogrenci1.selamVer();
ogrenci2.selamVer();
}
}Merhaba, ben Ali
Merhaba, ben Ayşe| Adım | Satır | Çağrı yığını | Değişkenler | Yeni çıktı |
|---|---|---|---|---|
| 1 | 13: Ogrenci ogrenci1 = new Ogrenci("Ali");4: public Ogrenci(String ad) {5: this.ad = ad; | main() → Ogrenci("Ali") | ad="Ali" | — |
| 2 | 14: Ogrenci ogrenci2 = new Ogrenci("Ayşe"); | main() | ogrenci1=→#1, ogrenci2=→#2 | — |
| 3 | 15: ogrenci1.selamVer();8: public void selamVer() {9: System.out.println("Merhaba, ben " + this.ad); | main() → selamVer() | ogrenci1=→#1, ogrenci2=→#2 | Merhaba, ben Ali |
| 4 | 16: ogrenci2.selamVer();8: public void selamVer() {9: System.out.println("Merhaba, ben " + this.ad); | main() → selamVer() | ogrenci1=→#1, ogrenci2=→#2 | Merhaba, ben Ayşe |
Birinci adımda yapıcı çalışır; this yeni oluşturulan nesnedir ve this.ad = ad; ile alana "Ali" yazılır. İkinci adımda ikinci nesne aynı yapıcıyla oluşur; o sırada this 2 numaralı nesneyi gösterir. Üçüncü adımda ogrenci1.selamVer() çağrılır. Noktanın solundaki nesne metodun this'i olur ve Merhaba, ben Ali yazılır. Dördüncü adımda aynı kod, farklı bir this ile çalışır ve Merhaba, ben Ayşe yazar. Bu metotta this.ad yerine yalnızca ad yazmak aynı sonucu verirdi, çünkü metodun ad adlı bir parametresi yoktur.
ogrenci1.selamVer(): this 1 numaralı nesneyi gösterir. Sağda ogrenci2.selamVer(): aynı metot, ama this 2 numaralı nesneyi gösterir.4.1.7.4this ile nesneyi aktarmak
this bir referans olduğu için başka bir metoda argüman olarak da verilebilir. Bir nesne kendini başka bir nesneye tanıtmak istediğinde bu yol kullanılır.
public class Takim {
private String ad;
public Takim(String ad) {
this.ad = ad;
}
public void oyuncuEkle(Oyuncu oyuncu) {
oyuncu.takimaKatil(this); // bu nesne
}
public String getAd() {
return ad;
}
public static void main(String[] args) {
Takim takim = new Takim("Giresunspor");
Oyuncu oyuncu = new Oyuncu("Mert");
takim.oyuncuEkle(oyuncu);
}
}Aynı dosyada, public olmayan ikinci bir sınıf:
class Oyuncu {
private String ad;
Oyuncu(String ad) {
this.ad = ad;
}
void takimaKatil(Takim takim) {
String t = takim.getAd();
System.out.println(ad + " -> " + t);
}
}Mert -> Giresunsportakim.oyuncuEkle(oyuncu) çağrısında metodun this'i takim nesnesidir. Metot gövdesindeki oyuncu.takimaKatil(this) bu referansı oyuncuya verir. takimaKatil'in takim parametresi artık aynı takım nesnesini gösterir ve takim.getAd() ile adı okur. Bu sırada iki çerçevede iki ayrı this vardır: oyuncuEkle'nin this'i takım, takimaKatil'in this'i oyuncudur. takimaKatil'deki yalın ad da oyuncunun adıdır. Çıktı Mert -> Giresunspor olur.
Oyuncu sınıfı Takim.java dosyasının içinde, public olmadan tanımlanmıştır. Bir dosyada yalnızca bir public sınıf bulunabilir, ama yardımcı sınıflar aynı dosyada yer alabilir. Bu sınıflar paket erişimlidir; aynı klasördeki öteki sınıflar onları kullanabilir.
4.1.7.5this(...) ile yapıcı zinciri
Bir sınıfın birden çok yapıcısı varsa çoğu aynı işleri yapar. Aynı atamaları her yapıcıya kopyalamak yerine yapıcılar birbirine devredebilir.
this(...), bir yapıcının içinden aynı sınıfın başka bir yapıcısını çağıran yazımdır. Bu yolla kurulan yapıcı dizisine yapıcı zinciri (constructor chaining) denir. this(...) yapıcının ilk deyimi olmalıdır.
class Calisan {
private String ad;
private double maas;
Calisan(String ad) {
this(ad, 20000.0); // aynı sınıfın yapıcısı
}
Calisan(String ad, double maas) {
this.ad = ad; // ilk değerler tek yerde
this.maas = maas;
}
}Elif 20000.0Ortak ilk değer atamaları tek bir yapıcıda toplanır; burada bu yapıcı Calisan(String, double)'dır. Öteki yapıcı eksik argümanı ekleyerek ona devreder: this(ad, 20000.0). Maaş verilmeden oluşturulan her çalışan 20000.0 ile başlar. Varsayılan maaş değişirse tek bir sayı değişir. Atama kuralları değişirse, örneğin adın boş olmaması denetlenmek istenirse, denetim tek bir yapıcıya yazılır.
Zincirin iki kuralı vardır ve ikisini de derleyici denetler. this(...) yapıcının ilk satırında olmalıdır:
public class ThisIlk {
int x;
ThisIlk() { x = 1; this(2); }
ThisIlk(int x) { this.x = x; }
}ThisIlk.java:3: error: call to this must be first statement in constructor
ThisIlk() { x = 1; this(2); }
^
1 errorYapıcılar birbirini döngüsel çağıramaz; böyle bir zincir hiç bitmezdi:
public class ThisDongu {
ThisDongu() { this(1); }
ThisDongu(int x) { this(); }
}ThisDongu.java:3: error: recursive constructor invocation
ThisDongu(int x) { this(); }
^
1 errorZincir kaç yapıcıdan geçerse geçsin tek bir nesne kurulur. Çağrılan yapıcının gövdesi önce biter; sonra çağıranın kalan satırları çalışır. Aşağıdaki izleme bunu gösterir.
class Kitap {
String ad;
String yazar;
int sayfaSayisi;
public Kitap(String ad) {
this.ad = ad;
}
public Kitap(String ad, String yazar) {
this(ad); // yapıcı zincirleme
this.yazar = yazar;
}
}
// main içinde:
Kitap k2 = new Kitap("Nutuk", "Atatürk");
System.out.println(k2.ad + " / "
+ k2.yazar + " / " + k2.sayfaSayisi);Nutuk / Atatürk / 0| Adım | Satır | Çağrı yığını | Değişkenler | Yeni çıktı |
|---|---|---|---|---|
| 1 | 14: Kitap k2 = new Kitap("Nutuk", "Atatürk"); | main() | k2=? | — |
| 2 | 8: public Kitap(String ad, String yazar) { | main() → Kitap(ad, yazar) | ad="Nutuk", yazar="Atatürk" | — |
| 3 | 9: this(ad); // yapıcı zincirleme | main() → Kitap(ad, yazar) → Kitap(ad) | ad="Nutuk" | — |
| 4 | 6: this.ad = ad; | main() → Kitap(ad, yazar) → Kitap(ad) | ad="Nutuk" | — |
| 5 | 10: this.yazar = yazar; | main() → Kitap(ad, yazar) | ad="Nutuk", yazar="Atatürk" | — |
| 6 | 14: Kitap k2 = new Kitap("Nutuk", "Atatürk"); | main() | k2=→Kitap | — |
| 7 | 15: System.out.println(k2.ad + " / "16: + k2.yazar + " / " + k2.sayfaSayisi); | main() | k2=→Kitap | Nutuk / Atatürk / 0 |
Birinci adımda new heap'te yeni bir Kitap nesnesi ayırır; alanlar varsayılan değerlerini alır: null, null, 0. İkinci adımda argümanlar iki String olduğu için iki parametreli yapıcı seçilir; this yeni nesneyi gösterir. Üçüncü adımda this(ad) çalışır ve tek parametreli yapıcı aynı nesne için çağrılır. Yığında artık iki yapıcı çerçevesi üst üste durur; ikisinin this'i de aynı nesnedir.
this'i de aynı nesneyi gösteriyor. Sağda iç yapıcı bitmiş, dış yapıcı yazar alanını doldurmuş.Dördüncü adımda iç yapıcı this.ad = ad; ile ad alanına "Nutuk" yazar ve biter. Beşinci adımda dış yapıcı kaldığı yerden sürer ve kalan satırını çalıştırır: this.yazar = yazar;. Altıncı adımda iki yapıcı da bitmiştir; new ifadesinin değeri olan referans k2'ye atanır. Yedinci adımda alanlar okunur. sayfaSayisi'na hiçbir yapıcı değer vermediği için varsayılan değeri yazılır: Nutuk / Atatürk / 0.
Bu sırada bir yan sonuç da vardır. Alanların tanım satırındaki ilk değerleri ve örnek ilklendiricileri, zincirin this(...) ile başlamayan yapıcısında, o yapıcının gövdesinden hemen önce bir kez çalışır. this(...) ile başlayan yapıcı bu işi zincirin sonundaki yapıcıya bırakır. Böylece zincir ne kadar uzun olursa olsun ilklendiriciler her nesne için tam bir kez çalışır.
4.1.8Nesneleri karşılaştırma: == ve equals
İki int değişkeni == ile karşılaştırmak sayıları karşılaştırır. İki referans değişkeni == ile karşılaştırmak ise nesneleri değil, değişkenlerin kutularındaki referansları karşılaştırır. Soru “bu iki nesne aynı içerikte mi?” değil, “bu iki değişken aynı nesneyi mi gösteriyor?” olur.
Referans eşitliği, iki referansın aynı nesneyi göstermesidir ve == ile sınanır. İçerik eşitliği, iki nesnenin anlamca aynı değeri taşımasıdır ve equals() metoduyla sınanır. equals'ın neyi “aynı” sayacağını sınıfı yazan programcı belirler.
Aşağıdaki programda Kisi sınıfı equals metodunu kendisi yazar: iki kişinin adı ve yaşı aynıysa onları eşit sayar. Kisi0 sınıfı ise equals yazmaz. Programın son bölümü klavyeden bir sözcük okur ve onu bir metin sabitiyle karşılaştırır.
import java.util.Objects;
import java.util.Scanner;
class Kisi {
private String ad;
private int yas;
Kisi(String ad, int yas) {
this.ad = ad;
this.yas = yas;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Kisi)) return false;
Kisi k = (Kisi) o;
return yas == k.yas && Objects.equals(ad, k.ad);
}
@Override
public int hashCode() {
return Objects.hash(ad, yas);
}
@Override
public String toString() {
return ad + " (" + yas + ")";
}
}
class Kisi0 {
String ad;
Kisi0(String ad) { this.ad = ad; }
}
public class Esitlik {
public static void main(String[] args) {
Kisi k1 = new Kisi("Ali", 20);
Kisi k2 = new Kisi("Ali", 20);
Kisi k3 = k1;
System.out.println(k1 == k3);
System.out.println(k1 == k2);
System.out.println(k1.equals(k2));
System.out.println(k1.hashCode() == k2.hashCode());
System.out.println(k1);
Kisi0 a = new Kisi0("Ali");
Kisi0 b = new Kisi0("Ali");
System.out.println(a.equals(b));
Scanner klavye = new Scanner(System.in);
String okunan = klavye.next();
System.out.println(okunan == "Java");
System.out.println(okunan.equals("Java"));
}
}Klavyeden Java girildiğinde program şunu yazar:
true
false
true
true
Ali (20)
false
false
truek1 ve k3 aynı Kisi nesnesini, k2 aynı içerikli ayrı bir nesneyi gösterir.| İfade | Sonuç | Neden |
|---|---|---|
k1 == k3 | true | İki değişken aynı nesneyi gösterir. |
k1 == k2 | false | Farklı nesneler; içerikleri aynı olsa da referanslar farklı. |
k1.equals(k2) | true | equals yeniden yazılmış; ad ve yaş aynı. |
equals, hashCode, toString ve getClass metotları her sınıfa Java'nın en üstteki sınıfı Object'ten gelir. Object'in equals sürümü yalnızca referansları karşılaştırır; yani == gibi davranır. Kisi0 sınıfı equals yazmadığı için a.equals(b) false verir: iki ayrı nesne vardır ve Object'in sürümü içeriğe bakmaz. Bir sınıfın nesneleri içerikleriyle karşılaştırılacaksa equals o sınıfta yeniden yazılmalıdır.
Kisi'deki equals üç adımda çalışır. Önce this == o ile aynı nesne olup olmadığına bakar; öyleyse hemen true döner. Sonra instanceof ile gelen nesnenin bir Kisi olup olmadığını denetler; değilse, ya da o null'sa, false döner. Son adımda alanları karşılaştırır. yas bir int olduğu için == ile, ad bir String olduğu için Objects.equals ile karşılaştırılır. Bu yardımcı metot iki taraftan biri null olduğunda da güvenle çalışır. Parametrenin tipi Kisi değil Object'tir. Tip Kisi yazılsaydı metot Object'in equals'ını yeniden yazmış olmaz, yalnızca aşırı yüklemiş olurdu; @Override işareti bu hatayı derleme sırasında yakalar.
equals yeniden yazılınca hashCode da yazılmalıdır. Kural şudur: equals'a göre eşit olan iki nesnenin hashCode değerleri de eşit olmalıdır. Programdaki dördüncü satır bunu doğrular. Kural çiğnenirse HashMap ve HashSet gibi koleksiyonlar eşit nesneleri farklı yerlerde arar ve bulamaz. Objects.hash(ad, yas), equals'ta kullanılan alanların aynılarından bir değer üretir. toString ise nesnenin metin hâlini verir; println(k1) bu metodu kendiliğinden çağırır ve Ali (20) yazar. toString yazılmasaydı Kisi@1b6d3586 gibi sınıf adı ve sayıdan oluşan bir metin görülürdü.
String ve sarmalayıcı sınıflar da nesnedir ve aynı kurala uyar. Programın son iki satırı bunu gösterir. Klavyeden okunan "Java" çalışma sırasında oluşturulmuş yeni bir String nesnesidir. Koddaki "Java" sabiti ise başka bir nesnedir. İçerikleri aynı olsa da == false, equals true verir. İki metin sabiti == ile karşılaştırıldığında bazen true görülmesi, Java'nın aynı sabiti tek bir nesne olarak saklamasındandır. Bu bir kural değil, bir gerçekleme ayrıntısıdır; metinler her zaman equals ile karşılaştırılır.
4.1.9Sarmalayıcı sınıflar
4.1.9.1İlkel tipin nesne karşılığı
Sarmalayıcı sınıf (wrapper class), bir ilkel tipin değerini bir nesnenin içinde taşıyan sınıftır. Java'daki sekiz ilkel tipin her birinin bir sarmalayıcı sınıfı vardır.
| İlkel tip | Sarmalayıcı sınıf | İlkel tip | Sarmalayıcı sınıf |
|---|---|---|---|
byte | Byte | float | Float |
short | Short | double | Double |
int | Integer | char | Character |
long | Long | boolean | Boolean |
Adların çoğu ilkel tipin büyük harfle başlayan hâlidir; iki istisna Integer ve Character'dır. Bu sınıflara üç nedenle ihtiyaç duyulur:
- Bazı yerlerde nesne gerekir.
ArrayList<Integer>gibi jenerik koleksiyonlar ilkel tip alamaz;ArrayList<int>yazımı derlenmez. - “Değer yok” durumu. İlkel tipe
nullatanamaz; sarmalayıcı tipteki değişkene atanabilir. Bir sınavın notu henüz girilmemişseInteger not = null;bunu açıkça gösterir.intile 0 yazılsaydı “sıfır aldı” ile “henüz not yok” ayırt edilemezdi. - Yardımcı metotlar ve sabitler.
Integer.parseInt("12")metni sayıya çevirir,Integer.MAX_VALUEen büyükintdeğerini verir,Character.isDigit(c)bir karakterin rakam olup olmadığını söyler.
int değeri doğrudan yığında tutulur. Integer değişkeni ise heap'teki bir nesneyi gösterir; değer o nesnenin içindedir ve değişkene null da atanabilir.Sarmalayıcıların bir bedeli vardır. Şekil 4.1.18'de görüldüğü gibi int değeri değişkenin kutusundadır; Integer ise ayrı bir nesne, ona giden bir referans ve okunurken bir dolaylı erişim gerektirir. Milyonlarca sayı işleyen bir döngüde bu fark ölçülebilir. Bu yüzden gerekmedikçe ilkel tip tercih edilir.
4.1.9.2Otomatik kutulama ve kutudan çıkarma
Otomatik kutulama (autoboxing), ilkel bir değerin gerektiğinde derleyicinin eklediği bir çağrıyla sarmalayıcı nesneye dönüştürülmesidir. Kutudan çıkarma (unboxing), sarmalayıcı nesnedeki değerin ilkel tipe geri dönüştürülmesidir.
public class Sarmalayici {
public static void main(String[] args) {
// int tipinde bir değişken
int sayi = 42;
// Integer wrapper sınıfı kullanımı
Integer wrapperSayi = Integer.valueOf(sayi);
// Autoboxing (otomatik kutulama)
Integer wrapperSayi2 = sayi;
// Unboxing (açık ve otomatik)
int sayi2 = wrapperSayi2.intValue();
int sayi3 = wrapperSayi2;
System.out.println(sayi2 + sayi3);
System.out.println(wrapperSayi.equals(wrapperSayi2));
System.out.println(Integer.parseInt("123") + 1);
System.out.println(Integer.MAX_VALUE);
System.out.println(Character.isDigit('7'));
System.out.println(Double.parseDouble("2.5") * 2);
Integer bos = null; // int bos = null; derlenmez
System.out.println(bos);
}
}84
true
124
2147483647
true
5.0
nullInteger wrapperSayi2 = sayi; satırı bir int'i Integer değişkene atar. Derleyici bu satırı sessizce Integer.valueOf(sayi) çağrısına çevirir; bu otomatik kutulamadır. int sayi3 = wrapperSayi2; satırı ise wrapperSayi2.intValue() çağrısına çevrilir; bu da kutudan çıkarmadır. Açık biçimler (Integer.valueOf, intValue()) ile otomatik biçimler aynı kodu üretir.
Çıktının ilk satırı 42 + 42 = 84'tür. İki sarmalayıcının içeriği aynıdır: true. "123" metni sayıya çevrilip 1 eklenir: 124. Dördüncü satır en büyük int değeridir: 2147483647, yani 231 − 1. '7' bir rakamdır: true. "2.5" double'a çevrilip 2 ile çarpılır: 5.0. Son satırda null değerli bir Integer yazdırılır. println null referansı null sözcüğüyle yazar; burada kutudan çıkarma olmadığı için hata da olmaz. Aynı değeri bir int'e atamak ise derlenmez:
public class IntNull {
public static void main(String[] args) {
int bos = null;
}
}IntNull.java:3: error: incompatible types: <null> cannot be converted to int
int bos = null;
^
1 error4.1.9.3Integer önbelleği ve null tuzağı
Otomatik dönüşümler kodu kısaltır, ama iki tuzak gizler. İkisi de sarmalayıcıların birer nesne olduğunun unutulmasından doğar.
import java.util.ArrayList;
public class Tuzaklar {
public static void main(String[] args) {
ArrayList<Integer> liste = new ArrayList<>();
liste.add(5); // autoboxing: 5 -> Integer
int ilk = liste.get(0); // unboxing: Integer -> int
System.out.println(ilk + 1);
Integer a = 127, b = 127;
Integer c = 128, d = 128;
System.out.println(a == b);
System.out.println(c == d);
System.out.println(c.equals(d));
Integer bos = null;
int x = bos; // NullPointerException
}
}6
true
false
true
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "<local7>" is null
at Tuzaklar.main(Tuzaklar.java:14)İlk üç satır sorunsuzdur: liste.add(5) 5'i kutular, liste.get(0) nesneyi verir ve int ilk'e atanırken kutudan çıkarılır; ilk + 1 6'dır. Sonraki iki karşılaştırma ise şaşırtıcıdır: a == b true, c == d false verir. Değerler eşit olduğu hâlde ikinci karşılaştırma neden yanlış çıkar? Aşağıdaki izleme adım adım gösterir.
Integer a = 127, b = 127;
Integer c = 128, d = 128;
System.out.println(a == b);
System.out.println(c == d);
System.out.println(c.equals(d));
int t = c + d;
System.out.println(t);
Integer bos = null;
int x = bos;| Adım | Satır | Çağrı yığını | Değişkenler | Yeni çıktı |
|---|---|---|---|---|
| 1 | 1: Integer a = 127, b = 127; | — | a=→önbellek(127), b=→önbellek(127) | — |
| 2 | 2: Integer c = 128, d = 128; | — | a=→önbellek, b=→önbellek, c=→#1, d=→#2 | — |
| 3 | 3: System.out.println(a == b); | — | a=→önbellek, b=→önbellek | true |
| 4 | 4: System.out.println(c == d); | — | c=→#1, d=→#2 | false |
| 5 | 5: System.out.println(c.equals(d)); | — | c=→#1, d=→#2 | true |
| 6 | 6: int t = c + d; | — | c=→#1, d=→#2, t=256 | — |
| 7 | 7: System.out.println(t); | — | t=256 | 256 |
| 8 | 8: Integer bos = null; | — | t=256, bos=null | — |
| 9 | 9: int x = bos; | — | bos=null, x=? | Exception in thread "main" ⏎ java.lang.NullPointerException |
Birinci adımda Integer a = 127 otomatik kutulamayla Integer.valueOf(127) çağrısına dönüşür. valueOf metodu −128 ile 127 arasındaki değerler için her seferinde yeni nesne oluşturmaz; bu değerlerin nesneleri bir önbellekte hazırdır. a ve b önbellekteki aynı nesneyi alır. İkinci adımda 128 bu aralığın dışındadır; valueOf her çağrıda yeni bir nesne oluşturur ve c ile d iki ayrı nesneyi gösterir.
c == d: değerler aynı (128), ama iki ok iki ayrı nesneye gider; a ve b ise önbellekteki tek nesneyi paylaşır. Sağda bos hiçbir nesneyi göstermez; int x = bos kutudan çıkarmaya çalışınca NullPointerException fırlatılır.Üçüncü ve dördüncü adımlarda == referansları karşılaştırır. a ile b aynı nesneyi gösterdiği için sonuç true, c ile d farklı nesneleri gösterdiği için false'tur. Değerler aynıdır, nesneler farklıdır. Beşinci adımda c.equals(d) nesnelerin içindeki int değerleri karşılaştırır ve true verir. Tuzak şuradadır: küçük sayılarla yapılan bir deneme =='nin doğru çalıştığı izlenimini verir; kod büyük sayılarla karşılaşınca sessizce yanlış sonuç üretir. Sarmalayıcı nesneler her zaman equals ile karşılaştırılır.
Altıncı adımda int t = c + d; çalışır. + işleci nesnelerle çalışmaz; iki nesne de kutudan çıkarılır (intValue()) ve 128 + 128 = 256 bulunur. t ilkel bir int'tir ve değeri yığındaki kutusunda durur. Aynı kural karşılaştırmada da geçerlidir: bir Integer bir int ile == kullanılarak karşılaştırılırsa Integer kutudan çıkarılır ve sayılar karşılaştırılır. Yedinci adımda 256 yazılır.
Sekizinci adımda Integer bos = null; çalışır. Sarmalayıcı değişkene null atanabilir. Dokuzuncu adımda int x = bos; gizlice bos.intValue() çağrısına çevrilir. Gösterilen nesne yoktur ve NullPointerException fırlatılır. İleti bu gizli çağrıyı açıkça adlandırır: Cannot invoke "java.lang.Integer.intValue()". Kodda hiçbir metot çağrısı görünmeyen bir satırın bu hatayı vermesi, otomatik kutudan çıkarmanın en tehlikeli yanıdır.
Sarmalayıcı nesneleri == ile karşılaştırmak. Integer değerleri −128…127 aralığında == ile doğru sonuç verir gibi görünür; aralığın dışında aynı değerler false verir. Önbelleğin üst sınırı JVM ayarıyla büyütülebilir, bu yüzden sınıra güvenmek de doğru değildir. İçerik her zaman equals ile karşılaştırılır.
4.1.10Kayıt ve enum
Bazı sınıflar yalnızca birkaç veriyi bir arada taşımak için yazılır: bir noktanın iki koordinatı, bir öğrencinin adı ve numarası. Bazı tipler ise önceden bilinen, sınırlı bir değer kümesinden birini alır: haftanın günleri, mevsimler. Java her iki durum için kısa ve güvenli birer yazım sunar.
4.1.10.1Kayıt (record)
Kayıt (record, Java 16 ve sonrası), yalnızca veri taşıyan sınıfları kısa yazmak için tasarlanmış bir sınıf türüdür. Kaydın bütün alanları private final'dır ve değiştirici metodu yoktur; bu yüzden nesneleri değiştirilemez (immutable).
record Nokta(double x, double y) { }Bu tek satır x ve y adlı iki double bileşeni olan bir kayıt tanımlar. Derleyici bu tanımdan iki private final alan ve iki parametreli bir yapıcı üretir. Ayrıca x() ve y() erişimcilerini, içerikle karşılaştıran bir equals'ı, ona uygun bir hashCode'u ve okunaklı bir toString'i ekler. Erişimcilerin adları get ile başlamaz; bileşenin adını taşır. Aynı sınıfı elle yazmak yaklaşık 30 satır tutardı.
public class KayitTesti {
public static void main(String[] args) {
Nokta nokta1 = new Nokta(1.0, 2.5);
Nokta nokta2 = new Nokta(1.0, 2.5);
boolean esitMi = nokta1.equals(nokta2);
System.out.println(esitMi);
System.out.println(nokta1 == nokta2);
System.out.println(nokta1);
System.out.println(nokta1.x() + nokta1.y());
}
}true
false
Nokta[x=1.0, y=2.5]
3.5İki nokta aynı koordinatlarla oluşturulmuştur. equals içeriği karşılaştırdığı için true, == referansları karşılaştırdığı için false verir. println(nokta1) kaydın ürettiği toString'i çağırır: Nokta[x=1.0, y=2.5]. Son satır erişimcilerle okunan iki değeri toplar: 3.5.
Kaydın alanı değiştirilmeye çalışılırsa program derlenmez:
record Nokta(double x, double y) { }
public class KayitDegistir {
public static void main(String[] args) {
Nokta n = new Nokta(1.0, 2.5);
n.x = 3.0;
}
}KayitDegistir.java:5: error: x has private access in Nokta
n.x = 3.0;
^
1 errorBir kayda yeni bir değer gerekiyorsa yeni bir kayıt oluşturulur. Kayda kendi metotları da eklenebilir. Yapıcıya denetim eklemek için bileşen listesi tekrar yazılmadan kısa bir yapıcı tanımlanır:
record Nokta(double x, double y) {
Nokta {
if (Double.isNaN(x) || Double.isNaN(y))
throw new IllegalArgumentException("NaN olamaz");
}
double uzaklik() { return Math.sqrt(x * x + y * y); }
Nokta kaydir(double dx) { return new Nokta(x + dx, y); }
}
public class KayitEk {
public static void main(String[] args) {
Nokta n = new Nokta(3.0, 4.0);
Nokta m = n.kaydir(1.0);
System.out.println(n + " " + n.uzaklik());
System.out.println(m);
System.out.println(n.hashCode() == new Nokta(3.0, 4.0).hashCode());
}
}Nokta[x=3.0, y=4.0] 5.0
Nokta[x=4.0, y=4.0]
trueNokta { ... } bloğu kısa yapıcıdır (compact constructor). Parametreleri derleyici ekler ve blok yalnızca denetimi yapar; alanlara atama blok bittikten sonra kendiliğinden yapılır. uzaklik() noktanın orijine uzaklığını hesaplar: √(3² + 4²) = 5.0. kaydir nesneyi değiştirmez, yeni bir Nokta döndürür; n eski değerinde kalır. Son satır, eşit iki kaydın hashCode değerlerinin de eşit olduğunu doğrular.
4.1.10.2enum
enum (numaralandırma tipi, enumeration), sabit ve sınırlı bir değer kümesini temsil eden özel bir sınıf türüdür. Her değer, o tipin program boyunca tek olan bir nesnesidir.
enum HaftaGunleri {
PAZARTESI, SALI, CARSAMBA, PERSEMBE,
CUMA, CUMARTESI, PAZAR
}
public class GunTesti {
public static void main(String[] args) {
HaftaGunleri[] tumGunler = HaftaGunleri.values();
HaftaGunleri persembe = HaftaGunleri.valueOf("PERSEMBE");
System.out.println(tumGunler.length + " gün");
System.out.println(persembe + " " + persembe.ordinal());
for (HaftaGunleri gun : tumGunler) {
boolean haftaSonu = gun == HaftaGunleri.CUMARTESI
|| gun == HaftaGunleri.PAZAR;
if (haftaSonu) {
System.out.println(gun + ": hafta sonu");
}
}
}
}7 gün
PERSEMBE 3
CUMARTESI: hafta sonu
PAZAR: hafta sonuHaftaGunleri tipinin yalnızca yedi değeri vardır ve başka bir değer oluşturulamaz. Haftanın günlerini int ile 0–6 arası sayılarla tutmak da mümkündü, ama o zaman gun = 9 gibi anlamsız bir atamayı derleyici yakalayamazdı. enum tipli bir değişkene yalnızca o tipin değerleri atanabilir.
Her enum tipiyle birlikte üç yararlı metot gelir. values() bütün değerleri tanım sırasıyla bir dizi olarak verir; üzerinde döngü kurulabilir. valueOf("PERSEMBE") metni aynı adlı değere çevirir. ordinal() değerin 0'dan başlayan sıra numarasını verir: PERSEMBE dördüncü değer olduğu için 3. Her değer tek bir nesne olduğu için enum değerleri == ile güvenle karşılaştırılır; döngüdeki hafta sonu denetimi böyle yazılmıştır.
enum bir sınıf olduğu için alanları, yapıcısı ve metotları olabilir. Aşağıdaki örnekte her gezegen kendi çekim katsayısını taşır. Program bir de switch ile enum kullanımını ve valueOf'un adı birebir aradığını gösterir.
enum Gezegen {
MERKUR(0.38), DUNYA(1.0), MARS(0.38);
private final double cekim;
Gezegen(double cekim) { this.cekim = cekim; }
double agirlik(double kutle) { return kutle * cekim; }
}
public class EnumEk {
public static void main(String[] args) {
Gezegen g = Gezegen.MARS;
switch (g) {
case DUNYA -> System.out.println("ev");
case MARS -> System.out.println("kızıl");
default -> System.out.println("başka");
}
System.out.println(g.agirlik(70.0));
System.out.println(Gezegen.valueOf("Mars"));
}
}kızıl
26.6
Exception in thread "main" java.lang.IllegalArgumentException: No enum constant Gezegen.Mars
at java.base/java.lang.Enum.valueOf(Enum.java:293)
at Gezegen.valueOf(EnumEk.java:1)
at EnumEk.main(EnumEk.java:16)Değerlerin yanındaki parantezler yapıcıya verilen argümanlardır. enum'un yapıcısı her zaman private'tır; dışarıdan new Gezegen(...) yazılamaz. switch'te case etiketlerine tip adı yazılmaz: case MARS. 70 × 0.38 = 26.6 hesaplanır. Son satırda valueOf("Mars") çağrılır; değerin adı MARS olduğu için eşleşme bulunmaz ve IllegalArgumentException fırlatılır. valueOf büyük-küçük harf ayrımı yapar.
4.1.11İç sınıflar
İç sınıf (inner class), başka bir sınıfın içinde tanımlanan sınıftır. İç sınıf, dış sınıfın alanlarına ve metotlarına, private olanlar dahil, doğrudan erişebilir.
İç sınıflar yalnızca bir sınıfın işine yarayan yardımcı sınıfları o sınıfın içinde toplamak için kullanılır. Grafik arayüzlerde bir düğmeye tıklandığında çalışacak kodu taşıyan olay dinleyici (listener) sınıfları bunun en yaygın örneğidir. Nerede tanımlandığına göre dört tür iç sınıf vardır.
| Tür | Nerede tanımlanır? | Nasıl oluşturulur? |
|---|---|---|
static iç sınıf | Sınıf gövdesinde, static ile | Dış sınıf nesnesi olmadan: new Outer.StaticInner() |
| Üye iç sınıf | Sınıf gövdesinde, static olmadan | Bir dış sınıf nesnesi üzerinden: outer.new MemberInner() |
| Yerel iç sınıf | Bir metodun içinde | Yalnızca o metodun içinden |
| Anonim iç sınıf | Bir ifadenin içinde, adsız | Tanımlandığı yerde tek nesne; bir arayüzü ya da soyut sınıfı uygular |
4.1.11.1static iç sınıf
public class Outer {
// Dış sınıfın içinde statik bir iç sınıf
static class StaticInner {
void display() {
System.out.println("Statik İç Sınıf");
}
}
public static void main(String[] args) {
// Dış sınıf örneği oluşturmadan erişim
Outer.StaticInner staticInner = new Outer.StaticInner();
staticInner.display();
}
}Statik İç Sınıfstatic iç sınıf, dış sınıfın içine yerleştirilmiş sıradan bir sınıf gibidir. Dış sınıfın bir nesnesi olmadan oluşturulabilir: new Outer.StaticInner(). Tam adı Outer.StaticInner'dır; dış sınıf burada bir ad alanı görevi görür. Bir dış nesneye bağlı olmadığı için dış sınıfın yalnızca static üyelerine doğrudan erişebilir. Dış sınıfın bir örnek alanını okumaya çalışırsa tanıdık ileti alınır:
public class IcStaticDisAlan {
private int sayi = 5;
static class StaticInner {
void display() { System.out.println(sayi); }
}
}IcStaticDisAlan.java:4: error: non-static variable sayi cannot be referenced from a static context
void display() { System.out.println(sayi); }
^
1 error4.1.11.2Üye iç sınıf
public class Outer {
private int sayi = 5; // dış sınıfın özel alanı
class MemberInner { // üye iç sınıf
void display() {
System.out.println("Üye İç Sınıf, sayi = " + sayi);
}
}
public static void main(String[] args) {
// Dış sınıf örneği üzerinden üye iç sınıfa erişim
Outer outer = new Outer();
Outer.MemberInner memberInner = outer.new MemberInner();
memberInner.display();
}
}Üye İç Sınıf, sayi = 5Üye iç sınıf, dış sınıfın bir üyesi olarak static olmadan tanımlanır. Her üye iç sınıf nesnesi bir dış nesneye bağlıdır. Bu yüzden oluşturulurken bir dış nesne gerekir ve yazım alışılmadıktır: outer.new MemberInner(). Bu bağ sayesinde display() dış nesnenin private sayi alanını okuyabilir. main içinde doğrudan new MemberInner() yazmak derlenmez:
public class UyeIcStatic {
private int sayi = 5;
class MemberInner {
void display() { System.out.println(sayi); }
}
public static void main(String[] args) {
MemberInner m = new MemberInner();
}
}UyeIcStatic.java:7: error: non-static variable this cannot be referenced from a static context
MemberInner m = new MemberInner();
^
1 errorİleti yine this'ten söz eder. new MemberInner(), sınıfın içinde this.new MemberInner() anlamına gelir: iç nesne this'e bağlanacaktır. main static olduğu için this yoktur. Bağlantının nasıl kurulduğunu aşağıdaki izleme gösterir.
public class Outer {
private int sayi = 5;
class MemberInner {
void display() {
System.out.println("sayi = " + sayi);
}
}
public static void main(String[] args) {
Outer o1 = new Outer();
Outer o2 = new Outer();
o2.sayi = 8;
Outer.MemberInner m = o2.new MemberInner();
m.display();
}
}sayi = 8| Adım | Satır | Çağrı yığını | Değişkenler | Yeni çıktı |
|---|---|---|---|---|
| 1 | 9: Outer o1 = new Outer(); | — | o1=→#1 | — |
| 2 | 10: Outer o2 = new Outer(); | — | o1=→#1, o2=→#2 | — |
| 3 | 11: o2.sayi = 8; | — | o1=→#1, o2=→#2 | — |
| 4 | 12: Outer.MemberInner m = o2.new MemberInner(); | — | o2=→#2, m=→#3 | — |
| 5 | 13: m.display();4: void display() { | — | this=→#3 | — |
| 6 | 5: System.out.println("sayi = " + sayi); | — | this=→#3, sayi=Outer.this.sayi=8 | sayi = 8 |
Birinci ve ikinci adımlarda iki Outer nesnesi oluşur; ikisinin de kendi sayi alanı vardır ve 5 ile başlar. Üçüncü adımda o2.sayi = 8; yalnızca 2 numaralı nesneyi değiştirir. main Outer'ın içinde olduğu için private alana yazabilir. Dördüncü adımda o2.new MemberInner() iç nesneyi oluşturur. İç nesne, onu oluşturan dış nesnenin referansını gizli bir alanda saklar. Bu alanın Java'daki adı Outer.this'tir.
Outer.this alanı 2 numaralı dış nesneyi gösteriyor. Sağda display() çalışıyor: sayi, Outer.this.sayi demektir ve ok 2 numaralı nesneye, 8 değerine gider.Beşinci adımda m.display() çağrılır. Metodun this'i iç nesnedir. İç sınıfın kendi sayi alanı yoktur. Altıncı adımda derleyici yalın sayi adını Outer.this.sayi olarak çözer: iç nesnenin gizli alanındaki ok 2 numaralı dış nesneye gider ve sayi = 8 yazılır. İç nesne o1.new MemberInner() ile oluşturulsaydı ok 1 numaralı nesneye gider ve 5 yazılırdı. İç ve dış sınıfta aynı adlı alanlar olduğunda iç sınıfın kendi alanı this.x, dış nesnenin alanı Outer.this.x yazımıyla ayırt edilir.
4.1.11.3Yerel ve anonim iç sınıf
public class Outer {
void outerMethod() {
class LocalInner { // metodun içinde tanımlanır
void display() {
System.out.println("Yerel İç Sınıf");
}
}
// Yerel iç sınıfa yalnızca bu metodun içinden erişilir
LocalInner localInner = new LocalInner();
localInner.display();
}
public static void main(String[] args) {
Outer outer = new Outer();
outer.outerMethod();
}
}Yerel İç SınıfYerel iç sınıf bir metodun içinde tanımlanır ve yalnızca o metodun içinden kullanılabilir. Metodun dışında LocalInner adı bilinmez:
public class YerelDisari {
void outerMethod() {
class LocalInner { void display() { } }
new LocalInner().display();
}
void baska() {
LocalInner li = new LocalInner();
}
}YerelDisari.java:7: error: cannot find symbol
LocalInner li = new LocalInner();
^
symbol: class LocalInner
location: class YerelDisari
YerelDisari.java:7: error: cannot find symbol
LocalInner li = new LocalInner();
^
symbol: class LocalInner
location: class YerelDisari
2 errorsYerel iç sınıf, bir metodun içinde birkaç kez gereken küçük bir yardımcı tip için kullanılır. Kapsamı bir yerel değişkeninki gibidir: tanımlandığı bloğun sonunda biter.
interface MyInterface {
void myMethod();
}
public class Outer {
public static void main(String[] args) {
// Genellikle arayüzleri veya soyut sınıfları uygular
MyInterface myInterface = new MyInterface() {
@Override
public void myMethod() {
System.out.println("Anonim İç Sınıf");
}
};
myInterface.myMethod();
MyInterface kisa = () -> System.out.println("Lambda");
kisa.myMethod();
System.out.println(myInterface.getClass().getName());
}
}Anonim İç Sınıf
Lambda
Outer$1Anonim iç sınıf (anonymous class) adı olmayan bir sınıftır; tanımlandığı yerde tek bir nesnesi oluşturulur. Genellikle bir arayüzü ya da soyut sınıfı uygular. new MyInterface() { ... } yazımı “MyInterface'i uygulayan adsız bir sınıf tanımla ve ondan bir nesne oluştur” demektir. Süslü parantezlerin içine arayüzün metotlarının gövdeleri yazılır. Sondaki noktalı virgül atama deyimine aittir; unutulursa derleme hatası alınır.
Adsız sınıfın da derleyicinin verdiği bir adı vardır. Programın son satırı bunu yazdırır: Outer$1, yani Outer'ın içindeki birinci anonim sınıf. Derleme klasöründe Outer$1.class adlı ayrı bir dosya da oluşur. Yalnızca tek bir soyut metodu olan arayüzler için Java 8'den beri daha kısa bir yazım vardır: lambda ifadesi. () -> System.out.println("Lambda") aynı işi tek satırda görür.
4.1.12Sınıf türleri ve tasarım
4.1.12.1Bir sınıf, birçok tip
Bir nesne yalnızca tek bir sınıfa ait olabilir: new ile hangi sınıftan oluşturulduysa o sınıfın nesnesidir. Ama birçok tipi olabilir. Aşağıdaki tanımlarda Kopek, Hayvan'dan türer ve Runner arayüzünü uygular; Hayvan da Printable ve Serializable arayüzlerini uygular.
interface Printable { ... }
interface Runner { ... }
class Hayvan implements Printable, Serializable { ... }
class Kopek extends Hayvan implements Runner { ... }
var kazanan = new Kopek(...);extends), kesik çizgi bir arayüzün uygulanmasını (implements) gösterir. Her sınıf en üstte Object'ten türer.kazanan değişkeninin gösterdiği nesne yalnızca Kopek sınıfına aittir. Ama bu nesnenin tipleri Kopek, Runner, Hayvan, Printable, Serializable ve Object'tir. instanceof işleci altısı için de true verir. Tanımları gerçek gövdelerle doldurup sınayan küçük bir program:
import java.io.Serializable;
interface Printable { }
interface Runner { }
class Hayvan implements Printable, Serializable { }
class Kopek extends Hayvan implements Runner { }
public class Tipler {
public static void main(String[] args) {
var kazanan = new Kopek();
Object o = kazanan;
System.out.println((o instanceof Kopek) + " " + (o instanceof Runner) + " " + (o instanceof Hayvan)
+ " " + (o instanceof Printable) + " " + (o instanceof Serializable) + " " + (o instanceof Object));
System.out.println(kazanan.getClass().getName());
}
}true true true true true true
KopekBir nesnenin birden çok tip olarak görülebilmesi çok biçimliliğin temelidir. Hayvan tipinde bir değişken bir Kopek nesnesini gösterebilir; Runner bekleyen bir metot bir Kopek alabilir. Buradan çıkan sonuç şudur: “sınıf” ile “tip” aynı kavram değildir.
4.1.12.2Sınıf çeşitleri
Sınıflar amaçlarına göre farklı biçimlerde yazılır. Tablo 4.1.19 sık karşılaşılan çeşitleri listeler. Bir sınıf birden çok çeşide aynı anda girebilir: Math hem araç sınıfıdır hem final'dır.
| Çeşit | Özellik | Örnek |
|---|---|---|
Numaralandırma (enum) | Değerleri sabit bir nesne kümesidir | enum Gun { PAZARTESI, SALI, ... } |
| Tek nesnelik (singleton) | Yalnızca bir nesnesi oluşturulabilir | private yapıcı + static tek nesne |
Soyut (abstract) | Kendi nesnesi oluşturulamaz | abstract class Sekil |
Mühürlü (sealed, Java 17+) | Alt tipleri sabit bir kümedir | sealed interface Hareket permits Yuru, Kos |
final | Alt sınıf türetilemez | final class String |
Kayıt (record) | Nesneleri değiştirilemez | record Nokta(double x, double y) |
| Araç (utility) | Yalnızca static metotlar içerir | Math, Arrays |
| Uygulama | main metodunu içerir | Programın başladığı sınıf |
final class Araclar { // araç ve final sınıf
private Araclar() { } // nesne oluşturulamaz
static int kare(int x) { return x * x; }
}
class Ayarlar { // singleton
private static final Ayarlar TEK = new Ayarlar();
private Ayarlar() { }
static Ayarlar getir() { return TEK; }
}
abstract class Sekil { // soyut sınıf
abstract double alan();
}
// mühürlü (sealed, Java 17+)
sealed interface Hareket permits Yuru, Kos { }
final class Yuru implements Hareket { }
final class Kos implements Hareket { }
public class Cesitler { // uygulama sınıfı
public static void main(String[] args) {
System.out.println(Araclar.kare(7));
System.out.println(Ayarlar.getir() == Ayarlar.getir());
}
}49
trueAraclar bir araç sınıfıdır. Yapıcısı private olduğu için dışarıdan nesnesi oluşturulamaz; zaten nesneye ihtiyaç yoktur, çünkü bütün metotları static'tir. final olduğu için ondan alt sınıf da türetilemez. İki kısıtlamayı da derleyici uygular:
final class Araclar {
private Araclar() { }
}
class AltAraclar extends Araclar { }
public class FinalTuret { }FinalTuret.java:4: error: cannot inherit from final Araclar
class AltAraclar extends Araclar { }
^
FinalTuret.java:4: error: Araclar() has private access in Araclar
class AltAraclar extends Araclar { }
^
2 errorsAyarlar tek nesnelik bir sınıftır. Tek nesne, sınıf yüklenirken private static final bir alanda oluşturulur. Yapıcı private olduğu için başka nesne oluşturulamaz; nesneye yalnızca getir() ile ulaşılır. getir() her çağrıda aynı referansı döndürdüğü için Ayarlar.getir() == Ayarlar.getir() true verir. Başka bir sınıfta new Ayarlar() yazılırsa:
TekilNew.java:8: error: Ayarlar() has private access in Ayarlar
Ayarlar a = new Ayarlar();
^
1 errorSekil soyut bir sınıftır: alan() metodunun gövdesi yoktur, çünkü bir “şeklin” alanı hesaplanamaz; ancak bir dairenin ya da karenin alanı hesaplanabilir. Soyut sınıftan nesne oluşturulamaz:
SoyutNew.java:6: error: Sekil is abstract; cannot be instantiated
Sekil s = new Sekil();
^
1 errorHareket mühürlü bir arayüzdür. permits listesi onu uygulayabilecek tipleri sayar: yalnızca Yuru ve Kos. Listede olmayan bir sınıf Hareket'i uygulamaya çalışırsa derlenmez:
MuhurIhlal.java:4: error: class is not allowed to extend sealed class: Hareket (as it is not listed in its 'permits' clause)
final class Ucus implements Hareket { }
^
1 errorMühürlü tipler, alt tiplerin kümesinin tasarım gereği kapalı olduğu durumlar için kullanılır. Derleyici bütün olası alt tipleri bildiği için, örneğin bir switch'in her durumu karşılayıp karşılamadığını denetleyebilir.
4.1.12.3Tasarım örneği: RasyonelSayi
Şimdiye kadarki parçalar bir araya getirilerek baştan bir sınıf tasarlanabilir. RasyonelSayi sınıfı pay/payda biçimindeki kesirleri temsil eder. Bir sınıfı tasarlarken üç soru sorulur:
- Tanımlama: Sınıfı kullanan kod neyi görecek? Burada
publicbir yapıcı, iki kesri toplayantoplave kesri metin olarak verentoString. - Temsil: Veri nasıl saklanacak? İki
private intalan:payvepayda. Yapıcı kesri sadeleştirerek saklar; böylece her nesne her zaman en sade biçimdedir. - Gerçekleme: Metotların gövdeleri ve dışarıya açılmayan yardımcılar. Sadeleştirme için gereken en büyük ortak bölen, gizli bir
ebobmetoduyla Öklid algoritmasıyla bulunur.
public class RasyonelSayi {
// Temsil: gizli alanlar
private int pay, payda;
// Tanımlama: public yapıcı ve metotlar
public RasyonelSayi(int pay, int payda) {
int e = ebob(Math.abs(pay), Math.abs(payda));
this.pay = pay / e;
this.payda = payda / e;
}
public RasyonelSayi topla(RasyonelSayi d) {
int yeniPay = pay * d.payda + d.pay * payda;
return new RasyonelSayi(yeniPay, payda * d.payda);
}
@Override
public String toString() { return pay + "/" + payda; }
// Gerçekleme ayrıntısı: gizli yardımcı metot
private static int ebob(int a, int b) {
while (b != 0) { int k = a % b; a = b; b = k; }
return a;
}
}Sınama programı a = new RasyonelSayi(8, 10) ve b = new RasyonelSayi(2, 3) oluşturur ve önce println(a), sonra println(a.topla(b)) çağırır. Programın bütün çıktısı şudur:
4/5
22/15
11/12 3/4
-2/3 1/-3 0/1
1/1Çıktının ilk iki satırı bu iki çağrıdır. 8/10 sadeleşip 4/5 olarak saklanmıştır. 4/5 + 2/3 = (4 × 3 + 2 × 5) / (5 × 3) = 22/15'tir; 22 ile 15'in ortak böleni olmadığı için kesir sade kalır. Üçüncü satırı a = (6, 8) ve b = (1, 6) ile yapılan aynı çağrı üretir. Bu çağrı aşağıda adım adım izlenir.
public RasyonelSayi(int pay, int payda) {
int e = ebob(Math.abs(pay), Math.abs(payda));
this.pay = pay / e;
this.payda = payda / e;
}
private static int ebob(int a, int b) {
while (b != 0) { int k = a % b; a = b; b = k; }
return a;
}
public RasyonelSayi topla(RasyonelSayi d) {
int yeniPay = pay * d.payda + d.pay * payda;
return new RasyonelSayi(yeniPay, payda * d.payda);
}
RasyonelSayi a = new RasyonelSayi(6, 8); // main
RasyonelSayi b = new RasyonelSayi(1, 6);
System.out.println(a.topla(b) + " " + a);| Adım | Satır | Çağrı yığını | Değişkenler | Yeni çıktı |
|---|---|---|---|---|
| 1 | 14: RasyonelSayi a = new RasyonelSayi(6, 8); // main1: public RasyonelSayi(int pay, int payda) { | — | pay=6, payda=8 | — |
| 2 | 2: int e = ebob(Math.abs(pay), Math.abs(payda));6: private static int ebob(int a, int b) { | — | a=6, b=8 | — |
| 3 | 7: while (b != 0) { int k = a % b; a = b; b = k; } | — | a=8, b=6, k=6 | — |
| 4 | 7: while (b != 0) { int k = a % b; a = b; b = k; } | — | a=6, b=2, k=2 | — |
| 5 | 7: while (b != 0) { int k = a % b; a = b; b = k; } | — | a=2, b=0, k=0 | — |
| 6 | 8: return a;3: this.pay = pay / e;4: this.payda = payda / e; | — | e=2, this.pay=3, this.payda=4 | — |
| 7 | 15: RasyonelSayi b = new RasyonelSayi(1, 6); | — | a=→#1 (3/4), b=→#2 (1/6) | — |
| 8 | 16: System.out.println(a.topla(b) + " " + a);10: public RasyonelSayi topla(RasyonelSayi d) {11: int yeniPay = pay * d.payda + d.pay * payda; | — | this=→#1, d=→#2, yeniPay=22 | — |
| 9 | 12: return new RasyonelSayi(yeniPay, payda * d.payda);1: public RasyonelSayi(int pay, int payda) {2: int e = ebob(Math.abs(pay), Math.abs(payda)); | — | pay=22, payda=24, e=2 | — |
| 10 | 16: System.out.println(a.topla(b) + " " + a); | — | a=→#1 (3/4), sonuç=→#3 (11/12) | 11/12 3/4 |
Birinci adımda new RasyonelSayi(6, 8) nesneyi ayırır; alanlar önce 0'dır, yapıcının parametreleri 6 ve 8'dir. İkinci adımda gizli yardımcı ebob(6, 8) çağrılır. Bu metot private olduğu için sınıfı kullanan kod onu göremez ve çağıramaz. static'tir, çünkü yalnızca parametreleriyle çalışır; hiçbir nesnenin alanına dokunmaz.
Üçüncü, dördüncü ve beşinci adımlar Öklid algoritmasının üç turudur. Her turda k = a % b hesaplanır, sonra a ← b ve b ← k olur. İlk turda 6 % 8 = 6'dır; sayılar yalnızca yer değiştirir: a = 8, b = 6. İkinci turda 8 % 6 = 2: a = 6, b = 2. Üçüncü turda 6 % 2 = 0: a = 2, b = 0. b sıfır olunca döngü biter ve a, yani 2 döndürülür. Altıncı adımda yapıcı payı ve paydayı 2'ye bölerek saklar: nesne 3/4 olur.
topla içinden yeni bir RasyonelSayi(22, 24) kuruluyor; sonuç 11/12 ayrı bir nesne, a hâlâ 3/4.Yedinci adım aynı işleri (1, 6) için yapar. ebob(1, 6) 1 döndürür; kesir zaten sadedir ve 1/6 olarak saklanır. Sekizinci adımda a.topla(b) çağrılır. Metodun this'i 1 numaralı nesne, d parametresi 2 numaralı nesnedir. yeniPay = pay * d.payda + d.pay * payda = 3 × 6 + 1 × 4 = 22 bulunur. d.payda başka bir nesnenin private alanıdır, ama kod aynı sınıfın içinde olduğu için okunabilir.
Dokuzuncu adımda new RasyonelSayi(22, 24) çağrılır. Yapıcı yine ebob'u çalıştırır: b sırasıyla 24, 22, 2, 0 değerlerini alır ve sonuç 2'dir. Yeni nesne 11/12 olarak saklanır. Onuncu adımda println önce dönen sonucu, sonra a'yı yazar: 11/12 3/4. topla mevcut nesneyi değiştirmemiş, yeni bir nesne döndürmüştür. RasyonelSayi'de değiştirici metot yoktur ve hiçbir metot alanları değiştirmez; nesneleri bu anlamda değiştirilemezdir.
Maliyet açısından ebob'un döngüsü küçük sayının basamak sayısıyla orantılı sayıda tur atar; Öklid algoritmasının tur sayısı O(log min(a, b))'dir. Yapıcı ve topla bu yüzden çok hızlıdır. topla'nın her çağrısı yeni bir nesne oluşturur; değiştirilemez tasarımın bedeli budur. Karşılığında bir kesri tutan değişken başka bir yerde paylaşılsa bile değeri hiçbir zaman beklenmedik biçimde değişmez.
Sınama programının son iki satırı önce sınır durumları dener. new RasyonelSayi(-4, 6) doğru biçimde -2/3 olur. new RasyonelSayi(0, 5) için ebob(0, 5) = 5 bulunur ve kesir 0/1 olarak saklanır. Son satırda 1/2 + 1/2 = 4/4 hesaplanır ve sadeleşip 1/1 olur. Bu denemeler tasarımın iki sınırını da ortaya çıkarır. new RasyonelSayi(3, -9) ise 1/-3 olarak saklanır: yapıcı işareti paydadan paya taşımaz. Aynı değer iki farklı biçimde saklanabildiği için bu sınıfa yazılacak bir equals eşit kesirleri farklı sayabilir. Düzeltme, yapıcıda payda negatifse ikisinin de işaretini çevirmektir. İkinci sınır sıfır paydadır. new RasyonelSayi(0, 0) çağrısında ebob(0, 0) 0 döndürür ve bölme şu hatayı verir:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at RasyonelSayi.<init>(RasyonelSifir.java:10)
at RasyonelSifir.main(RasyonelSifir.java:3)Paydanın sıfır olmasına izin vermeyen bir yapıcı, hatayı kesrin oluşturulduğu anda ve anlamlı bir iletiyle bildirmelidir. Örneğin yapıcının ilk satırı if (payda == 0) throw new IllegalArgumentException("payda 0 olamaz"); olabilir. Temsilin gizli olması bu düzeltmeleri kolaylaştırır: değişiklik yalnızca yapıcıyı etkiler, sınıfı kullanan kod aynen kalır.
4.1.13Örnek problemler ve alıştırmalar
Aşağıdaki program ne yazdırır?
class Sayac {
static int toplam = 0;
int adet = 0;
void arttir() { toplam++; adet++; }
}
public class Problem1 {
public static void main(String[] args) {
Sayac a = new Sayac(), b = new Sayac();
a.arttir(); a.arttir(); b.arttir();
System.out.print(a.adet + " " + b.adet);
System.out.println(" " + Sayac.toplam);
}
}2 1 3adet örneğe ait bir alandır; her nesnede ayrı bir kopyası vardır. a.arttir() iki kez, b.arttir() bir kez çağrıldığı için a.adet 2, b.adet 1 olur. toplam ise static'tir; üç çağrı da aynı kutuyu artırır ve değeri 3 olur. Çıktı 2 1 3'tür.
Aşağıdaki program ne yazdırır?
class Kutu { int deger; }
public class Problem2 {
public static void main(String[] args) {
Kutu k1 = new Kutu();
k1.deger = 5;
Kutu k2 = k1;
k2.deger = 9;
Kutu k3 = new Kutu();
k3.deger = 9;
System.out.println(k1.deger);
System.out.println(k1 == k2);
System.out.println(k2 == k3);
}
}9
true
falseKutu k2 = k1; yeni nesne oluşturmaz; k2 ile k1 aynı nesneyi gösterir. k2.deger = 9 bu ortak nesneye yazar, bu yüzden k1.deger de 9'dur. k1 == k2 true verir. k3 ise new ile oluşturulmuş ayrı bir nesnedir; değeri de 9 olsa k2 == k3 false verir.
Aşağıdaki iki program ne yazdırır?
record Para(int lira) { }
public class Problem5 {
public static void main(String[] args) {
Para p1 = new Para(10);
Para p2 = new Para(10);
Integer i1 = 1000, i2 = 1000;
System.out.println(p1.equals(p2));
System.out.println(p1 == p2);
System.out.println(i1.equals(i2));
System.out.println(p1);
}
}true
false
true
Para[lira=10]class Nokta {
int x, y;
Nokta(int x, int y) {
this.x = x;
this.y = y;
}
}
public class Problem6 {
public static void main(String[] args) {
Nokta a = new Nokta(1, 2);
Nokta b = new Nokta(1, 2);
System.out.println(a.equals(b));
System.out.println(a.x == b.x);
}
}false
trueBirinci programda Para bir kayıttır. Kaydın equals'ı içeriği karşılaştırır: true. == ise iki ayrı nesneyi karşılaştırır: false. i1 ve i2 1000 değerli iki ayrı Integer nesnesidir; önbellek aralığının dışında oldukları için == false verirdi, ama equals her zaman güvenlidir: true. Son satır kaydın ürettiği toString'dir. İkinci programda Nokta sıradan bir sınıftır ve equals yazmamıştır. Object'in sürümü referansları karşılaştırır: false. a.x == b.x ise iki int değeri karşılaştırır: true.
Aşağıdaki beş alıştırma bu notun konularını bir araya getirir. Ardından bir çözüm programı ve çıktısı gelir.
Kitapsınıfı:privatebaslik,yazar,sayfaSayisi; üç parametreli yapıcı, get metotları vetoString().Kitap'a birstatictoplamKitapSayisisayacı ve yalnızca pozitif değeri kabul edensetSayfaSayisi.Dairesınıfı:yaricap,alan(),cevre();new Daire(3)için iki ondalık basamakla yazdırma.Mevsimenum'unun değerlerini sıra numaralarıyla yazdırma.OgrenciKaydi(String ad, int numara)kaydı ve bir nesnesinin yazdırılması.
class Kitap {
private String baslik, yazar;
private int sayfaSayisi;
private static int toplamKitapSayisi = 0;
Kitap(String baslik, String yazar, int sayfaSayisi) {
this.baslik = baslik;
this.yazar = yazar;
setSayfaSayisi(sayfaSayisi);
toplamKitapSayisi++;
}
String getBaslik() { return baslik; }
String getYazar() { return yazar; }
int getSayfaSayisi() { return sayfaSayisi; }
void setSayfaSayisi(int sayfaSayisi) {
if (sayfaSayisi > 0) this.sayfaSayisi = sayfaSayisi;
}
static int getToplamKitapSayisi() { return toplamKitapSayisi; }
@Override
public String toString() { return baslik + " (" + yazar + ", " + sayfaSayisi + " s.)"; }
}
class Daire {
private double yaricap;
Daire(double yaricap) { this.yaricap = yaricap; }
double alan() { return Math.PI * yaricap * yaricap; }
double cevre() { return 2 * Math.PI * yaricap; }
}
enum Mevsim { ILKBAHAR, YAZ, SONBAHAR, KIS }
record OgrenciKaydi(String ad, int numara) { }
public class Alistirmalar {
public static void main(String[] args) {
Kitap k = new Kitap("Nutuk", "Atatürk", 600);
System.out.println(k);
Kitap k2 = new Kitap("Deneme", "Yazar", -5);
System.out.println(k2.getSayfaSayisi() + " " + Kitap.getToplamKitapSayisi());
Daire d = new Daire(3);
System.out.printf("%.2f %.2f%n", d.alan(), d.cevre());
for (Mevsim m : Mevsim.values()) System.out.print(m.ordinal() + ":" + m + " ");
System.out.println();
System.out.println(new OgrenciKaydi("Ali", 7));
}
}Nutuk (Atatürk, 600 s.)
0 2
28,27 18,85
0:ILKBAHAR 1:YAZ 2:SONBAHAR 3:KIS
OgrenciKaydi[ad=Ali, numara=7]Kitap'ın yapıcısı sayfa sayısını doğrudan alana yazmaz; setSayfaSayisi'nı çağırır. Böylece denetim tek bir yerde kalır ve yapıcı da ondan yararlanır. −5 sayfalı ikinci kitapta değiştirici değeri reddeder; alan varsayılan değeri 0'da kalır. Sayaç ise iki kitabı da sayar: 0 2. Daire için printf("%.2f %.2f%n", ...) kullanılır; Türkçe yerel ayarda ondalık ayırıcı virgüldür: π × 9 ≈ 28,27 ve 2 × π × 3 ≈ 18,85. Mevsim'in değerleri values() ile dolaşılır ve ordinal() 0'dan başlar.
4.1.14Sık yapılan hatalar
Nesne ve sınıflarla ilgili hataların çoğunu derleyici yakalar ve iletisi hatanın yerini doğrudan gösterir. Bir kısmı ise derlenen ama yanlış çalışan programlara yol açar; bunlar daha tehlikelidir.
Örneğe ait metodu main'den nesnesiz çağırmak. İleti: non-static method selamVer() cannot be referenced from a static context. Çağrı bir nesne üzerinden yapılır (new Selam().selamVer()); metot gerçekten hiçbir nesnenin verisini kullanmıyorsa static yapılır.
Yapıcıya void yazmak. public void Hata1(String m) yapıcı değil, sıradan bir metottur. new Hata1("selam") şu iletiyle reddedilir: constructor Hata1 in class Hata1 cannot be applied to given types. Yapıcının dönüş tipi olmaz.
Yalnızca parametreli yapıcı varken parametresiz nesne oluşturmak. Bir yapıcı yazılınca varsayılan yapıcı eklenmez; new Urun() cannot be applied to given types iletisini verir. Gerekiyorsa parametresiz yapıcı da yazılır.
new unutmak. Urun u = Urun("Kalem"); satırı bir metot çağrısı gibi okunur ve derleyici böyle bir metot arar:
NewUnutma.java:7: error: cannot find symbol
Urun u = Urun("Kalem");
^
symbol: method Urun(String)
location: class NewUnutma
1 errorprivate alana dışarıdan erişmek. İleti: hesapBakiyesi has private access in BankaHesabi. Alana sınıfın get ve set metotlarıyla erişilir.
static metotta this kullanmak. İleti: non-static variable this cannot be referenced from a static context. static metodun bir nesnesi yoktur.
Yapıcıda this unutmak (gölgeleme). ad = ad; derlenir, ama parametreyi kendine atar; alan null kalır. Doğrusu this.ad = ad;. Alan final yapılırsa derleyici hatayı yakalar.
this(...)'yi ilk satırdan sonra yazmak. İleti: call to this must be first statement in constructor. Öteki yapıcıya devretmek yapıcının ilk deyimi olmalıdır.
Nesneleri == ile karşılaştırmak. k1 == k2 içerikleri aynı iki ayrı nesne için false verir. İçerik equals() ile karşılaştırılır; String ve sarmalayıcı sınıflar dahil. Kendi sınıfında equals yazılmamışsa Object'in sürümü de == gibi davranır.
null referansı kullanmak. Nesne oluşturulmadan ya da null atandıktan sonra metot çağrılamaz; NullPointerException fırlatılır. Değer verilmemiş referans alanları da null'dır. Kullanmadan önce null denetimi yapılır.
Kayıt alanını değiştirmeye çalışmak. İleti: x has private access in Nokta. Kayıt değiştirilemezdir; yeni değer için yeni bir nesne oluşturulur.
Üye iç sınıfı static main'de doğrudan oluşturmak. İleti: non-static variable this cannot be referenced from a static context. Doğrusu outer.new MemberInner(); ya da iç sınıf static yapılır.
4.1.15Özet
- Nesne yönelimli programlama programı birbiriyle etkileşen nesnelerden kurar. Her nesnenin durumu (alanlar) ve davranışı (metotlar) vardır. Üç temel özellik sarmalama, kalıtım ve çok biçimliliktir; çoğu zaman soyutlama da eklenir.
- Sınıf nesnenin taslağıdır; nesne
newile üretilen örneğidir. Aynı sınıfın nesneleri aynı alan adlarını taşır, ama her birinin değerleri ayrıdır. Metot kodu sınıfta tektir. - Erişim belirleyiciler:
public,protected, paket erişimi,private. Alanlarprivate, dışarıya sunulan metotlarpublicolur; değer erişimci ile okunur, denetim yapan değiştirici ile yazılır (veri gizleme). - Yapıcı sınıfla aynı adı taşır ve dönüş tipi yoktur.
newheap'te yer ayırır, alanlara varsayılan değer verir, yapıcıyı çağırır ve referansı döndürür. Hiç yapıcı yazılmazsa derleyici parametresiz varsayılan yapıcıyı ekler; bir yapıcı yazılınca eklemez. - Alanların varsayılan değerleri: sayılar 0,
booleanfalse,char'\u0000', referanslarnull. Yerel değişkenler varsayılan değer almaz. - Örneğe ait alan her nesnede ayrı,
staticalan sınıfta tek ve ortaktır.staticmetodunthis'i yoktur; örnek üyelerine ancak bir nesne üzerinden ulaşır.finalalana bir kez değer verilir; sınıf sabitleristatic finalve büyük harfle yazılır. - İlklendirme sırası:
staticalanlar vestatic { }blokları sınıf ilk kullanılırken bir kez; sonra hernew'de varsayılan değerler, alan ilk değerleri ve{ }blokları (yazılış sırasıyla), en son yapıcının gövdesi. - Değişken nesneyi değil referansı tutar. Atama referansı kopyalar ve takma ad oluşturur. Metotlara nesne aktarmak da referansın kopyasını verir.
nullreferans üzerinden erişimNullPointerExceptionfırlatır; referansı kalmayan nesneyi çöp toplayıcı siler. thismetodun çağrıldığı nesnedir; gölgelenen alanathis.adile ulaşılır ve nesnethisile başka metotlara verilebilir.this(...)aynı sınıfın başka yapıcısını çağırır, ilk satırda olmalıdır ve tek bir nesne kurar.==referans eşitliğini,equalsiçerik eşitliğini sınar.equalsyeniden yazılıncahashCodeda yazılır.Stringve sarmalayıcılarequalsile karşılaştırılır.- Sarmalayıcı sınıflar ilkel değerleri nesne olarak taşır. Otomatik kutulama
valueOf, kutudan çıkarmaxxxValue()çağrısıdır. −128…127 arasıInteger'lar önbellekten gelir;nullbir sarmalayıcıyı kutudan çıkarmakNullPointerExceptionfırlatır. - Kayıt değiştirilemez veri sınıfıdır; yapıcı, erişimciler,
equals,hashCodevetoStringkendiliğinden üretilir. enum sabit bir değer kümesidir;values(),valueOf(),ordinal()ve==ile kullanılır. - İç sınıflar:
static(dış nesne gerekmez), üye (bir dış nesneye bağlı,outer.new), yerel (metodun içinde) ve anonim (adsız, tek nesne). - Bir nesne tek bir sınıfa aittir, ama birçok tipi olabilir. Sınıf çeşitleri:
enum, tek nesnelik, soyut, mühürlü,final, kayıt, araç ve uygulama sınıfı. Sınıf tasarımında tanımlama, temsil ve gerçekleme ayrılır; temsil gizli tutulur.
4.1.16Kavram listesi
Terimlerin İngilizce karşılıkları ve öteki bölümlerdeki kullanımları terim sözlüğünde bulunur.
- nesne yönelimli programlama object-oriented programming
- prosedürel programlama procedural programming
- sınıf class
- nesne object
- örnek instance
- üye member
- alan field
- metot method
- nokta operatörü dot operator
- erişim belirleyici access modifier
- erişimci getter
- değiştirici setter
- veri gizleme data hiding
- sarmalama encapsulation
- yapıcı constructor
- varsayılan yapıcı default constructor
- yapıcı zinciri constructor chaining
- referans reference
- heap heap
- yığın stack
- null null reference
- çöp toplayıcı garbage collector
- static üye static member
- ilklendirici blok initializer block
- this this keyword
- gölgeleme shadowing
- sarmalayıcı sınıf wrapper class
- otomatik kutulama / kutudan çıkarma autoboxing / unboxing
- kayıt record
- enum enum
- değiştirilemez immutable
- iç sınıf inner class
- anonim sınıf anonymous class
- mühürlü sınıf / arayüz sealed class / interface
- soyutlama abstraction
4.1.17Kendini Dene
class Sayac2 { static int s = 0; int n; Sayac2() { s += 2; n = s; } }sınıfından art arda üç nesne (p,q,r) oluşturuluyor.p.n,q.n,r.nveSayac2.skaçtır?Cevap
sstatic'tir ve üç yapıcı da aynı kutuyu artırır: 2, 4, 6. Her yapıcı o anki değeri nesnenin kendinalanına kopyalar. Sonradans'nin değişmesi önceki nesnelerinn'ini etkilemez. Sonuç:2 4 6 6.Hucresınıfının tek birint valanı vev'yi atayan bir yapıcısı var.static void degis(Hucre h, Hucre g) { h.v = 10; g = h; g.v = 20; }metodua = new Hucre(1)veb = new Hucre(2)iledegis(a, b)olarak çağrılıyor. Sonraa.vveb.vkaçtır?Cevap
h,a'nın nesnesini gösterir;h.v = 10onu 10 yapar.g = hyalnızca yerel kopyag'yia'nın nesnesine yöneltir;bdeğişmez.g.v = 20artıka'nın nesnesine yazar. Sonuç:a.v = 20,b.v = 2.class C { static int t = 0; int a = ++t; { t *= 10; } C() { t++; } }sınıfından iki nesne oluşturuluyor. Birinci nesnenina'sı, ikinci nesnenina'sı ve sonundaC.tkaçtır?Cevap
Her nesnede sıra şudur: alan ilk değeri, sonra örnek ilklendirici, sonra yapıcı gövdesi. Birinci nesne:
a = ++t→ t = 1, a = 1;t *= 10→ 10;t++→ 11. İkinci nesne: a = 12, t = 120, t = 121. Sonuç:1 12 121.Zincirsınıfındaint x; String s = "";alanları ve şu yapıcılar var:Zincir() { this(1); s += "a"; },Zincir(int x) { this(x, x + 1); s += "b"; },Zincir(int x, int y) { this.x = x * y; s += "c"; }.new Zincir()sonrasındaxvesnedir?Cevap
Zincir en içteki yapıcıya kadar iner:
Zincir(1, 2).s = ""ilk değeri bu yapıcının gövdesinden hemen önce bir kez verilir. En içteki gövde önce çalışır: x = 2, s = "c". Zincir geri sararken her yapıcı kendi kalan satırını çalıştırır: "cb", sonra "cba". Sonuç:2 cba.Integer i1 = 127, i2 = 127, i3 = 1000, i4 = 1000; Long l = 127L;tanımlarından sonrai1 == i2,i3 == i4,i3.equals(i4),l.equals(127)vel == 127ne verir?Cevap
true false true false true. 127 önbellekten gelir, 1000 için iki ayrı nesne oluşur.equalsdeğerleri karşılaştırır.l.equals(127)'de 127 birInteger'a kutulanır;Long'unequals'ı yalnızca başka birLong'u eşit sayar, bu yüzdenfalse.l == 127'de iselkutudan çıkarılır ve iki sayı karşılaştırılır:true.Bu bölümdeki
RasyonelSayisınıfıylanew RasyonelSayi(10, -4)nasıl yazdırılır?new RasyonelSayi(2, 6).topla(new RasyonelSayi(1, 6))ne verir?Cevap
ebob(10, 4) = 2; pay ve payda 2'ye bölünür ve işaret paydada kalır:5/-2. İkinci ifadede 2/6 önce 1/3 olarak saklanır. 1/3 + 1/6 = (1 × 6 + 1 × 3) / 18 = 9/18 bulunur ve yapıcı bunu sadeleştirir:1/2.Bir öğrenci
main'dekinon-static method ... cannot be referenced from a static contexthatasından kurtulmak için sınıftaki bütün alanları ve metotlarıstaticyapıyor. Program derleniyor. Bu çözümün sorunu nedir?Cevap
Alanlar
staticolunca her alanın sınıfta tek bir kopyası kalır. İki nesne oluşturulsa bile ikisi aynı alanları paylaşır; birinin adını değiştirmek ötekinin adını da değiştirir. Program fiilen prosedürel hâle gelir. Doğru çözüm, nesneye bağlı metotları bir nesne üzerinden çağırmak ve yalnızca gerçekten nesneden bağımsız olanlarıstaticyapmaktır.RasyonelSayisınıfındanew RasyonelSayi(0, 0)ne olur? Bu hatayı ve işaret sorununu gidermek için hangi metot, nasıl değiştirilir? Sınıfı kullanan kodun değişmesi gerekir mi?Cevap
ebob(0, 0)0 döndürür vepay / esatırıArithmeticException: / by zerofırlatır. Yalnızca yapıcı değişir: baştapayda == 0ise anlamlı iletili birIllegalArgumentExceptionfırlatılır; sadeleştirmeden sonrapaydanegatifse pay ve paydanın işareti çevrilir. Alanlarprivateve temsil gizli olduğu için sınıfı kullanan kod hiç değişmez.
4.1.18İleri okuma
- Oracle, The Java Tutorials, “Classes and Objects” dersi: sınıf tanımlama, yapıcılar,
this, erişim denetimi, sınıf üyeleri ve iç içe sınıflar konularını kısa örneklerle işler; çevrim içi ve ücretsizdir. - A. B. Downey ve C. Mayfield, Think Java (2. baskı, O'Reilly, 2019), 10. ve 11. bölümler: nesneleri, referansları ve kendi sınıfını yazmayı yığın diyagramlarıyla adım adım anlatır.
- J. Bloch, Effective Java (3. baskı, Addison-Wesley, 2018):
equals,hashCodevetoString'in doğru yazılması, değiştirilemez sınıflar vestaticfabrika metotları üzerine deneyimli bir programcının önerileri. - J. Gosling ve diğ., The Java Language Specification, Java SE 21, 8. bölüm: alanların, ilklendiricilerin, yapıcıların,
enum'ların ve kayıtların kesin kurallarını tanımlar; ilklendirme sırasıyla ilgili sorularda başvurulacak kaynaktır.
4.1.19Kaynaklar
- Oracle, The Java Tutorials: “Classes and Objects”, “Nested Classes”, “Enum Types”, “Record Classes”, “Autoboxing and Unboxing”.
- J. Gosling ve diğ., The Java Language Specification, Java SE 21 Edition (Bölüm 8 Classes: 8.3 Fields, 8.6–8.7 Initializers, 8.8 Constructors, 8.9 Enum Classes, 8.10 Record Classes).
- 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.
- C. S. Horstmann, Core Java, Volume I: Fundamentals, 12. baskı, 2022 (Bölüm 4: Objects and Classes).
- A. B. Downey ve C. Mayfield, Think Java, 2. baskı, O'Reilly, 2019 (Bölüm 10–11).
- J. Bloch, Effective Java, 3. baskı, Addison-Wesley, 2018 (
equals,hashCode,toStringmaddeleri).