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ı
byteveintsınırlarını ve taşmayı ikiye tümleyenle açıklamak; Java'da tabanlarla çalışmak;charileStringuzunluğ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.
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.
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.
- Önce elektrik sinyalleri sayılara dönüştürülür.
- Sayılarla harfler, harflerle metinler temsil edilir.
- Sayı dizileriyle resim, ses ve film gibi yapılandırılmış bilgi saklanır.
- Komutlar da sayı olarak bellekte saklanır.
- 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.
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.
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.
| 0 | 1 |
|---|---|
Yanlış (false) | Doğru (true) |
| Kapalı | Açık |
| Düşük voltaj | Yü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
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).
| 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.
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.
| Bit | En büyük (2d − 1) |
|---|---|
| 4 | 15 |
| 8 | 255 |
| 16 | 65.535 |
| 32 | 4.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
18446744073709551615Her 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.
| Tur | Koşul | kalan | Yazılan satır | ikili | Yeni sayi |
|---|---|---|---|---|---|
| 1 | 13 > 0 doğru | 1 | 13 % 2 = 1 | "1" | 6 |
| 2 | 6 > 0 doğru | 0 | 6 % 2 = 0 | "01" | 3 |
| 3 | 3 > 0 doğru | 1 | 3 % 2 = 1 | "101" | 1 |
| 4 | 1 > 0 doğru | 1 | 1 % 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ç: 1101Dö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.
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.
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.
| Ön ek | Taban | Örnek | Not |
|---|---|---|---|
0b ya da 0B | İkili | 0b1101 = 13 | Java 7 ile geldi. |
0 | Sekizli | 0207 = 135 | 010 on değil, 8'dir. |
0x ya da 0X | Onaltılı | 0xFF = 255 | Renk ve bayt değerlerinde sık kullanılır. |
| (yok) | Ondalık | 135 | Başı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.
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.
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).
| Sütun | 57'nin biti | 28'in biti | Gelen elde | Toplam | Yazılan | Yeni elde |
|---|---|---|---|---|---|---|
| 1 | 1 | 0 | 0 | 1 | 1 | 0 |
| 2 | 0 | 0 | 0 | 0 | 0 | 0 |
| 3 | 0 | 1 | 0 | 1 | 1 | 0 |
| 4 | 1 | 1 | 0 | 2 = 102 | 0 | 1 |
| 5 | 1 | 1 | 1 | 3 = 112 | 1 | 1 |
| 6 | 1 | 0 | 1 | 2 = 102 | 0 | 1 |
| 7 | 0 | 0 | 1 | 1 | 1 | 0 |
| 8 | 0 | 0 | 0 | 0 | 0 | 0 |
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.
İ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.
8 bitlik bir bilgisayarda −3'ü bulalım. 3 sayısı 00000011'dir.
- Bütün bitler ters çevrilir:
00000011→11111100(birler tümleyeni). - 1 eklenir:
11111100+ 1 =11111101.
Sonuç 11111101, −3'ün 8 bitlik ikiye tümleyen gösterimidir.
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.
−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.
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
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.
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.
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.
| Tip | İşaret | Üs | Kesir | Anlamlı ondalık rakam | En büyük değer |
|---|---|---|---|---|---|
float | 1 | 8 | 23 | yaklaşık 7 | ≈ 3,4·1038 |
double | 1 | 11 | 52 | yaklaşı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.
−6,5'in float gösterimi beş adımda bulunur.
- İşaret negatiftir: işaret biti 1.
- 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).
- Virgül iki basamak sola kaydırılır: 110,12 = 1,1012 × 22.
- Üs 2'dir; 127 eklenince 129 =
10000001saklanır. - Kesir, baştaki 1 atılarak
101ve 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); // trueBuradaki 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.
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.
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.
| Karakter | Ondalık | İkili |
|---|---|---|
'A' | 65 | 1000001 |
'a' | 97 | 1100001 |
'0' | 48 | 0110000 |
| boşluk | 32 | 0100000 |
yeni satır \n | 10 | 0001010 |
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.
| + | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 32 | SP | ! | " | # | $ | % | & | ' | ( | ) | * | + | , | - | . | / |
| 48 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | : | ; | < | = | > | ? |
| 64 | @ | A | B | C | D | E | F | G | H | I | J | K | L | M | N | O |
| 80 | P | Q | R | S | T | U | V | W | X | Y | Z | [ | \ | ] | ^ | _ |
| 96 | ` | a | b | c | d | e | f | g | h | i | j | k | l | m | n | o |
| 112 | p | q | r | s | t | u | v | w | x | y | z | { | | | } | ~ | 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.
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).
| Karakter | Kod noktası | Ondalık | UTF-8 baytları |
|---|---|---|---|
| A | U+0041 | 65 | 41 |
| ç | U+00E7 | 231 | C3 A7 |
| ğ | U+011F | 287 | C4 9F |
| € | U+20AC | 8364 | E2 82 AC |
| 😀 | U+1F600 | 128512 | F0 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.
| Kod noktası aralığı | Bayt kalıbı | Bayt sayısı | Taşınan bit |
|---|---|---|---|
| U+0000–U+007F | 0xxxxxxx | 1 | 7 |
| U+0080–U+07FF | 110xxxxx 10xxxxxx | 2 | 11 |
| U+0800–U+FFFF | 1110xxxx 10xxxxxx 10xxxxxx | 3 | 16 |
| U+10000–U+10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 4 | 21 |
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.
ç 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.
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.
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.
i | Koşul i < 8 | ch | (int) ch | Açıklama |
|---|---|---|---|---|
| 0 | doğru | 'ç' | 231 | U+00E7; ASCII'de yok |
| 1 | doğru | 'i' | 105 | |
| 2 | doğru | 'k' | 107 | |
| 3 | doğru | 'o' | 111 | |
| 4 | doğru | 'l' | 108 | |
| 5 | doğru | 'a' | 97 | |
| 6 | doğru | 't' | 116 | |
| 7 | doğru | 'a' | 97 | 5. konumdaki 'a' ile aynı kod |
| 8 | yanlış | Döngü biter; satır sonu ve Uzunluk: 8 yazılır. | ||
231 105 107 111 108 97 116 97
Uzunluk: 8"ç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.
- 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
#FF0000renk 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): 10584000Hesap 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.
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.
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.
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.
- 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.
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.
- İş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.
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.
| Adres | İçerik | Tür |
|---|---|---|
| 200 | ADD to R1 | talimat |
| 201 | 102 | veri |
| 202 | … | |
| 203 | … | |
| 204 | JUMP 7 | talimat (atla) |
| … | … | |
| 211 | sonraki talimat | talimat |
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ü
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.
- Getir (fetch): işlemci, PC'nin gösterdiği adresteki komutu bellekten alır ve IR'ye yazar. PC bir sonraki adrese ilerler.
- Çö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.
- 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.
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ç burayaKomutları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.
| Adım | Evre | Ne olur? | PC | IR | R1 | [207] |
|---|---|---|---|---|---|---|
| 0 | Başlangıç | Program ve veriler bellekte. | 200 | — | — | 0 |
| 1 | Getir | 200'deki komut IR'ye alınır, PC ilerler. | 201 | LOAD | — | 0 |
| 2 | Çöz | İşlem LOAD, hedef R1, adres 205. | 201 | LOAD | — | 0 |
| 3 | Yürüt | [205] = 102 okunur, R1'e yazılır. | 201 | LOAD | 102 | 0 |
| 4 | Getir | 201'deki komut alınır. | 202 | ADD | 102 | 0 |
| 5 | Çöz | İşlem ADD; R1 ile [206] toplanacak. | 202 | ADD | 102 | 0 |
| 6 | Yürüt | ALU 102 + 25 = 127 hesaplar, sonuç R1'e. | 202 | ADD | 127 | 0 |
| 7 | Getir | 202'deki STORE alınır. | 203 | STORE | 127 | 0 |
| 8 | Çöz | R1 207 adresine yazılacak (kontrol yolu: “yaz”). | 203 | STORE | 127 | 0 |
| 9 | Yürüt | [207] ← 127. Sonuç artık bellekte. | 203 | STORE | 127 | 127 |
| 10 | Getir | 203'teki HALT alınır. | 204 | HALT | 127 | 127 |
| 11 | Çöz + Yürüt | HALT: işlemci durur. | 204 | HALT | 127 | 127 |
İ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.
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
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.
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.
İş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 2Kodun 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.
| Satır | Ara değer | Sonuç | Açıklama |
|---|---|---|---|
komut = 0b00110010 | — | 50 | 32 + 16 + 2 = 50 |
islemKodu = komut / 64 | — | 0 | 64 = 26; sağdaki 6 bit atılır, geriye 00 kalır: toplama. |
hedef = (komut / 16) % 4 | 50 / 16 = 3 (0011) | 3 | Sağdaki 4 bit atılır; % 4 son 2 biti alır: 11 = R3. |
kaynak1 = (komut / 4) % 4 | 50 / 4 = 12 (001100) | 0 | Sağdaki 2 bit atılır; 12 % 4 = 0: R0. |
kaynak2 = komut % 4 | — | 2 | Son 2 bit doğrudan alınır: 10 = R2. |
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.
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 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ı.
| x86 komutu | Anlamı | Makine kodu |
|---|---|---|
MOV AL, 61h | AL yazmacına 61h (= 97) değerini taşı | B0 61 |
MOV AX, BX | BX'teki değeri AX'e kopyala | 66 89 D8 |
ADD EAX, 10 | EAX'e 10 ekle | 83 C0 0A |
XOR EAX, EAX | EAX'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.
| Hata | Kod | Sonuç | Neden ve doğrusu |
|---|---|---|---|
| Aralık dışı sabit | byte b = 200; | error: incompatible types: possible lossy conversion from int to byte | byte 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 unutmak | long x = 3000000000; | error: integer number too large | Eksiz 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ırmak | char 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ır | int x = 010;int y = 08; | x = 8error: ';' expected | Başta 0 sekizli demektir; 010 = 8'dir. Sekizli tabanda 8 rakamı olmadığı için ikinci satır derlenmez. |
| Taşmayı fark etmemek | Integer.MAX_VALUE + 1 | -2147483648 | Tam 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.3 | false | Kayan 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() | 2 | length() UTF-16 birimi sayar. Kod noktası için codePointCount, bayt için getBytes(...).length. |
char toplamını dizgi sanmak | System.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.
0bikili,0sekizli,0xonaltılı sabittir.Integer.toBinaryString,toHexStringveparseInt(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
float32,double64 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()charsayı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
- işlemci CPU
- bellek RAM
- bit bit
- bayt byte
- ikilik sayı sistemi binary number system
- basamak değeri positional value
- onaltılı taban hexadecimal
- elde carry
- ikiye tümleyen two's complement
- taşma overflow
- kayan noktalı sayı floating-point number
- karakter kodlaması character encoding
- ASCII ASCII
- Unicode Unicode
- kod noktası code point
- UTF-8 UTF-8
- vekil çift surrogate pair
- depolanan programlı bilgisayar stored-program computer
- bellek hiyerarşisi memory hierarchy
- önbellek cache
- veri yolu bus
- yazmaç register
- program sayacı program counter
- getir–çöz–yürüt döngüsü fetch–decode–execute cycle
- von Neumann mimarisi von Neumann architecture
- işlem kodu opcode
- assembly dili assembly language
1.1.12Kendini Dene
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 gruplar0110 0100= 6 ve 4 olduğu için onaltılı değer0x64'tür.11010110bit 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.
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, ters11111110, +1). −128 =10000000. 128 sayısıbytearalığında yoktur. −128'in bitleri ters çevrilince01111111, 1 eklenince yine10000000çıkar. Java'da-mönceintolarak 128 hesaplar, amabyte'a dönüşüm yalnız son 8 biti alır ve sonuç −128 olur.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,
Cve 67. 130 =10000010işaretli okununca 130 − 256 = −126'dır.'A' + 2birint'tir (67);(char)dönüşümüyle 67 numaralı karakterCyazılır, dönüşüm olmadan sayının kendisi yazılır."Işık"dizgisi içinlength()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.System.out.println(0x2A + 012 + 0b11);ne yazdırır?Cevap
55.
0x2A= 2·16 + 10 = 42,012sekizli olduğu için 1·8 + 2 = 10,0b11= 3. Toplam 42 + 10 + 3 = 55'tir; sonuç ondalık yazılır.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.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ı.
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
intile 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 daint'e sığar. Taşma, video gibi milyarları aşan çarpımlarda başlar.Java'da
0.1f + 0.2f == 0.3fifadesitrueverir. Bu,floatile 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ı toplamdoubleilefalseverir ve başka sayılardafloatda 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.