Java ile Nesne Yönelimli Programlama · Ders Notu

Bölüm 1.1: Bilgisayarlar

Java ile yazılan her satır, sonunda bir işlemcinin anladığı sayılara dönüşür. Bir int değişken neden 2.147.483.647'yi aşamaz? "😀".length() neden 2 verir, 0.1 + 0.2 neden tam 0,3 çıkmaz? Bu soruların cevabı o dönüşümdedir. Bu not, bilgisayarı katman katman kurar. Önce elektrik sinyalleri bitlere, bitler sayılara dönüşür. Sayılarla harfler ve metinler, sayı dizileriyle resim, ses ve film temsil edilir. En sonda komutların da sayı olarak bellekte durduğu saklanan program fikri ve onu donanıma taşıyan von Neumann mimarisi gelir. Her katmanda konunun Java'daki karşılığı kısa programlarla gösterilir ve programların çıktıları adım adım izlenir.

Öğrenme çıktıları

Donanım
İşlemci, bellek, disk ve veri yolunun görevini açıklamak; bir programın işlemcide Getir–Çöz–Yürüt döngüsüyle nasıl yürüdüğünü adım adım izlemek.
Sayılar
Sayıları ikili, sekizli, onaltılı ve ondalık tabanlar arasında çevirmek; ikili toplama yapmak ve negatif sayıları ikiye tümleyenle hesaplamak.
Metin
ASCII, Unicode ve UTF-8 arasındaki farkı açıklamak; bir dizginin bellekte sayılarla nasıl temsil edildiğini göstermek.
Java ile bağlantı
byte ve int sınırlarını ve taşmayı ikiye tümleyenle açıklamak; Java'da tabanlarla çalışmak; char ile String uzunluğunun UTF-16 ile ilişkisini kurmak; assembly komutlarının makine koduna karşılığını göstermek.

1.1.1Bilgisayarın temelleri

Bilgisayar, verilen komutları çok hızlı ve hatasız biçimde art arda yürüten bir makinedir. Komutlar tek başına çok basittir: iki sayıyı topla, bir sayıyı bellekten oku, sonucu belleğe yaz, şu koşul doğruysa başka bir komuta geç. Bilgisayarı güçlü yapan, bu basit adımları saniyede milyarlarca kez yapabilmesidir. Nesne yönelimli bir programın sınıfları, nesneleri ve metotları da sonunda bu basit adımlara dönüşür. Bu yüzden Java öğrenmeye makinenin kendisinden başlamak, ileride karşılaşılacak pek çok davranışı açıklar.

1.1.1.1Temel parçalar

Bir bilgisayarın içinde çok sayıda bileşen vardır, ama programcı açısından üçü öne çıkar. Bu üç parça birbirine veri taşıyan yollarla bağlıdır.

  • İşlemci (CPU, Central Processing Unit; merkezi işlem birimi): bilgisayarın “beyni” sayılır. Bütün aritmetik ve mantıksal hesapları yapar. Toplama, çıkarma, iki sayıyı karşılaştırma ve bitler üzerinde mantık işlemleri onun işidir.
  • Bellek (RAM, Random Access Memory; rastgele erişimli bellek): çalışan programı ve onun verilerini geçici olarak saklar. “Rastgele erişim”, herhangi bir hücreye diğerlerini sırayla dolaşmadan aynı sürede ulaşılabilmesi demektir. Belleğin kapasitesi ve erişim hızı, bilgisayarın hızını doğrudan etkiler.
  • Sabit disk (HDD ya da SSD): verileri kalıcı olarak saklar. Elektrik kesildiğinde RAM'deki her şey silinir, diskteki dosyalar ise yerinde kalır. HDD dönen manyetik plakalar kullanır, SSD ise hareketli parçası olmayan yarı iletken bellek yongalarıdır; ikisi de RAM'den çok daha büyük ve çok daha yavaştır.
Sabit DiskSSD / HDDBellekRAMİşlemciCPUkalıcı · büyükyavaşgeçici · ortahızlıhesaplarkarar verirprogram ve veri yüklenir →
Şekil 1.1.1: Disk, bellek ve işlemci arasındaki veri akışı. Program ve verisi diskten belleğe yüklenir, işlemci belleğe erişerek hesap yapar. Disk kalıcı ama yavaş, bellek geçici ama hızlıdır.

Bu üçlünün ilişkisi bir mutfak benzetmesiyle kolayca hatırlanır. Aşçı işlemcidir. Tezgâh RAM'dir: küçüktür ama aşçının hemen elinin altındadır. Kiler ise disktir: çok büyüktür ama uzaktadır. Aşçı kilerdeki malzemeyle doğrudan yemek yapmaz. Önce gereken malzemeyi kilerden tezgâha taşır, orada çalışır. Bilgisayarda da bir program çalıştırılınca kodu ve verisi önce diskten belleğe yüklenir. İşlemci yalnız bellekteki komut ve verilerle çalışır. Tezgâh dolarsa aşçı sık sık kilere gider ve yemek yavaşlar; RAM yetersiz kaldığında bilgisayarın yavaşlaması da aynı nedenledir.

Elektrik-elektronik açısından bakıldığında RAM ile disk arasındaki fark, bilginin nasıl tutulduğundan gelir. Yaygın kullanılan dinamik RAM (DRAM), her biti küçük bir kondansatördeki yük olarak saklar. Bu yük kendiliğinden boşaldığı için saniyede pek çok kez tazelenmesi gerekir; güç kesilince bilgi kaybolur. SSD'deki flaş bellek ise yükü yalıtılmış bir kapı içinde hapseder; bu yük güç olmadan da yıllarca kalır. Bu yüzden RAM'e uçucu, diske kalıcı bellek denir.

1.1.1.2Bilgisayarın işleyişi ve programlama

Bilgisayarın işleyişi üç temel adımla özetlenir. İşlemciye talimatlar, yani komutlar gönderilir. İşlemci bu talimatları sırayla işler. Sonuçlar kullanıcıya ya da başka yazılımlara sunulur. Ekrana bir yazı basmak, bir dosyayı kaydetmek ya da bir ağ paketini göndermek hep bu döngünün ürünüdür.

Tanım 1.1.1

Programlama, bilgisayarın istenen görevi yerine getirmesi için ona anlayacağı biçimde talimatlar yazma sürecidir. Yazılan talimatların bütününe program denir.

En kısa Java programı, ekrana tek bir satır yazan programdır. İçinde yalnız bir talimat vardır: “ekrana yaz”.

public class MerhabaDunya {
    public static void main(String[] args) {
        System.out.println("Merhaba, Dünya!");
    }
}
Merhaba, Dünya!

Programın kalıbı ilk anda kalabalık görünebilir. public class MerhabaDunya satırı bir sınıf tanımlar; Java'da her kod bir sınıfın içinde durur ve dosyanın adı da MerhabaDunya.java olmalıdır. main metodu programın başladığı yerdir; Java Sanal Makinesi (JVM, Java Virtual Machine) programı çalıştırırken ilk olarak bu metodu çağırır. Asıl iş, System.out.println(...) satırındadır. Bu satır, tırnak içindeki dizgiyi standart çıktıya yazar ve satırı bitirir.

İşlemci println sözcüğünü tanımaz. Java gibi bir dilde yazılan talimatlar, işlemcinin anladığı makine koduna çevrilerek çalıştırılır. Java'da bu çeviri iki aşamalıdır. Önce javac derleyicisi kaynak kodu bytecode denen ara bir koda çevirir. Ardından JVM bu kodu yorumlar ya da sık çalışan kısımlarını o işlemcinin makine koduna derler. Sonuçta ekrana harf basmak da işlemci için sayıları bir yerden bir yere taşımaktan ibarettir. Notun geri kalanı, bu sayıların neyi nasıl temsil ettiğini anlatır.

1.1.1.3Katman katman bir bilgisayar

Bir bilgisayarın nasıl çalıştığını tek seferde kavramak zordur. Bunun yerine bilgisayarı katman katman kurmak daha kolaydır. Her katman bir alttakinin üzerine oturur ve onun sunduğu yapı taşlarını kullanır.

1. SayılarNumbers2. Harf ve DizgilerLetters and Strings3. Yapılandırılmış BilgiStructured Information4. Saklanan Program KavramıStored Program Concept5. von Neumann Mimarisivon Neumann Architecture
Şekil 1.1.2: Notun yol haritası. En altta sayılar vardır; sayılarla harfler, harflerle yapılandırılmış bilgi kurulur. Komutlar da sayı olarak saklanır ve hepsini von Neumann mimarisi bir araya getirir.
  1. Önce elektrik sinyalleri sayılara dönüştürülür.
  2. Sayılarla harfler, harflerle metinler temsil edilir.
  3. Sayı dizileriyle resim, ses ve film gibi yapılandırılmış bilgi saklanır.
  4. Komutlar da sayı olarak bellekte saklanır.
  5. Hepsini von Neumann mimarisi bir araya getirir.

Bu sıra notun kesimlerini de belirler. Her katmanda aynı soru sorulur: bir üst katmandaki kavram, bir alttaki sayılarla nasıl ifade edilir? Bu soruya verilen cevaplar, Java'nın ilkel tiplerinin (byte, int, char, double) neden öyle tasarlandığını da açıklar.

1.1.2İkili sayılar

1.1.2.1Elektrik sinyalinden bite

Bilgisayarın temel işi, elektrik sinyallerini belirli kurallara göre işlemektir. Bu kurallar sayesinde toplama, karşılaştırma ve kopyalama gibi pek çok işlev gerçekleşir. Sinyaller tek başına bir anlam taşımaz. Onları insanın anlayacağı sayı ve sembollerle ilişkilendirdiğimizde bilgisayar bize anlamlı bilgi sunabilir.

Bilgisayarlar sinyalleri ikili (binary, taban 2) temsille işler. Bir teldeki voltaj belirli bir eşiğin üstündeyse 1, altındaysa 0 okunur. Bütün işlemler bu ikili temelin üzerine kuruludur.

Tanım 1.1.2

Bit (binary digit, ikili basamak), yalnız 0 ya da 1 değerini alabilen en küçük bilgi birimidir. Sekiz bitlik bir gruba bayt (byte) denir.

voltajeşik01101011yüksekdüşük
Şekil 1.1.3: Bir teldeki voltaj zamanla değişir. Eşiğin üstündeki aralıklar 1, altındakiler 0 okunur; bu dalga 01101011 bit dizisini taşır.

0 ve 1 değerleri bağlama göre farklı adlar alır. Tablo 1.1.1 en sık kullanılan karşılıkları gösterir. Mantıkta 0 yanlış, 1 doğrudur; anahtarda 0 kapalı, 1 açıktır; devrede ise 0 düşük, 1 yüksek voltajdır. Hepsi aynı ikiliğin farklı yorumlarıdır.

Tablo 1.1.1: Bitin iki değerinin farklı yorumları.
01
Yanlış (false)Doğru (true)
KapalıAçık
Düşük voltajYüksek voltaj

Neden on değil de iki durum kullanılır? Gerçek bir telde voltaj hiçbir zaman tam istenen değerde durmaz. Isı, komşu tellerden gelen girişim ve güç kaynağındaki dalgalanma, sinyale gürültü ekler. Bir teldeki voltajın “var mı, yok mu” olduğunu gürültüye rağmen ayırt etmek kolaydır, çünkü iki durum arasında geniş bir güvenlik payı bırakılabilir. Aynı voltaj aralığını on eşit seviyeye bölmek ise her seviyeyi daraltır. Küçük bir gürültü sinyali komşu seviyeye kaydırır ve 7 diye gönderilen rakam 6 diye okunur. Elektronikteki mantık kapıları bu yüzden iki durumla çalışır ve sayısal devreler bu güvenlik payı sayesinde son derece güvenilirdir.

Bellek ve dosya boyutları bayt cinsinden ölçülür. Ön ekler iki farklı biçimde kullanılır. Uluslararası Birimler Sistemi'nde (SI) 1 kilobayt (KB) 1000 bayttır. İkinin kuvvetlerine dayanan ikili ön eklerde ise 1 kibibayt (KiB) 1024 bayttır. Fark büyüdükçe artar: 1 GB = 109 bayt, 1 GiB = 230 = 1.073.741.824 bayttır. Bir diskin üzerinde yazan 1 TB'lık kapasitenin işletim sisteminde yaklaşık 931 “GB” görünmesinin nedeni, işletim sisteminin aslında GiB ile hesaplamasıdır.

1.1.2.2Basamak değeri

Gündelik hayatta kullanılan ondalık sistem bir basamak değerli sistemdir. 603 sayısında 6, yüzler basamağında olduğu için 600 değer taşır. Aynı rakam başka bir basamakta başka bir değer taşır. İkili sistem de aynı ilkeyle çalışır, yalnız taban 10 yerine 2'dir.

Bir tam sayı, 2'nin kuvvetlerinin toplamı olarak yazılır ve her kuvvet ya hiç kullanılmaz (0) ya da bir kez kullanılır (1). Dört ardışık kuvvetle, yani 20, 21, 22, 23 ile 0–15 arasındaki bütün tam sayılar temsil edilir. 13 sayısı şöyle yazılır:

1310 = 1·23 + 1·22 + 0·21 + 1·20 = 8 + 4 + 0 + 1 = 11012

2³ = 811·88+2² = 411·44+2¹ = 200·20+2⁰ = 111·118 + 4 + 0 + 1 = 13
Şekil 1.1.4: 11012 sayısında basamak ağırlıkları soldan sağa 8, 4, 2 ve 1'dir. Değeri 1 olan basamakların ağırlıkları toplanır: 8 + 4 + 0 + 1 = 13.

Sayının sağ altındaki küçük rakam, yani alt simge, tabanı gösterir. 11012 “bir bir sıfır bir, ikili” diye okunur, “bin yüz bir” diye okunmaz. Alt simge yazılmayan sayılar ondalık kabul edilir. Aynı kural öteki tabanlarda da geçerlidir (Tablo 1.1.2).

Tablo 1.1.2: Basamak değeri kuralı her tabanda aynıdır.
TabanÖrnek
Ondalık (10)60310 = 6·102 + 0·101 + 3·100
Sekizli (8)2078 = 2·82 + 0·81 + 7·80 = 13510
Onaltılı (16)FF16 = 15·161 + 15·160 = 25510

İkili bir sayıyı ondalığa çevirmek bu yüzden kolaydır: 1 olan basamakların ağırlıkları toplanır. 101101102 sayısında 1'ler 7, 5, 4, 2 ve 1 numaralı basamaklardadır (en sağdaki basamak 0 numaralıdır). Değer 128 + 32 + 16 + 4 + 2 = 182'dir. Ağırlıkları ezbere bilmek bu hesabı hızlandırır: 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024. Programcılar bu diziyi çarpım tablosu gibi bilir.

1.1.2.3Herhangi bir taban ve bit sınırları

Basamak değeri fikri her tabana genellenir. Taban b ≥ 2 ise her basamak 0, 1, …, b − 1 değerlerinden birini alır. İkilide basamaklar 0–1, sekizlide 0–7, onaltılıda 0–9 ve A–F'dir. Onaltılıda A = 10, B = 11, C = 12, D = 13, E = 14 ve F = 15'tir. Tek basamakla 15'e kadar sayabilmek için altı yeni simge gerekir.

Teorem 1.1.1 (Basamak sınırı)

Taban b ≥ 2 ve d basamakla, 0 ile bd − 1 arasındaki her tam sayı yazılabilir ve başka sayı yazılamaz.

Her basamak b farklı değer alabildiği için d basamakla toplam bd farklı rakam dizisi kurulur. En küçük dizi 00…0 = 0'dır. En büyük dizinin bütün basamakları b − 1'dir; buna 1 eklenince elde ilerleyerek en solda yeni bir basamak açılır ve bd elde edilir. Demek ki en büyük değer bd − 1'dir. Arada bd tane sayı vardır ve her biri farklı bir diziye karşılık gelir. ∎

Ondalıkta üç basamakla 0–999 yazılır: 103 − 1 = 999. İkilide d bitle en büyük işaretsiz değer 2d − 1'dir (Tablo 1.1.3). 64 bitlik bir bilgisayar, işaretsiz olarak 264 − 1 = 18.446.744.073.709.551.615'e kadar sayabilir.

Tablo 1.1.3: Bit sayısına göre en büyük işaretsiz değer.
BitEn büyük (2d − 1)
415
8255
1665.535
324.294.967.295

Java'nın tam sayı tipleri ise işaretlidir: bitlerin temsil ettiği değerlerin yarısı negatif sayılara ayrılır. Bu yüzden 8 bitlik byte 255'e değil, 127'ye kadar çıkar. Aşağıdaki program dört tam sayı tipinin en büyük değerini yazdırır.

System.out.println(Byte.MAX_VALUE);    // 8 bit
System.out.println(Short.MAX_VALUE);   // 16 bit
System.out.println(Integer.MAX_VALUE); // 32 bit
System.out.println(Long.MAX_VALUE);    // 64 bit
// 64 bitin hepsi 1, işaretsiz okunursa:
System.out.println(Long.toUnsignedString(-1L));
127
32767
2147483647
9223372036854775807
18446744073709551615

Her sabit, tablodaki işaretsiz sınırın yaklaşık yarısıdır. Byte.MAX_VALUE = 127 = 27 − 1, Short.MAX_VALUE = 32.767 = 215 − 1, Integer.MAX_VALUE = 2.147.483.647 = 231 − 1 ve Long.MAX_VALUE = 263 − 1'dir. Bir bitin, negatif sayıları ayırt etmek için kullanıldığı görülür. Son satırdaki -1L sayısının 64 bitinin hepsi 1'dir. Long.toUnsignedString bu bitleri işaretsiz okuyunca 264 − 1 değerini verir. Aynı bitler, nasıl yorumlandığına göre −1 de olabilir, 18 kentilyondan büyük bir sayı da. Bu ikilik negatif sayılar konusunda yeniden karşımıza çıkar.

Java'da int her platformda 32 bit, long her platformda 64 bittir. C dilinde int'in boyu derleyiciye ve işlemciye göre değişebilir. Java'nın sabit boyutları, aynı programın her makinede aynı sonucu vermesini sağlar.

1.1.2.4Ondalıktan ikiliye: 2'ye bölme yöntemi

Ondalık bir sayıyı ikiliye çevirmenin en sistemli yolu, sayıyı art arda 2'ye bölmektir. Her bölmede kalan 0 ya da 1'dir ve bu kalan, sayının henüz yazılmamış en sağdaki bitidir. Bölüm 0 olunca durulur ve kalanlar aşağıdan yukarı okunur.

Yöntemin neden çalıştığını görmek için bir sayının ikili yazılışına bakmak yeter. 13 = 11012 ise 13 = 2·6 + 1'dir. Kalan 1, en sağdaki bittir; bölüm 6 ise geri kalan bitlerin oluşturduğu sayıdır, yani 1102. Aynı işlem 6'ya uygulanınca bir sonraki bit çıkar. Her bölme bir biti “koparır” ve sayıyı bir bit sağa kaydırır. Aşağıdaki program bu yöntemi Java ile uygular.

public static void main(String[] args) {
  int sayi = 13;
  String ikili = "";
  while (sayi > 0) {
    int kalan = sayi % 2;
    System.out.println(sayi + " % 2 = " + kalan);
    ikili = kalan + ikili;
    sayi = sayi / 2;
  }
  System.out.println("Sonuç: " + ikili);
}

Kodda dört nokta önemlidir. sayi % 2, kalan alma (mod) işlemidir ve son biti verir. sayi / 2, iki int arasında yapıldığı için tam sayı bölmesidir: kesir atılır, 13 / 2 = 6 olur. ikili = kalan + ikili satırında kalan bir int, ikili ise bir String'dir. Bir dizgiyle toplanan sayı dizgiye çevrilir ve iki dizgi birleştirilir. Yeni bit dizginin önüne eklendiği için bitler doğru sırada birikir ve kalanları ayrıca ters çevirmek gerekmez. Döngü, sayi 0 olunca biter. Tablo 1.1.4 13 için her turu gösterir.

Tablo 1.1.4: sayi = 13 için döngünün izlemesi.
TurKoşulkalanYazılan satırikiliYeni sayi
113 > 0 doğru113 % 2 = 1"1"6
26 > 0 doğru06 % 2 = 0"01"3
33 > 0 doğru13 % 2 = 1"101"1
41 > 0 doğru11 % 2 = 1"1101"0
—0 > 0 yanlışDöngü biter; Sonuç: 1101 yazılır.
13 % 2 = 1
6 % 2 = 0
3 % 2 = 1
1 % 2 = 1
Sonuç: 1101
sayi÷ 2kalan13→ 616→ 303→ 111→ 01okuikili1101
Şekil 1.1.5: 13'ün 2'ye bölünmesi. Her satırda sayı, yarısı ve kalanı görülür. Kalanlar aşağıdan yukarı okununca 1101 elde edilir.

Döngü dört tur döndü, çünkü 13'ün ikili gösterimi dört bittir. Genel olarak n ≥ 1 için tur sayısı ⌊log2 n⌋ + 1'dir: sayı her turda yarıya iner ve 1'den küçük olana kadar yarılanma sayısı bu kadardır. 1000 sayısı 10 turda, bir milyar 30 turda çevrilir.

Kodun bir sınırı da vardır: sayi = 0 için döngü hiç çalışmaz ve ikili boş kalır; ekrana yalnız Sonuç: yazılır. Negatif sayılarda da koşul ilk turda yanlış olur. Gerçek bir programda 0 için ayrı bir durum yazmak gerekir. Java'nın hazır Integer.toBinaryString metodu bu ayrıntıları kendisi çözer.

Örnek 1.1.1

45 sayısı 8 bitlik ikili olarak nasıl yazılır? Bölme zinciri şöyledir: 45 / 2 = 22 kalan 1; 22 / 2 = 11 kalan 0; 11 / 2 = 5 kalan 1; 5 / 2 = 2 kalan 1; 2 / 2 = 1 kalan 0; 1 / 2 = 0 kalan 1. Kalanlar aşağıdan yukarı okununca 101101 çıkar. Sekiz bite tamamlamak için soluna iki sıfır eklenir: 00101101. Sağlama: 32 + 8 + 4 + 1 = 45.

Aynı sonuca kuvvetleri çıkararak da ulaşılır. 45'e sığan en büyük kuvvet 32'dir; kalan 13. 13'e sığan en büyük kuvvet 8'dir; kalan 5. Sonra 4, kalan 1; sonra 1, kalan 0. Kullanılan kuvvetler 32, 8, 4 ve 1'dir; bu basamaklara 1, ötekilere 0 yazılır.

1.1.2.5Sekizli ve onaltılı tabanlar

İkili sayılar uzundur. 32 bitlik bir int'i otuz iki rakamla yazmak hem yorucudur hem de hataya açıktır. Programcılar bu yüzden ikili sayıları daha kısa yazmak için sekizli ve onaltılı tabanları kullanır. Bu iki tabanın seçilme nedeni, 8 ve 16'nın 2'nin kuvveti olmasıdır: 8 = 23 ve 16 = 24.

Bu ilişki çevirmeyi hesapsız hâle getirir. Bir onaltılı basamak tam olarak dört bite, bir sekizli basamak tam olarak üç bite karşılık gelir. İkiliden onaltılıya çevirmek için bitler sağdan başlayarak dörderli gruplara ayrılır ve her grup tek bir onaltılı basamağa çevrilir. Sekizli için gruplar üçlüdür.

Örnek 1.1.2

101111102 sayısı onaltılı ve sekizli tabana çevrilecek.

Onaltılı için dörderli gruplar: 1011 1110. 1011 = 8 + 2 + 1 = 11 = B, 1110 = 8 + 4 + 2 = 14 = E. Sonuç BE16'dır.

Sekizli için sağdan üçerli gruplar: 10 111 110. En soldaki eksik grup başına sıfır eklenerek 010 olur. 010 = 2, 111 = 7, 110 = 6; sonuç 2768'dir.

Sağlama: 11·16 + 14 = 190 ve 2·64 + 7·8 + 6 = 190. İkili sayının ondalık değeri de 128 + 32 + 16 + 8 + 4 + 2 = 190'dır.

Onaltılı taban günümüzde sekizliden çok daha yaygındır. Bir bayt sekiz bit olduğu için tam iki onaltılı basamağa sığar: 00–FF. Bellek adresleri, renk kodları (#FF0000 kırmızı), dosyaların ham içerikleri ve Unicode kod noktaları hep onaltılı yazılır. Sekizli taban ise Unix dosya izinleri (chmod 755) gibi az sayıda yerde yaşar.

1.1.2.6Java'da taban dönüşümleri

Java, tabanlar arasında çeviri için hazır metotlar ve tam sayı sabitlerini farklı tabanlarda yazmak için ön ekler sunar. Aşağıdaki program ikisini birlikte kullanır.

System.out.println(Integer.toBinaryString(13));  // ikili
System.out.println(Integer.toOctalString(135));  // sekizli
System.out.println(Integer.toHexString(255));    // onaltılı
System.out.println(Integer.parseInt("1101", 2)); // 2 → 10
System.out.println(Integer.parseInt("207", 8));  // 8 → 10
System.out.println(0b1101 + " " + 0207 + " " + 0xFF);
1101
207
ff
13
135
13 135 255

İlk üç satır bir int değeri dizgiye çevirir. Integer.toBinaryString(13) ikili, toOctalString(135) sekizli, toHexString(255) onaltılı gösterimi verir. toHexString küçük harf kullanır; çıktı FF değil ff'dir. Ters yöndeki çeviriyi Integer.parseInt yapar. İkinci argüman, dizginin hangi tabanda yazıldığını söyler. parseInt("1101", 2) dizgiyi ikili okur ve 13 döndürür. Dönen değer artık sıradan bir int'tir; taban yalnız yazılışın özelliğidir, sayının değil.

Son satır, kaynak kodda sabitleri doğrudan farklı tabanlarda yazmayı gösterir. Bu yazılışlara sabit ya da değişmez (literal) denir ve ön ekleri Tablo 1.1.5'te özetlenmiştir.

Tablo 1.1.5: Java'da tam sayı sabitlerinin ön ekleri.
Ön ekTabanÖrnekNot
0b ya da 0Bİkili0b1101 = 13Java 7 ile geldi.
0Sekizli0207 = 135010 on değil, 8'dir.
0x ya da 0XOnaltılı0xFF = 255Renk ve bayt değerlerinde sık kullanılır.
(yok)Ondalık135Başında 0 olmamalıdır.

Ön ekler yalnız yazılışı değiştirir. 0b1101, 015, 0xD ve 13 aynı int değerini üretir ve bellekte aynı 32 bit olarak durur. Uzun sabitler alt çizgiyle gruplanabilir: 1_000_000 ya da 0b1011_1110. Derleyici alt çizgileri yok sayar; okunurluk artar.

Sık yapılan hata

Sayıyı hizalamak için başına sıfır koymak. int x = 010; derlenir, ama sabit sekizli okunduğu için x 10 değil 8 olur. int y = 08; ise hiç derlenmez: sekizli tabanda 8 rakamı yoktur ve javac “error: ';' expected” iletisini verir. İleti ilk bakışta ilgisiz görünür; derleyici 0'ı tam bir sabit sayıp ardından gelen 8'i beklenmedik bir sözcük olarak görür.

1.1.2.7İkili toplama

Toplama, hangi taban kullanılırsa kullanılsın aynı ilkelerle yapılır. Sayılar basamak değerlerine göre alt alta yazılır ve sağdan sola toplanır. Bir basamağa sığmayan değer, bir sonraki basamağa elde (carry, taşıma) olarak aktarılır. Onlukta 7 + 8 = 15 olunca 5 yazılır ve 1 elde kalır. İkilide 1 + 1 = 2 = 102 olunca 0 yazılır ve 1 elde kalır. Elde ile birlikte üç tane 1 toplanırsa 3 = 112 olur: 1 yazılır, 1 elde kalır.

İkili toplamanın bütün durumları dört satırlık bir tabloya sığar: 0 + 0 = 0, 0 + 1 = 1, 1 + 0 = 1, 1 + 1 = 10. Elde de hesaba katılınca her sütunda üç bit toplanır ve sonuç 0 ile 3 arasında kalır. Elektronikte bu işi tam toplayıcı devresi yapar: üç bit girer, bir toplam biti ve bir elde biti çıkar. Sekiz tam toplayıcı zincirlenince 8 bitlik bir toplayıcı elde edilir.

Örnek 1.1.3

57 ile 28, 8 bitlik ikili sayılar olarak toplanıyor. 57 = 001110012 ve 28 = 000111002'dir. Toplama en sağdaki sütundan başlar (Tablo 1.1.6).

Tablo 1.1.6: 57 + 28 toplamının sütun sütun hesabı (sütun 1 en sağdadır).
Sütun57'nin biti28'in bitiGelen eldeToplamYazılanYeni elde
1100110
2000000
3010110
41102 = 10201
51113 = 11211
61012 = 10201
7001110
8000000
elde572885000100111001111110011000101+
Şekil 1.1.6: 57 + 28 toplamının son hâli. Üst satırdaki kırmızı 1'ler eldelerdir; dördüncü sütunda doğan elde üç sütun boyunca ilerler. Sonuç 010101012 = 85'tir.

Sonuç 010101012 = 64 + 16 + 4 + 1 = 85'tir ve 57 + 28 = 85 ile uyuşur. Sekizinci sütundan dışarıya elde çıkmadığı için sonuç 8 bite sığmıştır. Toplam 255'i aşsaydı sekizinci sütundan bir elde taşacak ve 8 bitlik sonuç yanlış olacaktı. Bu durum taşmanın ilk örneğidir ve negatif sayılarla birlikte ayrıntılı incelenir.

1.1.3Negatif sayılar ve taşma

1.1.3.1İkiye tümleyen

Şimdiye kadar yalnız sıfır ve pozitif tam sayılar yazıldı. Negatif sayılar için ilk akla gelen yol, en soldaki biti işaret için ayırmaktır: 0 pozitif, 1 negatif. Bu gösterime işaret-büyüklük denir ve kâğıt üzerinde doğal görünür. Ama iki kusuru vardır. Sıfırın iki ayrı gösterimi olur (+0 ve −0). Daha önemlisi, toplama devresi işaretlere göre ayrı ayrı davranmak zorunda kalır. Bilgisayarlar bu yüzden başka bir yöntem kullanır.

Tanım 1.1.3

İkiye tümleyen (two's complement) gösteriminde bir pozitif sayının negatifi şöyle bulunur: sayının bütün bitleri ters çevrilir (0 ↔ 1), sonra sonuca 1 eklenir. Bitleri yalnız ters çevirmekle elde edilen ara sonuca birler tümleyeni denir.

Bu gösterimde en soldaki bit bir işaret biti gibi davranır: 0 ise sayı pozitif, 1 ise negatiftir. 8 bitle temsil edilebilen aralık −128 … 127'dir. Genel olarak n bitle aralık −2n−1 … 2n−1 − 1'dir. Negatif tarafta bir sayı fazla vardır, çünkü sıfır pozitif tarafta yer tutar.

Örnek 1.1.4

8 bitlik bir bilgisayarda −3'ü bulalım. 3 sayısı 00000011'dir.

  1. Bütün bitler ters çevrilir: 00000011 → 11111100 (birler tümleyeni).
  2. 1 eklenir: 11111100 + 1 = 11111101.

Sonuç 11111101, −3'ün 8 bitlik ikiye tümleyen gösterimidir.

+300000011ters çevir11111100+1 → −311111101
Şekil 1.1.7: −3'ün bulunuşu. Üst satır 3, orta satır bitleri ters çevrilmiş hâli, alt satır 1 eklendikten sonraki −3'tür. Alttaki sayılar basamak ağırlıklarıdır; en soldaki bitin ağırlığı −128'dir.

Ters yönde de aynı işlem uygulanır. Bir negatif sayının mutlak değerini bulmak için yine bitler ters çevrilir ve 1 eklenir: 11111101 → 00000010 → 00000011 = 3. İşlem kendi tersidir; iki kez uygulanınca başa dönülür.

Örnek 1.1.5

−20'nin 8 bitlik gösterimi şöyle bulunur. 20 = 16 + 4 = 000101002. Bitler ters çevrilir: 11101011. 1 eklenir: 11101100. Sağlama için bu bit dizisi işaretsiz okunursa 128 + 64 + 32 + 8 + 4 = 236 çıkar ve 236 = 256 − 20'dir. Bu eşitlik rastlantı değildir; bir sonraki kesim nedenini açıklar.

1.1.3.2İkiye tümleyen neden işe yarar?

İkiye tümleyenin asıl üstünlüğü, toplama devresinin hiç değişmeden negatif sayılarla da çalışmasıdır. 3 + (−3) toplamına bakalım:

00000011 + 11111101 = 1 00000000

Sonuç dokuz bittir. Bilgisayar yalnız sekiz bit tuttuğu için en soldaki dokuzuncu bit atılır ve geriye tam olarak 00000000 = 0 kalır. Toplama devresi sayının işaretini hiç bilmez; bitleri her zamanki gibi toplar ve doğru sonucu verir. Çıkarma da ayrı bir devre istemez: a − b, a + (−b) olarak hesaplanır. Ters çevirme kapıları ve bir toplayıcı, hem toplamayı hem çıkarmayı yapar.

Aynı gösterime ikinci bir bakış daha vardır ve işaretli bir sayıyı ondalığa çevirmenin en kısa yoludur. 8 bitlik ikiye tümleyende en soldaki bitin ağırlığı +128 değil, −128'dir. Öteki bitlerin ağırlıkları değişmez. Bu yüzden en soldaki bit 1 ise sayı mutlaka negatiftir: geri kalan yedi bit en fazla 127 ekleyebilir ve −128'i aşamaz.

+300000011ters çevir11111100+1 → −311111101−1286432168421−128 + 64 + 32 + 16 + 8 + 4 + 1 = −3
Şekil 1.1.8: −3'ün ağırlıklarla okunuşu. En soldaki bitin ağırlığı −128 alınınca −128 + 64 + 32 + 16 + 8 + 4 + 1 = −3 bulunur.
Örnek 1.1.6

10110110 bit dizisi işaretsiz ve işaretli olarak kaçtır?

İşaretsiz okumada bütün ağırlıklar pozitiftir: 128 + 32 + 16 + 4 + 2 = 182.

İşaretli okumada en soldaki bitin ağırlığı −128'dir: −128 + 32 + 16 + 4 + 2 = −74. Aynı sonuca 182 − 256 = −74 yoluyla da varılır. En soldaki bit 1 olduğunda işaretli değer, işaretsiz değerden her zaman 28 = 256 eksiktir, çünkü o bitin katkısı +128 yerine −128 alınır ve fark 256'dır.

Bu gözlem genel bir kural verir. n bitlik bir ikiye tümleyen sayıda −x, işaretsiz olarak 2n − x diye saklanır. −20'nin 236, −3'ün 253 olarak görünmesi bundandır. Sayıların bir saat kadranı gibi dizildiğini düşünmek yardımcı olur: 0'dan saat yönünde 127'ye kadar pozitifler, 127'den sonra −128 ve oradan −1'e kadar negatifler gelir. −1'den bir adım sonra yeniden 0'a dönülür.

1.1.3.3Java'da ikiye tümleyen

Java'nın bütün tam sayı tipleri (byte, short, int, long) ikiye tümleyen kullanır. Bu, Java dil belirtiminde açıkça yazılıdır ve her platformda aynıdır. Aşağıdaki program aynı kuralı Java ile sınar.

int sayi = 3;
int eksi = ~sayi + 1; // ters çevir, 1 ekle
System.out.println(eksi);
byte b = (byte) 0b11111101;
System.out.println(b);
System.out.println(Integer.toBinaryString(-3));
System.out.println(Byte.MIN_VALUE + " .. "
    + Byte.MAX_VALUE);
-3
-3
11111111111111111111111111111101
-128 .. 127

~ işleci bir tam sayının bütün bitlerini ters çevirir; adı bit düzeyinde değil (bitwise NOT) işlecidir. ~sayi + 1 tam olarak ikiye tümleyen kuralıdır ve sonuç −3 çıkar. Java'da -x ile ~x + 1 her tam sayı için aynı değeri verir.

İkinci kısımda 0b11111101 sabiti bir byte'a dönüştürülüyor. Bu sabit Java için bir int'tir ve değeri 253'tür. 253, byte aralığına sığmadığı için açık bir tür dönüşümü (cast) gerekir: (byte). Dönüşüm yalnız en sağdaki 8 biti alır; bu bitler 11111101'dir ve işaretli okununca −3 olur.

Integer.toBinaryString(-3) otuz iki bitin hepsini yazar: başta otuz tane 1 ve sonda 01. Gösterim 32 bite genişleyince değişmez; sola eklenen bitler işaret bitinin kopyasıdır. Bu kurala işaret genişletme denir. Pozitif bir sayı sola sıfırlarla, negatif bir sayı sola birlerle genişletilir ve değer korunur. Son satır byte tipinin sınırlarını yazar: -128 .. 127.

Java'da byte'ın işaretli olması bazen şaşırtıcıdır. Ağdan ya da dosyadan okunan ham baytlar çoğu zaman 0–255 arasında düşünülür. Java'da ise 200 değerli bir bayt −56 olarak görünür. Baytı işaretsiz okumak için b & 0xFF ifadesi kullanılır: & işlemi işaret genişletmesiyle gelen birleri siler ve 200'ü geri verir. Aynı işi Byte.toUnsignedInt(b) metodu da yapar.

1.1.3.4Taşma

Tanım 1.1.4

Taşma (overflow), bir işlemin sonucunun tipin aralığına sığmaması durumudur. Java'da tam sayı taşması hata vermez: sonuç “saat gibi” başa sarar ve aralığın öteki ucundan devam eder.

byte b = 127;
b++;
System.out.println(b);
int m = Integer.MAX_VALUE;
System.out.println(m + 1);
System.out.println(Math.addExact(m, 1));
-128
-2147483648
Exception in thread "main" java.lang.ArithmeticException: integer overflow
    at java.base/java.lang.Math.addExact(Math.java:911)
    at Tasma.main(Tasma.java:7)

İlk üç satırda b 127'dir ve 1 artırılınca 128 olması gerekir. 127 = 01111111'e 1 eklenince 10000000 çıkar. Bu bit dizisi işaretli okununca −128'dir. Toplama devresi doğru çalıştı; yalnız sonuç, işaret bitine taşınan elde yüzünden negatif bölgeye geçti. Saat kadranında 127'den bir adım ileri gidince −128'e gelinir. Integer.MAX_VALUE + 1 için de aynı şey olur ve sonuç −231 = −2.147.483.648 çıkar.

Taşmanın tehlikesi sessiz olmasıdır. Program çökmez, uyarı vermez, yalnız yanlış bir sayıyla çalışmayı sürdürür. Bir banka hesabının bakiyesi ya da bir sayaç taşarsa hata çok sonra, başka bir yerde ortaya çıkar. Bu yüzden Java, denetimli işlemler için Math.addExact, Math.subtractExact ve Math.multiplyExact metotlarını sunar. Bu metotlar sonucun sığmadığını görünce ArithmeticException istisnası fırlatır ve program o noktada durur. Son satırdaki çağrı bu yüzden bir sayı yazmaz; istisna iletisi hatanın yerini ve nedenini bildirir. Çıktıdaki at satırları, istisnanın hangi metot çağrılarından geçerek fırlatıldığını gösterir. Burada kod Tasma adlı bir sınıfın main metoduna yerleştirilerek çalıştırıldı ve satır numaraları bu dosyaya göredir.

Taşmayı önlemenin bir yolu daha geniş bir tip kullanmaktır. İki int'in çarpımı long'a sığar, ama çarpımın kendisi long olarak yapılmalıdır. long x = 100000 * 100000; yazmak işe yaramaz: sağ taraf iki int çarpımıdır, önce 32 bitte hesaplanır ve taşar; ancak taşmış sonuç long'a atanır. Doğrusu, çarpanlardan birini long yapmaktır: 100000L * 100000. Sabitin sonundaki L eki onu long yapar ve bütün çarpım 64 bitte yürür.

Örnek 1.1.7

01101101 + 00110110 toplamı kaçtır ve sonuç 8 bitlik işaretli bir byte olarak nasıl okunur?

Ondalıkta 109 + 54 = 163'tür. Bitler toplanınca 10100011 çıkar: 128 + 32 + 2 + 1 = 163. Dokuzuncu bite elde taşmadı; işaretsiz yorumda sonuç doğrudur. İşaretli yorumda ise en soldaki bit 1'dir ve değer 163 − 256 = −93 olur. İki pozitif sayının toplamı negatif çıktı. İşaretli toplamada taşmanın belirtisi tam olarak budur: aynı işaretli iki sayı toplanıp işareti farklı bir sonuç elde edilir. Java'da (byte) (0b01101101 + 0b00110110) ifadesi −93 verir.

1.1.4Kesirli sayılar: kayan nokta

1.1.4.1Kesir ve üs

Sınırlı sayıda rakamla ancak sınırlı sayıda değer temsil edilebilir. 32 bitlik bir int yaklaşık ±2 milyara kadar sayar. Işık hızının (yaklaşık 3·108 m/s) karesini almaya bile yetmez. Bir elektronun yükünü (yaklaşık 1,6·10−19 coulomb) ise hiç yazamaz. Mühendislik hesaplarında hem çok büyük hem çok küçük sayılar gerekir.

Çözüm, bilimsel gösterimdeki fikirdir. Sayı iki parçaya ayrılır: anlamlı rakamları taşıyan kesir (mantissa, mantis) ve ondalık noktanın yerini belirleyen üs. Parçalar ayrı ayrı saklanır. Sekiz rakamlık bir alanda 31415901 yazılı olsun. İlk altı rakam (314159) kesri, son iki rakam (01) 10'un kuvvetini, yani 101'i belirtsin. Kesir 0,314159 diye okunursa sayı 0,314159 × 101 = 3,14159 olur.

31415901kesir (6 rakam)üs (2)0.314159 × 10¹ = 3.14159double (64 bit):111 bit üs52 bit kesir±
Şekil 1.1.9: Üstte sekiz rakamın altısı kesir, ikisi üs olarak kullanılıyor. Altta Java'nın double tipi: 1 işaret biti, 11 üs biti ve 52 kesir biti.

Bu gösterimin adı da buradan gelir: kayan nokta (floating point). Üs değiştikçe ondalık noktanın yeri kayar. Aynı kesir 103 ile çarpılırsa 0,314159 × 103 = 314,159 olur. Rakamlar aynıdır; yalnız noktanın yeri değişmiştir. Böylece sınırlı sayıda rakamla hem çok büyük hem çok küçük sayılar yazılır. Bedeli şudur: her sayı yalnız kesre sığan kadar anlamlı rakamla, yani yaklaşık olarak saklanır.

1.1.4.2IEEE 754 ve Java'nın float, double tipleri

Bilgisayarlar aynı fikri ikili tabanda uygular. Kuralları IEEE 754 standardı belirler ve bugün neredeyse bütün işlemciler bu standarda uyar. Java'da iki kayan noktalı tip vardır: float 32 bit, double 64 bittir.

Tablo 1.1.7: Java'nın kayan noktalı tiplerinin bit dağılımı.
TipİşaretÜsKesirAnlamlı ondalık rakamEn büyük değer
float1823yaklaşık 7≈ 3,4·1038
double11152yaklaşık 15–16≈ 1,8·10308

İkili kayan noktada sayı (−1)işaret × 1,kesir2 × 2üs biçimine getirilir. Sıfırdan farklı her ikili sayı “1,…” ile başlayacak şekilde kaydırılabildiği için baştaki 1 saklanmaz; kesir alanı yalnız virgülden sonraki bitleri tutar. Üs ise bir sabit eklenerek negatif olmayan bir sayı olarak yazılır; float'ta bu sabit 127, double'da 1023'tür.

Örnek 1.1.8

−6,5'in float gösterimi beş adımda bulunur.

  1. İşaret negatiftir: işaret biti 1.
  2. 6,5 = 4 + 2 + 0,5'tir. İkilide 110,12 yazılır (virgülden sonraki ilk basamağın ağırlığı 2−1 = 0,5'tir).
  3. Virgül iki basamak sola kaydırılır: 110,12 = 1,1012 × 22.
  4. Üs 2'dir; 127 eklenince 129 = 10000001 saklanır.
  5. Kesir, baştaki 1 atılarak 101 ve ardından 20 sıfırdır.

Sonuç 1 10000001 10100000000000000000000'dır. Java'da Integer.toBinaryString(Float.floatToIntBits(-6.5f)) tam olarak bu 32 biti yazar.

−6,5 gibi sayılar ikilide tam yazılır, çünkü 0,5 bir ikinin kuvvetidir. 0,1 ise öyle değildir. 1/3'ün ondalıkta 0,3333… diye sonsuza kadar sürmesi gibi, 0,1 de ikilide 0,000110011001100…2 diye sonsuza kadar tekrar eder. Kesir alanı sonlu olduğu için bir yerde kesilir ve yuvarlanır. Bellekteki 0,1 aslında 0,1'e çok yakın ama ondan farklı bir sayıdır.

1.1.4.3Yuvarlama hatası ve karşılaştırma

Yuvarlamanın en bilinen sonucu, Java'da 0.1 + 0.2 toplamının 0.30000000000000004 çıkmasıdır. 0,1 ve 0,2'nin saklanan değerlerindeki küçük hatalar toplanır ve sonuç, 0,3'ün saklanan değerine eşit olmaz. Bu yüzden 0.1 + 0.2 == 0.3 ifadesi false verir. Bu bir Java hatası değildir; aynı sonuç C, Python ve JavaScript'te de görülür, çünkü hepsi aynı IEEE 754 donanımını kullanır.

Kayan noktalı sayılar bu yüzden == ile karşılaştırılmaz. Bunun yerine iki sayının farkının yeterince küçük olup olmadığına bakılır:

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

Buradaki 10−9 sınırına tolerans denir ve problemin ölçeğine göre seçilir. Volt cinsinden ölçülen bir gerilim için 10−9 çok küçük bir farktır; ama 10−12 farad mertebesindeki bir sığa için çok büyüktür. Para gibi kuruşu kuruşuna doğru olması gereken değerlerde ise double hiç kullanılmaz. Ya tutarlar kuruş cinsinden long olarak saklanır ya da Java'nın ondalık tabanda hesap yapan BigDecimal sınıfı kullanılır.

Sık yapılan hata

Bir döngüyü kayan noktalı bir sayaçla eşitlik koşuluna bağlamak: for (double x = 0; x != 1.0; x += 0.1). On adımda x 1,0 olacak gibi görünür, ama birikmiş yuvarlama hataları yüzünden x 0,9999999999999999 olur ve 1,0'a hiç eşit olmaz; döngü sonsuza kadar döner. Sayaçlar tam sayı olmalı ya da koşul x < 1.0 gibi bir eşitsizlikle yazılmalıdır.

1.1.5Harfler ve dizgiler

1.1.5.1Karakter kodlaması ve ASCII

Bilgisayar yalnız sayılarla çalışır. Metin işleyebilmesi için her harfe bir sayı karşılık getirilmelidir. Hangi harfin hangi sayıyla gösterileceği keyfî bir seçimdir, ama herkesin aynı seçimi yapması gerekir. Yoksa bir bilgisayarda “A” diye yazılan sayı, ötekinde başka bir harf olarak okunur.

Tanım 1.1.5

Karakter kodlaması (character encoding), karakterlerin hangi sayılarla ve bellekte hangi bayt dizileriyle saklanacağını belirleyen kuraldır.

En eski ve en etkili kodlama ASCII'dir (American Standard Code for Information Interchange, Bilgi Değişimi İçin Amerikan Standart Kodu). ASCII 1963'te ABD standartlar kurumu ASA (bugünkü ANSI) yayımladı. Büyük ve küçük İngiliz harflerini, rakamları, noktalama işaretlerini ve bazı özel karakterleri sayılarla temsil eder.

ASCII 7 bitlik bir koddur ve 27 = 128 karakter tanımlar (0–127). 0–31 arası ve 127, ekranda görünmeyen kontrol karakterleridir. Bunlar ilk olarak yazıcıları ve teleks makinelerini yönetmek için tasarlandı: satır başına dön, bir satır aşağı in, zil çal. 32–126 arası ise yazdırılabilir karakterlerdir. Tablo 1.1.8 birkaç önemli kodu gösterir.

Tablo 1.1.8: Bazı ASCII kodları.
KarakterOndalıkİkili
'A'651000001
'a'971100001
'0'480110000
boşluk320100000
yeni satır \n100001010

Tablodaki düzen bilinçli olarak seçilmiştir. Harfler alfabetik sırayla ardışık kodlara dizilir: 'A' = 65, 'B' = 66, …, 'Z' = 90. Bu sayede harfleri karşılaştırmak, sayıları karşılaştırmaya dönüşür. Küçük harf, büyük harfin 32 fazlasıdır: 'a' = 97 = 65 + 32. İkilide bu fark yalnız tek bir bittir, 25 basamağı. 'A' = 1000001, 'a' = 1100001'dir. Büyük-küçük harf dönüşümü tek bir biti açıp kapatmaktan ibarettir. Rakamlar da 48'den başlayarak dizilir; bir rakam karakterinin sayısal değeri c - '0' ile bulunur: '7' - '0' = 55 − 48 = 7.

Java'da char bir sayı tipidir ve aritmetiğe katılabilir. (char) ('a' - 32) ifadesi 'A' verir. Bu yakınlık bazen şaşırtır: System.out.println('a' + 'b'); satırı ab değil, 195 yazar. İki char'ın toplamı bir int'tir: 97 + 98 = 195. Aynı satır "" + 'a' + 'b' diye yazılırsa önce boş dizgiye 'a' eklenir, sonuç dizgi olduğu için 'b' de eklenir ve ab çıkar. Java'da + işleci soldan sağa işler ve işlenenlerden biri dizgi olduğu anda birleştirmeye döner.

7 bit, bir baytın sekiz bitinden birini boşta bırakır. 8 bitlik genişletmeler bu biti kullanarak 128–255 arasına ek harfler koyar ve 256 sembole çıkar. Türkçe için ISO 8859-9 (Latin-5) ve Windows-1254 bu türden kodlamalardır; ğ, ş, ı ve İ harflerini bu üst yarıya yerleştirirler. Ama her dil kendi üst yarısını tanımlayınca sorun çıktı. Aynı 253 değeri bir tabloda ı, ötekinde ý harfiydi. Türkçe bir metin yanlış kodlamayla açılınca harfler bozuk görünüyordu. Tek bir 8 bitlik tablo dünyanın bütün dillerine yetmez; Çince ve Japoncada binlerce karakter vardır.

1.1.5.2ASCII tablosu

Yazdırılabilir ASCII karakterleri 16 sütunlu bir tabloya dizilince kodları kolayca okunur (Tablo 1.1.9). Bir karakterin kodu, satır başlığı ile sütun başlığının toplamıdır. Örneğin 'J' 64 satırında ve 10 sütunundadır: 64 + 10 = 74.

Tablo 1.1.9: Yazdırılabilir ASCII karakterleri (32–126). Kod = satır + sütun; SP boşluk karakteridir.
+0123456789101112131415
32SP!"#$%&'()*+,-./
480123456789:;<=>?
64@ABCDEFGHIJKLMNO
80PQRSTUVWXYZ[\]^_
96`abcdefghijklmno
112pqrstuvwxyz{|}~DEL

Tablonun son hücresi DEL (127) bir kontrol karakteridir. 0–31 arasındaki kontrol karakterlerinden üçü bugün de her gün kullanılır: 9 sekme (\t), 10 yeni satır (\n) ve 13 satır başı (\r). Windows metin dosyalarında satır sonu \r\n ikilisiyle, Linux ve macOS'ta yalnız \n ile işaretlenir. Bir dosyanın bir sistemde düzgün, ötekinde tek satır görünmesinin nedeni çoğu zaman bu farktır. 0–255 arasının tamamı ve karakter ya da kodla arama, ASCII Tablosu sayfasında yer alır.

1.1.5.3Unicode

Ülkeye özgü kod tablolarının yarattığı karmaşa, bütün diller için tek bir tablo fikrini doğurdu.

Tanım 1.1.6

Unicode, dünyadaki bütün yazı sistemlerinin harf, sembol ve işaretlerine birer numara veren standarttır. Bu numaraya kod noktası (code point) denir ve U+ ön ekiyle onaltılı yazılır: U+0041 = A, U+00E7 = ç.

Unicode'u 1991'den beri Unicode Konsorsiyumu geliştirir. Uluslararası karşılığı olan ISO/IEC 10646 standardı onunla eş zamanlı tutulur. İlk sürüm 16 bitlikti ve 65.536 karakterin yeteceği düşünülmüştü. Tarihî yazılar, matematik sembolleri ve Çin-Japon-Kore ideogramlarının tamamı eklenince bu alan yetmedi. Kod alanı U+10FFFF'e kadar genişletildi. Böylece 1.114.112 kod noktası oluştu; bugün bunların 150.000'den fazlasına karakter atanmıştır.

Unicode'un ilk 128 kod noktası ASCII ile birebir aynıdır: U+0041 hem ASCII'de hem Unicode'da 65'tir. İlk 256 kod noktası da Batı Avrupa dilleri için kullanılan Latin-1 (ISO 8859-1) tablosuyla örtüşür; ç bu yüzden hem Latin-1'de hem Unicode'da 231'dir. Türkçeye özgü ğ, ş ve ı ise Latin-1'de yoktur ve 256'nın üstündedir (Tablo 1.1.10).

Tablo 1.1.10: Bazı karakterlerin kod noktaları ve UTF-8 baytları (onaltılı).
KarakterKod noktasıOndalıkUTF-8 baytları
AU+00416541
çU+00E7231C3 A7
ğU+011F287C4 9F
€U+20AC8364E2 82 AC
😀U+1F600128512F0 9F 98 80

Kod noktası bir numaradır; bellekte nasıl saklanacağını söylemez. En basit yol, her karakteri sabit 32 bitle saklamaktır. Bu kodlamaya UTF-32 denir. Ama İngilizce bir metinde her harfin 7 bitlik bir ASCII kodu vardır ve 32 bit ayırmak yerin dörtte üçünü boşa harcar. Bu yüzden değişken uzunluklu UTF (Unicode Transformation Format, Unicode Dönüşüm Biçimi) kodlamaları tanımlanmıştır.

1.1.5.4UTF-8

UTF-8, bir kod noktasını 1 ile 4 bayt arasında değişen uzunlukta saklar. Sık kullanılan karakterler az, nadir olanlar çok bayt kullanır. ASCII karakterleri tek bayttır ve bu bayt ASCII koduyla aynıdır. Bu yüzden saf ASCII bir metin, hiçbir değişiklik yapılmadan geçerli bir UTF-8 metnidir; UTF-8 ASCII ile geriye uyumludur. Bugün web sayfalarının çok büyük bölümü UTF-8 ile kodlanır.

Bir UTF-8 okuyucu, bir karakterin kaç bayt süreceğini ilk baytın başındaki bitlerden anlar. Tablo 1.1.11 kalıpları verir. x ile gösterilen yerlere kod noktasının bitleri sırayla yerleştirilir.

Tablo 1.1.11: UTF-8 bayt kalıpları.
Kod noktası aralığıBayt kalıbıBayt sayısıTaşınan bit
U+0000–U+007F0xxxxxxx17
U+0080–U+07FF110xxxxx 10xxxxxx211
U+0800–U+FFFF1110xxxx 10xxxxxx 10xxxxxx316
U+10000–U+10FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx421

Kalıpların tasarımı akıllıcadır. 0 ile başlayan bir bayt tek başına bir ASCII karakteridir. 110, 1110 ya da 11110 ile başlayan bayt bir karakterin ilk baytıdır ve baştaki 1'lerin sayısı toplam bayt sayısını verir. 10 ile başlayan bayt ise her zaman bir devam baytıdır. Okuyucu bir metnin ortasından okumaya başlasa bile devam baytlarını atlayıp bir sonraki karakterin başını bulabilir.

Örnek 1.1.9

ç karakterinin UTF-8 kodlaması şöyle bulunur. Kod noktası U+00E7 = 231'dir ve U+0080–U+07FF aralığında olduğu için iki bayt kullanır. 231, 11 bitle 00011 100111 diye yazılır. İlk beş bit birinci baytın 110xxxxx kalıbına, son altı bit ikinci baytın 10xxxxxx kalıbına yerleşir:

110 00011 = C3     10 100111 = A7

Sonuç C3 A7'dir. Aynı yolla € (U+20AC = 0010 000010 101100) üç bayta yerleşir: 1110 0010 = E2, 10 000010 = 82, 10 101100 = AC.

U+00E7 = 231 = 00011 100111 (11 bit)1100001110100111C3A7renkli: kalıp bitleri · diğerleri: kod noktasının bitleri
Şekil 1.1.10: ç karakterinin UTF-8 kodlaması. Renkli kutular kalıbın sabit bitleri, diğerleri kod noktasının bitleridir; iki bayt onaltılı C3 ve A7'dir.

Java'da bir dizginin UTF-8 ile kaç bayt tuttuğu getBytes metoduyla görülür. Kodun derlenmesi için dosyanın başına import java.nio.charset.StandardCharsets; satırı eklenmelidir.

String kelime = "çikolata";
System.out.println(kelime.length());
System.out.println(kelime.getBytes(
    StandardCharsets.UTF_8).length);
8
9

"çikolata" sekiz karakterlidir ve length() 8 verir. UTF-8'de ç iki bayt, öteki yedi harf birer bayt tutar: 2 + 7 = 9. Bir dosyanın bayt cinsinden boyutu, içindeki karakter sayısıyla bu yüzden aynı değildir.

Kodlama yalnız uzunluğu değil, okunabilirliği de belirler. Bir metin hangi kodlamayla yazıldıysa o kodlamayla okunmalıdır. UTF-8 ile yazılmış “ç” iki bayttır (C3 A7). Bu iki bayt Windows-1254 tablosuyla okunursa her biri ayrı bir karakter sayılır ve ekranda “ç” görünür. Türkçe karakterlerin bozuk göründüğü dosyaların çoğunda sorun budur. Java'da dosya okurken ve yazarken kodlamayı her zaman açıkça belirtmek, örneğin StandardCharsets.UTF_8 vermek, bu hatayı önler.

1.1.5.5Java'da char, UTF-16 ve emoji

Java tasarlandığında Unicode henüz 16 bitlikti. Java'nın char tipi bu yüzden 16 bit seçildi ve her karakterin tek bir char'a sığacağı varsayıldı. Unicode genişleyince bu varsayım bozuldu. Bugün bir char, bir karakter değil, bir UTF-16 birimidir. UTF-16 kodlaması, U+FFFF'e kadar olan karakterleri tek bir 16 bitlik birimle yazar; ş = 01 5F, € = 20 AC. Daha büyük kod noktaları ise iki birimle yazılır. Bu ikiliye vekil çift (surrogate pair) denir.

Gündelik metinlerin çoğu bu ayrımı hissettirmez, çünkü Türkçe, Rusça, Arapça ve Çincedeki yaygın karakterlerin hepsi U+FFFF'in altındadır. Fark en çok emojilerde ortaya çıkar. Emojiler, duygu, ifade ve nesneleri hızlıca aktaran simgelerdir ve bilgisayar onları resim olarak değil, metin olarak işler. Standartlarını Unicode Konsorsiyumu belirler ve her emoji benzersiz bir kod noktasıdır: 😀 = U+1F600. Kod her cihazda aynıdır; böylece gönderilen emoji karşı tarafta aynı anlamla okunur. Çizimi, yani glifi ise yazı tipine ve üreticiye göre farklı görünebilir.

Bazı emojiler birden çok kod noktasının birleşimidir. Aile emojisi 👨‍👩‍👧, beş kod noktasından oluşur: adam, ZWJ, kadın, ZWJ, kız. ZWJ (zero width joiner, sıfır genişlikli birleştirici) U+200D'dir ve ekranda görünmez; yalnız komşu emojilerin tek bir glif olarak çizilmesini ister. Bu birleştiriciyi tanımayan bir sistem, üç ayrı yüz gösterir.

String yuz = "😀";
System.out.println(yuz.length());
System.out.println(
    yuz.codePointCount(0, yuz.length()));
String aile = "👨‍👩‍👧";
System.out.println(aile.length());
2
1
8

😀 dizgisinin length() değeri 2'dir, çünkü U+1F600 iki char'lık bir vekil çifttir: \uD83D ve \uDE00. codePointCount ise kod noktalarını sayar ve 1 verir. Aile emojisinde üç insan emojisi ikişer char, iki ZWJ birer char tutar: 3·2 + 2·1 = 8. Ekranda tek bir simge görünse de length() 8, kod noktası sayısı 5'tir. UTF-8'de bu emoji 3·4 + 2·3 = 18 bayt tutar.

Bu kesimin Java için sonucu şudur: String karakterleri UTF-16 ile saklar ve length() “karakter sayısı” değil, char sayısıdır. Türkçe metinlerde ikisi neredeyse her zaman aynıdır. Emojili, matematik sembollü ya da tarihî yazılı metinlerde ise farklıdır. Böyle metinlerde karakterleri tek tek dolaşmak için charAt yerine codePoints() ya da codePointAt kullanılır. Java 9'dan bu yana String, yalnız Latin-1 karakteri içeren dizgileri içeride karakter başına bir baytla saklar. Bu bir bellek iyileştirmesidir; length() ve charAt() her durumda UTF-16 birimleriyle çalışmaya devam eder.

Sık yapılan hata

Bir dizginin length() değerini ekranda görünen simge sayısı ya da bayt sayısı sanmak. "😀".length() 1 değil 2'dir; "ağaç".length() 4 iken aynı dizgi UTF-8'de 6 bayt tutar. Bir veritabanı alanına ya da ağ paketine sığıp sığmadığı denetlenecekse kodlanmış bayt sayısına, kullanıcıya gösterilecek uzunluk hesaplanacaksa kod noktalarına bakılmalıdır.

1.1.5.6Kod izleme: bir dizginin karakter kodları

Bir dizgi bellekte iki parçayla durur: karakterlerin sayı olarak dizildiği bir dizi ve bu dizinin uzunluğu. Uzunluk ayrıca tutulur; metnin nerede bittiği böyle bilinir. Aşağıdaki program "çikolata" dizgisinin her karakterinin kodunu yazdırır.

String kelime = "çikolata";
for (int i = 0; i < kelime.length(); i++) {
  char ch = kelime.charAt(i);
  System.out.print((int) ch + " ");
}
System.out.println();
System.out.println("Uzunluk: " + kelime.length());

Döngü i = 0'dan başlar ve i < kelime.length() doğru olduğu sürece döner. charAt(i), i konumundaki char'ı verir. (int) ch tür dönüşümü, karakterin kendisini değil, sayısal kodunu yazdırır; dönüşüm olmasa ekrana harfin kendisi basılırdı. print, println'den farklı olarak satırı bitirmez; kodlar aynı satıra yan yana dizilir. Döngüden sonraki println() satırı bitirir. Tablo 1.1.12 her turu gösterir.

Tablo 1.1.12: "çikolata" için döngünün izlemesi.
iKoşul i < 8ch(int) chAçıklama
0doğru'ç'231U+00E7; ASCII'de yok
1doğru'i'105
2doğru'k'107
3doğru'o'111
4doğru'l'108
5doğru'a'97
6doğru't'116
7doğru'a'975. konumdaki 'a' ile aynı kod
8yanlışDöngü biter; satır sonu ve Uzunluk: 8 yazılır.
231 105 107 111 108 97 116 97 
Uzunluk: 8
8uzunluk0ç2311i1052k1073o1114l1085a976t1167a97
Şekil 1.1.11: "çikolata" dizgisi bellekte: solda uzunluk (8), sağda indisleri 0–7 olan karakterler ve altlarında kodları.

ç dışındaki bütün kodlar ASCII tablosundaki değerlerdir. 231 ise ç'nin Unicode kod noktasıdır (U+00E7). Aynı değer Latin-1 ve Türkçe ISO 8859-9 tablolarında da 231'dir. Bu yüzden program hangi tabloyla düşünülürse düşünülsün aynı sayıyı yazar. (int) ch ifadesi UTF-8 baytlarını değil, kod noktasını verir. ç'nin UTF-8 karşılığı C3 A7 baytlarıdır, ama Java belleğinde ç tek bir 16 bitlik char olarak durur ve değeri 231'dir.

1.1.6Yapılandırılmış bilgi

1.1.6.1Resim, ses ve film

Sayılar harfleri, harfler metinleri temsil ettiği gibi, sayı dizileri de çok daha karmaşık bilgiyi temsil eder. Bir dosyada ne saklanırsa saklansın, bilgisayar için o dosya bir bayt dizisidir. Dizinin anlamı, onu yazan ve okuyan programların uyduğu kurallardan, yani dosya biçiminden gelir. Bilgisayarla etkileşimimizin çoğu, bu kurallar sayesinde sayı dizilerini resim, ses ya da film olarak görmemize dayanır.

0,0,00,0,2550,0,0255,255,2550,0,255255,255,2550,0,00,0,2550,0,0Resim: piksel = (R, G, B)Ses: basınç örnekleriFilm: saniyede 24–30 karekare 1kare 2kare 3kare 4
Şekil 1.1.12: Yapılandırılmış bilginin üç örneği. Solda her pikseli (R, G, B) üçlüsü olan bir resim, ortada zaman içinde düzenli aralıklarla ölçülen ses basıncı, sağda saniyede 24–30 kare hızında ardışık resimlerden oluşan film.
  • Resim. Bir resim, satırlar ve sütunlar hâlinde dizilmiş küçük noktalardan, yani piksellerden oluşur. Her piksel kırmızı, yeşil ve mavi ışığın miktarını veren üç sayıdır ve her sayı 0–255 arasındadır. Bu üçlüye RGB (red, green, blue) denir. Mavi bir piksel (0, 0, 255), beyaz bir piksel (255, 255, 255), siyah bir piksel (0, 0, 0)'dır. Her renk bileşeni bir bayta sığar ve bir piksel üç bayt tutar. Web'de kullanılan #FF0000 renk kodu, bu üç baytın onaltılı yazılışıdır: FF kırmızı, 00 yeşil, 00 mavi.
  • Ses. Ses, havadaki basıncın zamanla değişmesidir. Mikrofon bu değişimi bir gerilime çevirir. Analog-sayısal dönüştürücü (ADC) gerilimi düzenli aralıklarla ölçer ve her ölçümü bir sayı olarak kaydeder. Bu işleme örnekleme denir. CD kalitesinde saniyede 44.100 örnek alınır ve her örnek 16 bitlik bir sayıdır. Örnekleme hızının seçimi rastgele değildir. İnsan kulağı yaklaşık 20 kHz'e kadar duyar. Bir sinyali kaybetmeden örneklemek için de en yüksek frekansının iki katından hızlı ölçmek gerekir.
  • Film. Bir film, saniyede 24 ya da 30 kare hızında gösterilen ardışık resimlerdir. Göz bu hızda geçen resimleri kesintisiz bir hareket olarak algılar. Her kare, resim gibi bir sayı dizisidir; film ise bu dizilerin dizisidir ve yanına bir ses kanalı eklenir.

1.1.6.2Bir video ne kadar yer tutar?

Sayıların boyutunu hesaplamak, bu temsilin bedelini somutlaştırır. Aşağıdaki program, sıkıştırılmamış bir dakikalık Full HD videonun ve bir dakikalık CD sesinin kaç bayt tuttuğunu hesaplar.

long piksel = 1920L * 1080;  // Full HD bir kare
long kare = piksel * 3;      // R, G, B: 3 bayt
long saniye = kare * 30;     // saniyede 30 kare
System.out.println("Piksel: " + piksel);
System.out.println("Kare (bayt): " + kare);
System.out.println("1 saniye (bayt): " + saniye);
System.out.println("1 dakika (MB): "
    + saniye * 60 / 1_000_000);
// 44,1 kHz, 16 bit (2 bayt), stereo, 1 dakika
long ses = 44_100L * 2 * 2 * 60;
System.out.println("1 dk CD sesi (bayt): " + ses);
Piksel: 2073600
Kare (bayt): 6220800
1 saniye (bayt): 186624000
1 dakika (MB): 11197
1 dk CD sesi (bayt): 10584000

Hesap adım adım şöyledir. Full HD bir kare 1920 × 1080 = 2.073.600 pikseldir. Her piksel üç bayt tuttuğu için bir kare 2.073.600 × 3 = 6.220.800 bayttır, yani yaklaşık 6,2 MB. Saniyede 30 kare 6.220.800 × 30 = 186.624.000 bayt eder. Bir dakika bunun 60 katıdır: 11.197.440.000 bayt. Program bunu 1_000_000'a bölerek megabayta çevirir. Tam sayı bölmesi kesri attığı için çıktı 11197 MB'tır; yani sıkıştırmasız bir dakikalık Full HD video yaklaşık 11 GB tutar. Ses tarafında 44.100 örnek, 2 bayt ve 2 kanal (stereo) çarpılınca saniyede 176.400 bayt, bir dakikada 10.584.000 bayt, yani yaklaşık 10,6 MB bulunur.

Gerçek hayatta video ve ses sıkıştırılarak saklanır. H.264 gibi video kodlayıcıları ardışık kareler arasındaki benzerliği, MP3 gibi ses kodlayıcıları kulağın duyamayacağı ayrıntıları kullanarak dosyayı onlarca kat küçültür. Ama sıkıştırılmış dosya da yine bir sayı dizisidir; değişen yalnız sayıların anlamını belirleyen kurallardır.

Kodun Java açısından en önemli ayrıntısı ilk satırdaki L ekidir. 1920L * 1080 yazımı çarpımı long tipinde yapar. L eki olmasaydı bütün çarpımlar int olarak yapılırdı. Bir saniyelik değer (186.624.000) int'e sığar, ama bir dakikalık değer (11.197.440.000) Integer.MAX_VALUE'yu beş kattan fazla aşar. 1920 * 1080 * 3 * 30 * 60 ifadesi bu yüzden sessizce taşar ve −1.687.461.888 gibi anlamsız bir negatif sayı verir. Ses hesabındaki 44_100L da aynı nedenle L ekiyle yazılmıştır; orada sonuç int'e sığardı, ama ek yazmak alışkanlık olarak güvenlidir.

1.1.7Saklanan program

1.1.7.1Saklanan program kavramı

Buraya kadar sayıların veriyi nasıl temsil ettiği görüldü. Son adım, işlemcinin ne yapacağını söyleyen komutların da sayı olmasıdır. Bu fikir modern bilgisayarların temel ilkesidir.

Tanım 1.1.7

Saklanan program kavramı (stored-program concept): komutlar da veriler gibi sayıdır ve verilerle aynı bellekte saklanır. Bellekte saklanan komutlar ve veriler birlikte programı oluşturur. Bu ilkeyle çalışan makineye depolanan programlı bilgisayar denir.

Her komut belirli bir görevi ya da işlemi temsil eder: bellekten oku, topla, belleğe yaz. Komutlar normalde ardışık olarak, bellekteki sıralarıyla işlenir. Ama bir koşul sağlandığında program farklı bir komuta atlayabilir. Programların duruma göre farklı yollar izlemesini sağlayan budur. Java'daki if deyimi ve döngüler, işlemci düzeyinde tam olarak bu tür atlama komutlarına dönüşür.

200LOAD201ADD202STORE203HALT204102205252060komutlarveriler
Şekil 1.1.13: Komutlar ve veriler aynı bellekte, yan yana adreslerde durur. 200–203 adreslerinde LOAD, ADD, STORE ve HALT komutları, 204–206 adreslerinde veriler vardır.

Fikrin önemi, ondan önceki makinelerle karşılaştırınca anlaşılır. 1945'te tamamlanan ENIAC, saniyede binlerce toplama yapabilen ilk büyük elektronik bilgisayarlardan biriydi. Ama programı bellekte değil, kablolarda ve anahtarlarda dururdu. Yeni bir hesap için operatörler panellerdeki kabloları elle yeniden takardı ve bu iş günler sürebilirdi.

Aynı yıl ENIAC ekibinin üzerinde çalıştığı EDVAC tasarımı farklı bir yol önerdi: komutlar ve veriler bellekte sırayla saklanacaktı. Program değiştirmek, belleğe yeni sayılar yazmak demekti. Bu yaklaşım esnek ve genişletilebilirdi; bir program başka bir programı bile veri olarak okuyup üretebilirdi. Derleyiciler de tam olarak bunu yapar: kaynak kodu okur, makine kodunu üretir. Fikri, John von Neumann'ın 1945 tarihli “First Draft of a Report on the EDVAC” raporu yaydı. Saklanan programı ilk çalıştıran makine ise 1948'de Manchester Üniversitesinde çalışan ve “Baby” diye anılan küçük deneysel bilgisayar oldu.

1.1.7.2Bellek hiyerarşisi

Bellek, çalışan programların ve verilerin geçici olarak saklandığı yerdir. İdeal bir bellek hem çok büyük, hem çok hızlı, hem de ucuz olurdu. Ne yazık ki bu üç özellik birbiriyle çelişir. Hızlı bellek teknolojileri pahalıdır ve yonga üzerinde çok yer kaplar. Ucuz ve büyük bellekler ise yavaştır. Bilgisayarlar bu çelişkiyi, farklı hız ve boyuttaki bellekleri katmanlar hâlinde dizerek çözer.

Tanım 1.1.8

Bellek hiyerarşisi (memory hierarchy), belleklerin işlemciye yakın, hızlı ve küçük düzeylerden uzak, yavaş ve büyük düzeylere doğru dizilmesidir. Sıra şöyledir: yazmaçlar, L1, L2 ve L3 önbellekler, ana bellek (RAM) ve disk.

Yazmaçlarbayt · < 1 nsL1 önbellek32–64 KB · ~1 nsL2 önbellek256 KB–2 MB · ~4 nsL3 önbellek8–64 MB · ~15 nsAna bellek (RAM)8–64 GB · ~80 nsDisk (SSD / HDD)TB · µs–msdahahızlı
Şekil 1.1.14: Bellek hiyerarşisi. Yukarı çıktıkça bellek küçülür ve hızlanır. Boyut ve erişim süreleri yaklaşık değerlerdir ve işlemciye göre değişir; 1 ns saniyenin milyarda biridir.
  • L1 önbellek (cache): en hızlı önbellektir ve işlemcinin içindedir. Boyutu çekirdek başına birkaç on kilobayttır ve yaklaşık 1 ns'de erişilir.
  • L2 önbellek: L1'i destekler ve ondan büyüktür. Çoğu işlemcide bir de bütün çekirdeklerin paylaştığı, birkaç on megabaytlık L3 önbellek vardır.
  • Ana bellek (RAM): gigabaytlarca büyüktür, ama erişimi önbelleklerden onlarca kat yavaştır.
  • Disk: terabaytlarca büyük ve kalıcıdır; erişim süresi mikrosaniyeler ile milisaniyeler arasındadır.

Hiyerarşinin işe yaramasının nedeni, programların belleğe rastgele değil, belirli bir düzenle erişmesidir. Bir program kısa süre önce kullandığı veriyi yakında yeniden kullanma eğilimindedir; döngü değişkenleri buna örnektir. Bir adrese erişen program, çoğu zaman hemen yanındaki adreslere de erişir; bir diziyi baştan sona dolaşmak buna örnektir. Bu iki eğilime yerellik denir. Önbellek, son kullanılan verileri ve komşularını işlemcinin yanında tutar. Böylece erişimlerin büyük çoğunluğu yavaş RAM'e gitmeden hızlı önbellekte karşılanır. İşlemciye yakın bellek daha hızlı ama daha küçük ve pahalıdır; bu hiyerarşinin nasıl kullanıldığı programın başarımını doğrudan etkiler. Aynı sayıda toplama yapan iki Java programından biri diziyi sırayla, öteki rastgele atlayarak dolaşırsa aralarında birkaç kat hız farkı görülebilir.

1.1.7.3İşlemci ve veri yolu

Merkezi işlem birimi (CPU), bilgisayarın bütün hesaplarının yapıldığı yerdir. İçinde üç temel bölüm vardır.

  • Aritmetik mantık birimi (ALU, Arithmetic Logic Unit): toplama, çıkarma, karşılaştırma ve bit düzeyinde mantık işlemleri gibi hesapları yapar. İkili toplamada anlatılan tam toplayıcılar ALU'nun içindedir.
  • Kontrol birimi (control unit): komutları yorumlar, işlemleri sıraya koyar ve yönlendirir. Hangi yazmacın ALU'ya bağlanacağına, belleğe okuma mı yazma mı sinyali gideceğine o karar verir.
  • Yazmaçlar (register): geçici veriler için çok küçük ama çok hızlı saklama alanlarıdır. ALU'nun işlediği sayılar yazmaçlardan gelir ve sonuçlar yazmaçlara yazılır.

İşlemciyi bellek ve giriş/çıkış aygıtlarına bağlayan yapıya veri yolu (bus) denir. Veri yolu, birçok paralel telden oluşan bir iletişim sistemidir. Teller görevlerine göre üç gruba ayrılır.

  • Adres sinyalleri (adres yolu): hangi bellek hücresine ya da aygıta erişileceğini seçer.
  • Veri sinyalleri (veri yolu): işlenecek ya da saklanacak asıl veriyi taşır.
  • Kontrol sinyalleri (kontrol yolu): işlemi yönlendirir; örneğin işlemin “oku” mu “yaz” mı olduğunu bildirir.
CPUALUKontrolYazmaçhesap · yönetim · hızlı saklamaadres yoluveri yolukontrol yoluBellekG/Ç aygıtları
Şekil 1.1.15: İşlemci (ALU, kontrol birimi, yazmaçlar) ile bellek ve G/Ç aygıtları arasındaki adres, veri ve kontrol yolları.

Adres yolundaki tel sayısı, adreslenebilecek bellek miktarını belirler. Her tel bir bit taşır; n telli bir adres yolu 2n farklı adres seçebilir. 32 hatlı bir adres yolu 232 = 4.294.967.296 farklı adres seçer. Her adres bir baytı gösteriyorsa bu 4 GiB bellek eder. 32 bitlik sistemlerin en fazla yaklaşık 4 GB RAM kullanabilmesinin nedeni budur. 64 bitlik işlemcilere geçişin başlıca gerekçelerinden biri de bu sınırdır. Teorem 1.1.1'in açısından bakılınca bir adres yolunun her teli tek bir bitlik basamak gibidir ve n basamakla 2n farklı sayı yazılır.

1.1.7.4Yazmaçlar

İşlemci, bellek ve veri yolunun birlikte nasıl çalıştığını Şekil 1.1.16 özetler. Belleğin her hücresinin bir adresi ve bir içeriği vardır. İşlemcinin içindeki yazmaçlar ise belleğin işlemciye en yakın, en küçük ve en hızlı katmanıdır.

Ana Bellek (RAM)0i = 71j = 912k = j + 1003l = i * kMerkezi İşlem Birimi (CPU)Kontrol BirimiYazmaçlarPC0IR—R1—R2—AC—ALU+ − × ÷sonuçAdres yoluVeri yolukontrol: oku / yaz
Şekil 1.1.16: Üstte ana bellek (0–3 adreslerinde dört hücre), altta işlemci: kontrol birimi, PC, IR, R1, R2 ve AC yazmaçları ve ALU. Solda adres yolu, sağda çift yönlü veri yolu, ortada kontrol sinyali vardır.
  • İşlemci, adres yolundan hangi hücreyi istediğini söyler.
  • Hücrenin içeriği veri yolundan işlemciye gelir (okuma) ya da işlemciden hücreye gider (yazma).
  • PC (program counter, program sayacı): sıradaki komutun adresini tutar.
  • IR (instruction register, komut yazmacı): o an çözülen komutu tutar.
  • R1, R2 ve AC (accumulator, akümülatör): ara sonuçları tutar.
  • ALU, yazmaçlardaki sayılarla hesap yapar.

Yazmaçlar elektronikte flip-floplardan kurulur. Bir flip-flop bir biti saklar; 32 flip-flop yan yana dizilince 32 bitlik bir yazmaç elde edilir. Yazmaçlar saat sinyalinin her darbesinde yeni değer alabilir, bu yüzden bir saat çevriminde okunup yazılabilirler. Sayıları azdır: tipik bir işlemcide birkaç düzine genel amaçlı yazmaç bulunur. Derleyicinin önemli işlerinden biri, sık kullanılan değişkenleri bellekte değil yazmaçlarda tutmaktır.

1.1.7.5Bellek, adresler ve program sayacı

Bellek, veri ve talimatların saklandığı alandır. İşlemci açısından bir program, bellekte art arda dizilmiş uzun bir talimat listesidir. Her bellek hücresinin bir adresi vardır ve hücrede bir talimatın ya da bir verinin sayıyla kodlanmış hâli bulunur. Talimatlar bellekten tek tek alınır, işlemcideki yazmaçlara yüklenir ve sırayla çalıştırılır.

Tanım 1.1.9

Program sayacı (PC, program counter), sıradaki talimatın adresini tutan yazmaçtır. İşlemci bir talimatı aldıktan sonra PC'yi bir sonraki adrese ilerletir.

İşlemci normalde adres sırasıyla ilerler: 200, 201, 202 … Ama bazı talimatlar bu sırayı değiştirir. Tablo 1.1.13'teki küçük bellek parçasında 204 adresinde bir JUMP 7 talimatı vardır. Bu örnekte 7, talimatın kendi adresinden ne kadar ileri atlanacağını söyler: 204 + 7 = 211. Talimat PC'ye 211 yazar ve işlemci 205–210 arasını atlayıp 211'deki talimatla devam eder. Atlama hedefi bazı makinelerde böyle göreli bir uzaklıkla, bazılarında da doğrudan mutlak bir adresle (JUMP 211 gibi) verilir; iki durumda da sonuç PC'nin değişmesidir.

Tablo 1.1.13: Bellekte talimatlar ve veriler yan yana.
AdresİçerikTür
200ADD to R1talimat
201102veri
202…
203…
204JUMP 7talimat (atla)
……
211sonraki talimattalimat

Tablonun “Tür” sütunu yalnız okura yardım etmek içindir; bellekte böyle bir sütun yoktur. Bir bellek hücresinin talimat mı veri mi olduğu, hücreye bakarak anlaşılmaz: ikisi de sayıdır. Fark, işlemcinin o hücreyi nasıl kullandığıdır. PC bir hücreyi gösterdiğinde içerik talimat olarak çözülür; bir talimat o hücreyi okuduğunda içerik veri olarak kullanılır. PC yanlışlıkla bir veri hücresini gösterirse işlemci o sayıyı talimat sanır ve anlamsız işler yapar. Bu yüzden atlama hedeflerinin doğru hesaplanması çok önemlidir.

1.1.7.6Getir–Çöz–Yürüt döngüsü

Tanım 1.1.10

Getir–Çöz–Yürüt döngüsü (fetch–decode–execute cycle), işlemcinin her komut için tekrarladığı temel çalışma döngüsüdür. Programların işlemcide nasıl yürütüldüğünü tanımlar.

  1. Getir (fetch): işlemci, PC'nin gösterdiği adresteki komutu bellekten alır ve IR'ye yazar. PC bir sonraki adrese ilerler.
  2. Çöz (decode): komut çözülür. Hangi işlem yapılacak, hangi yazmaçlar kullanılacak, hangi adrese erişilecek? Her komutun belirli bir işlevi vardır ve kontrol birimi bu işlevi komutun bitlerinden okur.
  3. Yürüt (execute): komutun gerektirdiği işlem yapılır. Bu bir veri işleme, bir sonuç hesaplama, bellekten okuma ya da belleğe yazma olabilir.
GetirFetchYürütExecuteÇözDecodesüreklitekrar
Şekil 1.1.17: Getir, Çöz ve Yürüt adımlarından oluşan döngü. Bilgisayar açık olduğu sürece sürekli tekrarlanır.

Ardından bir sonraki komut alınır ve döngü baştan başlar. Bu döngü bilgisayar açık olduğu sürece hiç durmadan tekrarlanır. Bir Java programının bütün çalışması, milyarlarca kez dönen bu döngüden ibarettir.

Döngüyü bir saat sinyali yönetir. 3 GHz'lik bir işlemcinin saati saniyede 3 milyar kez “tık” eder; bir saat çevrimi yaklaşık 0,33 ns sürer. Basit bir işlemcide her komut birkaç saat çevrimi alır. Modern işlemciler ise birden çok komutu üst üste bindirerek işler. Bir komut yürütülürken bir sonraki çözülür, ondan sonraki getirilir. Bu düzene boru hattı (pipeline) denir ve bir fabrikadaki montaj hattına benzer. Hat dolu olduğunda her saat çevriminde bir komut tamamlanır.

1.1.7.7Kod izleme: oyuncak bilgisayarda 102 + 25

Döngünün işleyişi, oyuncak bir bilgisayarda kolayca izlenir. Bu bilgisayarda belleğin 200–203 adreslerinde dört komut, 205–207 adreslerinde veri vardır. Program, 205'teki sayı ile 206'daki sayıyı toplar ve sonucu 207'ye yazar. Satırlardaki noktalı virgülden sonraki kısımlar açıklamadır ve [x] gösterimi “x adresindeki bellek hücresi” demektir.

200: LOAD  R1, [205] ; R1 ← bellek[205]
201: ADD   R1, [206] ; R1 ← R1 + bellek[206]
202: STORE R1, [207] ; bellek[207] ← R1
203: HALT            ; dur
204: ...
205: 102             ; veri
206: 25              ; veri
207: 0               ; sonuç buraya

Komutların anlamı şöyledir. LOAD (yükle) bellekteki bir değeri yazmaca kopyalar. ADD (topla) bellekteki bir değeri yazmaçtaki değere ekler. STORE (sakla) yazmaçtaki değeri belleğe yazar. HALT (dur) işlemciyi durdurur. Başlangıçta PC = 200'dür. Tablo 1.1.14 her adımı gösterir.

Tablo 1.1.14: Oyuncak bilgisayarda Getir–Çöz–Yürüt adımları.
AdımEvreNe olur?PCIRR1[207]
0BaşlangıçProgram ve veriler bellekte.200——0
1Getir200'deki komut IR'ye alınır, PC ilerler.201LOAD—0
2Çözİşlem LOAD, hedef R1, adres 205.201LOAD—0
3Yürüt[205] = 102 okunur, R1'e yazılır.201LOAD1020
4Getir201'deki komut alınır.202ADD1020
5Çözİşlem ADD; R1 ile [206] toplanacak.202ADD1020
6YürütALU 102 + 25 = 127 hesaplar, sonuç R1'e.202ADD1270
7Getir202'deki STORE alınır.203STORE1270
8ÇözR1 207 adresine yazılacak (kontrol yolu: “yaz”).203STORE1270
9Yürüt[207] ← 127. Sonuç artık bellekte.203STORE127127
10Getir203'teki HALT alınır.204HALT127127
11Çöz + YürütHALT: işlemci durur.204HALT127127
GetirFetchYürütExecuteÇözDecodesüreklitekrar
Şekil 1.1.18: 6. adımda döngünün durumu: ADD komutunun yürütme evresi etkin. ALU 102 + 25 = 127'yi hesaplar.

İzlemeden çıkan üç gözlem önemlidir. Birincisi, PC her Getir adımında hemen ilerler; komut yürütülürken PC zaten bir sonraki komutu gösterir. Program bittiğinde PC 204'tür, çünkü HALT getirildiği anda PC bir kez daha ilerlemiştir. İkincisi, yürütme evresinde veri yolu iki yönde de kullanılır. LOAD ve ADD bellekten okur, STORE belleğe yazar; hangisinin yapılacağını kontrol yolundaki “oku/yaz” sinyali belirler. Üçüncüsü, 204 adresindeki ... hiç çalışmaz. HALT işlemciyi durdurduğu için PC'nin gösterdiği hücre bir daha getirilmez.

Java'daki toplam = a + b; satırı da sonunda bu tür komutlara dönüşür. Derleyici a'yı ve b'yi bellekten ya da yazmaçlardan alan, toplayan ve sonucu toplam'ın yerine yazan birkaç makine komutu üretir. Gerçek işlemcilerin komut kümeleri yüzlerce komut içerir ama her biri aynı Getir–Çöz–Yürüt döngüsüyle çalışır.

Örnek 1.1.10

Aynı oyuncak bilgisayarda 200 adresinden başlayan şu program çalışsın: 200: LOAD R1, [206]; 201: JUMP 205; 202–204: başka komutlar; 205: HALT; 206: 40. Program bitince R1 ve PC kaçtır?

Getir 200: PC 201 olur; yürütmede [206] = 40 R1'e yazılır. Getir 201: PC önce 202 olur; yürütmede JUMP, PC'ye 205 yazar. Bu atlama mutlak adreslidir. Getir 205: PC 206 olur; HALT işlemciyi durdurur. Sonuçta R1 = 40 ve PC = 206'dır. 202–204 arasındaki komutlar hiç getirilmemiştir.

1.1.8von Neumann mimarisi

1.1.8.1Mimari ve darboğaz

Tanım 1.1.11

von Neumann mimarisi (von Neumann architecture), saklanan program fikrinin donanım planıdır: işlemci, bellek, giriş/çıkış birimleri ve bunları bağlayan veri yolu. Komutlar ve veriler aynı bellekte durur ve aynı yoldan taşınır.

İşlemci (CPU)Kontrol BirimiALUYazmaçlarPC · IR · R0–R3Girişklavye, sensörÇıkışekran, motorBellekkomutlar + veriler (tek bellek)veri yolu (darboğaz)
Şekil 1.1.19: von Neumann mimarisi: giriş, işlemci (kontrol birimi, ALU, yazmaçlar), çıkış ve komutlarla verilerin birlikte durduğu bellek. İşlemci ile bellek arasında tek bir yol vardır.

Mimaride her komut bir işlem kodu (opcode) ile temsil edilir. İşlem kodu, komutun bitleri arasında hangi işlemin yapılacağını belirten kısımdır: toplama mı, yükleme mi, atlama mı? Komutun geri kalan bitleri işlenenleri, yani hangi yazmaçların ya da hangi adresin kullanılacağını söyler.

Bu tasarımın bir bedeli vardır. İşlemci ile bellek arasındaki tek yol, hem komutları hem verileri taşımak zorundadır. İşlemci bellekten çok daha hızlı çalıştığı için çoğu zaman bu yoldan gelecek bir komutu ya da veriyi bekler. Bu sınırlamaya von Neumann darboğazı denir. Önbellekler bu yüzden vardır: sık kullanılan komut ve verileri işlemcinin yanında tutarak yavaş yola gitme sayısını azaltırlar.

Karşılaştırma için Harvard mimarisinde komut belleği ile veri belleği ayrıdır ve her birinin kendi yolu vardır. İşlemci aynı anda hem bir komut getirebilir hem bir veri okuyabilir. Arduino kartlarındaki AVR gibi pek çok mikrodenetleyici bu yapıyı kullanır: program flaş bellekte, veriler SRAM'de durur. Modern masaüstü işlemcileri ise bir karma kullanır. Ana bellek tektir (von Neumann), ama işlemcinin içindeki L1 önbellek komutlar ve veriler için ikiye ayrılmıştır (Harvard).

1.1.8.2Komutların kodlanması

Komutların sayı olması ne demektir? Yine oyuncak bir bilgisayar üzerinde düşünelim: 8 bitlik, 4 yazmaçlı (R0–R3), 256 adet 8 bitlik bellek hücresi olan ve yalnız 4 komut bilen bir makine. Komutlar toplama (00), çıkarma (01), çarpma (10) ve bölmedir (11). Her komut 8 bitlik bir sayıdır ve bitler dört alana bölünür.

  • İlk 2 bit işlem kodudur. 4 komutu ayırt etmek için 2 bit yeter, çünkü 22 = 4.
  • Sonraki 2 bit hedef yazmaçtır (destination register): sonucun yazılacağı yazmaç. 4 yazmaç için 2 bit gerekir.
  • Son 4 bit, iki işlenen yazmaçtır; her biri 2 bittir.

“R0 ile R2'nin içeriğini topla, sonucu R3'e yaz” komutu alanlarıyla 0 3 0 2 diye yazılır: işlem kodu 0 (toplama), hedef 3, işlenenler 0 ve 2. Her alan 2 bitle ikiliye çevrilip yan yana konunca 00 11 00 10 = 00110010 elde edilir.

işlem kodu000 → toplahedef11R3kaynak 100R0kaynak 210R2
Şekil 1.1.20: 8 bitlik komut 00110010'un alanları: işlem kodu 00 (toplama), hedef 11 (R3), birinci işlenen 00 (R0), ikinci işlenen 10 (R2).

İşlemcinin “çöz” adımı, bu bit alanlarını sayıdan geri çıkarmaktır. Aynı işlem Java'da basamak değeriyle bölme ve kalan alma ile yapılabilir.

int komut = 0b00110010;
int islemKodu = komut / 64;     // ilk 2 bit
int hedef = (komut / 16) % 4;   // sonraki 2 bit
int kaynak1 = (komut / 4) % 4;  // sonraki 2 bit
int kaynak2 = komut % 4;        // son 2 bit
System.out.println(komut);
System.out.println(islemKodu + " " + hedef
    + " " + kaynak1 + " " + kaynak2);
50
0 3 0 2

Kodun mantığı ondalıktaki bir benzetmeyle kolayca anlaşılır. 4728 sayısının yüzler basamağını bulmak için önce 100'e bölünür (47), sonra 10'a göre kalan alınır (7). İkilide de aynıdır. 2k'ye bölmek sağdaki k biti atar; 4'e göre kalan almak ise son 2 biti verir, çünkü 4 = 22. Her alan bu iki işlemle sırası gelince ayıklanır.

1.1.8.3Kod izleme: komut çözme

Tablo 1.1.15, kodun her satırını ikili gösterimle birlikte izler.

Tablo 1.1.15: komut = 0b00110010 için alanların çözülmesi.
SatırAra değerSonuçAçıklama
komut = 0b00110010—5032 + 16 + 2 = 50
islemKodu = komut / 64—064 = 26; sağdaki 6 bit atılır, geriye 00 kalır: toplama.
hedef = (komut / 16) % 450 / 16 = 3 (0011)3Sağdaki 4 bit atılır; % 4 son 2 biti alır: 11 = R3.
kaynak1 = (komut / 4) % 450 / 4 = 12 (001100)0Sağdaki 2 bit atılır; 12 % 4 = 0: R0.
kaynak2 = komut % 4—2Son 2 bit doğrudan alınır: 10 = R2.
işlem koduhedefkaynak 1kaynak 200110010komutR3 ← R0 + R2
Şekil 1.1.21: Çözülen alanlar: 0 3 0 2. 50 sayısı işlemci için “R3 ← R0 + R2” komutudur.

Program önce 50, sonra 0 3 0 2 yazar. 50 sayısı, bir int olarak bakılınca sıradan bir tam sayıdır; bu oyuncak işlemci için ise “R3 ← R0 + R2” komutudur. Sayının anlamı, onu kimin nasıl kullandığına bağlıdır.

Gerçek Java kodunda bit alanları çoğu zaman bölme yerine bit düzeyinde işleçlerle ayıklanır. komut >> 4 sayıyı 4 bit sağa kaydırır ve pozitif sayılarda komut / 16 ile aynı sonucu verir. & 3 ise son iki biti maskeleyerek alır, yani % 4'ün işini görür. Hedef alanı böylece (komut >> 4) & 3 diye yazılır. Bu yazılış, donanımın gerçekte yaptığı işe daha yakındır: kontrol birimi bölme yapmaz, yalnız ilgili telleri seçer.

Örnek 1.1.11

Oyuncak bilgisayarda 10011100 komutu ne yapar?

Bitler 2'şerli alanlara ayrılır: 10 01 11 00. İşlem kodu 10 çarpmadır. Hedef 01, yani R1'dir. İşlenenler 11 (R3) ve 00 (R0)'dır. Komut R1 ← R3 × R0 anlamına gelir. Java koduyla sağlamak için komutun sayı değeri 128 + 16 + 8 + 4 = 156 alınır: 156 / 64 = 2; 156 / 16 = 9 ve 9 % 4 = 1; 156 / 4 = 39 ve 39 % 4 = 3; 156 % 4 = 0. Alanlar 2 1 3 0'dır.

1.1.8.4Assembly dili

Komutları 00110010 gibi bit dizileriyle yazmak insan için çok zordur. Assembly dili (assembly language), her makine komutuna ADD, MOV gibi akılda kalan kısaltmalar verir. Assembly ile yazılmış metni makine koduna çevirici (assembler) adlı bir program çevirir.

MOV AL, 61hçevirici (assembler)B0 61işlemci çalıştırırAL = 97
Şekil 1.1.22: MOV AL, 61h komutu çevirici ile B0 61 makine koduna çevrilir; işlemci çalıştırınca AL yazmacına 97 yazılır.
  • Assembly dili belirli bir işlemci mimarisine özeldir. x86 için yazılan kod ARM işlemcide çalışmaz, çünkü iki mimarinin komutları ve kodlamaları farklıdır.
  • Her satır genellikle tek bir makine komutuna karşılık gelir. Java'nın tek bir satırı ise çoğu zaman birçok makine komutuna dönüşür.
  • Bilgisayarın alt düzeyde nasıl çalıştığını anlamak için önemlidir.
  • Başarım iyileştirmesinde ve donanımla doğrudan etkileşimde kullanılır: aygıt sürücüleri, gömülü sistemler, işletim sistemi çekirdeğinin bazı parçaları.
Tablo 1.1.16: Bazı x86 komutları ve makine kodları (onaltılı).
x86 komutuAnlamıMakine kodu
MOV AL, 61hAL yazmacına 61h (= 97) değerini taşıB0 61
MOV AX, BXBX'teki değeri AX'e kopyala66 89 D8
ADD EAX, 10EAX'e 10 ekle83 C0 0A
XOR EAX, EAXEAX'i sıfırla (x XOR x = 0)31 C0

Tablo 1.1.16'daki kodlar, oyuncak bilgisayardaki fikrin gerçek bir işlemcideki karşılığıdır. B0 61 kodunda B0, “AL yazmacına bir sabit yükle” işlem kodudur; 61 ise yüklenecek sabittir. ADD EAX, 10 komutunda son bayt 0A = 10'dur. x86 komutları oyuncak bilgisayardaki gibi sabit 8 bit değildir; 1 ile 15 bayt arasında değişir. MOV AX, BX kodunun başındaki 66 baytı, komutun 32 değil 16 bitlik yazmaçlarla çalışacağını bildiren bir ön ektir. XOR EAX, EAX bir yazmacı sıfırlamanın yaygın bir yoludur. Bir sayının kendisiyle XOR'u her bitte 0 verir. Üstelik bu komut, MOV EAX, 0'dan daha kısa kodlanır.

İlk satırdaki 61h, onaltılı 61 demektir (assembly'de h eki, Java'da 0x ön eki). 61h = 0x61 = 97'dir ve 97 aynı zamanda ASCII 'a''dır. Bellekteki bir bayt sayı mı, harf mi, komut mu? Bunu baytın kendisi söylemez; programın onu nasıl kullandığı belirler. Bu cümle, notun bütün katmanlarının ortak sonucudur.

Java'nın bu katmanlar içindeki yeri de buradan anlaşılır. Programcı yüksek seviyeli bir dilde, Java'da yazar. javac bu kodu bytecode'a çevirir. Bytecode, gerçek bir işlemcinin değil, JVM'nin komut kümesidir ve her işlemcide aynıdır. JVM çalışırken bytecode'u o makinenin assembly düzeyindeki makine komutlarına çevirir. Java'nın “bir kez yaz, her yerde çalıştır” ilkesi, bu ara katman sayesinde mümkündür.

1.1.9Sık yapılan hatalar

Bu notta anlatılan konuların Java'daki en yaygın sonuçları, derleyicinin reddettiği satırlar ya da derlenip yanlış çalışan programlar olarak ortaya çıkar. Aşağıdaki listede her hata, derleyici iletisiyle ya da programın gerçek çıktısıyla birlikte verilmiştir. İletiler JDK 21 derleyicisine aittir.

Tablo 1.1.17: Sık yapılan hatalar ve nedenleri.
HataKodSonuçNeden ve doğrusu
Aralık dışı sabitbyte b = 200;error: incompatible types: possible lossy conversion from int to bytebyte en fazla 127'dir. 200 bir int sabitidir ve sığmaz; gerçekten isteniyorsa (byte) 200 yazılır ve değer −56 olur.
L ekini unutmaklong x = 3000000000;error: integer number too largeEksiz tam sayı sabiti int sayılır ve int 2.147.483.647'yi aşamaz. Doğrusu 3000000000L'dir.
Karakter ile dizgiyi karıştırmakchar c = "A";error: incompatible types: String cannot be converted to charÇift tırnak bir String üretir. Karakter tek tırnakla yazılır: 'A'.
Başta sıfırint x = 010;
int y = 08;
x = 8
error: ';' expected
Başta 0 sekizli demektir; 010 = 8'dir. Sekizli tabanda 8 rakamı olmadığı için ikinci satır derlenmez.
Taşmayı fark etmemekInteger.MAX_VALUE + 1-2147483648Tam sayı taşması hata vermez, başa sarar. Büyük çarpımlarda long gerekir ve çarpım long olarak başlamalıdır: 1920L * 1080. Denetim için Math.addExact.
Kesirlerde ==0.1 + 0.2 == 0.3falseKayan nokta değerleri yaklaşıktır. Farkın küçüklüğüne bakılır: Math.abs(a - b) < 1e-9.
length()'i karakter sayısı sanmak"😀".length()2length() UTF-16 birimi sayar. Kod noktası için codePointCount, bayt için getBytes(...).length.
char toplamını dizgi sanmakSystem.out.println('a' + 'b');195İki char'ın toplamı int'tir. Birleştirmek için "" + 'a' + 'b' ya da String.valueOf.

Listedeki ilk dört satırın hatalarını, 010 dışında, derleyici yakalar ve program hiç çalışmaz. Bunlar can sıkıcıdır ama zararsızdır, çünkü hata satırı ve nedeni hemen görülür. 010 ile son dört satırın hataları ise derlenir ve sessizce yanlış sonuç üretir. Bu yüzden daha tehlikelidir. Taşan bir sayaç, kayan noktalı bir eşitlik ya da yanlış hesaplanan bir dizgi uzunluğu ancak program başka bir yerde beklenmedik davranınca fark edilir. Bu hataların ortak kaynağı, bir değerin bellekte nasıl temsil edildiğini unutmaktır.

1.1.10Özet

  • Parçalar. CPU hesap yapar ve ALU, kontrol birimi ve yazmaçlardan oluşur. RAM geçici, disk kalıcı bellektir. Veri yolu adres, veri ve kontrol sinyallerini taşır; n hatlı adres yolu 2n adres seçer.
  • İkili sayılar. Bit 0 ya da 1'dir; 8 bit 1 bayttır. d basamak ve b tabanla 0 … bd − 1 yazılır. Ondalıktan ikiliye çevirmek için sayı 2'ye bölünür ve kalanlar aşağıdan yukarı okunur. Onaltılı bir basamak 4, sekizli bir basamak 3 bittir.
  • Java'da tabanlar. 0b ikili, 0 sekizli, 0x onaltılı sabittir. Integer.toBinaryString, toHexString ve parseInt(s, taban) çeviri yapar.
  • Negatif sayılar. İkiye tümleyen: bitler ters çevrilir ve 1 eklenir. En soldaki bitin ağırlığı negatiftir. 8 bitle aralık −128 … 127'dir. Aralık aşılırsa Java'da sessiz taşma olur ve değer başa sarar.
  • Kesirli sayılar. Kesir × tabanüs biçiminde saklanır. IEEE 754'e göre float 32, double 64 bittir. Değerler yaklaşıktır: 0.1 + 0.2 != 0.3.
  • Metin. ASCII 7 bit ve 128 karakterdir. Unicode her karaktere bir kod noktası verir (U+0000–U+10FFFF). UTF-8 bir kod noktasını 1–4 baytla yazar ve ASCII ile uyumludur. Java'nın char'ı 16 bitlik bir UTF-16 birimidir; length() char sayısını verir.
  • Yapılandırılmış bilgi. Resim RGB piksel dizisi, ses basınç örnekleri, film kare dizisidir. Sıkıştırmasız bir dakikalık Full HD video yaklaşık 11 GB tutar.
  • Saklanan program. Komutlar da bellekte sayı olarak durur. PC sıradaki adresi tutar. İşlemci Getir–Çöz–Yürüt döngüsünü sürekli tekrarlar; atlama komutları sırayı değiştirir.
  • von Neumann mimarisi. Komut ve veri aynı bellekte ve aynı yolda durur; tek yol darboğaz yaratır, önbellek bunu hafifletir. Harvard mimarisinde komut ve veri belleği ayrıdır.
  • Seviyeler. Yüksek seviyeli dil → assembly → makine kodu (bitler). Assembly işlemciye özeldir ve her satırı genellikle tek bir komuttur. Bir baytın sayı mı, harf mi, komut mu olduğunu onu kullanan program belirler.

1.1.11Kavram listesi

1.1.12Kendini Dene

  1. 100 sayısı 8 bitlik ikili ve onaltılı olarak nasıl yazılır?

    Cevap

    100'e sığan kuvvetler 64 ve 32'dir; kalan 4. 100 = 64 + 32 + 4, yani 01100100. Dörderli gruplar 0110 0100 = 6 ve 4 olduğu için onaltılı değer 0x64'tür.

  2. 11010110 bit dizisi işaretsiz olarak ve 8 bitlik ikiye tümleyenle kaçtır?

    Cevap

    İşaretsiz: 128 + 64 + 16 + 4 + 2 = 214. İşaretli: en soldaki bitin ağırlığı −128 alınır, −128 + 64 + 16 + 4 + 2 = −42; ya da 214 − 256 = −42.

  3. 8 bitlik ikiye tümleyende −1 ve −128 nasıl yazılır? byte m = -128; için (byte) (-m) neden yine −128 verir?

    Cevap

    −1 = 11111111 (1 = 00000001, ters 11111110, +1). −128 = 10000000. 128 sayısı byte aralığında yoktur. −128'in bitleri ters çevrilince 01111111, 1 eklenince yine 10000000 çıkar. Java'da -m önce int olarak 128 hesaplar, ama byte'a dönüşüm yalnız son 8 biti alır ve sonuç −128 olur.

  4. Bu satırlar ne yazdırır? System.out.println((byte) 130);, System.out.println((char) ('A' + 2));, System.out.println('A' + 2);

    Cevap

    −126, C ve 67. 130 = 10000010 işaretli okununca 130 − 256 = −126'dır. 'A' + 2 bir int'tir (67); (char) dönüşümüyle 67 numaralı karakter C yazılır, dönüşüm olmadan sayının kendisi yazılır.

  5. "Işık" dizgisi için length() ve UTF-8 bayt sayısı nedir?

    Cevap

    length() 4'tür; dört karakterin hepsi U+FFFF'in altındadır. UTF-8'de I (U+0049) 1 bayt, ş (U+015F) 2 bayt, ı (U+0131) 2 bayt, k 1 bayttır: 1 + 2 + 2 + 1 = 6.

  6. System.out.println(0x2A + 012 + 0b11); ne yazdırır?

    Cevap

    55. 0x2A = 2·16 + 10 = 42, 012 sekizli olduğu için 1·8 + 2 = 10, 0b11 = 3. Toplam 42 + 10 + 3 = 55'tir; sonuç ondalık yazılır.

  7. Oyuncak bilgisayarda (2 bit işlem kodu: 00 topla, 01 çıkar, 10 çarp, 11 böl; 2 bit hedef; 2'şer bit iki işlenen) “R1 ← R2 − R3” komutu hangi 8 bitlik sayıdır? Notun Java koduyla çözülünce hangi alanlar çıkar?

    Cevap

    01 01 10 11 = 01011011 = 91. Çözme: 91 / 64 = 1; 91 / 16 = 5, 5 % 4 = 1; 91 / 4 = 22, 22 % 4 = 2; 91 % 4 = 3. Alanlar 1 1 2 3'tür.

  8. Bir işlemcinin adres yolu 20 hatlıdır ve her adres bir bayt seçer. En fazla ne kadar bellek adreslenebilir?

    Cevap

    220 = 1.048.576 bayt, yani 1 MiB. Intel 8086 işlemcisinin adres yolu tam olarak bu kadardı.

  9. Telefon kalitesinde ses saniyede 8000 örnekle, örnek başına 1 bayt ve tek kanalla (mono) kaydediliyor. Sıkıştırmasız bir dakikalık konuşma kaç bayttır? Hesap int ile yapılırsa taşma olur mu?

    Cevap

    8000 · 1 · 1 · 60 = 480.000 bayt. Bu değer Integer.MAX_VALUE'dan çok küçüktür, taşma olmaz. CD sesi için aynı hesap 10.584.000 bayt verir; o da int'e sığar. Taşma, video gibi milyarları aşan çarpımlarda başlar.

  10. Java'da 0.1f + 0.2f == 0.3f ifadesi true verir. Bu, float ile eşitlik karşılaştırmasının güvenli olduğunu mu gösterir?

    Cevap

    Hayır. float'ın daha kaba yuvarlaması bu üç değerde hataları rastlantı eseri aynı sayıya götürmüştür. Aynı toplam double ile false verir ve başka sayılarda float da yanılır. Kayan noktalı değerler her zaman bir toleransla karşılaştırılmalıdır.

1.1.13İleri okuma

  • C. Petzold, Code: The Hidden Language of Computer Hardware and Software (2. baskı, Microsoft Press, 2022): röle ve mantık kapılarından başlayıp toplayıcıyı, belleği ve basit bir işlemciyi adım adım kurar; bu notun katmanlı anlatımının en akıcı uzun hâlidir.
  • R. E. Bryant ve D. R. O'Hallaron, Computer Systems: A Programmer's Perspective (3. baskı, Pearson, 2016), 2. bölüm: işaretsiz ve ikiye tümleyen tam sayıları, taşmayı, bit düzeyinde işlemleri ve IEEE 754 kayan noktayı programcı gözüyle ve çok sayıda alıştırmayla işler.
  • D. Goldberg, “What Every Computer Scientist Should Know About Floating-Point Arithmetic”, ACM Computing Surveys 23(1), 1991: yuvarlama hatasının nereden geldiğini ve IEEE 754 standardının tasarım gerekçelerini ayrıntılı anlatan klasik makale.
  • D. A. Patterson ve J. L. Hennessy, Computer Organization and Design (RISC-V sürümü, 2. baskı, 2020), 2. ve 5. bölümler: komutların kodlanmasını gerçek bir komut kümesi üzerinde ve bellek hiyerarşisini önbellek ayrıntılarıyla anlatır.

1.1.14Kaynaklar

  • D. A. Patterson ve J. L. Hennessy, Computer Organization and Design: The Hardware/Software Interface, RISC-V sürümü, 2. baskı, Morgan Kaufmann, 2020.
  • C. Petzold, Code: The Hidden Language of Computer Hardware and Software, 2. baskı, Microsoft Press, 2022.
  • A. B. Downey ve C. Mayfield, Think Java, 2. baskı, O'Reilly, 2019 (Bölüm 1).
  • Oracle, The Java Tutorials: “Primitive Data Types” ve “Characters”.
  • J. Gosling ve diğ., The Java Language Specification, Java SE 21, §3.10 (Sabitler) ve §4.2 (İlkel tipler).
  • The Unicode Consortium, The Unicode Standard, unicode.org; IEEE 754-2019, Standard for Floating-Point Arithmetic.
  • J. von Neumann, First Draft of a Report on the EDVAC, 1945.
  • R. E. Bryant ve D. R. O'Hallaron, Computer Systems: A Programmer's Perspective, 3. baskı, Pearson, 2016.
  • D. Goldberg, “What Every Computer Scientist Should Know About Floating-Point Arithmetic”, ACM Computing Surveys, 23(1), 5–48, 1991.