Java ile Nesne Yönelimli Programlama · Ders Notu

Bölüm 3.2: Fonksiyonlar

Program büyüdükçe tek bir main metodunun içine sığmaz. Aynı hesap farklı yerlerde yeniden gerekir, hatalar uzun kod yığınının içinde kaybolur. Fonksiyon bu iki soruna verilen cevaptır: bir işi bir kez yazar, ona bir ad verir ve istendiği kadar çağırır. Java'da fonksiyonlar her zaman bir sınıfın içinde yazılır ve metot adını alır. Not, bir metodun nasıl tanımlandığını ve çağrıldığını, çağrı sırasında bellekte neler olduğunu ve argümanların neden kopyalandığını adım adım gösterir. Aynı adlı metotlar arasında derleyicinin nasıl seçim yaptığı, hazır Java metotları, değişkenlerin kapsamı ve yaşam süresi ayrıca işlenir. Son kesimler iyi bir metodun nasıl tasarlandığını tartışır. Örneklerdeki bütün programlar JDK 21 ile derlenip çalıştırılmıştır; izleme tablolarındaki değerler ve derleyici iletileri bu çalıştırmalardan alınmıştır.

Öğrenme çıktıları

Tanımlamak ve çağırmak
Erişim belirleyici, dönüş tipi, ad ve parametre listesiyle bir metot yazmak; metodu argümanlarla çağırıp dönüş değerini kullanmak.
Çalışmayı izlemek
Çağrı yığınında çerçevelerin nasıl açılıp kapandığını ve argümanların değer olarak kopyalandığını adım adım göstermek.
Aşırı yükleme
Aynı adlı metotları parametre listeleriyle ayırmak; derleyicinin hangi sürümü seçeceğini argüman tiplerinden önceden söylemek.
Hazır metotlar
System.out ve Math metotlarını doğru kullanmak; Java kütüphanesinin sık kullanılan sınıflarını ve paketlerini tanımak.
Kapsam
Yerel değişkenlerin, sınıf alanlarının ve iç içe blokların kapsamını ve yaşam süresini açıklamak; gölgelemeyi fark etmek.
Tasarım
Soyutlama, genelleme, DRY ve tek sorumluluk ilkeleriyle iyi metot tasarlamak; bir problemi metotlara bölmek.

3.2.1Fonksiyon kavramı

Şimdiye kadar yazılan programların hepsi tek bir metottan oluşuyordu: main. Kısa programlar için bu yeterlidir. Ama bir öğrenci not sistemi düşünelim. Notlar okunur, ortalama hesaplanır, harf notu bulunur, sonuçlar yazdırılır. Bütün bu adımlar main'in içine art arda yazılırsa yüzlerce satırlık tek bir blok ortaya çıkar. Ortalama formülü üç ayrı yerde gerekiyorsa üç kez kopyalanır. Formülde bir hata çıkınca üç yerin de ayrı ayrı düzeltilmesi gerekir. Fonksiyonlar bu düzensizliği ortadan kaldırır.

3.2.1.1Fonksiyon nedir?

Tanım 3.2.1

Fonksiyon (function), bir işi yapan, adı olan ve tekrar tekrar kullanılabilen kod parçasıdır. Aldığı parametrelerle ve döndürdüğü değerin tipiyle tanımlanır. Java'da fonksiyonlar her zaman bir sınıfın içinde yazılır ve metot (method) adını alır.

Fonksiyonu yemek tarifine benzetmek mümkündür. “Çay demle” bir tariftir. Tarif bir kez yazılır ve istendiği kadar uygulanır. Malzemeler tarifin girdileridir; programlamadaki karşılıkları parametrelerdir. Ortaya çıkan çay tarifin sonucudur; programlamadaki karşılığı dönüş değeridir. Tarifi uygulayan kişi her adımı yeniden düşünmez; yalnızca “çay demle” der ve sonucu alır. Program da bir metodu adıyla çağırır ve sonucunu kullanır.

Fonksiyon aslında yeni bir kavram değildir. Her Java programında bulunan main bir metottur. Ekrana yazı yazdırmak için kullanılan System.out.println(...) ise Java kütüphanesinde hazır bulunan bir metodun çağrısıdır. Programcı println'ın karakterleri ekrana nasıl gönderdiğini bilmez; yalnızca ne yaptığını bilir. Bu not, aynı yapıyı programcının kendi metotlarıyla kurmayı anlatır.

Fonksiyon kullanmanın kazançları şöyle sıralanabilir:

  • Okunabilirlik. İyi adlandırılmış bir metot çağrısı, ne yapıldığını tek satırda söyler. ortalamaHesapla(notlar) satırını okuyan kişi, döngünün ayrıntısına bakmadan ne olduğunu anlar.
  • Kod tekrarının azalması. Aynı iş bir kez yazılır, her gerektiğinde çağrılır. Bir hata çıktığında tek bir yer düzeltilir.
  • Modülerlik. Program küçük, yönetilebilir parçalara ayrılır. Her parça ayrı ayrı yazılabilir ve sınanabilir.
  • Düzen. Parçalar arasındaki bağlantı parametreler ve dönüş değerleriyle sınırlanır. Bir parçanın içi değiştiğinde ötekiler etkilenmez.

“Fonksiyon” ve “metot” sözcükleri bu notta çoğu yerde aynı anlamda kullanılır. Ayrım şudur: fonksiyon genel programlama kavramıdır; metot, bu kavramın Java'daki biçimidir. C gibi dillerde fonksiyonlar herhangi bir sınıfa bağlı olmadan yazılabilir. Java'da ise sınıf dışında metot yazılamaz.

3.2.1.2Girdi, işlem, çıktı

Her fonksiyon üç aşamada düşünülebilir. Girdi aşamasında fonksiyona veri sağlanır; bu veriler parametrelerdir ve fonksiyonun ne üzerinde çalışacağını belirler. İşlem aşamasında fonksiyonun gövdesindeki deyimler girdiyi işler. Çıktı aşamasında bir sonuç üretilir ve return deyimiyle çağırana geri verilir. Şekil 3.2.1 bu üç aşamayı toplama fonksiyonu üzerinde gösterir.

Girdi (Input) sayi1 = 5sayi2 = 3 İşlem (Process) toplama(...) sonuc = sayi1 + sayi2 Çıktı (Output) return 8 ne üzerinde çalışılacak? belirli bir görevi yerine getirir sonuç geri döndürülür
Şekil 3.2.1: Girdi, işlem ve çıktı. Girdiler (sayi1 = 5, sayi2 = 3) toplama fonksiyonuna girer. Gövde sonuc = sayi1 + sayi2 işlemini yapar. return 8 sonucu çağırana geri verir.

Bu şema matematikteki fonksiyon kavramıyla aynıdır. f(x) = x² fonksiyonunda x girdidir, kare alma işlemdir, x² çıktıdır. f(3) yazıldığında x yerine 3 konur ve sonuç 9 bulunur. Programlamadaki fonksiyon da aynı şekilde işler: girdiyi alır, hesaplar ve sonucu verir.

Programlamadaki fonksiyonlar matematiktekinden iki noktada ayrılır. Birincisi, bazı fonksiyonların girdisi yoktur. Ekrana sabit bir selam yazan fonksiyon hiçbir veriye ihtiyaç duymaz. İkincisi, bazı fonksiyonlar değer döndürmez; işleri bir etki bırakmaktır, örneğin ekrana yazmak. Java bu durumu void anahtar kelimesiyle belirtir. “void” İngilizcede “boşluk” demektir; metot geriye hiçbir değer vermez.

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

Bir metodu kullanmanın iki ayrı adımı vardır. Önce metot tanımlanır: adı, girdileri, sonucunun tipi ve gövdesi yazılır. Tanım tek başına hiçbir şey çalıştırmaz; yalnızca tarifi kayda geçirir. Sonra metot çağrılır: program akışı metoda gider, gövde çalışır ve akış çağrının yapıldığı yere döner. Bir metot bir kez tanımlanır, ama istendiği kadar çağrılabilir.

3.2.2.1Metot başlığının parçaları

Bir metot tanımı iki kısımdan oluşur: başlık ve gövde. Şekil 3.2.2 iki sayıyı toplayan toplama metodunun parçalarını ayrı ayrı gösterir.

public int toplama (int sayi1, int sayi2) { erişimbelirleyici dönüştipi metot adı parametre listesi (tip + ad) imza (signature) = ad + parametre tipleri int sonuc = sayi1 + sayi2; return sonuc; } gövde(kod bloğu) ← sonucu geri döndür
Şekil 3.2.2: Metot tanımının parçaları. Başlıkta sırasıyla erişim belirleyici (public), dönüş tipi (int), metot adı (toplama) ve parametre listesi bulunur. Ad ile parametre tipleri metodun imzasını oluşturur. Süslü parantezler arasındaki kısım gövdedir.
Tablo 3.2.1: public int toplama(int sayi1, int sayi2) { ... } başlığının parçaları.
ParçaÖrnekteGörevi
Erişim belirleyicipublicMetodu kimlerin çağırabileceğini söyler. public her yerden çağrılabilir demektir.
Dönüş tipiintMetodun geri vereceği değerin tipi. Değer döndürmeyen metotta void yazılır.
Metot adıtoplamaMetodun çağrılırken kullanılan adı. Fiil ya da fiil öbeğidir, küçük harfle başlar.
Parametre listesi(int sayi1, int sayi2)Metodun girdileri. Her parametrenin tipi ve adı ayrı ayrı yazılır; parametreler virgülle ayrılır.
Gövde{ ... }Metot çağrıldığında çalışacak deyimler.

Erişim belirleyici (access modifier) ile dönüş tipi arasına başka düzenleyiciler de girebilir. Bu notta en sık görülen düzenleyici static'tir; metodun nesne gerektirmeden çağrılabileceğini belirtir. Başlıktaki sıra bellidir: önce düzenleyiciler, sonra dönüş tipi, sonra ad, en son parametre listesi. Erişim belirleyici yazılmayabilir; o zaman metot yalnızca aynı paketteki sınıflardan çağrılabilir. Dönüş tipi ise hiçbir zaman atlanamaz.

Adlandırmada Java geleneği camelCase yazımıdır: ilk sözcük küçük harfle başlar, sonraki sözcüklerin ilk harfi büyük yazılır. ortalamaHesapla, asalMi, notlariOku bu kurala uyar. Metot bir iş yaptığı için adı da bir eylem bildirir. boolean döndüren metotlarda ad çoğu zaman bir soru gibi okunur: asalMi(17), “17 asal mı?” sorusunun cevabını verir.

Tanım 3.2.2

Metot imzası (method signature), metodun adı ile parametre tiplerinin sırasıdır. toplama metodunun imzası toplama(int, int)'tir. Dönüş tipi, parametre adları ve erişim belirleyici imzaya dahil değildir.

İmza kavramı iki yerde önem kazanır. Birincisi, derleyici bir çağrıyı hangi metotla eşleştireceğine imzaya bakarak karar verir. İkincisi, aynı sınıfta aynı imzalı iki metot bulunamaz. Bu kural, aynı adlı metotların hangi koşulda bir arada yazılabileceğini belirler.

Başlık yazarken yapılan hataların çoğunu derleyici yakalar. Üç tipik örnek ve JDK 21'in verdiği iletiler aşağıdadır. İlkinde ikinci parametrenin tipi unutulmuştur. Matematikte “a, b tam sayı olsun” demek olağandır, ama Java'da her parametre kendi tipini taşır:

public class VirgulParam {
    static int topla(int a, b) { return a + b; }
}
VirgulParam.java:2: error: <identifier> expected
    static int topla(int a, b) { return a + b; }
                             ^
1 error

İkincisinde dönüş tipi yazılmamıştır. Dönüş tipi olmayan başlığı derleyici yalnızca sınıfla aynı adlı yapıcı (constructor) için kabul eder. Yapıcı, nesne oluşturulurken çalışan özel bir metottur. topla sınıfın adı olmadığı için hata verilir:

public class TipsizMetot {
    static topla(int a, int b) { return a + b; }
}
TipsizMetot.java:2: error: invalid method declaration; return type required
    static topla(int a, int b) { return a + b; }
           ^
1 error

Üçüncüsünde dönüş tipi düzenleyiciden önce yazılmıştır. Derleyici int'ten sonra bir ad bekler, static ile karşılaşınca iki ayrı hata bildirir:

public class StaticSira {
    int static topla(int a, int b) { return a + b; }
}
StaticSira.java:2: error: <identifier> expected
    int static topla(int a, int b) { return a + b; }
       ^
StaticSira.java:2: error: invalid method declaration; return type required
    int static topla(int a, int b) { return a + b; }
               ^
2 errors

Bir başka kural da metotların iç içe tanımlanamamasıdır. Her metot sınıfın gövdesinde, öteki metotların dışında yazılır. main'in içine bir metot tanımı konursa derleyici bunu anlamsız bir ifade olarak görür:

public class IcIceMetot {
    public static void main(String[] args) {
        static int kare(int n) { return n * n; }
    }
}
IcIceMetot.java:3: error: illegal start of expression
        static int kare(int n) { return n * n; }
        ^
IcIceMetot.java:5: error: class, interface, enum, or record expected
}
^
2 errors

Metotların sınıf içindeki sırası ise önemli değildir. main en üstte, yardımcı metotlar altta yazılabilir ya da tersi yapılabilir. Derleyici sınıfın tamamını okuduktan sonra çağrıları eşleştirir.

3.2.2.2Çağrı, parametre ve argüman

Tanımlanan bir metodu çalıştırmak için metodun adı ve parantez içinde girdileri yazılır. Aşağıdaki program toplama metodunu tanımlar ve main'den çağırır.

public class ToplamaOrnegi {
    public static int toplama(int sayi1, int sayi2) {
        int sonuc = sayi1 + sayi2;
        return sonuc;
    }

    public static void main(String[] args) {
        int sonuc = toplama(5, 3);
        System.out.println("Toplam: " + sonuc);
    }
}
Toplam: 8
Tanım 3.2.3

Metot çağrısı (method call), bir metodu argümanlarla çalıştırmaktır. Parametre (parameter), metot tanımında girdiyi temsil eden değişkendir. Argüman (argument), çağrı sırasında parametreye verilen gerçek değerdir.

Programda toplama(5, 3) ifadesi bir çağrıdır. Çağrıdaki 5 ve 3 argümandır; tanımdaki sayi1 ve sayi2 ise parametredir. Ayrımı akılda tutmanın kolay yolu şudur: parametre tanımda yazılır ve bir ad taşır, argüman çağrıda yazılır ve bir değer taşır. Çağrı anında argümanların değerleri sırasıyla parametrelere kopyalanır: sayi1 = 5, sayi2 = 3.

Çağrı yapılınca program akışı metoda gider. Gövde çalışır, sonuc 8 olur ve return sonuc; bu değeri geri verir. Akış çağrının yapıldığı yere döner. toplama(5, 3) ifadesinin değeri artık 8'dir ve bu değer main'deki sonuc değişkenine atanır. Son satırdaki println da bir metot çağrısıdır; argümanı "Toplam: " + sonuc ifadesidir ve ekrana Toplam: 8 yazılır.

Argümanlar parametrelerle üç bakımdan uyuşmalıdır: sayı, sıra ve tip. İki parametreli bir metoda tek argüman verilirse derleyici çağrıyı reddeder ve beklenen tiplerle verilen tipleri yan yana yazar:

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

İletideki required: int,int satırı metodun beklediği tipleri, found: int satırı çağrıda verilenleri gösterir. reason satırı ise sorunun argüman sayısında olduğunu söyler. Tip uyuşmazlığında da hata verilir. toplama(5, 2.5) çağrısında 2.5 bir double'dır ve int parametreye bilgi kaybı olmadan sığmaz:

TipArguman.java:4: error: incompatible types: possible lossy conversion from double to int
        int s = toplama(5, 2.5);
                           ^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

Ters yöndeki dönüşüm ise serbesttir. double parametreli bir metoda int argüman verilebilir; değer otomatik olarak double'a genişletilir. Bu kural, aşırı yüklemede derleyicinin seçimini de belirler.

Argüman yalnızca sabit bir sayı olmak zorunda değildir; değişken ya da herhangi bir ifade olabilir. kare(a + 1) çağrısında Java önce a + 1 ifadesini hesaplar. a 3 ise argümanın değeri 4'tür ve bu 4 parametreye kopyalanır. Metot ne a değişkenini ne de a + 1 ifadesini görür; yalnızca 4 değerini alır. Bu kural, argümanların kopyalanması konusunun temelidir.

3.2.2.3Çağrıyı adım adım izlemek

Bir metot çağrıldığında bellekte neler olduğunu anlamak için iki kavram gerekir: çerçeve ve çağrı yığını.

Tanım 3.2.4

Çerçeve (stack frame), bir metot çağrısının parametrelerini ve yerel değişkenlerini tutan bellek kaydıdır. Çağrı yığını (call stack), çalışmakta olan metotların çerçevelerini üst üste tutan yapıdır. Her çağrı yığının üstüne yeni bir çerçeve ekler; metot bitince çerçevesi yığından kaldırılır.

Yığın, üst üste konan tabaklara benzer. En son konan tabak en üsttedir ve ilk o alınır. Çağrı yığınında da en son çağrılan metodun çerçevesi en üstte durur. Çalışan metot her zaman en üstteki çerçevenin sahibidir. O metot bitince çerçevesi kalkar ve altındaki metot kaldığı yerden devam eder.

Önceki programın çalışması Tablo 3.2.2'de adım adım verilmiştir. Satır numaraları, sunumdaki izleme slaytının kod düzenine göredir: 1–4. satırlar toplama, 6–9. satırlar main metodudur.

Tablo 3.2.2: toplama(5, 3) çağrısının izlenmesi. Çağrı yığını sütununda en sağdaki çerçeve en üsttedir; değişkenler sütunu o çerçevenin değişkenlerini gösterir.
AdımSatırÇağrı yığınıDeğişkenlerÇıktı
16: public static void main(String[] args) {main()args=[]—
27: int sonuc = toplama(5, 3);main() → toplama(5, 3)sayi1=5, sayi2=3—
32: int sonuc = sayi1 + sayi2;main() → toplama(5, 3)sayi1=5, sayi2=3, sonuc=8—
43: return sonuc;main() → toplama(5, 3)sayi1=5, sayi2=3, sonuc=8, dönüş=8—
57: int sonuc = toplama(5, 3);main()args=[], sonuc=8—
68: System.out.println("Toplam: " + sonuc);main()args=[], sonuc=8Toplam: 8

İlk adımda yığında yalnızca main'in çerçevesi vardır. Program her zaman main metodundan başlar; args komut satırından gelen argümanları tutan dizidir ve burada boştur. İkinci adımda toplama(5, 3) çağrılır. Yığının üstüne yeni bir çerçeve açılır ve argümanlar bu çerçevedeki parametrelere kopyalanır.

Üçüncü adımda gövde çalışır: sonuc = 5 + 3 = 8. Bu sonuc, toplama'nın yerel değişkenidir ve toplama'nın çerçevesinde durur. Şekil 3.2.3 bu anı gösterir. main'in çerçevesinde de sonuc adlı bir değişken vardır, ama değeri henüz belli değildir. İki değişken aynı adı taşır, fakat farklı çerçevelerde yaşayan ayrı değişkenlerdir.

Yığın (stack)toplama(5, 3)sayi15sayi23sonuc8main()sonuc? Yığın (stack)main()sonuc8
Şekil 3.2.3: Çağrı yığınının iki anı. Solda toplama(5, 3) çalışıyor: üstteki çerçevede sayi1 = 5, sayi2 = 3, sonuc = 8; alttaki main çerçevesinde sonuc henüz belirsiz. Sağda metot dönmüş, çerçevesi silinmiş; main'deki sonuc 8 olmuş.

Dördüncü adımda return sonuc; çalışır. 8 değeri çağırana gönderilir, metot biter ve çerçevesi yığından silinir. Silinen çerçeveyle birlikte sayi1, sayi2 ve toplama'nın sonuc'u da yok olur. Beşinci adımda akış çağrının yapıldığı 7. satıra döner; dönen 8, main'in sonuc değişkenine atanır. Altıncı adımda println çağrılır ve ekrana Toplam: 8 yazılır. println da bir metottur; çalıştığı kısa süre boyunca onun da bir çerçevesi yığına eklenir ve kaldırılır.

Bu izlemeden çıkan sonuç şudur: metot çağrısı, akışın geçici olarak başka bir yere gidip geri gelmesidir. Gidilen yerde yeni bir çalışma alanı açılır, iş bitince bu alan temizlenir. Geriye yalnızca return ile gönderilen değer kalır.

3.2.2.4Değer döndüren ve döndürmeyen metotlar

Aşağıdaki program girdi ve çıktı bakımından farklı üç metot içerir.

public class OrnekFonksiyonlar {
    // Toplama fonksiyonu
    public static int toplama(int sayi1, int sayi2) {
        return sayi1 + sayi2;
    }
    // Kare hesaplama fonksiyonu
    public static double kareHesapla(double sayi) {
        return sayi * sayi;
    }
    // Merhaba dünya fonksiyonu
    public static void merhabaDunya() {
        System.out.println("Merhaba, Dünya!");
    }

    public static void main(String[] args) {
        System.out.println(toplama(2, 7));
        System.out.println(kareHesapla(1.5));
        merhabaDunya();
    }
}
9
2.25
Merhaba, Dünya!
Tablo 3.2.3: Üç metodun girdileri ve çıktıları.
MetotGirdiÇıktıÇağrının kullanımı
toplama2 × intintprintln(toplama(2, 7)) → 9
kareHesapla1 × doubledoubleprintln(kareHesapla(1.5)) → 2.25
merhabaDunyayokyok (void)merhabaDunya(); tek başına bir deyim

toplama bu kez ara değişken kullanmadan sonucu doğrudan döndürür: return sayi1 + sayi2;. Java önce ifadeyi hesaplar, sonra değeri döndürür. Ara değişkenli ve ara değişkensiz iki yazım aynı sonucu verir.

kareHesapla(1.5) çağrısı 1,5 × 1,5 = 2,25 değerini döndürür ve ekrana 2.25 yazılır. Java ondalık sayıları kod içinde ve println çıktısında nokta ile yazar. merhabaDunya metodunun ne parametresi ne de dönüş değeri vardır. Parantezler yine de yazılır: hem tanımda merhabaDunya() hem çağrıda merhabaDunya();. Parantezsiz merhabaDunya; yazmak derleme hatasıdır, çünkü Java parantez görmeden bir adı metot çağrısı olarak tanımaz.

İki tür metot arasındaki asıl fark, çağrının nasıl kullanıldığındadır. Değer döndüren bir metodun çağrısı bir ifadedir; değeri vardır. Bu değer bir değişkene atanabilir, başka bir hesaba katılabilir ya da başka bir metoda argüman olarak verilebilir: println(toplama(2, 7)) ifadesinde toplama'nın sonucu println'ın argümanı olur. void bir metodun çağrısının ise değeri yoktur. Bu çağrı yalnızca tek başına bir deyim olarak yazılabilir.

Değer döndüren bir metot tek başına da çağrılabilir: toplama(2, 7); geçerli bir deyimdir. Ancak dönen değer hiçbir yere atanmadığı için kaybolur. Derleyici bu durumda uyarı vermez. Bu yüzden “metot çağrıldı ama sonuç değişmedi” şikâyetlerinin çoğunda dönen değer hiçbir değişkene atanmamıştır.

3.2.3return ve void

return deyimi metodun sonucunu çağırana verir. Kuralları basittir, ama yeni başlayanların derleyiciyle en çok tartıştığı yerlerden biridir. Bu kesim return'ün nasıl çalıştığını, dönen değerin tipinin hangi koşulda kabul edildiğini ve derleyicinin “her yol bir return ile bitmeli” kuralını nasıl denetlediğini anlatır.

3.2.3.1return deyimi

Tanım 3.2.5

Dönüş değeri (return value), metodun return ile çağırana verdiği değerdir. Dönüş tipi (return type), bu değerin tipidir ve başlıkta metot adından önce yazılır; metot değer döndürmüyorsa dönüş tipi void'dir.

return ifade; deyiminin iki etkisi vardır. Birincisi, ifadenin değeri hesaplanır ve çağırana verilir. İkincisi, metot hemen biter. return'den sonra gelen satırlar çalışmaz. Bu özellik, metodun içinde erken çıkış yapmak için kullanılır.

void bir metot değer döndürmez, ama yine de erken bitebilir. Bunun için tek başına return; yazılır. Aşağıdaki program bölme yapmadan önce böleni denetler. Bölen sıfırsa uyarı yazar ve metottan hemen çıkar.

public class Erken {
    static void bolumYazdir(int bolunen, int bolen) {
        if (bolen == 0) {
            System.out.println("Sıfıra bölünemez!");
            return;                      // metottan hemen çık
        }
        System.out.println(bolunen / bolen);
    }
    public static void main(String[] args) {
        bolumYazdir(10, 2);
        bolumYazdir(7, 0);
    }
}
5
Sıfıra bölünemez!

İlk çağrıda bolen 2'dir. if koşulu yanlıştır, gövdesi atlanır ve 10 / 2 sonucu olan 5 yazılır. İkinci çağrıda bolen 0'dır. Koşul doğrudur; uyarı yazılır ve return; metodu bitirir. println(bolunen / bolen) satırına hiç gelinmez. Bu satıra gelinseydi tam sayı bölmesi sıfıra bölmeye çalışacak ve program ArithmeticException: / by zero hatasıyla duracaktı.

Aynı metot else ile de yazılabilirdi. Erken return kalıbının yararı, hatalı durumları metodun başında ayıklayıp asıl işi girintisiz bırakmasıdır. Denetlenecek koşul sayısı arttıkça bu yazım iç içe if'lerden daha okunaklı olur.

return'den hemen sonra, aynı blokta bir deyim yazmak derleme hatasıdır. Derleyici bu deyime hiçbir yoldan ulaşılamayacağını görür ve unreachable statement iletisini verir:

public class Ulasilamaz {
    static int f(int n) {
        return n * 2;
        System.out.println("bitti");
    }
}
Ulasilamaz.java:4: error: unreachable statement
        System.out.println("bitti");
        ^
1 error

3.2.3.2Dönüş tipiyle uyum

return'ün döndürdüğü değerin tipi, başlıkta bildirilen dönüş tipiyle uyumlu olmalıdır. Uyum, atamadaki kuralın aynısıdır: değer, dönüş tipinden bir değişkene atanabiliyorsa döndürülebilir de. Bu yüzden int bir değer double dönüş tipli bir metottan döndürülebilir; değer otomatik olarak genişletilir. Tersi ise bilgi kaybı demektir ve derleyici kabul etmez:

public class Daraltma {
    static int yarim(double x) { return x / 2; }
}
Daraltma.java:2: error: incompatible types: possible lossy conversion from double to int
    static int yarim(double x) { return x / 2; }
                                          ^
1 error

Genişletme serbest olduğu için derlenen ama yanlış sonuç veren bir tuzak doğar. Aşağıdaki iki metodun ikisi de double döndürür, ama sonuçları farklıdır.

public class Genisletme {
    // int sonuç double dönüş tipine genişler
    static double yarim(int n) {
        return n / 2;
    }
    static double yarimDogru(int n) {
        return n / 2.0;
    }
    public static void main(String[] args) {
        System.out.println(yarim(7));
        System.out.println(yarimDogru(7));
    }
}
3.0
3.5

yarim(7) metodunda n / 2 iki int arasında bir bölmedir. Tam sayı bölmesi kesri atar ve sonuç 3 olur. Dönüş tipinin double olması bu hesabı değiştirmez; 3 değeri dönerken 3.0'a genişletilir. Yani dönüş tipi, ifadenin nasıl hesaplanacağını değil, hesaplanmış sonucun hangi tipe çevrileceğini belirler. Doğru sonuç için bölmenin kendisi ondalıklı yapılmalıdır: n / 2.0 ifadesi 3.5 verir.

void ile değer döndürme birbirini dışlar. void bir metotta return ile değer döndürülemez. Değer döndüren bir metotta da değersiz return; yazılamaz. Derleyici iki durum için ayrı iletiler verir:

public class VoidDeger {
    static void selam() { return "Merhaba"; }
}
VoidDeger.java:2: error: incompatible types: unexpected return value
    static void selam() { return "Merhaba"; }
                                 ^
1 error
public class IntBosReturn {
    static int ikiKat(int n) { int s = n * 2; return; }
}
IntBosReturn.java:2: error: incompatible types: missing return value
    static int ikiKat(int n) { int s = n * 2; return; }
                                              ^
1 error

void metodun sonucunu kullanmaya çalışmak da hatadır. void bir çağrının değeri olmadığı için ne bir değişkene atanabilir ne de println'a argüman olarak verilebilir:

public class VoidKullan {
    static void selamla() { System.out.println("Selam"); }
    public static void main(String[] args) {
        int x = selamla();
    }
}
VoidKullan.java:4: error: incompatible types: void cannot be converted to int
        int x = selamla();
                       ^
1 error
VoidPrint.java:4: error: 'void' type not allowed here
        System.out.println(selamla());
                                  ^
1 error

3.2.3.3Her yol bir return ile bitmeli

Değer döndüren bir metodun içinde akışın izleyebileceği her yol bir return ile bitmelidir. Akış hiçbir return'e uğramadan metodun sonundaki süslü paranteze ulaşabiliyorsa derleyici missing return statement der. Hatanın en şaşırtıcı biçimi, insana tam görünen koşul zincirlerinde ortaya çıkar:

public class EksikReturn {
    static int isaret(int n) {
        if (n > 0) return 1;
        if (n <= 0) return -1;
    }
}
EksikReturn.java:5: error: missing return statement
    }
    ^
1 error

Bir insan için n > 0 ile n <= 0 koşulları bütün tam sayıları kapsar; ikisinden biri mutlaka doğrudur. Derleyici ise koşulların değerlerini düşünmez. Yalnızca yapıya bakar: iki if vardır ve ikisi de yanlış olabilirmiş gibi değerlendirilir. O zaman akış metodun sonuna ulaşır ve orada return yoktur. Aynı hata if/else if zincirinde de görülür; son dal else if olduğu sürece derleyici zincirin bir yerden çıkılabileceğini varsayar.

Çözüm, son koşulu düz bir else'e çevirmek ya da metodun sonuna varsayılan bir return koymaktır:

static int isaret(int n) {
    if (n > 0) return 1;
    return -1;          // n <= 0 olan her durum buraya gelir
}

void metotlarda böyle bir zorunluluk yoktur. Akış metodun sonuna ulaşınca metot kendiliğinden biter; sonda return; yazmak gereksizdir.

3.2.4static ve nesne üzerinden çağrı

Bu ana kadarki bütün metotların başlığında static sözcüğü vardı. Bu sözcük, metodun nasıl çağrılacağını belirler. Nesnelerin ayrıntısına girmeden iki çağrı biçimini ayırt etmek şimdilik yeterlidir.

Tanım 3.2.6

static metot (static method), nesneye değil sınıfa ait olan metottur. Çağrılmak için nesne gerekmez. Aynı sınıfın içinden adıyla, başka bir sınıftan SinifAdi.metot() biçiminde çağrılır. static olmayan metot ise bir nesne üzerinden çağrılır: nesne.metot().

static metodun en tanıdık örneği Math sınıfıdır. Karekök almak için bir Math nesnesi oluşturulmaz; doğrudan Math.sqrt(16) yazılır. Aynı sınıf içinde sınıf adını yazmaya gerek yoktur:

static int toplama(int sayi1, int sayi2) {
    return sayi1 + sayi2;
}
// main içinde, doğrudan:
int sonuc = toplama(5, 3);

static olmayan bir metodu çağırmak için önce new ile o sınıftan bir nesne oluşturulur, sonra metot nesnenin üzerinden çağrılır:

public class Hesap {
    int toplama(int sayi1, int sayi2) {
        return sayi1 + sayi2;
    }
    public static void main(String[] args) {
        Hesap hesap = new Hesap();
        int sonuc = hesap.toplama(5, 3);
        System.out.println(sonuc);  // 8
    }
}
8

Hesap hesap = new Hesap(); satırı Hesap sınıfından bir nesne oluşturur ve ona hesap adlı bir değişkenle ulaşır. hesap.toplama(5, 3) çağrısı metodu bu nesne üzerinden çalıştırır. Nokta işareti “bu nesnenin metodu” anlamına gelir. Sonuç yine 8'dir.

İki biçim karıştırılırsa derleyici hata verir. main bir static metottur; çalışırken ortada henüz hiçbir nesne yoktur. static olmayan bir metodu main'den doğrudan çağırmak, “hangi nesnenin metodu?” sorusunu cevapsız bırakır:

public class StaticBaglam {
    int toplama(int sayi1, int sayi2) { return sayi1 + sayi2; }
    public static void main(String[] args) {
        int sonuc = toplama(5, 3);
    }
}
StaticBaglam.java:4: error: non-static method toplama(int,int) cannot be referenced from a static context
        int sonuc = toplama(5, 3);
                    ^
1 error

İletideki “static context” static metodun içi demektir. Düzeltmenin iki yolu vardır: ya metodun başlığına static eklenir ya da bir nesne oluşturulup metot onun üzerinden çağrılır. Bu bölümdeki metotlar nesnenin durumuna ihtiyaç duymadığı için hepsi static yazılır.

main'in kendisinin neden static olduğu da buradan anlaşılır. Program başladığında JVM (Java Sanal Makinesi) henüz hiçbir nesne oluşturmamıştır. main'i bir nesneye ihtiyaç duymadan çağırabilmesi için metodun sınıfa ait olması gerekir.

3.2.5Değer ile aktarım

Bir metot, kendisine argüman olarak verilen değişkeni değiştirebilir mi? Bu sorunun cevabı Java'nın en çok yanlış anlaşılan konularından biridir. Kısa cevap şudur: hayır. Metot, argümanın kendisini değil kopyasını alır. Ama dizilerde ve nesnelerde kopyalanan şey bir referans olduğu için durum ilk bakışta farklı görünür. Bu kesim iki durumu ayrı ayrı izler.

Tanım 3.2.7

Değer ile aktarım (pass by value), çağrı sırasında argümanın değerinin kopyalanıp parametreye verilmesidir. Java bütün argümanları değer ile aktarır. Argüman bir dizi ya da nesne ise kopyalanan değer, o dizinin ya da nesnenin yerini gösteren referanstır.

3.2.5.1İlkel tipte argüman

Aşağıdaki programda hem main'in hem arttir'ın sayi adlı bir değişkeni vardır. arttir, kendi sayi'sına 10 ekler.

static void arttir(int sayi) {
  sayi = sayi + 10;
  System.out.println("arttir: " + sayi);
}

public static void main(String[] args) {
  int sayi = 5;
  arttir(sayi);
  System.out.println("main: " + sayi);
}
arttir: 15
main: 5

Program arttir: 15 ve main: 5 yazar. main'deki sayi değişmemiştir. Nedenini Tablo 3.2.4 adım adım gösterir.

Tablo 3.2.4: arttir(sayi) çağrısının izlenmesi. Değişkenler sütunu en üstteki çerçeveyi gösterir.
AdımSatırÇağrı yığınıDeğişkenlerÇıktı
17: int sayi = 5;main()sayi=5—
28: arttir(sayi);main() → arttir(5)sayi=5—
32: sayi = sayi + 10;main() → arttir(5)sayi=15—
43: System.out.println("arttir: " + sayi);main() → arttir(5)sayi=15arttir: 15
59: System.out.println("main: " + sayi);main()sayi=5main: 5

İkinci adımda arttir(sayi) çağrılır. Java argüman olan sayi değişkeninin değerini okur: 5. Yeni açılan çerçevede arttir'ın parametresi için ayrı bir kutu ayrılır ve 5 bu kutuya kopyalanır. Artık bellekte iki ayrı sayi vardır: biri main'in çerçevesinde, biri arttir'ın çerçevesinde. Adlarının aynı olması önemsizdir; Java her çerçevenin değişkenlerini ayrı tutar.

Yığın (stack)arttir(5)sayi15main()sayi5 Yığın (stack)main()sayi5
Şekil 3.2.4: Değer ile aktarım. Solda arttir çalışıyor: üstteki çerçevede kopya 15 olmuş, alttaki main çerçevesinde sayi hâlâ 5. Sağda metot bitmiş; kopya çerçeveyle birlikte silinmiş, main'deki 5 olduğu gibi kalmış.

Üçüncü adımda sayi = sayi + 10; çalışır. Bu satır en üstteki çerçevede, yani arttir'ın içinde çalıştığı için yalnızca kopyayı değiştirir: kopya 15 olur. Dördüncü adımda arttir: 15 yazılır. Beşinci adımda metot biter; çerçevesi ve onunla birlikte kopya silinir. main'e dönüldüğünde main'in sayi'sı hiç dokunulmamış hâliyle 5'tir.

Metodun yaptığı değişikliği main'e taşımanın tek yolu return'dür. arttir yeni değeri döndürecek biçimde yazılır ve çağıran bu değeri kendi değişkenine atar:

static int arttir(int sayi) {
    return sayi + 10;
}
// main içinde:
sayi = arttir(sayi);   // main'deki sayi artık 15

Burada da metot main'in değişkenine dokunmaz. Değişikliği yapan, atamayı yazan main'in kendisidir. Bu düzen programı güvenli kılar: bir metot çağrıldığında, çağıranın yerel değişkenlerinin değişmeyeceği kesindir.

3.2.5.2Dizi argümanı: referansın kopyası

Dizi parametreli metotlarda durum farklı görünür. Önce dizinin bellekte nerede durduğunu bilmek gerekir. Java'da bellek iki bölgeye ayrılır. Çerçeveler ve yerel değişkenler yığında (stack) durur. Diziler ve nesneler ise heap adı verilen ayrı bir bölgede tutulur. Bir dizi değişkeni dizinin kendisini değil, dizinin heap'teki yerini gösteren bir referansı (reference) tutar.

Aşağıdaki metot bir dizi ve bir int alır. Diziyi ve tam sayıyı değiştirmeye çalışır, sonra parametresine yepyeni bir dizi atar.

static void degistir(int[] d, int x) {
    d[0] = 50;
    x = 50;
    d = new int[2];
    d[1] = 50;
}

public static void main(String[] args) {
    int[] a = {1, 2};
    int x = 1;
    degistir(a, x);
    System.out.println(a[0] + " " + a[1] + " " + x);
}
50 2 1

Program 50 2 1 yazar: dizinin ilk öğesi değişmiş, ikincisi değişmemiş, x ise hiç değişmemiştir. Tablo 3.2.5 her adımda iki dizinin ve iki x'in durumunu gösterir. Tabloda main'de oluşturulan dizi A, metodun içinde oluşturulan yeni dizi B diye adlandırılmıştır.

Tablo 3.2.5: degistir(a, x) çağrısının izlenmesi. “A {50, 2}” yazımı, değişkenin A dizisini gösterdiğini ve dizinin o anki içeriğini belirtir.
AdımSatıra → dizid → dizix (main)x (degistir)
19: int[] a = {1, 2};A {1, 2}———
210: int x = 1;A {1, 2}—1—
31: static void degistir(int[] d, int x) {A {1, 2}A {1, 2}11
42: d[0] = 50;A {50, 2}A {50, 2}11
53: x = 50;A {50, 2}A {50, 2}150
64: d = new int[2];A {50, 2}B {0, 0}150
75: d[1] = 50;A {50, 2}B {0, 50}150
811: degistir(a, x);A {50, 2}—1—
912: System.out.println(a[0] + " " + a[1] + " " + x);A {50, 2}—1—

Birinci adımda int[] a = {1, 2}; heap'te iki öğeli bir dizi oluşturur; a bu dizinin referansını tutar. İkinci adımda x = 1 olur. İlkel bir değer olduğu için 1 doğrudan main'in çerçevesinde saklanır.

Üçüncü adımda degistir(a, x) çağrılır ve iki argüman da değer ile aktarılır. d'ye a'daki referansın kopyası verilir; degistir'in x'ine 1'in kopyası verilir. Referans kopyalanmıştır, ama dizi kopyalanmamıştır. a ve d artık heap'teki aynı A dizisini gösterir. Şekil 3.2.5'nin ilk parçası bu anı çizer: iki ayrı çerçevedeki iki ok aynı diziye uzanır.

çağrı yığınıheapmainax1502degistirdx1
çağrı yığınıheapmainax1502degistirdx50050
çağrı yığınıheapmainax1502
Şekil 3.2.5: Referansın kopyası. Üstte d[0] = 50 sonrası: a ve d aynı diziyi gösterir, değişiklik ortak diziye yazılır. Ortada d = new int[2] ve d[1] = 50 sonrası: d artık yeni diziyi gösterir, bağ kopmuştur. Altta metot bitmiştir; yeni diziyi gösteren referans kalmamıştır.

Dördüncü adımda d[0] = 50; çalışır. Bu atama d değişkenini değil, d'nin gösterdiği dizinin ilk hücresini değiştirir. d ve a aynı diziyi gösterdiği için değişiklik main'den de görülür. Beşinci adımda x = 50; yalnızca degistir'in kopyasını değiştirir; main'in x'i 1 kalır.

Altıncı adımda d = new int[2]; heap'te yeni bir B dizisi oluşturur ve d'ye bu dizinin referansını atar. Bu atama d'nin kendisini değiştirir. d bir kopya olduğu için a etkilenmez; a hâlâ A'yı gösterir. d ile a arasındaki bağ bu satırda kopar. Yedinci adımdaki d[1] = 50; yeni diziye yazar; A dizisi değişmez.

Sekizinci adımda metot biter ve çerçevesi silinir. B dizisini gösteren tek referans d idi; artık hiçbir değişken B'yi göstermez. Böyle bir dizi programdan bir daha erişilemez. JVM'in çöp toplayıcısı (garbage collector) bu tür nesnelerin belleğini zamanı gelince geri alır. Dokuzuncu adımda ekrana a[0], a[1] ve x yazılır: 50 2 1.

Bu örnekten çıkan kural şudur. Java her şeyi değer ile aktarır; referans da kopyalanan bir değerdir. Metot, aldığı referansın gösterdiği diziyi değiştirebilir, çünkü dizi ortaktır. Ama parametreye yeni bir dizi atamak çağıranın değişkenini değiştirmez, çünkü parametre yalnızca bir kopyadır. Bazı kaynaklar Java'nın nesneleri “referans ile aktardığını” yazar. Bu ifade yanlıştır: gerçek referans ile aktarımda d = new int[2] satırı main'deki a'yı da değiştirirdi.

3.2.5.3Takas tuzağı

Değer ile aktarımın en bilinen sonucu, iki değişkenin değerini değiştiren bir metodun Java'da yazılamamasıdır. Aşağıdaki programda yerDegistir metodunun iki sürümü vardır. İlki iki int alır, ikincisi bir dizi ve iki indis alır.

import java.util.Arrays;

public class Takas {
    static void yerDegistir(int x, int y) {
        int gecici = x;
        x = y;
        y = gecici;
    }
    static void yerDegistir(int[] d, int i, int j) {
        int gecici = d[i];
        d[i] = d[j];
        d[j] = gecici;
    }
    public static void main(String[] args) {
        int[] dizi = {4, 7, 9};
        yerDegistir(dizi[0], dizi[2]);
        System.out.println(Arrays.toString(dizi));
        yerDegistir(dizi, 0, 2);
        System.out.println(Arrays.toString(dizi));
    }
}
[4, 7, 9]
[9, 7, 4]

İlk çağrı yerDegistir(dizi[0], dizi[2])'dir. Argümanlar dizi[0] ve dizi[2] ifadelerinin değerleridir, yani 4 ve 9. Metot bu iki kopyayı kendi içinde takas eder: x 9, y 4 olur. Metot bitince kopyalar silinir ve dizi hiç değişmeden kalır: [4, 7, 9].

İkinci çağrı diziyi ve indisleri alır. Bu kez metot, referansın gösterdiği dizinin hücrelerine yazar: d[0] 9, d[2] 4 olur. Dizi ortak olduğu için değişiklik main'den görülür: [9, 7, 4]. Arrays.toString, bir diziyi köşeli parantez içinde yazdıran hazır bir metottur.

Değer ile aktarımın davranışı Değer ile Aktarım Laboratuvarı simülatöründe de adım adım izlenebilir. Simülatör ilkel değer, dizi ve nesne geçirmeyi çağrı yığını ve heap üzerinde ayrı ayrı çizer.

3.2.6Metot aşırı yükleme

Bazen aynı iş farklı tipte ya da farklı sayıda girdiyle yapılmak istenir. İki tam sayıyı, iki ondalıklı sayıyı ve üç tam sayıyı toplayan metotlar düşünelim. Her birine ayrı bir ad vermek mümkündür: toplaIkiInt, toplaIkiDouble, toplaUcInt. Ama bu adlar kullanımı zorlaştırır; programcı her seferinde doğru adı hatırlamak zorunda kalır. Java aynı adın birden çok metot için kullanılmasına izin verir.

3.2.6.1Aynı ad, farklı parametre listesi

Tanım 3.2.8

Aşırı yükleme (method overloading), aynı sınıfta aynı adlı birden çok metot tanımlamaktır. Metotların parametre listeleri farklı olmalıdır: parametre sayısı, tipi ya da sırası. Başka bir deyişle, aşırı yüklenen metotların imzaları farklıdır.

public class AsiriYukleme {
    static int topla(int a, int b) {
        return a + b;
    }
    static double topla(double a, double b) {
        return a + b;
    }
    static int topla(int a, int b, int c) {
        return a + b + c;
    }

    public static void main(String[] args) {
        System.out.println(topla(2, 3));
        System.out.println(topla(2.5, 1.2));
        System.out.println(topla(1, 2, 3));
    }
}
5
3.7
6

Sınıfta üç topla metodu vardır. İmzaları topla(int, int), topla(double, double) ve topla(int, int, int)'tir; üçü de birbirinden farklıdır. topla(2, 3) çağrısı iki int argüman taşır ve ilk metodu çalıştırır: 5. topla(2.5, 1.2) iki double taşır ve ikinci metodu çalıştırır: 3.7. topla(1, 2, 3) üç argüman taşır ve üçüncü metodu çalıştırır: 6. Çıktıdaki 5 ile 3.7'nin farklı biçimi de seçilen metodu ele verir; int döndüren metodun sonucu ondalık nokta olmadan yazılır.

Hangi metodun çalışacağına derleyici karar verir. Karar, program çalışmadan önce, derleme zamanında ve yalnızca argümanların tiplerine bakılarak verilir. Derlenmiş programda her çağrı belirli bir metoda bağlanmış durumdadır.

Java kütüphanesi aşırı yüklemeyi yoğun biçimde kullanır. println tek bir metot değildir: println(int), println(double), println(String), println(char), println(boolean) ve parametresiz println() ayrı ayrı tanımlanmıştır. Programcı hepsini tek bir ad olarak görür ve hangi tipi yazdırırsa yazdırsın aynı adı kullanır. Math.abs ve Math.max da int, long, float ve double için ayrı ayrı aşırı yüklenmiştir.

Aşırı yüklemenin ölçütü imzadır ve dönüş tipi imzaya dahil değildir. Bu yüzden yalnızca dönüş tipi farklı olan iki metot aynı sınıfta bulunamaz:

public class DonusTipi {
    static int f(int a) { return a; }
    static double f(int a) { return a; }
}
DonusTipi.java:3: error: method f(int) is already defined in class DonusTipi
    static double f(int a) { return a; }
                  ^
1 error

Kuralın gerekçesi açıktır. f(5); gibi dönen değeri kullanmayan bir çağrıda derleyici iki metottan hangisini seçeceğini bilemezdi. Parametre adlarının farklı olması da yetmez: topla(int a, int b) ile topla(int x, int y) aynı imzayı taşır.

3.2.6.2Derleyici hangi sürümü seçer?

Argümanların tipleri parametrelerle birebir aynıysa seçim kolaydır. Zor olan, tam eşleşme bulunmadığında ne olduğudur. Derleyici bu durumda argümanları genişleterek uyan metotları arar. Java'daki genişletme dönüşümleri şu sırayı izler:

byte → short → int → long → float → double  ve  char → int

Bir tip, bu zincirde kendisinden sonra gelen her tipe otomatik olarak genişleyebilir. Ters yöndeki daraltma dönüşümleri ise hiçbir zaman otomatik yapılmaz. Seçim üç adımda düşünülebilir:

  1. Parametre sayısı argüman sayısına eşit olmayan metotlar elenir.
  2. Kalan metotlardan, her argümanın ya aynı tipte olduğu ya da genişletmeyle uyduğu metotlar uygun sayılır.
  3. Birden çok uygun metot varsa derleyici en özel olanı seçer. Bir metodun parametre tiplerinin hepsi ötekinin parametre tiplerine genişleyebiliyorsa o metot daha özeldir. (int, int) sürümü (double, double)'dan daha özeldir, çünkü int double'a genişler.

Aşağıdaki program aynı üç metodu dört farklı çağrıyla dener. Çıktısı ve izlemesi aşağıdadır.

static int topla(int a, int b) {
    return a + b;
}
static double topla(double a, double b) {
    return a + b;
}
static int topla(int a, int b, int c) {
    return a + b + c;
}
public static void main(String[] args) {
    System.out.println(topla(4, 6));
    System.out.println(topla(4, 2.5));
    System.out.println(topla(7, 1, 2));
    System.out.println(topla('A', 1));
}
10
6.5
10
66
Tablo 3.2.6: Dört çağrıda seçilen topla sürümü. Çağrı yığını sütunu, seçilen sürümün imzasını gösterir.
AdımSatırÇağrı yığınıDeğişkenlerÇıktı
111: System.out.println(topla(4, 6));main()——
22: return a + b;main() → topla(int, int)a=4, b=6—
311: System.out.println(topla(4, 6));main()—10
412: System.out.println(topla(4, 2.5));main()——
55: return a + b;main() → topla(double, double)a=4.0, b=2.5—
612: System.out.println(topla(4, 2.5));main()—6.5
713: System.out.println(topla(7, 1, 2));main()——
88: return a + b + c;main() → topla(int, int, int)a=7, b=1, c=2—
913: System.out.println(topla(7, 1, 2));main()—10
1014: System.out.println(topla('A', 1));main()——
112: return a + b;main() → topla(int, int)a=65, b=1—
1214: System.out.println(topla('A', 1));main()—66

Birinci çağrı topla(4, 6)'dır; argümanlar (int, int)'tir. Üç parametreli sürüm sayı yüzünden elenir. (int, int) tam eşleşmedir; (double, double) da genişletmeyle uyar. En özel olan (int, int) seçilir ve 10 yazılır. Şekil 3.2.6'nin ilk parçası bu seçimi gösterir.

topla(4, 6)argümanlar: int, inttopla(int, int)✓ tam eşleşmetopla(double, double)✓ genişletmeyle uyartopla(int, int, int)✗ sayı tutmuyor
topla(4, 2.5)argümanlar: int, doubletopla(int, int)✗ 2.5 int olamaztopla(double, double)✓ 4 → 4.0topla(int, int, int)✗ sayı tutmuyor
topla('A', 1)argümanlar: char, inttopla(int, int)✓ 'A' → 65topla(double, double)✓ genişletmeyle uyartopla(int, int, int)✗ sayı tutmuyor
Şekil 3.2.6: Derleyicinin aday metotları değerlendirmesi. Üstte topla(4, 6): (int, int) tam eşleşir. Ortada topla(4, 2.5): 2.5 int'e daralamaz, (int, int) elenir; 4 genişletilir ve (double, double) seçilir. Altta topla('A', 1): char iki sürüme de genişleyebilir; en özel olan (int, int) seçilir.

İkinci çağrı topla(4, 2.5)'tir; argümanlar (int, double)'dır. (int, int) sürümü uymaz, çünkü 2.5 int'e örtük olarak dönüşemez. (double, double) uyar: 4 değeri 4.0'a genişletilir. Metot a = 4.0, b = 2.5 ile çalışır ve 6.5 döner.

Üçüncü çağrı topla(7, 1, 2) üç argüman taşır. Yalnızca üç parametreli sürüm uyar ve 10 döner. Çıktıda ilk ve üçüncü satırın ikisi de 10'dur, ama farklı metotlardan gelmiştir.

Dördüncü çağrı topla('A', 1)'dir; argümanlar (char, int)'tir. char tipi Java'da karakterin sayısal kodunu tutar ve int'e de double'a da genişleyebilir. İki sürüm de uygundur; derleyici en özel olanı, (int, int)'i seçer. 'A' karakterinin kodu 65 olduğu için metot a = 65, b = 1 ile çalışır ve 66 döner. Ekranda bir harf değil, 66 sayısı görülür.

Genişletme zincirinin sonuçları bazen şaşırtıcıdır. Aşağıdaki programda yaz metodunun int, long ve double sürümleri vardır; her sürüm farklı bir harf yazar.

public class Yaz {
    static void yaz(int a) {
        System.out.print("I");
    }
    static void yaz(long a) {
        System.out.print("L");
    }
    static void yaz(double a) {
        System.out.print("D");
    }
    public static void main(String[] args) {
        yaz(5); yaz(5L);
        yaz(5.0f); yaz('A');
        System.out.println();
        byte b = 3; short s = 4;
        yaz(b); yaz(s); yaz(5_000_000_000L);
        System.out.println();
    }
}
ILDI
IIL

yaz(5) tam eşleşmedir: I. yaz(5L)'deki L soneki sayıyı long yapar: L. yaz(5.0f)'deki f soneki sayıyı float yapar. float sürümü yoktur; float zincirde yalnızca double'a genişleyebilir: D. yaz('A')'da char hem int'e hem long'a hem double'a genişleyebilir; en özel olan int'tir: I. İkinci satırda byte ve short argümanlar en yakın sürüme, int'e gider: I I. 5_000_000_000L int aralığını aşan bir long'dur: L. Sayıdaki alt çizgiler yalnızca okunurluk içindir; değeri değiştirmez.

En özel metodun bulunamadığı durumlar da vardır. İki uygun metottan hiçbiri ötekinden daha özel değilse derleyici seçim yapmaz ve çağrıyı reddeder:

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

g(1, 1) çağrısında iki sürüm de uygundur: birincisinde ikinci argüman, ikincisinde birinci argüman double'a genişletilir. (int, double) ile (double, int) arasında hiçbiri ötekine genişleyemez; ikisi de “daha özel” değildir. İletideki ambiguous sözcüğü “belirsiz” demektir. Çağrı g(1, 1.0) biçiminde yazılırsa belirsizlik kalkar. Bu hata, aşırı yüklemede parametre tiplerinin yalnızca sırasını değiştirmenin neden iyi bir fikir olmadığını gösterir.

3.2.7Fonksiyon içinde fonksiyon

Metotlar yalnızca main'den çağrılmaz; bir metot başka bir metodu da çağırabilir. Büyük bir görev bu sayede küçük parçalara bölünür. Her parça kendi işini yapar ve sonucunu bir üst düzeye verir.

3.2.7.1Metotların birbirini çağırması

Aşağıdaki programda iki sayının karelerinin toplamını bulan ikiKareToplami metodu işi kendisi yapmaz. Kare alma işini kareAl'a, toplama işini topla'ya yaptırır.

public class IcIce {
    public static int kareAl(int sayi) {
        return sayi * sayi;
    }
    public static int topla(int sayi1, int sayi2) {
        return sayi1 + sayi2;
    }
    public static int ikiKareToplami(int sayi1, int sayi2) {
        return topla(kareAl(sayi1), kareAl(sayi2));
    }
    public static void main(String[] args) {
        int sonuc = ikiKareToplami(3, 4);
        System.out.println(sonuc);
    }
}
25

ikiKareToplami(3, 4) çağrısının sonucu 3² + 4² = 9 + 16 = 25'tir. Kodun önemli satırı return topla(kareAl(sayi1), kareAl(sayi2));'dir. Bu satırda bir çağrının argümanları başka çağrılardır. Java bu tür ifadeyi içten dışa hesaplar. topla çağrılmadan önce iki argümanının değeri bilinmelidir; bu yüzden önce kareAl(sayi1), sonra kareAl(sayi2) çalışır. İki sonuç hazır olunca topla(9, 16) çağrılır.

Argümanların hesaplanma sırası Java'da kesin olarak tanımlıdır: soldan sağa. Bu kural, argümanlardan biri ekrana yazdığında ya da bir değişkeni değiştirdiğinde fark eder. Aşağıdaki programda deger metodu hem değer döndürür hem de adını ekrana yazar; böylece çağrıların sırası çıktıda görülür.

public class Sira {
    static int deger(String ad, int v) {
        System.out.print(ad + " ");
        return v;
    }
    static int fark(int a, int b) {
        return a - b;
    }
    public static void main(String[] args) {
        int s = fark(deger("sol", 10), deger("sağ", 3));
        System.out.println("→ " + s);
    }
}
sol sağ → 7

Önce sol, sonra sağ yazılır. Ardından fark(10, 3) çalışır ve 7 döner. C gibi bazı diller bu sırayı belirlemez ve derleyiciye bırakır. Java ise sırayı garanti eder.

Metotların birbirini çağırması programı okunur ve yönetilebilir kılar. ikiKareToplami'nın gövdesi ne yaptığını tek satırda söyler: iki karenin toplamı. kareAl ve topla ayrı ayrı sınanabilir. kareAl yanlış çalışıyorsa hata tek bir yerde düzeltilir ve onu kullanan bütün metotlar düzelir.

3.2.7.2Çağrı yığınını izlemek

İç içe çağrılarda çağrı yığını birden çok katlı olur. Tablo 3.2.7 programın çalışmasını adım adım verir.

Tablo 3.2.7: ikiKareToplami(3, 4) çağrısında çağrı yığını. Değişkenler sütunu en üstteki çerçeveyi gösterir; arg1, topla'nın hesaplanmış ilk argümanıdır.
AdımSatırÇağrı yığınıDeğişkenlerÇıktı
112: int sonuc = ikiKareToplami(3, 4);main()sonuc=?—
29: return topla(kareAl(sayi1), kareAl(sayi2));main() → ikiKareToplami(3, 4)sayi1=3, sayi2=4—
32: return sayi * sayi;main() → ikiKareToplami(3, 4) → kareAl(3)sayi=3, dönüş=9—
49: return topla(kareAl(sayi1), kareAl(sayi2));main() → ikiKareToplami(3, 4)sayi1=3, sayi2=4, arg1=9—
52: return sayi * sayi;main() → ikiKareToplami(3, 4) → kareAl(4)sayi=4, dönüş=16—
65: return sayi1 + sayi2;main() → ikiKareToplami(3, 4) → topla(9, 16)sayi1=9, sayi2=16, dönüş=25—
79: return topla(kareAl(sayi1), kareAl(sayi2));main() → ikiKareToplami(3, 4)sayi1=3, sayi2=4, dönüş=25—
812: int sonuc = ikiKareToplami(3, 4);main()sonuc=25—
913: System.out.println(sonuc);main()sonuc=2525

Birinci adımda main, ikiKareToplami(3, 4)'ü çağırır; yığında iki çerçeve olur. İkinci adımda ikiKareToplami, topla'yı çağırmadan önce argümanlarını hesaplamaya başlar. Üçüncü adımda kareAl(3) için üçüncü bir çerçeve açılır; 3 × 3 = 9 döner. Şekil 3.2.7'in ilk parçası bu anı gösterir.

Yığın (stack)kareAl(3)sayi3ikiKareToplami(3, 4)sayi13sayi24main()sonuc? Yığın (stack)topla(9, 16)sayi19sayi216ikiKareToplami(3, 4)sayi13sayi24main()sonuc?
Şekil 3.2.7: Üç katlı çağrı yığını. Solda kareAl(3) çalışıyor; altında ikiKareToplami(3, 4) ve main bekliyor. Sağda topla(9, 16) çalışıyor; kendi sayi1'i 9'dur, alttaki ikiKareToplami'nın sayi1'i ise hâlâ 3'tür.

Dördüncü adımda kareAl'ın çerçevesi kapanır. İlk argüman artık bilinir: 9. Beşinci adımda kareAl(4) için yeni bir çerçeve açılır ve 16 döner. Bu çerçeve, önceki kareAl çerçevesinin yeniden kullanılması değildir; aynı metodun yeni bir çağrısı için açılmış ayrı bir çerçevedir. Her çağrı kendi parametrelerini ve yerel değişkenlerini taşır.

Altıncı adımda argümanlar hazırdır ve topla(9, 16) çağrılır. topla'nın parametreleri de sayi1 ve sayi2 adını taşır. topla'nın çerçevesinde sayi1 = 9'dur; aynı anda yığının bir alt katındaki ikiKareToplami çerçevesinde sayi1 = 3'tür. İki değişken birbirini etkilemez. Yedinci adımda topla'nın döndürdüğü 25, ikiKareToplami'nın da dönüş değeri olur. Sekizinci adımda main'e dönülür ve sonuc = 25 olur. Dokuzuncu adımda 25 yazılır.

İzleme boyunca yığında aynı anda en fazla üç çerçeve bulunmuştur: main, ikiKareToplami ve o an çalışan yardımcı metot. Yığının derinliği, iç içe çağrıların en uzun zincirine eşittir. kareAl ile topla aynı anda yığında hiç bulunmamıştır, çünkü biri bitmeden öteki başlamamıştır.

Yığının sınırsız olmadığını da bilmek gerekir. Her çerçeve bellekte yer kaplar. Bir metot kendini durmadan çağırırsa çerçeveler birikir ve yığın dolar; program StackOverflowError hatasıyla durur. Kendini çağıran metotlara özyinelemeli metot denir; bu tür metotlarda yığının derinliği ayrıca denetlenmelidir.

3.2.8Hazır metotlar

Her programcı her işi baştan yazmaz. Java, binlerce sınıf ve metottan oluşan bir standart kütüphaneyle gelir. Karekök almak, ekrana biçimli yazmak, diziyi sıralamak, dosya okumak için hazır metotlar vardır. Bu metotları kullanmak, kendi yazdığımız metotları kullanmaktan farklı değildir: adları, parametreleri ve dönüş tipleri bilinir, içleri bilinmez.

3.2.8.1Java kütüphanesi ve paketler

Java'nın standart kütüphanesine Java API (Application Programming Interface, uygulama programlama arayüzü) denir. Kütüphanedeki sınıflar paketlere (package) ayrılmıştır. Paket, ilgili sınıfları bir araya toplayan bir ad alanıdır. Tablo 3.2.8 en sık kullanılan sınıfları ve paketlerini listeler.

Tablo 3.2.8: Java API'de en sık kullanılan sınıflar ve metotlar.
AlanSınıflarÖrnek metotlarPaket
Ekrana yazmaSystem.outprint, println, printfjava.lang
MatematikMathsqrt, abs, max, powjava.lang
MetinStringlength, charAt, substring, equalsjava.lang
DiziArrayssort, copyOf, binarySearch, equalsjava.util
Kullanıcı girişiScannernextInt, nextLine, closejava.util
DosyaFile, FileWritercreateNewFile, write, closejava.io
Veri tabanıDriverManager, StatementgetConnection, executeQuery, executeUpdatejava.sql
Veri yapılarıArrayListadd, remove, get, sizejava.util
ZamanLocalDateTime, DateTimeFormatternow, ofPattern, formatjava.time
Web / grafikURI, URL, JFrameURI.create, openConnection, setVisiblejava.net, javax.swing

java.lang paketi özeldir: her Java programına otomatik olarak dahil edilir. System, Math ve String bu pakette bulunduğu için hiçbir hazırlık yapmadan kullanılır. Öteki paketlerdeki sınıflar için dosyanın başına bir içe aktarma (import) bildirimi yazılır: import java.util.Scanner;. Bu bildirim yazılmazsa derleyici Scanner adını tanımaz ve cannot find symbol der.

Tablodaki metotların bir kısmı static'tir ve sınıf adıyla çağrılır: Math.sqrt(16), Arrays.sort(dizi). Bir kısmı ise bir nesne üzerinden çağrılır: giris.nextInt(), metin.length(). Bir metodun hangi biçimde çağrılacağı, Java'nın resmî belgelerinde metodun başlığında yazar.

3.2.8.2Ekrana yazdırma

System.out, standart çıktıyı, yani konsolu temsil eden hazır bir nesnedir. Üç metodu çok sık kullanılır.

public class Yazdirma {
    public static void main(String[] args) {
        System.out.println("Merhaba, Dünya!");
        System.out.print("Bir ");
        System.out.print("satır");
        System.out.println();
        int yas = 20;
        System.out.println("Yaş: " + yas);
        System.out.printf("Pi yaklaşık %.2f%n", Math.PI);
    }
}
Merhaba, Dünya!
Bir satır
Yaş: 20
Pi yaklaşık 3,14
  • System.out.println(x), x'i yazar ve alt satıra geçer. Argümansız println() yalnızca satır sonu yazar.
  • System.out.print(x), x'i yazar ama aynı satırda kalır. Programda print("Bir ") ile print("satır") art arda çalışır, ardından println() satırı bitirir: Bir satır.
  • System.out.printf(biçim, değerler...), biçimli yazar. Biçim metnindeki yer tutucuların yerine değerler konur.

Metin ile değer + işleciyle birleştirilir: "Yaş: " + yas ifadesinin değeri "Yaş: 20" metnidir. + işleci soldan sağa çalıştığı için birleştirmenin sırası sonucu değiştirir. Bu ayrıntı yeni başlayanların sık takıldığı bir noktadır:

import java.util.Locale;

public class Bicim {
    public static void main(String[] args) {
        double ort = 81.66666666666667;
        System.out.printf("%.2f%n", ort);
        System.out.printf(Locale.US, "%.2f%n", ort);
        System.out.printf("%d öğrenci, ortalama %.1f, not %s%n", 3, ort, "BB");
        System.out.printf("[%5d] [%-5d] [%05d]%n", 42, 42, 42);
        System.out.println(1 + 2 + " toplam");
        System.out.println("toplam " + 1 + 2);
        System.out.println("toplam " + (1 + 2));
        System.out.println(String.format("%.3f", Math.PI));
    }
}
81,67
81.67
3 öğrenci, ortalama 81,7, not BB
[   42] [42   ] [00042]
3 toplam
toplam 12
toplam 3
3,142

1 + 2 + " toplam" ifadesinde önce 1 + 2 iki sayının toplamıdır: 3. Sonra 3 metne eklenir: 3 toplam. "toplam " + 1 + 2 ifadesinde ise ilk işlem bir metinle bir sayının birleştirilmesidir: "toplam 1". Sonuç artık metin olduğu için 2 de metne eklenir: toplam 12. Toplamın sayı olarak hesaplanması için parantez gerekir: "toplam " + (1 + 2).

printf'teki en sık kullanılan yer tutucular Tablo 3.2.9'te verilmiştir.

Tablo 3.2.9: printf yer tutucuları (Türkçe yerel ayarla çalıştırıldı).
Yer tutucuAnlamıÖrnekÇıktı
%dtam sayıprintf("%d", 42)42
%.2fondalık sayı, 2 basamakprintf("%.2f", Math.PI)3,14
%smetinprintf("%s", "BB")BB
%nsatır sonuprintf("a%nb")iki satır
%5d / %-5d5 karakterlik alana sağa / sola yaslıprintf("[%5d]", 42)[ 42]
%05dboşluk yerine sıfırla doldurprintf("[%05d]", 42)[00042]

printf, ondalık sayıyı yazarken bilgisayarın yerel ayarını (locale) kullanır. Türkçe ayarlı bir sistemde ondalık ayırıcı virgüldür; bu yüzden Math.PI 3,14 olarak yazılır. println ise yerel ayara bakmaz ve her zaman nokta kullanır: 3.141592653589793. Bir program aynı sayıyı iki yöntemle yazarsa iki farklı biçim görülür. Ondalık ayırıcının her sistemde nokta olması isteniyorsa yerel ayar açıkça verilir: printf(Locale.US, "%.2f%n", ort) çıktısı 81.67'dir. Locale sınıfı java.util paketindedir ve içe aktarılması gerekir.

%.2f yer tutucusu sayıyı keserek değil yuvarlayarak yazar: 81,666… değeri 81,67 olur. String.format da printf ile aynı biçim kurallarını kullanır, ama sonucu ekrana yazmak yerine metin olarak döndürür. Son satırdaki 3,142 bu metottan gelir.

3.2.8.3Math sınıfı

Math sınıfı matematiksel işlemler için hazır metotlar içerir. Metotlarının hepsi static'tir; bu yüzden bir Math nesnesi oluşturulmaz, metotlar sınıf adıyla çağrılır.

double kareKok = Math.sqrt(16);
double mutlakDeger = Math.abs(-17.2);
double maksimum = Math.max(3, 5);
System.out.println(kareKok);
System.out.println(mutlakDeger);
System.out.println(maksimum);
System.out.println(Math.pow(2, 10));
System.out.println(Math.round(2.6));
System.out.println(Math.min(-4, 7));
System.out.println(Math.max(3, 5));
4.0
17.2
5.0
1024.0
3
-4
5

Çıktıdaki her satırın biçimi, metodun dönüş tipini ele verir. Tablo 3.2.10 bu metotları dönüş tipleriyle birlikte listeler.

Tablo 3.2.10: Sık kullanılan Math metotları ve dönüş tipleri.
MetotGöreviDönüş tipiÖrnek → sonuç
sqrt(x)karekökher zaman doubleMath.sqrt(16) → 4.0
pow(a, b)a üzeri bher zaman doubleMath.pow(2, 10) → 1024.0
abs(x)mutlak değerargümanın tipiMath.abs(-17.2) → 17.2
max(a, b), min(a, b)büyük / küçük olanargümanların ortak tipiMath.max(3, 5) → 5
round(x)en yakın tam sayıya yuvarlamadouble için longMath.round(2.6) → 3
floor(x), ceil(x)aşağı / yukarı yuvarlamadoubleMath.floor(-2.5) → -3.0
PIπ sabiti (metot değil)doubleMath.PI → 3.141592653589793

abs, max ve min aşırı yüklenmiştir; argümanın tipine göre farklı sürümleri çalışır. Math.max(3, 5) iki int alır ve int döndürür: 5. Aynı çağrının sonucu programda bir double değişkene atanınca 5.0'a genişletilir ve 5.0 yazılır. sqrt ve pow'un ise yalnızca double sürümü vardır; tam sayı argüman verilse de sonuç double'dır. Bu yüzden sonucu doğrudan bir int değişkene atamak derleme hatasıdır:

public class SqrtInt {
    public static void main(String[] args) {
        int k = Math.sqrt(16);
    }
}
SqrtInt.java:3: error: incompatible types: possible lossy conversion from double to int
        int k = Math.sqrt(16);
                         ^
1 error

Sonuç tam sayı olarak gerekiyorsa açık dönüşüm yazılır: int k = (int) Math.sqrt(16);. (int) dönüşümü ondalık kısmı keser, yuvarlamaz. Dönüş tiplerinin birlikte kullanıldığı ifadelerde sonuç dikkatle izlenmelidir. Aşağıdaki program birkaç ek örnek verir.

public class MathEk {
    public static void main(String[] args) {
        System.out.println(Math.round(2.5) + " " + Math.round(-2.5) + " " + Math.round(-2.6));
        System.out.println(Math.floor(-2.5) + " " + Math.ceil(-2.5));
        System.out.println(Math.sqrt(-4));
        System.out.println((int) Math.pow(2, 10));
        System.out.println(Math.max(Math.abs(-8), Math.sqrt(49)));
        System.out.println(Math.sqrt(16) + Math.max(3, 7) / 2);
        System.out.println(Math.PI);
        System.out.println(Math.hypot(3, 4));
        long r = Math.round(2.6);
        System.out.println(r);
    }
}
3 -2 -3
-3.0 -2.0
NaN
1024
8.0
7.0
3.141592653589793
5.0
3

İlk satır Math.round'un davranışını gösterir. Metot sayıya 0,5 ekleyip aşağı yuvarlar. 2,5 için sonuç 3'tür; −2,5 için −2,5 + 0,5 = −2 bulunur. Yani yarımlar her zaman artı sonsuza doğru yuvarlanır; −2,6 ise −3 olur. İkinci satırda floor −2,5'i aşağıya, −3,0'a; ceil yukarıya, −2,0'a yuvarlar. Negatif bir sayının karekökü gerçel sayılarda tanımsızdır. Math.sqrt(-4) hata vermez; özel NaN (Not a Number, sayı değil) değerini döndürür.

Beşinci satırdaki Math.max(Math.abs(-8), Math.sqrt(49)) ifadesinde iç çağrılar önce çalışır. Math.abs(-8) bir int verir: 8. Math.sqrt(49) bir double verir: 7.0. max'ın argümanları (int, double) olduğu için aşırı yükleme kuralı devreye girer ve max(double, double) sürümü seçilir. Sonuç 8.0'dır, 8 değil.

Altıncı satırdaki Math.sqrt(16) + Math.max(3, 7) / 2 ifadesinde işlem önceliği önemlidir. Bölme toplamadan önce yapılır. Math.max(3, 7) int döndürür: 7. 7 / 2 tam sayı bölmesidir: 3. Sonra 4.0 + 3 hesaplanır ve sonuç 7.0 olur. Math.hypot(3, 4) dik kenarları 3 ve 4 olan üçgenin hipotenüsünü verir: 5.0.

3.2.9Kapsam

Bir değişken programın her yerinden kullanılamaz. Nerede tanımlandığı, nerede görülebileceğini ve ne kadar süre yaşayacağını belirler. Bu kurallar metotların birbirinden bağımsız çalışmasını sağlar: bir metodun içindeki değişken adları, öteki metotlardaki adlarla karışmaz.

3.2.9.1Yerel değişken

Tanım 3.2.9

Yerel değişken (local variable), bir metodun ya da bir bloğun içinde tanımlanan, yalnızca orada geçerli olan değişkendir. Metodun parametreleri de yerel değişkendir. Yerel değişken yalnızca metot çalışırken var olur; metot dönünce silinir.

Aşağıdaki kodda carpma metodu sonuc adlı bir yerel değişken kullanır. baskaMetot, carpma'yı çağırdıktan sonra bu değişkene ulaşmaya çalışır.

public int carpma(int sayi1, int sayi2) {
    int sonuc = sayi1 * sayi2; // 'sonuc' bir yerel değişkendir
    return sonuc;
}
public void baskaMetot() {
    carpma(2, 3);
    System.out.println(sonuc);  // HATA
}
YerelDisari.java:8: error: cannot find symbol
        System.out.println(sonuc);  // HATA
                           ^
  symbol:   variable sonuc
  location: class YerelDisari
1 error

Derleyici sonuc adını baskaMetot'un içinde bulamaz. sonuc, carpma'ya aittir ve yalnızca carpma çalışırken vardır. Şekil 3.2.8 bunu bellek üzerinde gösterir: carpma(2, 3) çalışırken çerçevesinde sayi1, sayi2 ve sonuc bulunur. Metot dönünce çerçeve ve içindeki değişkenler silinir.

carpma(2, 3) çalışırkencarpmasayi1=2 sayi2=3sonuc=6baskaMetotdönüşten sonrasilindibaskaMetot
Şekil 3.2.8: Yerel değişkenin ömrü. Solda carpma(2, 3) çalışırken yığının üstünde onun çerçevesi vardır: sayi1 = 2, sayi2 = 3, sonuc = 6. Sağda metot dönmüştür; çerçeve silinmiş, altta yalnızca baskaMetot kalmıştır.

Değeri metodun dışına taşımanın yolu yine return'dür. baskaMetot içinde int s = carpma(2, 3); yazılırsa dönen 6, baskaMetot'un kendi yerel değişkeni olan s'ye atanır. Derleme hatası veren kodda carpma(2, 3); satırı ise sonucu hiçbir yere atamadığı için 6 kaybolur.

Yerel değişkenlerin bir kuralı daha vardır: kullanılmadan önce değer almaları gerekir. Sınıf alanlarının ve dizi öğelerinin varsayılan değeri (0, false, null) vardır; yerel değişkenlerin yoktur. Değer verilmemiş bir yerel değişkeni okumak derleme hatasıdır:

public class DegersizYerel {
    public static void main(String[] args) {
        int t;
        System.out.print(t);
    }
}
DegersizYerel.java:4: error: variable t might not have been initialized
        System.out.print(t);
                         ^
1 error

3.2.9.2Kapsam kuralları ve iç içe bloklar

Tanım 3.2.10

Kapsam (scope), bir değişkenin adının programda geçerli olduğu bölgedir. Java'da yerel bir değişkenin kapsamı, tanımlandığı satırdan başlar ve tanımlandığı süslü parantez bloğunun sonunda biter.

Kapsam iki şeyi belirler: değişkene nereden erişilebileceğini ve değişkenin ne kadar yaşayacağını. Java'da bloklar iç içe olabilir. Sınıfın gövdesi en dıştaki bloktur. Metotlar onun içindedir. Metotların içinde de if, döngü ya da tek başına yazılmış süslü parantez blokları bulunabilir. Şekil 3.2.9 bu katmanları gösterir.

class Ornek (sınıf kapsamı) alan: globalDegisken void fonksiyon() (metot kapsamı) disDegisken { ... } (blok kapsamı) icDegisken görür: icDegisken, disDegisken, globalDegisken
Şekil 3.2.9: İç içe kapsamlar. En dışta sınıf kapsamı ve onun alanı globalDegisken; içinde metot kapsamı ve disDegisken; en içte blok kapsamı ve icDegisken. En içteki blok, dıştaki bütün değişkenleri görür.

Kural tek yönlüdür: iç kapsam, dış kapsamdaki değişkenleri görür; dış kapsam, içteki değişkenleri göremez. Aşağıdaki programda iç blok hem metodun hem sınıfın değişkenlerine erişir.

public class IcKapsam {
    static int globalDegisken = 10; // global (sınıf alanı)
    static void fonksiyon() {
        int disDegisken = 10;       // yerel kapsam
        {
            int icDegisken = 5;
            // disDegisken ve icDegisken burada erişilebilir
            System.out.println(disDegisken + icDegisken
                    + globalDegisken);
        }
        // icDegisken burada YOK
    }
    public static void main(String[] args) {
        fonksiyon();
    }
}
25

İç bloktaki println üç değişkeni de görür: icDegisken kendi bloğunda, disDegisken bir dış blokta, globalDegisken sınıf kapsamındadır. Toplam 10 + 5 + 10 = 25'tir. Bloğun kapanan süslü parantezinden sonra icDegisken artık yoktur. Orada icDegisken kullanılırsa derleyici cannot find symbol der.

Sınıf gövdesinde, metotların dışında tanımlanan globalDegisken bir alandır (field). Sınıfın bütün metotları bu alanı görür. Programlama dillerinde “global değişken” diye anılan, her yerden erişilen ve program boyunca yaşayan değişkenlerin Java'daki karşılığı static alanlardır. Java'da sınıf dışında değişken tanımlanamaz; gerçek anlamda global bir değişken yoktur.

Tablo 3.2.11: Kapsam türleri.
KapsamNerede tanımlanır?Nereden erişilir?Ne kadar yaşar?
Yerel (blok)Bir bloğun içindeTanımlandığı satırdan bloğun sonuna kadarBlok çalıştığı sürece
Yerel (metot)Metot gövdesinde ya da parametre listesindeMetodun içindenMetot çalıştığı sürece
static alanSınıf gövdesinde, metotların dışındaSınıfın bütün metotlarındanProgram boyunca

Döngü değişkenleri de bu kurala uyar. for (int i = 0; ...) başlığında tanımlanan i'nin kapsamı döngünün kendisidir. Döngü bittikten sonra i'yi kullanmak derleme hatasıdır:

public class DonguDisi {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) { }
        System.out.print(i);
    }
}
DonguDisi.java:4: error: cannot find symbol
        System.out.print(i);
                         ^
  symbol:   variable i
  location: class DonguDisi
1 error

Kapsamın sınırlı olmasının bir yararı da aynı adın birbirini görmeyen bloklarda yeniden kullanılabilmesidir. İki kardeş blok, yani biri bitince öteki başlayan bloklar, aynı adlı değişkenleri birbirine karışmadan tanımlayabilir. Art arda yazılan iki for döngüsünün ikisinde de i kullanmak bu yüzden geçerlidir:

public class KardesBlok {
    public static void main(String[] args) {
        { int t = 1; System.out.print(t + " "); }
        { int t = 2; System.out.println(t); }
        for (int i = 0; i < 2; i++) { int kare = i * i; System.out.print(kare + " "); }
        for (int i = 5; i < 7; i++) { System.out.print(i + " "); }
        System.out.println();
    }
}
1 2
0 1 5 6 

3.2.9.3İsim çakışması ve gölgeleme

Aynı adın farklı kapsamlarda kullanılması iki ayrı sonuç doğurur. Java bunlardan birini yasaklar, ötekine izin verir.

Yasak olan durum: bir iç blok, dıştaki yerel bir değişkenle aynı adı kullanamaz. İki değişken aynı anda yaşayacağı ve aynı adı taşıyacağı için hangisinin kastedildiği belirsiz olurdu. Java bu belirsizliğe izin vermez:

public void fonksiyon() {
    int disDegisken = 10;
    {
        int disDegisken = 5;  // HATA
    }
}
AyniAd.java:5: error: variable disDegisken is already defined in method fonksiyon()
            int disDegisken = 5;  // HATA
                ^
1 error

Aynı kural parametreler için de geçerlidir, çünkü parametreler de yerel değişkendir. Metodun gövdesinde parametreyle aynı adlı bir değişken tanımlanamaz:

public class ParametreAyniAd {
    static int f(int x) {
        int x = 3;
        return x;
    }
}
ParametreAyniAd.java:3: error: variable x is already defined in method f(int)
        int x = 3;
            ^
1 error

İzin verilen durum: bir yerel değişken, sınıfın bir alanıyla aynı adı taşıyabilir. Bu durumda yerel değişken alanı gölgeler.

Tanım 3.2.11

Gölgeleme (shadowing), iç kapsamdaki bir adın dış kapsamdaki aynı adı gizlemesidir. Yerel değişken bir alanı gölgelediğinde, o kapsam içinde ad yerel değişkeni gösterir. Gölgelenen static alana sınıf adıyla ulaşılır: SinifAdi.alan.

public class Golge {
    static int deger = 10;  // sınıf alanı
    static void goster() {
        int deger = 5;      // alanı gölgeler
        System.out.println(deger);
        System.out.println(Golge.deger);
    }
    public static void main(String[] args) {
        goster();
        System.out.println(deger);
    }
}
5
10
10

Java bir adı görünce önce en içteki kapsama bakar, bulamazsa bir dış kapsama çıkar. goster'in içinde deger adı yerel değişkende bulunur ve 5 yazılır. Golge.deger yazımı ise aramayı doğrudan sınıfın alanına yönlendirir ve 10 yazılır. main'de deger adlı bir yerel değişken yoktur; arama sınıf kapsamına çıkar ve alan bulunur: 10. Tablo 3.2.12 bu adımları izler.

Tablo 3.2.12: Gölgelemenin izlenmesi. Golge.deger sınıf alanını, “deger (yerel)” goster'in yerel değişkenini gösterir.
AdımSatırÇağrı yığınıDeğişkenlerÇıktı
19: goster();main()Golge.deger=10—
23: static void goster() {main() → goster()Golge.deger=10—
34: int deger = 5; // alanı gölgelermain() → goster()Golge.deger=10, deger (yerel)=5—
45: System.out.println(deger);main() → goster()Golge.deger=10, deger (yerel)=55
56: System.out.println(Golge.deger);main() → goster()Golge.deger=10, deger (yerel)=510
610: System.out.println(deger);main()Golge.deger=1010
sınıf alanıGolge.deger = 10maingosterdeger = 5
Şekil 3.2.10: Gölgeleme anı. Sınıf alanı Golge.deger = 10 yığının dışında, program boyunca yaşar. goster'in çerçevesinde aynı adlı yerel deger = 5 vardır; metodun içinde deger adı bu yerel değişkeni gösterir.

Gölgeleme derleme hatası vermez, ama sinsi hatalara yol açabilir. Programcı alanı değiştirdiğini sanırken yerel bir kopyayı değiştiriyor olabilir. Gölgelemenin bilinçli kullanıldığı yaygın bir yer de vardır: nesnelerle çalışırken yapıcı ve set metotlarında parametre, alanla aynı adı taşır ve alana this.alan ile ulaşılır.

3.2.9.4Yaşam süresi

Kapsam bir değişkenin nereden görüldüğünü, yaşam süresi ne zaman var olduğunu anlatır. Yerel değişkenler bloklarına girildiğinde doğar, bloktan çıkıldığında ölür. static alanlar program başlarken bir kez oluşur ve program bitene kadar yaşar. Aşağıdaki program iki tür değişkeni bir arada kullanır.

static int cagriSayisi = 0;  // sınıf alanı
static int carpma(int sayi1, int sayi2) {
  cagriSayisi++;
  int sonuc = sayi1 * sayi2;
  return sonuc;
}
public static void main(String[] args) {
  int ilk = carpma(2, 3);
  {
    int ikinci = carpma(ilk, 4);
    System.out.println(ikinci);
  }
  System.out.println(ilk + " " + cagriSayisi);
}
24
6 2
Tablo 3.2.13: Değişkenlerin doğuşu ve ölümü. cagriSayisi sütunu sınıf alanının o anki değerini gösterir.
AdımSatırÇağrı yığınıDeğişkenlercagriSayisiÇıktı
18: int ilk = carpma(2, 3);main()ilk=?0—
23: cagriSayisi++;main() → carpma(2, 3)sayi1=2, sayi2=31—
34: int sonuc = sayi1 * sayi2;main() → carpma(2, 3)sayi1=2, sayi2=3, sonuc=61—
48: int ilk = carpma(2, 3);main()ilk=61—
54: int sonuc = sayi1 * sayi2;main() → carpma(6, 4)sayi1=6, sayi2=4, sonuc=242—
610: int ikinci = carpma(ilk, 4);main()ilk=6, ikinci=242—
711: System.out.println(ikinci);main()ilk=6, ikinci=24224
813: System.out.println(ilk + " " + cagriSayisi);main()ilk=626 2

Program başlarken cagriSayisi alanı oluşur ve 0 değerini alır. Birinci adımda carpma(2, 3) çağrılır. İkinci adımda metot sınıf alanını 1 artırır. Metotlar sınıf alanına erişebilir, çünkü alan sınıf kapsamındadır. Üçüncü adımda yerel sonuc doğar ve 6 olur. Dördüncü adımda metot döner; sayi1, sayi2 ve sonuc yok olur. Dönen 6, main'in yerel değişkeni ilk'e atanır.

Beşinci adımda iç bloğun içinden carpma(ilk, 4) çağrılır. Yeni çerçevede sonuc = 24 olur ve alan 2'ye çıkar. Bu sonuc, ilk çağrıdaki sonuc değildir; ilk çağrının sonuc'u çoktan silinmiştir. Altıncı adımda ikinci doğar. ikinci iç blokta tanımlandığı için yalnızca o blokta yaşar. Şekil 3.2.11 bu anı gösterir.

Bellekmain()ilk6ikinci24static alanlarcagriSayisi2 Bellekmain()ilk6static alanlarcagriSayisi2
Şekil 3.2.11: Yaşam süresi. Solda iç blok çalışıyor: main'in çerçevesinde ilk = 6 ve ikinci = 24; sınıf alanı cagriSayisi = 2 ayrı bir bölgede. Sağda blok bitmiş: ikinci yok olmuş, ilk ve alan yaşamaya devam ediyor.

Yedinci adımda blok içinde ikinci yazılır: 24. Sekizinci adımda blok bitmiştir ve ikinci artık yoktur. ilk ise main bitene kadar yaşar. Son satır 6 2 yazar: ilk 6'dır ve carpma iki kez çağrılmıştır.

Bu örnekte sınıf alanının bir “sayaç” olarak kullanılması dikkat çekicidir. Yerel bir değişken bu işi yapamazdı: carpma'nın içinde tanımlanan bir sayaç her çağrıda yeniden doğar ve 0'dan başlar. Çağrılar arasında saklanması gereken bir bilgi, yerel değişkende değil alanda tutulur. Yine de alanlar ölçülü kullanılmalıdır. Her metodun değiştirebildiği bir alan, programın davranışını izlemeyi zorlaştırır.

3.2.10Soyutlama ve tasarım

Bir metodu derlenir ve doğru çalışır hâle getirmek işin yarısıdır. Öteki yarısı, metodun iyi tasarlanmasıdır: ne kadar iş yapacağı, hangi girdileri alacağı, adının ne olacağı. İyi tasarlanmış metotlar başka programlarda yeniden kullanılır, kolay sınanır ve yıllar sonra bile anlaşılır. Bu kesim tasarımın dayandığı fikirleri, yani soyutlamayı, genellemeyi ve problemi parçalamayı, ardından pratik ilkeleri anlatır.

3.2.10.1Soyutlama ve kara kutu

Tanım 3.2.12

Soyutlama (abstraction), karmaşıklığı azaltmak için ayrıntıları gizleyip yalnızca önemli özellikleri göstermektir.

Günlük hayatta soyutlama her yerdedir. Araba sürmek için motorun içini bilmek gerekmez. Direksiyon, pedallar ve vites kolu, sürücünün arabayla konuştuğu bir arayüzdür. Motor değişse bile, örneğin benzinli motor yerine elektrik motoru konsa bile, sürücü aynı direksiyonu ve aynı pedalları kullanır.

Metotlar da bir tür soyutlamadır. Şekil 3.2.12'de Math.sqrt bir kara kutu olarak çizilmiştir. Kullanıcı kutuya 16 verir ve 4.0 alır. Kutunun içinde karekökün hangi yöntemle hesaplandığı gizlidir ve kullanıcıyı ilgilendirmez.

16 Math.sqrt nasıl hesaplıyor? (gizli ayrıntı) 4.0 ne verdim? ne aldım?
Şekil 3.2.12: Kara kutu. Kullanıcı yalnızca girdiyi (“ne verdim?” 16) ve çıktıyı (“ne aldım?” 4.0) bilir. Math.sqrt'ın sonucu nasıl hesapladığı gizli bir ayrıntıdır.

Soyutlamanın iki kazancı vardır. Birincisi, programı anlaşılır kılar. Büyük bir program, her biri ayrı ayrı düşünülebilen parçalara ayrılır; bir parçayı kullanan, onun içini bilmek zorunda kalmaz. İkincisi, değişikliği kolaylaştırır. Bir metodun içi değiştiğinde, metodun ne yaptığı aynı kaldığı sürece onu kullanan kod etkilenmez.

Hesaplamalar üzerindeki bu tür soyutlamaya işlevsel soyutlama ya da yordamsal soyutlama (procedural abstraction) denir. Temel fikir, bir fonksiyonun nasıl hesapladığından çok ne hesapladığının önemli olmasıdır. Kullanıcı yalnızca metodun imzasını ve görevini bilir:

// Kullanan yalnızca imzayı bilir:
static boolean asalMi(int sayi)
// "sayi asal ise true döner"
if (asalMi(17)) { ... }
17asalMigövde gizlitruekullanıcı yalnızca girdiyi ve çıktıyı bilir
Şekil 3.2.13: asalMi bir kara kutu olarak. 17 girer, true çıkar. Gövde gizlidir; kullanıcı yalnızca girdiyi ve çıktıyı bilir.

asalMi(17) çağrısını yazan programcının bilmesi gereken tek şey, metodun bir sayının asal olup olmadığını boolean olarak döndürdüğüdür. Metot sayıyı 2'den başlayarak bütün sayılara mı bölüyor, yoksa yalnızca kareköke kadar mı deniyor? Bu soru metodu kullanan için önemsizdir. Yarın daha hızlı bir algoritma yazılırsa yalnızca asalMi'nin gövdesi değişir; if (asalMi(17)) satırı aynen kalır.

En basit örnek yine System.out.println("Merhaba")'dır. Bu metot verilen metni konsola yazar. Karakterlerin hangi kodlamayla gönderildiği, işletim sisteminin hangi çağrısının kullanıldığı gibi ayrıntılar programcıdan gizlidir. Soyutlama olmasaydı, ekrana tek bir satır yazmak için bile bu ayrıntıların hepsini bilmek gerekirdi.

Soyutlamanın işlemesi için metodun görevi açık olmalıdır. Kullanıcı gövdeye bakmayacağına göre metodun adı, parametreleri ve dönüş tipi onun ne yaptığını eksiksiz anlatmalıdır. asalMi(int sayi) başlığı bunu yapar. islem2(int a) başlığı ise hiçbir şey anlatmaz; kullanıcı gövdeyi okumak zorunda kalır ve soyutlama çöker.

3.2.10.2Genelleme

Bir kod parçasının anlamı çevresine bağlıdır. Bir sayının değeri denklemdeki öteki terimlerle, bir kelimenin anlamı cümledeki yeriyle ilgilidir. Kodda da bir deyimin ne yaptığı, içinde bulunduğu blokta hangi değişkenlerin ne değer taşıdığına bağlıdır. Bu çevreye bağlam (context) denir.

Tanım 3.2.13

Genelleme (generalization), bir bağlama ait çözümü başka bağlamlarda da kullanılabilir hâle getirmektir. Kodda genellemenin aracı parametredir: çözümün içine gömülü sabit değerler parametreye dönüştürülür.

public class Genelleme {
    // Özel: yalnızca 10 tane '-' yazar
    static void cizgiCiz() {
        System.out.println("----------");
    }
    // Genel: uzunluğu ve karakteri parametre
    static void cizgiCiz(int uzunluk, char karakter) {
        for (int i = 0; i < uzunluk; i++) {
            System.out.print(karakter);
        }
        System.out.println();
    }
    public static void main(String[] args) {
        cizgiCiz();
        cizgiCiz(5, '*');
        cizgiCiz(12, '=');
    }
}
----------
*****
============

Parametresiz cizgiCiz yalnızca tek bir iş yapar: on tane tire yazar. Uzunluk ve karakter metodun içine gömülüdür. Beş yıldızlı bir çizgi gerektiğinde bu metot işe yaramaz; yeni bir metot yazmak gerekir. İki parametreli sürüm ise aynı işi genelleştirir. Gömülü 10 sayısı uzunluk parametresine, gömülü '-' karakteri karakter parametresine dönüşmüştür. Artık tek bir metot her uzunlukta ve her karakterle çizgi çizer: cizgiCiz(5, '*') beş yıldız, cizgiCiz(12, '=') on iki eşittir işareti yazar.

Örnekteki iki metodun adı aynıdır; bu bir aşırı yükleme örneğidir. Özel sürüm genel sürümle de yazılabilirdi: gövdesi tek satırlık cizgiCiz(10, '-'); çağrısı olurdu. Böylece çizgi çizme mantığı tek bir yerde kalır.

Genellemenin bir sınırı vardır. Her sabiti parametreye dönüştürmek metodu kullanışsız yapar. On parametreli bir metodu çağırmak, işi elle yapmaktan daha zor olabilir. Genelleme, gerçekten değişmesi beklenen değerlerle sınırlı tutulur.

3.2.10.3Problemi parçalama

Tanım 3.2.14

Problemi parçalama (decomposition), büyük bir problemi küçük ve birbirinden bağımsız alt problemlere bölmektir. Programlamada her alt problem bir metot olur.

Bir öğrencinin üç notunu okuyup ortalamasını ve harf notunu yazan bir program düşünelim. Problem üç alt göreve ayrılır: notları okumak, ortalamayı hesaplamak ve ortalamaya göre harf notunu bulmak. Şekil 3.2.14 bu yapıyı gösterir.

main (not programı) notlariOkugirdi ortalamaHesaplaişlem harfNotukarar
Şekil 3.2.14: Not programının parçaları. main üç alt metodu çağırır: notlariOku (girdi), ortalamaHesapla (işlem) ve harfNotu (karar).

Aşağıdaki programda dört metodun hepsi tam olarak yazılmıştır. harfNotu'ndaki eşikler yaygın bir harf notu tablosuna göre seçilmiştir; farklı bir tablo yalnızca bu metodun değişmesini gerektirir.

import java.util.Scanner;

public class NotProgrami {
    // notlariOku ve harfNotu da ayrı metotlardır
    static int[] notlariOku(Scanner giris, int adet) {
        int[] notlar = new int[adet];
        for (int i = 0; i < adet; i++) {
            notlar[i] = giris.nextInt();
        }
        return notlar;
    }
    static double ortalamaHesapla(int[] notlar) {
        int toplam = 0;
        for (int n : notlar) {
            toplam += n;
        }
        return (double) toplam / notlar.length;
    }
    static String harfNotu(double ort) {
        if (ort >= 90) return "AA";
        if (ort >= 85) return "BA";
        if (ort >= 80) return "BB";
        if (ort >= 75) return "CB";
        if (ort >= 70) return "CC";
        if (ort >= 60) return "DC";
        if (ort >= 50) return "DD";
        return "FF";
    }
    public static void main(String[] args) {
        Scanner giris = new Scanner(System.in);
        int[] notlar = notlariOku(giris, 3);
        double ort = ortalamaHesapla(notlar);
        System.out.println("Ortalama: " + ort + " -> " + harfNotu(ort));
    }
}

Program iki farklı girdiyle çalıştırıldı:

Ortalama: 81.66666666666667 -> BB
Ortalama: 52.333333333333336 -> DD

Her metot tek bir iş yapar. notlariOku, Scanner nesnesini ve okunacak not sayısını parametre olarak alır, okuduğu notları bir dizi olarak döndürür. Scanner nesnesi main'de bir kez oluşturulur ve metoda verilir; böylece okuma metodu girdinin nereden geldiğini bilmek zorunda kalmaz. ortalamaHesapla diziyi alır ve ortalamayı döndürür. (double) toplam / notlar.length ifadesindeki açık dönüşüm, tam sayı bölmesini önler. 80 + 90 + 75 = 245'tir; 245 / 3 tam sayı bölmesiyle 81 olurdu, ondalıklı bölmeyle 81,666… olur. harfNotu ortalamaya bakar ve bir String döndürür. Erken return kalıbı burada bir else if zincirinin yerini tutar: koşullar yukarıdan aşağıya denenir ve ilk sağlanan koşulda metot biter.

main artık bir “içindekiler” sayfası gibi okunur: oku, hesapla, yazdır. Ne yapıldığı main'de görünür; nasıl yapıldığı alt metotlardadır. Bir hata çıktığında hangi metoda bakılacağı da bellidir. Ortalama yanlışsa ortalamaHesapla'ya, harf notu yanlışsa harfNotu'na bakılır.

Parçalamanın bir başka kazancı da metotların ayrı ayrı sınanabilmesidir. harfNotu klavyeye hiç dokunmadan sınanabilir: harfNotu(85) “BA”, harfNotu(84.99) “BB” döndürmelidir. Okuma ile hesaplama iç içe yazılmış olsaydı her deneme için klavyeden not girmek gerekirdi.

3.2.10.4Ne zaman fonksiyon? DRY ilkesi

Bir kod parçasının metot olması gerektiğini anlamanın en açık işareti tekrardır. Bu ilkenin adı DRY'dır (Don't Repeat Yourself, kendini tekrar etme). Kodu kopyalayıp yapıştırma ihtiyacı doğduğu an, o kod bir metot olmaya adaydır.

Aşağıdaki ilk parça tekrarlı, ikincisi metotlu sürümdür. İkisi de iki öğrencinin ortalamasını yazar.

double ort1 = (70 + 80 + 90) / 3.0;
System.out.println(ort1);
double ort2 = (55 + 60 + 100) / 3.0;
System.out.println(ort2);
// formül değişirse her yerde
// ayrı ayrı düzeltmek gerekir
static double ortalama(int a, int b, int c) {
    return (a + b + c) / 3.0;
}
System.out.println(ortalama(70, 80, 90));
System.out.println(ortalama(55, 60, 100));
80.0
71.66666666666667
80.0
71.66666666666667

Tekrarlı sürümde ortalama formülü iki kez yazılmıştır. Formül değişirse, örneğin notlar farklı ağırlık alacak olursa, iki yerin de ayrı ayrı düzeltilmesi gerekir. Gerçek programlarda aynı formül onlarca yerde bulunabilir ve birinin unutulması kolaydır. Metotlu sürümde formül tek bir yerdedir: ortalama metodunun gövdesi. Değişiklik bir kez yapılır ve bütün çağrılara yansır.

Çıktıdaki 71.66666666666667 değeri double'ın sınırlı duyarlılığından kaynaklanır. 215 / 3 = 71,666… sonsuz bir ondalık açılımdır. double bunu yaklaşık 16 anlamlı basamakla saklar ve son basamak yuvarlanır. Kullanıcıya gösterilecek bir sonuçta printf("%.2f", ...) ile basamak sayısı sınırlanır.

Tekrar tek ölçüt değildir. Bir kez kullanılan bir kod parçası da metot olmayı hak edebilir. Bunun iki nedeni vardır: parça kendi başına anlamlı bir iş yapıyordur ya da onu bir ad altında toplamak çağıran kodu okunur yapıyordur. Not programındaki harfNotu yalnızca bir kez çağrılır, ama ayrı bir metot olması main'i sadeleştirir ve sınamayı kolaylaştırır.

3.2.10.5İyi metot tasarımı

İyi bir metodun özellikleri altı ilkede toplanabilir.

Tek sorumluluk
Her metot yalnızca bir iş yapar. Metodun görevi “ve” bağlacıyla anlatılıyorsa (oku ve hesapla ve yazdır) metot bölünmelidir.
Açıklayıcı ad
Metodun adı ne yaptığını anlatan bir fiildir: ortalamaHesapla, asalMi, notlariOku. islem2 gibi adlar bir şey anlatmaz.
Parametreler
Metot ihtiyaç duyduğu verileri parametrelerle alır; gizlice dış değişkenlere bağlı olmaz. Az sayıda parametre okunurluğu artırır.
Dönüş değeri
Sonuç açıkça return ile döndürülür. Hesaplayan metot yazdırmaz; yazdırma işini çağıran yapar.
Kısa ve odaklı
Bir ekrana sığan, birkaç satırlık metotlar kolay anlaşılır ve kolay sınanır.
Yerinde çağrı
Metot yalnızca gerektiği yerde çağrılır. Aynı sonucu bir döngüde tekrar tekrar hesaplamak gereksiz iştir; sonuç bir kez hesaplanıp bir değişkende tutulur.

“Hesaplayan metot yazdırmaz” ilkesi en sık çiğnenen ilkedir. Bir öğrencinin ilk aklına gelen tasarım genellikle şudur:

static void ortalamaHesaplaVeYaz(int a, int b, int c) {
    System.out.println((a + b + c) / 3.0);
}

Bu metot ortalamayı yalnızca ekrana yazabilir. Ortalamanın 50'den büyük olup olmadığını sınamak, ortalamayı bir dosyaya kaydetmek ya da sınıf ortalamasına katmak isteyen kod onu kullanamaz; sonuç ekranda kalmıştır. Daha iyi tasarım, ortalamayı döndürmek ve ne yapılacağına çağıranın karar vermesidir:

static double ortalama(int a, int b, int c) {
    return (a + b + c) / 3.0;
}
// çağıran karar verir:
System.out.println(ortalama(70, 80, 90));
if (ortalama(40, 50, 45) < 50) System.out.println("kaldı");

“Gizlice dış değişkenlere bağlı olmamak” ilkesi de aynı gerekçeye dayanır. Değerlerini sınıf alanlarından okuyan bir metot, alanların o anki değerine bağlıdır. Metodun ne döndüreceğini anlamak için programın geri kalanında alanların nerede değiştirildiğini bilmek gerekir. Parametre alan bir metodun sonucu ise yalnızca argümanlarına bağlıdır; aynı argümanlarla her seferinde aynı sonucu verir.

3.2.10.6İsimlendirme

İyi ad seçmek yalnızca metotlar için değil, değişkenler için de geçerlidir. Her değişken tek bir şeyi temsil etmelidir. Aynı değişkenin önce yaş, sonra toplam için kullanılması okuyanın kafasını karıştırır ve mantık hatalarına yol açar. Bir değişkenin adı, tuttuğu verinin ne olduğunu anlatmalıdır.

int ogrenciSayisi;      // iyi bir isimlendirme
int a;                  // anlamsız bir isim
int yas = 25;
String isim = "Ahmet";
double ortalama = 85.5;
String kitapAdi;

Anlamsız ya da tek harfli adlar okunurluğu düşürür. İki istisna vardır: döngü sayacı olarak i, j ve matematiksel formüllerdeki x, y gibi geleneksel adlar. Tablo 3.2.14 birkaç kötü adı daha iyileriyle karşılaştırır.

Tablo 3.2.14: Kötü ve iyi değişken adları.
KötüİyiNeden?
int x1, x2;int vizeNotu, finalNotu;Hangi notun hangisi olduğu addan anlaşılır.
double d;double daireAlani;Değişkenin neyin değerini tuttuğu bellidir.
boolean f;boolean asalMi;if (asalMi) bir soru gibi okunur.

Java'nın yazım gelenekleri de adın türünü ele verir. Değişken ve metot adları camelCase yazılır ve küçük harfle başlar: ogrenciSayisi, notlariOku. Sınıf adları büyük harfle başlar: Kitap, Scanner. Değişmeyen sabitler büyük harflerle ve alt çizgiyle yazılır: MAKS_DEGER. Bu gelenekler derleyici için zorunlu değildir, ama Java programcıları arasında evrenseldir. Bir adı gören okuyucu, onun değişken mi, sınıf mı, sabit mi olduğunu hemen anlar.

Değişken ve metot adlarında Türkçe karakter kullanmak Java'da teknik olarak serbesttir: int öğrenciSayısı; derlenir. Ancak dosyanın kodlaması ile derleyicinin beklediği kodlama uyuşmadığında sorun çıkar. Bu yüzden derslerde ve çoğu projede adlar İngiliz alfabesindeki harflerle yazılır: ogrenciSayisi.

3.2.11Örnek problemler

Aşağıdaki problemler metot çağrısını, değer ile aktarımı ve aşırı yüklemeyi birlikte kullanır. Bütün çözümler Java ile çalıştırılmıştır.

Örnek 3.2.1

Aşağıdaki program ne yazdırır? a neden değişmez? kup(2) çalışırken yığında en çok kaç çerçeve bulunur?

static int f(int x) {
    x = x * 2;
    return x + 1;
}
public static void main(String[] args) {
    int a = 3;
    int b = f(a);
    System.out.println(a + " " + b);
    System.out.println(f(f(1)));
}
static int kare(int n) { return n * n; }
static int kup(int n) { return n * kare(n); }
// main içinde:
System.out.println(kup(2) + kare(3));
3 7
7
17

f(a) çağrısında a'nın değeri, yani 3, f'nin parametresi x'e kopyalanır. x = x * 2 kopyayı 6 yapar ve x + 1, yani 7 döner. b 7 olur. a değişmez, çünkü f yalnızca kopyasını değiştirmiştir: ilk satır 3 7'dir.

f(f(1)) ifadesinde önce içteki çağrı çalışır: f(1) = 1 × 2 + 1 = 3. Dıştaki çağrı bu değerle çalışır: f(3) = 3 × 2 + 1 = 7. İkinci satır 7'dir.

kup(2) + kare(3) ifadesinde kup(2), 2 * kare(2)'yi hesaplar. kare(2) 4 döndürür ve kup(2) 8 olur. Sonra kare(3) = 9 hesaplanır. Toplam 8 + 9 = 17'dir. kup(2) çalışırken en derin an kare(2)'nin çalıştığı andır: yığında main, kup(2) ve kare(2) olmak üzere 3 çerçeve vardır. kare(3) çağrıldığında kup'un çerçevesi çoktan kapanmıştır; o an yığında yalnızca 2 çerçeve bulunur.

Örnek 3.2.2

Aşağıdaki ilk iki println hangi topla metodunu çağırır? Son satır neden 8 değil 8.0 yazdırır?

static int topla(int a, int b) { return a + b; }
static double topla(double a, double b) { return a + b; }
// main içinde:
System.out.println(topla(2, 3.0));
System.out.println(topla(2, 3));
System.out.println(Math.max(Math.abs(-8), Math.sqrt(49)));
5.0
5
8.0

topla(2, 3.0) çağrısının argümanları (int, double)'dır. (int, int) sürümü uymaz, çünkü 3.0 int'e daralmaz. (double, double) sürümü uyar: 2 değeri 2.0'a genişletilir. Sonuç 5.0'dır. topla(2, 3) çağrısında iki sürüm de uygundur; en özel olan (int, int) seçilir ve 5 yazılır. Son satırda Math.sqrt double döndürdüğü için Math.max'ın (double, double) sürümü seçilir. 8 ile 7.0'ın büyüğü 8.0'dır.

Örnek 3.2.3

Aşağıdaki metotları yazalım: bir sayının çift olup olmadığını döndüren ciftMi; üç sayının en büyüğünü Math.max ile bulan enBuyuk; Math.pow kullanmadan üs alan usAl; rakamlar toplamını bulan basamakToplami; dairenin alanını döndüren daireAlani; daire ve dikdörtgen için aşırı yüklenmiş alan.

public class Alistirmalar {
    static boolean ciftMi(int sayi) {
        return sayi % 2 == 0;
    }
    static int enBuyuk(int a, int b, int c) {
        return Math.max(a, Math.max(b, c));
    }
    static int usAl(int taban, int us) {
        int sonuc = 1;
        for (int i = 0; i < us; i++) sonuc *= taban;
        return sonuc;
    }
    static int basamakToplami(int n) {
        int t = 0;
        while (n != 0) { t += n % 10; n /= 10; }
        return t;
    }
    static double daireAlani(double r) {
        return Math.PI * r * r;
    }
    static double alan(double r) {
        return Math.PI * r * r;
    }
    static double alan(double en, double boy) {
        return en * boy;
    }
    public static void main(String[] args) {
        System.out.println(ciftMi(7) + " " + ciftMi(10) + " " + ciftMi(-4));
        System.out.println(enBuyuk(4, 9, 2));
        System.out.println(usAl(2, 10) + " " + usAl(5, 0));
        System.out.println(basamakToplami(2024) + " " + basamakToplami(-2024));
        System.out.printf("%.2f%n", daireAlani(2));
        System.out.println(alan(1) + " " + alan(3, 4));
    }
}
false true true
9
1024 1
8 -8
12,57
3.141592653589793 12.0

ciftMi'nin gövdesi tek satırdır: return sayi % 2 == 0;. Karşılaştırmanın sonucu zaten bir boolean'dır; if (sayi % 2 == 0) return true; else return false; yazmaya gerek yoktur. Negatif sayılarda da doğru çalışır: -4 % 2 0'dır.

enBuyuk, Math.max'ı iç içe çağırır: önce b ile c'nin büyüğü bulunur, sonra bu değer a ile karşılaştırılır. enBuyuk(4, 9, 2) 9 döndürür. usAl bir biriktirici kullanır. sonuc 1'den başlar ve us kez taban ile çarpılır. usAl(2, 10) 1024 döndürür. usAl(5, 0)'da döngü hiç dönmez ve 1 döner; bu, matematikteki 5⁰ = 1 kuralıyla uyumludur.

basamakToplami, sayının son rakamını n % 10 ile alır ve n /= 10 ile atar. Tablo 3.2.15 2024 için döngüyü izler.

Tablo 3.2.15: basamakToplami(2024) döngüsünün izlenmesi (t 0'dan başlar).
Turn % 10t (sonra)n / 10
144202
22620
3062
4280
n = 0: döngü biter, return t; → 8

Negatif sayılarda Java'nın kalan işlemi bölünenin işaretini aldığı için basamakToplami(-2024) −8 döndürür. Rakamlar toplamının her zaman pozitif olması isteniyorsa metodun başında n = Math.abs(n); yazılır.

daireAlani(2) = π × 2 × 2 ≈ 12,566 değerini döndürür. printf("%.2f%n", ...) bunu iki basamağa yuvarlar; Türkçe yerel ayarda çıktı 12,57'dir. Aşırı yüklenmiş alan metotlarında hangi sürümün çağrılacağını argüman sayısı belirler. alan(1) daire sürümünü çalıştırır ve π'yi verir: 3.141592653589793. alan(3, 4) dikdörtgen sürümünü çalıştırır: 12.0. Tam sayı argümanlar double parametrelere genişletildiği için sonuç da double'dır.

3.2.12Sık yapılan hatalar

Metotlarla ilgili hataların bir kısmını derleyici yakalar ve iletisiyle yerini gösterir. Bir kısmı ise derlenen ama beklenmedik sonuç veren programlara yol açar. İkinci gruptakiler daha tehlikelidir, çünkü program hiçbir uyarı vermeden yanlış çalışır.

Sık yapılan hata

static olmayan metodu main'den çağırmak. İleti: non-static method toplama(int,int) cannot be referenced from a static context. Ya metodun başlığına static eklenir ya da bir nesne oluşturulup metot onun üzerinden çağrılır.

Sık yapılan hata

Bazı yollarda return unutmak. İleti: missing return statement. Derleyici koşulların değerini düşünmez; if (n > 0) ... if (n <= 0) ... zinciri insana tam görünse de hata verir. Son dal düz bir else olur ya da metodun sonuna varsayılan bir return konur.

Sık yapılan hata

void metodun sonucunu kullanmak. int x = selamla(); satırı incompatible types: void cannot be converted to int, println(selamla()) ise 'void' type not allowed here iletisini verir. void metot değer üretmez; çağrısı yalnızca tek başına bir deyim olabilir.

Sık yapılan hata

Eksik ya da fazla argüman. İleti: method toplama in class ... cannot be applied to given types; altında required: int,int ve found: int satırları beklenen ve verilen tipleri gösterir. Argümanların sayısı, sırası ve tipleri imzayla uyuşmalıdır.

Sık yapılan hata

Yerel değişkeni metodun dışında kullanmak. İleti: cannot find symbol ... variable sonuc. Yerel değişken metot bitince yok olur; değeri return ile döndürülür ve çağıran onu kendi değişkenine atar.

Sık yapılan hata

İç blokta dıştaki yerel değişkenle aynı ad. İleti: variable disDegisken is already defined in method fonksiyon(). Aynı kural parametreler için de geçerlidir. Bir yerel değişkenin bir alanla aynı adı taşıması ise serbesttir; o zaman gölgeleme olur.

Sık yapılan hata

Takas metodunun çalışacağını sanmak. yerDegistir(int x, int y) yalnızca kopyaları takas eder; yerDegistir(dizi[0], dizi[2]) çağrısından sonra dizi aynı kalır. Dizi hücrelerini takas etmek için metot diziyi ve indisleri alır: yerDegistir(int[] d, int i, int j).

Sık yapılan hata

Argümanın değişeceğini sanmak. arttir(sayi) çağrısından sonra sayi aynı kalır; değişen, metodun kopyasıdır. Yeni değer return ile döndürülür ve atanır: sayi = arttir(sayi);. Derleyici, dönen değerin atanmamasına uyarı vermez.

Sık yapılan hata

Dönüş tipiyle aşırı yüklemek. int f(int a) ve double f(int a) birlikte olamaz: method f(int) is already defined. İmzayı yalnızca ad ve parametre tipleri belirler; dönüş tipi ve parametre adları imzaya girmez.

Sık yapılan hata

Math sonucunun tipini unutmak. int k = Math.sqrt(16); satırı possible lossy conversion from double to int iletisini verir. sqrt ve pow her zaman double döndürür; tam sayı gerekiyorsa (int) dönüşümü yazılır.

Sık yapılan hata

Dönüş tipinin hesabı değiştireceğini sanmak. static double yarim(int n) { return n / 2; } metodu 7 için 3.0 döndürür. Bölme iki int arasında yapılır ve kesir atılır; double dönüş tipi yalnızca sonucu genişletir. Doğru yazım n / 2.0'dır.

Sık yapılan hata

Hesaplayan metotta yazdırmak. ortalamaHesaplaVeYaz gibi bir metodun sonucu ekranda kalır; başka bir hesapta kullanılamaz. Metot sonucu döndürür, yazdırma işini çağıran yapar.

3.2.13Özet

  • Fonksiyon bir işi yapan, adı olan ve tekrar kullanılabilen kod parçasıdır. Java'da her zaman bir sınıfın içinde yazılır ve metot adını alır. Girdi parametrelerle gelir, çıktı return ile döner.
  • Metot başlığı: erişim [static] dönüşTipi ad(parametreler). Her parametre kendi tipini taşır. İmza = ad + parametre tiplerinin sırası; dönüş tipi imzaya girmez. Metotlar iç içe tanımlanamaz.
  • Parametre tanımdaki değişken, argüman çağrıdaki değerdir. Argümanlar önce hesaplanır (soldan sağa), sonra parametrelere kopyalanır. Sayı, sıra ve tip uyuşmalıdır.
  • Her çağrı çağrı yığınında yeni bir çerçeve açar; çerçeve parametreleri ve yerel değişkenleri tutar. Metot dönünce çerçeve silinir. Yığının derinliği, iç içe çağrıların en uzun zinciri kadardır.
  • return değeri verir ve metodu hemen bitirir; arkasındaki deyime ulaşılamaz. Değer döndüren metotta her yol bir return ile bitmelidir; derleyici koşulların değerini düşünmez. void metot değer döndürmez, tek başına return; ile erken çıkabilir.
  • Dönen değer dönüş tipine genişleyebilir (int → double), daralamaz. Dönüş tipi ifadenin nasıl hesaplanacağını değiştirmez.
  • static metot sınıfa aittir; doğrudan ya da Sinif.metot() ile çağrılır. static olmayan metot bir nesne üzerinden çağrılır: nesne.metot().
  • Java argümanları değer ile aktarır. İlkel argümanın kopyası değişse de çağıranın değişkeni değişmez. Dizi argümanında referans kopyalanır: metot ortak diziyi değiştirebilir, ama parametreye yeni dizi atamak çağıranı etkilemez.
  • Aşırı yükleme: aynı ad, farklı parametre listesi. Seçimi derleyici, derleme zamanında ve argüman tiplerine bakarak yapar. Önce tam eşleşme aranır, yoksa genişletme denenir (byte → short → int → long → float → double, char → int). Birden çok uygun sürüm varsa en özeli seçilir; en özel sürüm yoksa çağrı belirsizdir.
  • System.out.println satır sonu ekler, print eklemez, printf biçimli yazar ve ondalık ayırıcıda yerel ayarı kullanır. + birleştirmesi soldan sağa çalışır.
  • Math metotları static'tir. sqrt ve pow her zaman double, abs/max/min argümanın tipini, round(double) long döndürür. java.lang dışındaki paketler import ister.
  • Yerel değişken tanımlandığı blokta yaşar ve kullanılmadan önce değer almalıdır. İç kapsam dışı görür, dış kapsam içi göremez. İç blok dıştaki yerelle aynı adı kullanamaz; yerel değişken ise aynı adlı alanı gölgeler. static alanlar program boyunca yaşar.
  • Tasarım: soyutlama (ne yaptığı önemli, nasıl yaptığı gizli), genelleme (sabitler parametreye), problemi parçalama (her alt görev bir metot), DRY (tekrar eden kod metoda), tek sorumluluk, açıklayıcı ad, sonucu döndürmek ve yazdırmayı çağırana bırakmak.

3.2.14Kavram listesi

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

3.2.15Kendini Dene

  1. static int a(int n) { return b(n + 1) * 2; }, static int b(int n) { return c(n) + n; } ve static int c(int n) { return n * n; } metotları veriliyor. main'den a(2) çağrılırsa ne döner? Yığında aynı anda en çok kaç çerçeve bulunur?

    Cevap

    a(2), b(3)'ü çağırır. b(3), c(3)'ü çağırır ve c(3) 9 döndürür. b(3) = 9 + 3 = 12 döner. a(2) = 12 × 2 = 24 döner. c(3) çalışırken yığında main, a, b ve c olmak üzere 4 çerçeve vardır; en derin an budur.

  2. static int degis(int[] d, int k) { d[k] = d[k] + k; k = 0; return d[k]; } metodu, main'de int[] d = {3, 4, 5}; int k = 2; int r = degis(d, k); ile çağrılıyor. Çağrıdan sonra d, k ve r nedir?

    Cevap

    Metodun d'si main'deki diziyi gösteren referansın kopyasıdır; k'si 2'nin kopyasıdır. d[2] = 5 + 2 = 7 ortak diziyi değiştirir. k = 0 yalnızca kopyayı değiştirir. return d[0] 3 döndürür. Sonuç: dizi {3, 4, 7}, main'deki k hâlâ 2, r = 3.

  3. Bir sınıfta m(long x), m(double x) ve m(int x, int y) metotları var; her biri kendi parametre tiplerini metin olarak döndürüyor. short s = 7; char ch = 'x'; tanımlarından sonra m(s), m(ch), m(3.0f) ve m(2, 'b') hangi sürümleri çağırır?

    Cevap

    Tek argümanlı çağrılarda iki parametreli sürüm sayı yüzünden elenir. short hem long'a hem double'a genişleyebilir; long daha özeldir: m(s) → long. char da önce int'e, oradan long'a genişler; yine long seçilir: m(ch) → long. float yalnızca double'a genişler: double. m(2, 'b')'de 'b' int'e genişler: int,int. JDK 21 çıktısı: long long double int,int.

  4. Sınıfta static int sayac = 0; alanı ve static int artir(int sayac) { sayac++; KendiniDene.sayac += 2; return sayac; } metodu var. int t = artir(5); satırından sonra t ve sınıf alanı sayac kaçtır?

    Cevap

    Parametre sayac alanı gölgeler. sayac++ parametreyi 6 yapar; alan etkilenmez. KendiniDene.sayac += 2 sınıf adıyla alana ulaşır ve onu 2 yapar. Metot parametreyi döndürür: t = 6. Alan 2'dir.

  5. static double oran(int a, int b) { return a / b; } metodu oran(7, 2) ve oran(1, 3) için ne döndürür? Metot nasıl düzeltilir?

    Cevap

    a / b iki int arasında tam sayı bölmesidir. 7 / 2 = 3 ve 1 / 3 = 0 bulunur; dönerken double'a genişletilir: 3.0 ve 0.0. Dönüş tipi hesabı değiştirmez. Düzeltme: return (double) a / b;. Bu durumda sonuçlar 3.5 ve 0.333… olur.

  6. Aşağıdaki metot çiftlerinden hangileri aynı sınıfta birlikte bulunabilir? (a) void g(int x) ve int g(int y); (b) int h(int a, double b) ve int h(double a, int b); (c) int k(int a) ve int k(int a, int b).

    Cevap

    (a) olamaz: iki imza da g(int)'tir; dönüş tipi ve parametre adı imzaya girmez. (b) birlikte bulunabilir, çünkü parametre tiplerinin sırası farklıdır. Ama h(1, 1) gibi bir çağrı reference to h is ambiguous hatası verir; derleyici iki sürümden birini seçemez. (c) birlikte bulunabilir: parametre sayıları farklıdır.

  7. Bir metot şöyle yazılmış: static String durum(int puan) { if (puan >= 50) return "geçti"; else if (puan < 50) return "kaldı"; }. Derlenir mi? Derlenmiyorsa en küçük düzeltme nedir?

    Cevap

    Derlenmez: missing return statement. İki koşul her tam sayıyı kapsasa da derleyici koşulların değerini değerlendirmez; zincirin son dalı else if olduğu için akışın metodun sonuna ulaşabileceğini varsayar. En küçük düzeltme else if (puan < 50) yerine düz else yazmaktır.

  8. System.out.println("Sonuç: " + 2 + 3); ve System.out.println(2 + 3 + " sonuç"); satırları ne yazar? Türkçe ayarlı bir bilgisayarda System.out.printf("%.1f%n", 2.25); ne yazar?

    Cevap

    + soldan sağa çalışır. İlk satırda önce "Sonuç: " + 2 bir metin olur, sonra 3 eklenir: Sonuç: 23. İkinci satırda önce 2 + 3 sayı olarak toplanır: 5 sonuç. printf yerel ayarı kullandığı için ondalık ayırıcı virgüldür ve sayı bir basamağa yuvarlanır: 2,3.

  9. “Bir öğrencinin notlarını klavyeden oku, ortalamasını hesapla ve 50'den büyükse ‘geçti’ yaz” işini tek bir main yerine metotlara bölmek isteniyor. Hangi metotlar, hangi parametre ve dönüş tipleriyle yazılabilir?

    Cevap

    Bir olası tasarım: int[] notlariOku(Scanner giris, int adet) okur ve diziyi döndürür; double ortalama(int[] notlar) hesaplar ve döndürür; boolean gectiMi(double ort) karar verir. Yazdırma işi main'de kalır. Her metot tek bir iş yapar, verisini parametrelerle alır ve sonucunu döndürür. ortalama ve gectiMi klavyeye dokunmadan sınanabilir.

3.2.16İleri okuma

  • A. B. Downey ve C. Mayfield, Think Java (2. baskı, O'Reilly, 2019), 4. bölüm: metot yazmayı, parametreleri ve yığın diyagramlarını kısa örneklerle anlatır; çevrim içi olarak ücretsiz okunabilir.
  • J. Bloch, Effective Java (3. baskı, Addison-Wesley, 2018), “Methods” bölümü: parametrelerin denetlenmesi, metot imzalarının tasarımı ve aşırı yüklemenin ne zaman sorun çıkardığı üzerine deneyimli bir programcının önerileri.
  • R. C. Martin, Clean Code (Prentice Hall, 2008), 3. bölüm “Functions”: kısa metot, tek iş, az parametre ve yan etkisiz tasarım ilkelerini örnek kodlar üzerinde tartışır.
  • J. Gosling ve diğ., The Java Language Specification, Java SE 21, §15.12: derleyicinin bir çağrı için uygun metotları nasıl bulduğunu ve en özel olanı nasıl seçtiğini kesin kurallarla tanımlar.

3.2.17Kaynaklar

  • Oracle, The Java Tutorials: “Defining Methods”, “Passing Information to a Method or a Constructor”.
  • J. Gosling ve diğ., The Java Language Specification, Java SE 21 Edition (§8.4 Method Declarations, §6.3 Scope of a Declaration, §15.12 Method Invocation Expressions).
  • P. Deitel ve H. Deitel, Java: How to Program, Early Objects, 11. baskı, Pearson, 2017 (Bölüm 6: Methods).
  • K. Sierra ve B. Bates, Head First Java, 3. baskı, O'Reilly, 2022.
  • C. S. Horstmann, Core Java, Volume I: Fundamentals, 12. baskı, 2022.
  • A. B. Downey ve C. Mayfield, Think Java, 2. baskı, O'Reilly, 2019 (Bölüm 4: Methods and Testing).
  • J. Bloch, Effective Java, 3. baskı, Addison-Wesley, 2018.
  • R. C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship, Prentice Hall, 2008.