Bilgisayar ve Programlamaya Giriş · Ders Notu

Değişkenler, Veri Tipleri ve Operatörler

Her program veriyle çalışır: bir öğrencinin yaşı, bir ürünün fiyatı, bir sınavın ortalaması. Program bu verileri bellekte adlandırılmış kutularda, yani değişkenlerde tutar. Her kutunun bir tipi vardır ve tip, kutuya hangi değerlerin sığacağını belirler. Bu not, Java'da değişken bildirmeyi ve değer vermeyi, sekiz ilkel tipi ve aralıklarını, sınır aşıldığında ortaya çıkan taşmayı ve ondalık sayıların neden tam saklanamadığını anlatır. Ardından tipler arasındaki dönüşümler, tam sayı bölmesi, aritmetik, karşılaştırma ve mantık operatörleri, işlem önceliği ve kısa devre değerlendirme ele alınır. Son kesimler klavyeden Scanner ile veri okumayı, printf ile biçimli çıktı üretmeyi ve bu araçları birleştiren küçük programları içerir. Nottaki bütün Java kodları JDK 21 ile derlenip çalıştırılmıştır; verilen çıktılar programların gerçek çıktısıdır.

Öğrenme çıktıları

Değişken
Değişken bildirmek, ilk değer vermek ve değişkenin değerini güncellemek.
Tip
İlkel tipleri ve değer aralıklarını tanımak; taşma ve kayan nokta hatası riskine göre uygun tipi seçmek.
İfadeler
Atama, tip dönüşümü ve operatörleri doğru kullanmak; bir ifadenin değerini öncelik ve tip kurallarıyla adım adım hesaplamak.
Girdi/Çıktı
Scanner ile klavyeden girdi almak, printf ile biçimli çıktı üretmek.

6.1Değişkenler

Bir hesap makinesiyle 70, 85 ve 90 notlarının ortalamasını alan biri, önce üç sayıyı toplar, ara sonucu ekranda tutar, sonra 3'e böler. Ara sonucu bir kâğıda yazmak da mümkündür. Bilgisayar programı da aynı ihtiyacı duyar: okuduğu sayıları, ara sonuçları ve son cevabı bir yerde saklamalıdır. Bu saklama yeri bellektir. Program, bellekte ayırdığı her yere bir ad verir ve o yere adıyla ulaşır.

6.1.1Değişken nedir?

Tanım 6.1

Değişken (variable), belirli tipteki bir değer için bellekte ayrılmış adlı bir kutudur. Değişkenin üç özelliği vardır: adı, tipi (type) ve o anki değeri. Ad ve tip program boyunca sabit kalır; değer ise değişebilir.

20yasad (tanımlayıcı)değer(değişebilir)tip: int → 32 bitlik yer ayrılırint yas = 20;
Şekil 6.1: int yas = 20; satırının bellekteki karşılığı. Kutunun adı yas, tipi int, değeri 20'dir. int tipi için 32 bitlik yer ayrılır.

Şekil 6.1 bu üç özelliği bir arada gösterir. Kutunun üstündeki yas adı, programcının bu kutuya verdiği addır; Java'daki adıyla bir tanımlayıcıdır (identifier). Kutunun içindeki 20 değeri program çalışırken değişebilir: yeni bir yıl başladığında yaş 21 olur. Kutunun altındaki int ise tiptir. Tip iki şeyi belirler. Birincisi, kutunun boyutudur: int için 32 bit, yani 4 bayt ayrılır. İkincisi, kutuya konabilecek değerlerdir: int bir tam sayı tipidir, bu kutuya 20 konabilir ama 20,5 ya da “yirmi” konamaz.

“Değişken” adı matematikten gelir ama anlamı biraz farklıdır. Matematikte x = x + 1 çözümü olmayan bir denklemdir. Programlamada ise bu satır bir komuttur: “x kutusundaki değeri oku, 1 ekle, sonucu yine x kutusuna yaz.” Değişken, zamanla farklı değerler tutabilen bir kutudur; eşitlik işareti de bir eşitlik iddiası değil, bir yazma işlemidir.

6.1.2Bildirim ve ilk değer

Java'da bir değişken kullanılmadan önce bildirilmelidir. Bildirim, derleyiciye “bu adla, bu tipte bir kutu istiyorum” demektir.

Tanım 6.2

Bildirim (declaration), bir değişkenin tipini ve adını belirten deyimdir: int yas;. İlk değer verme (initialization) ise değişkenin başlangıç değerini belirtir. İkisi tek deyimde de yazılabilir: double ortalama = 82.5;.

Sunumdaki dört satır, bildirimin farklı biçimlerini gösterir:

int yas;          // bildirim
yas = 20;        // atama
double ortalama = 82.5; // birlikte
final int MAKSIMUM = 100; // sabit

Birinci satır yalnızca bildirimdir: yas adlı int kutusu ayrılır ama içine henüz bir değer konmamıştır. İkinci satır bir atamadır (assignment): 20 değeri yas kutusuna yazılır. Üçüncü satır bildirimle ilk değeri birleştirir; ortalama kutusu ayrılır ve aynı anda 82.5 değerini alır. Dördüncü satırdaki final sözcüğü kutuyu bir sabite çevirir: MAKSIMUM bir kez 100 değerini alır ve program boyunca bu değeri korur.

1 · bildirimyas?tip: intdeğer yok: okunamaz2 · atamayas20tip: intartık okunabilir3 · bildirim + ilk değerortalama82.5tip: doubletek satırda4 · sabitMAKSIMUM100tip: final intyeniden atanamazsonra
Şekil 6.2: Dört satırın bellekteki etkisi. Bildirimden sonra yas kutusunun değeri yoktur ve okunamaz; atamadan sonra 20'dir. ortalama tek satırda bildirilip değer alır. final ile bildirilen MAKSIMUM yeniden atanamaz.

Java, bir metodun içinde bildirilen değişkenlere yerel değişken (local variable) der. Yerel değişkenler için katı bir kural vardır: değişken okunmadan önce kesin olarak bir değer almış olmalıdır. Derleyici bu kuralı programı çalıştırmadan, kodu okuyarak denetler. Aşağıdaki iki satırlık parça bu yüzden derlenmez:

int yas;
System.out.println(yas);
error: variable yas might not have been initialized
System.out.println(yas);
                   ^
1 error

İleti “yas değişkenine değer verilmemiş olabilir” der. Derleyici yas kutusunun boş olduğunu görür ve programı hiç üretmez. Bu kural bir güvenlik önlemidir. Kural olmasaydı program, kutuda o an rastlantıyla bulunan bir değeri okuyabilir ve anlamsız sonuçlar üretebilirdi. Bazı eski dillerde (örneğin C'de) bu durum gerçekten yaşanır.

final değişkenler için de benzer bir denetim vardır. Değer bir kez verildikten sonra ikinci atama derleme hatasıdır:

final int MAKSIMUM = 100;
MAKSIMUM = 200;
error: cannot assign a value to final variable MAKSIMUM
MAKSIMUM = 200;
^
1 error

Sabitler, programda birden çok yerde geçen ve değişmemesi gereken değerler için kullanılır: en yüksek not, KDV oranı, bir dizinin boyutu. Sayıyı her yerde ayrı ayrı yazmak yerine bir sabit tanımlamak iki yarar sağlar. Kod okunur hâle gelir; puan <= MAKSIMUM ifadesi puan <= 100 ifadesinden daha çok şey anlatır. Ayrıca değer değişirse yalnızca tek satır düzeltilir.

Sık yapılan hata

Bildirilen bir değişkenin otomatik olarak 0 değerini aldığını sanmak. Java'da yerel değişkenler kendiliğinden değer almaz; int toplam; satırından sonra toplam += 5; yazmak “might not have been initialized” derleme hatası verir. Toplam tutacak bir değişken açıkça sıfırla başlatılır: int toplam = 0;.

6.1.3Değişkenler bellekte adım adım

Değişkenlerin nasıl değer aldığını ve değiştirdiğini görmenin en iyi yolu, bir programı satır satır izlemektir. Sunumdaki örnek program şudur:

int yas;                  // bildirim
yas = 20;                 // ilk atama
yas = yas + 1;            // güncelleme
double ortalama = 82.5;   // bildirim + ilk değer
ortalama += 5;            // bileşik atama
final int MAKSIMUM = 100; // sabit
System.out.println(yas);
System.out.println(ortalama);

Program iki satır çıktı üretir:

21
87.5

Tablo 6.1, her satır çalıştıktan sonra kutuların durumunu verir. Soru işareti, kutunun ayrıldığını ama henüz değer almadığını; tire, kutunun henüz hiç ayrılmadığını gösterir.

Tablo 6.1: Bellek örneğinin izleme tablosu. Her satır, o satırdaki deyim çalıştıktan sonraki durumu gösterir.
SatırDeyimyasortalamaMAKSIMUMÇıktı
1int yas;?––
2yas = 20;20––
3yas = yas + 1;21––
4double ortalama = 82.5;2182.5–
5ortalama += 5;2187.5–
6final int MAKSIMUM = 100;2187.5100
7System.out.println(yas);2187.510021
8System.out.println(ortalama);2187.510087.5

Üçüncü satır dikkatle okunmalıdır. yas = yas + 1; deyimi iki aşamada çalışır. Önce eşittir işaretinin sağındaki ifade hesaplanır: yas kutusundaki 20 okunur ve 1 eklenir, sonuç 21'dir. Bu aşamada kutu henüz değişmemiştir. İkinci aşamada 21 değeri soldaki kutuya yazılır; eski 20 silinir. Bir kutu aynı anda yalnızca bir değer tutar ve yeni değer eskisinin üzerine yazılır.

Bellek (değişkenler)yas?int Bellek (değişkenler)yas20intİfadeyas + 120 + 1→ 21 Bellek (değişkenler)yas21int Bellek (değişkenler)yas21intortalama82.5doubleİfadeortalama + 582.5 + 5→ 87.5
Şekil 6.3: Bellek örneğinin dört anı. Bildirimden sonra yas kutusu boştur (?). Güncellemede önce sağ taraf hesaplanır: 20 + 1 → 21. Sonuç aynı kutuya yazılır. ortalama += 5 deyimi de aynı biçimde işler: 82.5 + 5 → 87.5.

Yedinci ve sekizinci satırlar kutuları yalnızca okur. Okuma bir değeri kutudan kopyalar ama kutuyu değiştirmez; System.out.println(yas) satırından sonra yas hâlâ 21'dir. Son durumda bellekte üç kutu vardır ve ekranda iki satır çıktı görünür. Altıncı satırdan sonra MAKSIMUM = 200; yazılsaydı program derlenmezdi; final değişkenin değeri bir kez verilir.

6.1.4Atama ve güncelleme

Atama operatörü =, Java'nın en çok kullanılan operatörüdür ve her zaman aynı sırayla çalışır: önce sağ taraf hesaplanır, sonra değer soldaki değişkene yazılır. Sol tarafta her zaman tek bir değişken bulunur. 5 = x; ya da x + 1 = 7; gibi satırlar anlamsızdır, çünkü 5 ya da x + 1 bir kutu değildir.

Atanan değer, değişkenin tipiyle uyumlu olmalıdır. int bir kutuya "Ada" dizgisi atanamaz; derleyici bunu tip uyuşmazlığı hatası olarak bildirir.

Programlarda bir değişkenin değerini, kendi eski değerini kullanarak değiştirmek çok sık yapılır: toplama yeni bir fiyat eklemek, sayacı bir artırmak, bakiyeden bir tutar düşmek. Bu iş için Java'da kısa bir yazım vardır.

Tanım 6.3

Bileşik atama (compound assignment), bir aritmetik işlemi atamayla birleştiren operatördür: +=, -=, *=, /=, %=. toplam += fiyat deyimi toplam = toplam + fiyat anlamına gelir.

toplam50intfiyat20inttoplam += fiyat; toplam50intfiyat20inttoplam + fiyat → 50 + 20 → 70 toplam70intfiyat20inttoplam ← 70
Şekil 6.4: toplam += fiyat; deyiminin üç adımı. Başta toplam = 50, fiyat = 20'dir. Önce iki kutu okunur ve 50 + 20 → 70 hesaplanır. Sonuç soldaki kutuya yazılır: toplam = 70. fiyat değişmez.

Şekil 6.4 işlemin adımlarını gösterir. Sağdaki değişken (fiyat) yalnızca okunur, soldaki değişken (toplam) hem okunur hem yazılır. Bileşik atamanın bir inceliği daha vardır: sağ taraf, işlemden önce bütünüyle hesaplanır. i *= 3 + 7 deyimi i = i * 3 + 7 değil, i = i * (3 + 7) demektir. i = 5 için sonuç 22 değil 50'dir.

Örnek 6.1

Bir alışveriş sepetinde toplam 0 TL'den başlar. Sırayla 45 TL'lik ve 30 TL'lik iki ürün eklenir, sonra 10 TL indirim uygulanır, en son tutar 2 taksite bölünür. Hangi deyimler yazılır ve her adımdan sonra toplam kaç olur?

DeyimHesaptoplam
int toplam = 0;ilk değer0
toplam += 45;0 + 4545
toplam += 30;45 + 3075
toplam -= 10;75 − 1065
toplam /= 2;65 / 2 (tam sayı bölmesi)32

Son satırdaki sonuç 32,5 değil 32'dir. toplam bir int olduğu için bölme tam sayı bölmesidir ve kesir atılır. Taksit tutarının kuruşuyla hesaplanması gerekiyorsa değişken baştan double seçilmelidir. İki tam sayının bölümü Java'da her zaman tam sayıdır.

6.1.5Tanımlayıcı kuralları ve adlandırma

Değişkenlere, metotlara ve sınıflara verilen adların hepsi tanımlayıcıdır. Java, bir tanımlayıcının nasıl yazılabileceğini birkaç kesin kuralla sınırlar. Bu kurallara uymayan bir ad derleme hatası verir.

Tablo 6.2: Tanımlayıcı kuralları (sunumdaki tablo).
KuralGeçerliGeçersiz
Harf, _ veya $ ile başlarogrenci11ogrenci
Boşluk içermeztoplamPuantoplam puan
Anahtar kelime değildirsinifNoclass
Tek alt çizgi ad olamaz_deger_
Büyük-küçük harfe duyarlıpuan ve PuanAynı değişken sanmak

Tanımlayıcı bir harfle, alt çizgiyle (_) ya da dolar işaretiyle ($) başlar; sonraki karakterler bunlara ek olarak rakam da olabilir. Rakamla başlayan bir ad derleyiciyi şaşırtır. int 1ogrenci = 5; satırında derleyici int'ten sonra bir ad bekler ama 1 rakamıyla karşılaşır ve satırı anlamlandıramaz:

error: not a statement
int 1ogrenci = 5;
^
error: ';' expected
int 1ogrenci = 5;
   ^
2 errors

İletiler sorunu doğrudan söylemez; “bu bir deyim değil” ve “noktalı virgül bekleniyordu” der. Derleyici hatayı bulduğu yerde, kendi anladığı biçimde bildirir. Bu yüzden bir derleme iletisi okunurken iletinin işaret ettiği satırın tamamına bakılmalıdır.

Anahtar kelimeler (keyword), Java'nın kendi işleri için ayırdığı sözcüklerdir: int, double, class, if, final, new, return bunlardan birkaçıdır. Bu sözcükler ad olarak kullanılamaz; int class = 5; satırı üç ayrı hata iletisi üretir. Tek başına alt çizgi de JDK 21'de ad değildir. int _ = 3; satırını derleyici “adsız değişkenler önizleme özelliğidir” iletisiyle reddeder. Daha yeni Java sürümlerinde _ “adsız değişken” anlamına gelir; bu durumda da değişkene adıyla ulaşılamaz.

Son kural en sinsi olanıdır. Java büyük ve küçük harfi farklı karakter sayar. puan ve Puan iki ayrı değişkendir ve aynı programda yan yana yaşayabilir:

int puan = 70;
int Puan = 90;
System.out.println(puan + " " + Puan);
70 90

Program hatasız derlenir ve iki farklı değer yazar. Programcı aynı değişkeni kullandığını sanıyorsa hata sessizce oluşur. Bu yüzden büyük-küçük harf kullanımı rastgele bırakılmaz; adlandırma kuralları bu tutarlılığı sağlar.

Derleyicinin denetlediği kuralların yanında, Java programcılarının uyduğu yazılı olmayan adlandırma (naming convention) gelenekleri de vardır. Bu geleneklere uymayan kod derlenir ama okuyan herkesi yorar.

lowerCamelCasedeğişken ve metottoplamPuanint toplamPuan = 0;UpperCamelCasesınıfOgrenciKaydiclass OgrenciKaydi { }BÜYÜK_HARFsabit (final)MAKSIMUM_PUANfinal int MAKSIMUM_PUAN = 100;Rolü anlat:x✗→toplamPuan✓Kelime sınırını büyük harf (ya da sabitte _) gösterir.
Şekil 6.5: Üç adlandırma biçimi. Değişken ve metot adları küçük harfle başlar ve her yeni kelime büyük harfle gelir (toplamPuan). Sınıf adları büyük harfle başlar (OgrenciKaydi). Sabitler büyük harflerle yazılır ve kelimeler alt çizgiyle ayrılır (MAKSIMUM_PUAN).
  • lowerCamelCase: değişken ve metot adları. İlk kelime küçük harfle başlar, sonraki her kelimenin ilk harfi büyüktür: toplamPuan, ogrenciSayisi, ortalamaHesapla. Kelimeler arasındaki büyük harfler, devenin hörgüçleri gibi kelime sınırını gösterir.
  • UpperCamelCase: sınıf adları. Her kelime büyük harfle başlar: OgrenciKaydi, Scanner, String.
  • BÜYÜK_HARF_VE_ALT_ÇİZGİ: final sabitler. MAKSIMUM_PUAN, KDV_ORANI.

Bir adın en önemli niteliği, değerin rolünü anlatmasıdır. x, a1, temp2 gibi adlar, kodu yazan kişiye o an anlamlı gelir; bir hafta sonra kimseye bir şey söylemez. toplamPuan adı ise hem tipini hem görevini sezdirir. Türkçe karakterler (ç, ğ, ı, ö, ş, ü) Java tanımlayıcılarında geçerlidir, ama derste ve çoğu projede ASCII harflerle yazmak tercih edilir: ogrenci, sinif, yas. Bu tercih, farklı karakter kodlamalı bilgisayarlar arasında dosya taşınırken çıkabilecek sorunları önler.

Sık yapılan hata

Bir yerde toplamPuan, başka bir yerde toplampuan yazmak. İkinci ad bildirilmemişse derleyici “cannot find symbol” (simge bulunamadı) hatası verir. Daha kötüsü, iki ad da bildirilmişse program derlenir ve değerler farklı kutulara gider. Adlandırma geleneğine her yerde aynı biçimde uymak, bu tür hataların çoğunu baştan önler.

6.2Veri tipleri

Bellek, en alt düzeyde yalnızca bitlerden, yani 0 ve 1'lerden oluşur. 01000001 bit dizisi bir yerde 65 sayısı, başka bir yerde 'A' harfi anlamına gelebilir. Bitlerin nasıl yorumlanacağını tip belirler. Java, her değişkenin tipinin derleme sırasında bilinmesini ister. Bu yüzden Java katı tipli bir dildir: derleyici her işlemin tiplere uygun olup olmadığını program çalışmadan önce denetler ve pek çok hatayı daha ilk derlemede yakalar.

6.2.1Temel tip grupları

Java tipleri iki büyük aileye ayrılır. İlkel tipler (primitive type) dilin içine gömülü sekiz temel tiptir. İlkel bir değişkenin kutusunda değerin kendisi durur. Referans tipleri ise dizgiler (String), diziler ve sınıflardır. Referans tipinde bir değişkenin kutusunda nesnenin kendisi değil, bellekte başka bir yerde duran nesneye giden bir referans (reference) bulunur.

Tablo 6.3: Temel tip grupları (sunumdaki tablo).
GrupÖrnekKullanım
Tam sayıbyte, short, int, longSayma ve ayrık değer
Kayan noktalıfloat, doubleÖlçüm ve yaklaşık hesap
KaraktercharTek UTF-16 kod birimi
Mantıksalbooleantrue / false
ReferansString, dizi, sınıfNesneye referans
Java tipleriİlkel (8 tip)ReferansTam sayıbyte shortint longKayan noktalıfloat doubleKaraktercharMantıksalbooleanStringdizisınıfdeğişken nesnenin kendisini değil,ona referansı tutardeğer doğrudan değişkenin kutusunda durur
Şekil 6.6: Java tiplerinin iki ailesi. Sekiz ilkel tip dört gruba ayrılır; bunlarda değer doğrudan değişkenin kutusunda durur. String, diziler ve sınıflar referans tipleridir; değişken nesnenin kendisini değil, ona giden referansı tutar.

Gruplar, verinin doğasına göre seçilir. Bir sınıftaki öğrenci sayısı, bir dizideki konum, bir sayaç gibi sayılan değerler tam sayıdır. Boy, sıcaklık, fiyat gibi ölçülen değerler kayan noktalı sayıyla tutulur. Tek bir harf için char, evet/hayır cevabı için boolean kullanılır. Ad, adres, cümle gibi metinler ise String nesneleridir. Bu notta ağırlıklı olarak ilkel tipler ele alınır; String yalnızca birleştirme ve girdi okuma bağlamında görünür.

6.2.2İlkel tipler ve boyutları

Sekiz ilkel tipin her biri bellekte sabit sayıda bit kaplar. Bu sayı Java'da her bilgisayarda ve her işletim sisteminde aynıdır. C gibi bazı dillerde int'in boyutu makineye göre değişebilir; Java'da int her yerde 32 bittir. Bu kararlılık, aynı programın her yerde aynı sonucu vermesini sağlar.

Tablo 6.4: İlkel tipler, boyutları ve sınırları. Sınır değerleri JDK 21'de Byte.MAX_VALUE gibi sabitler yazdırılarak alınmıştır.
TipBitEn küçükEn büyükSabit örneği
byte8−128127byte seviye = 7;
short16−32.76832.767short yil = 2024;
int32−2.147.483.6482.147.483.647int adet = 42;
long64−9.223.372.036.854.775.8089.223.372.036.854.775.807long nufus = 8_000_000L;
float32yaklaşık ±3,4 × 1038; ≈ 7 anlamlı basamakfloat oran = 1.5F;
double64yaklaşık ±1,8 × 10308; ≈ 15–16 anlamlı basamakdouble fiyat = 19.90;
char16065.535char not = 'A';
booleanJVM'e bağlıtrue ya da falseboolean aktif = true;
016324864bit sayısıbyte8-128 … 127short16-32.768 … 32.767char160 … 65.535int32≈ ±2,1 milyarlong64≈ ±9,2 × 10¹⁸float32≈ 7 anlamlı basamakdouble64≈ 15–16 anlamlı basamakbooleantrue / false — boyutu JVM'e bağlı
Şekil 6.7: İlkel tiplerin bit sayıları. Vurgulu çubuklar varsayılan seçimlerdir: tam sayı için int, kayan noktalı sayı için double. 1 bayt = 8 bittir.

Bir tipin aralığı bit sayısından hesaplanır. n bitle 2n farklı bit dizisi yazılabilir. byte için 28 = 256 farklı değer vardır. Java tam sayı tipleri işaretlidir: bu değerlerin yarısı negatif sayılara, yarısı sıfır ve pozitif sayılara ayrılır. Sıfır pozitif tarafta yer kapladığı için aralık −128'den 127'ye uzanır; 128 sayısı byte'a sığmaz. Aynı hesap int için −231 … 231 − 1 aralığını verir:

231 = 2.147.483.648,   dolayısıyla int aralığı −2.147.483.648 … 2.147.483.647

Kabaca akılda tutmak için int'in sınırı “yaklaşık ±2,1 milyar”, long'unki “yaklaşık ±9,2 kentilyon” (9,2 × 1018) olarak hatırlanabilir. char tipi 16 bitlik olmasına karşın işaretsizdir; negatif değeri yoktur ve 0 ile 65.535 arasındaki kodları tutar.

Programda doğrudan yazılan değerlere sabit (literal) denir: 42, 19.90, 'A', true. Sabitlerin de tipi vardır ve bu tip yazılış biçiminden anlaşılır:

  • Noktasız bir tam sayı sabiti (42) int'tir. Sonuna L eklenirse long olur (42L). Küçük l de geçerlidir ama 1 rakamına çok benzediği için kullanılmaz.
  • Noktalı bir sabit (19.90) ya da bilimsel gösterimle yazılmış bir sabit (3.9e10, yani 3,9 × 1010) double'dır. Sonuna F eklenirse float olur (1.5F).
  • Uzun sayılarda basamakları gruplamak için alt çizgi kullanılabilir: 8_000_000_000L. Alt çizgiler yalnızca okumayı kolaylaştırır; değeri değiştirmez.

Java kodunda ondalık ayırıcı her zaman noktadır. 19.90 Java'da on dokuz virgül doksandır; 19,90 ise sözdizimi hatasıdır. Bu not, Türkçe metin içinde ondalık virgül kullanır (19,90 TL), ama kod parçalarında ve program çıktılarında Java'nın yazdığı biçimi aynen korur.

Örnek 6.2

Dünya nüfusu (yaklaşık 8 milyar) bir değişkende tutulacak. int nufus = 8_000_000_000; satırı neden derlenmez? long nufus = 8_000_000_000; satırı neden yine derlenmez?

8 milyar, int'in en büyük değeri olan 2.147.483.647'den büyüktür; değer int kutusuna sığmaz. Ama asıl sorun daha önce başlar: noktasız ve L'siz yazılan 8_000_000_000 sabitinin kendisi int tipindedir. Derleyici bu sabiti int olarak okumaya çalışırken sınırı aşar ve sol taraftaki tipe bakmadan hata verir:

error: integer number too large
long nufus = 8_000_000_000;
             ^
1 error

Hedef long olsa bile ileti aynıdır. Doğru yazım, sabitin kendisini L ile long yapmaktır: long nufus = 8_000_000_000L;. Bu satır derlenir ve System.out.println(nufus) 8000000000 yazar.

Kayan noktalı sabitlerde de benzer bir kural vardır. 1.5 bir double sabitidir; float oran = 1.5; satırı “possible lossy conversion from double to float” (double'dan float'a olası kayıplı dönüşüm) iletisiyle derlenmez. float istendiğinde sabit 1.5F yazılır. Çoğu programda bu soru hiç doğmaz, çünkü varsayılan seçim zaten double'dır.

6.2.3Tam sayılar bellekte nasıl durur?

Tam sayı tipi seçerken çoğu durumda int yeterlidir ve varsayılan seçim odur. long, int'in yetmediği büyük değerler için kullanılır: dünya nüfusu, milisaniye cinsinden zaman, banka hesabındaki kuruş tutarı. byte ve short ise genellikle büyük veri dizilerinde bellekten kazanmak için seçilir. Bir hesapta beklenen en büyük değer önceden kestirilmelidir; int'in sınırına yaklaşabilecek bir değer baştan long seçilir. Bu kararın önemi, sınır aşıldığında olanlarda yatar. Bunu anlamak için tam sayıların bitlerle nasıl gösterildiğine kısaca bakmak gerekir.

Java tam sayıları ikiye tümleyen (two's complement) gösterimle saklar. Bu gösterimde en soldaki bit işaret bitidir (sign bit): 0 ise sayı sıfır ya da pozitif, 1 ise negatiftir. byte üzerinde birkaç örnek:

Tablo 6.5: Bazı byte değerlerinin bit gösterimi. İşaret biti 1 olan bit dizisinin değeri, işaretsiz değerden 256 çıkarılarak bulunur.
Bitlerİşaretsiz okunuşbyte değeri
0000 000000
0000 000111
0111 1111127127 (en büyük)
1000 0000128128 − 256 = −128 (en küçük)
1000 0010130130 − 256 = −126
1100 1000200200 − 256 = −56
1111 1111255255 − 256 = −1

Tablonun son sütunu basit bir kurala dayanır: işaret biti 1 olan 8 bitlik bir dizi, işaretsiz değerinden 256 çıkarılarak okunur. Aynı kural int için 232 ile, long için 264 ile geçerlidir. Bu kural, taşmayı ve daraltma dönüşümlerini hesaplamak için yeterlidir.

6.2.4Taşma

Bir tam sayı işleminin sonucu tipin aralığına sığmadığında ne olur? Bir arabanın kilometre sayacı 999.999'dan sonra 000.000'a döner. Java tam sayıları da benzer davranır: sonuç aralığın bir ucundan taşar ve öbür uçtan geri girer.

Tanım 6.4

Taşma (overflow), bir tam sayı işleminin sonucunun tipin aralığını aşmasıdır. Java'da taşma hata vermez. Sonucun yalnızca tipe sığan alt bitleri saklanır; değer sessizce aralığın öbür ucuna sarar.

12701111111+ 100000001−12810000000işaret bitibyte (8 bit) örneği
Şekil 6.8: byte için taşma. 127'nin bitleri 0111 1111'dir; 1 eklenince 1000 0000 olur. İşaret biti 1 olduğu için bu dizi −128 diye okunur.

Sunumdaki örnek program iki tipte de taşmayı gösterir:

int x = Integer.MAX_VALUE;
x++;
byte b = 127;
b++;
System.out.println(x + " " + b);
-2147483648 -128

Program satır satır şöyle çalışır:

Tablo 6.6: Taşma örneğinin izleme tablosu.
SatırDeyimxbAçıklama
1int x = Integer.MAX_VALUE;2147483647–bitler 0111 1111 … 1111
2x++;−2147483648–bitler 1000 0000 … 0000
3byte b = 127;−2147483648127bitler 0111 1111
4b++;−2147483648−128bitler 1000 0000
5System.out.println(x + " " + b);−2147483648−128çıktı: -2147483648 -128
işaret biti ↓x01111111111111111111111111111111int: 32 bit. En büyük değer 2.147.483.647 (2³¹ − 1) x01111111111111111111111111111111+ 100000000000000000000000000000001x10000000000000000000000000000000x = −2.147.483.648 (en küçük int)
Şekil 6.9: int için taşma. Integer.MAX_VALUE'da işaret biti 0, öteki 31 bit 1'dir. 1 eklenince elde bütün bitler boyunca ilerler, alttaki 31 bit 0 olur ve işaret biti 1'e döner. Sonuç en küçük int'tir: −2.147.483.648.

İkili toplama, onluk sistemdeki elde mantığıyla çalışır. 0111…1 dizisine 1 eklenince en sağdaki 1 ile 1'in toplamı 0 olur ve 1 elde kalır. Elde sola doğru bütün 1'leri sıfırlayarak ilerler ve en sonunda işaret bitine ulaşır. Sonuç 1000…0'dır. Matematiksel olarak doğru cevap 2.147.483.648'dir, ama bu değer int aralığının dışındadır. İşaret biti 1 olan dizinin değeri 2.147.483.648 − 232 = −2.147.483.648 çıkar. Kilometre sayacı benzetmesiyle, sayı aralığın tepesinden taşar ve dibinden geri girer.

Taşmanın tehlikesi sessiz olmasıdır. Program durmaz, uyarı vermez; yanlış sayıyla hesaba devam eder. Taşma yalnızca sınıra bir eklemekle de olmaz. İki makul görünen sayının çarpımı bile int'i aşabilir:

int a = 50000;
int b = 50000;
System.out.println(a * b);
System.out.println((long) a * b);
System.out.println(a * b / 1000);
long yanlis = a * b;
System.out.println(yanlis);
-1794967296
2500000000
-1794967
-1794967296

50.000 × 50.000 = 2.500.000.000'dır ve int sınırını aşar. İlk satır bu yüzden negatif bir sayı yazar: 2.500.000.000 − 232 = −1.794.967.296. İkinci satırda (long) a dönüşümü çarpmadan önce yapılır; işlem long ile yürür ve doğru sonuç çıkar. Üçüncü satır, taşmanın sonraki işlemlere de bulaştığını gösterir: yanlış çarpım 1000'e bölünür ve yanlış bir bölüm elde edilir. Dördüncü satır en yanıltıcı olanıdır. Sonuç long bir değişkene atanır ama çarpma yine int ile yapılır, çünkü işlemin iki işleneni (operand), yani üzerinde işlem yapılan iki değer de int'tir. Sonuç taşmış hâliyle long'a kopyalanır. Hedefin long olması, işlemin tipini değiştirmez.

Sık yapılan hata

long sonuc = a * b; yazınca çarpmanın long ile yapılacağını sanmak. Java bir ifadenin tipini yalnızca işlenenlerine bakarak belirler; sonucun nereye atanacağına bakmaz. İki int'in çarpımı int'tir ve gerekirse taşar. Doğrusu, işlenenlerden birini işlemden önce long yapmaktır: long sonuc = (long) a * b; ya da a * 1L * b.

6.2.5Sınır sabitleri ve Math.addExact

Tiplerin sınır değerlerini elle yazmak hem zor hem tehlikelidir; 2147483647 sayısında tek bir basamak yanlış yazılabilir. Java bu değerleri hazır sabitler olarak sunar:

Tablo 6.7: int ve long sınır sabitleri (sunumdaki tablo).
SabitDeğer
Integer.MIN_VALUE-2147483648
Integer.MAX_VALUE2147483647
Long.MIN_VALUE-9223372036854775808
Long.MAX_VALUE9223372036854775807

Benzer sabitler öteki tipler için de vardır: Byte.MAX_VALUE, Short.MIN_VALUE, Double.MAX_VALUE. Bu adlar hem okunaklıdır hem de yanlış yazılamaz.

Taşmanın sessizce geçmesi istenmiyorsa Math sınıfının “kesin” (exact) metotları kullanılır. Math.addExact, Math.subtractExact ve Math.multiplyExact işlemi yapar; sonuç tipe sığmıyorsa sessizce sarmak yerine bir istisna (exception) fırlatır. İstisna, programın olağan akışını durduran bir hata bildirimidir.

int x = Integer.MAX_VALUE;
System.out.println(x + 1);
// -2147483648 (sessiz taşma)
System.out.println(
    Math.addExact(x, 1));
// ArithmeticException:
// integer overflow
-2147483648
Exception in thread "main" java.lang.ArithmeticException: integer overflow

İlk println sessiz taşmayı gösterir ve −2147483648 yazar. İkinci satırda Math.addExact(x, 1) çağrılır; sonuç int'e sığmadığı için ArithmeticException fırlatılır ve program “integer overflow” iletisiyle durur. long değerlerde ileti “long overflow” olur. Programın durması ilk bakışta kötü görünür, ama yanlış bir sonuçla devam etmesinden çok daha iyidir: hata hemen fark edilir ve yeri bellidir.

Taşmanın şaşırtıcı bir örneği de mutlak değerdedir. Math.abs(Integer.MIN_VALUE), beklenen 2.147.483.648 yerine yine −2147483648 verir. Nedeni aralığın simetrik olmamasıdır: en küçük int'in mutlak değeri olan 231 int'e sığmaz. Negatifini almak taşmaya yol açar ve sayı kendisine geri döner. Aynı durum -Integer.MIN_VALUE ifadesinde de görülür.

6.2.6Kayan noktalı sayılar

Ondalık kesirli sayılar Java'da kayan noktalı sayı (floating-point number) olarak saklanır. Ad, sayının bilimsel gösterime benzer bir biçimde tutulmasından gelir: bir dizi anlamlı basamak ve virgülün yerini belirten bir üs. 12.345,678 = 1,2345678 × 104 yazılışında virgül, üsse göre “kayar”. Bu sayede aynı 64 bit hem çok küçük (10−300) hem çok büyük (10300) sayıları tutabilir.

double, çoğu yaklaşık hesap için varsayılan tiptir; yaklaşık 15–16 anlamlı basamağı vardır. float yarı boyuttadır ve yaklaşık 7 anlamlı basamak tutar. Aradaki fark kolayca görülür:

System.out.println(0.1 + 0.2);
System.out.println(1.0 - 0.9);
System.out.println(0.1 + 0.2 == 0.3);
System.out.println(0.1 + 0.2 - 0.3);
System.out.println(Math.abs(0.1 + 0.2 - 0.3) < 1e-9);
System.out.println(0.1f + 0.2f);
System.out.println(1.1f * 3);
System.out.println(1.1 * 3);
System.out.println(100.0 / 3);
System.out.println(100.0f / 3);
System.out.println(0.5 + 0.25 == 0.75);
System.out.println(new java.math.BigDecimal(0.1));
System.out.println(new java.math.BigDecimal(0.2));
System.out.println(new java.math.BigDecimal(0.1 + 0.2));
System.out.println(new java.math.BigDecimal(0.3));
0.30000000000000004
0.09999999999999998
false
5.551115123125783E-17
true
0.3
3.3000002
3.3000000000000003
33.333333333333336
33.333332
true
0.1000000000000000055511151231257827021181583404541015625
0.200000000000000011102230246251565404236316680908203125
0.3000000000000000444089209850062616169452667236328125
0.299999999999999988897769753748434595763683319091796875

Bu çıktının satırları tek tek okunduğunda kayan noktalı sayıların temel sorunu ortaya çıkar. Sorun, ikili tabanın bazı ondalık kesirleri tam gösterememesidir. Onluk sistemde 1/3 sonsuz bir kesirdir: 0,3333… Hiçbir sonlu basamak sayısı onu tam yazamaz. İkili sistemde aynı sorun 1/10 için yaşanır. 0,1'in ikili açılımı 0,0001100110011… biçiminde sonsuza kadar sürer. Bellekteki 52 bitlik kesir alanı bu açılımı bir yerde keser ve sayı, 0,1'e çok yakın ama ondan biraz farklı bir değer olarak saklanır.

0.10.10000000000000000555…0.20.20000000000000001110…bellekte saklanan en yakın ikili değer0.1 + 0.2 → 0.30000000000000004bu yüzden double değerler == ile karşılaştırılmaz
Şekil 6.10: 0.1 ve 0.2 bellekte tam saklanamaz; en yakın ikili değerler saklanır. Bu küçük hatalar toplandığında sonuç 0.30000000000000004 yazdırılır.

Çıktının son dört satırı, BigDecimal sınıfıyla bellekteki değerlerin tam ondalık açılımını gösterir. Saklanan “0.1”, gerçekte 0,1000000000000000055511…'dir. Saklanan “0.2” de 0,2'den biraz büyüktür. İki küçük fazlalık toplanınca sonuç, 0,3 için saklanabilecek en yakın double değerine değil, bir sonrakine düşer. println bu değeri 0.30000000000000004 diye yazar. 0,3 için saklanan değer ise 0,29999999999999998889…'dur. İki sayı farklı olduğu için 0.1 + 0.2 == 0.3 ifadesi false verir.

Aynı satırlar başka iki dersi de taşır. 1.1f * 3 float ile 3.3000002 verir, double ile 3.3000000000000003 verir. float'taki hata yedinci basamakta, double'daki hata on altıncı basamakta görünür. Buna karşılık 0.5 + 0.25 == 0.75 true'dur. 0,5 ve 0,25 ikinin kuvvetleriyle yazılabildiği için (1/2 ve 1/4) tam saklanır. Hatanın kaynağı kayan noktalı sayıların kendisi değil, onluk kesirlerin ikili tabanda sonsuz açılmasıdır.

double a = 0.1 + 0.2;
System.out.println(a);
System.out.println(a == 0.3);
System.out.println(Math.abs(a - 0.3) < 1e-9);
0.30000000000000004
false
true

Sunumdaki bu kısa program, karşılaştırmanın doğru yolunu gösterir. İki kayan noktalı sayı == ile karşılaştırılmaz. Bunun yerine aralarındaki farkın mutlak değeri küçük bir eşikle (epsilon) karşılaştırılır: Math.abs(a - 0.3) < 1e-9. Buradaki fark yaklaşık 5,55 × 10−17'dir ve 10−9'dan çok küçüktür; karşılaştırma true verir. Eşiğin büyüklüğü probleme göre seçilir. Milimetre düzeyinde ölçümlerde 10−6 yeterlidir; çok küçük fiziksel büyüklüklerde daha küçük bir eşik gerekir.

Küçük hatalar tekrarlanan işlemlerde birikir. 0,1'i on kez toplayan bir döngü 1,0 değil 0.9999999999999999 bulur. Para gibi kuruşu kuruşuna doğru olması gereken hesaplarda bu birikim kabul edilemez. Böyle hesaplarda ya tutarlar kuruş cinsinden tam sayı (long) olarak tutulur ya da BigDecimal sınıfı kullanılır. BigDecimal, sayıyı onluk basamaklarıyla saklar:

BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b));
System.out.println(a.add(b).compareTo(new BigDecimal("0.3")) == 0);
BigDecimal fiyat = new BigDecimal("19.90");
BigDecimal adet = new BigDecimal("3");
System.out.println(fiyat.multiply(adet));
0.3
true
59.70

BigDecimal nesnesi dizgiyle ("0.1") oluşturulur; new BigDecimal(0.1) yazılırsa hatalı double değeri olduğu gibi kopyalanır. Toplama + yerine add, çarpma multiply metoduyla yapılır. 19,90 TL'lik üç ürünün tutarı tam olarak 59,70 çıkar.

Sık yapılan hata

İki double değeri == ile karşılaştırıp şaşırmak: if (toplam == 1.0) koşulu, toplam 0,1'lerin birikmesiyle oluştuysa hiç sağlanmaz. Kayan noktalı sayılar her zaman bir eşikle karşılaştırılır. Ayrıca kuruş hesabı double ile yapılmaz; tam sayı kuruş ya da BigDecimal kullanılır.

6.2.7char ve boolean

char tipi tek bir karakter tutar ve sabiti tek tırnakla yazılır: 'A', '7', 'ç'. Bilgisayar harfleri doğrudan saklayamaz; her karaktere bir sayı karşılık gelir ve kutuda o sayı durur. Bu eşleme Unicode standardının işidir. 'A' harfinin kodu 65, 'a' harfininki 97, '0' rakamınınki 48, boşluk karakterininki 32'dir. Java char'ı, Unicode metnini 16 bitlik birimlerle saklayan UTF-16 kodlamasının bir kod birimidir. Türkçe harflerin hepsi tek bir char'a sığar: 'ç' 231, 'ğ' 287, 'ı' 305, 'İ' 304, 'ş' 351 kodlarıyla saklanır. (Bazı emojiler gibi çok az kullanılan karakterler iki char gerektirir; bu ayrıntı başlangıç programlarında karşılaşılmaz.)

Tek tırnak ile çift tırnak farklı tipler üretir. 'A' bir char'dır; kutuda 65 sayısı durur. "A" ise tek harflik bir dizgidir (string), yani bir String nesnesidir; değişken bu nesneye giden referansı tutar. İkisi birbirinin yerine kullanılamaz. char c = "A"; satırı “String cannot be converted to char” iletisiyle derlenmez.

boolean tipi yalnızca iki değer alır: true (doğru) ve false (yanlış). Karşılaştırmaların ve koşulların sonucu boolean'dır. C dilinde 0 yanlış, sıfırdan farklı her sayı doğru sayılır; Java'da bu eşleme yoktur. boolean aktif = 1; satırı “int cannot be converted to boolean” hatası verir. Bu katılık, koşullarda sık yapılan bir hatayı derleme sırasında yakalar.

char harf = 'A';'A'= 65 (Unicode)0000000001000001String metin = "A";String nesnesi"A"referansboolean aktif = true;trueyalnızca true / false
Şekil 6.11: Üç farklı kutu. char kutusunda 'A' harfinin kodu olan 65 durur (16 bit). String değişkeni bellekteki bir nesneye referans tutar. boolean kutusu yalnızca true ya da false alır.

6.2.8char aritmetiği

char kutusunda bir sayı durduğu için char değerleriyle aritmetik yapılabilir. Sunumdaki örnek bu ilişkiyi adım adım gösterir:

char c = 'A';
c++;
int k = c + 1;
char d = (char)(c + 1);
System.out.println(c + " " + k + " " + d);
B 67 C
Tablo 6.8: char aritmetiği örneğinin izleme tablosu. Parantez içinde kutudaki sayısal kod verilmiştir.
SatırDeyimc (char)k (int)d (char)Açıklama
1char c = 'A';'A' (65)––kutuda 65 durur
2c++;'B' (66)––kod 1 artar; tip char kalır
3int k = c + 1;'B' (66)67–c + 1 int'tir: 66 + 1 = 67
4char d = (char)(c + 1);'B' (66)67'C' (67)67 yeniden char'a çevrilir
5System.out.println(…);'B'67'C'çıktı: B 67 C
Unicode kod noktaları'A'65'B'66'C'67'D'68▲ c▲ k = 67c + 1: c int'e yükselir, 66 + 1 = 67 (int) Unicode kod noktaları'A'65'B'66'C'67'D'68▲ c▲ d(char)(c + 1): 67 tekrar char'a çevrilir → 'C'
Şekil 6.12: Unicode kod noktaları doğrusunda c, k ve d. c + 1 ifadesinde c int'e yükselir ve sonuç 67 sayısıdır. (char)(c + 1) dönüşümü aynı 67'yi yeniden harf olarak yorumlar: 'C'.

Örneğin can alıcı noktası üçüncü satırdır. c + 1 ifadesinde bir char ile bir int toplanır. Java, aritmetik işlemlerde char'ı önce int'e çevirir; sonuç int olur. k bu yüzden 'C' harfini değil 67 sayısını tutar. Sonucu harf olarak saklamak için açık bir (char) dönüşümü gerekir. Dönüşüm yazılmazsa satır derlenmez:

char c = 'A';
char d = c + 1;
error: incompatible types: possible lossy conversion from int to char
char d = c + 1;
           ^
1 error

İkinci satırdaki c++ ise sorunsuz derlenir; ++ operatörü değişkenin kendi tipini korur. Son satırda yazdırma, tipe göre yapılır: char değer harf olarak (B, C), int değer sayı olarak (67) görünür.

char aritmetiğinin pratik kullanımları vardır:

System.out.println((int) 'A' + " " + (int) 'a' + " " + (int) '0' + " " + (int) ' ');
System.out.println('a' - 'A');
System.out.println((char) ('a' - 32));
System.out.println('7' - '0');
System.out.println('A' + 'B');
System.out.println("" + 'A' + 'B');
System.out.println((int) 'ç' + " " + (int) 'ğ' + " " + (int) 'İ' + " " + (int) 'ı' + " " + (int) 'ş');
char c = 'A';
c += 2;
System.out.println(c);
char h = 66;
System.out.println(h);
65 97 48 32
32
A
7
131
AB
231 287 304 305 351
C
B

Küçük ve büyük harf arasındaki fark İngiliz alfabesinde sabittir: 'a' − 'A' = 97 − 65 = 32. Küçük bir harften 32 çıkarılıp sonuç char'a çevrilince büyük harf elde edilir. Bir rakam karakterinden '0' çıkarılınca rakamın sayısal değeri bulunur: '7' − '0' = 55 − 48 = 7. Bu yöntem, metin olarak okunan bir sayının basamaklarını tek tek işlemek için kullanılır. Türkçe harflerde bu kısa yol işe yaramaz: 'ı' (305) ile 'I' (73) arasındaki fark 32 değildir. Türkçe metinlerde büyük-küçük harf dönüşümü için Java'nın dile duyarlı hazır metotları tercih edilir.

Çıktının beşinci ve altıncı satırları bir tuzağı gösterir. 'A' + 'B' ifadesinde iki char toplanır; ikisi de int'e çevrilir ve sonuç 65 + 66 = 131 sayısıdır, “AB” değil. Harfleri yan yana yazmak için işlemi bir dizgiyle başlatmak gerekir: "" + 'A' + 'B' “AB” verir. Kural basittir: + işleminin işlenenlerinden biri dizgiyse işlem toplama değil birleştirmedir. Son iki satır ise char'ın tip korumasını gösterir. c += 2 'A'yı 'C'ye ilerletir; bileşik atama sonucu kendiliğinden char'a çevirir. char h = 66; satırı da derlenir, çünkü 66 sabiti char aralığındadır ve derleyici bu dönüşümü güvenli bulur; h, 'B' harfini tutar.

6.3Tip dönüşümü

Bir programda farklı tipler sık sık karşılaşır. Bir öğrenci sayısı (int) bir ortalamayla (double) çarpılır, bir harfin koduna (char) bir sayı (int) eklenir, bir double sonuç int bir kutuya yazılmak istenir. Her karşılaşmada Java'nın bir değeri bir tipten öbürüne çevirmesi gerekir. Bu işleme tip dönüşümü (type conversion) denir. Bazı dönüşümleri Java kendiliğinden yapar, bazıları ise programcının açıkça istemesini gerektirir. Ayrımın ölçütü tektir: dönüşümde bilgi kaybı riski var mı?

6.3.1Atama uyumluluğu: genişletme ve daraltma

Tanım 6.5

Genişletme (widening conversion), bir değeri daha geniş aralıklı bir tipe çevirmektir (örneğin int'ten double'a). Java genişletmeyi kendiliğinden, yani örtük olarak yapar. Daraltma (narrowing conversion) ise daha dar aralıklı bir tipe çevirmektir (örneğin double'dan int'e). Daraltma bilgi kaybettirebileceği için açık dönüşüm (cast) ister: hedef tip parantez içinde değerin önüne yazılır, (int) 9.8.

byteshortintlongfloatdoublegenişletme (otomatik): soldan sağachardaraltma: açık dönüşüm gerekir → int x = (int) 9.8; // 9long → float: aralık genişler, basamak kaybı olabilir
Şekil 6.13: Genişletme zinciri: byte → short → int → long → float → double. char da int'e genişler. Soldan sağa dönüşümler otomatiktir; ters yön daraltmadır ve açık dönüşüm gerektirir.

Genişletme güvenlidir, çünkü dar tipin her değeri geniş tipte de vardır. 7 sayısı int'te de double'da da gösterilebilir; double'a çevrilince 7.0 olur ve hiçbir şey kaybolmaz. Aşağıdaki atamaların hepsi derlenir:

int adet = 7;
double genis = adet;
long uzun = adet;
float f = 10;
double d = 'A';
long l = 5;
System.out.println(genis + " " + uzun + " " + f + " " + d + " " + l);
long buyuk = 123456789123L;
float ff = buyuk;
System.out.println(ff);
int i2 = 123456789;
float f2 = i2;
System.out.println(f2 + " " + (int) f2);
7.0 7 10.0 65.0 5
1.2345679E11
1.2345679E8 123456792

İlk satırdaki değerler beklenen dönüşümleri gösterir: int 7 double'da 7.0, long'da 7 olur; 10 float'ta 10.0; 'A' double'da 65.0 olur. Zincirin bir halkasında ise ince bir kayıp vardır. long'dan float'a ve int'ten float'a geçiş “genişletme” sayılır, çünkü aralık genişler. Ama float yalnızca yaklaşık 7 anlamlı basamak tutar. 123.456.789 sayısı float'a çevrilip geri alınınca 123.456.792 çıkar; son basamaklar yuvarlanmıştır. Java bu dönüşümde uyarı vermez. Büyük tam sayılarla çalışırken float yerine double seçmek bu sorunu büyük ölçüde ortadan kaldırır.

Daraltmada ise bilgi kaybı olağan bir durumdur. double 9.8 bir int kutusuna sığmaz; ondalık kısmın atılması gerekir. Java bu kararı programcıya bırakır ve açık dönüşüm yazılmadıkça atamayı reddeder:

double oran = 9.8;
int dar = oran;
error: incompatible types: possible lossy conversion from double to int
int dar = oran;
          ^
1 error

İletideki “possible lossy conversion” ifadesi “olası kayıplı dönüşüm” demektir. Derleyici kaybın gerçekten olup olmayacağını bilmez; yalnızca olabileceğini bildirir. int dar = (int) oran; yazıldığında programcı kaybı kabul ettiğini açıkça söylemiş olur ve satır derlenir. Aynı kural sabitler için de geçerlidir: int x = 5L; satırı, 5 sayısı int'e rahatça sığdığı hâlde “possible lossy conversion from long to int” verir, çünkü sabitin tipi long'dur.

Genişletme (otomatik)7int · 32 bitkayıpsız7.0double · 64 bitdouble genis = adet;Daraltma (açık dönüşüm)9.8double(int)9int.8 atılır (yuvarlanmaz)
Şekil 6.14: Genişletme ve daraltma. int 7, double kutuya kayıpsız olarak 7.0 biçiminde geçer. double 9.8 ise (int) ile 9'a çevrilir: .8 atılır, yuvarlama yapılmaz.

Dönüşüm, kaynak değişkeni değiştirmez. (int) oran ifadesi, oran kutusundaki 9.8'i okuyup 9 değerini üretir; oran hâlâ 9.8'dir. Dönüşüm yalnızca üretilen sonucu etkiler. Bir dönüşümü değişkenin tipini kalıcı olarak değiştirmek sanmak yaygın bir yanılgıdır; Java'da bir değişkenin tipi bildirildiği anda belirlenir ve bir daha değişmez.

6.3.2Tip dönüşümü adım adım

Sunumdaki örnek, genişletmeyi, daraltmayı ve iki bölme biçimini tek programda gösterir:

int adet = 7;
double genis = adet;       // 7.0
double oran = 9.8;
int dar = (int) oran;      // 9
int bolum = 5 / 2;         // 2
double dogru = 5.0 / 2;    // 2.5
Tablo 6.9: Tip dönüşümü örneğinin izleme tablosu. Her satırda yalnızca yeni ayrılan kutu verilmiştir; öncekiler değişmez.
SatırDeyimYeni kutuDeğerNe oldu?
1int adet = 7;adet (int)732 bitlik int kutusu
2double genis = adet;genis (double)7.0genişletme: otomatik, kayıpsız
3double oran = 9.8;oran (double)9.864 bitlik double kutusu
4int dar = (int) oran;dar (int)9daraltma: kesir atıldı; oran hâlâ 9.8
5int bolum = 5 / 2;bolum (int)2int / int: tam sayı bölmesi
6double dogru = 5.0 / 2;dogru (double)2.5double / int: 2, 2.0'a genişler
Bellek (değişkenler)adet7intgenis7.0doubleoran9.8doubledar9intbolum2intdogru2.5doubledouble / int  →  5.0 / 2.0  →  2.5
Şekil 6.15: Programın sonunda bellek. Altı kutunun dördü önceki satırlarda doldu; son satırda 5.0 / 2.0 → 2.5 hesaplanır ve dogru kutusuna yazılır.

Dördüncü satırdaki dönüşüm yuvarlama değil kesmedir (truncation): 9.8 en yakın tam sayı olan 10'a değil, 9'a çevrilir. Ondalık kısım ne olursa olsun atılır; 9.99 da 9 olur. Beşinci ve altıncı satırlar ise bir sonraki kesimin konusunu önceden gösterir: aynı 5 ve 2 sayıları, yazılış biçimlerine göre 2 ya da 2.5 sonucunu verir.

6.3.3Dönüşümlerde taşma ve kesme

Açık dönüşüm, derleyiciye “kaybı kabul ediyorum” demektir. Kaybın ne kadar olacağı ise dönüşümün türüne bağlıdır. Sunumdaki tablo, sık karşılaşılan durumları bir arada verir. Değerler JDK 21'de çalıştırılarak doğrulanmıştır.

Tablo 6.10: Daraltma dönüşümlerinin ve Math.round'un sonuçları (sunumdaki tablo).
İfadeSonuçNeden
(byte) 200-56yalnız alt 8 bit kalır: 200 − 256
(int) 3000000000L-1294967296long → int: üst bitler atılır
(int) 3.93ondalık kısım kesilir, yuvarlanmaz
(int) -2.7-2sıfıra doğru kesilir
(int) 3.9e102147483647double → int: int sınırında sıkışır
Math.round(3.9)4en yakın tam sayı (long döner)
Math.round(-2.7)-3en yakın tam sayı
Math.round(3.9e10)39000000000long olduğu için sığar

Tablodaki satırlar üç ayrı davranışı gösterir.

Tam sayıdan daha dar tam sayıya. Dönüşüm, kaynak sayının yalnızca hedef tipe sığan alt bitlerini tutar ve üst bitleri atar. 200'ün ikili gösterimi 1100 1000'dir. Bu sekiz bit byte'a aynen kopyalanır. İşaret biti 1 olduğu için değer 200 − 256 = −56 olarak okunur. 3.000.000.000 için hesap int boyutunda yapılır: 3.000.000.000 − 232 = 3.000.000.000 − 4.294.967.296 = −1.294.967.296. Taşmadaki sarma kuralıyla aynı kuraldır. Birkaç örnek daha: (byte) 130 −126, (byte) 256 0, (byte) -129 127, (short) 40000 −25536 verir.

Kayan noktalıdan tam sayıya. Ondalık kısım atılır ve sayı sıfıra doğru kesilir: pozitif sayılar aşağı, negatif sayılar yukarı gider. 3.9 → 3 ve −2.7 → −2 bu yüzdendir. −2.7'nin −3'e değil −2'ye gitmesi şaşırtıcı gelebilir; −2, −3'ten daha büyüktür ve sıfıra daha yakındır. Sayı hedef tipin aralığını aşıyorsa bitler kesilmez; sonuç aralığın ucunda sıkışır. 3,9 × 1010 int'e sığmaz ve Integer.MAX_VALUE olan 2147483647 çıkar. Aynı biçimde (int) -3.9e10 en küçük int'i, (int) Double.NaN ise 0 verir.

Yuvarlama. Kesme yerine en yakın tam sayı isteniyorsa Math.round kullanılır. 3.9 → 4 ve −2.7 → −3 olur. Math.round, double alınca long döndürür; bu yüzden 3,9 × 1010 sorunsuz sığar. Sonuç int bir kutuya yazılacaksa ikinci bir dönüşüm gerekir: int ri = (int) Math.round(x);. Dönüşüm yazılmazsa int r = Math.round(2.7); satırı “possible lossy conversion from long to int” hatası verir.

Sık yapılan hata

(int) dönüşümünün sayıyı yuvarladığını sanmak. (int) 9.8 10 değil 9'dur; (int) -2.7 −3 değil −2'dir. Yuvarlama için Math.round, int sonuç için (int) Math.round(x) yazılır. Daraltma dönüşümü hata vermez; veri sessizce kaybolur.

6.3.4Tam sayı bölmesi: 5 / 2 ile 5.0 / 2

Java'da bölme operatörü / iki farklı iş yapar ve hangisini yapacağına işlenenlerin tipine bakarak karar verir. İki işlenen de tam sayıysa tam sayı bölmesi (integer division) yapılır: sonuç bölümün tam kısmıdır, kalan atılır. İşlenenlerden en az biri kayan noktalıysa öbürü de kayan noktalıya yükseltilir ve olağan bölme yapılır.

System.out.println(5 / 2);
System.out.println(5.0 / 2);
System.out.println(5 / 2.0);
System.out.println((double) 5 / 2);
System.out.println((double) (5 / 2));
System.out.println(5 % 2);
2
2.5
2.5
2.5
2.0
1
Tablo 6.11: Altı satırın işlenen tipleri ve sonuçları.
İfadeİşlenen tipleriYapılan işlemÇıktı
5 / 2int, inttam sayı bölmesi2
5.0 / 2double, int2 → 2.0; 5.0 / 2.02.5
5 / 2.0int, double5 → 5.0; 5.0 / 2.02.5
(double) 5 / 2double, intönce dönüşüm: 5.0; sonra 5.0 / 2.02.5
(double) (5 / 2)int, intönce 5 / 2 = 2; sonra 2 → 2.02.0
5 % 2int, intkalan: 5 = 2 · 2 + 11
5int/2int→2int 5.0double/2int→2.5double 5int/2int→2int (double)2int→2.0double
Şekil 6.16: Bölme ifadelerinin işlenen tipleri. İki int: sonuç 2 (int). Bir double: int işlenen double'a genişler ve sonuç 2.5 olur. (double) (5 / 2) ifadesinde önce parantez içi int olarak hesaplanır (2), dönüşüm ancak sonra gelir (2.0).

Dördüncü ve beşinci satırlar arasındaki fark yalnızca bir çift paranteze dayanır ama sonuç değişir. Tip dönüşümü bölmeden önce gelen bir tekli operatördür. (double) 5 / 2 ifadesinde önce 5 double'a çevrilir, sonra bölme kayan noktalı yapılır. (double) (5 / 2) ifadesinde ise parantez önce hesaplanır; bölme iki int arasında yapılır ve 0,5 kaybolur. Sonradan yapılan dönüşüm 2'yi 2.0 yapar ama kaybolan kesri geri getirmez.

Örnek 6.3

70, 85 ve 90 notlarının ortalaması dört farklı ifadeyle hesaplanıyor. Hangisi doğru sonucu verir?

int n1 = 70, n2 = 85, n3 = 90;
System.out.println((n1 + n2 + n3) / 3);
System.out.println((n1 + n2 + n3) / 3.0);
System.out.println((double) (n1 + n2 + n3) / 3);
System.out.printf("%.2f%n", (n1 + n2 + n3) / 3.0);
81
81.66666666666667
81.66666666666667
81.67

Toplam 70 + 85 + 90 = 245'tir. İlk satırda 245 ve 3 int olduğu için tam sayı bölmesi yapılır: 245 / 3 = 81, kalan 2 atılır. İkinci satırda 3.0 double'dır; 245 de double'a yükselir ve 245 / 3 = 81,666… bulunur. Üçüncü satırda dönüşüm bölmeden önce yapılır ve sonuç yine doğrudur. Son satır sonucu iki ondalık basamakla yazar: 81.67. Doğru ortalama için bölmenin en az bir işleneni kayan noktalı olmalıdır.

Tam sayı bölmesi her zaman bir hata değildir; çoğu zaman tam da istenen işlemdir. 17 elma 5'erli paketlere konursa 17 / 5 = 3 tam paket çıkar. 3725 saniyede 3725 / 3600 = 1 tam saat vardır. Bu tür hesaplarda tam sayı bölmesi, kalanı veren % operatörüyle birlikte kullanılır. Hata, ortalama ya da oran gibi kesirli bir sonuç beklenirken iki tam sayının bölünmesidir.

6.3.5Tip yükseltme

Bölmede görülen kural, bütün ikili aritmetik operatörlerde geçerlidir.

Tanım 6.6

Tip yükseltme (type promotion), karma tipli bir ifadede dar tipteki işlenenin, işlem yapılmadan önce geniş tipe otomatik çevrilmesidir. Sonuç geniş tiptedir: int + double → double, int * long → long. Ayrıca byte, short ve char işlenenler her aritmetik işlemde önce int'e yükseltilir.

byte8 bitshort16 bitint32 bitlong64 bitfloat32 bitdouble64 bitchar16 bit (işaretsiz)Oklar yönünde dönüşüm otomatiktir · ters yönde açık (tip) dönüşümü gerekirlong → float: kesinlik kaybı olabilir
Şekil 6.17: Tip yükseltme sırası. Oklar yönündeki dönüşümler otomatiktir; ters yönde açık dönüşüm gerekir. char 16 bitlik işaretsiz bir tiptir ve int'e yükselir. long → float geçişinde kesinlik kaybı olabilir.

Kuralın ikinci yarısı şaşırtıcıdır: iki byte'ın toplamı bile int'tir. Java bu küçük tiplerle doğrudan aritmetik yapmaz; işlemciler 32 bitlik işlemlerde daha verimli çalışır ve ara sonucun taşma riski azalır. Bunun sonucu olarak aşağıdaki satırlar derlenmez:

short a = 1, b = 2;
short c = a + b;
error: incompatible types: possible lossy conversion from int to short
short c = a + b;
            ^
1 error

a + b int'tir ve int'ten short'a daraltma açık dönüşüm ister: short c = (short) (a + b);. Aynı nedenle byte b = 10; b = b + 5; de derlenmez. Bu davranış, char aritmetiğinde görülen c + 1 ifadesinin neden int olduğunu da açıklar.

Bir ifadede birden çok işlem varsa yükseltme her işlem için ayrı ayrı yapılır. 1 / 2 * 10.0 ifadesinde önce 1 / 2 iki int arasında hesaplanır ve 0 olur; ancak ondan sonra 0, 10.0 ile çarpılırken double'a yükselir. Sonuç 5.0 değil 0.0'dır. Bir double işlenenin ifadede bir yerde bulunması, ifadenin her yerini kayan noktalı yapmaz; yükseltme yalnızca o işlenenin katıldığı işlemde gerçekleşir.

6.4Operatörler

Tanım 6.7

Operatör (operator), bir ya da daha fazla değer üzerinde işlem yapan simgedir: +, *, <, &&. Operatörün üzerinde işlem yaptığı değerlere işlenen (operand) denir. Operatörlerin, değişkenlerin ve sabitlerin bir değer üreten birleşimine ifade (expression) denir: a + b * 2.

Operatörler, aldıkları işlenen sayısına göre adlandırılır. -x, !aktif, i++ gibi tek işlenenli operatörler tekli, a + b gibi iki işlenenli olanlar ikili operatörlerdir. Her ifadenin bir değeri ve bir tipi vardır. 7 / 2 ifadesinin değeri 3, tipi int'tir; 7 > 2 ifadesinin değeri true, tipi boolean'dır.

6.4.1Aritmetik operatörler ve kalan

Tablo 6.12: Aritmetik operatörler (sunumdaki tablo).
OperatörAnlamÖrnek
+ -Toplama / çıkarmaa + b
* /Çarpma / bölmea * b
%Kalansayi % 2
++ --Bir artır / azalti++
+= -=Bileşik atamatoplam += deger

Toplama, çıkarma ve çarpma matematikteki gibidir; tek fark, sonucun tipin aralığına sığmak zorunda olmasıdır. Bölme operatörünün iki yüzü tam sayı bölmesi kesiminde görüldü. Bu kesimin asıl konusu % operatörüdür.

Tanım 6.8

Kalan (remainder) operatörü %, tam sayı bölmesinden artan değeri verir. a ve b tam sayıları için a = (a / b) · b + (a % b) eşitliği her zaman sağlanır. Bölmede bölünen sayıya bölünen, bölme sonucunun tam kısmına bölüm denir.

1. grup2. grup3. grupartan17 / 5 = 317 % 5 = 2
Şekil 6.18: 17 nesne beşerli gruplanır. 3 tam grup oluşur (17 / 5 = 3) ve 2 nesne artar (17 % 5 = 2).

Şekil 6.18, 17 nesnenin beşerli gruplanmasını gösterir: / kaç tam grup oluştuğunu, % kaç nesne arttığını söyler. Eşitlik kontrol edilebilir: 3 · 5 + 2 = 17. Kalan operatörü, ilk bakışta sanıldığından çok daha sık kullanılır:

  • Çift-tek denetimi. sayi % 2 == 0 ise sayı çifttir. Genel olarak a % b == 0, a'nın b'ye tam bölündüğünü gösterir.
  • Basamak ayırma. sayi % 10 son basamağı verir; sayi / 10 son basamağı atar. 1234 için 1234 % 10 = 4 ve 1234 / 10 = 123'tür.
  • Birim dönüşümü. Saniyeyi dakika ve saniyeye, kuruşu lira ve kuruşa ayırmak: 1575 kuruş, 1575 / 100 = 15 lira ve 1575 % 100 = 75 kuruştur.
  • Döngüsel sayma. Haftanın günü, saat kadranı gibi başa dönen değerler: bugün çarşambaysa (2) 10 gün sonrası (2 + 10) % 7 = 5, yani cumartesidir (pazartesi 0 kabul edilirse).

Negatif sayılarda kalanın işareti bir kurala bağlıdır: Java'da kalan her zaman bölünenin işaretini alır. Bu kural, tam sayı bölmesinin sıfıra doğru kesmesinden doğar. Aşağıdaki programın ilk dört satırı negatif işlenenleri, sonraki satırları ise bu kesimde ve sıfıra bölme kesiminde geçen öteki durumları sınar:

System.out.println(7 / 2 + " " + 7 % 2);
System.out.println(-7 / 2 + " " + -7 % 2);
System.out.println(7 / -2 + " " + 7 % -2);
System.out.println(-7 / 3 + " " + -7 % 3 + " " + Math.floorMod(-7, 3) + " " + Math.floorDiv(-7, 3));
System.out.println(17 / 5 + " " + 17 % 5);
System.out.println(5.5 % 2);
System.out.println(1 / 2 * 10 + " " + 10 * 1 / 2);
System.out.println(1.0 / 0 + " " + -1.0 / 0 + " " + 0.0 / 0);
double nan = 0.0 / 0;
System.out.println((nan == nan) + " " + (nan != nan) + " " + Double.isNaN(nan));
System.out.println(Math.sqrt(-1) + " " + (1.0 / 0 - 1.0 / 0) + " " + (1.0 / 0 + 5));
System.out.println(1 / 0.0 > 1e308);
3 1
-3 -1
-3 1
-2 -1 2 -3
3 2
1.5
0 5
Infinity -Infinity NaN
false true true
NaN NaN Infinity
true

Çıktının ilk dört satırı bu kuralı gösterir. −7 / 2 = −3'tür (−3,5 sıfıra doğru kesilir), kalan ise −7 − (−3 · 2) = −1'dir. 7 / −2 = −3 ve 7 % −2 = 1'dir; bölünen 7 pozitif olduğu için kalan da pozitiftir. −7 % 3 = −1'dir; matematikte alışılan “0 ile 2 arasındaki kalan” 2'dir. Bu değer gerekiyorsa Math.floorMod(-7, 3) kullanılır. Bu yüzden bir sayının tek olup olmadığı sayi % 2 == 1 ile değil sayi % 2 != 0 ile denetlenir; −7 için ilki yanlış sonuç verir.

Beşinci satır şekildeki 17 nesne örneğidir. % operatörü kayan noktalı sayılarla da çalışır: 5,5 = 2 · 2 + 1,5 olduğu için 5.5 % 2 1.5 verir. Çıktının yedinci satırı ise işlem sırasının tam sayı bölmesindeki etkisini gösterir: 1 / 2 * 10 önce 1 / 2 = 0 hesaplar ve 0 bulur; 10 * 1 / 2 önce 10 · 1 = 10 hesaplar ve 5 bulur. Matematikte iki ifade eşittir; Java'da tam sayı bölmesi yüzünden değildir.

6.4.2Sıfıra bölme ve özel değerler

Matematikte sıfıra bölme tanımsızdır. Java bu durumu tam sayılarda ve kayan noktalı sayılarda farklı ele alır.

Tablo 6.13: Sıfıra bölme ve özel değerler (sunumdaki tablo).
İfadeSonuçAçıklama
1 / 0ArithmeticException: / by zerotam sayı bölmesinde sıfıra bölme çalışma zamanı hatasıdır
1.0 / 0Infinityondalıkta hata yok: sonsuz
-1.0 / 0-Infinityeksi sonsuz
0.0 / 0NaNNot a Number: tanımsız sonuç
NaN == NaNfalseNaN kendine bile eşit değildir
NaN != NaNtrueNaN denetimi için Double.isNaN(x)
-7 % 3-1kalanın işareti bölünenin işaretidir
5.5 % 21.5% kayan noktalı sayılarda da çalışır

Tam sayılarda sıfıra bölme programı durdurur. Derleme başarılıdır, çünkü derleyici çoğu zaman bölenin sıfır olacağını önceden bilemez. Hata program çalışırken ortaya çıkar:

int a = 1;
int b = 0;
System.out.println("önce");
System.out.println(a / b);
System.out.println("sonra");
önce
Exception in thread "main" java.lang.ArithmeticException: / by zero

“önce” yazılır, bölme satırında JVM bir ArithmeticException fırlatır ve program orada biter; “sonra” hiç yazılmaz. Aynı hata kalan işleminde de görülür: 5 % 0 da “/ by zero” iletisiyle durur.

Kayan noktalı sayılarda ise sıfıra bölme hata vermez. Kayan nokta standardı (IEEE 754) bu durumlar için özel değerler tanımlar. Pozitif bir sayının sıfıra bölümü Infinity (sonsuz), negatifinki -Infinity olur. 0,0 / 0 gibi anlamı olmayan işlemler NaN (Not a Number, “sayı değil”) üretir. Math.sqrt(-1) ve ∞ − ∞ da NaN verir. Bu değerler programı durdurmaz, sonraki işlemlere taşınır: ∞ + 5 = ∞, NaN'lı her aritmetik işlem yine NaN'dır. Bir hesabın en sonunda beklenmedik biçimde NaN çıkması, genellikle çok önceki bir satırda sıfıra bölündüğünü gösterir.

NaN'ın en tuhaf özelliği kendine eşit olmamasıdır: x == x, x NaN ise false verir. Bu yüzden NaN denetimi x == Double.NaN ile yapılamaz; her zaman false döner. Doğru yol Double.isNaN(x) metodudur.

Sık yapılan hata

Kayan noktalı bölmede bölenin sıfır olabileceğini unutmak. Tam sayı bölmesi bu hatayı istisnayla hemen gösterir; kayan noktalı bölme ise Infinity ya da NaN üretip sessizce devam eder. Bölen sıfır olabiliyorsa bölmeden önce denetlenir.

6.4.3Artırma ve azaltma

Bir değişkeni 1 artırmak ya da 1 azaltmak o kadar sık yapılır ki Java bunun için ayrı operatörler sunar: ++ ve --. Tek başına bir deyim olarak yazıldıklarında i++;, i = i + 1; ile aynıdır. İnce bir fark, operatör bir ifadenin içinde kullanıldığında ortaya çıkar.

Tanım 6.9

Sonek (postfix) biçiminde (i++) ifadenin değeri değişkenin eski değeridir; değişken bu değer kullanıldıktan sonra artırılır. Önek (prefix) biçiminde (++i) önce değişken artırılır; ifadenin değeri yeni değerdir.

int i = 5;
int a = i++;   // önce kullan, sonra artır
int b = ++i;   // önce artır, sonra kullan
i--;
System.out.println(i + " " + a + " " + b);
6 5 7
Tablo 6.14: Önek ve sonek artırma örneğinin izleme tablosu.
SatırDeyimİfadenin değeriiab
1int i = 5;–5––
2int a = i++;i++ → 5 (eski değer)65–
3int b = ++i;++i → 7 (yeni değer)757
4i--;tek başına deyim657
5System.out.println(…);çıktı: 6 5 7657
Bellek (değişkenler)i5inta5intb?intİfadei++değer 5, sonra i + 1 Bellek (değişkenler)i7inta5intb?intİfade++ii = 7
Şekil 6.19: İkinci ve üçüncü satır. Sonek i++: ifadenin değeri 5'tir, a = 5 olur, sonra i 6'ya çıkar. Önek ++i: önce i = 7 olur, ifadenin değeri yeni değer 7'dir ve b = 7 olur.

İkinci satırda i++ önce eski değer olan 5'i üretir; bu değer a'ya yazılır. Artırma da aynı satırda gerçekleşir ve i 6 olur. Üçüncü satırda ++i önce i'yi 7 yapar, sonra bu yeni değeri üretir; b = 7 olur. Dördüncü satırda i-- tek başına bir deyimdir. Değeri hiçbir yerde kullanılmadığı için önek ve sonek arasındaki fark burada önemsizdir: --i; yazılsaydı da i 6 olurdu.

Önek ve sonek farkı tuzaklara açıktır. j = j++; satırı j'yi artırmaz: sağ taraf eski değer olan 5'i üretir, j bir an 6 olur, sonra atama 5'i yeniden yazar. Bir ifadede aynı değişkenin birden çok kez artırılması da okunması zor kod üretir. k++ + k++ ifadesi k = 1 için 1 + 2 = 3 verir ve k'yi 3 yapar. Sonuçlar Java'da kesin kurallarla belirlidir ama okuyan herkesi düşündürür. ++ ve --'yi yalnızca tek başına bir deyim olarak kullanmak kodu açık tutar.

6.4.4Bileşik atama ve örtük daraltma

Bileşik atama operatörleri, +=, -=, *=, /= ve %=, yalnızca kısa bir yazım değildir. Sonucu soldaki değişkenin tipine kendiliğinden daraltırlar. Java dil tanımı, x op= y deyimini x = (T) (x op y) diye tanımlar; buradaki T, x'in tipidir.

byte b = 10;
b += 5;
int i = 5;
i += 3.7;
// b = b + 5;   // derlenmez: int → byte
System.out.println(b + " " + i);
15 8
Tablo 6.15: Bileşik atama örneğinin izleme tablosu.
SatırDeyimDerleyicinin gördüğübi
1byte b = 10;10–
2b += 5;b = (byte) (b + 5): 10 + 5 = 15 (int) → 15 (byte)15–
3int i = 5;155
4i += 3.7;i = (int) (i + 3.7): 5 + 3.7 = 8.7 (double) → 8158
5// b = b + 5;yorum: derlenmeyeceği için devre dışı158
6System.out.println(b + " " + i);çıktı: 15 8158
b += 5 ≡ b = (byte)(b + 5)b + 5int ifadesi10 + 5 = 15int(byte) 15byte'a daralt i += 3.7 ≡ i = (int)(i + 3.7)i + 3.7double ifadesi5 + 3.7 = 8.7double(int) 8.7kesir atılır b = b + 5;b + 5 → int (byte + int = int)error: incompatible types: possible lossy conversion from int to byte
Şekil 6.20: Bileşik atamanın örtük dönüşümü. b += 5, int toplamı byte'a daraltır. i += 3.7, double toplamı int'e daraltır ve kesri atar. Aynı işi b = b + 5; biçiminde yazmak derleme hatası verir.

Beşinci satırdaki yorum önemlidir. b = b + 5; satırında b + 5 int'tir ve int'ten byte'a atama açık dönüşüm ister; satır derlenmez. Aynı işi yapan b += 5; ise derlenir, çünkü dönüşüm bileşik atamanın tanımında gizlidir. Bu kolaylık bir tehlike de taşır: dönüşüm sessizdir. i += 3.7 sonucu 8.7'yi uyarı vermeden 8'e indirir. byte sınırı aşıldığında taşma da sessizce olur:

int i = 5;
i *= 3 + 7;
System.out.println(i);
int x = 17;
x %= 5;
System.out.println(x);
int y = 20;
y /= 3;
System.out.println(y);
double z = 20;
z /= 3;
System.out.println(z);
byte b = 120;
b += 10;
System.out.println(b);
short s = 1;
s++;
System.out.println(s);
char c = 'a';
c -= 32;
System.out.println(c);
50
2
6
6.666666666666667
-126
2
A

Çıktının satırları sırasıyla okunur. i *= 3 + 7 deyiminde sağ taraf bütünüyle önce hesaplanır: 5 · (3 + 7) = 50. x %= 5, 17'yi kalan olan 2'ye çevirir. y /= 3 int'te tam sayı bölmesidir: 20 / 3 = 6; double z ile aynı işlem 6.666666666666667 verir. b += 10, 120 + 10 = 130'u byte'a sığdırmak için 256 çıkarır: −126. Son iki satır, ++ ve -= operatörlerinin de değişkenin tipini koruduğunu gösterir: short s 2 olur, char c 'a'dan 32 çıkarılarak 'A' olur.

Sık yapılan hata

x += y ile x = x + y deyimlerini her durumda aynı sanmak. İki fark vardır. Birincisi, bileşik atama örtük daraltma yapar; x = x + y ise tipler uymadığında derlenmez. İkincisi, bileşik atamada sağ taraf bir bütündür: x *= a + b, x = x * (a + b) demektir, x = x * a + b değil.

6.4.5Karşılaştırma ve mantık operatörleri

Programlar karar verir: not 60'tan büyükse geçti yaz, yaş 18'den küçükse uyar. Bu kararların temelinde, sonucu true ya da false olan ifadeler vardır.

Tablo 6.16: Karşılaştırma ve mantık operatörleri (sunumdaki tablo).
OperatörAnlamÖrnek
== !=Eşit / farklıa == b
< <= > >=Sıra karşılaştırmasınot >= 60
&&İki koşul da doğruyas >= 18 && belgeVar
||En az biri doğruhaftaSonu || tatil
!Mantıksal değil!aktif

Karşılaştırma operatörleri (comparison operator) iki değeri karşılaştırır ve her zaman boolean bir sonuç üretir. Eşitlik iki eşittir işaretiyle yazılır: ==. Tek eşittir (=) atamadır; ikisini karıştırmak en sık yapılan hatalardandır. != “eşit değil” demektir. Matematikteki ≤ ve ≥ işaretleri <= ve >= diye iki karakterle yazılır.

Mantık operatörleri (logical operator) boolean değerleri birleştirir. && (ve) iki koşul da doğruysa doğrudur. || (veya) en az biri doğruysa doğrudur. ! (değil) tek işlenenlidir ve değeri tersine çevirir. Dört olası durumun tamamı bir doğruluk tablosunda gösterilir:

Tablo 6.17: && ve || için doğruluk tablosu.
pqp && qp || q!p
falsefalsefalsefalsetrue
falsetruefalsetruetrue
truefalsefalsetruefalse
truetruetruetruefalse
int yas = 20;
boolean belgeVar = true;
System.out.println(yas >= 18 && belgeVar);
System.out.println(!belgeVar);
System.out.println(yas > 65 || yas < 12);
System.out.println(5 > 3 == true);
int not = 75;
System.out.println(not >= 60 && not <= 100);
System.out.println(true ^ true);
true
false
false
true
true
false

Yaş 20 ve belge varken yas >= 18 && belgeVar doğrudur. yas > 65 || yas < 12 ise iki koşul da yanlış olduğu için yanlıştır. Bir değerin bir aralıkta olup olmadığı iki karşılaştırmanın && ile birleşimiyle yazılır: not >= 60 && not <= 100. (not Java'da anahtar kelime değildir; değişken adı olarak kullanılabilir.) Son satırdaki ^ “ya o ya bu” (özel veya) operatörüdür; iki işlenen farklıysa doğrudur.

Matematikte alışılan 0 < x < 10 yazılışı Java'da çalışmaz:

error: bad operand types for binary operator '<'
System.out.println(0 < x < 10);
                         ^
  first type:  boolean
  second type: int
1 error

0 < x önce hesaplanır ve boolean bir değer üretir. Ardından bu boolean değer 10 ile karşılaştırılmak istenir; boolean ile int arasında < tanımlı değildir. Doğru yazılış 0 < x && x < 10'dur.

6.4.6İşlem önceliği ve ifade ağacı

2 + 3 * 4 ifadesinin değeri 20 mi, 14 mü? Matematikte çarpmanın toplamadan önce yapıldığı bilinir ve cevap 14'tür. Java da aynı kuralı izler ve bunu bütün operatörlere genişletir.

Tanım 6.10

İşlem önceliği (operator precedence), bir ifadede farklı operatörlerin hangi sırayla uygulanacağını belirleyen kurallar dizisidir. Aynı öncelikteki operatörler soldan sağa uygulanır; atama operatörleri ise sağdan sola uygulanır.

( )parantez!  ++  --  (tip)tekli*  /  %çarpımsal+  -toplamsal<  <=  >  >=sıra==  !=eşitlik&&ve||veya=  +=  -=atama
Şekil 6.21: İşlem önceliği merdiveni. Üstteki basamak önce uygulanır: parantez, tekli operatörler (!, ++, --, tip dönüşümü), çarpımsal, toplamsal, sıra karşılaştırması, eşitlik, &&, || ve en son atama.

Merdiven, sunumdaki dört kurala karşılık gelir. Parantez içi her zaman önce değerlendirilir; tekli ! ve tip dönüşümü de erken uygulanır. Çarpma, bölme ve kalan; toplama ve çıkarmadan önce gelir. Karşılaştırmalar aritmetikten sonra, && ve || ise karşılaştırmalardan sonra değerlendirilir; &&, ||'den önce gelir. Aynı düzeydeki operatörlerde soldan sağa gidilir. Bu yüzden 10 - 4 - 3 ifadesi (10 − 4) − 3 = 3'tür, 10 − (4 − 3) = 9 değil. 100 / 10 / 5 de (100 / 10) / 5 = 2'dir. Atama ise sağdan sola işler: a = b = 4; önce b'ye 4'ü yazar, sonra bu değeri a'ya yazar.

Derleyici bir ifadeyi okurken onu, önceliğe göre bir ifade ağacına (expression tree) dönüştürür. Ağacın yapraklarında sabitler ve değişkenler, iç düğümlerinde operatörler bulunur. En alttaki işlemler önce, kökteki işlem en son hesaplanır. Sunumdaki ifade bu süreci adım adım gösterir:

2 + 3 * 4 > 10 && 7 % 2 == 1
2+3*4>10&&7%2==12 + 3 * 4 > 10 && 7 % 2 == 1
Şekil 6.22: 2 + 3 * 4 > 10 && 7 % 2 == 1 ifadesinin ağacı. Kökte en düşük öncelikli operatör && bulunur; onun iki alt ağacı iki karşılaştırmadır.
Tablo 6.18: İfadenin adım adım sadeleşmesi. Her adımda en derindeki işlem hesaplanır.
AdımHesaplanan işlemİfadenin yeni hâli
0–2 + 3 * 4 > 10 && 7 % 2 == 1
13 * 4 = 12 (*, +'dan önce)2 + 12 > 10 && 7 % 2 == 1
22 + 12 = 1414 > 10 && 7 % 2 == 1
314 > 10 → truetrue && 7 % 2 == 1
47 % 2 = 1true && 1 == 1
51 == 1 → truetrue && true
6true && true → truetrue
2143124true10&&7%2==1true && 7 % 2 == 1 2143124true10true712true1true
Şekil 6.23: Ağacın 3. ve 6. adımı. Sol alt ağaç true değerine indirgenmiştir. Sol taraf true olduğu için && sağ tarafı da hesaplar; son adımda kök true olur.

Üçüncü adımda bir tip değişimi olur: 14 > 10 karşılaştırması iki int alır ama boolean üretir. Ağacın üst katlarında artık sayılar değil mantıksal değerler dolaşır. && operatörünün iki işleneni de boolean olmalıdır. Sol taraf true olduğu için sonuç henüz belli değildir; sağ taraf da hesaplanır. Sol taraf false olsaydı sağ tarafa hiç bakılmazdı.

System.out.println(2 + 3 * 4);
System.out.println((2 + 3) * 4);
System.out.println(10 - 4 - 3);
System.out.println(100 / 10 / 5);
System.out.println(2 + 3 * 4 > 10 && 7 % 2 == 1);
System.out.println(10 - 4 / 2 * 3 + 7 % 4);
System.out.println(-3 * 2 + 8 / 3 % 2);
int a, b;
a = b = 4;
System.out.println(a + " " + b);
boolean p = true, q = false, r = true;
System.out.println(p || q && !r);
System.out.println((p || q) && !r);
14
20
3
2
true
7
-6
4 4
true
false

Son iki satır mantık operatörlerinin önceliğini gösterir. p || q && !r ifadesinde önce !r, sonra &&, en son || uygulanır: true || (false && false) = true. Parantezle sıra değiştirilince sonuç da değişir: (true || false) && false = false. -3 * 2 + 8 / 3 % 2 ifadesinde ise tekli eksi önce gelir, sonra soldan sağa *, / ve % uygulanır: −6 + (8 / 3) % 2 = −6 + 2 % 2 = −6 + 0 = −6.

Öncelik kurallarının hepsini ezberlemek gerekmez. Okuyanın aklında soru işareti bırakacak her yerde parantez kullanmak, hem hatayı önler hem de niyeti görünür kılar. (a + b) / 2 yazmak, a + b / 2'nin ne anlama geldiğini düşünmekten kolaydır.

6.4.7Karma ifade: öncelik ve tip birlikte

Gerçek ifadelerde öncelik ve tip kuralları birlikte çalışır. Sunumdaki örnek ikisinin etkileşimini gösterir:

int a = 7, b = 2;
double r = a / b * 2.0 + a % b;
System.out.println(r);
7.0
Tablo 6.19: a / b * 2.0 + a % b ifadesinin a = 7, b = 2 için adım adım hesaplanması.
AdımİşlemTiplerİfadenin yeni hâli
1a / b = 7 / 2 = 3int / int → int (tam sayı bölmesi)3 * 2.0 + a % b
23 * 2.0 = 6.0int * double → double6.0 + a % b
3a % b = 7 % 2 = 1int % int → int6.0 + 1
46.0 + 1 = 7.0double + int → double7.0
3 * 2.0 + a % ba / b = 7 / 2 = 3 (int / int → tam sayı bölmesi) 6.0 + a % b3 * 2.0 = 6.0 (int → double, sonuç double) 7.06.0 + 1 = 7.0 (1 → 1.0, double)
Şekil 6.24: Karma ifadenin sadeleşmesi. /, * ve % aynı öncelikte olduğu için soldan sağa işlenir; + hepsinden sonra gelir.

/, * ve % aynı öncelik düzeyindedir ve soldan sağa işlenir; + ise hepsinden sonra gelir. İlk işlem a / b'dir ve iki int olduğu için 3.5 değil 3 verir. İfadede 2.0 gibi bir double bulunması bu ilk işlemi etkilemez; 2.0 ancak ikinci işlemde devreye girer. Sonuç 7.0'dır. a / b yerine a / 2.0 yazılsaydı ilk işlem 3.5 verir ve sonuç 3,5 · 2,0 + 1 = 8,0 olurdu. Aynı sayılarla iki farklı cevap çıkması, sonucu operatörlerin sırasının ve işlenen tiplerinin birlikte belirlediğini gösterir.

6.4.8Kısa devre değerlendirme

&& ve || operatörlerinin işlerini hızlandıran ve programları güvenli kılan bir özelliği vardır.

Tanım 6.11

Kısa devre değerlendirme (short-circuit evaluation), mantık ifadesinin sonucu sol işlenenden belli olduğunda sağ işlenenin hiç hesaplanmamasıdır. && için sol taraf false ise sonuç false'tur; || için sol taraf true ise sonuç true'dur. Bu durumlarda sağ taraf çalıştırılmaz.

int a = 10, b = 0;
if (b != 0 && a / b > 2) {
    System.out.println("Oran büyük");
}
b = 2;
if (b != 0 && a / b > 2) {
    System.out.println("Oran büyük");
}
Oran büyük

Program aynı koşulu iki farklı b değeriyle sınar. Koşulun sol tarafı b != 0, sağ tarafı a / b > 2'dir. (if deyimi, koşul doğruysa süslü parantez içindeki satırları çalıştırır; yanlışsa atlar.)

Tablo 6.20: Kısa devre örneğinin izleme tablosu.
bSol: b != 0Sağ: a / b > 2KoşulÇıktı
00 != 0 → falsehesaplanmazfalseyok (blok atlanır)
22 != 0 → true10 / 2 = 5; 5 > 2 → truetrueOran büyük
b = 0 iken:b != 00 != 0 → falsetrue isea / b > 2hiç hesaplanmazfalse: sonuç zaten bellikoşul = falseif gövdesi atlanır
Şekil 6.25: b = 0 iken kısa devre. Sol koşul false olduğu için && ifadesinin sonucu bellidir; a / b > 2 hiç hesaplanmaz ve sıfıra bölme hatası oluşmaz.

b = 0 iken sağ taraf çalıştırılsaydı a / b tam sayıyı sıfıra bölerdi ve program ArithmeticException ile çökerdi. Kısa devre bunu önler: sol taraf false olduğunda sonuç ne olursa olsun false'tur ve Java sağ tarafa bakmaz. Bu yüzden koşulların sırası önemlidir. Koşullar ters yazılırsa koruma ortadan kalkar:

int a = 10, b = 0;
if (a / b > 2 && b != 0) {
    System.out.println("Oran büyük");
}
Exception in thread "main" java.lang.ArithmeticException: / by zero

Burada önce a / b hesaplanır ve program, b != 0 denetimine sıra gelmeden durur. Koruyucu koşul her zaman sola, korunan işlem sağa yazılır. || için aynı fikir tersinden işler: b == 0 || a / b > 2 ifadesinde b = 0 ise sol taraf true'dur ve bölme yapılmaz.

int a = 10, b = 0;
if (b == 0 || a / b > 2) {
    System.out.println("b sıfır ya da oran büyük");
}
int sayac = 0;
boolean s1 = false && (++sayac > 0);
boolean s2 = true || (++sayac > 0);
boolean s3 = false & (++sayac > 0);
System.out.println(sayac + " " + s1 + " " + s2 + " " + s3);
b sıfır ya da oran büyük
1 false true false

İkinci kısımdaki sayaç, hangi sağ tarafların gerçekten çalıştığını sayar. false && (++sayac > 0) ve true || (++sayac > 0) ifadelerinde sağ taraf kısa devre yüzünden çalışmaz. Tek & ise kısa devre yapmayan “ve” operatörüdür; iki tarafı da her zaman hesaplar. Sayacın 1 olması, yalnızca bu üçüncü ifadede artırmanın gerçekleştiğini gösterir. Günlük programlamada koşullar için hep && ve || kullanılır.

6.4.9Dizgi birleştirme

+ operatörünün de iki yüzü vardır. İki işlenen sayıysa toplama yapar. İşlenenlerden en az biri bir dizgiyse (String) dizgi birleştirme (string concatenation) yapar: öteki işlenen metne çevrilir ve iki metin uç uca eklenir. Operatörün davranışını işlenenlerin tipi belirler.

System.out.println(2 + 3);       // 5
System.out.println("2" + "3");   // 23
System.out.println("Sonuç: " + 2 + 3); // Sonuç: 23
System.out.println("Sonuç: " + (2 + 3)); // Sonuç: 5

Dört satırın sonuçlarını açıklamak için iki kural yeterlidir. Birincisi, + soldan sağa işler. İkincisi, her adımda o adımdaki iki işlenene bakılır: ikisi de sayıysa toplama, biri dizgiyse birleştirme yapılır.

Tablo 6.21: Dört println satırının adımları.
İfade1. adım2. adımÇıktı
2 + 3int + int → toplama: 5–5
"2" + "3"String + String → birleştirme: "23"–23
"Sonuç: " + 2 + 3String + int → "Sonuç: 2"String + int → "Sonuç: 23"Sonuç: 23
"Sonuç: " + (2 + 3)parantez: int + int → 5String + int → "Sonuç: 5"Sonuç: 5
"Sonuç: "String+2int+3int→"Sonuç: 2"String "Sonuç: 2"String+3int→"Sonuç: 23"String "Sonuç: "String+5int→"Sonuç: 5"String
Şekil 6.26: "Sonuç: " + 2 + 3 ifadesinde önce "Sonuç: " + 2 birleştirilir ve "Sonuç: 2" olur; sonra 3 de metne eklenir. Parantezli yazılışta önce 2 + 3 = 5 toplanır, sonra "Sonuç: 5" oluşur.

Üçüncü satır en çok şaşırtan satırdır. Okuyan kişi 2 ile 3'ün toplanacağını bekler ama Java soldan sağa gider: ilk adımda “Sonuç: ” dizgisiyle 2 birleşir. Bu adımın sonucu bir dizgidir; ikinci adımda dizgiyle 3 birleşir. 2 + 3 hiç toplanmaz. Parantez toplamanın önce yapılmasını sağlar. Kural, işlemin başında da geçerlidir:

System.out.println(2 + 3);
System.out.println("2" + "3");
System.out.println("Sonuç: " + 2 + 3);
System.out.println("Sonuç: " + (2 + 3));
System.out.println(2 + 3 + " elma");
System.out.println("Sonuç: " + 2 * 3);
System.out.println("Sonuç: " + 7 / 2);
System.out.println('A' + 1 + "B");
System.out.println("A" + 'B' + 1);
System.out.println(1 + 'A');
System.out.println("x" + 1.5 + true);
String s = "Sonuç: ";
s += 5;
System.out.println(s);
5
23
Sonuç: 23
Sonuç: 5
5 elma
Sonuç: 6
Sonuç: 3
66B
AB1
66
x1.5true
Sonuç: 5
  • 2 + 3 + " elma": soldaki iki işlenen sayıdır ve önce toplanır; sonuç “5 elma”dır.
  • "Sonuç: " + 2 * 3 ve "Sonuç: " + 7 / 2: * ve /, +'dan önceliklidir. Önce sayısal işlem yapılır (6 ve 3), sonra birleştirme.
  • 'A' + 1 + "B": ilk adımda char ile int toplanır (65 + 1 = 66), sonra “B” eklenir: “66B”. "A" + 'B' + 1 ise baştan dizgiyle başladığı için “AB1” verir.
  • "x" + 1.5 + true: double ve boolean değerler de metne çevrilir: “x1.5true”.
  • s += 5: bileşik atama dizgilerde de çalışır ve metnin sonuna ekler.

Dizgilerle ilgili bir uyarı da karşılaştırmayla ilgilidir. İki dizginin içeriğinin aynı olup olmadığı == ile değil equals metoduyla denetlenir. ==, String gibi referans tiplerinde iki değişkenin aynı nesneyi gösterip göstermediğine bakar, içeriğe bakmaz:

String s1 = "Ada";
String s2 = new String("Ada");
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
false
true

s1 ve s2 aynı “Ada” metnini taşır ama bellekte iki ayrı nesnedir. == false, equals true verir.

6.5Girdi ve çıktı

Şimdiye kadarki programlar hep aynı sayılarla çalıştı; değerler kodun içine yazılmıştı. Bir sıcaklık dönüştürücüsü ya da not ortalaması hesaplayan bir program ise her çalıştığında farklı değerlerle çalışmalıdır. Bu değerler kullanıcıdan, çoğunlukla klavyeden alınır. Java'da klavyeden okumanın en kolay yolu Scanner sınıfıdır. Sonuçların düzgün hizalanmış, belirli sayıda ondalık basamakla yazılması için de printf metodu kullanılır.

6.5.1Scanner ile girdi

Scanner klavye = new Scanner(System.in);
System.out.print("Yaş: ");
int yas = klavye.nextInt();
klavye.nextLine(); // satır sonunu tüket
System.out.print("Ad: ");
String ad = klavye.nextLine();

Scanner, Java'nın java.util paketindedir. Kullanılmadan önce dosyanın en başında, sınıf tanımından önce import java.util.Scanner; satırı yazılır. Bu satır derleyiciye Scanner adının nereden geleceğini söyler; yazılmazsa “cannot find symbol” hatası alınır.

  1. Scanner klavye = new Scanner(System.in);: new ile bir Scanner nesnesi oluşturulur ve standart girdiye, yani klavyeye (System.in) bağlanır. klavye bu nesneye referans tutan değişkendir; adı serbestçe seçilebilir.
  2. System.out.print("Yaş: ");: kullanıcıya ne girmesi gerektiğini söyleyen bir istem (prompt) yazılır. println değil print kullanıldığı için satır sonu eklenmez; kullanıcı aynı satırda yazar.
  3. int yas = klavye.nextInt();: program durur ve kullanıcının bir tam sayı yazıp Enter'a basmasını bekler. Okunan sayı yas değişkenine yazılır.
  4. klavye.nextLine();: satır sonunda kalan Enter karakteri tüketilir. nextInt() sayının arkasındaki Enter'ı okumaz; bu çağrı onu tampondan alır.
  5. String ad = klavye.nextLine();: kullanıcının yazdığı satırın tamamı, boşluklar dahil, okunur.

Scanner'ın okuma metotları, beklenen tipe göre seçilir: nextInt() int, nextLong() long, nextDouble() double, nextBoolean() boolean okur. next() boşluğa kadar tek bir kelime, nextLine() ise satırın sonuna kadar her şeyi okur. Sayı ve kelime okuyan metotlar, girdideki boşlukları ve satır sonlarını ayırıcı sayar ve atlar. Kullanıcı 20, 30 ve 40 sayılarını ister aynı satıra boşluklarla, ister ayrı satırlara yazsın; üç nextInt() çağrısı üçünü de okur.

Programın ekranda görünen hâli, kullanıcının yazdıklarıyla birlikte şöyledir (kullanıcı 20 ve Ada Yılmaz yazar):

Yaş: 20
Ad: Ada Yılmaz

6.5.2Girdi tamponu adım adım

Kullanıcının yazdıkları Scanner'a doğrudan değil, bir tampon (buffer) üzerinden ulaşır. Tampon, klavyeden gelen karakterlerin okunana kadar beklediği bir sıradır. Kullanıcı Enter'a bastığında yazdığı satır, sonundaki satır sonu karakteriyle birlikte tampona girer. Satır sonu, Java'da \n ile, şekillerde ⏎ simgesiyle gösterilir. Scanner'ın her okuma metodu tampondan belirli karakterleri alır, gerisini sonraki okumalara bırakır.

girdi tamponu(kullanıcı: 20⏎ Ada⏎)20\nAda\nnextInt() → 20nextLine() → ""nextLine() → "Ada"1. nextInt sayıyı alır, \n kalır2. boş nextLine onu tüketir3. sonraki nextLine adı okur
Şekil 6.27: Girdi tamponu: kullanıcı önce 20⏎, sonra Ada⏎ yazar. nextInt() 20'yi okur ve satır sonunu tamponda bırakır. Boş nextLine() bu satır sonunu tüketir. Sonraki nextLine() adı okur.
Tablo 6.22: Scanner örneğinin izleme tablosu. Tampon sütunu, deyim çalıştıktan sonra okunmayı bekleyen karakterleri gösterir.
DeyimNe olur?OkunanTampon
new Scanner(System.in)Scanner klavyeye bağlanır–boş
print("Yaş: ")istem yazılır; imleç aynı satırda kalır–boş
nextInt()tampon boş: bekler. Kullanıcı 20⏎ yazar20⏎
nextLine()⏎'e kadar okur, ⏎'i tüketir"" (boş dizgi)boş
print("Ad: ")istem yazılır–boş
nextLine()tampon boş: bekler. Kullanıcı Ada Yılmaz⏎ yazar"Ada Yılmaz"boş
klavye → System.in tamponu20⏎▲okuma konumuokunan: 20 klavye → System.in tamponu20⏎▲okuma konumuokunan: "" klavye → System.in tamponu20⏎Ada Yılmaz⏎▲okuma konumuokunan: "Ada Yılmaz"
Şekil 6.28: Tamponun üç anı. nextInt yalnızca 20'yi okur; okuma konumu ⏎'in önünde durur. nextLine ⏎'e kadar okur ve boş dizgi döndürür. Son nextLine satırın tamamını okur: “Ada Yılmaz”. Okunan karakterler soluk gösterilmiştir.

Tablonun üçüncü satırı her şeyi açıklar. nextInt() sayının rakamlarını okur ve rakam olmayan ilk karakterde durur. Bu karakter kullanıcının bastığı Enter'dır ve tamponda kalır. nextLine() ise “satırın geri kalanını, satır sonuna kadar” okur. Bir sonraki satır sonu hemen okuma konumundadır; bu yüzden ilk nextLine() hiç beklemeden boş dizgi döndürür ve satır sonunu tüketir. Bu çağrının döndürdüğü değer bir yere atanmaz; çağrının tek amacı tamponu temizlemektir. Son nextLine() boş tamponla karşılaşır ve kullanıcıyı bekler. Kullanıcının yazdığı “Ada Yılmaz” boşluğuyla birlikte okunur; satır sonu tüketilir ama döndürülen dizgiye eklenmez.

6.5.3Satır sonu tüketilmezse

Tamponu temizleyen satır unutulursa ne olur? Sunumdaki ikinci program bu durumu gösterir:

Scanner klavye = new Scanner(System.in);
System.out.print("Yaş: ");
int yas = klavye.nextInt();
System.out.print("Ad: ");
String ad = klavye.nextLine();
System.out.println("[" + ad + "]");

Kullanıcı 20 yazıp Enter'a bastığında ekranda şu görünür ve program biter:

Yaş: 20
Ad: []
klavye → System.in tamponu20⏎▲okuma konumuokunan: ""
Şekil 6.29: Satır sonu tüketilmezse: nextInt'ten kalan ⏎ tampondadır. nextLine beklemeden bu ⏎'e kadar okur ve boş dizgi döndürür; kullanıcıya ad sorulamaz.

Program adı sormadan bitmiş gibi görünür. Gerçekte “Ad: ” istemi yazılmış, nextLine() çağrılmış ve tamponda bekleyen satır sonunu bularak hemen dönmüştür. ad değişkeni boş dizgidir; köşeli parantezlerin arasında hiçbir şey yoktur. Kullanıcı ikinci bir şey yazmaya fırsat bulamaz. Çözüm, sunumdaki ilk programdaki gibi nextInt()'ten sonra fazladan bir klavye.nextLine(); çağırmaktır. Bir başka çözüm, sayıları da satır olarak okuyup dönüştürmektir: int yas = Integer.parseInt(klavye.nextLine());.

Aynı etki, kullanıcı sayıyla adı aynı satıra yazdığında da görülür. Girdi “20 Ada Yılmaz⏎” ise nextInt() 20'yi, sonraki nextLine() ise satırın geri kalanını okur: “ Ada Yılmaz”. Baştaki boşluk da dizgiye dahildir. next() ile nextLine() arasındaki fark da benzer biçimde görülür: “Ada Yılmaz⏎” girdisinde next() yalnızca “Ada”yı, ardından gelen nextLine() ise “ Yılmaz”ı okur.

Sık yapılan hata

nextInt(), nextDouble() ya da next() çağrısından hemen sonra nextLine() ile bir satır okumaya çalışmak. İlk nextLine(), önceki satırdan kalan satır sonunu okur ve boş dizgi döndürür. Sayı okuduktan sonra satır okunacaksa araya tamponu temizleyen bir klavye.nextLine(); konur.

6.5.4Girdi doğrulama

Kullanıcının her zaman beklenen değeri gireceği varsayılamaz. Yaş sorulan bir programa “yirmi” yazılabilir, bir hata sonucu −5 ya da 200 girilebilir. nextInt(), sayı beklediği yerde sayı olmayan bir kelimeyle karşılaşınca programı durdurur:

Yaş: yirmi
Exception in thread "main" java.util.InputMismatchException

Programın bu tür girdilere dayanıklı olması için girdi doğrulama (input validation) yapılır. Doğrulama iki katmanlıdır. Önce tip denetlenir: girilen şey gerçekten bir tam sayı mı? Sonra aralık ve iş kuralı denetlenir: tam sayı makul bir yaş mı?

girdiyi okutam sayı mı?hata:tiphayırevet0 ≤ yas ≤ 150 ?hata:aralıkhayırevetkabul ethata → iletiyi göster, yeniden sor
Şekil 6.30: Girdi doğrulama akışı. Önce girdinin tipi, sonra aralığı denetlenir. İki denetimden geçen değer kabul edilir; geçemeyen için hata iletisi gösterilir ve değer yeniden sorulur.

Scanner, tip denetimi için hasNextInt() gibi metotlar sunar. Bu metot girdiyi okumadan bakar: sıradaki parça bir tam sayıysa true, değilse false döndürür. Aşağıdaki program iki denetimi de if deyimleriyle yapar. if (koşul) { … } else { … } yapısı, koşul doğruysa ilk bloğu, yanlışsa else'ten sonraki bloğu çalıştırır.

Scanner klavye = new Scanner(System.in);
System.out.print("Yaş: ");
if (klavye.hasNextInt()) {
    int yas = klavye.nextInt();
    if (yas >= 0 && yas <= 150) {
        System.out.println("Kabul: " + yas);
    } else {
        System.out.println("Hata: yaş 0 ile 150 arasında olmalı.");
    }
} else {
    System.out.println("Hata: tam sayı bekleniyor (örnek: 20).");
}

Program üç farklı girdiyle çalıştırıldığında şu çıktıları verir:

Yaş: yirmi
Hata: tam sayı bekleniyor (örnek: 20).

Yaş: 200
Hata: yaş 0 ile 150 arasında olmalı.

Yaş: 20
Kabul: 20

“yirmi” girildiğinde hasNextInt() false döndürür; nextInt() hiç çağrılmadığı için program çökmez ve anlaşılır bir ileti yazar. 200 girildiğinde tip denetimi geçilir ama aralık denetimi başarısız olur. 20 her iki denetimi de geçer. Hata iletisinin beklenen biçimi açıkça söylediğine dikkat edilmelidir: “Geçersiz girdi” yerine “tam sayı bekleniyor (örnek: 20)” yazmak, kullanıcıya ne yapacağını gösterir. Gerçek bir programda yanlış girdiden sonra değer yeniden sorulur; bu tekrar, bir döngüyle yapılır.

6.5.5Biçimli çıktı: printf

println değerleri olduğu gibi yazar: 87.456 değeri 87.456, 2.0/3 değeri 0.6666666666666666 olarak görünür. Bir not çizelgesinde ise ortalamaların iki basamakla ve sütunlar hizalı yazılması istenir. Bu iş için biçimli çıktı (formatted output) üreten printf metodu kullanılır.

String ad = "Ada";
double ortalama = 87.456;
System.out.printf("%-10s %6.2f%n", ad, ortalama);
Ada         87.46

printf'in ilk argümanı bir biçim dizgisidir (format string). Biçim dizgisindeki sıradan karakterler aynen yazılır. % ile başlayan parçalar ise biçim belirteçleridir (format specifier): her biri, sıradaki argümanın nasıl yazılacağını tarif eder ve argümanları sırayla tüketir.

%-10ssola yasla10 karakter · dizgiad → "Ada"␣boşluk%6.2f6 karakter2 ondalık · sağa yaslaortalama → 87.456%nsatır sonuA1d2a3·4·5·6·7·8·9·10·11·12813714.1541661710 karakter: "Ada" + 7 boşluk6 karakter: " 87.46"87.456 → 87.46: yuvarlanır (kesilmez)
Şekil 6.31: %-10s %6.2f%n biçim dizgisinin çözümlenmesi. %-10s “Ada”yı 10 karakterlik alana sola yaslı yazar (3 harf + 7 boşluk). Sonra bir boşluk gelir. %6.2f, 87.456'yı iki basamağa yuvarlar ve 6 karakterlik alana sağa yaslar: “ 87.46”.

Bir biçim belirteci % işaretiyle başlar ve bir dönüşüm harfiyle biter. Aradaki üç parça isteğe bağlıdır. Bayraklar hizalamayı ve dolguyu belirler: - sola yaslar, 0 boşluk yerine sıfırla doldurur, , binlik ayırıcı ekler. Genişlik, alanın en az kaç karakter olacağını söyler. Kesinlik nokta ile yazılır ve ondalık basamak sayısını verir. En sık kullanılan dönüşüm harfleri şunlardır:

Tablo 6.23: Sık kullanılan biçim belirteçleri. Çıktılar köşeli parantez içinde, alan genişliği görünsün diye verilmiştir (JDK 21, Locale.US).
BelirteçAnlamÖrnekÇıktı
%dtam sayıprintf("[%d]", 42)[42]
%5d5 karakter, sağa yaslıprintf("[%5d]", 42)[ 42]
%-5d5 karakter, sola yaslıprintf("[%-5d]", 42)[42 ]
%05d5 karakter, soldan sıfırlıprintf("[%05d]", 42)[00042]
%,dbinlik ayırıcılıprintf("[%,d]", 1234567)[1,234,567]
%fkayan noktalı, 6 basamakprintf("[%f]", Math.PI)[3.141593]
%.3f3 ondalık basamakprintf("[%.3f]", 2.0005)[2.001]
%ebilimsel gösterimprintf("[%e]", 12345.678)[1.234568e+04]
%sdizgi (her değer metne çevrilir)printf("[%s]", 3.5)[3.5]
%ckarakterprintf("[%c]", 'Z')[Z]
%bbooleanprintf("[%b]", false)[false]
%nsatır sonuprintf("a%nb")a ve b ayrı satırda
%%% işaretinin kendisiprintf("%%%d indirim", 20)%20 indirim

Tablodaki örnekler aşağıdaki programdan alınmıştır. Program aynı zamanda iki ayrıntıyı daha gösterir. Genişlik bir alt sınırdır: %3s belirteci 8 harflik bir adı kesmez, olduğu gibi yazar; %2d de 12345'i kesmez. Ayrıca String.format metodu aynı biçimlendirmeyi yapar ama sonucu yazmak yerine bir dizgi olarak döndürür.

String ad = "Ada";
double ortalama = 87.456;
System.out.printf("%-10s %6.2f%n", ad, ortalama);
System.out.printf("[%-10s][%6.2f]%n", ad, ortalama);
System.out.printf("%-8s|%6.2f|%05d%n", "Ada", 3.14159, 42);
System.out.printf("%-6s|%7.3f|%04d%n", "Can", 2.71828, 7);
System.out.printf("[%5d][%-5d][%05d][%d]%n", 42, 42, 42, 42);
System.out.printf("[%.0f][%.1f][%.3f][%f]%n", 2.5, 2.45, 2.0005, Math.PI);
System.out.printf("[%e][%.2e]%n", 12345.678, 0.000123);
System.out.printf("[%,d][%,.2f]%n", 1234567, 1234567.891);
System.out.printf("[%s][%s][%s][%b][%c]%n", 42, 3.5, true, false, 'Z');
System.out.printf("%%%d indirim%n", 20);
System.out.printf("[%3s][%2d]%n", "Uzunİsim", 12345);
System.out.printf("[%.1f][%.1f][%.1f][%.1f]%n", 0.25, 0.35, 0.05, 2.25);
String s = String.format("%d:%02d:%02d", 1, 2, 5);
System.out.println(s);
Ada         87.46
[Ada       ][ 87.46]
Ada     |  3.14|00042
Can   |  2.718|0007
[   42][42   ][00042][42]
[3][2.5][2.001][3.141593]
[1.234568e+04][1.23e-04]
[1,234,567][1,234,567.89]
[42][3.5][true][false][Z]
%20 indirim
[Uzunİsim][12345]
[0.3][0.4][0.1][2.3]
1:02:05

%.1f ile yazılan 0.25, 0.35, 0.05 ve 2.25 değerlerinin hepsi yukarı yuvarlanır: 0.3, 0.4, 0.1, 2.3. printf yuvarlarken sayının ondalık yazılışına bakar ve yarımları yukarı yuvarlar.

Sunumdaki ikinci örnek üç belirteci tek satırda kullanır:

System.out.printf("%-8s|%6.2f|%05d%n",
                  "Ada", 3.14159, 42);
Ada     |  3.14|00042
Tablo 6.24: "%-8s|%6.2f|%05d%n" biçim dizgisinin soldan sağa çözümlenmesi.
ParçaArgümanKuralÜretilen
%-8s"Ada"8 karakter, - sola yaslar: 3 harf + 5 boşlukAda␣␣␣␣␣
|–düz karakter, aynen yazılır|
%6.2f3.141592 basamağa yuvarla (3.14), 6 karaktere sağa yasla: 2 boşluk␣␣3.14
|–düz karakter|
%05d425 karakter, 0 bayrağı soldan sıfırla doldurur00042
%n–satır sonu⏎
%-8s ← "Ada"Ada␣␣␣␣␣genişlik 8, - = sola yaslı, 5 boşluk eklenir | sabit, %6.2f ← 3.14159|␣␣3.14genişlik 6, noktadan sonra 2 basamak, sağa yaslı | sabit, %05d ← 42|00042genişlik 5, 0 bayrağı = soldan sıfırla doldur
Şekil 6.32: Üç biçim belirtecinin ürettiği karakterler. ␣ simgesi boşluğu gösterir. Her % belirteci bir argüman tüketir; dikey çizgiler düz metindir.

Belirteç sayısı ile argüman sayısı ve tipleri uyuşmalıdır. Uyuşmazlık derleme sırasında değil, program çalışırken fark edilir. printf("%d%n", 3.5) çağrısı IllegalFormatConversionException fırlatır, çünkü %d tam sayı bekler ve 3.5 bir double'dır. printf("%s ve %s%n", "Ada") çağrısı ise ilk belirteci doldurur, ikinci belirteç için argüman bulamaz ve MissingFormatArgumentException ile durur.

Biçimli çıktının son bir ayrıntısı yerel ayardır (locale). printf, ondalık ayırıcıyı ve binlik ayırıcıyı bilgisayarın dil ve bölge ayarına göre seçer. Türkçe yerel ayarlı bir bilgisayarda aynı program farklı çıktı verir:

String ad = "Ada";
double ortalama = 87.456;
System.out.printf("%-10s %6.2f%n", ad, ortalama);
System.out.printf("%-8s|%6.2f|%05d%n", "Ada", 3.14159, 42);
System.out.printf("[%,d][%,.2f]%n", 1234567, 1234567.891);
System.out.printf(Locale.US, "%6.2f%n", ortalama);
System.out.println(ortalama);
Ada         87,46
Ada     |  3,14|00042
[1.234.567][1.234.567,89]
 87.46
87.456

Türkçe ayarda ondalık ayırıcı virgül, binlik ayırıcı noktadır: 87,46 ve 1.234.567,89. İlk argüman olarak Locale.US verilirse ayar o çağrı için geçersiz kılınır ve nokta kullanılır. println ise yerel ayardan etkilenmez; her zaman nokta kullanır. Derste ve sunumlarda çıktılar Locale.US biçiminde verilir.

Yerel ayar Scanner'ı da etkiler ve bu etki daha can sıkıcıdır. Türkçe yerel ayarda nextDouble(), ondalık sayının virgülle yazılmasını bekler. Kullanıcı “37.5” yazarsa InputMismatchException fırlatılır; “37,5” yazarsa 37.5 okunur. Locale.US ayarında durum tam tersidir. Programın her bilgisayarda aynı davranması isteniyorsa Scanner oluşturulduktan sonra ayar sabitlenir: klavye.useLocale(Locale.US);. Bu satırdan sonra Türkçe ayarlı bir bilgisayarda da “37.5” girdisi okunur.

Sık yapılan hata

Ödev programında ondalık sayı girildiğinde InputMismatchException alıp kodu hatalı sanmak. Kod doğru olabilir; sorun, bilgisayarın Türkçe yerel ayarda nokta yerine virgül beklemesidir. Ya girdi virgülle yazılır ya da Scanner'a useLocale(Locale.US) uygulanır. (Locale sınıfı da java.util paketindedir.)

6.6Örnekler

Bu kesimdeki küçük programlar, notun bütün araçlarını bir arada kullanır: değişkenler, tipler, dönüşümler, operatörler, Scanner ve printf. Her programda önce kod, sonra adım adım hesap ve en son gerçek çıktı verilir.

6.6.1Math sınıfı

Karekök, üs alma, mutlak değer gibi işlemler için Java'nın hazır Math sınıfı kullanılır. Math, java.lang paketindedir. Bu paket her Java programına kendiliğinden dahil edildiği için import satırı gerekmez. Math'in metotları bir nesne oluşturmadan, sınıf adıyla çağrılır: Math.sqrt(16).

Tablo 6.25: Sık kullanılan Math öğeleri (sunumdaki tablo; değerler JDK 21 ile doğrulanmıştır).
İfadeSonuçAçıklama
Math.PI3.141592653589793π sabiti
Math.sqrt(16)4.0karekök (double)
Math.pow(2, 10)1024.0üs alma (double)
Math.abs(-7)7mutlak değer
Math.max(3, 8)8büyük olan (min: küçük)
Math.round(2.5)3en yakın tam sayı (long)
Math.round(-2.5)-2yarım değer yukarı yuvarlanır

sqrt ve pow her zaman double döndürür; tam kare olan 16'nın karekökü bile 4 değil 4.0'dır. abs, max ve min ise argümanın tipini korur: Math.abs(-7) int 7, Math.abs(-7.5) double 7.5 verir. Argümanlardan biri double ise sonuç da double'dır: Math.max(3, 8.0) 8.0 verir.

-3-2-10123round(2.5) = 3round(-2.5) = -2yarım değer hep +∞ yönüne
Şekil 6.33: Math.round sayı doğrusunda. Tam yarım değerler her zaman pozitif sonsuz yönüne yuvarlanır: 2.5 → 3, −2.5 → −2.

Math.round, en yakın tam sayıyı bulur. Sayı iki tam sayının tam ortasındaysa (2.5 gibi) yukarıya, yani pozitif sonsuz yönüne yuvarlar. Bu yüzden 2.5 → 3 ama −2.5 → −2 olur; −2, −3'ten büyüktür. Okulda öğretilen “yarımı sıfırdan uzağa yuvarla” kuralı −2.5 için −3 verir; Java'nın kuralı bundan farklıdır. Math sınıfının başka metotları da aşağıdaki programda görülür:

System.out.println(Math.PI);
System.out.println(Math.E);
System.out.println(Math.sqrt(16));
System.out.println(Math.pow(2, 10));
System.out.println(Math.abs(-7) + " " + Math.abs(-7.5));
System.out.println(Math.max(3, 8) + " " + Math.min(3, 8));
System.out.println(Math.round(2.5) + " " + Math.round(-2.5) + " " + Math.round(2.4) + " " + Math.round(-2.6));
System.out.println(Math.floor(2.7) + " " + Math.ceil(2.1) + " " + Math.floor(-2.7) + " " + Math.ceil(-2.1));
System.out.println(Math.pow(2, 0.5) + " " + Math.sqrt(2));
System.out.println(Math.pow(2, 31) + " " + (int) Math.pow(2, 31));
System.out.println(Math.max(3, 8.0));
System.out.println(Math.sqrt(2) * Math.sqrt(2));
System.out.println(Math.hypot(3, 4));
3.141592653589793
2.718281828459045
4.0
1024.0
7 7.5
8 3
3 -2 2 -3
2.0 3.0 -3.0 -2.0
1.4142135623730951 1.4142135623730951
2.147483648E9 2147483647
8.0
2.0000000000000004
5.0

Math.floor sayıyı aşağıya (negatif sonsuza doğru), Math.ceil yukarıya (pozitif sonsuza doğru) yuvarlar ve sonucu double olarak verir: floor(−2,7) = −3,0, ceil(−2,1) = −2,0. (int) dönüşümünün sıfıra doğru kestiği hatırlanırsa, negatif sayılarda floor ile (int)'in farklı sonuç verdiği görülür. Math.pow(2, 31) 2.147483648E9'dur; bu değer int'e çevrilince sınırda sıkışır ve 2147483647 olur. √2 · √2 ise kayan nokta hatası yüzünden tam 2 değil 2.0000000000000004'tür. Math.hypot(3, 4) dik üçgenin hipotenüsünü verir: √(9 + 16) = 5.

6.6.2İki değişkeni takas etme

İki değişkenin değerlerini değiştirmek, yani takas (swap) etmek, sıralama gibi pek çok algoritmanın temel adımıdır. İlk akla gelen çözüm yanlıştır:

int a = 5, b = 8;
a = b;                // yanlış deneme
b = a;
System.out.println(a + " " + b);
a = 5; b = 8;         // baştan
int gecici = a;       // doğru yöntem
a = b;
b = gecici;
System.out.println(a + " " + b);
8 8
8 5
Tablo 6.26: Takas örneğinin izleme tablosu. İlk dört satır yanlış denemeyi, sonraki satırlar doğru yöntemi gösterir.
DeyimabgeciciAçıklama
int a = 5, b = 8;58–hedef: a = 8, b = 5
a = b;88–a'nın eski değeri 5 silindi
b = a;88–5 artık hiçbir kutuda yok
println88–çıktı: 8 8 (başarısız)
a = 5; b = 8;58–baştan
int gecici = a;585a'nın değeri güvende
a = b;885
b = gecici;855
println855çıktı: 8 5 (tamam)
Bellek (değişkenler)a8intb8intİfadeb ← ab = 8 Bellek (değişkenler)a8intb5intgecici5intİfadeb ← gecici
Şekil 6.34: Yanlış ve doğru takas. Üstte: a = b ve b = a sonrasında iki kutu da 8'dir. Altta: geçici değişken 5'i korur ve b = gecici takası tamamlar.

Yanlış denemenin sorunu, atamanın eski değeri silmesidir. a = b çalıştığında a'daki 5 kaybolur; sonraki b = a yalnızca 8'i geri kopyalar. İki bardaktaki suyu yer değiştirmek için üçüncü bir bardak gerektiği gibi, takas da üçüncü bir kutu ister. Bu kutuya geçici değişken (temporary variable) denir. Doğru yöntem üç atama ve bir geçici değişken kullanır.

Sayılarda geçici değişken olmadan da takas yapılabilir: a = a + b; b = a - b; a = a - b;. a = 5, b = 8 için a önce 13 olur, sonra b = 13 − 8 = 5, en son a = 13 − 5 = 8 bulunur. Bu yöntem taşmaya karşı şaşırtıcı biçimde dayanıklıdır:

int a = 5, b = 8;
a = a + b;
b = a - b;
a = a - b;
System.out.println(a + " " + b);
int x = 2_000_000_000, y = 1_500_000_000;
x = x + y;
System.out.println("ara: " + x);
y = x - y;
x = x - y;
System.out.println(x + " " + y);
8 5
ara: -794967296
1500000000 2000000000

İki milyar ile bir buçuk milyarın toplamı int'i taşar ve ara değer −794.967.296 olur. Yine de sonuç doğru çıkar: taşma sırasında eklenen 232, sonraki çıkarmalarda geri alınır. Buna karşın bu hile okunabilirliği bozar ve yalnızca sayılarla çalışır; günlük programlamada geçici değişkenli takas tercih edilir.

6.6.3Celsius'tan Fahrenheit'a

Celsius cinsinden bir sıcaklığı Fahrenheit'a çeviren formül F = C · 9 / 5 + 32'dir. Program, sıcaklığı klavyeden okur ve sonucu bir ondalık basamakla yazar:

Scanner klavye = new Scanner(System.in);
double c = klavye.nextDouble();
double f = c * 9 / 5 + 32;
System.out.printf("%.1f C = %.1f F%n", c, f);
Tablo 6.27: Üç çalıştırmada formülün soldan sağa hesaplanması (değerler programa ara çıktı eklenerek JDK 21'de alınmıştır).
Girdicc * 9… / 5… + 32 = fÇıktı
100100.0900.0180.0212.0100.0 C = 212.0 F
3737.0333.066.698.637.0 C = 98.6 F
-40-40.0-360.0-72.0-40.0-40.0 C = -40.0 F
Bellek (değişkenler)c37.0doublef?doubleİfadec * 9 / 5 + 32333.0 / 5 + 32→ 98.6
Şekil 6.35: İkinci çalıştırma: c = 37.0 için 333.0 / 5 + 32 → 98.6 hesaplanır ve f kutusuna yazılır.

Kullanıcı 100 yazsa da nextDouble() değeri double olarak okur: c = 100.0. Formül soldan sağa hesaplanır. c * 9 double ile int'in çarpımıdır ve double'dır; bölme de toplama da kayan noktalı yapılır. %.1f belirteci her iki değeri bir ondalık basamakla yazar. Üçüncü çalıştırma ilginç bir gerçeği gösterir: −40 derece iki ölçekte de aynı sıcaklıktır.

Formülün yazılış sırası önemlidir. Aynı formül 9 / 5 * c + 32 biçiminde yazılsaydı ilk işlem 9 / 5 olurdu. İki int'in bölümü 1,8 değil 1'dir. Sonuç c = 37 için 98.6 yerine 1 · 37 + 32 = 69 çıkar:

double c = 37;
double f1 = 9 / 5 * c + 32;
double f2 = 9.0 / 5 * c + 32;
double f3 = c * 9 / 5 + 32;
System.out.println(f1 + " " + f2 + " " + f3);
System.out.println(9 / 5 + " " + 9.0 / 5);
69.0 98.60000000000001 98.6
1 1.8

9.0 / 5 yazılınca bölme kayan noktalı olur ve sonuç doğru çıkar. Bu yazılışta değer 98.60000000000001'dir; işlem sırası farklı olduğu için kayan nokta hatası başka bir basamakta görünür. printf ile bir basamağa yuvarlanınca iki yazılış da 98.6 verir.

6.6.4Dairenin alanı ve çevresi

double r = 2.5;
double alan = Math.PI * r * r;
double cevre = 2 * Math.PI * r;
System.out.printf("Alan: %.2f%n", alan);
System.out.printf("Çevre: %.2f%n", cevre);
Alan: 19.63
Çevre: 15.71
rr2.5doublealan19.634954doublecevre?doubleπ · r²≈ 19.635 rr2.5doublealan19.634954doublecevre15.707963double2 · π · r≈ 15.708
Şekil 6.36: r = 2.5 için alan π · r² ≈ 19,635 ve çevre 2 · π · r ≈ 15,708 hesaplanır ve kutulara yazılır.

Alan π · r · r = 3,14159… · 2,5 · 2,5'tir ve alan kutusunda 19.634954084936208 değeri durur. Çevre 2 · π · r'dir: 15.707963267948966. π değeri elle 3.14 diye yazılmaz; Math.PI 16 anlamlı basamaklı hazır bir sabittir. %.2f belirteci değerleri iki basamağa yuvarlar: 19.634… → 19.63 ve 15.7079… → 15.71. Yuvarlama yalnızca yazdırmayı etkiler; değişkenlerdeki değerler değişmez ve sonraki hesaplarda tam değerler kullanılır. Kare almak için Math.pow(r, 2) da yazılabilir; r * r hem daha kısa hem daha hızlıdır.

6.6.5Saniyeyi saate çevirme

Bir süre saniye cinsinden verildiğinde onu saat:dakika:saniye biçimine çevirmek, tam sayı bölmesi ile kalan operatörünün birlikte kullanıldığı tipik bir problemdir.

int toplam = 3725;
int saat = toplam / 3600;
int kalan = toplam % 3600;
int dakika = kalan / 60;
int saniye = kalan % 60;
System.out.printf("%d:%02d:%02d%n",
                  saat, dakika, saniye);
1:02:05
Tablo 6.28: toplam = 3725 için izleme tablosu.
DeyimHesapSonuçAnlamı
saat = toplam / 36003725 / 36001kaç tam saat
kalan = toplam % 36003725 % 3600125saatlerden artan saniye
dakika = kalan / 60125 / 602kaç tam dakika
saniye = kalan % 60125 % 605dakikalardan artan saniye
printf("%d:%02d:%02d%n", …)1, 2, 51:02:05%02d: en az 2 hane, soldan 0
Bellek (değişkenler)toplam3725intsaat1intkalan125intdakika2intsaniye5int
Şekil 6.37: Programın sonunda beş kutu: toplam = 3725, saat = 1, kalan = 125, dakika = 2, saniye = 5.

Yöntem büyük birimden küçüğe ilerler. Önce kaç tam saat olduğu bulunur, sonra kalan saniyeler dakika ve saniyeye ayrılır. Sonuç bir sağlamayla doğrulanır: 1 · 3600 + 2 · 60 + 5 = 3725. %02d belirteci, tek basamaklı dakika ve saniyeleri önüne 0 koyarak iki haneye tamamlar; çıktı 1:2:5 yerine 1:02:05 olur. Aynı yöntem başka sürelerde de çalışır: 86399 saniye 23:59:59, 7384 saniye 2:03:04'tür. Formüller tek satırda da yazılabilir. toplam % 3600 / 60 ifadesinde % ve / aynı önceliktedir ve soldan sağa işlenir; önce kalan, sonra dakika bulunur.

Örnek 6.4

Kuruş cinsinden verilen bir tutar lira ve kuruşa nasıl ayrılır? 1575 ve 1005 kuruş için sonuç nedir?

int tutar = 1575;
System.out.println(tutar / 100 + " lira " + tutar % 100 + " kuruş");
tutar = 1005;
System.out.println(tutar / 100 + " lira " + tutar % 100 + " kuruş");
System.out.printf("%d,%02d TL%n", tutar / 100, tutar % 100);
15 lira 75 kuruş
10 lira 5 kuruş
10,05 TL

1 lira 100 kuruştur. 1575 / 100 = 15 tam lira, 1575 % 100 = 75 kuruş artar. 1005 kuruşta 1005 / 100 = 10 ve 1005 % 100 = 5 bulunur. Tutar Türkçe biçimde yazılırken kuruş iki haneye tamamlanmalıdır; aksi hâlde 10,05 TL yerine 10,5 TL yazılır ve okuyan 50 kuruş anlar. Son satırdaki %02d bu tamamlamayı yapar. Tutarı kuruş cinsinden int ya da long olarak tutmak, double'ın kayan nokta hatalarını da baştan önler.

6.7Yaygın hatalar

Bu bölümün konularında yapılan hataların çoğu birkaç kalıba girer. Sunumdaki tablo bunların beşini bir araya getirir:

Tablo 6.29: Yaygın hatalar (sunumdaki tablo).
HataÖrnekNe olur?
= ile == karışırif (x = 5)Derleme hatası: int, boolean değil
Tam sayı bölmesidouble o = 7 / 2;o = 3.0, 3.5 değil
Kapsam dışı kullanım{ int k = 1; } k++;k blok dışında tanımsız
Yanlış noktalı virgülif (x > 0);Boş deyim: koşul hiçbir şeyi denetlemez
String ve ==s1 == s2İçeriği değil, referansı karşılaştırır → s1.equals(s2)

= ile == karışması. Koşulda eşitlik denetlenmek istenirken tek eşittir yazılırsa ifade bir atama olur. x = 5 ifadesinin değeri 5'tir ve tipi int'tir; if ise boolean bir koşul ister. Java bu hatayı derleme sırasında yakalar:

error: incompatible types: int cannot be converted to boolean
if (x = 5) {
      ^
1 error

C gibi dillerde bu satır derlenir ve koşul her zaman doğru sayılır. Java'nın int ile boolean'ı karıştırmaması, bu hatayı zararsız bir derleme hatasına çevirir.

Tam sayı bölmesi. double o = 7 / 2; satırında hedef double'dır ama bölme iki int arasında yapılır: 7 / 2 = 3. Sonuç double'a genişler ve o = 3.0 olur. Taşma kesimindeki long sonuc = a * b hatasıyla aynı kökten gelir: ifadenin tipi, sonucun yazılacağı yere değil, işlenenlere bağlıdır.

Kapsam dışı kullanım. Bir değişken, bildirildiği süslü parantez bloğu boyunca geçerlidir. Bu alana değişkenin kapsamı (scope) denir. Blok kapanınca değişken ortadan kalkar:

{
    int k = 1;
}
k++;
error: cannot find symbol
k++;
^
  symbol:   variable k
  location: class Kapsam
1 error

Yanlış noktalı virgül. if koşulundan hemen sonra konan noktalı virgül, Java için kendi başına bir deyimdir: hiçbir şey yapmayan boş deyim (empty statement). Koşul yalnızca bu boş deyimi denetler; arkasındaki blok koşuldan bağımsız olarak her zaman çalışır:

int x = -4;
if (x > 0);
{
    System.out.println("pozitif");
}
pozitif

x = −4 olduğu hâlde “pozitif” yazılır. Program derlenir ve hiçbir uyarı vermez; bu yüzden bu hata zor bulunur.

String ve ==. Dizgi birleştirme kesiminde görüldüğü gibi, == iki String değişkeninin aynı nesneyi gösterip göstermediğine bakar. İçerik karşılaştırması equals ile yapılır.

Notun önceki kesimlerinde ayrıntılı ele alınan beş hata da bu listeye eklenebilir:

  • tipe sığmayan sonucun sessizce taşması,
  • kayan noktalı sayıların == ile karşılaştırılması,
  • (int) dönüşümünün yuvarlama sanılması,
  • "Sonuç: " + 2 + 3 ifadesinde toplama yapılacağının sanılması,
  • nextInt()'ten sonra satır sonunun tüketilmemesi.

6.8Özet

  • Değişken, belirli tipteki bir değer için bellekte ayrılmış adlı bir kutudur; adı, tipi ve değeri vardır. Bildirim tipi ve adı belirtir, ilk değer verme başlangıç değerini verir. Yerel değişken okunmadan önce kesin olarak değer almış olmalıdır; final değişken yalnızca bir kez atanır.
  • Atama önce sağ tarafı hesaplar, sonra sonucu soldaki değişkene yazar; eski değer silinir. Bileşik atama (+=, -= …) değişkenin eski değerini kullanarak günceller ve sağ tarafı bir bütün olarak hesaplar.
  • Tanımlayıcı harf, _ ya da $ ile başlar; boşluk içermez, anahtar kelime olamaz ve büyük-küçük harfe duyarlıdır. Değişkenler lowerCamelCase, sınıflar UpperCamelCase, sabitler BÜYÜK_HARF ile adlandırılır.
  • Java'nın sekiz ilkel tipi vardır: byte (8), short (16), int (32), long (64), float (32), double (64), char (16) ve boolean. Varsayılan seçimler int ve double'dır. long sabitleri L, float sabitleri F soneki ister.
  • Tam sayılar ikiye tümleyen gösterimle saklanır. Aralığı aşan sonuç taşar ve sessizce öbür uca sarar: Integer.MAX_VALUE + 1 → −2147483648. Taşmayı yakalamak için Math.addExact gibi metotlar istisna fırlatır. İfadenin tipi işlenenlerden belirlenir; long s = a * b yine int ile çarpar.
  • Kayan noktalı sayılar 0.1 gibi onluk kesirleri tam saklayamaz: 0.1 + 0.2 0.30000000000000004'tür. double değerler == ile değil bir eşikle karşılaştırılır. Para hesabında tam sayı kuruş ya da BigDecimal kullanılır.
  • char bir UTF-16 kod birimini sayı olarak tutar ('A' = 65); aritmetikte int'e yükselir. boolean yalnızca true ya da false alır ve sayılarla yer değiştirmez.
  • Genişletme otomatik ve kayıpsızdır (int → double). Daraltma açık dönüşüm ister: (int) 9.8 9 verir; kesir yuvarlanmaz, sıfıra doğru kesilir. Tam sayı daraltmasında alt bitler kalır: (byte) 200 −56'dır. Yuvarlama için Math.round kullanılır.
  • İki tam sayının bölümü tam sayı bölmesidir: 5 / 2 2'dir. İşlenenlerden biri double ise öbürü yükseltilir ve 5.0 / 2 2.5 olur. (double) (5 / 2) ise kaybolan kesri geri getirmez. byte, short ve char işlemlerde önce int'e yükselir.
  • % kalanı verir ve işaretini bölünenden alır. Tam sayıyı sıfıra bölmek ArithmeticException fırlatır; kayan noktalı bölme ise Infinity ya da NaN üretir.
  • Sonek i++ eski değeri, önek ++i yeni değeri üretir. Bileşik atama sonucu sol tarafın tipine örtük olarak daraltır: b += 5 derlenir, b = b + 5 derlenmez.
  • Karşılaştırma operatörleri boolean üretir; &&, || ve ! boolean değerleri birleştirir. İşlem önceliği: parantez, tekli, çarpımsal, toplamsal, sıra, eşitlik, &&, ||, atama. Aynı düzeyde soldan sağa gidilir. Derleyici ifadeyi bir ifade ağacına çevirir.
  • Kısa devre: &&'de sol false ise, ||'de sol true ise sağ taraf hesaplanmaz. Koruyucu koşul (b != 0) sola yazılır.
  • + işlenenlerden biri dizgiyse birleştirme yapar ve soldan sağa işler: "Sonuç: " + 2 + 3 “Sonuç: 23”, "Sonuç: " + (2 + 3) “Sonuç: 5” verir. Dizgi içeriği equals ile karşılaştırılır.
  • Scanner klavyeden okur; girdi bir tamponda bekler. nextInt() satır sonunu tamponda bırakır; ardından satır okunacaksa önce bir nextLine() ile tüketilir. Girdi önce tip, sonra aralık açısından doğrulanır.
  • printf, biçim dizgisindeki biçim belirteçlerini (%d, %f, %s, %n …) argümanlarla sırayla doldurur; genişlik, kesinlik ve bayraklar (-, 0, ,) çıktıyı hizalar. Ondalık ayırıcı yerel ayara bağlıdır; Türkçe ayarda printf virgül yazar, nextDouble virgül bekler.

6.9Kavram listesi

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

6.10Kendini Dene

  1. int x = 1_000_000; int y = x * 3000; long z = x * 3000L; satırlarından sonra System.out.println(y + " " + z); ne yazar? y neden yanlıştır?

    Cevap

    Çıktı -1294967296 3000000000'dır. 1.000.000 · 3.000 = 3.000.000.000, int sınırı 2.147.483.647'yi aşar. İki işlenen de int olduğu için çarpma int ile yapılır ve taşar: 3.000.000.000 − 232 = −1.294.967.296. z'de 3000L bir long sabitidir; x long'a yükselir ve çarpma long ile yapılır.

  2. (byte) 300, (int) -7.9, Math.round(-7.5) ve (int) Math.round(7.5) ifadelerinin değerleri nedir?

    Cevap

    Sırasıyla 44, −7, −7 ve 8. 300'ün alt 8 biti tutulur: 300 − 256 = 44 (işaret biti 0 olduğu için pozitiftir). (int) -7.9 sıfıra doğru keser: −7. Math.round yarımları pozitif yöne yuvarlar: −7.5 → −7, 7.5 → 8; ikincisi long döndürdüğü için int'e çevrilmiştir.

  3. int a = 7, b = 8; double o = (a + b) / 2; satırından sonra o kaçtır? Doğru ortalamayı veren iki yazılış nedir?

    Cevap

    o = 7.0'dır. a + b 15'tir ve int'tir; 15 / 2 tam sayı bölmesiyle 7 olur, sonra double'a genişleyerek 7.0 yazılır. Doğru yazılışlar (a + b) / 2.0 ve (double) (a + b) / 2'dir; ikisi de 7.5 verir. (double) ((a + b) / 2) ise yine 7.0 verir, çünkü dönüşüm bölmeden sonra yapılır.

  4. int i = 10; int a = i-- - 3; int b = --i * 2; satırlarından sonra i, a ve b kaçtır?

    Cevap

    i = 8, a = 7, b = 16. Sonek i-- eski değeri (10) üretir: a = 10 − 3 = 7; ardından i 9 olur. Önek --i önce i'yi 8 yapar, sonra yeni değeri kullanır: b = 8 · 2 = 16.

  5. short s = 10; s *= 2.6; derlenir mi? Derlenirse s kaç olur? s = s * 2.6; için ne söylenebilir?

    Cevap

    Derlenir ve s = 26 olur. Bileşik atama s = (short) (s * 2.6) anlamına gelir: 10 · 2,6 = 26,0 (double) hesaplanır ve short'a daraltılır. s = s * 2.6; ise derlenmez; sağ taraf double'dır ve double'dan short'a örtük dönüşüm yoktur (“possible lossy conversion from double to short”).

  6. 5 + 4 % 3 * 2 - 8 / 3 ve !(3 > 2) || 4 >= 4 && 1 != 1 ifadelerinin değerleri nedir? Hesap hangi sırayla yapılır?

    Cevap

    İlki 5'tir. *, / ve % aynı düzeydedir ve soldan sağa işlenir: 4 % 3 = 1, 1 · 2 = 2, 8 / 3 = 2; sonra 5 + 2 − 2 = 5. İkincisi false'tur. Parantez içi 3 > 2 true'dur ve ! ile false olur. &&, ||'den önce gelir: 4 >= 4 true, 1 != 1 false, true && false = false. Sonuç false || false = false.

  7. System.out.println(1 + 2 + "3" + 4 + 5); ve System.out.println('a' + 2 * 3 + ""); ne yazar?

    Cevap

    İlki “3345”tir: soldan sağa önce iki int toplanır (3), sonra “3” dizgisiyle birleşir (“33”); bundan sonra her işlem birleştirmedir: “334”, “3345”. İkincisi “103”tür: 2 · 3 = 6 önce hesaplanır; 'a' int'e yükselir ve 97 + 6 = 103 bulunur; en son boş dizgiyle birleşir.

  8. Kullanıcı ilk satıra 3 4.5, ikinci satıra Ali Veli yazıyor (Locale.US). Program sırasıyla nextInt(), nextDouble(), nextLine() ve nextLine() çağırıyor. Dört çağrı ne döndürür?

    Cevap

    3, 4.5, "" (boş dizgi) ve "Ali Veli". nextInt ve nextDouble aynı satırdaki iki sayıyı okur; ilk satırın sonundaki satır sonu tamponda kalır. İlk nextLine bu satır sonuna kadar okur ve boş dizgi döndürür. İkinci nextLine ikinci satırı okur. Türkçe yerel ayarda ise nextDouble “4.5” girdisinde InputMismatchException fırlatırdı.

  9. System.out.printf("%-5s|%8.3f|%,d%n", "Kalem", 12.34567, 1500000); satırı Locale.US ayarında ne yazar? Her alan kaç karakter tutar?

    Cevap

    Kalem| 12.346|1,500,000. “Kalem” tam 5 harftir; %-5s boşluk eklemez. %8.3f 12.34567'yi üç basamağa yuvarlar (12.346, 6 karakter) ve 8 karaktere tamamlamak için önüne 2 boşluk koyar. %,d binlik ayırıcı ekler; Türkçe ayarda bu alan 1.500.000, ikinci alan 12,346 olurdu.

6.11İleri okuma

  • R. Sedgewick, K. Wayne, Computer Science: An Interdisciplinary Approach (Addison-Wesley, 2016), 1.2 Built-in Types of Data: Java'nın ilkel tiplerini, tip dönüşümünü ve kayan nokta sorunlarını kısa programlarla anlatır; kitabın sitesinde (introcs.cs.princeton.edu) bölümün özeti ve alıştırmaları vardır.
  • Oracle, The Java Tutorials, Language Basics: Variables ve Operators: ilkel tipleri, sabitlerin yazılışını, operatör öncelik tablosunu ve bileşik atamayı resmî kaynaktan, örneklerle verir.
  • Y. D. Liang, Introduction to Java Programming and Data Structures (13. baskı, Pearson, 2024), Bölüm 2 Elementary Programming ve Bölüm 4: Scanner ile girdi, sayısal tipler, printf ve Math sınıfı üzerine çok sayıda çözülmüş örnek içerir.
  • Oracle, Java Language Specification, Java SE 21: Bölüm 5 (Conversions and Contexts) ve Bölüm 15 (Expressions): bu notta anlatılan dönüşüm, yükseltme ve değerlendirme sırası kurallarının kesin tanımlarıdır; ileri düzeyde merak edilen bir ayrıntının son sözü buradadır.

6.12Kaynaklar

  1. P. Deitel, H. Deitel, Java: How to Program, Early Objects, 11. baskı, Pearson, 2018.
  2. Y. D. Liang, Introduction to Java Programming and Data Structures, 13. baskı, Pearson, 2024.
  3. Oracle, The Java Tutorials: Variables, Operators.
  4. Oracle, Java Language Specification, Java SE 21 Edition: Bölüm 4, Types, Values, and Variables; Bölüm 15, Expressions.
  5. Oracle, Java SE 21 API: java.util.Scanner, java.lang.Math, java.util.Formatter (biçim belirteçleri).
  6. R. Sedgewick, K. Wayne, Computer Science: An Interdisciplinary Approach, Addison-Wesley, 2016: 1.2 Built-in Types of Data.

İlgili sayfalar: Değişkenler, Veri Tipleri ve Operatörler sunumu · Çalışma soruları · İfade değerlendirici · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.