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
new operatö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, == ve equals farkı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ı, enum tü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.

Tanım 4.1.1

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.

Tablo 4.1.1: Gerçek dünyadan nesneler. Durum alanlarla, davranış metotlarla temsil edilir.
NesneAlanlar (durum)Davranışlar
Öğrenciad, numara, bölümderseKaydol(), notGor()
Masaen, boy, malzemetasi()
Çembermerkez, yarıçapalan(), cevre()
Düğmeyazı, renk, etkin mitikla()
Kreditutar, faiz, vadetaksitHesapla()

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 : Ogrenciad = "Ali"numara = 2012bolum = "EEM"selamVer()dersEkle(ad, not)ortalama()← alanlar (fields)← davranışlar (methods)
Şekil 4.1.1: 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.

Tablo 4.1.2: Prosedürel ve nesne yönelimli yaklaşımın karşılaştırması.
Prosedürel yaklaşımNesne yönelimli yaklaşım
Temel birimFonksiyon (metot)Nesne (veri + metotlar)
VeriAyrı değişkenlerde; her fonksiyon erişebilirNesnenin içinde; dışarıya kontrollü açılır
ÖrnekalanHesapla(en, boy)dikdortgen.alanHesapla()
Büyük programlarDeğişiklik birçok fonksiyonu etkilerDeğişiklik çoğunlukla bir sınıfın içinde kalır
Yeniden kullanımFonksiyonları kopyalayarakSı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().

ProsedürelalanHesapla()cevreHesapla()ciz()en, boy (veri)Nesne yönelimlidikdortgenen=5, boy=3alanHesapla() …kareen=4, boy=4alanHesapla() …
Şekil 4.1.2: Solda prosedürel düzen: üç fonksiyon aynı ortak veriye dışarıdan erişir. Sağda nesne yönelimli düzen: her nesne kendi verisini (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ğı

Tanım 4.1.2

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.

Tanım 4.1.3

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.

Sınıf (taslak)Arabamarkamodelyilrenkilerle()dur()sinyalVer()newaraba1 : ArabaToyota Corollaaraba2 : ArabaFord Mustangaraba3 : ArabaFiat EgeaNesneler (örnekler)
Şekil 4.1.3: 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- marka : String- model : String- yil : int- renk : String+ ilerle() : void+ dur() : void+ sinyalVer() : voidaraba1 : Arabamarka = "Toyota"model = "Corolla"yil = 2022renk = "Gri"araba2 : Arabamarka = "Ford"model = "Mustang"yil = 2023renk = "Mavi"örneği
Şekil 4.1.4: UML gösterimi. Solda 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 Ogrenci sınıfında ad ve soyad birer alandır.
  • Metotlar, nesnelerin yapabileceği işlemleri tanımlar. Bir Kopek sınıfında havla() ve kos() 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

Tanım 4.1.4

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 error

Dış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ı.

Tanım 4.1.5

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.

Sık yapılan hata

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?

Tanım 4.1.6

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
42

new ifadesi üç adımda çalışır:

  1. 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, boolean alanlar false, referans alanlar null olur.
  2. Argümanlara uyan yapıcı çağrılır ve alanlara ilk değerlerini verir.
  3. new ifadesinin 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.

Tanım 4.1.7

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.

Yığın (stack)Heapmain()sporArabaklasikArabayeniArabanullAraba nesnesimodel"2022"renk"KIRMIZI"Araba nesnesimodel"2023"renk"BEYAZ"
Şekil 4.1.5: Yerel değişkenler yığında, nesneler heap'te durur. 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 - BEYAZ

Tablo 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.

Tablo 4.1.3: new Araba(...) çağrılarının ve bilgiYazdir()'ın izlenmesi.
AdımSatırÇağrı yığınıDeğişkenlerYeni çıktı
115: Araba sporAraba = new Araba("2022", "KIRMIZI");main()sporAraba=?, klasikAraba=?—
25: public Araba(String model, String renk) {main() → Araba(model, renk)model="2022", renk="KIRMIZI"—
36: this.model = model;main() → Araba(model, renk)model="2022", renk="KIRMIZI"—
47: this.renk = renk;main() → Araba(model, renk)model="2022", renk="KIRMIZI"—
515: Araba sporAraba = new Araba("2022", "KIRMIZI");main()sporAraba=→#1, klasikAraba=?—
616: Araba klasikAraba = new Araba("2023", "BEYAZ");main()sporAraba=→#1, klasikAraba=→#2—
717: sporAraba.bilgiYazdir();
11: System.out.println(model + " - " + renk);
main() → bilgiYazdir()sporAraba=→#1, klasikAraba=→#22022 - KIRMIZI
818: klasikAraba.bilgiYazdir();
11: System.out.println(model + " - " + renk);
main() → bilgiYazdir()sporAraba=→#1, klasikAraba=→#22023 - 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.

Yığın (stack)Heapmain()sporAraba?klasikAraba?Araba #1modelnullrenknull Yığın (stack)HeapAraba(...)thismodel"2022"renk"KIRMIZI"main()sporAraba?klasikAraba?Araba #1model"2022"renk"KIRMIZI" Yığın (stack)HeapbilgiYazdir()thismain()sporArabaklasikArabaAraba #1model"2022"renk"KIRMIZI"Araba #2model"2023"renk"BEYAZ"
Şekil 4.1.6: Üç an. Solda 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.

Tanım 4.1.8

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.

Tablo 4.1.4: Alanların varsayılan değerleri. Yerel değişkenler varsayılan değer almaz.
Alan tipiVarsayılan değer
byte, short, int, long0
float, double0.0
booleanfalse
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
0

Nokta3 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 error

Bu 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 2021

Arac() 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.

Sık yapılan hata

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

Tanım 4.1.9

Ö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.

Tablo 4.1.5: Örneğe ait ve sınıfa ait alanların karşılaştırması.
Örneğe ait alanSınıfa ait alan (static)
Tanımprivate String ad;private static int toplamHesapSayisi;
Kopya sayısıHer nesnede ayrı bir kopyaSınıfta tek kopya, bütün nesneler paylaşır
Ne zaman oluşur?Her new ileSınıf belleğe yüklenirken, bir kez
Erişimnesne.adSinifAdi.toplamHesapSayisi
ÖrnekOgrenci: ad, soyadBankaHesabi: 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
Tablo 4.1.6: static sayacın izlenmesi. Değişkenler sütununda toplamHesapSayisi sınıf alanının değeridir.
AdımSatırÇağrı yığınıDeğişkenlerYeni çıktı
13: private static int toplamHesapSayisi = 0;(sınıf yükleniyor)toplamHesapSayisi=0—
25: public BankaHesabi(String hesapSahibi) {
6: this.hesapSahibi = hesapSahibi;
7: toplamHesapSayisi++;
main() → BankaHesabi("Ali")toplamHesapSayisi=1, h1=?—
315: BankaHesabi h1 = new BankaHesabi("Ali");main()toplamHesapSayisi=1, h1=→#1—
416: BankaHesabi h2 = new BankaHesabi("Ayşe");main()toplamHesapSayisi=2, h1=→#1, h2=→#2—
517: BankaHesabi h3 = new BankaHesabi("Can");main()toplamHesapSayisi=3, h1=→#1, h2=→#2, h3=→#3—
618: System.out.println(h2.hesapSahibi);main()toplamHesapSayisi=3Ayşe
719: int sayi = BankaHesabi.getToplamHesapSayisi();
10: public static int getToplamHesapSayisi() {
11: return toplamHesapSayisi;
main() → getToplamHesapSayisi()toplamHesapSayisi=3—
820: System.out.println(sayi);main()toplamHesapSayisi=3, sayi=33

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.

Yığın (stack)HeapSınıf alanı (static): BankaHesabitoplamHesapSayisi0 Yığın (stack)HeapSınıf alanı (static): BankaHesabitoplamHesapSayisi3main()h1h2h3BankaHesabi #1hesapSahibi"Ali"BankaHesabi #2hesapSahibi"Ayşe"BankaHesabi #3hesapSahibi"Can"
Şekil 4.1.7: Solda sınıf yüklenmiş, sayaç 0, henüz nesne yok. Sağda üç nesne oluşmuş: her birinin kendi 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 error

Sı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.

Tanım 4.1.10

İ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:

  1. Sınıf ilk kez kullanılıyorsa static alanların ilk değerleri ve static bloklar, yazılış sırasıyla bir kez çalışır.
  2. new nesne için bellek ayırır; bütün alanlar varsayılan değer alır.
  3. Alanların tanım satırındaki ilk değerleri ve örnek ilklendirici blokları, yazılış sırasıyla çalışır.
  4. Yapıcının gövdesi çalışır.
1 static { } ve static alanlarsınıf ilk kullanılınca, bir kez2 new: bellek ayrılıralanlar varsayılan değerde3 alan ilk değerleri, { }yazılış sırasıyla, her new4 yapıcının gövdesiher new
Şekil 4.1.8: Nesne oluşturma sırası. Birinci kutu yalnızca sınıf ilk kez kullanılırken, öteki üçü her 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.
Gece

main'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.

Tablo 4.1.7: Kopek sınıfında ilklendiricilerin ve yapıcıların çalışma sırası.
AdımSatırÇağrı yığınıDeğişkenlerYeni çıktı
111: System.out.println("Köpekler!");static ilklendirici—Köpekler!
217: var d1 = new Kopek("Kara");main()d1=?, d2=?—
314: System.out.println("Köpek oluşturuluyor.");main() → Kopek("Kara")d1=?, d2=?Köpek oluşturuluyor.
44: this.adi = adi;main() → Kopek("Kara")adi="Kara"—
55: System.out.println(adi);main() → Kopek("Kara")adi="Kara"Kara
618: var d2 = new Kopek("Gece");main()d1=→#1, d2=→#2Köpek oluşturuluyor. ⏎ Gece
719: 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.

Yığın (stack)HeapKopek(adi)thisadi"Kara"main()d1?d2?Kopek #1adinull Yığın (stack)Heaphavla()thismain()d1d2Kopek #1adi"Kara"Kopek #2adi"Gece"
Şekil 4.1.9: Solda örnek ilklendirici çalışmış, yapıcının çerçevesi açık; nesnenin 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.

Örnek 4.1.1

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ç:

MSICIC

4.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 2

aramaYap 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 false

Metot 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 error

4.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() kodusınıfta tek kopyaogrenci1 nesnesiad = "Ali"thisogrenci2 nesnesiad = "Ayşe"this
Şekil 4.1.10: 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.
Tablo 4.1.8: Örneğe ait ve static metotların karşılaştırması.
Örneğe aitstatic
Kime ait?NesneyeSınıfa
Çağrınesne.metot()Sinif.metot()
Nesne gerekir mi?EvetHayır
this var mı?EvetHayır
Örnekstr.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
Tablo 4.1.9: DikdortgenTesti'nin izlenmesi.
AdımSatırÇağrı yığınıDeğişkenlerYeni çıktı
13: Dikdortgen dikdortgen = new Dikdortgen(5.0, 3.0);
4: Dikdortgen kare = new Dikdortgen(5.0, 5.0);
main()dikdortgen=→#1, kare=→#2—
25: double alan = dikdortgen.alanHesapla();main() → alanHesapla()dikdortgen=→#1, kare=→#2—
36: double cevre = dikdortgen.cevreHesapla();main() → cevreHesapla()alan=15.0—
47: System.out.println("Alanı: " + alan
8: + " Çevresi: " + cevre);
main()alan=15.0, cevre=16.0Alanı: 15.0 Çevresi: 16.0
59: alan = kare.alanHesapla();
10: cevre = kare.cevreHesapla();
main() → alanHesapla()alan=25.0, cevre=20.0—
611: System.out.println("Alanı: " + alan
12: + " Çevresi: " + cevre);
main()alan=25.0, cevre=20.0Alanı: 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.

Yığın (stack)HeapalanHesapla()thismain()dikdortgenkarealan?cevre?Dikdortgen #1uzunluk5.0genislik3.0Dikdortgen #2uzunluk5.0genislik5.0 Yığın (stack)HeapalanHesapla()thismain()dikdortgenkarealan25.0cevre20.0Dikdortgen #1uzunluk5.0genislik3.0Dikdortgen #2uzunluk5.0genislik5.0
Şekil 4.1.11: Solda 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

Tanım 4.1.11

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.0

Matematik.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.

Tablo 4.1.10: Hangi metot hangi üyeyi doğrudan kullanabilir?
NeredenÖrnek alanı / metodustatic 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 error

static 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.

Tanım 4.1.12

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
Tablo 4.1.11: Referans atamanın izlenmesi. Satır numaraları main'in satırlarıdır.
AdımSatırÇağrı yığınıDeğişkenlerYeni çıktı
12: Araba araba1 = new Araba("Toyota");main()araba1=→#1, araba2=?—
23: Araba araba2 = araba1;main()araba1=→#1, araba2=→#1—
34: System.out.println("Araba 1: " + araba1.getMarka());
5: System.out.println("Araba 2: " + araba2.getMarka());
main()araba1=→#1, araba2=→#1Araba 1: Toyota ⏎ Araba 2: Toyota
46: araba2.setMarka("Ford");main() → setMarka("Ford")araba1=→#1, araba2=→#1—
57: System.out.println("Araba 1: " + araba1.getMarka());main()araba1=→#1, araba2=→#1Araba 1: Ford
68: araba1 = null;main()araba1=null, araba2=→#1—
79: System.out.println("Araba 2: " + araba2.getMarka());main()araba1=null, araba2=→#1Araba 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.

Yığın (stack)Heapmain()araba1araba2Araba #1marka"Toyota" Yığın (stack)HeapsetMarka()thismarka"Ford"main()araba1araba2Araba #1marka"Ford" Yığın (stack)Heapmain()araba1nullaraba2Araba #1marka"Ford"
Şekil 4.1.12: Soldan sağa: 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);
  }
}
100

degistir(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.

main()bnullb.ad→ NullPointerExceptionreferansı kalmayan nesneçöp toplayıcı siler
Şekil 4.1.13: 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

Tanım 4.1.13

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);
  }
}
null
Golge("Ali") çerçevesiad"Ali"thisad = adparametre kendineGolge nesnesiadnullthis.ad = ad yazılsaydı buraya "Ali" giderdi
Şekil 4.1.14: Yapıcıda ad = 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
Tablo 4.1.12: this'in her çağrıda hangi nesneyi gösterdiği.
AdımSatırÇağrı yığınıDeğişkenlerYeni çıktı
113: Ogrenci ogrenci1 = new Ogrenci("Ali");
4: public Ogrenci(String ad) {
5: this.ad = ad;
main() → Ogrenci("Ali")ad="Ali"—
214: Ogrenci ogrenci2 = new Ogrenci("Ayşe");main()ogrenci1=→#1, ogrenci2=→#2—
315: ogrenci1.selamVer();
8: public void selamVer() {
9: System.out.println("Merhaba, ben " + this.ad);
main() → selamVer()ogrenci1=→#1, ogrenci2=→#2Merhaba, ben Ali
416: ogrenci2.selamVer();
8: public void selamVer() {
9: System.out.println("Merhaba, ben " + this.ad);
main() → selamVer()ogrenci1=→#1, ogrenci2=→#2Merhaba, 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.

Yığın (stack)HeapselamVer()thismain()ogrenci1ogrenci2Ogrenci #1ad"Ali"Ogrenci #2ad"Ayşe" Yığın (stack)HeapselamVer()thismain()ogrenci1ogrenci2Ogrenci #1ad"Ali"Ogrenci #2ad"Ayşe"
Şekil 4.1.15: Solda 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 -> Giresunspor

takim.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.

Tanım 4.1.14

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.0

Ortak 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 error

Yapı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 error

Zincir 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
Tablo 4.1.13: new Kitap("Nutuk", "Atatürk") çağrısında yapıcı zincirinin izlenmesi.
AdımSatırÇağrı yığınıDeğişkenlerYeni çıktı
114: Kitap k2 = new Kitap("Nutuk", "Atatürk");main()k2=?—
28: public Kitap(String ad, String yazar) {main() → Kitap(ad, yazar)ad="Nutuk", yazar="Atatürk"—
39: this(ad); // yapıcı zincirlememain() → Kitap(ad, yazar) → Kitap(ad)ad="Nutuk"—
46: this.ad = ad;main() → Kitap(ad, yazar) → Kitap(ad)ad="Nutuk"—
510: this.yazar = yazar;main() → Kitap(ad, yazar)ad="Nutuk", yazar="Atatürk"—
614: Kitap k2 = new Kitap("Nutuk", "Atatürk");main()k2=→Kitap—
715: System.out.println(k2.ad + " / "
16: + k2.yazar + " / " + k2.sayfaSayisi);
main()k2=→KitapNutuk / 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.

Yığın (stack)HeapKitap(ad)thisad"Nutuk"Kitap(ad, yazar)thisad"Nutuk"yazar"Atatürk"main()k2?Kitap nesnesiadnullyazarnullsayfaSayisi0 Yığın (stack)HeapKitap(ad, yazar)thisad"Nutuk"yazar"Atatürk"main()k2?Kitap nesnesiad"Nutuk"yazar"Atatürk"sayfaSayisi0
Şekil 4.1.16: Solda zincirin en derin anı: iki yapıcı çerçevesi açık, ikisinin 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.

Tanım 4.1.15

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
true
Yığın (stack)Heapmain()k1k2k3Kisi #1ad"Ali"yas20Kisi #2ad"Ali"yas20
Şekil 4.1.17: k1 ve k3 aynı Kisi nesnesini, k2 aynı içerikli ayrı bir nesneyi gösterir.
Tablo 4.1.14: Üç referans için karşılaştırma sonuçları.
İfadeSonuçNeden
k1 == k3trueİki değişken aynı nesneyi gösterir.
k1 == k2falseFarklı nesneler; içerikleri aynı olsa da referanslar farklı.
k1.equals(k2)trueequals 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ığı

Tanım 4.1.16

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.

Tablo 4.1.15: İlkel tipler ve sarmalayıcı sınıfları.
İlkel tipSarmalayıcı sınıfİlkel tipSarmalayıcı sınıf
byteBytefloatFloat
shortShortdoubleDouble
intIntegercharCharacter
longLongbooleanBoolean

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 null atanamaz; sarmalayıcı tipteki değişkene atanabilir. Bir sınavın notu henüz girilmemişse Integer not = null; bunu açıkça gösterir. int ile 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_VALUE en büyük int değerini verir, Character.isDigit(c) bir karakterin rakam olup olmadığını söyler.
YığınHeapint sayi42Integer wInteger nesnesivalue = 42değerin kendisiw = null da olabilir
Şekil 4.1.18: 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

Tanım 4.1.17

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
null

Integer 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 error

4.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;
Tablo 4.1.16: Integer önbelleğinin ve null kutudan çıkarmanın izlenmesi.
AdımSatırÇağrı yığınıDeğişkenlerYeni çıktı
11: Integer a = 127, b = 127;—a=→önbellek(127), b=→önbellek(127)—
22: Integer c = 128, d = 128;—a=→önbellek, b=→önbellek, c=→#1, d=→#2—
33: System.out.println(a == b);—a=→önbellek, b=→önbellektrue
44: System.out.println(c == d);—c=→#1, d=→#2false
55: System.out.println(c.equals(d));—c=→#1, d=→#2true
66: int t = c + d;—c=→#1, d=→#2, t=256—
77: System.out.println(t);—t=256256
88: Integer bos = null;—t=256, bos=null—
99: 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.

YığınHeapmain()abcdönbellek: 127value127Integer #1value128Integer #2value128c == d → false (#1 ≠ #2) YığınHeapmain()abcdt256bosnullx?önbellek: 127value127Integer #1value128Integer #2value128bos.intValue() → NullPointerException
Şekil 4.1.19: Solda 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.

Sık yapılan hata

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)

Tanım 4.1.18

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 error

Bir 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]
true

Nokta { ... } 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

Tanım 4.1.19

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 sonu

HaftaGunleri 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

Tanım 4.1.20

İç 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.

Tablo 4.1.17: İç sınıf türleri.
TürNerede tanımlanır?Nasıl oluşturulur?
static iç sınıfSınıf gövdesinde, static ileDış sınıf nesnesi olmadan: new Outer.StaticInner()
Üye iç sınıfSınıf gövdesinde, static olmadanBir dış sınıf nesnesi üzerinden: outer.new MemberInner()
Yerel iç sınıfBir metodun içindeYalnızca o metodun içinden
Anonim iç sınıfBir ifadenin içinde, adsızTanı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ıf

static 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 error

4.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
Tablo 4.1.18: Üye iç sınıf nesnesinin dış nesneye bağlanması.
AdımSatırÇağrı yığınıDeğişkenlerYeni çıktı
19: Outer o1 = new Outer();—o1=→#1—
210: Outer o2 = new Outer();—o1=→#1, o2=→#2—
311: o2.sayi = 8;—o1=→#1, o2=→#2—
412: Outer.MemberInner m = o2.new MemberInner();—o2=→#2, m=→#3—
513: m.display();
4: void display() {
—this=→#3—
65: System.out.println("sayi = " + sayi);—this=→#3, sayi=Outer.this.sayi=8sayi = 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.

YığınHeapmain()o1o2mOuter #1sayi5Outer #2sayi8MemberInner #3Outer.this YığınHeapdisplay()thismain()o1o2mOuter #1sayi5Outer #2sayi8MemberInner #3Outer.thissayi = Outer.this.sayi = 8
Şekil 4.1.20: Solda iç nesne (#3) oluşmuş; gizli 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ıf

Yerel 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 errors

Yerel 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$1

Anonim 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(...);
Object«interface»Printable«interface»SerializableHayvan«interface»RunnerKopekextendsextends
Şekil 4.1.21: UML tip hiyerarşisi. Düz çizgi ve boş üçgen kalıtımı (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
Kopek

Bir 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.

Tablo 4.1.19: Sınıf çeşitleri.
ÇeşitÖzellikÖrnek
Numaralandırma (enum)Değerleri sabit bir nesne kümesidirenum Gun { PAZARTESI, SALI, ... }
Tek nesnelik (singleton)Yalnızca bir nesnesi oluşturulabilirprivate yapıcı + static tek nesne
Soyut (abstract)Kendi nesnesi oluşturulamazabstract class Sekil
Mühürlü (sealed, Java 17+)Alt tipleri sabit bir kümedirsealed interface Hareket permits Yuru, Kos
finalAlt sınıf türetilemezfinal class String
Kayıt (record)Nesneleri değiştirilemezrecord Nokta(double x, double y)
Araç (utility)Yalnızca static metotlar içerirMath, Arrays
Uygulamamain metodunu içerirProgramı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
true

Araclar 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 errors

Ayarlar 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 error

Sekil 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 error

Hareket 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 error

Mü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 public bir yapıcı, iki kesri toplayan topla ve kesri metin olarak veren toString.
  • Temsil: Veri nasıl saklanacak? İki private int alan: pay ve payda. 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 ebob metoduyla Ö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);
Tablo 4.1.20: RasyonelSayi yapıcısının, ebob'un ve topla'nın izlenmesi. Satır numaraları yukarıdaki kısaltılmış kodun satırlarıdır.
AdımSatırÇağrı yığınıDeğişkenlerYeni çıktı
114: RasyonelSayi a = new RasyonelSayi(6, 8); // main
1: public RasyonelSayi(int pay, int payda) {
—pay=6, payda=8—
22: int e = ebob(Math.abs(pay), Math.abs(payda));
6: private static int ebob(int a, int b) {
—a=6, b=8—
37: while (b != 0) { int k = a % b; a = b; b = k; }—a=8, b=6, k=6—
47: while (b != 0) { int k = a % b; a = b; b = k; }—a=6, b=2, k=2—
57: while (b != 0) { int k = a % b; a = b; b = k; }—a=2, b=0, k=0—
68: return a;
3: this.pay = pay / e;
4: this.payda = payda / e;
—e=2, this.pay=3, this.payda=4—
715: RasyonelSayi b = new RasyonelSayi(1, 6);—a=→#1 (3/4), b=→#2 (1/6)—
816: 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—
912: 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—
1016: 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.

YığınHeapRasyonelSayi()pay6payda8e2main()a?Rasyonel #1pay3payda46/8 → 6/2 = 3, 8/2 = 4 YığınHeapRasyonelSayi()pay22payda24e2topla()thisdyeniPay22main()abRasyonel #1pay3payda4Rasyonel #2pay1payda6Rasyonel #3pay11payda12ebob(22, 24) = 2 → 11/12 YığınHeapmain()abRasyonel #1pay3payda4Rasyonel #2pay1payda6Rasyonel #3pay11payda12topla() yeni nesne döndürdü; a hâlâ 3/4
Şekil 4.1.22: Soldan sağa: ilk nesne sadeleşmiş, 3/4; 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

Örnek 4.1.2

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 3

adet ö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.

Örnek 4.1.3

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
false

Kutu 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.

Örnek 4.1.4

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
true

Birinci 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.

  1. Kitap sınıfı: private baslik, yazar, sayfaSayisi; üç parametreli yapıcı, get metotları ve toString().
  2. Kitap'a bir static toplamKitapSayisi sayacı ve yalnızca pozitif değeri kabul eden setSayfaSayisi.
  3. Daire sınıfı: yaricap, alan(), cevre(); new Daire(3) için iki ondalık basamakla yazdırma.
  4. Mevsim enum'unun değerlerini sıra numaralarıyla yazdırma.
  5. 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.

Sık yapılan hata

Ö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.

Sık yapılan hata

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.

Sık yapılan hata

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.

Sık yapılan hata

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 error
Sık yapılan hata

private 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.

Sık yapılan hata

static metotta this kullanmak. İleti: non-static variable this cannot be referenced from a static context. static metodun bir nesnesi yoktur.

Sık yapılan hata

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.

Sık yapılan hata

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.

Sık yapılan hata

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.

Sık yapılan hata

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.

Sık yapılan hata

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.

Sık yapılan hata

Ü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 new ile ü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. Alanlar private, dışarıya sunulan metotlar public olur; 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. new heap'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, boolean false, char '\u0000', referanslar null. Yerel değişkenler varsayılan değer almaz.
  • Örneğe ait alan her nesnede ayrı, static alan sınıfta tek ve ortaktır. static metodun this'i yoktur; örnek üyelerine ancak bir nesne üzerinden ulaşır. final alana bir kez değer verilir; sınıf sabitleri static final ve büyük harfle yazılır.
  • İlklendirme sırası: static alanlar ve static { } blokları sınıf ilk kullanılırken bir kez; sonra her new'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. null referans üzerinden erişim NullPointerException fırlatır; referansı kalmayan nesneyi çöp toplayıcı siler.
  • this metodun çağrıldığı nesnedir; gölgelenen alana this.ad ile ulaşılır ve nesne this ile 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, equals içerik eşitliğini sınar. equals yeniden yazılınca hashCode da yazılır. String ve sarmalayıcılar equals ile karşılaştırılır.
  • Sarmalayıcı sınıflar ilkel değerleri nesne olarak taşır. Otomatik kutulama valueOf, kutudan çıkarma xxxValue() çağrısıdır. −128…127 arası Integer'lar önbellekten gelir; null bir sarmalayıcıyı kutudan çıkarmak NullPointerException fırlatır.
  • Kayıt değiştirilemez veri sınıfıdır; yapıcı, erişimciler, equals, hashCode ve toString kendiliğ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.

4.1.17Kendini Dene

  1. 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.n ve Sayac2.s kaçtır?

    Cevap

    s static'tir ve üç yapıcı da aynı kutuyu artırır: 2, 4, 6. Her yapıcı o anki değeri nesnenin kendi n alanına kopyalar. Sonradan s'nin değişmesi önceki nesnelerin n'ini etkilemez. Sonuç: 2 4 6 6.

  2. Hucre sınıfının tek bir int v alanı ve v'yi atayan bir yapıcısı var. static void degis(Hucre h, Hucre g) { h.v = 10; g = h; g.v = 20; } metodu a = new Hucre(1) ve b = new Hucre(2) ile degis(a, b) olarak çağrılıyor. Sonra a.v ve b.v kaçtır?

    Cevap

    h, a'nın nesnesini gösterir; h.v = 10 onu 10 yapar. g = h yalnızca yerel kopya g'yi a'nın nesnesine yöneltir; b değişmez. g.v = 20 artık a'nın nesnesine yazar. Sonuç: a.v = 20, b.v = 2.

  3. class C { static int t = 0; int a = ++t; { t *= 10; } C() { t++; } } sınıfından iki nesne oluşturuluyor. Birinci nesnenin a'sı, ikinci nesnenin a'sı ve sonunda C.t kaç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.

  4. Zincir sınıfında int 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ında x ve s nedir?

    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.

  5. Integer i1 = 127, i2 = 127, i3 = 1000, i4 = 1000; Long l = 127L; tanımlarından sonra i1 == i2, i3 == i4, i3.equals(i4), l.equals(127) ve l == 127 ne verir?

    Cevap

    true false true false true. 127 önbellekten gelir, 1000 için iki ayrı nesne oluşur. equals değerleri karşılaştırır. l.equals(127)'de 127 bir Integer'a kutulanır; Long'un equals'ı yalnızca başka bir Long'u eşit sayar, bu yüzden false. l == 127'de ise l kutudan çıkarılır ve iki sayı karşılaştırılır: true.

  6. Bu bölümdeki RasyonelSayi sınıfıyla new 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.

  7. Bir öğrenci main'deki non-static method ... cannot be referenced from a static context hatasından kurtulmak için sınıftaki bütün alanları ve metotları static yapıyor. Program derleniyor. Bu çözümün sorunu nedir?

    Cevap

    Alanlar static olunca 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ı static yapmaktır.

  8. RasyonelSayi sınıfında new 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 ve pay / e satırı ArithmeticException: / by zero fırlatır. Yalnızca yapıcı değişir: başta payda == 0 ise anlamlı iletili bir IllegalArgumentException fırlatılır; sadeleştirmeden sonra payda negatifse pay ve paydanın işareti çevrilir. Alanlar private ve 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, hashCode ve toString'in doğru yazılması, değiştirilemez sınıflar ve static fabrika 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, toString maddeleri).