Bilgisayar ve Programlamaya Giriş · Ders Notu

Metotlar

Program büyüdükçe aynı işler farklı yerlerde tekrar tekrar yazılmaya başlar ve main metodu okunamayacak kadar uzar. Metot, bir görevi bir ad altında toplar. Görev bir kez yazılır, sonra adıyla istenen yerden çağrılır. Not önce bir metodun nasıl tanımlandığını ve çağrıldığını anlatır. Ardından bir çağrı sırasında bellekte olanlar gelir: argümanların parametrelere kopyalanması, her çağrıya açılan çerçeve ve bu çerçevelerin oluşturduğu çağrı yığını. Dönüş değeri, void metotlar ve erken dönüş ayrı ayrı ele alınır. Java'nın argümanları neden her zaman değer ile aktardığı, swap örneğiyle adım adım gösterilir. Aşırı yükleme, değişken sayıda argüman, kapsam ve gölgeleme konularında derleyicinin verdiği gerçek hata iletileri de incelenir. Son kesimler iyi metot tasarımını ve tek/çift, basamak sayısı, asal sayı, EBOB, EKOK gibi örnek metotları kapsar. Nottaki bütün programlar JDK 21 ile derlenip çalıştırılmıştır.

Öğrenme çıktıları

Tanımlama
Bir metodun başlığını ve gövdesini yazmak; metodu main'den ve başka metotlardan çağırmak; main metodunun imzasını ve static niteleyicisinin anlamını açıklamak.
Veri akışı
Parametre ile argümanı ayırt etmek; dönüş değerini bir ifadede kullanmak; void metodu ve erken dönüşü doğru yerde kullanmak; Java'da argümanların değer ile aktarıldığını izleyerek göstermek; aşırı yükleme ve değişken sayıda argümanla çalışmak.
Kapsam
Yerel değişkenin nerede görünür olduğunu ve ne kadar yaşadığını açıklamak; çağrı yığınındaki çerçeveleri adım adım izlemek; gölgelemeyi ve kapsamla ilgili derleme hatalarını tanımak.
Tasarım
Bir problemi tek görevli metotlara bölmek; metodun sözleşmesini yazmak; sonucu ekrana yazmak yerine döndüren, kısa, odaklı ve test edilebilir metotlar kurmak.

9.1Metot nedir?

Şimdiye kadar yazılan Java programlarının bütün işi tek bir yerde, main metodunun içinde yapıldı. Kısa programlarda bu yeterlidir. Ama program birkaç yüz satıra ulaşınca aynı hesap üç dört yerde yeniden yazılır. Bir hata düzeltildiğinde öteki kopyalar unutulur. main'in neyi hangi sırayla yaptığı da okunamaz hâle gelir. Metotlar bu sorunun çözümüdür.

9.1.1Bir görevi adlandırmak

Matematikte bir fonksiyon, girdisine karşılık bir çıktı üretir: f(x) = x² için f(3) = 9'dur. Programlamadaki fonksiyon da benzer bir birimdir. Belirli bir görevi yapan kod parçasına bir ad verilir. Bu kod girdilerini dışarıdan alır, işini yapar ve gerekiyorsa bir sonuç verir. Java'da her şey bir sınıfın içinde yazıldığı için fonksiyonlar da sınıfın içinde tanımlanır ve bunlara metot denir.

Tanım 9.1

Metot (method), bir sınıfın içinde tanımlanan, adı olan ve belirli bir görevi yapan deyimler bütünüdür. Metot girdilerini parametreleri aracılığıyla alır, sonucunu dönüş değeri olarak verir. Metodun adıyla birlikte parantez içinde değerler yazılarak metot çalıştırılır; buna çağrı (method call) denir.

Java'nın kendi kütüphanesindeki metotlar bu fikri iyi gösterir. Math.sqrt(16) çağrısı 16'nın karekökünü, yani 4.0'ı verir. Karekökün hangi yöntemle hesaplandığını bilmek gerekmez. Bilinmesi gereken üç şey vardır: metodun adı (sqrt), ne istediği (bir sayı) ve ne verdiği (karekökü). Metodu kullanan için iç ayrıntı gizlidir. Şekil 9.1 bu durumu bir kara kutu olarak gösterir.

girdi53topla(a, b)çıktı8parametrelergövde (ayrıntı gizli)dönüş değeri
Şekil 9.1: Metot bir kara kutu gibidir. topla iki girdi (5 ve 3) alır ve bir sonuç (8) verir. Girdiler parametrelerle içeri girer, sonuç dönüş değeri olarak çıkar; gövdedeki ayrıntı dışarıdan görünmez.

Şekildeki kutunun solunda iki girdi, sağında bir çıktı vardır. Kutunun içinde ne olduğu ancak metodun gövdesine bakılınca görülür. Metodu çağıran kod yalnızca “bu iki sayıyı ver, toplamı al” diye düşünür. Bu ayrım, metotların en önemli kazancıdır: bir işin ne yaptığı ile nasıl yaptığı birbirinden ayrılır.

9.1.2Neden metot yazılır?

Bir öğrenci üç dersin notlarını ekrana yazdıran bir program yazıyor olsun. Her dersin başına ve sonuna çizgi çekmek istiyor. Metot olmadan çizgiyi çizen döngü altı kez kopyalanır. Çizginin uzunluğu değişecek olsa altı yerin hepsi düzeltilmelidir; biri unutulursa çıktı tutarsız olur. Bu döngü cizgiCiz adlı bir metoda konursa altı satırlık tek bir çağrı kalır: cizgiCiz(5);. Uzunluk değişirse yalnızca argüman değişir.

Metot yazmanın kazançları şöyle sıralanabilir:

  • Tekrarın önlenmesi. Aynı kod bir kez yazılır. Bir hata bulunduğunda tek yerde düzeltilir ve düzeltme her çağrıya yansır.
  • Okunabilirlik. İyi adlandırılmış bir metot çağrısı ne yapıldığını söyler. if (asalMi(n)) satırı, bölen arayan bir döngüden çok daha kolay okunur.
  • Test edilebilirlik. Küçük bir metot, programın geri kalanından bağımsız olarak birkaç örnek girdiyle denenebilir. basamakSayisi(-507)'nin 3 vermesi tek satırla kontrol edilir.
  • Yeniden kullanım (reuse). Bir kez yazılan metot başka programlarda, başka metotların içinde de kullanılır. EKOK hesaplayan bir metot, EBOB metodunu çağırarak işini kısaltır.
  • İş bölümü. Büyük bir problem küçük alt görevlere bölünür. Her alt görev ayrı bir metot olur ve ayrı ayrı yazılıp denenir.

9.1.3Bir çağrıda neler olur?

Bir metot çağrıldığında programın akışı geçici olarak çağrıyı yapan yerden ayrılır, metodun gövdesine gider ve metot bitince kaldığı yere döner. Şekil 9.2 bu gidiş dönüşü gösterir.

mainint x = 5;int t = topla(x, 3);System.out.println(t);toplastatic int topla(int a, int b) { return a + b;}① çağrı: a = 5, b = 3② return 8 → çağrı yerine 8③ main kaldığı yerden devam eder: t = 8, sonra println
Şekil 9.2: Bir çağrının yolculuğu. (1) main, topla(x, 3)'ü çağırır; kontrol metoda geçer ve a = 5, b = 3 olur. (2) return 8'i geri gönderir; 8 çağrının yerine geçer. (3) main kaldığı yerden sürer: t = 8 olur, sonra println çalışır.

Bu yolculuk her çağrıda aynı beş adımdan oluşur:

  1. Argümanlar değerlendirilir. Çağrıdaki parantezin içindeki ifadelerin değeri bulunur. topla(x, 3)'te x'in o anki değeri (5) okunur.
  2. Değerler parametrelere kopyalanır. Metot için bellekte yeni bir alan açılır. Birinci değer birinci parametreye, ikinci değer ikinci parametreye yazılır: a = 5, b = 3.
  3. Gövde çalışır. Metodun küme parantezleri arasındaki deyimler sırayla çalışır.
  4. Değer döndürülür. return deyimine gelindiğinde metot biter ve return'ün yanındaki değer çağıran yere gönderilir.
  5. Çağıran kod devam eder. Çağrı ifadesinin yerine dönen değer geçer. Program, çağrının bulunduğu satırdan sürer.

Bu adımlarda gözden kaçmaması gereken bir nokta vardır: her çağrı kendi parametreleri ve kendi yerel değişkenleriyle çalışır. Aynı metot iki kez çağrılırsa iki ayrı bellek alanı açılır. İkinci çağrı, birincinin değişkenlerini görmez; birinci çağrı bittiğinde onun değişkenleri zaten silinmiştir. Değer ile aktarım, kapsam ve çağrı yığını bu kurala dayanır.

9.2Metot tanımı ve çağrısı

Bir metodu kullanmak için önce onu tanımlamak, yani adını, girdilerini, sonucunun tipini ve gövdesini yazmak gerekir. Tanımlanmış metot daha sonra istenen yerden ve istenen sayıda çağrılır. Tanımlama bir kez yapılır, çağrı ise defalarca.

9.2.1İlk metot: topla

İki tam sayının toplamını veren topla metodu ve onu kullanan iki satır şöyledir:

static int topla(int a, int b) {
    int sonuc = a + b;
    return sonuc;
}

int toplam = topla(5, 3);
System.out.println(toplam);

İlk dört satır tanımlamadır. static int topla(int a, int b) satırı metodun başlığıdır. int, metodun bir tam sayı döndüreceğini söyler. topla metodun adıdır. Parantezin içindeki int a, int b, metodun iki tam sayı beklediğini ve bunlara metodun içinde a ve b adıyla erişileceğini bildirir. Küme parantezleri arasındaki iki satır metodun gövdesidir: önce sonuc adlı bir değişkene a + b yazılır, sonra bu değer return ile geri verilir. Son iki satır ise çağrıdır: topla(5, 3) metodu 5 ve 3 ile çalıştırır, dönen değer toplam'a atanır ve ekrana yazılır.

Bu parçalar tek başına derlenmez; bir sınıfın içinde olmaları gerekir. Tam program aşağıdadır. Metot main ile aynı sınıfın içinde, main'in dışında yazılır. Metotlar iç içe tanımlanamaz: topla'yı main'in küme parantezlerinin içine yazmak derleme hatasıdır.

public class Topla {
    static int topla(int a, int b) {
        int sonuc = a + b;
        return sonuc;
    }

    public static void main(String[] args) {
        int toplam = topla(5, 3);
        System.out.println(toplam);
    }
}
8

Program çalışmaya her zaman main'den başlar. topla sınıfta main'den önce yazılmış olsa da kendiliğinden çalışmaz; yalnızca çağrıldığında çalışır. Metotların sınıf içindeki sırası önemli değildir. topla, main'in altına da yazılabilirdi.

Tanımlamadaki a ve b ile çağrıdaki 5 ve 3 farklı rolleri üstlenir ve farklı adlar alır.

Tanım 9.2

Parametre (parameter), ya da tam adıyla biçimsel parametre (formal parameter), metot başlığında tipiyle birlikte bildirilen değişkendir; metodun hangi girdileri beklediğini söyler. Argüman (argument), ya da gerçek argüman (actual argument), çağrı sırasında parantezin içine yazılan ve parametreye kopyalanan değerdir.

int toplam = topla(5, 3);topla(5,3)gerçek argümanlardeğer kopyalanır53abbiçimsel parametrelersonuc = 5 + 3 = 8returntoplam = 8
Şekil 9.3: int toplam = topla(5, 3); çağrısında gerçek argümanlar (5 ve 3) biçimsel parametrelere (a ve b) kopyalanır. Gövdede sonuc = 5 + 3 = 8 hesaplanır ve return 8'i toplam'a gönderir.

Parametre bir değişkendir; argüman ise bir değerdir ya da değeri hesaplanacak bir ifadedir. Parametreler metot her çağrıldığında yeniden doğar ve o çağrının argümanlarıyla dolar. Argümanlar parametrelere sırayla eşlenir: birinci argüman birinci parametreye, ikinci argüman ikinci parametreye. Argüman sayısı parametre sayısıyla aynı olmalı, her argümanın tipi de karşılık gelen parametreye uymalıdır. topla(5) ya da topla(5, 3, 1) çağrıları derlenmez, çünkü topla tam olarak iki tam sayı bekler.

public class EksikArguman {
    static int topla(int a, int b) { return a + b; }
    public static void main(String[] args) {
        System.out.println(topla(5));
    }
}
EksikArguman.java:4: error: method topla in class EksikArguman cannot be applied to given types;
        System.out.println(topla(5));
                           ^
  required: int,int
  found:    int
  reason: actual and formal argument lists differ in length
1 error

Derleyici bu kez daha uzun bir ileti verir: topla metodu verilen tiplere uygulanamaz. required: int,int satırı metodun beklediği parametre tiplerini, found: int satırı çağrıda bulunan argümanları gösterir. Son satır nedeni söyler: gerçek ve biçimsel argüman listelerinin uzunlukları farklıdır.

Sık yapılan hata

Çağrıda argümanın tipini yeniden yazmak: topla(int 5, int 3). Tipler yalnızca tanımlamada, parametrelerin yanında yazılır. Çağrıda yalnızca değerler ya da ifadeler bulunur: topla(5, 3). Tersine, tanımlamada parametre tipini unutmak da hatadır: static int topla(a, b) derlenmez. Her parametrenin kendi tipi ayrı ayrı yazılır; int a, b biçimi parametre listesinde geçerli değildir.

9.2.2Çağrının sonucunu kullanmak

Değer döndüren bir metodun çağrısı, Java'da bir ifadedir. Çağrı çalıştıktan sonra yerine dönen değer geçer. Bu yüzden çağrı, bir değerin kullanılabildiği her yerde kullanılabilir: bir atamanın sağında, bir aritmetik işlemin içinde, bir koşulda ya da başka bir metodun argümanı olarak. Aşağıdaki program dört farklı kullanımı bir arada gösterir:

public class ToplaKullan {
    static int topla(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) {
        int x = 5;
        int t = topla(x, 3);                     // değişken ve sabit argüman
        System.out.println(t);
        System.out.println(topla(10, 20) * 2);   // çağrı bir ifadenin parçası
        System.out.println(topla(topla(1, 2), 4)); // çağrının sonucu başka çağrıya argüman
        topla(7, 8);                             // dönen değer kullanılmadı
    }
}
8
60
7

İlk çağrı bir değişkeni ve bir sabiti argüman olarak alır. x'in değeri 5 olduğundan topla(5, 3) = 8 hesaplanır. İkinci çağrıda topla(10, 20) önce 30'u döndürür, ardından bu değer 2 ile çarpılır ve 60 yazılır. Üçüncü satırda bir çağrının sonucu başka bir çağrıya argüman olur. Java önce içteki topla(1, 2)'yi çalıştırır, 3'ü bulur; sonra dıştaki çağrı topla(3, 4) olarak çalışır ve 7 yazılır.

Son satırdaki topla(7, 8); da geçerli bir deyimdir. Metot 15'i döndürür ama bu değer hiçbir yerde kullanılmadığı için atılır. Java, dönen değerin kullanılmasını zorunlu tutmaz. Ancak bu satır programa hiçbir şey katmaz: topla yalnızca hesap yapar, ekrana yazmaz, hiçbir değişkeni değiştirmez. Değer döndüren bir metot çağrılıp sonucu atılıyorsa bu genellikle bir unutkanlığın işaretidir.

Bir ifadede birden fazla çağrı varsa Java argümanları ve işlenenleri soldan sağa değerlendirir. Aşağıdaki küçük metot, çağrıldığını anlamak için argümanını köşeli parantez içinde yazıyor ve iki katını döndürüyor:

public class SiraOrnek {
    static int iki(int x) {
        System.out.print("[" + x + "] ");
        return 2 * x;
    }

    public static void main(String[] args) {
        int r = iki(3) - iki(1);
        System.out.println(r);
        System.out.println(iki(iki(1)));
    }
}
[3] [1] 4
[1] [2] 4

iki(3) - iki(1) ifadesinde önce soldaki çağrı çalışır ve [3] yazar, sonra sağdaki çalışır ve [1] yazar. Çıkarma iki değer de hazır olunca yapılır: 6 − 2 = 4. Son satırdaki iç içe çağrıda ise içteki iki(1) önce çalışmak zorundadır, çünkü dıştaki çağrının argümanı onun sonucudur. Önce [1], sonra [2] yazılır ve 4 basılır. Ekrana yazdırmak ile değer döndürmek birbirinden bağımsızdır. Metot ekrana ne yazarsa yazsın, ifadeye katılan yalnızca dönüş değeridir.

9.2.3topla(5, 3) adım adım

Bir çağrıda olup biteni ayrıntısıyla görmek için kod satır satır yürütülür. Satır numaraları aşağıdaki koda göredir; 7. ve 8. satırlar main'in içindedir.

static int topla(int a, int b) {
    int sonuc = a + b;
    return sonuc;
}

// main içinde:
int toplam = topla(5, 3);
System.out.println(toplam);

Tablo 9.1 her adımda hangi satırın çalıştığını, bellekte hangi çerçevelerin açık olduğunu ve en üstteki çerçevedeki değişkenleri verir. Çerçeve, bir çağrıya ayrılmış bellek alanıdır; kesin tanımı çağrı yığını ile birlikte verilir.

Tablo 9.1: topla(5, 3) çağrısının izleme tablosu. Çağrı yığını sütununda en üstteki çerçeve önce yazılmıştır.
AdımSatırÇağrı yığını (üstteki önce)Değişkenler (üstteki çerçeve)Çıktı
17main()——
21topla(5, 3) ← main()a=5, b=3—
32topla(5, 3) ← main()a=5, b=3, sonuc=8—
43topla(5, 3) ← main()a=5, b=3, sonuc=8—
57main()toplam=8—
68main()toplam=88

Yürütme main'deki 7. satırla başlar. Atamanın sağ tarafı hesaplanmadan toplam'a bir değer yazılamaz. Sağ tarafta bir çağrı olduğu için önce o çalışmalıdır. 2. adımda topla için yeni bir çerçeve açılır ve argümanlar kopyalanır: a = 5, b = 3. 3. adımda yerel değişken sonuc oluşur ve 8 değerini alır. Bu değişken yalnızca topla'nın çerçevesinde yaşar; main onu göremez. 4. adımda return sonuc; çalışır, 8 çağırana gönderilir ve topla'nın çerçevesi kapanır. 5. adımda yürütme 7. satıra döner. Çağrının yerine 8 geçmiştir ve atama tamamlanır: toplam = 8. Bu noktada a, b ve sonuc artık yoktur. 6. adımda 8 ekrana yazılır.

main()toplam = topla(5, 3)toplaa5b3 main()toplam = topla(5, 3)toplaa5b3sonuc8kapandıreturn 8 main()toplam8toplam = 8
Şekil 9.4: topla(5, 3) çağrısının üç anı. (a) topla'nın çerçevesi main'in üstüne açıldı: a = 5, b = 3. (b) return 8: çerçeve kapanıyor. (c) main'e dönüldü: toplam = 8; a, b ve sonuc artık yok.

İzleme tablosunda dikkat çeken bir ayrıntı vardır: 7. satır iki kez görünür. İlk kez çağrı başlarken, ikinci kez çağrı bittikten sonra atama tamamlanırken. Bir satırın “yarıda bırakılıp” sonra sürdürülmesi, metot çağrılarının doğal bir sonucudur. Yürütme bir çağrıya rastladığında bulunduğu satırı askıya alır, metodu bitirir ve aynı satıra geri gelir.

9.3Çağrı yığını

topla örneğinde yalnızca iki metot vardı: main ve topla. Gerçek programlarda bir metot başka bir metodu, o da bir başkasını çağırır. Bir anda birden çok metot “yarıda” bekler. Java hangi metodun nerede kaldığını ve hangi değişkenlerin kime ait olduğunu bir yığın yapısıyla izler.

9.3.1Yığın ve çerçeve

Yemekhanede üst üste konmuş tabaklar düşünülsün. Yeni tabak hep en üste konur, alınan tabak da hep en üstten alınır. En son konan tabak ilk alınır. Bu düzene son giren ilk çıkar denir ve böyle davranan yapıya yığın (stack) adı verilir. Metot çağrıları da tam olarak bu düzende açılır ve kapanır.

Tanım 9.3

Çerçeve (stack frame), bir metot çağrısı için bellekte ayrılan alandır. Çerçevede o çağrının parametreleri, yerel değişkenleri ve metot bitince yürütmenin nereye döneceği bilgisi tutulur. Çağrı yığını (call stack), o anda devam eden bütün çağrıların çerçevelerinden oluşan yığındır. Her çağrı yığının üstüne yeni bir çerçeve koyar; metot bitince en üstteki çerçeve kaldırılır.

Program başladığında yığında tek bir çerçeve vardır: main'in çerçevesi. main bir metodu çağırınca o metodun çerçevesi main'in üstüne konur. O metot da bir başkasını çağırırsa yığın bir kat daha yükselir. Bu düzenden iki kural çıkar. Birincisi, yalnızca en üstteki çerçevenin metodu çalışır; alttakiler, üstlerindeki çağrının bitmesini bekler. İkincisi, en son çağrılan metot ilk biter. Bir metot, kendisinin çağırdığı metot bitmeden bitemez.

9.3.2ortalama(9, 7) adım adım

Aşağıdaki örnekte ortalama metodu iki sayının ortalamasını bulmak için topla metodunu çağırır. Böylece yığında üç çerçeve birden bulunur:

static int topla(int a, int b) {
    return a + b;
}
static int ortalama(int x, int y) {
    int t = topla(x, y);
    return t / 2;
}
int o = ortalama(9, 7);  // main içinde
System.out.println(o);

Satır numaraları: 1–3 topla, 4–7 ortalama, 8–9 main'deki iki satırdır. Tablo 9.2 dokuz adımı verir.

Tablo 9.2: ortalama(9, 7) çağrısının izleme tablosu. 4. ve 5. adımlarda yığında üç çerçeve vardır.
AdımSatırÇağrı yığını (üstteki önce)Değişkenler (üstteki çerçeve)Çıktı
18main()——
24ortalama(9, 7) ← main()x=9, y=7—
35ortalama(9, 7) ← main()x=9, y=7—
41topla(9, 7) ← ortalama(9, 7) ← main()a=9, b=7—
52topla(9, 7) ← ortalama(9, 7) ← main()a=9, b=7—
65ortalama(9, 7) ← main()x=9, y=7, t=16—
76ortalama(9, 7) ← main()x=9, y=7, t=16—
88main()o=8—
99main()o=88

1. adımda main, ortalama(9, 7)'yi çağırır; yığında henüz tek çerçeve vardır. 2. adımda ortalama'nın çerçevesi main'in üstüne konur ve argümanlar kopyalanır: x = 9, y = 7. 3. adımda ortalama, 5. satırda kendisi bir çağrı yapar: topla(x, y). Gönderilen, x ve y değişkenlerinin kendisi değil, değerleridir: 9 ve 7. 4. adımda topla'nın çerçevesi en üste konur ve yığında üç çerçeve olur. Bu anda yalnızca topla çalışır. ortalama 5. satırda, main de 8. satırda bekler.

5. adımda topla 16'yı döndürür ve çerçevesi kaldırılır. Son açılan çerçeve ilk kapanmıştır. 6. adımda yürütme ortalama'nın 5. satırına döner: çağrının yerine 16 geçer ve t = 16 olur. 7. adımda return t / 2; çalışır. İki tam sayının bölümü tam sayı bölmesidir: 16 / 2 = 8. ortalama'nın çerçevesi de kapanır. 8. adımda main'e dönülür ve o = 8 olur. Bu anda x, y, t, a ve b değişkenlerinin hiçbiri artık yoktur. 9. adımda 8 yazılır.

main()o = ortalama(9, 7)ortalamax9y7 main()o = ortalama(9, 7)ortalamax9y7t = topla(x, y)toplaa9b7
main()o = ortalama(9, 7)ortalamax9y7t = topla(x, y)toplaa9b7kapandıreturn 16 main()o = ortalama(9, 7)ortalamax9y7t16kapandıreturn 8
Şekil 9.5: Çağrı yığınının dört anı. (a) ortalama'nın çerçevesi main'in üstünde: x = 9, y = 7. (b) topla en üstte; yığında üç çerçeve var. (c) topla 16 döndürüp kapanıyor. (d) ortalama 8 döndürüp kapanıyor; yığında yine yalnızca main kalacak.

Programın tamamı ve gerçek çıktısı aşağıdadır. İkinci satırdaki ortalama(9, 8) çağrısı tam sayı bölmesinin etkisini gösterir: 9 + 8 = 17 ve 17 / 2 = 8'dir. Gerçek ortalama 8,5 olsa da dönüş tipi int ve bölme iki int arasında olduğu için kesir atılır.

public class Ortalama {
    static int topla(int a, int b) {
        return a + b;
    }

    static int ortalama(int x, int y) {
        int t = topla(x, y);
        return t / 2;
    }

    public static void main(String[] args) {
        int o = ortalama(9, 7);
        System.out.println(o);
        System.out.println(ortalama(9, 8));
    }
}
8
8
Sık yapılan hata

Metotlardaki aynı adları aynı değişken sanmak. ortalama'nın x'i ile main'deki bir x, ya da topla'nın a'sı ile başka bir metodun a'sı birbirinden tamamen bağımsızdır. Her biri kendi çerçevesindeki ayrı bir bellek kutusudur. Değer bir metottan ötekine yalnızca iki yolla geçer: çağrıda argüman olarak ya da return ile dönüş değeri olarak.

9.3.3Yığının kuralları

Çağrı yığını Java'ya özgü değildir; C, Python ve hemen her programlama dilindeki metot ya da fonksiyon çağrıları aynı biçimde işler. İzleme sırasında şu kurallar akılda tutulursa çoğu sorunun cevabı kendiliğinden çıkar:

  • Her çağrı yeni bir çerçeve açar. Aynı metot iki kez çağrılırsa iki ayrı çerçeve açılır, ikinci çağrı birincinin değişkenlerini görmez.
  • Yalnızca en üstteki çerçeve çalışır. Alttaki her çerçeve, bir çağrının ortasında, o çağrının sonucunu bekler.
  • Metot bitince çerçevesi bütün içeriğiyle silinir. Yerel değişkenler ve parametreler bir çağrıdan ötekine değer taşımaz.
  • Dönen değer, çağrının yapıldığı ifadede çağrının yerine geçer ve hesap oradan sürer.
  • Son açılan çerçeve ilk kapanır. Çerçeveler açıldıkları sıranın tersiyle kapanır.

Yığının bir sınırı da vardır. Java her iş parçacığına sınırlı büyüklükte bir yığın ayırır. Birbirini sonu gelmeden çağıran metotlar yığını doldurursa program StackOverflowError hatasıyla durur. Sıradan programlarda yığın derinliği birkaç ya da birkaç düzine çerçeveyi geçmez. Metot Çağrı Yığını simülatörü, çerçevelerin açılıp kapanışını adım adım canlandırır.

9.4Metot tanımlamasının parçaları

Bir metot tanımlaması iki kısımdan oluşur: başlık ve gövde. Başlık metodun dış dünyaya görünen yüzüdür. Kimin çağırabileceğini, ne döndüreceğini, adını ve hangi girdileri beklediğini söyler. Gövde ise metodun işini yapan deyimleri içerir.

9.4.1Başlık ve gövde

Şekil 9.6 public static int topla(int a, int b) başlığını parçalarına ayırır.

public static int topla(int a, int b) {erişimkim çağırabilir?niteleyicinesne gerekmezdönüş tipine döner?adgörevin adıparametre listesihangi girdiler?int sonuc = a + b;return sonuc;}gövde: çağrılınca çalışan deyimlerreturn ile dönüş tipinde değer veririmza = ad + parametretipleri: topla(int, int)
Şekil 9.6: Bir metot tanımlamasının parçaları: erişim belirteci (public), niteleyici (static), dönüş tipi (int), ad (topla), parametre listesi (int a, int b) ve küme parantezleri arasındaki gövde. Ad ile parametre tipleri birlikte metodun imzasını oluşturur: topla(int, int).
Tablo 9.3: Metot başlığının parçaları ve cevapladıkları sorular.
ParçaÖrnekCevapladığı soruAçıklama
erişim belirtecipublicKim çağırabilir?public her yerden, private yalnızca aynı sınıftan çağrılabilir. Yazılmazsa aynı paketteki sınıflar çağırabilir.
niteleyicistaticNesne gerekir mi?static metot sınıfa aittir; çağırmak için nesne oluşturmak gerekmez.
dönüş tipiintNe döner?Metodun vereceği değerin tipi. Değer döndürmüyorsa void.
adtoplaGörevin adı nedir?Metodun ne yaptığını söyleyen tanımlayıcı.
parametre listesi(int a, int b)Hangi girdiler?Virgülle ayrılmış “tip ad” çiftleri; girdi yoksa boş parantez ().
gövde{ … }Nasıl yapılır?Çağrılınca çalışan deyimler; return ile dönüş tipinde bir değer verir.

Başlıktaki parçaların sırası sabittir: önce erişim belirteci ve niteleyici, sonra dönüş tipi, sonra ad ve parametre listesi. int static topla(…) gibi bir sıralama derlenmez. Erişim belirteci ve static isteğe bağlıdır; dönüş tipi, ad ve parantezler ise zorunludur. Bu nottaki örneklerde metotlar main ile aynı sınıfta ve static olarak yazılır. Erişim belirteci çoğunlukla yazılmaz, çünkü metotlar zaten yalnızca aynı sınıftan çağrılır.

Tanım 9.4

İmza (method signature), bir metodun adı ile parametrelerinin tiplerinin sırayla yazılmış listesidir. static int topla(int a, int b) metodunun imzası topla(int, int)'dir. Dönüş tipi, parametre adları ve niteleyiciler imzaya dahil değildir. Bir sınıfta aynı imzalı iki metot bulunamaz.

İmza, derleyicinin bir çağrıyı hangi metoda bağlayacağına karar verirken baktığı bilgidir. topla(5, 3) çağrısını gören derleyici, adı topla olan ve iki int kabul eden bir metot arar. Parametre adlarının (a, b) imzada yer almaması doğaldır, çünkü çağrıda bu adlar hiç görünmez. Dönüş tipi de imzada yer almaz, çünkü çağrının kendisinde dönen değerin tipine dair bir ipucu yoktur.

9.4.2Metot adları

Metot adları değişken adlarıyla aynı kurallara uyar: harfle başlar, harf, rakam ve alt çizgi içerebilir, Java'nın ayrılmış sözcüklerinden biri olamaz. Java geleneğinde metot adları küçük harfle başlar ve birden çok sözcük birleşirse sonraki sözcüklerin ilk harfi büyük yazılır: basamakSayisi, yazdirPozitif, enBuyuk3. Türkçe karakterler (ç, ğ, ı, ö, ş, ü) Java'da geçerlidir ama dosyaların farklı bilgisayarlarda sorunsuz açılması için çoğu zaman kullanılmaz.

İyi bir metot adı metodun ne yaptığını söyler. Bir iş yapan metotlar bir eylemle adlandırılır: cizgiCiz, raporYaz, notlariOku. Bir değer hesaplayan metotlar o değerin adını alır: ortalama, alan, ebob. boolean döndüren metotlar ise bir soru gibi okunur: ciftMi, asalMi. Böyle adlandırılan bir metot bir koşulda kullanıldığında satır neredeyse düz Türkçe gibi okunur: if (asalMi(n)), “n asal mı?”.

9.4.3main metodu ve static

Her Java programında bulunan main de bir metottur ve başlığındaki her sözcüğün bir nedeni vardır.

Tablo 9.4: public static void main(String[] args) başlığının parçaları.
ParçaNeden?
publicJVM (Java Sanal Makinesi) metodu sınıfın dışından çağırabilsin.
staticMetot, hiçbir nesne üretilmeden çağrılabilsin.
voidmain bir değer döndürmez.
mainJVM'nin programı başlatmak için aradığı ad.
String[] argsProgram çalıştırılırken komut satırından verilen argümanlar.

static niteleyicisinin anlamı şöyledir. Java nesneye yönelik bir dildir; çoğu metot bir nesneye aittir ve o nesne üzerinden çağrılır. static metot ise bir nesneye değil, sınıfın kendisine aittir. Çağırmak için önce bir nesne oluşturmak gerekmez. JVM programı başlattığında henüz hiçbir nesne yoktur. main'i çalıştırabilmesi için bu metodun nesne gerektirmemesi, yani static olması zorunludur.

Bu durumun bir sonucu vardır: static bir metot, aynı sınıftaki static olmayan bir metodu ya da alanı doğrudan kullanamaz, çünkü ortada o metodun ait olacağı bir nesne yoktur. Aşağıdaki örnek bu hatayı gösterir. Aşağıda örneğin tam bir sınıf içindeki hâli ve JDK 21 derleyicisinin verdiği ileti yer alıyor:

public class Ornek {
    void selamla() { }
    public static void main(String[] args) {
        selamla();   // hata
    }
}
Ornek.java:4: error: non-static method selamla() cannot be referenced from a static context
        selamla();   // hata
        ^
1 error

İleti, “static olmayan selamla() metoduna static bir bağlamdan başvurulamaz” der. Derleyici hatanın satırını (Ornek.java:4) ve ^ işaretiyle yerini gösterir. Bu notun kapsamında çözüm basittir: çağrılan metodu da static yapmak.

public class StaticCozum {
    static void selamla() {
        System.out.println("Merhaba");
    }

    public static void main(String[] args) {
        selamla();
    }
}
Merhaba

main'in parametresi String[] args, program çalıştırılırken komut satırına yazılan sözcükleri bir dizi olarak alır. Aşağıdaki program kaç argüman geldiğini ve bunları yazar.

public class MainArgs {
    public static void main(String[] args) {
        System.out.println(args.length);
        for (String a : args) System.out.println(a);
    }
}

java MainArgs komutuyla argümansız çalıştırıldığında args boş bir dizidir ve program yalnızca 0 yazar. java MainArgs Ali 42 ile çalıştırıldığında çıktı şudur:

2
Ali
42

Komut satırındaki her sözcük ayrı bir String olarak gelir. 42 de bir sayı değil, iki karakterlik bir metindir.

Sık yapılan hata

main'in başlığını eksik ya da farklı yazmak. static unutulursa ya da ad Main yazılırsa program derlenebilir, ama çalıştırılırken JVM başlangıç noktasını bulamaz ve programı başlatmaz. Java büyük-küçük harf ayrımı yapar: main ile Main iki ayrı addır.

9.5Dönüş değeri

Bir metot işini bitirince çağırana bir sonuç verebilir ya da hiçbir şey vermeden yalnızca bir iş yapabilir. Hangisinin olacağını başlıktaki dönüş tipi belirler. Sonuç return deyimiyle verilir.

9.5.1return deyimi

Tanım 9.5

Dönüş tipi (return type), metodun vereceği değerin tipidir ve başlıkta adın önüne yazılır. Dönüş değeri (return value), metodun return deyimiyle çağırana gönderdiği değerdir. return ifade; deyimi ifadenin değerini hesaplar, metodu hemen bitirir ve bu değeri çağrının yapıldığı yere döndürür.

return iki iş birden yapar: bir değer gönderir ve metodu bitirir. return'den sonra aynı yolda yazılmış deyimler çalışmaz. Derleyici, return'den hemen sonra gelen ve hiçbir yolla ulaşılamayacak bir deyimi “ulaşılamaz deyim” (unreachable statement) hatasıyla reddeder.

Döndürülen değerin tipi dönüş tipine uymalıdır. int döndüren bir metot return 8; yazabilir, return "sekiz"; yazamaz. Dönüş tipi double ise return 8; geçerlidir; int değer kendiliğinden double'a genişletilir ve çağırana 8.0 gider. Tersi geçerli değildir: int döndüren metot return 2.5; yazamaz, çünkü kesirli kısım kaybolurdu.

9.5.2void metotlar

Her metodun bir değer hesaplaması gerekmez. Ekrana bir çizgi çizen, bir rapor yazan ya da bir dosyaya kayıt ekleyen metodun çağırana gönderecek bir sonucu yoktur. Bu metotların dönüş tipi void'dir. void “boş” demektir: metot çağrıya bir değer döndürmez.

static void cizgiCiz(int n) {
    for (int i = 0; i < n; i++)
        System.out.print("-");
    System.out.println();
}
// main içinde:
cizgiCiz(5);           // -----
int x = cizgiCiz(5);   // HATA: void değer vermez

cizgiCiz metodu n tane tire yazar ve satırı bitirir. Metodun işi bir yan etkidir: ekranın durumunu değiştirir ama çağırana bir değer vermez. Tam program ve çıktısı:

public class CizgiCiz {
    static void cizgiCiz(int n) {
        for (int i = 0; i < n; i++)
            System.out.print("-");
        System.out.println();
    }

    public static void main(String[] args) {
        cizgiCiz(5);
        System.out.println("Metotlar");
        cizgiCiz(8);
        cizgiCiz(0);
        System.out.println("son");
    }
}
-----
Metotlar
--------

son

cizgiCiz(5) beş, cizgiCiz(8) sekiz tire yazar. cizgiCiz(0) çağrısında döngü hiç dönmez; yalnızca println() çalışır ve çıktıda boş bir satır oluşur. Örnekteki son satır, int x = cizgiCiz(5);, derlenmez. Metot bir değer vermediği için atamanın sağ tarafında kullanılamaz:

VoidAtama.java:9: error: incompatible types: void cannot be converted to int
        int x = cizgiCiz(5);   // HATA: void değer vermez
                        ^
1 error

İleti “uyumsuz tipler: void, int'e dönüştürülemez” der. Aynı nedenle void bir çağrı başka bir metoda argüman da olamaz:

public class VoidYaz {
    static void cizgiCiz(int n) {
        System.out.println("-".repeat(n));
    }
    public static void main(String[] args) {
        System.out.println(cizgiCiz(5));
    }
}
VoidYaz.java:6: error: 'void' type not allowed here
        System.out.println(cizgiCiz(5));
                                   ^
1 error

Bu kez derleyici “void tipine burada izin verilmez” der. Kural tektir: void bir çağrı yalnızca kendi başına bir deyim olarak, sonuna noktalı virgül konarak yazılabilir. Bir ifadenin parçası olamaz.

topla(5, 3)return8int t = 8;değer çağrı yerine gelircizgiCiz(5)-----ekrana yazar (yan etki), değer döndürmezint x = cizgiCiz(5); ✗
Şekil 9.7: Değer döndüren ve döndürmeyen metot. topla(5, 3)'ün döndürdüğü 8 çağrının yerine gelir ve atamada kullanılır. cizgiCiz(5) yalnızca ekrana yazar; sonucu bir değişkene atanamaz.

void bir metotta return zorunlu değildir. Metot, gövdesinin kapanan küme parantezine ulaşınca kendiliğinden biter. Yine de void metotta değersiz bir return; deyimi yazılabilir. Bu deyim bir değer göndermez, yalnızca metodu o noktada bitirir. Erken dönüş kalıbı bu deyime dayanır.

Sık yapılan hata

Bir sonucu hem yazdırmak hem döndürmek gerektiğini sanmak ya da ikisini karıştırmak. System.out.println(x) değeri ekrana yazar ama çağırana bir şey göndermez. return x; ise değeri çağırana gönderir ama ekrana hiçbir şey yazmaz. Hesap yapan metotlar sonucu döndürmeli, yazdırma kararını çağırana bırakmalıdır.

9.5.3Erken dönüş

Bazı girdiler için metodun yapacak bir işi yoktur. Bu durum metodun başında yakalanır ve metottan hemen çıkılır. Böylece asıl iş, iç içe if blokları içine gömülmeden düz bir sırayla yazılır.

Tanım 9.6

Erken dönüş (early return), özel bir durumu metodun başında bir koşulla yakalayıp return ile metodu gövdenin sonuna gelmeden bitirmektir. void metotta değersiz return;, değer döndüren metotta ise o durum için uygun bir değerle return kullanılır.

static void yazdirPozitif(int sayi) {
    if (sayi <= 0) return;
    System.out.println(sayi);
}

yazdirPozitif, yalnızca pozitif sayıları yazar. Sayı 0 ya da negatifse return; metodu hemen bitirir ve println'e hiç gelinmez. Şekil 9.8 bu iki yolu akış diyagramıyla gösterir.

yazdirPozitif(sayi)sayi <= 0 ?evetreturn;erken çıkışhayırprintln(sayi);metot biteryazdirPozitif(-3) → çıktı yokyazdirPozitif(7) → 7
Şekil 9.8: yazdirPozitif'in akışı. sayi <= 0 ise Evet dalı return; ile metottan erken çıkar; değilse sayı yazılır ve metot sona ulaşır. yazdirPozitif(-3) hiçbir şey yazmaz, yazdirPozitif(7) 7 yazar.

Aşağıdaki programda metot iki kez çağrılır: önce −3, sonra 7 ile. Satır numaraları: 1–4 metot, 6 ve 7 main'deki iki çağrıdır. Tablo 9.5 iki çağrıyı arka arkaya izler.

static void yazdirPozitif(int sayi) {
    if (sayi <= 0) return;
    System.out.println(sayi);
}
// main içinde:
yazdirPozitif(-3);
yazdirPozitif(7);
Tablo 9.5: yazdirPozitif(-3) ve yazdirPozitif(7) çağrılarının izleme tablosu.
AdımSatırÇağrı yığını (üstteki önce)Değişkenler (üstteki çerçeve)Çıktı
16main()——
21yazdirPozitif(-3) ← main()sayi=−3—
32yazdirPozitif(-3) ← main()sayi=−3—
47main()——
51yazdirPozitif(7) ← main()sayi=7—
62yazdirPozitif(7) ← main()sayi=7—
73yazdirPozitif(7) ← main()sayi=77
84yazdirPozitif(7) ← main()sayi=77

İlk çağrıda çerçeve açılır ve sayi = −3 olur. 2. satırda −3 <= 0 doğrudur ve return; çalışır. Metot hemen biter; 3. satırdaki println'e hiç gelinmez ve çıktı boş kalır. main bir sonraki satırdan, yani ikinci çağrıdan devam eder. İkinci çağrı yepyeni bir çerçeve açar: sayi = 7. Önceki çağrının sayi değişkeni çoktan silinmiştir; iki çağrının hiçbir ortak değişkeni yoktur. Bu kez 7 <= 0 yanlıştır, erken dönüş olmaz ve 7 yazılır. Son adımda yürütme 4. satırdaki kapanan küme parantezine gelir; void metot burada kendiliğinden döner. Programın tam hâline bir de yazdirPozitif(0) çağrısı eklenince çıktı değişmez, çünkü 0 da erken dönüşe takılır:

public class YazdirPozitif {
    static void yazdirPozitif(int sayi) {
        if (sayi <= 0) return;
        System.out.println(sayi);
    }

    public static void main(String[] args) {
        yazdirPozitif(-3);
        yazdirPozitif(7);
        yazdirPozitif(0);
    }
}
7

Erken dönüş olmadan aynı metot if (sayi > 0) { System.out.println(sayi); } biçiminde yazılırdı. Tek satırlık bir işte iki yazım arasında fark azdır. Ama asıl iş on satır sürüyorsa ve üç farklı özel durum varsa, erken dönüşsüz yazım üç kat iç içe if doğurur. Erken dönüşle her özel durum metodun başında bir satırda elenir ve asıl iş girintisiz kalır.

Değer döndüren metotlarda da erken dönüş sık kullanılır. Bu durumda return'ün yanında o özel durum için doğru değer yazılır. Basamak sayısı metodundaki if (kalan == 0) return 1; ve asal sayı metodundaki if (n < 2) return false; satırları birer erken dönüştür.

9.5.4return ve tip hataları

Derleyici, değer döndüren bir metodun her durumda gerçekten bir değer döndürdüğünü denetler. Aşağıdaki ilk örnekte metot yalnızca n pozitifken bir değer döndürür:

public class EksikReturn {
    static int yaz(int n) {
        if (n > 0) {
            return 1;
        }
    }
}
EksikReturn.java:6: error: missing return statement
    }
    ^
1 error

İleti “eksik return deyimi” der ve metodun kapanan küme parantezini gösterir. Derleyici şöyle düşünür: n pozitif değilse if'in içine girilmez ve yürütme metodun sonuna ulaşır. Bu yolda döndürülecek bir değer yoktur. void olmayan bir metotta her yol bir return ile bitmelidir. Çözüm, eksik yolu kapatmaktır: metodun sonuna return 0; eklemek ya da if'e bir else dalı yazıp orada da bir değer döndürmek.

Derleyici bu denetimi koşulun değerine bakmadan yapar. if (n > 0) return 1; if (n <= 0) return 0; yazılsa bile sonuç değişmez. İki koşul insan için bütün durumları kapsar, ama derleyici yalnızca iki ayrı if görür ve aynı hatayı verir. Derleyiciyi ikna etmenin yolu if-else kullanmak ya da metodun sonuna koşulsuz bir return koymaktır.

İkinci hata türü, döndürülen değerin dönüş tipine sığmamasıdır:

public class KayipDonusum {
    static int f() {
        return 2.5;
    }
}
KayipDonusum.java:3: error: incompatible types: possible lossy conversion from double to int
        return 2.5;
               ^
1 error

İleti “uyumsuz tipler: double'dan int'e dönüşümde kayıp olabilir” der. Java, bilgi kaybına yol açabilecek daraltma (narrowing conversion) dönüşümlerini kendiliğinden yapmaz. Programcı kesirin atılmasını gerçekten istiyorsa bunu açık dönüşümle belirtir: return (int) 2.5; derlenir ve 2 döner. Kesirli sonuç isteniyorsa dönüş tipi double yapılır.

return ile dönüş tipinin uyuşmadığı iki durum daha vardır. void bir metot bir değer döndüremez; değer döndüren bir metotta da değersiz return; yazılamaz:

public class VoidReturn {
    static void g() {
        return 1;
    }
}
VoidReturn.java:3: error: incompatible types: unexpected return value
        return 1;
               ^
1 error
public class DonusDegerYok {
    static int h() {
        return;
    }
}
DonusDegerYok.java:3: error: incompatible types: missing return value
        return;
        ^
1 error
Tablo 9.6: return ile ilgili derleme hataları (JDK 21 iletileri) ve çözümleri.
DurumİletiÇözüm
Bir yolda return yokmissing return statementeksik dala ya da metodun sonuna return eklemek
int metotta return 2.5;incompatible types: possible lossy conversion from double to int(int) ile açık dönüşüm ya da dönüş tipini double yapmak
void metotta return 1;incompatible types: unexpected return valuedeğeri silmek ya da dönüş tipini değiştirmek
int metotta return;incompatible types: missing return valuereturn'e bir değer yazmak
void çağrıyı atamakincompatible types: void cannot be converted to intmetodu değer döndürecek biçimde yeniden yazmak
void çağrıyı argüman yapmak'void' type not allowed hereçağrıyı ayrı bir deyim olarak yazmak
Örnek 9.1

Bir sayının işaretini −1, 0 ya da 1 olarak döndüren isaret metodu şöyle yazılmış: static int isaret(int x) { if (x > 0) return 1; else if (x < 0) return -1; }. Derlenir mi?

Derlenmez. x = 0 için iki koşul da yanlıştır ve yürütme metodun sonuna, return'süz olarak ulaşır. Derleyici missing return statement hatası verir. Zincirin sonuna else return 0; ya da metodun sonuna return 0; eklenince her yol bir değerle biter: isaret(5) = 1, isaret(−3) = −1, isaret(0) = 0.

9.6Parametre aktarımı

Bir çağrıda argümanın değeri parametreye geçer. Peki metot parametresini değiştirirse çağıranın değişkeni de değişir mi? Bu sorunun cevabı Java'da kesindir ve birçok yeni programcıyı şaşırtır.

9.6.1Değer ile aktarım

Tanım 9.7

Değer ile aktarım (pass by value), bir çağrıda argümanın değerinin kopyalanıp parametreye yazılmasıdır. Parametre, argümandan ayrı bir bellek kutusudur. Metot parametresine ne yaparsa yapsın, çağıranın değişkeni etkilenmez. Java argümanları her zaman değer ile aktarır.

int, double, boolean, char gibi ilkel tiplerde aktarılan şey sayının ya da karakterin kendisidir. Çağıranın değişkeni ile metodun parametresi iki ayrı kutudur; çağrı anında ikincisine birincinin içindekinin bir kopyası yazılır. Kopyalamadan sonra iki kutu birbirinden bağımsızdır.

main()sayi5artir(sayi)x5değer kopyalanır
Şekil 9.9: artir(sayi) çağrısında main'deki sayi kutusunun değeri (5) artir'ın çerçevesindeki x kutusuna kopyalanır. İki ayrı bellek kutusu vardır.

Nesnelerde, örneğin dizilerde, durum ilk bakışta farklı görünür. Bir dizi değişkeninin içinde dizinin kendisi değil, dizinin bellekteki yerini gösteren bir referans bulunur. Diziyi bir metoda gönderirken kopyalanan bu referanstır. Kopya referans da aynı diziyi gösterdiği için metot dizinin öğelerini değiştirebilir. Ama bu da değer ile aktarımdır: kopyalanan değer bir referanstır. Metot parametresine yeni bir dizi atarsa çağıranın değişkeni yine eski diziyi gösterir.

9.6.2artir(sayi) adım adım

İlk örnek, bir sayıyı artırmaya çalışan bir void metottur:

static void artir(int x) {
    x++;
}
// main içinde:
int sayi = 5;
artir(sayi);
System.out.println(sayi);   // 5

Satır numaraları: 1–3 artir, 5–7 main'deki satırlardır. Program çalıştırılınca 6 değil 5 yazar:

5
Tablo 9.7: artir(sayi) çağrısının izleme tablosu. x değişir, sayi değişmez.
AdımSatırÇağrı yığını (üstteki önce)Değişkenler (üstteki çerçeve)Çıktı
15main()sayi=5—
26main()sayi=5—
31artir(5) ← main()x=5—
42artir(5) ← main()x=6—
53artir(5) ← main()x=6—
67main()sayi=55

1. adımda main'in çerçevesinde sayi = 5 olur. 2. adımda artir(sayi) çağrılır. Gönderilen şey sayi değişkeni değil, onun değeri olan 5'tir. 3. adımda yeni çerçevede ayrı bir kutu açılır: x = 5. Bu bir kopyadır. 4. adımda x++ çalışır ve x = 6 olur. Değişen yalnızca artir'ın kutusudur; main'deki sayi hâlâ 5'tir. 5. adımda metot biter, çerçeve ve içindeki x (6) silinir. artir bir void metot olduğu için 6 değeri hiçbir yere gönderilmez ve tamamen kaybolur. 6. adımda sayi'nin değeri olan 5 yazılır.

main()sayi5artirx5kopya main()sayi5artirx6
Şekil 9.10: (a) Çağrı anı: artir'ın çerçevesinde x = 5, sayi'nin kopyası. (b) x++ sonrası: x = 6, ama main'deki sayi hâlâ 5.

9.6.3swap tuzağı

İki değişkenin değerlerini yer değiştirmek, yani takas etmek, programlamada sık yapılan bir iştir. Takas bir geçici değişkenle üç adımda yapılır. Bu üç adımı bir metoda koymak akla yatkın görünür:

static void swap(int a, int b) {
    int t = a;
    a = b;
    b = t;
}
// main içinde:
int x = 3, y = 5;
swap(x, y);
System.out.println(x + " " + y);   // 3 5

Yorum satırında da görüldüğü gibi program 5 3 değil 3 5 yazar. Metodun içindeki takas doğrudur; ama takas edilen, x ve y'nin kendisi değil kopyalarıdır. Tablo 9.8 durumu adım adım gösterir. Satır numaraları: 1–5 swap, 7–9 main'deki satırlardır.

Tablo 9.8: swap(x, y) çağrısının izleme tablosu. a ve b takas edilir, x ve y değişmez.
AdımSatırÇağrı yığını (üstteki önce)Değişkenler (üstteki çerçeve)Çıktı
17main()x=3, y=5—
28main()x=3, y=5—
31swap(3, 5) ← main()a=3, b=5—
42swap(3, 5) ← main()a=3, b=5, t=3—
53swap(3, 5) ← main()a=5, b=5, t=3—
64swap(3, 5) ← main()a=5, b=3, t=3—
79main()x=3, y=53 5

Çağrı anında a = 3 ve b = 5 olur; bunlar x ve y'nin kopyalarıdır ve onlarla hiçbir bağları yoktur. int t = a; geçici kutuya 3'ü koyar. a = b; ile a = 5 olur; a'nın eski değeri artık yalnızca t'de saklıdır. b = t; ile b = 3 olur. Bu noktada swap'in kendi kutuları gerçekten yer değiştirmiştir: a = 5, b = 3. Ancak metot bitince a, b ve t'nin hepsi silinir. main'deki x ve y'ye hiç dokunulmamıştır: x = 3, y = 5.

main()x3y5swap(x, y)swapa5b3t3
Şekil 9.11: swap'in son satırından sonra: swap'in çerçevesinde a = 5, b = 3, t = 3; alttaki main çerçevesinde ise x = 3, y = 5. Takas yalnızca kopyalarda olmuştur.

Bunu metodun içine birkaç yazdırma satırı ekleyerek de görmek mümkündür. Aşağıdaki sürüm, metodun başında ve sonunda kendi değişkenlerini, dönüşten sonra da main'in değişkenlerini yazar:

public class SwapIz {
    static void swap(int a, int b) {
        System.out.println("swap başı: a=" + a + " b=" + b);
        int t = a;
        a = b;
        b = t;
        System.out.println("swap sonu: a=" + a + " b=" + b + " t=" + t);
    }

    public static void main(String[] args) {
        int x = 3, y = 5;
        swap(x, y);
        System.out.println("main: x=" + x + " y=" + y);
    }
}
swap başı: a=3 b=5
swap sonu: a=5 b=3 t=3
main: x=3 y=5

Çıktının ikinci satırı takasın metodun içinde başarılı olduğunu, üçüncü satırı ise bunun dışarıya hiçbir etkisi olmadığını gösterir. Java'da iki ilkel değişkeni takas eden bir metot yazılamaz. Takas ya çağıran kodun içinde, geçici bir değişkenle yapılır ya da değerler bir dizi içinde tutulur ve dizinin öğeleri takas edilir.

Sık yapılan hata

Parametreyle argümanın adı aynı olunca metodun çağıranın değişkenini değiştireceğini sanmak. static void artir(int sayi) { sayi++; } yazılsa ve main'de artir(sayi); çağrılsa da sonuç değişmez. Parametrenin adı sayi olsa bile o, artir'ın çerçevesindeki ayrı bir kutudur. Adların aynı olması iki değişkeni birbirine bağlamaz.

9.6.4Sonucu çağırana iletmek

Bir metot, çağıranın bir değişkenini değiştirmek istiyorsa yeni değeri döndürür; çağıran da bu değeri kendi değişkenine atar. Düzeltilmiş sürüm şudur:

public class ArtirDondur {
    static int artir(int x) {
        return x + 1;
    }

    public static void main(String[] args) {
        int sayi = 5;
        sayi = artir(sayi);
        System.out.println(sayi);   // 6
    }
}
6

Burada da değer ile aktarım geçerlidir: x, sayi'nin kopyasıdır ve main'deki sayi'yi değiştiren metot değildir. Değişikliği main'in kendisi, sayi = artir(sayi); atamasıyla yapar. Bu yazım ne olduğunu açıkça gösterir: okuyan, sayi'nin bu satırda değiştiğini atamadan anlar. Bir metodun çağıranın değişkenlerini gizlice değiştirememesi bir kısıt gibi görünse de programları anlaşılır kılan bir güvencedir. hesapla(x) gibi bir çağrının x'i bozmayacağı, metodun gövdesine bakmadan bilinir.

9.7Aşırı yükleme

Karenin alanı tek bir kenarla, dikdörtgenin alanı iki kenarla hesaplanır. İki işe kareAlani ve dikdortgenAlani gibi ayrı adlar verilebilir. Ama ikisi de aslında “alan” hesaplar. Java, aynı işi farklı girdilerle yapan metotlara aynı adı vermeye izin verir.

9.7.1Aynı ad, farklı parametre listesi

Tanım 9.8

Aşırı yükleme (method overloading), aynı sınıfta aynı adlı birden çok metot tanımlamaktır. Bu metotların parametre listeleri farklı olmalıdır: parametre sayısı, parametre tipleri ya da tiplerin sırası. Kısacası imzaları farklı olmalıdır. Derleyici bir çağrıyı, argümanların sayısına ve tiplerine bakarak bu metotlardan birine bağlar.

static int alan(int kenar) {
    return kenar * kenar;
}
static int alan(int en, int boy) {
    return en * boy;
}
// HATA: yalnız dönüş tipi farklı
static double alan(int kenar) { … }

İlk iki metot geçerli bir aşırı yüklemedir. alan(int kenar) bir parametre, alan(int en, int boy) iki parametre alır; imzaları alan(int) ve alan(int, int)'dir. Üçüncü metot ise hatalıdır: parametre listesi birinciyle aynıdır, yalnızca dönüş tipi farklıdır.

public class Alan {
    static int alan(int kenar) {
        return kenar * kenar;
    }

    static int alan(int en, int boy) {
        return en * boy;
    }

    public static void main(String[] args) {
        System.out.println(alan(4));
        System.out.println(alan(3, 5));
    }
}
16
15
alan(4)1 int argümanalan(int kenar)16alan(3, 5)2 int argümanalan(int en, int boy)15Derleyici argümanların sayı ve tipine bakar; dönüş tipine bakmaz.
Şekil 9.12: alan(4)'te tek int argüman vardır ve tek parametreli sürüm seçilir (16). alan(3, 5)'te iki int argüman vardır ve iki parametreli sürüm seçilir (15). Derleyici argümanların sayısına ve tipine bakar; dönüş tipine bakmaz.

Aşırı yüklemenin en tanıdık örneği System.out.println'dir. println(5), println(2.5), println("Merhaba") ve println(true) çağrılarının her biri ayrı bir metoda gider: println(int), println(double), println(String), println(boolean). Programcı her tip için ayrı bir ad ezberlemek zorunda kalmaz. Math.abs ve Math.max da int, long, float ve double için aşırı yüklenmiştir.

9.7.2Derleyici hangi metodu seçer?

Aşırı yüklenmiş metotlar arasında seçim program çalışmadan önce, derleme sırasında yapılır. Derleyici çağrıdaki her argümanın tipini bilir ve bu tiplere uyan imzayı arar. Aşağıdaki program bu kararı iki çağrı için gösterir:

static int alan(int kenar) {
    return kenar * kenar;
}
static int alan(int en, int boy) {
    return en * boy;
}
System.out.println(alan(4));      // main
System.out.println(alan(3, 5));   // main
alan(4)çağrı (argümanlar)alan(int)✓ eşleşiralan(int, int)✗ 2 parametre isterderleyicinin gördüğü imzalar alan(3, 5)çağrı (argümanlar)alan(int)✗ 1 parametre isteralan(int, int)✓ eşleşirderleyicinin gördüğü imzalar
Şekil 9.13: Derleyicinin seçimi. (a) alan(4): alan(int) eşleşir; alan(int, int) iki parametre ister. (b) alan(3, 5): alan(int) bir parametre ister; alan(int, int) eşleşir.

alan(4) çağrısında tek bir int argüman vardır. alan(int) sayı ve tip bakımından tam uyar; alan(int, int) iki argüman istediği için elenir. Çalışma sırasında alan(int) çağrılır, kenar = 4 olur ve 4 × 4 = 16 döner. alan(3, 5) çağrısında iki int argüman vardır ve bu kez yalnızca alan(int, int) uyar: en = 3, boy = 5 ve 3 × 5 = 15 döner.

Argüman tipi hiçbir parametreye tam uymadığında derleyici genişletme (widening conversion) dönüşümlerini dener. Bir tip, bilgi kaybetmeden daha geniş bir tipe dönüştürülebilir: char → int → long → float → double. Derleyici önce tam eşleşen bir imza arar. Bulamazsa argümanı genişleterek uyan imzalara bakar ve en yakın genişlemeyi gerektireni seçer. Daraltma ise hiçbir zaman kendiliğinden yapılmaz. Aşağıdaki programda alan'a bir de double sürüm eklenmiştir:

public class AlanDouble {
    static int alan(int kenar) {
        return kenar * kenar;
    }

    static int alan(int en, int boy) {
        return en * boy;
    }

    static double alan(double kenar) {
        return kenar * kenar;
    }

    public static void main(String[] args) {
        System.out.println(alan(4));      // alan(int)
        System.out.println(alan(2.5));    // alan(double)
        System.out.println(alan(3, 5));   // alan(int, int)
        System.out.println(alan(4L));     // long → double: alan(double)
        System.out.println(alan('B'));    // char → int: alan(int)
    }
}
16
6.25
15
16.0
4356
Tablo 9.9: AlanDouble programındaki çağrılar için derleyicinin seçimi.
ÇağrıArgüman tipleriSeçilen metotNeden?Sonuç
alan(4)intalan(int)tam eşleşme16
alan(2.5)doublealan(double)tam eşleşme6.25
alan(3, 5)int, intalan(int, int)tam eşleşme15
alan(4L)longalan(double)long, int'e daraltılamaz; double'a genişler16.0
alan('B')charalan(int)char → int en yakın genişleme; 'B' = 664356

Son iki satır dikkat ister. 4L, long tipinde bir sabittir. alan(int)'e verilemez, çünkü long'dan int'e geçiş bir daraltmadır. long, double'a genişleyebildiği için alan(double) seçilir ve sonuç 16.0 olarak, ondalık noktasıyla yazılır. 'B' ise bir char'dır. Hem int'e hem double'a genişleyebilir; derleyici daha yakın olan int'i seçer. 'B' karakterinin kodu 66'dır ve 66 × 66 = 4356 yazılır. Seçim tamamen derleme sırasında yapılır; program çalışırken argümanın değerine bakılmaz.

9.7.3Hatalı aşırı yükleme

Yukarıdaki üçüncü metot, static double alan(int kenar), birinci metotla aynı imzayı taşır: alan(int). Yalnızca dönüş tipi farklıdır. Derleyici bunu kabul etmez:

public class AlanTekrar {
    static int alan(int kenar) {
        return kenar * kenar;
    }
    static double alan(int kenar) {
        return kenar * kenar;
    }
}
AlanTekrar.java:5: error: method alan(int) is already defined in class AlanTekrar
    static double alan(int kenar) {
                  ^
1 error

İleti “alan(int) metodu bu sınıfta zaten tanımlı” der. Dönüş tipinin imzaya dahil olmamasının nedeni burada açıkça görülür. alan(4); gibi sonucu kullanılmayan bir çağrıyla karşılaşan derleyici, hangi sürümün kastedildiğini bilemezdi. System.out.println(alan(4)); çağrısında da durum aynıdır, çünkü println hem int hem double kabul eder. Çağrının biçimi yalnızca ad ve argümanlardan oluşur; seçim de yalnızca bunlara dayanabilir.

Uyan hiçbir imza yoksa derleme yine başarısız olur. alan(2.5) çağrısı, yalnızca alan(int) ve alan(int, int) tanımlıyken derlenmez:

public class AlanHata {
    static int alan(int kenar) {
        return kenar * kenar;
    }
    static int alan(int en, int boy) {
        return en * boy;
    }
    public static void main(String[] args) {
        System.out.println(alan(2.5));
    }
}
AlanHata.java:9: error: incompatible types: possible lossy conversion from double to int
        System.out.println(alan(2.5));
                                ^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

JDK 21 bu durumda kısaltılmış bir ileti verir. Tek parametreli tek aday alan(int) olduğu için, sorunu doğrudan double'dan int'e daraltma olarak bildirir. İletinin altındaki not, ayrıntılı çıktının -Xdiags:verbose seçeneğiyle alınabileceğini söyler. Bu seçenekle derleyici “alan(double) için uygun metot bulunamadı” (no suitable method found for alan(double)) der ve iki adayın neden uymadığını ayrı ayrı sıralar: birinde kayıplı dönüşüm, ötekinde argüman sayısı farkı.

Üçüncü bir hata türü de belirsizliktir. İki ayrı imza da genişletmeyle eşit derecede uyuyorsa derleyici bir seçim yapamaz:

public class BelirsizCagri {
    static void f(int a, double b) { }
    static void f(double a, int b) { }
    public static void main(String[] args) {
        f(1, 2);
    }
}
BelirsizCagri.java:5: error: reference to f is ambiguous
        f(1, 2);
        ^
  both method f(int,double) in BelirsizCagri and method f(double,int) in BelirsizCagri match
1 error

f(1, 2) çağrısında iki argüman da int'tir. f(int, double) için ikinci argümanın, f(double, int) için birinci argümanın genişletilmesi gerekir. İki seçenek de birer genişletme ister ve hiçbiri ötekinden daha iyi değildir. Derleyici “f'ye başvuru belirsiz” der ve iki adayı da gösterir. Argümanlardan biri açıkça double yazılırsa belirsizlik kalkar:

public class Belirsiz {
    static void f(int a, double b) {
        System.out.println("f(int, double)");
    }

    static void f(double a, int b) {
        System.out.println("f(double, int)");
    }

    public static void main(String[] args) {
        f(1, 2.0);
        f(1.0, 2);
    }
}
f(int, double)
f(double, int)

9.8Değişken sayıda argüman

İki sayıyı toplayan bir metot, üç sayıyı toplayan bir başkası, dört sayıyı toplayan bir üçüncüsü… Aşırı yüklemeyle bu yol sonsuza kadar uzar. Java, bir metodun istenen sayıda argüman almasına izin veren bir sözdizimi sunar.

9.8.1int... sözdizimi ve kuralları

Tanım 9.9

Değişken sayıda argüman (varargs, variable-length arguments), parametre tipinden sonra üç nokta yazılarak bildirilen ve sıfır ya da daha fazla argüman kabul eden parametredir: int... s. Derleyici çağrıdaki değerleri bir diziye paketler; metodun içinde s bir int[] dizisidir.

static int topla(int... s) {
    int t = 0;
    for (int v : s) t += v;
    return t;
}
topla(1, 2, 3)   // 6
topla()          // 0
topla(new int[]{4, 5})   // 9

Metodun içinde s sıradan bir dizi gibi kullanılır. s.length kaç argüman geldiğini verir. for (int v : s) biçimindeki gelişmiş for döngüsü dizinin öğelerini sırayla v'ye koyar; her turda bir öğe işlenir. Tam program ve çıktısı:

public class Varargs {
    static int topla(int... s) {
        int t = 0;
        for (int v : s) t += v;
        return t;
    }

    public static void main(String[] args) {
        System.out.println(topla(1, 2, 3));
        System.out.println(topla());
        System.out.println(topla(new int[]{4, 5}));
        System.out.println(topla(10));
    }
}
6
0
9
10

topla(1, 2, 3) için derleyici new int[]{1, 2, 3} dizisini kendisi oluşturur ve metoda bu diziyi geçirir. topla() çağrısında hiç argüman yoktur; derleyici bu kez boş bir dizi geçirir ve sonuç 0'dır. Üçüncü çağrıda dizi doğrudan verilmiştir: varargs parametresi bir dizi de kabul eder, sonuç 4 + 5 = 9'dur. Tek argümanlı topla(10) da geçerlidir ve 10 döner.

Varargs parametresinin iki kuralı vardır. Parametre listesinin en sonunda bulunmalıdır, çünkü derleyici çağrıdaki değerlerden hangilerinin diziye gireceğini ancak böyle anlayabilir. Bir metotta da yalnızca bir tane bulunabilir. İki kural da aynı derleyici iletisiyle denetlenir:

public class VarargsSon {
    static int topla(int... s, int x) {
        return x;
    }
}
VarargsSon.java:2: error: varargs parameter must be the last parameter
    static int topla(int... s, int x) {
                            ^
1 error

İki varargs parametresi yazıldığında da (int... s, int... t) aynı ileti çıkar, çünkü birincisi en sonda değildir. Varargs'tan önce sıradan parametreler bulunabilir. Aşağıdaki metot önce bir başlık, ardından istenen sayıda not alır:

public class Etiket {
    static void yaz(String baslik, int... notlar) {
        System.out.print(baslik + ":");
        for (int n : notlar) System.out.print(" " + n);
        System.out.println(" (" + notlar.length + " not)");
    }

    public static void main(String[] args) {
        yaz("Ali", 70, 85, 90);
        yaz("Ayşe", 100);
        yaz("Can");
        System.out.printf("%d + %d = %d%n", 2, 3, 2 + 3);
    }
}
Ali: 70 85 90 (3 not)
Ayşe: 100 (1 not)
Can: (0 not)
2 + 3 = 5

“Can” için hiç not verilmemiştir; dizi boştur, döngü dönmez ve notlar.length 0'dır. Programın son satırındaki printf, Java kütüphanesinde varargs kullanan bir metottur: imzası printf(String, Object...)'tır. Biçim dizgisinden sonra istenen sayıda değer alabilmesi bundandır.

9.8.2topla(1, 2, 3) adım adım

Aşağıdaki main'de topla iki kez çağrılır:

static int topla(int... s) {
    int t = 0;
    for (int v : s) t += v;
    return t;
}
// main içinde:
System.out.println(topla(1, 2, 3));
System.out.println(topla());

İlk çağrının izlemesi, metoda her turda değişkenleri yazdıran satırlar eklenmiş bir sürüm çalıştırılarak elde edildi. Tablo 9.10 bu çıktıdan oluşturulmuştur.

Tablo 9.10: topla(1, 2, 3) çağrısında gelişmiş for döngüsünün turları.
Turvİşlemt
giriş—s = {1, 2, 3}, s.length = 30
1. tur10 + 1 = 11
2. tur21 + 2 = 33
3. tur33 + 3 = 66

Çağrı anında derleyicinin hazırladığı dizi s parametresine geçer: s = {1, 2, 3} ve s.length = 3. int t = 0; toplam için yerel bir kutu açar. Gelişmiş for, 1. turda v = 1 yapar ve t = 0 + 1 = 1 olur. 2. turda v = 2 ve t = 1 + 2 = 3, 3. turda v = 3 ve t = 3 + 3 = 6 olur. Dizide başka öğe kalmadığı için döngü biter. return t; 6'yı döndürür. v değişkeni döngünün içinde tanımlandığı için döngüyle birlikte kapsam dışına çıkmıştır. topla'nın çerçevesi (s ve t) silinir ve main 6'yı yazar.

main()topla(1, 2, 3)toplas123t6v3
Şekil 9.14: 3. turun sonu: topla'nın çerçevesinde s dizisi {1, 2, 3}, t = 6 ve v = 3.

9.8.3Boş çağrı

İkinci çağrı topla()'dır ve hiç argüman içermez. Derleyici null değil, boş bir dizi (new int[0]) geçirir; s.length = 0'dır. t = 0 atanır. Dizide hiç öğe olmadığı için gelişmiş for'un gövdesi bir kez bile çalışmaz ve t 0 kalır. Metot 0 döndürür, program 0 yazar.

main()topla()toplas[ ] boşt0
Şekil 9.15: topla() çağrısında s boş bir dizidir. Döngü gövdesi hiç çalışmaz, t = 0 kalır.

Boş çağrının bir hata olmadığına ve sonucun 0 çıktığına dikkat edilmelidir. 0 toplamanın etkisiz elemanıdır: bir sayıya 0 eklemek onu değiştirmez. Hiç sayı yokken toplamın 0 olması bu yüzden matematikte de doğal kabul edilir. Aynı mantıkla çarpım hesaplayan bir varargs metodunda biriktirici 1 ile başlar ve boş çağrı 1 döndürür. Buna karşılık “en büyük öğe” ya da “ortalama” gibi hesapların boş girdi için anlamlı bir cevabı yoktur. Böyle metotlarda boş dizi başta ayrıca ele alınmalıdır. Örneğin tam sayı ortalamasında 0 / 0 bölmesi program çalışırken ArithmeticException hatasına yol açar.

9.9Kapsam

Bir değişken bildirildiği her yerden kullanılamaz. Java her değişkene, programın belirli bir bölgesinde görünme hakkı verir. Bu bölgenin sınırlarını küme parantezleri çizer.

9.9.1Yerel değişken ve kapsam

Tanım 9.10

Kapsam (scope), bir değişkenin adıyla kullanılabildiği program bölgesidir. Bir metodun içinde bildirilen değişkenlere ve metodun parametrelerine yerel değişken (local variable) denir. Yerel değişkenin kapsamı, bildirildiği satırdan başlar ve bildirildiği bloğun kapanan küme parantezinde biter. Parametrelerin kapsamı metodun bütün gövdesidir.

Kapsamın dört kuralı vardır:

  • Parametreler ve metot içinde bildirilen değişkenler yereldir.
  • Bir blokta, yani bir çift küme parantezi { } arasında bildirilen değişken yalnızca o blokta ve onun içindeki bloklarda görünür.
  • Aynı kapsamda aynı adlı iki yerel değişken bildirilemez.
  • Bir metot, başka bir metodun yerel değişkenine erişemez.
hesapla(int n) { … } — metot kapsamın3sonuc0if (n > 0) { … } — blok kapsamıkatki6
Şekil 9.16: İç içe kapsamlar. Dıştaki kutu hesapla'nın metot kapsamıdır: n ve sonuc burada yaşar. İçteki kesikli kutu if bloğunun kapsamıdır: katki yalnızca burada vardır.

Son kural, çağrı yığınıyla birlikte düşünülünce kendiliğinden anlaşılır. Bir metodun yerel değişkenleri o metodun çerçevesinde durur. Başka bir metot ya o çerçeve henüz yokken ya da başka bir çerçevenin içinde çalışırken o değişkene ulaşamaz. Derleyici bu erişimi baştan reddeder:

public class BaskaMetot {
    static void birinci() {
        int gizli = 42;
    }
    static void ikinci() {
        System.out.println(gizli);
    }
}
BaskaMetot.java:6: error: cannot find symbol
        System.out.println(gizli);
                           ^
  symbol:   variable gizli
  location: class BaskaMetot
1 error

İleti “sembol bulunamadı” der ve aranan sembolün gizli adlı bir değişken olduğunu belirtir. ikinci metodunun içinde bu adla bilinen hiçbir şey yoktur. Değeri birinci'den ikinci'ye taşımanın yolu, birinci'nin değeri döndürmesi ve çağıranın bu değeri ikinci'ye argüman olarak vermesidir.

9.9.2hesapla(3) adım adım

hesapla metodu iki iç içe kapsam içerir: metodun kendi kapsamı ve if bloğunun kapsamı.

static int hesapla(int n) {
    int sonuc = 0;
    if (n > 0) {
        int katki = n * 2;
        sonuc += katki;
    }
    return sonuc;
}
System.out.println(hesapla(3));  // main
Tablo 9.11: hesapla(3) çağrısının izleme tablosu. katki yalnızca 5. ve 6. adımlarda vardır.
AdımSatırÇağrı yığını (üstteki önce)Değişkenler (üstteki çerçeve)Çıktı
19main()——
21hesapla(3) ← main()n=3—
32hesapla(3) ← main()n=3, sonuc=0—
43hesapla(3) ← main()n=3, sonuc=0—
54hesapla(3) ← main()n=3, sonuc=0, katki=6—
65hesapla(3) ← main()n=3, sonuc=6, katki=6—
76hesapla(3) ← main()n=3, sonuc=6—
87hesapla(3) ← main()n=3, sonuc=6—
99main()—6

main, hesapla(3)'ü çağırır ve metot kapsamı açılır: n = 3. 2. satırda sonuc bildirilir. Metodun en dış bloğunda bildirildiği için metodun sonuna kadar görünür. 3. satırda 3 > 0 doğrudur ve if bloğuna girilir; burada yeni bir iç kapsam açılır. 4. satırda katki bildirilir ve 3 × 2 = 6 değerini alır. Bu değişken yalnızca if bloğunun içinde vardır. 5. satırda iç bloktan dış kapsamdaki sonuc'a erişilir: sonuc = 0 + 6 = 6. İç blok dıştaki değişkenleri görür; tersi doğru değildir. 6. satırda blok kapanır ve katki yok olur. 7. satırda return sonuc; 6'yı döndürür ve metot kapsamı da kapanır. Dönen 6, println'e argüman olur ve yazılır.

hesapla(int n) { … } — metot kapsamın3sonuc6if (n > 0) { … } — blok kapsamıkatki6 hesapla(int n) { … } — metot kapsamın3sonuc6if (n > 0) { … } — blok kapsamıkatki → blok bitti, değişken yok
Şekil 9.17: (a) 5. satırdan sonra: iç blokta katki = 6, dış kapsamda sonuc = 6. (b) Blok kapandıktan sonra: katki artık yok; n ve sonuc duruyor.

Metodu negatif bir sayıyla çağırmak if bloğunu hiç açmaz: hesapla(-2) için katki hiç oluşmaz ve 0 döner.

public class Hesapla {
    static int hesapla(int n) {
        int sonuc = 0;
        if (n > 0) {
            int katki = n * 2;
            sonuc += katki;
        }
        return sonuc;
    }

    public static void main(String[] args) {
        System.out.println(hesapla(3));
        System.out.println(hesapla(-2));
    }
}
6
0

Blok kapandıktan sonra katki'yi kullanmaya çalışmak derleme hatasıdır. Aşağıdaki sürüm, return satırında katki'yi de toplamaya çalışıyor:

public class KatkiDisarida {
    static int hesapla(int n) {
        int sonuc = 0;
        if (n > 0) {
            int katki = n * 2;
            sonuc += katki;
        }
        return sonuc + katki;
    }
}
KatkiDisarida.java:8: error: cannot find symbol
        return sonuc + katki;
                       ^
  symbol:   variable katki
  location: class KatkiDisarida
1 error

Derleyici 8. satırdaki katki'yi tanımaz. Bu satır if bloğunun dışındadır ve orada bu adla bir değişken yoktur. Derleyicinin bakış açısından durum, değişkenin hiç bildirilmemiş olmasından farksızdır. Değişkenin bloktan sonra da gerekmesi durumunda bildirimi bloktan önceye, dış kapsama taşınır.

9.9.3Aynı adlı yerel değişkenler

İç blok, dış kapsamdaki değişkenleri gördüğüne göre iç blokta aynı adla yeni bir değişken bildirmek ne olur? Java'da bu bir hatadır:

public class AyniAd {
    public static void main(String[] args) {
        int x = 1;
        if (x > 0) {
            int x = 5;   // hata
        }
    }
}
AyniAd.java:5: error: variable x is already defined in method main(String[])
            int x = 5;   // hata
                ^
1 error
main metodu kapsamıx1dış x: iç blokta da görünürif (x > 0) { … } bloğuint x = 5;✗ x zaten tanımlı
Şekil 9.18: main'in kapsamındaki x = 1, iç if bloğunda da görünür. İç blokta int x = 5; bildirmek hatadır: x zaten tanımlıdır.

İleti “x değişkeni main(String[]) metodunda zaten tanımlı” der. Bazı dillerde iç bloktaki yeni değişken dıştakini geçici olarak gizler. Java bunu yerel değişkenler arasında bilerek yasaklar: iki x aynı anda görünseydi okuyan, bir satırdaki x'in hangisi olduğunu karıştırabilirdi.

Parametreler de yerel değişkendir ve metodun en dış kapsamında bulunur. Bu yüzden metot gövdesinde parametreyle aynı adlı bir değişken bildirilemez:

public class ParametreAyniAd {
    static int topla(int a, int b) {
        int a = 3;
        return a + b;
    }
}
ParametreAyniAd.java:3: error: variable a is already defined in method topla(int,int)
        int a = 3;
            ^
1 error

İletide metodun imzası parametre tipleriyle birlikte görünür: topla(int,int). Parametreye yeni bir değer atamak (a = 3;) ise geçerlidir; hata yalnızca tipin yeniden yazılmasından, yani yeniden bildirimden doğar.

Aynı ad, kapsamı bitmiş bir değişkenden sonra ise yeniden kullanılabilir. Blok kapanınca içindeki değişken silinir ve ad serbest kalır:

public class BlokSonrasi {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            System.out.print(i + " ");
        }
        int i = 7;                 // önceki i'nin kapsamı bitti
        System.out.println(i);
        {
            int k = 1;
            System.out.println(k);
        }
        {
            int k = 2;             // ayrı blok, ayrı k
            System.out.println(k);
        }
    }
}
0 1 2 7
1
2

for başlığında bildirilen i'nin kapsamı döngüyle sınırlıdır. Döngü bitince int i = 7; yazılabilir. İki ayrı blokta bildirilen iki k de birbirini görmez; her biri kendi bloğu bitince yok olur. Aynı adlı iki değişkenin kapsamları üst üste binmiyorsa çakışma da yoktur.

9.9.4Gölgeleme: static alan ve yerel değişken

Değişkenler yalnızca metotların içinde bildirilmez. Bir değişken sınıfın içinde ama bütün metotların dışında da bildirilebilir. Böyle bir değişkene alan (field) denir. static bir alan sınıfa aittir, sınıftaki bütün metotlar onu görür ve program boyunca yaşar. Yerel değişkenlerin aksine bir çağrının sonunda silinmez.

Tanım 9.11

Gölgeleme (shadowing), bir metodun içinde, sınıftaki bir alanla aynı adlı yerel değişken bildirilmesidir. Yerel değişkenin kapsamı içinde bu ad yerel değişkeni gösterir; alan gölgede kalır ve değişmez. Alana yine de SinifAdi.alanAdi yazımıyla ulaşılabilir.

public class AlanGolge {
    static int sayac = 100;          // static alan

    static void dene() {
        int sayac = 1;               // yerel değişken alanı gölgeler
        sayac += 5;
        System.out.println("yerel sayac = " + sayac);
        System.out.println("alan sayac  = " + AlanGolge.sayac);
    }

    public static void main(String[] args) {
        dene();
        System.out.println("main: " + sayac);
    }
}
yerel sayac = 6
alan sayac  = 100
main: 100

dene metodunda int sayac = 1; yazıldığı anda yerel bir sayac doğar. Bundan sonra metodun içinde sayac adı bu yerel değişkeni gösterir. sayac += 5; yerel değişkeni 6 yapar. Alanın değeri 100'de kalır; AlanGolge.sayac yazımı alanın kendisine ulaşır ve 100 yazılır. dene bitince yerel sayac silinir ve main'deki sayac adı yine alanı gösterir: 100.

Gölgelemenin derlenmesi, yerel değişkenler arasındaki yasakla çelişmez. Yasak, iki yerel değişkenin aynı adla aynı anda görünmesini önler. Alan ile yerel değişken ise farklı türlerdendir ve alan, sınıf adıyla her zaman ayırt edilebilir. Yine de gölgeleme çoğu zaman bir dikkatsizlik sonucudur. Programcı alanı değiştirdiğini sanırken yerel bir kopyayı değiştirir ve hata uzun süre fark edilmez. Alanlara ve yerel değişkenlere farklı adlar vermek en güvenli yoldur.

9.9.5Yerel değişkenin ömrü ve ilk değeri

Kapsam bir değişkenin nerede görünür olduğunu söyler. Bir de değişkenin ne zaman var olduğu sorusu vardır. Yerel değişken, bildirim satırı çalışınca doğar ve bloğu bitince ölür. Bir metot her çağrıldığında yerel değişkenleri yeniden doğar. Önceki çağrıdan hiçbir değer hatırlanmaz. yazdirPozitif'in ikinci çağrısındaki sayi'nin birinci çağrıdaki −3'ten habersiz olması bundandır.

Bir çağrıdan ötekine değer taşıması gereken bir sayaç bu yüzden yerel olamaz; static bir alan olmalıdır. Örneğin bir metodun kaç kez çağrıldığını sayan bir sayaç, metodun içinde int sayac = 0; diye bildirilirse her çağrıda 0'dan başlar ve 1'den öteye geçemez.

Alanlarla yerel değişkenler arasında bir fark daha vardır. Bir static int alan, değer verilmezse kendiliğinden 0 ile başlar. Yerel değişkenin ise kendiliğinden bir ilk değeri yoktur. Derleyici, değer verilmemiş bir yerel değişkenin okunmasına izin vermez:

public class IlkDegerYok {
    static int toplam(int n) {
        int t;
        for (int i = 1; i <= n; i++) t += i;
        return t;
    }
}
IlkDegerYok.java:4: error: variable t might not have been initialized
        for (int i = 1; i <= n; i++) t += i;
                                     ^
IlkDegerYok.java:5: error: variable t might not have been initialized
        return t;
               ^
2 errors

İleti “t değişkenine bir ilk değer verilmemiş olabilir” der. Hata iki satırda bildirilir: t += i t'nin eski değerini okur, return t de onu okur. Çözüm biriktiriciyi int t = 0; ile başlatmaktır. Biriktirici toplama için 0, çarpma için 1 ile başlar.

9.10Hazır metotlar: Math

Java'nın kütüphanesi binlerce hazır metot içerir. Bunların en sık kullanılanlarından biri Math sınıfıdır. Math'in metotları static'tir; nesne oluşturmadan sınıf adıyla çağrılır: Math.sqrt(16). Bunlar da bu notta yazılan metotlar gibi parametre alır, değer döndürür ve birçoğu aşırı yüklenmiştir.

Tablo 9.12: Sık kullanılan Math metotları (sonuçlar JDK 21 çıktısıdır).
ÇağrıSonuçDönüş tipiNot
Math.abs(-7)7intmutlak değer; int, long, float, double için aşırı yüklü
Math.abs(-7.5)7.5doubleargüman double olunca double sürüm seçilir
Math.max(3, 9)9intbüyük olanı döndürür
Math.max(3, 9.5)9.5double3 double'a genişler, max(double, double) seçilir
Math.min(-4, 2)-4intküçük olanı döndürür
Math.sqrt(16)4.0doublekarekök; sonuç her zaman double
Math.sqrt(-1)NaNdoublenegatif sayının gerçel karekökü yoktur: “sayı değil”
Math.pow(2, 10)1024.0doubleüs alma; int'e atamak için (int) gerekir
Math.round(2.5)3longen yakın tam sayıya yuvarlar
Math.round(2.4)2longaşağı yuvarlanır
Math.round(-2.5)-2longtam yarıda büyük olana, yani −2'ye yuvarlar
public class MathOrnek {
    public static void main(String[] args) {
        System.out.println(Math.abs(-7));
        System.out.println(Math.max(3, 9));
        System.out.println(Math.sqrt(16));
        System.out.println(Math.pow(2, 10));
        System.out.println(Math.round(2.5));
        System.out.println(Math.sqrt(-1));
        System.out.println(Math.abs(-7.5));
        System.out.println(Math.max(3, 9.5));
        System.out.println(Math.round(-2.5));
        System.out.println(Math.round(2.4));
        int p = (int) Math.pow(2, 10);
        System.out.println(p);
        System.out.println(Math.abs(Integer.MIN_VALUE));
        System.out.println(Math.min(-4, 2));
    }
}
7
9
4.0
1024.0
3
NaN
7.5
9.5
-2
2
1024
-2147483648
-4

Çıktıdaki 4.0 ve 1024.0 sayılarının ondalık noktası, dönüş tipinin double olmasından gelir. Math.sqrt(16)'nın sonucu matematikte tam sayı olsa da metot her zaman double döndürür. Bir metodu kullanırken sonucun değeri kadar tipi de önemlidir. Dönüş tipi double ya da long olan bir metodun sonucu bir int değişkene doğrudan atanamaz:

public class PowInt {
    public static void main(String[] args) {
        int p = Math.pow(2, 10);
    }
}
PowInt.java:3: error: incompatible types: possible lossy conversion from double to int
        int p = Math.pow(2, 10);
                        ^
1 error
public class RoundInt {
    public static void main(String[] args) {
        int r = Math.round(2.5);
    }
}
RoundInt.java:3: error: incompatible types: possible lossy conversion from long to int
        int r = Math.round(2.5);
                          ^
1 error

İki hata da daraltma hatasıdır. Math.pow double, Math.round(double) ise long döndürür. Programcı sonucun int'e sığacağından eminse açık dönüşüm yazar: int p = (int) Math.pow(2, 10); derlenir ve 1024 olur.

Math.round(-2.5)'in −3 değil −2 vermesi şaşırtıcı gelebilir. Math.round, sayıya 0,5 ekleyip sonucun tabanını alır gibi davranır: −2,5 + 0,5 = −2. Tam yarıdaki sayılar hep büyük olana, yani artı sonsuza doğru yuvarlanır: 2,5 → 3 ve −2,5 → −2. Yine Math.abs(Integer.MIN_VALUE) çağrısının sonucu da negatiftir (−2147483648). int'in en küçük değerinin mutlak değeri olan 2147483648, int'in en büyük değerinden (2147483647) bir fazladır ve int'e sığmaz. Basamak sayısını bulan metot bu yüzden sayıyı önce long'a çevirir.

Hazır bir metodu kullanmadan önce belgesinde parametreleri, dönüş tipi ve özel durumlar okunmalıdır. Math.sqrt(-1)'in bir hata fırlatmak yerine NaN döndürmesi ya da Math.round'un long döndürmesi, ancak belgeye bakılınca öğrenilir.

9.11Metot tasarımı

Derlenen ve doğru sonuç veren bir metot her zaman iyi bir metot değildir. Yüz satırlık, beş iş birden yapan, sonucu hem ekrana yazıp hem bir alana kaydeden bir metot çalışabilir, ama okunması, denenmesi ve değiştirilmesi zordur. İyi bir metodu kötüsünden ayıran, nasıl bölündüğü ve nasıl adlandırıldığıdır.

9.11.1Soyutlama

Tanım 9.12

Soyutlama (abstraction), bir işin ayrıntılarını gizleyip yalnızca o işin ne yaptığını gösteren bir ad ve arayüz sunmaktır. Bir metodun soyutlaması, imzası ve sözleşmesidir: ne aldığı, ne döndürdüğü ve hangi koşullarda doğru çalıştığı.

çağıran kodMath.sqrt(16)görünen: imza + sözleşmedouble sqrt(double x)x ≥ 0 için √xgizli: gövde? ? ?değişebilir
Şekil 9.19: Soyutlama. Çağıran kod yalnızca Math.sqrt(16) yazar. Görünen kısım imza (double sqrt(double x)) ve sözleşmedir (x ≥ 0 için √x). Gövde gizlidir ve değişebilir.

Metot, çağıran koddan uygulama ayrıntısını saklar. İyi adlandırılmış bir metot ne yapıldığını söyler; nasıl yapıldığı gövdede kalır. Math.sqrt'ü kullanan bir öğrenci, karekökün Newton yöntemiyle mi yoksa işlemcinin özel bir komutuyla mı hesaplandığını bilmez ve bilmesi de gerekmez.

Soyutlamanın önemli bir kazancı vardır: gövde değişse bile imza aynı kaldıkça çağıranlar etkilenmez. Asal sayı metodu önce 2'den n − 1'e kadar bütün bölenleri deneyen yavaş bir döngüyle yazılmış olsun. Sonra yalnızca √n'e kadar bakan hızlı sürümle değiştirilsin. asalMi(n)'i çağıran yüz satırın hiçbiri değişmez; hepsi kendiliğinden hızlanır.

9.11.2Problemi parçalama

Büyük bir görev, her biri tek bir sorumluluğu olan alt görevlere bölünür. Her alt görev için girdinin, çıktının ve hatalı girdide ne olacağının belirlenmesi gerekir. Üst düzey metot bu alt görevleri anlamlı bir sırada birleştirir. Böyle yazılmış bir üst düzey metot, okunurken bir içindekiler tablosu gibidir. Bu yaklaşıma parçalama (decomposition) denir.

notRaporu()notlariOku()girdiortalamaHesapla()hesap, safharfNotu(ort)kararraporYaz(…)çıktı
Şekil 9.20: Not raporu görevinin parçalanması. notRaporu() dört alt metodu çağırır: notları okuyan (girdi), ortalamayı hesaplayan (hesap, saf), harf notunu belirleyen (karar) ve raporu yazan (çıktı).

Aşağıdaki program, şekildeki parçalamayı küçük bir örnekle uygular. Okuma işi kısa olduğu için ayrı bir metoda alınmamış, notRaporu'nun ilk üç satırına yazılmıştır. Harf notu sınırları örnek olarak seçilmiştir.

import java.util.Scanner;

public class NotRaporu {
    static double ortalamaHesapla(int a, int b, int c) {
        return (a + b + c) / 3.0;
    }

    static String harfNotu(double ort) {
        if (ort >= 85) return "AA";
        if (ort >= 70) return "BB";
        if (ort >= 60) return "CC";
        if (ort >= 50) return "DD";
        return "FF";
    }

    static void raporYaz(double ort, String harf) {
        System.out.println("Ortalama: " + Math.round(ort * 100) / 100.0);
        System.out.println("Harf notu: " + harf);
    }

    static void notRaporu(Scanner giris) {
        int n1 = giris.nextInt();               // notları oku
        int n2 = giris.nextInt();
        int n3 = giris.nextInt();
        double ort = ortalamaHesapla(n1, n2, n3);
        raporYaz(ort, harfNotu(ort));
    }

    public static void main(String[] args) {
        notRaporu(new Scanner(System.in));
    }
}

Üç farklı girdiyle çıktılar:

Tablo 9.13: NotRaporu programının üç girdi için çıktısı.
GirdiOrtalama (hesap)Çıktı
70 85 91246 / 3 = 82
Ortalama: 82.0
Harf notu: BB
50 60 71181 / 3 ≈ 60,33
Ortalama: 60.33
Harf notu: CC
40 45 52137 / 3 ≈ 45,67
Ortalama: 45.67
Harf notu: FF

Her metodun tek bir işi vardır ve her biri ayrı ayrı denenebilir. ortalamaHesapla yalnızca hesap yapar. Bölmenin 3.0 ile yapıldığına dikkat edilmelidir: (a + b + c) / 3 yazılsaydı iki int arasında tam sayı bölmesi yapılır, 181 / 3 için 60,33 yerine 60 bulunurdu. Dönüş tipinin double olması bu bölmeyi kendiliğinden kesirli yapmaz; dönüşüm bölme bittikten sonra olur. harfNotu yalnızca bir karar verir; art arda erken dönüşlerle yazıldığı için her eşik bir satırdır. raporYaz yalnızca çıktıyla ilgilenir. Ortalamayı iki basamağa yuvarlamak için Math.round(ort * 100) / 100.0 kullanılır: 60,333… × 100 = 6033,3… → 6033 → 60,33. Harf notu sınırları değişirse yalnızca harfNotu, raporun biçimi değişirse yalnızca raporYaz değişir.

9.11.3İyi metot tasarımının ilkeleri

Bir metodun iyi tasarlanıp tasarlanmadığı birkaç soruyla sınanabilir:

Tablo 9.14: İyi metot tasarımının ilkeleri ve her ilkenin sorusu.
İlkeSoruKötü örnek → iyi örnek
Tek görevMetot tek cümleyle açıklanabiliyor mu?hem okuyan hem hesaplayan hem yazan isle() → notlariOku, ortalamaHesapla, raporYaz
Anlamlı adAd sonucu ya da eylemi söylüyor mu?f(x), islem2() → basamakSayisi(x), ciftMi(x)
Az parametreGirdilerin hepsi gerekli mi?kullanılmayan bir int mod parametresi → parametreyi kaldırmak
Belirgin sonuçDönüş ve yan etkiler açık mı?hem yazan hem döndüren metot → yalnızca döndüren metot
Test edilebilirlikMetot bağımsız örneklerle denenebilir mi?klavyeden okuyan hesap metodu → değeri parametre olarak alan metot

Bu ilkeler birbirini destekler. Tek görevli bir metoda anlamlı bir ad vermek kolaydır. Az parametreli ve sonucu döndüren bir metot kolay denenir. Uzunluk için kesin bir sınır yoktur. Yine de bir metot bir ekran yüksekliğini aşıyorsa bölünmeye adaydır. Gövdesinde “şimdi şunu yap” diye ayrılan bölümler varsa, bu bölümlerin her biri büyük olasılıkla ayrı bir metot olmalıdır.

9.12Örnek metotlar

Aşağıdaki örnekler, metotlarla ilgili kavramları küçük ve tanıdık problemler üzerinde bir araya getirir. Her metot önce yazılır, sonra çağrı yığınıyla ya da bir izleme tablosuyla adım adım yürütülür.

9.12.1Önce sözleşme

Bir metot yazılmadan önce ne yapacağı açıkça belirlenmelidir. Metodun adı yaptığı işi söylemeli, parametrelerin geçerli değer aralığı tanımlanmalı, dönüş değerinin anlamı ve hatalı girdide ne olacağı bilinmelidir. Bu bilgilerin toplamına metodun sözleşmesi denir.

Tanım 9.13

Sözleşme (contract), bir metodun çağıran ile arasındaki anlaşmadır. Ön koşul, çağıranın sağlaması gereken koşuldur (ör. “n ≥ 0”). Son koşul, ön koşul sağlandığında metodun garanti ettiği sonuçtur (ör. “n'nin basamak sayısını döndürür”).

// Görev : sayının onluk basamak sayısını döndürür.
// Girdi : herhangi bir int (negatif de olabilir).
// Çıktı : 1..10; 0 için 1, -507 için 3.
static int basamakSayisi(int sayi) { … }
girdi (ön koşul)herhangi intnegatif de olabilirbasamakSayisiçıktı (son koşul)1 … 10döndürür, ekrana yazmazörnek:0 → 1-507 → 32147483647 → 10-2147483648 → 10
Şekil 9.21: basamakSayisi'nın sözleşmesi. Ön koşul: herhangi bir int, negatif de olabilir. Son koşul: 1 ile 10 arasında bir sayı döndürür, ekrana yazmaz. Örnekler: 0 → 1, −507 → 3, 2147483647 → 10, −2147483648 → 10.

Sözleşmedeki örnekler, metot yazılmadan önce belirlenmiş birer test durumudur. Sınır değerleri bilerek içerir: 0 tek basamaklıdır ama döngüsü hiç dönmeyecek bir girdidir; int'in en büyük ve en küçük değerleri ise en uzun sayılardır. Gövde yazıldıktan sonra bu örneklerin hepsi denenir. Sözleşme yazılmadan başlanan bir metotta “0 için ne dönmeli?” sorusu çoğu zaman ancak bir hata çıktığında sorulur.

9.12.2Tek mi, çift mi?

static boolean ciftMi(int sayi) {
    return sayi % 2 == 0;
}

Bir sayı 2'ye bölündüğünde kalan 0 ise sayı çifttir. ciftMi bu karşılaştırmanın sonucunu, yani bir boolean değeri doğrudan döndürür. if (sayi % 2 == 0) return true; else return false; biçiminde yazmak da doğrudur, ama gereksiz uzundur: sayi % 2 == 0 ifadesi zaten true ya da false'tur.

sayisayi % 2ciftMi(sayi)71false100true-40true-3-1falseJava’da negatif sayının kalanı negatif olabilir (-3 % 2 = -1); bu yüzden == 0 ile bakılır.
Şekil 9.22: ciftMi dört girdi için: 7 % 2 = 1 → false; 10 % 2 = 0 → true; −4 % 2 = 0 → true; −3 % 2 = −1 → false.

Şekildeki son sütun önemli bir ayrıntıyı gösterir. Java'da negatif bir sayının kalanı negatif olabilir: kalanın işareti bölünenin işaretini izler ve −3 % 2 = −1'dir. Tek sayı denetimini sayi % 2 == 1 diye yazmak bu yüzden yanlıştır; −3 için false verir ve −3'ü çift sayar. == 0 ile çiftliğe bakmak her işarette doğru çalışır. Programın gerçek çıktısı, her satırda sayıyı, kalanı ve metodun sonucunu verir:

public class CiftMi {
    static boolean ciftMi(int sayi) {
        return sayi % 2 == 0;
    }

    public static void main(String[] args) {
        if (ciftMi(7)) System.out.println("çift");
        else System.out.println("tek");
        int[] denenecek = {7, 10, -4, -3, 0};
        for (int s : denenecek)
            System.out.println(s + " " + (s % 2) + " " + ciftMi(s));
    }
}
tek
7 1 false
10 0 true
-4 0 true
-3 -1 false
0 0 true

Metot ekrana yazmaz, sonucu döndürür. Bu yüzden if koşulunda doğrudan kullanılır. ciftMi(7) çağrısı izlendiğinde, çerçevede sayi = 7 olur, 7 % 2 = 1 ve 1 == 0 yanlış olduğundan false döner. Dönüş değeri çağrının yerine geçer: if (false). Then dalı atlanır, else dalı “tek” yazar. Ne yazılacağına metot değil, çağıran karar vermiştir. Aynı ciftMi, başka bir programda çift sayıları saymak ya da bir listeyi süzmek için değiştirilmeden kullanılabilir.

9.12.3Basamak sayısı

static int basamakSayisi(int sayi) {
    long kalan = Math.abs((long) sayi);
    if (kalan == 0) return 1;
    int adet = 0;
    while (kalan > 0) {
        adet++;
        kalan /= 10;
    }
    return adet;
}
System.out.println(basamakSayisi(-507));

Metot önce sayının mutlak değerini alır, çünkü −507 ile 507'nin basamak sayısı aynıdır. Ardından 0 için erken dönüş yapar. Sonra sayı 10'a bölündükçe bir basamak eksilir; sayı 0 olana kadar kaç kez bölündüğü sayılır. Sonuç ekrana yazılmaz, döndürülür; çağıran onu istediği gibi kullanır.

Tablo 9.15 basamakSayisi(-507) çağrısında döngünün turlarını verir. Döngüden önce kalan = |−507| = 507 olur; 507 == 0 yanlış olduğu için erken dönüş yapılmaz ve adet = 0 ile döngüye girilir.

Tablo 9.15: basamakSayisi(-507) çağrısında while döngüsünün turları. kalan /= 10 tam sayı bölmesidir.
TurKoşul kalan > 0adetkalan
1. tur507 > 0 → doğru1507 / 10 = 50
2. tur50 > 0 → doğru250 / 10 = 5
3. tur5 > 0 → doğru35 / 10 = 0
çıkış0 > 0 → yanlış3—

Her turda kalan /= 10 sayının son basamağını atar: 507 → 50 → 5 → 0. Döngü üç kez döner ve 3 döndürülür. main'deki println'in argümanı için önce bu çağrı yapılmış, dönen 3 çağrının yerine geçmiş ve println(3) çalışmıştır.

main()println(basamakSayisi(-507))basamakSayisisayi-507kalan507 main()println(basamakSayisi(-507))basamakSayisisayi-507kalan0adet3kapandıreturn 3
Şekil 9.23: (a) Math.abs'ten sonra: sayi = −507, kalan = 507. (b) Döngü bitti: kalan = 0, adet = 3; return 3 ile çerçeve kapanıyor.

Sözleşmedeki bütün örnekler gerçekten denenmiştir:

public class BasamakSayisi {
    static int basamakSayisi(int sayi) {
        long kalan = Math.abs((long) sayi);
        if (kalan == 0) return 1;
        int adet = 0;
        while (kalan > 0) {
            adet++;
            kalan /= 10;
        }
        return adet;
    }

    public static void main(String[] args) {
        System.out.println(basamakSayisi(-507));
        System.out.println(basamakSayisi(0));
        System.out.println(basamakSayisi(9));
        System.out.println(basamakSayisi(10));
        System.out.println(basamakSayisi(2147483647));
        System.out.println(basamakSayisi(-2147483648));
    }
}
3
1
1
2
10
10

Birinci satırdaki (long) dönüşümünün de bir nedeni vardır. int'in en küçük değeri olan −2147483648'in mutlak değeri int'e sığmaz ve Math.abs bu sayı için yine −2147483648 döndürür. Dönüşüm yapılmadan yazılmış bir sürüm bu yüzden yanlış sonuç verir:

public class BasamakHatali {
    static int basamakSayisi(int sayi) {
        int kalan = Math.abs(sayi);      // long'a çevrilmeden
        if (kalan == 0) return 1;
        int adet = 0;
        while (kalan > 0) {
            adet++;
            kalan /= 10;
        }
        return adet;
    }

    public static void main(String[] args) {
        System.out.println(Math.abs(-2147483648));
        System.out.println(basamakSayisi(-2147483648));
        System.out.println(basamakSayisi(-507));
    }
}
-2147483648
0
3

Hatalı sürümde kalan negatif kalır. 0'a eşit olmadığı için erken dönüş yapılmaz. kalan > 0 koşulu ilk denemede yanlış olduğu için döngü hiç dönmez ve 0 döner. Oysa sözleşme “1 ile 10 arası” diyordu. Doğru sürüm sayıyı önce long'a çevirir. long'da 2147483648 rahatça temsil edilir ve Math.abs'in long sürümü doğru sonucu verir. Bu hata, yalnızca sınır değerle denendiğinde ortaya çıkan bir hatadır ve sözleşmeye sınır değerleri yazmanın değerini gösterir.

9.12.4Asal mı?

1'den büyük olup kendisinden ve 1'den başka böleni olmayan sayıya asal sayı denir. Bir sayının asal olup olmadığını anlamak için olası bölenler sırayla denenir. Bir bölen bulunduğu anda cevap kesinleşir ve metot erken dönüş yapar.

static boolean asalMi(int n) {
    if (n < 2) return false;
    for (int i = 2; i <= n / i; i++)
        if (n % i == 0) return false;
    return true;
}

Döngü koşulundaki i <= n / i ifadesi ilk bakışta tuhaf görünebilir. Bu koşul, i'nin n'nin kareköküne kadar denenmesini sağlar. Gerekçe şudur: n iki sayının çarpımı olarak yazılabiliyorsa, yani n = p × q ise, p ile q'dan en az biri √n'den büyük olamaz. İkisi de √n'den büyük olsaydı çarpımları n'yi aşardı. Bu yüzden n'nin √n'den büyük bir böleni varsa, onun √n'den küçük bir eşi de vardır ve o eş daha önce denenmiş olur. Örneğin 91 = 7 × 13'tür ve √91 ≈ 9,54'tür. 13'e kadar beklemeye gerek yoktur; 7 denendiğinde bölen bulunur.

i <= n / i koşulu i * i <= n koşulunun tam sayı bölmesiyle yazılmış biçimidir. Bu biçim, n çok büyük olduğunda i * i çarpımının int sınırını aşmasını önler. Tablo 9.16 koşulun 37 için nasıl işlediğini gösterir. Tablonun değerleri, döngüye yazdırma satırı eklenmiş bir sürüm JDK 21 ile çalıştırılarak elde edilmiştir.

Tablo 9.16: asalMi(37): yalnızca i ≤ n / i olan i değerleri denenir (√37 ≈ 6,08).
in / iKoşul i <= n / in % iBölüyor mu?
237 / 2 = 182 ≤ 18 → doğru37 % 2 = 1hayır
337 / 3 = 123 ≤ 12 → doğru37 % 3 = 1hayır
437 / 4 = 94 ≤ 9 → doğru37 % 4 = 1hayır
537 / 5 = 75 ≤ 7 → doğru37 % 5 = 2hayır
637 / 6 = 66 ≤ 6 → doğru37 % 6 = 1hayır
737 / 7 = 57 ≤ 5 → yanlış—döngü biter: return true

i = 2'den 6'ya kadar beş değer denenir ve hiçbiri 37'yi bölmez. i = 7'de 37 / 7 = 5'tir ve 7 ≤ 5 yanlıştır; döngü biter ve metot true döndürür. 2'den 36'ya kadar bütün sayıları deneyen basit bir sürüm 35 kalan hesaplardı; bu sürüm yalnızca 5 hesaplar. Sonuç aynıdır. Asal olmayan bir sayıda ise döngü ilk bölende biter. 49 = 7 × 7 için i = 7'de 7 ≤ 49 / 7 = 7 doğrudur ve 49 % 7 = 0 bulunur:

Tablo 9.17: asalMi(49): koşul i = 7 için hâlâ doğrudur, çünkü 7 × 7 = 49'dur.
in / iKoşul i <= n / in % iBölüyor mu?
249 / 2 = 242 ≤ 24 → doğru49 % 2 = 1hayır
349 / 3 = 163 ≤ 16 → doğru49 % 3 = 1hayır
449 / 4 = 124 ≤ 12 → doğru49 % 4 = 1hayır
549 / 5 = 95 ≤ 9 → doğru49 % 5 = 4hayır
649 / 6 = 86 ≤ 8 → doğru49 % 6 = 1hayır
749 / 7 = 77 ≤ 7 → doğru49 % 7 = 0evet: return false

Bu örnek koşulda neden < değil <= kullanıldığını gösterir. i < n / i yazılsaydı 7 hiç denenmez ve 49 asal sanılırdı. Tam kare sayılar bu tür sınır hatalarını yakalamak için iyi test girdileridir.

Metot 15 ve 7 için izlenebilir. asalMi(15)'te n = 15 için önce 15 < 2 yanlıştır. i = 2 için 2 ≤ 15 / 2 = 7 doğrudur ama 15 % 2 = 1'dir; i artar. i = 3 için 3 ≤ 15 / 3 = 5 doğrudur ve 15 % 3 = 0'dır: return false çalışır, kalan turlar hiç çalışmaz. asalMi(7)'de i = 2 için 2 ≤ 7 / 2 = 3 doğrudur, 7 % 2 = 1'dir. i = 3 için 3 ≤ 7 / 3 = 2 yanlıştır: √7'ye kadar bölen bulunmamıştır, döngü erken dönüş olmadan biter ve true döner.

public class AsalMi {
    static boolean asalMi(int n) {
        if (n < 2) return false;
        for (int i = 2; i <= n / i; i++)
            if (n % i == 0) return false;
        return true;
    }

    public static void main(String[] args) {
        System.out.println(asalMi(15));
        System.out.println(asalMi(7));
        System.out.println(asalMi(37));
        System.out.println(asalMi(1) + " " + asalMi(2) + " " + asalMi(4) + " " + asalMi(49) + " " + asalMi(97));
        for (int k = 1; k <= 30; k++)
            if (asalMi(k)) System.out.print(k + " ");
        System.out.println();
    }
}
false
true
true
false true false false true
2 3 5 7 11 13 17 19 23 29 

Son satır, metodun 1'den 30'a kadar sayılarla bir döngüde çağrılmasıyla elde edilen asal sayılardır. asalMi(1)'in false, asalMi(2)'nin true döndüğüne dikkat edilmelidir. 1 asal değildir ve ilk satırdaki erken dönüşle elenir. 2 için döngü koşulu 2 ≤ 2 / 2 = 1 baştan yanlıştır; döngü hiç dönmez ve true döner.

9.12.5EBOB: Öklid algoritması

İki sayının en büyük ortak böleni (EBOB), ikisini de kalansız bölen en büyük sayıdır. 1'den başlayıp küçük sayıya kadar bütün sayıları denemek yerine, yaklaşık 2300 yıllık Öklid algoritması çok daha hızlı bir yol sunar: ebob(a, b) = ebob(b, a % b) ve b = 0 olunca cevap a'dır.

static int ebob(int a, int b) {
    while (b != 0) {
        int r = a % b;
        a = b;
        b = r;
    }
    return a;
}
System.out.println(ebob(48, 18));  // main

Algoritmanın doğruluğu şu gözleme dayanır: a'yı ve b'yi bölen her sayı, a % b = a − k × b farkını da böler. Tersine, b'yi ve a % b'yi bölen her sayı a'yı da böler. Bu yüzden (a, b) çiftinin ortak bölenleri ile (b, a % b) çiftinin ortak bölenleri aynıdır ve EBOB'ları da eşittir. Sayılar her turda küçülür. Sonunda b = 0 olur ve ebob(a, 0) = a'dır, çünkü 0'ı her sayı böler.

Tablo 9.18: ebob(48, 18)'in turları (değerler JDK 21 ile çalıştırılan izleme sürümünden alınmıştır).
Turr = a % babKoşul b != 0
başlangıç—481818 != 0 → doğru
1. tur48 % 18 = 12181212 != 0 → doğru
2. tur18 % 12 = 61266 != 0 → doğru
3. tur12 % 6 = 0600 != 0 → yanlış

Başlangıçta a = 48, b = 18 ve 18 != 0 doğrudur. 1. turda r = 48 % 18 = 12 (48 = 2 × 18 + 12); a = 18, b = 12 olur. Bu, ebob(48, 18) = ebob(18, 12) demektir. 2. turda r = 18 % 12 = 6; a = 12, b = 6. 3. turda r = 12 % 6 = 0; a = 6, b = 0. Koşul 0 != 0 yanlıştır ve 6 döndürülür. Denetim: 48 = 6 × 8, 18 = 6 × 3 ve 8 ile 3'ün 1'den başka ortak böleni yoktur. Yalnızca üç tur yetmiştir.

main()println(ebob(48, 18))eboba18b12r12 main()println(ebob(48, 18))eboba6b0kapandıreturn 6
Şekil 9.24: (a) 1. turdan sonra ebob'un çerçevesi: a = 18, b = 12, r = 12. (b) Döngü bitti: a = 6, b = 0; return 6.

Sayılar ters sırayla verilirse ilk tur onları kendiliğinden yer değiştirir: 18 % 48 = 18 olduğu için ebob(18, 48) ilk turdan sonra ebob(48, 18) durumuna gelir. Metot ayrıca 0 ile de doğru çalışır: ebob(7, 0) döngüye hiç girmez ve 7 döndürür.

public class Ebob {
    static int ebob(int a, int b) {
        while (b != 0) {
            int r = a % b;
            a = b;
            b = r;
        }
        return a;
    }

    static int ekok(int a, int b) {
        return a / ebob(a, b) * b;
    }

    public static void main(String[] args) {
        System.out.println(ebob(48, 18));
        System.out.println(ekok(12, 18));
        System.out.println(ebob(18, 48) + " " + ebob(17, 5) + " " + ebob(7, 0) + " " + ebob(100, 75));
        System.out.println(ekok(4, 6) + " " + ekok(5, 7) + " " + ekok(8, 12));
    }
}
6
36
6 1 7 25
12 35 24

9.12.6EKOK: metot içinden metot çağrısı

İki sayının en küçük ortak katı (EKOK) EBOB'la kolayca bulunur: ekok(a, b) = a × b / ebob(a, b). EBOB'u hesaplayan metot zaten yazılmış olduğu için ekok metodu onu çağırır ve işi tek satırda biter:

static int ekok(int a, int b) {
    return a / ebob(a, b) * b;
}
// ebob: Öklid algoritmasıyla EBOB
System.out.println(ekok(12, 18));  // main
Tablo 9.19: ekok(12, 18) çağrısının izleme tablosu. 3. ve 4. adımlarda yığında üç çerçeve vardır.
AdımSatırÇağrı yığını (üstteki önce)Değişkenler (üstteki çerçeve)Çıktı
15main()——
21ekok(12, 18) ← main()a=12, b=18—
32ebob(12, 18) ← ekok(12, 18) ← main()a=12, b=18—
42ebob(12, 18) ← ekok(12, 18) ← main()a=6, b=0—
52ekok(12, 18) ← main()a=12, b=18—
62ekok(12, 18) ← main()a=12, b=18—
75main()—36

main, ekok(12, 18)'i çağırır ve ekok'un çerçevesi açılır: a = 12, b = 18. return satırındaki ifadeyi hesaplamak için önce ebob(a, b) çağrılmalıdır. Yığında üç çerçeve olur. ebob kendi döngüsünü çalıştırır: (12, 18) → (18, 12) → (12, 6) → (6, 0) ve 6 döndürür. Adları aynı olsa da ebob'un a ve b'si, ekok'un a ve b'sinden ayrı kutulardır. ebob'un a'sı 6'ya inerken ekok'un a'sı 12'de kalır. Çağrının yerine 6 geçer ve ifade hesaplanır: 12 / 6 × 18 = 2 × 18 = 36. ekok 36'yı döndürür ve 36 yazılır.

main()println(ekok(12, 18))ekoka12b18a / ebob(a, b) * beboba6b0kapandıreturn 6
Şekil 9.25: ebob, ekok'un üstünde: ebob'un a = 6, b = 0 olmuştur ve 6 döndürmek üzeredir; alttaki ekok'un a = 12, b = 18 değişmemiştir.

Kodda formülün a * b / ebob(a, b) yerine a / ebob(a, b) * b biçiminde yazılmasının bir nedeni vardır. Önce bölmek, ara sonucun büyümesini önler. a × b çarpımı int'e sığmayacak kadar büyük olabilir. Oysa a, EBOB'a her zaman kalansız bölünür ve bölüm küçüktür. İki yazımın farkı büyük sayılarda ortaya çıkar:

public class EkokTasma {
    static int ebob(int a, int b) {
        while (b != 0) {
            int r = a % b;
            a = b;
            b = r;
        }
        return a;
    }

    static int ekokOnceCarp(int a, int b) {
        return a * b / ebob(a, b);
    }

    static int ekok(int a, int b) {
        return a / ebob(a, b) * b;
    }

    public static void main(String[] args) {
        System.out.println(ekokOnceCarp(12, 18) + " " + ekok(12, 18));
        System.out.println(ekokOnceCarp(100000, 300000) + " " + ekok(100000, 300000));
    }
}
36 36
-647 300000

12 ve 18 için iki yazım da 36 verir. 100000 ve 300000 için ise 100000 × 300000 = 30.000.000.000 çarpımı int'in üst sınırını (yaklaşık 2,1 milyar) aşar. Taşan çarpım anlamsız bir negatif sayıya dönüşür ve sonuç −647 çıkar. Önce bölen yazım 100000 / 100000 × 300000 = 300000 ile doğru cevabı verir. Bu hatanın sinsi yanı, derleyicinin de çalışma zamanının da hiçbir uyarı vermemesidir.

Metot içinden metot çağırmak kod tekrarını önler. EBOB'u hesaplayan döngü ekok'un içine bir daha yazılmamıştır. ebob'da bir hata bulunup düzeltilirse ekok da kendiliğinden düzelir.

9.12.7Yeniden kullanım: enBuyuk3

Üç sayının en büyüğünü bulmak için yeni bir karşılaştırma zinciri yazmak gerekmez. İki sayının büyüğünü veren metot iki kez kullanılır: önce ilk iki sayının büyüğü bulunur, sonra bu sonuç üçüncü sayıyla karşılaştırılır.

static int enBuyuk(int a, int b) {
    if (a > b) return a;
    return b;
}
static int enBuyuk3(int a, int b, int c) {
    return enBuyuk(enBuyuk(a, b), c);
}
System.out.println(enBuyuk3(4, 9, 7));  // main
Tablo 9.20: enBuyuk3(4, 9, 7) çağrısının izleme tablosu. enBuyuk iki kez, iki ayrı çerçeveyle çağrılır.
AdımSatırÇağrı yığını (üstteki önce)Değişkenler (üstteki çerçeve)Çıktı
18main()——
25enBuyuk3(4, 9, 7) ← main()a=4, b=9, c=7—
36enBuyuk3(4, 9, 7) ← main()a=4, b=9, c=7—
41, 2enBuyuk(4, 9) ← enBuyuk3(4, 9, 7) ← main()a=4, b=9—
53enBuyuk(4, 9) ← enBuyuk3(4, 9, 7) ← main()a=4, b=9—
66enBuyuk3(4, 9, 7) ← main()a=4, b=9, c=7—
72enBuyuk(9, 7) ← enBuyuk3(4, 9, 7) ← main()a=9, b=7—
86enBuyuk3(4, 9, 7) ← main()a=4, b=9, c=7—
98main()—9

enBuyuk3'ün çerçevesinde a = 4, b = 9, c = 7 olur. return enBuyuk(enBuyuk(a, b), c); satırında iç içe çağrı vardır ve önce içteki hesaplanır: enBuyuk(4, 9). Yeni çerçevede a = 4, b = 9 olur. 4 > 9 yanlış olduğu için ilk return atlanır ve return b; 9'u döndürür. İfade sadeleşmiştir: enBuyuk(9, 7). Aynı metot ikinci kez, yepyeni bir çerçeveyle çağrılır: a = 9, b = 7. 9 > 7 doğrudur ve return a; 9'u döndürür. enBuyuk3 de bu 9'u döndürür ve 9 yazılır.

main()println(enBuyuk3(4, 9, 7))enBuyuk3a4b9c7enBuyuk(enBuyuk(4, 9), 7)enBuyuka4b9 main()println(enBuyuk3(4, 9, 7))enBuyuk3a4b9c7enBuyuk(9, 7)enBuyuka9b7kapandıreturn 9
Şekil 9.26: (a) İçteki çağrı: enBuyuk(4, 9)'un çerçevesi enBuyuk3'ün üstünde. (b) Dıştaki çağrı: enBuyuk(9, 7) 9 döndürüyor.

Bu tasarımda karşılaştırma mantığı tek bir yerdedir. Bir hata varsa, örneğin > yerine yanlışlıkla < yazılmışsa, tek bir yerde düzeltilir. Sayıların hangi sırada verildiği sonucu değiştirmez; eşit ve negatif sayılarla da doğru çalışır:

public class EnBuyuk3 {
    static int enBuyuk(int a, int b) {
        if (a > b) return a;
        return b;
    }

    static int enBuyuk3(int a, int b, int c) {
        return enBuyuk(enBuyuk(a, b), c);
    }

    public static void main(String[] args) {
        System.out.println(enBuyuk3(4, 9, 7));
        System.out.println(enBuyuk3(9, 4, 7) + " " + enBuyuk3(4, 7, 9) + " " + enBuyuk3(5, 5, 5) + " " + enBuyuk3(-2, -7, -1));
    }
}
9
9 9 5 -1

9.12.8double döndüren metotlar: sıcaklık dönüşümü

Celsius ile Fahrenheit arasındaki dönüşüm F = C × 9 / 5 + 32 ve C = (F − 32) × 5 / 9 formülleriyle yapılır. Sonuçlar kesirli olabileceği için iki metot da double alır ve double döndürür.

static double fahrenheit(double c) {
    return c * 9 / 5 + 32;
}
static double celsius(double f) {
    return (f - 32) * 5 / 9;
}
double f = fahrenheit(25);          // main
System.out.println(f);
System.out.println(celsius(f));
Tablo 9.21: fahrenheit(25) ve celsius(f) çağrılarının izleme tablosu.
AdımSatırÇağrı yığını (üstteki önce)Değişkenler (üstteki çerçeve)Çıktı
17main()——
21fahrenheit(25.0) ← main()c=25.0—
32fahrenheit(25.0) ← main()c=25.0—
47main()f=77.0—
58main()f=77.077.0
69celsius(77.0) ← main()f=77.077.0
75celsius(77.0) ← main()f=77.077.0
89main()f=77.077.0 / 25.0

fahrenheit(25) çağrısında argüman int'tir ama parametre double'dır. Genişletme kendiliğinden yapılır ve c = 25.0 olur. Gövdede 25,0 × 9 = 225,0; 225,0 / 5 = 45,0; 45,0 + 32 = 77,0 hesaplanır. main'de f = 77.0 olur ve double olduğu için 77.0 biçiminde, ondalık noktasıyla yazılır. Sonra celsius(f) çağrılır. f'nin değeri, celsius'un kendi f parametresine kopyalanır; adların aynı olması yine bir bağ kurmaz. (77,0 − 32) × 5 / 9 = 45,0 × 5 / 9 = 225,0 / 9 = 25,0 bulunur ve 25.0 yazılır. İki metot birbirinin tersidir. Birinin sonucunu ötekine verip başlangıç değerine dönmek, ikisini birlikte sınamanın kolay bir yoludur.

public class Sicaklik {
    static double fahrenheit(double c) {
        return c * 9 / 5 + 32;
    }

    static double celsius(double f) {
        return (f - 32) * 5 / 9;
    }

    public static void main(String[] args) {
        double f = fahrenheit(25);
        System.out.println(f);
        System.out.println(celsius(f));
        System.out.println(fahrenheit(100) + " " + fahrenheit(-40) + " " + celsius(98.6));
        System.out.println(5 / 9 * (f - 32));
        System.out.println((f - 32) * 5 / 9);
    }
}
77.0
25.0
212.0 -40.0 37.0
0.0
25.0

Üçüncü satır bilinen birkaç değeri doğrular: suyun kaynama noktası 100 °C = 212 °F, iki ölçeğin kesiştiği −40 ve insan vücut sıcaklığı 98,6 °F = 37 °C. Dördüncü satır sık yapılan bir hatayı gösterir. Formül 5 / 9 * (f - 32) diye yazılırsa önce 5 / 9 hesaplanır. İki int arasında tam sayı bölmesi 0 verir ve sonuç ne olursa olsun 0.0 çıkar. Doğru yazımda ise önce (f - 32), yani bir double hesaplanır; sonraki çarpma ve bölme double aritmetiğiyle yapılır ve doğru sonuç (25.0) elde edilir.

9.12.9Saf metot ve yan etki

Tanım 9.14

Yan etki (side effect), bir metodun değer döndürmenin dışında dünyada bıraktığı her değişikliktir: ekrana yazmak, bir alanı değiştirmek, bir dosyaya kayıt eklemek gibi. Saf metot (pure method), aynı girdiler için her zaman aynı sonucu döndüren ve hiçbir yan etkisi olmayan metottur.

İki küçük metot farkı açıkça gösterir:

public class SafYan {
    static int kare(int x) {
        return x * x;
    }

    static void kareYaz(int x) {
        System.out.println(x * x);
    }

    public static void main(String[] args) {
        int t = kare(3) + kare(4);
        System.out.println(t);
        kareYaz(4);
        System.out.println(kare(kare(2)));
    }
}
25
16
16

kare saftır. kare(4) her zaman 16'dır ve her yerde kullanılabilir: bir toplamın içinde (9 + 16 = 25), başka bir çağrının argümanı olarak (kare(kare(2)) = kare(4) = 16) ya da bir koşulda. kareYaz ise yalnızca ekrana yazar. Sonucu başka bir hesapta kullanılamaz. kareYaz(3) + kareYaz(4) derlenmez bile, çünkü kareYaz void'dir.

Saf metotlar daha kolay test edilir. kare(4) == 16 diye denetlemek yeter; ekran çıktısını yakalamaya ya da bir alanın önceki değerini hatırlamaya gerek yoktur. Bu yüzden hesaplama metodu ile ekrana yazma metodu birbirinden ayrılır. Hesap saf bir metotta yapılır, yazma işi ayrı bir metoda ya da main'e bırakılır. Yan etki kaçınılmaz olduğunda, örneğin bir rapor gerçekten yazılacaksa, yan etki açıkça adlandırılır ve sınırlandırılır: raporYaz adı ne yaptığını söyler ve başka hiçbir şey yapmaz.

Bir metodun saf olmasını bozan yollar yalnızca ekrana yazmak değildir. Bir static alanı değiştiren metot da yan etkilidir. Her çağrıda bir sayacı artıran bir metodun sonucu, kaçıncı kez çağrıldığına göre değişebilir. Klavyeden okuyan ya da rastgele sayı üreten metotlar da saf değildir, çünkü aynı girdiyle çağrıldıklarında farklı sonuçlar verebilir.

9.13Özet

  • Metot, bir sınıfın içinde tanımlanan, adı olan ve bir görevi yapan deyimler bütünüdür. Girdisini parametrelerle alır, sonucunu dönüş değeri olarak verir. Metot bir kez tanımlanır, istenen yerden ve istenen sayıda çağrılır.
  • Tanımlamada yazılan biçimsel parametreler değişkendir; çağrıda yazılan gerçek argümanlar değerdir. Argümanlar parametrelere sırayla eşlenir; sayıları ve tipleri uymalıdır.
  • Bir çağrıda argümanlar değerlendirilir, parametrelere kopyalanır, gövde çalışır, return değeri gönderir ve çağıran kod çağrının yerine geçen değerle sürer.
  • Her çağrı çağrı yığınının üstüne kendi çerçevesini açar. Yalnızca en üstteki çerçeve çalışır; metot bitince çerçevesi bütün yerel değişkenleriyle silinir. Son açılan çerçeve ilk kapanır.
  • Başlık erişim belirteci, niteleyici, dönüş tipi, ad ve parametre listesinden oluşur. İmza, ad ile parametre tiplerinin listesidir; dönüş tipi imzaya dahil değildir.
  • main static'tir, çünkü JVM onu hiçbir nesne yokken çağırır. static bir metot, static olmayan bir metodu doğrudan çağıramaz.
  • void metot değer döndürmez; işi bir yan etkidir. void çağrı bir ifadenin parçası olamaz. Erken dönüş özel durumları metodun başında eler.
  • void olmayan bir metotta her yol bir return ile bitmelidir. Döndürülen değer dönüş tipine sığmalıdır; daraltma kendiliğinden yapılmaz.
  • Java argümanları her zaman değer ile aktarır. Metot parametresini değiştirse de çağıranın değişkeni değişmez; bu yüzden ilkel değişkenleri takas eden bir swap metodu yazılamaz. Yeni değer döndürülür ve çağıran onu atar.
  • Aşırı yüklemede aynı adlı metotların parametre listeleri farklıdır. Derleyici seçimi derleme sırasında, önce tam eşleşmeye sonra en yakın genişletmeye bakarak yapar. Yalnızca dönüş tipi farklı iki metot ve eşit derecede uyan iki aday derleme hatasıdır.
  • Değişken sayıda argüman (int... s) metodun içinde bir dizidir. Parametre listesinin en sonunda bulunur; argümansız çağrıda boş bir dizi geçer.
  • Kapsam, bir değişkenin kullanılabildiği bölgedir ve küme parantezleriyle sınırlanır. Aynı kapsamda aynı adlı iki yerel değişken bildirilemez. Alanla aynı adlı yerel değişken alanı gölgeler. Yerel değişken bir ilk değer almadan okunamaz.
  • Math metotları static'tir ve aşırı yüklenmiştir. sqrt ve pow double, round(double) long döndürür.
  • İyi bir metot tek bir görev yapar, anlamlı bir ad taşır, az parametre alır, sonucunu döndürür ve bağımsız örneklerle denenebilir. Soyutlama ne yapıldığını nasıl yapıldığından ayırır; parçalama büyük görevi alt metotlara böler; sözleşme ön ve son koşulları belirler.
  • Metot başka metodu çağırabilir (ekok → ebob, enBuyuk3 → enBuyuk); her çağrı yığında kendi çerçevesini açar. Saf metot aynı girdiye hep aynı sonucu verir ve yan etkisi yoktur; bu yüzden kolay test edilir ve her yerde yeniden kullanılır.

9.14Kavram listesi

Terimlerin İngilizce karşılıkları ve öteki sunumlardaki kullanımları terim sözlüğünde bulunur.

9.15Kendini Dene

  1. static double daireAlani(double r) { return Math.PI * r * r; } metodu main'de double s = daireAlani(2 * yaricap); diye çağrılıyor. Bu satırlarda parametre, argüman, dönüş tipi ve imza nedir?

    Cevap

    Biçimsel parametre r'dir; başlıkta tipiyle (double r) bildirilen değişkendir. Gerçek argüman 2 * yaricap ifadesidir; önce değeri hesaplanır ve bu değer r'ye kopyalanır. Dönüş tipi double'dır. İmza daireAlani(double)'dır; dönüş tipi ve parametre adı imzaya girmez.

  2. Üç metot şöyle tanımlanmış: a() önce “a1 ” yazar, sonra b()'yi çağırır, sonra “a2 ” yazar. b() “b1 ” yazar, c()'yi çağırır, “b2 ” yazar. c() yalnızca “c ” yazar. main, a()'yı çağırıyor. Çıktı nedir? Yığında en çok kaç çerçeve bulunur?

    Cevap

    Çıktı a1 b1 c b2 a2'dir. Her metot, çağırdığı metot bitene kadar bekler ve sonra kaldığı yerden sürer. c çalışırken yığında main, a, b ve c olmak üzere 4 çerçeve vardır. Çerçeveler açıldıkları sıranın tersiyle kapanır: önce c, sonra b, en son a.

  3. static void sifirla(int n) { n = 0; } metodu için int k = 9; sifirla(k); System.out.println(k); ne yazar? k'yi gerçekten sıfırlamak için metot ve çağrı nasıl değiştirilir?

    Cevap

    9 yazar. Java değer ile aktarır: n, k'nin kopyasıdır ve n = 0 yalnızca kopyayı değiştirir. Metot bitince n silinir. Metot bir değer döndürecek biçimde yazılır, static int sifirla() { return 0; }, ve çağıran atamayı kendisi yapar: k = sifirla();. Bu örnekte en sade çözüm ise metot kullanmadan k = 0; yazmaktır.

  4. Bir sınıfta yalnızca static void g(long x) ve static void g(double x) var; ikisi de parametresinin tipini ve değerini yazıyor. g(5), g(5.5f) ve g('x') çağrıları hangi metodu çalıştırır?

    Cevap

    Hiçbir çağrı için tam eşleşme yoktur; derleyici en yakın genişletmeyi seçer. g(5): int → long, double'dan daha yakındır; long 5 yazılır. g(5.5f): float, long'a daraltılamaz ama double'a genişler; double 5.5 yazılır. g('x'): char → int → long genişlemesiyle long sürüm seçilir; 'x' karakterinin kodu 120 olduğu için long 120 yazılır.

  5. static int say(int... s) { return s.length; } için say(), say(5), say(new int[]{1, 2, 3}) ve say(1, 2) ne döndürür? static int say(int... s, int k) başlığı neden derlenmez?

    Cevap

    Sırasıyla 0, 1, 3 ve 2. Argümansız çağrıda boş bir dizi geçer (uzunluğu 0); dizi doğrudan verilirse o dizi kullanılır. İkinci başlıkta varargs parametresi listenin sonunda değildir; derleyici varargs parameter must be the last parameter hatası verir, çünkü çağrıdaki değerlerden hangisinin k'ye gideceğini bilemez.

  6. Aşağıdaki metotta hangi satır derleme hatası verir, neden? static int toplamKare(int n) { for (int i = 1; i <= n; i++) { int kare = i * i; } return kare; }

    Cevap

    return kare; satırı cannot find symbol hatası verir. kare, for döngüsünün gövde bloğunda bildirilmiştir ve blok kapanınca kapsamı biter; döngünün dışında bu adla bir değişken yoktur. Ayrıca metot amacına da ulaşmıyor: kareler biriktirilmiyor. Doğrusu, döngüden önce int toplam = 0; bildirmek, gövdede toplam += i * i; yazmak ve return toplam; ile bitirmektir.

  7. ebob(105, 30) Öklid algoritmasıyla kaç turda biter, ne döndürür? Aynı sayılar için ekok metodu ne döndürür?

    Cevap

    1. tur: 105 % 30 = 15 (105 = 3 × 30 + 15); a = 30, b = 15. 2. tur: 30 % 15 = 0; a = 15, b = 0. Koşul yanlış olur ve 2 turda 15 döner. ekok = 105 / 15 × 30 = 7 × 30 = 210. Denetim: 210 = 2 × 105 = 7 × 30.

  8. asalMi(91) çağrısında döngü kaç tur döner ve ne döner? asalMi(37) kaç tur döner?

    Cevap

    91 = 7 × 13'tür. i = 2, 3, 4, 5, 6 için koşul (i ≤ 91 / i) doğrudur ve kalan 0 değildir. i = 7'de 7 ≤ 91 / 7 = 13 doğrudur ve 91 % 7 = 0'dır: 6. turda false döner. 37 için i = 2…6 denenir (5 tur); i = 7'de 7 ≤ 37 / 7 = 5 yanlıştır ve true döner.

  9. Math.round(3.5), Math.round(-3.5), Math.abs(-0.5) ve Math.max(-1, -8) ne döndürür? int r = Math.round(3.5); neden derlenmez?

    Cevap

    4, −3, 0.5 ve −1. Tam yarıdaki sayılar büyük olana yuvarlanır: 3,5 → 4, −3,5 → −3. Math.abs double argümanla double döndürür. −1, −8'den büyüktür. Math.round(double) long döndürdüğü için sonuç int'e doğrudan atanamaz (possible lossy conversion from long to int); int r = (int) Math.round(3.5); yazılmalıdır.

  10. static int ucKati(int x) { return 3 * x; } için ucKati(ucKati(2)) ve ucKati(2) * ucKati(1) ne verir? Aynı metot sonucu döndürmek yerine System.out.println(3 * x) ile yazsaydı bu iki ifade yazılabilir miydi?

    Cevap

    İlki: içteki çağrı 6 verir, dıştaki ucKati(6) = 18. İkincisi: 6 × 3 = 18. İkisi de 18'dir. Metot yazdıran bir void metot olsaydı iki ifade de derlenmezdi: void bir çağrı ne başka bir çağrıya argüman ne de bir çarpmanın işleneni olabilir. Sonucu döndüren saf metot her türlü ifadede kullanılabilir.

9.16İleri okuma

  • Oracle, The Java Tutorials: “Defining Methods” ve “Passing Information to a Method or a Constructor” sayfaları; metot bildirimi, aşırı yükleme, varargs ve değer ile aktarım kısa örneklerle anlatılır.
  • R. Sedgewick, K. Wayne, Computer Science: An Interdisciplinary Approach (Addison-Wesley, 2016), Kesim 2.1 “Static Methods”: static metotlar, kapsam ve metotlarla modüler programlama, bilimsel hesaplardan örneklerle işlenir.
  • A. B. Downey, C. Mayfield, Think Java (2. baskı, O'Reilly, 2019): metotlar, parametreler ve dönüş değerleri programlamaya yeni başlayanlar için yığın diyagramlarıyla anlatılır; kitabın çevrim içi sürümü ücretsizdir.
  • Oracle, Java Language Specification, Java SE 21, §8.4 “Method Declarations” ve §15.12 “Method Invocation Expressions”: imza, dönüş ve aşırı yükleme seçimini derleyicinin nasıl yaptığının kesin kuralları.

9.17Kaynaklar

  1. P. Deitel, H. Deitel, Java: How to Program, Early Objects, 11. baskı, Pearson, 2018.
  2. Y. D. Liang, Introduction to Java Programming and Data Structures, 13. baskı, Pearson, 2024.
  3. Oracle, The Java Tutorials: Defining Methods; Passing Information to a Method or a Constructor.
  4. Oracle, Java Language Specification, Java SE 21 Edition, Bölüm 8: Classes (§8.4 Method Declarations).
  5. R. Sedgewick, K. Wayne, Computer Science: An Interdisciplinary Approach, Addison-Wesley, 2016: 2.1 Static Methods.
  6. A. B. Downey, C. Mayfield, Think Java, 2. baskı, O'Reilly, 2019.

İlgili sayfalar: Metotlar sunumu · Çalışma soruları · Metot Çağrı Yığını simülatörü · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.