Java ile Nesne Yönelimli Programlama · Ders Notu

Bölüm 2.1: Programlama

Bir algoritma kâğıt üzerinde ne kadar açık yazılmış olursa olsun, bilgisayar onu ancak bir programlama diliyle yazıldığında çalıştırabilir. Programlama dili, insanın düşündüğü çözüm ile makinenin yaptığı işlem arasındaki köprüdür. Bu not o köprünün nasıl kurulduğunu anlatır. İlk kesimler dilleri soyutlama düzeyine, uygulama alanına, yürütme biçimine ve paradigmaya göre sınıflandırır. Java'nın kaynak kodu nasıl bytecode'a derlediği ve sanal makinede nasıl çalıştırdığı da burada yer alır. Ardından her dilin üç katmanı ele alınır: söz dizimi, statik anlam ve anlam. Bu katmanlar derleme, çalışma zamanı ve mantık hatalarını ayırmanın da temelidir. Sonraki kesimler ifadelerin işlem önceliğiyle nasıl değerlendirildiğini, tam sayı bölmesini, statik ve dinamik tiplemeyi, tipler arası işlemleri ve dizgi birleştirmeyi işler. Son kesim bir algoritmanın Java programına dönüşmesini, ifade ile deyim ayrımını ve deyim sırasının sonucu nasıl belirlediğini gösterir.

Öğrenme çıktıları

Programlama dilleri
Programlama dilini tanımlamak; dilleri soyutlama düzeyi, uygulama alanı, yürütme biçimi ve paradigmaya göre sınıflandırmak.
Java'nın çalışması
Kaynak kodun javac ile bytecode'a derlenip JVM'de çalıştırılmasını açıklamak.
Söz dizimi ve anlam
Söz dizimi, statik anlam ve anlam kavramlarını ayırmak; derleme, çalışma zamanı ve mantık hatalarını tanımak.
İfadeler
İşlem önceliği, soldan sağa değerlendirme ve tam sayı bölmesiyle bir ifadenin değerini bulmak.
Değişkenler ve tipler
Değişken tanımlamak, atamayı iki adımda izlemek; statik ve dinamik tiplemeyi, tipler arası işlemleri açıklamak.
Algoritmadan programa
İfade ile deyimi ayırt etmek; bir algoritmayı Java programına çevirmek ve deyim sırasının önemini göstermek.

2.1.1Programlama dilleri

İnsanlar birbirleriyle doğal dillerle anlaşır. Türkçe, İngilizce ya da Almanca zengin dillerdir; aynı düşünce pek çok biçimde söylenebilir ve dinleyen kişi eksik kalan yerleri bağlamdan tamamlar. Bilgisayar böyle bir tamamlama yapmaz. Ona verilen her talimat tek bir biçimde okunmalı ve tek bir anlama gelmelidir. Programlama dilleri bu ihtiyaçtan doğmuştur. Bu kesim programlama dilini tanımlar ve dilleri dört ölçüte göre sınıflandırır: soyutlama düzeyi, uygulama alanı, yürütme biçimi ve paradigma. Paradigmalar ayrı bir kesimde işlenir.

2.1.1.1Programlama dili nedir?

Tanım 2.1.1

Programlama dili (programming language), bir makinenin yapabileceği hesaplamaları ifade etmek için tasarlanmış yapay bir dildir. Bu dille yazılan metne program, programın yazıldığı hâline de kaynak kod (source code) denir.

Tanımdaki “yapay” sözcüğü önemlidir. Doğal diller yüzyıllar içinde kendiliğinden gelişir; kuralları sonradan, gözlemle yazılır. Programlama dilleri ise baştan tasarlanır. Hangi sözcüklerin kullanılacağı, bu sözcüklerin hangi sırayla dizileceği ve her dizilişin ne anlama geldiği bir belgeyle kesin olarak belirlenir. Java için bu belge Java Dil Belirtimi'dir (The Java Language Specification).

Programlama dili iki amaçla kullanılır. Birincisi, makinenin davranışını denetleyen programlar yazmaktır. Bunun için algoritmaların açık ve kesin biçimde ifade edilmesi gerekir. İkincisi daha az fark edilir: program, programcılar arasında bir iletişim aracıdır. Yazılan kodu yıllar sonra başka biri okuyacak, değiştirecek ya da hatasını arayacaktır. İyi bir program yalnız doğru çalışmaz, aynı zamanda kolay okunur.

Bilgisayarlar, telefonlar ve gömülü cihazlar işlevlerini programlardan alır. Bir çamaşır makinesinin yıkama adımları, bir arabadaki fren denetim sisteminin kararları ya da bir telefonun ekranındaki her dokunuşa verilen yanıt, bir programlama diliyle yazılmış talimatlardır. Programlama dili bu açıdan algoritmaların ve mantıksal süreçlerin yazılı ifadesidir.

Probleminsan dilinde tariftasarlaAlgoritmakesin adımlar dizisikodlaProgramJava gibi bir dilde kodçalıştırMakinekomutları çalıştırır
Şekil 2.1.1: Problemden makineye giden yol. İnsan dilindeki tarif bir algoritmaya, algoritma bir programa dönüşür; program da makinede çalışır.

Şekil 2.1.1 bu yolun dört durağını gösterir. Her geçişin kendi zorluğu vardır. Problemden algoritmaya geçişte belirsizlikler giderilir ve çözüm kesin adımlara bölünür. Algoritmadan programa geçişte bu adımlar bir dilin kurallarına göre yazılır. Programdan makineye geçişi ise derleyici ya da yorumlayıcı denen yardımcı programlar üstlenir. Bu notun büyük bölümü ikinci geçişle ilgilidir: bir algoritma Java'da nasıl doğru yazılır ve yazılan kod gerçekte ne anlama gelir?

Örnek dil sayısı çok fazladır: C, C++, Java, Python, Prolog, Haskell, Scala ve yüzlercesi. Hiçbiri her iş için en iyisi değildir. Dil seçimi projenin ihtiyaçlarına göre yapılır. Hız, güvenilirlik, geliştirme süresi, çalışılacak donanım ve ekibin deneyimi bu seçimi etkiler. Sonraki alt kesimler bu seçimde işe yarayan sınıflandırmaları verir.

2.1.1.2Soyutlama düzeyleri

İşlemci yalnız kendi komut kümesini anlar. Bu komutlar ikili sayılarla kodlanmıştır. Her biri çok küçük bir iş yapar: bir yazmaçtan ötekine değer taşır, iki yazmacı toplar ya da bellekten bir sözcük okur. İşlemcinin doğrudan çalıştırdığı bu biçime makine dili (machine language) ya da makine kodu denir. Makine dili bir işlemci ailesine özgüdür; bir Intel işlemcinin makine kodu bir ARM işlemcide çalışmaz.

Tanım 2.1.2

Düşük seviyeli dil (low-level language), donanıma yakın dildir. Bu dilde yazılan her satır işlemcinin bir ya da birkaç komutuna karşılık gelir. Makine dili ve assembly dili (assembly language) bu gruptadır. Yüksek seviyeli dil (high-level language) ise donanımın ayrıntısını gizler; programcı değişkenler, ifadeler, döngüler ve metotlarla düşünür. Java, Python ve C# yüksek seviyeli dillerdir.

Assembly dili makine komutlarına okunabilir kısaltmalar verir. ADD, MOV, LOAD gibi adlar ikili kodların yerini alır. Yine de her satır hâlâ tek bir makine komutuna karşılık gelir. a = b + c gibi basit bir işlem assembly'de kabaca üç satırdır: b'yi bir yazmaca yükle, c'yi ekle, sonucu a'nın bellek adresine yaz. Java'da ise aynı iş tek bir satırdır ve hangi yazmacın kullanılacağı programcıyı ilgilendirmez.

Soyutlama düzeyi yükseldikçe program kısalır, okunur hâle gelir ve başka donanımlara taşınması kolaylaşır. Buna karşılık programcı donanım üzerindeki denetimin bir kısmını bırakır. Elektrik-elektronik mühendisliğinde iki ucun da yeri vardır. Bir mikrodenetleyicinin kesme yordamı ya da çok sıkı zaman kısıtı olan bir sürücü bazen assembly ya da C ile yazılır. Ölçüm verisini işleyen, arayüzü olan ya da ağda haberleşen bir uygulama ise çoğunlukla Java ya da Python ile yazılır.

Tablo 2.1.1: Soyutlama düzeylerinin karşılaştırması.
DüzeyÖrnekBir satır neye karşılık gelir?Taşınabilirlik
Makine diliikili komut kodlarıtek bir işlemci komutuyalnız o işlemci ailesi
AssemblyADD, MOVtek bir işlemci komutu (okunur adla)yalnız o işlemci ailesi
Yüksek seviyeJava, Python, C#çok sayıda işlemci komutuderleyici ya da sanal makine olan her yer

C dili bu sınıflandırmada ara bir yerde durur. Yüksek seviyeli bir dildir ama belleğe adresle erişmeye ve donanıma yakın kod yazmaya izin verir. Bu yüzden gömülü sistemlerde çok yaygındır. Java ise programcıyı bellek adreslerinden tümüyle uzak tutar; bellek yönetimini de çöp toplayıcı denen bir bileşene bırakır.

2.1.1.3Genel amaçlı ve hedefe odaklı diller

Dilleri sınıflandırmanın ikinci yolu uygulama alanına bakmaktır.

Tanım 2.1.3

Genel amaçlı dil (general-purpose language), çok çeşitli uygulamaları destekleyen geniş bir temel komut kümesi sunar. Hedefe odaklı dil (domain-specific language) ise belirli bir uygulama türüne odaklanır ve o türdeki işlemleri kolaylaştırır.

Java, C, C++ ve Python genel amaçlı dillerdir. Aynı dille bir masaüstü programı, bir web sunucusu, bir mobil uygulama ya da bir gömülü sistem yazılabilir. Hedefe odaklı dillerin en tanıdık iki örneği MATLAB ve SQL'dir. MATLAB sayısal hesaplama için tasarlanmıştır; matris ve vektör işlemleri dile gömülüdür ve hızlı çalışacak biçimde eniyilenmiştir. SQL (Structured Query Language, yapılandırılmış sorgu dili) veri tabanından veri istemek ve veri tabanını güncellemek içindir.

Farkı en iyi aynı iş gösterir. İki n × n matrisin çarpımı MATLAB'de tek satırdır:

C = A * B;   % tek satır

Java'da aynı iş elle yazılan üç iç içe döngüyle yapılır:

for (int i = 0; i < n; i++)
    for (int j = 0; j < n; j++)
        for (int k = 0; k < n; k++)
            C[i][j] += A[i][k] * B[k][j];

Java kodunda en içteki satır Cij = Σk Aik · Bkj toplamını adım adım kurar. Dış iki döngü sonuç matrisinin her hücresini dolaşır, en içteki döngü o hücre için çarpımları toplar. C[i][j] += … yazımı “C[i][j]'ye ekle” anlamına gelir; C dizisinin başlangıçta sıfırlarla dolu olduğu varsayılır. Java'da yeni oluşturulan bir double dizisinin öğeleri zaten 0.0'dır.

İki yazımın maliyeti de farklıdır. Üç iç içe döngü her biri n tur döndüğü için toplam n3 çarpma ve toplama yapar. MATLAB'deki A * B da sonuçta benzer sayıda işlem yapar, ama bu işlemleri önbelleği iyi kullanan, çoğu zaman paralel çalışan hazır bir kitaplığa bırakır. Hedefe odaklı dil bu yüzden hem daha kısa hem de o alanda çoğu zaman daha hızlıdır. Bedeli esnekliktir: MATLAB ile bir web sunucusu yazmak, Java ile yazmaktan çok daha zordur.

Sık yapılan hata

MATLAB alışkanlığıyla Java'da C = A * B; yazmak. Java'da * operatörü yalnız sayılar için tanımlıdır; iki dizinin çarpımını bilmez. Derleyici bad operand types for binary operator '*' iletisiyle satırı reddeder. Java'da matris çarpımı ya döngülerle ya da bir kitaplık metoduyla yapılır.

2.1.1.4Derlenen ve yorumlanan diller

Üçüncü sınıflandırma, kaynak kodun nasıl çalıştırıldığına bakar. İşlemci kaynak kodu doğrudan anlamaz; kodun önce makine diline çevrilmesi gerekir. Bu çeviri iki temel yolla yapılır.

Tanım 2.1.4

Derleyici (compiler), kaynak kodun tamamını program çalıştırılmadan önce başka bir dile, çoğunlukla makine koduna çeviren programdır. Yorumlayıcı (interpreter) ise kaynak kodu okur ve komutları sırayla, o anda çalıştırır; önceden bütün bir çeviri üretmez. Bu iki yola göre diller derlenen (compiled) ve yorumlanan (interpreted) diye ayrılır.

Kaynak kodDerleyicitek seferde çevirirMakine koduçalıştırılabilir dosyaÇalıştır Kaynak kodYorumlayıcısatır satır oku ve yapÇıktı
Şekil 2.1.2: Yürütme biçimleri. Üstte derlenen dil (C, C++, Go, Rust): kaynak kod derleyiciyle tek seferde makine koduna çevrilir, oluşan çalıştırılabilir dosya sonra çalıştırılır. Altta yorumlanan dil (Python, JavaScript): yorumlayıcı kaynak kodu satır satır okur ve doğrudan çalıştırır.

Şekil 2.1.2'de iki yol yan yana durur. Derlenen bir dilde çeviri bir kez yapılır ve sonucu bir dosyaya yazılır. Windows'taki bir .exe dosyası böyle bir çıktıdır. Program her çalıştırıldığında yeniden çevrilmez; makine kodu doğrudan işlemciye verilir. Derleyici bütün programı birden gördüğü için kodu eniyileyebilir: gereksiz işlemleri atar, sık kullanılan değerleri yazmaçlarda tutar. Bu yüzden derlenen programlar genellikle hızlıdır.

Yorumlanan bir dilde ayrı bir çeviri dosyası yoktur. Yorumlayıcı programın yanında sürekli çalışır, her komutu sırası geldiğinde çözümler ve yerine getirir. Bu yaklaşım esneklik sağlar: kod yazılır yazılmaz denenebilir, program çalışırken bile yeni komutlar verilebilir. Bedeli hızdır. Aynı döngü bin kez dönüyorsa yorumlayıcı aynı satırı bin kez çözümler.

Tablo 2.1.2: Derlenen ve yorumlanan dillerin karşılaştırması.
DerlenenYorumlanan
Nasıl?Tüm kaynak kod, çalıştırılmadan önce derleyiciyle nesne kodu (object code) ya da makine koduna çevrilir.Yorumlayıcı kaynak kodu okur ve komutları sırayla o anda çalıştırır.
HızHızlı ve eniyilenmiş yürütmeGenellikle daha yavaş
HatalarBirçoğu derleme sırasında bulunurÇoğu ancak o satır çalışınca görülür
DağıtımHer işletim sistemi ve işlemci için ayrı derleme gerekirYorumlayıcısı olan her yerde aynı kaynak kod çalışır

Tablonun üçüncü satırı programcı için belki de en önemli farktır. Derleyici programı çalıştırmadan önce bütünüyle okur. Yanlış yazılmış bir sözcük, eksik bir parantez ya da uyumsuz bir işlem bu okumada yakalanır ve program hiç çalışmaz. Yorumlanan bir dilde ise hatalı bir satır, programın akışı o satıra ulaşana kadar fark edilmez. Bir Python programı bir saat sorunsuz çalışıp ancak nadiren girilen bir if dalında yazım hatası yüzünden durabilir.

2.1.1.5Java'nın yolu: bytecode ve JVM

Java iki yaklaşımı birleştirir. Kaynak kod önce derlenir, ama doğrudan bir işlemcinin makine koduna değil, bytecode denen ara bir koda. Bu ara kodu da Java Sanal Makinesi (Java Virtual Machine, JVM) çalıştırır.

Tanım 2.1.5

Bytecode (ara kod), Java derleyicisi javac'ın ürettiği ve .class dosyalarında saklanan, platformdan bağımsız komutlardır. JVM, bytecode'u yorumlayan ve sık çalışan kısımlarını JIT derleyici (just-in-time compiler, tam zamanında derleyici) ile çalışma sırasında makine koduna çeviren sanal makinedir.

Bir Java programı iki komutla çalıştırılır. İlki derleme, ikincisi çalıştırmadır:

> javac Paradigma.java      (Paradigma.class oluşur)
> java Paradigma
Buyruksal: 15
Fonksiyonel: 15

javac Paradigma.java komutu kaynak dosyayı okur. Hata yoksa aynı klasörde Paradigma.class dosyasını oluşturur. java Paradigma komutu ise JVM'i başlatır ve ona bu sınıfı yükletir. Dikkat edilirse ikinci komutta dosya uzantısı yazılmaz; JVM'e bir dosya adı değil, bir sınıf adı verilir.

Bytecode'un neye benzediği JDK ile gelen javap -c aracıyla görülebilir. İki tam sayıyı toplayan küçük bir metot:

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

javac bu metodu şu dört bytecode komutuna çevirir:

static int topla(int, int);
  Code:
     0: iload_0
     1: iload_1
     2: iadd
     3: ireturn

Komutlar bir yığın üzerinde çalışır. iload_0 birinci parametreyi (a), iload_1 ikinci parametreyi (b) yığına koyar. iadd yığının üstündeki iki tam sayıyı alır, toplar ve sonucu yığına geri koyar. ireturn yığının üstündeki değeri metodun sonucu olarak döndürür. Baştaki i harfi her komutun int ile çalıştığını gösterir. Bu komutlar hiçbir gerçek işlemciye ait değildir; JVM için tanımlanmış soyut bir komut kümesidir.

Bu ara katmanın büyük bir kazancı vardır. Aynı .class dosyası Windows'ta, Linux'ta ya da macOS'ta, Intel ya da ARM işlemcide değiştirilmeden çalışır. Her platform için ayrı bir JVM yazılmıştır; program ise tek bir kez derlenir. Java'nın tanıtımında kullanılan “bir kez yaz, her yerde çalıştır” (write once, run anywhere) sözü bu özelliği anlatır.

Hız sorununu JIT derleyici çözer. JVM programı önce yorumlayarak çalıştırır ve hangi metotların sık çağrıldığını sayar. Sık çalışan bir metot, çalışma sırasında o makinenin gerçek makine koduna derlenir; sonraki çağrılar yorumlama olmadan doğrudan işlemcide yürür. Bu yüzden uzun süre çalışan Java programları derlenen dillere yakın bir hıza ulaşır. Kısa programlarda ise JVM'in açılış süresi hissedilir.

Java'nın derleme adımı, derlenen dillerin en önemli avantajını da korur. javac bütün programı okur, tipleri denetler ve hatalı programın .class dosyasını hiç üretmez. Yanlış yazılmış bir değişken adı ya da bir sayıya bir dizginin atanması program çalışmadan yakalanır. Kotlin ve Scala gibi başka diller de JVM bytecode'una derlenir; bu diller aynı sanal makineyi ve Java'nın kitaplıklarını paylaşır.

Sık yapılan hata

Programı java Paradigma.class ya da sınıf adından farklı adlı bir dosyayla çalıştırmaya çalışmak. java komutu bir sınıf adı bekler; .class uzantısı yazılırsa JVM Paradigma.class adlı bir sınıf arar ve bulamaz. Ayrıca public bir sınıfın kaynak dosyası sınıfla aynı adı taşımalıdır: public class Paradigma yalnız Paradigma.java adlı dosyada derlenir.

2.1.2Programlama paradigmaları

İki programcı aynı problemi aynı dille bile çok farklı biçimlerde çözebilir. Dillerin kendisi de belli bir çözüm biçimini kolaylaştıracak şekilde tasarlanır. Bir dilin hangi düşünme biçimini öne çıkardığı, o dilin paradigmasıyla anlatılır. Bu kesim dört temel paradigmayı tanıtır ve her birini kısa bir programla gösterir.

2.1.2.1Paradigma kavramı

Tanım 2.1.6

Programlama paradigması (programming paradigm), bir problemi çözerken izlenen temel yaklaşımdır. Paradigma, programın neyden oluştuğunu (komutlar, fonksiyonlar, kurallar ya da nesneler) ve hesaplamanın nasıl ilerlediğini belirler.

Dört paradigma bu derste sık anılır. Tablo 2.1.3 bunları özetler.

Tablo 2.1.3: Dört temel programlama paradigması.
ParadigmaTemel fikirÖrnek diller
Buyruksal (imperative)Programcı makineye, durumunu nasıl değiştireceğini adım adım söyler.FORTRAN, BASIC, Pascal, C
Fonksiyonel (functional)Hesaplama, matematiksel fonksiyonların değerlendirilmesidir; durum değiştirmekten kaçınılır.Lisp, Scheme, Haskell
Mantıksal (logic)Hesaplamanın mantığı olgular ve kurallarla açıklanır; kontrol akışı ayrıntılandırılmaz.Prolog
Nesne yönelimli (object-oriented)İlişkili veri ve metotlar bir nesne altında toplanır; komutlar, üzerinde çalıştıkları durumla birlikte gruplanır.C++, Java, C#, Python

Tablodaki “durum” (state) sözcüğü paradigmaları ayırmanın anahtarıdır. Durum, programın belli bir andaki değişkenlerinin değerleridir. Buyruksal bir programda bu değerler her adımda değişir ve sonuç bu değişikliklerin toplamıdır. Fonksiyonel bir programda ise değerler bir kez hesaplanır ve değiştirilmez; sonuç, fonksiyonların birbirine bağlanmasıyla elde edilir. Mantıksal programda programcı ne adımları ne de fonksiyonları yazar; yalnız neyin doğru olduğunu söyler. Nesne yönelimli programda durum ortadan kalkmaz, nesnelerin içine yerleşir.

Birden çok paradigmayı destekleyen dillere çok paradigmalı diller denir. Java temelde nesne yönelimli ve buyruksaldır. Java 8'den bu yana lambda ifadeleri ve akışlar (streams) gibi fonksiyonel öğeler de içerir. Python ve C# da çok paradigmalıdır. Paradigma bu yüzden bir dilin değil, çoğu zaman bir programın ya da programın bir parçasının özelliğidir.

2.1.2.2Buyruksal ve fonksiyonel: aynı problem

Problem basittir: 1'den 5'e kadar sayıların toplamı bulunsun. Aşağıdaki Java programı bu toplamı iki yolla hesaplar.

import java.util.stream.IntStream;

public class Paradigma {
    public static void main(String[] args) {
        // Buyruksal: adım adım durum değiştir
        int toplam = 0;
        for (int i = 1; i <= 5; i++) {
            toplam = toplam + i;
        }
        System.out.println("Buyruksal: " + toplam);

        // Fonksiyonel (Java 8+): ne istediğini söyle
        int sonuc = IntStream.rangeClosed(1, 5).sum();
        System.out.println("Fonksiyonel: " + sonuc);
    }
}

Program çalıştırıldığında iki yol da aynı sonucu verir:

Buyruksal: 15
Fonksiyonel: 15

Buyruksal parça, toplamın nasıl bulunacağını yazar. toplam adlı bir değişken 0 ile başlar. for döngüsü i'yi 1'den 5'e kadar sayar ve her turda toplam = toplam + i deyimiyle toplam'ı günceller. Sonuç, beş güncellemenin ardından değişkende kalan değerdir. Programcı burada makinenin yerine düşünür: hangi değişken, hangi sırayla, nasıl değişecek?

Fonksiyonel parça ise ne istendiğini yazar. IntStream.rangeClosed(1, 5) 1, 2, 3, 4 ve 5 sayılarından oluşan bir akış üretir; closed sözcüğü üst sınırın da dahil olduğunu belirtir. .sum() bu akıştaki bütün sayıları toplar. Kodda adım adım güncellenen bir değişken yoktur; sonuc değişkeni yalnız bir kez, son değeriyle atanır. Toplamanın hangi sırayla yapılacağı kitaplığa bırakılmıştır.

Programın ilk satırındaki import java.util.stream.IntStream; satırı derleyiciye IntStream sınıfının nerede olduğunu söyler. Bu satır olmasa derleyici IntStream adını tanımaz ve cannot find symbol hatası verir. System ve String gibi sınıflar java.lang paketinde olduğu için onları ayrıca içe aktarmak gerekmez.

2.1.2.3Kod izleme: buyruksal toplam

Buyruksal yaklaşımda durumun nasıl değiştiğini görmenin en iyi yolu kodu satır satır izlemektir. Tablo 2.1.4 döngünün her turunda koşulun değerini ve toplam'ın yeni değerini verir.

Tablo 2.1.4: Buyruksal toplamın izlemesi. Her satır bir koşul denetimini ve ardından gelen gövde çalışmasını gösterir.
ii <= 5Gövdetoplam
——başlangıç0
1doğru0 + 11
2doğru1 + 23
3doğru3 + 36
4doğru6 + 410
5doğru10 + 515
6yanlışdöngü biter15
i değerleri12345toplam0 i değerleri12345toplam31 + 2 = 3 i değerleri12345toplam15 i değerleri12345rangeClosed(1, 5).sum() → 15
Şekil 2.1.3: Toplam programının izlemesinden dört an. (a) toplam durum değişkeni 0 ile başlar. (b) i = 2: 1 + 2 = 3; birinci tur yeşille işaretlenmiştir. (c) i 6 olunca 6 <= 5 yanlıştır ve döngü biter; beş turda biriken toplam 15'tir. (d) Fonksiyonel yol: rangeClosed(1, 5) beş sayıyı birlikte üretir, sum() hepsini toplar; hiçbir değişken adım adım güncellenmez.

İzlemede üç nokta dikkat çeker. Birincisi, koşul altı kez denetlenir ama gövde yalnız beş kez çalışır; son denetim döngüyü bitiren denetimdir. İkincisi, döngü bittiğinde i 6'dır, 5 değil. i döngünün başlığında tanımlandığı için döngüden sonra zaten kullanılamaz; yine de bu ayrıntı sınır hatalarını anlamak için önemlidir. Üçüncüsü, toplam'ın her ara değeri bir sonraki turun girdisidir. Bir turdaki hata sonraki bütün turlara taşınır.

İki yolun maliyeti aynıdır. Her ikisi de beş toplama yapar; n sayı için n toplama gerekir. Fark yazımdadır, iş miktarında değil. Şekil 2.1.3'ün son karesi bu farkı özetler. İki yaklaşım aynı sonucu verir. Fark, nasıl yapılacağını mı yoksa ne istendiğini mi yazdığımızdadır.

Örnek 2.1.1

3'ten 6'ya kadar sayıların toplamı iki yolla yazılsın.

Buyruksal: int t = 0; for (int i = 3; i <= 6; i++) t = t + i;. Turlar sırasıyla t'yi 3, 7, 12 ve 18 yapar; i 7 olunca koşul yanlış çıkar.

Fonksiyonel: IntStream.rangeClosed(3, 6).sum(). Akış 3, 4, 5, 6 sayılarını üretir; toplamları 18'dir.

İki yol da 18 verir. rangeClosed(3, 6) yerine range(3, 6) yazılsaydı üst sınır dahil olmaz, sonuç 3 + 4 + 5 = 12 olurdu.

Sık yapılan hata

Toplam değişkenine ilk değer vermeyi unutmak. int toplam; yazıp döngüde toplam = toplam + i; denirse Java variable toplam might not have been initialized iletisiyle derlemeyi durdurur. Yerel değişkenlerin varsayılan değeri yoktur. Başka dillerde böyle bir değişken rastgele bir değerle başlayabilir ve yanlış bir toplam sessizce üretilir; Java bu hatayı derlemede yakalar.

2.1.2.4Mantıksal programlama: Prolog

Mantıksal paradigmanın bilinen örneği Prolog'dur. Prolog programı adımlardan değil, olgulardan (facts) ve kurallardan (rules) oluşur. Programcı neyin doğru olduğunu yazar; bir soru sorulduğunda cevabı Prolog kendisi arar.

% olgular
ebeveyn(ali, ayse).
ebeveyn(ayse, can).
% kural
dede(X, Z) :- ebeveyn(X, Y),
              ebeveyn(Y, Z).
% sorgu
?- dede(ali, can).
true.

İlk iki satır iki olgu bildirir: Ali, Ayşe'nin ebeveynidir; Ayşe de Can'ın ebeveynidir. Prolog'da küçük harfle başlayan adlar sabittir, büyük harfle başlayanlar değişkendir. Kural satırı “X, Z'nin dedesidir, eğer X bir Y'nin ebeveyniyse ve Y de Z'nin ebeveyniyse” diye okunur. :- işareti “eğer”, virgül “ve” anlamındadır.

Sorgu ?- dede(ali, can). “Ali, Can'ın dedesi midir?” sorusudur. Prolog kuralı kullanır ve X yerine ali, Z yerine can koyar. Ardından ebeveyn(ali, Y) olgusunu sağlayan bir Y arar ve ayse'yi bulur. Son olarak ebeveyn(ayse, can) olgusunun var olup olmadığına bakar. Olgu vardır, cevap true olur.

Bu arama sırasının hiçbiri programda yazılı değildir. Programcı döngü kurmamış, hangi olgunun önce deneneceğini söylememiştir. Mantıksal paradigmada kontrol akışı dilin kendi arama yöntemine bırakılır. Aynı program başka sorulara da cevap verir: ?- dede(ali, X). sorgusu “Ali kimin dedesidir?” anlamına gelir ve Prolog X = can cevabını bulur. Buyruksal bir dilde bu iki soru için iki ayrı kod yazmak gerekirdi.

2.1.2.5Nesne yönelimli programlama: Dikdortgen

Nesne yönelimli paradigma, bu dersin asıl konusudur. Temel fikir, birbirine ait veri ile o veri üzerinde çalışan işlemleri tek bir birimde toplamaktır. Bu birime nesne denir; nesnelerin kalıbı da sınıftır.

Tanım 2.1.7

Nesne yönelimli programlama (object-oriented programming, OOP), programı durumu (veri) ve davranışı (metotlar) birleştiren nesneler üzerine kurar. Nesnenin verisini tutan değişkenlere alan (field), davranışını tanımlayan işlemlere metot (method) denir.

Aşağıdaki sınıf bir dikdörtgeni modeller. Java'da tanımlayıcılarda Türkçe karakter kullanılabilir, ama derste dosya adlarıyla sorun çıkmasın diye Dikdortgen yazımı seçilmiştir.

public class Dikdortgen {
    double en, boy;          // veri (durum)

    Dikdortgen(double en, double boy) {
        this.en = en;
        this.boy = boy;
    }

    double alan() {          // davranış
        return en * boy;
    }

    public static void main(String[] args) {
        Dikdortgen d = new Dikdortgen(3, 4);
        System.out.println("Alan: " + d.alan());
    }
}
Alan: 12.0

Sınıfın dört parçası vardır:

  • double en, boy; satırı iki alan tanımlar. Her Dikdortgen nesnesi kendi en ve boy değerlerini taşır; bu iki değer nesnenin durumudur.
  • Dikdortgen(double en, double boy) bir yapıcıdır (constructor). Yapıcının adı sınıfın adıyla aynıdır ve dönüş tipi yoktur. Nesne oluşturulurken bir kez çağrılır ve alanlara ilk değerlerini verir.
  • double alan() bir metottur. Nesnenin kendi alanlarını kullanarak alanı hesaplar ve double bir değer döndürür.
  • main metodu programın giriş noktasıdır. Bir nesne oluşturur, nesnenin alan() metodunu çağırır ve sonucu yazdırır.

Yapıcının içindeki this.en = en; satırı ilk bakışta tuhaf görünür. Burada iki ayrı en vardır. Sağdaki en, yapıcının parametresidir ve çağrıda verilen değeri taşır. Soldaki this.en ise nesnenin alanıdır. this anahtar kelimesi “şu anda üzerinde çalışılan nesne” anlamına gelir. Parametre ile alan aynı adı taşıdığında parametre alanı gölgeler; alana ulaşmak için this. yazmak gerekir.

alan() metodunda ise this yazılmamıştır: return en * boy;. Metodun içinde en adında bir parametre ya da yerel değişken olmadığı için bu ad doğrudan alana gider. return this.en * this.boy; yazmak da aynı anlama gelir.

Sık yapılan hata

Yapıcıda this yazmayı unutmak: en = en;. Bu satır derlenir, ama parametreyi kendisine atar; alan değişmez. Nesnenin en ve boy alanları varsayılan 0.0 değerinde kalır ve program Alan: 0.0 yazar. Derleyici bu durumda hata vermez, çünkü satır dil açısından geçerlidir. Bu bir mantık hatasıdır.

2.1.2.6Kod izleme: Dikdortgen nesnesi

Nesne yönelimli bir programı izlerken bellekte iki bölge birlikte düşünülür. Yığın (stack), metot çağrılarının çerçevelerini ve yerel değişkenleri tutar. Heap ise new ile oluşturulan nesnelerin yaşadığı bölgedir. Yerel değişken d yığındadır; gösterdiği Dikdortgen nesnesi heap'tedir.

Yığın (stack)Heapmaind— Yığın (stack)Heapmaind—Dikdortgen(…)this = •en = 3.0boy = 4.0Dikdortgenen = 0.0boy = 0.0 Yığın (stack)Heapmaind•Dikdortgenen = 3.0boy = 4.0 Yığın (stack)Heapmaind•alan()this = •Dikdortgenen = 3.0boy = 4.0
Şekil 2.1.4: Dikdortgen programının izlemesinden dört an. (a) main başlar; yığında main çerçevesi vardır, d henüz bir nesneyi göstermez. (b) new heap'te nesneye yer ayırır, alanlar varsayılan 0.0'dır; yapıcı çerçevesinde this nesneyi gösterir, parametreler 3.0 ve 4.0'dır. (c) Yapıcı bitti, alanlar 3.0 ve 4.0 oldu; d artık nesneyi gösterir. (d) d.alan() çağrısında this ile d aynı nesneyi gösterir.

Şekil 2.1.4 programın akışını adım adım verir. Ayrıntılar şöyledir:

  1. main çalışmaya başlar. Yığında main çerçevesi açılır, d değişkenine yer ayrılır ama henüz bir nesne yoktur.
  2. new Dikdortgen(3, 4) ifadesi değerlendirilir. new heap'te nesneye yer ayırır ve alanlara varsayılan değer 0.0'ı verir. Ardından yapıcı çağrılır. Çağrıdaki 3 ve 4 tam sayıdır, parametreler ise double'dır; bu yüzden 3 ve 4 otomatik olarak 3.0 ve 4.0'a genişletilir.
  3. this.en = en; çalışır: nesnenin en alanı 3.0 olur.
  4. this.boy = boy; çalışır: boy alanı 4.0 olur. Yapıcı biter ve çerçevesi yığından kalkar.
  5. new ifadesinin değeri nesnenin referansıdır, yani heap'teki yerini gösteren değerdir. Bu referans d'ye atanır.
  6. d.alan() çağrılır. Yeni çerçevede this, d ile aynı nesneyi gösterir. en * boy = 3.0 × 4.0 = 12.0 hesaplanır ve döndürülür.
  7. Dönen 12.0, "Alan: " dizgisiyle birleştirilir ve Alan: 12.0 yazılır.

Çıktıda 12 değil 12.0 yazmasının nedeni tiptir. alan() bir double döndürür. Java bir double değeri dizgiye çevirirken, değer tam sayı olsa bile en az bir ondalık basamak yazar.

Bu küçük program nesne yönelimli paradigmanın özünü taşır. Veri (en, boy) ve davranış (alan) aynı nesnede durur. main metodu alanın nasıl hesaplandığını bilmez; yalnız nesneden alanını ister. Hesaplama yöntemi değişse bile, örneğin alanlar santimetre yerine milimetre tutulsa, main'deki çağrı aynı kalır.

Örnek 2.1.2

main'e şu iki satır eklensin: Dikdortgen k = new Dikdortgen(2, 5); ve System.out.println(k.alan() + k.en);.

Heap'te ikinci bir nesne oluşur; alanları 2.0 ve 5.0'dır. İlk nesne bundan etkilenmez, çünkü her nesnenin alanları ayrıdır. k.alan() 10.0 döndürür; k.en 2.0'dır. İki double toplanır ve 12.0 yazılır.

Satır "Alan: " + k.alan() + k.en olsaydı sonuç farklı olurdu. Soldaki işlenen dizgi olduğu için iki + da birleştirme yapar ve Alan: 10.02.0 yazılır.

2.1.3Söz dizimi ve anlam

Programlama dilleri ile doğal diller birçok ortak yön paylaşır. Her ikisinde de temel yapılar vardır, bu yapıların nasıl dizileceğini belirleyen kurallar vardır ve doğru dizilmiş bir metnin bir anlamı vardır. Bir dili incelemenin üç katmanı bu gözleme dayanır: söz dizimi, statik anlam ve anlam. Bu ayrım yalnız kuramsal değildir. Programdaki bir hatanın katmanı, hatayı kimin ve ne zaman bulacağını belirler: derleyici mi, JVM mi, yoksa test eden kişi mi?

2.1.3.1Temel yapılar ve söz dizimi

Türkçede temel yapılar kelimeler ve noktalama işaretleridir. Java'da ise sayılar, dizgiler, basit operatörler (+ - * /) ve anahtar kelimelerdir. int, class, if ve return anahtar kelimelere örnektir; dil bu sözcükleri kendine ayırmıştır.

Tanım 2.1.8

Söz dizimi (syntax), bir dilde hangi karakter ve sembol dizilerinin düzgün olduğunu belirleyen biçim kurallarıdır. Bu kurallara uymayan bir metin, içindeki sözcükler tek tek doğru olsa bile geçersizdir.

“kedi köpek çocuk” dizisini oluşturan kelimelerin her biri Türkçedir, ama ortada bir cümle yoktur; özne, nesne ve yüklem kurulmamıştır. Java'da da durum aynıdır. int x = 5; geçerli bir satırdır: önce tip, sonra ad, sonra atama işareti, sonra değer, en sonda noktalı virgül gelir. Aynı parçalar başka bir sırayla dizilirse satır geçersiz olur:

int = x 5;

Derleyici bu satırı reddeder. JDK 21'in tam iletisi şudur:

Hata.java:3: error: not a statement
        int = x 5;
        ^
Hata.java:3: error: ';' expected
        int = x 5;
           ^
Hata.java:3: error: not a statement
        int = x 5;
              ^
Hata.java:3: error: ';' expected
        int = x 5;
               ^
4 errors

Tek bir satırdaki tek bir sıralama yanlışı dört hata iletisine yol açmıştır. Bunun nedeni derleyicinin hatadan sonra durmaması, satırın geri kalanını anlamlandırmaya çalışmasıdır. int'ten sonra bir ad beklerken = görür ve “bu bir deyim değil” der. Ardından bir ; bekler, bulamaz. Kalan x ve 5 parçalarını da ayrı ayrı yorumlamaya çalışır ve iki ileti daha üretir. Sonraki iletiler ilk hatanın yan etkisidir. Bu yüzden birden çok hata listelendiğinde düzeltmeye ilkinden başlanır; ilk hata düzelince çoğu zaman ötekiler de kaybolur.

Söz dizimi dilin anlaşılırlığını da etkiler. Kuralları kesin olan bir dilde bir satırın nasıl okunacağı konusunda tartışma çıkmaz. Söz dizimi hataları bu yüzden en kolay bulunan hatalardır: kurallar açıkça tanımlanmıştır ve derleyici her satırı bu kurallara göre denetler.

2.1.3.2Statik anlam

Söz dizimi doğru olan her metin anlamlı değildir. İngilizcedeki “I are big” cümlesi biçim olarak <isim> <fiil> <isim> kalıbına uyar. Ama “I” tekil, “are” çoğuldur; cümle dil açısından geçersizdir. Bu tür bir uyumsuzluk cümlenin dizilişinde değil, parçaların birbirine uygunluğundadır.

Tanım 2.1.9

Statik anlam (static semantics), söz dizimi doğru olan bir ifadenin anlamlı olup olmadığını program çalışmadan önce denetleyen kurallardır. Java'da bu kuralların en önemlisi tip uyumudur.

Java'da <değer> <işleç> <değer> geçerli bir kalıptır. 2.3 / 4 bu kalıba uyar ve anlamlıdır. Aşağıdaki satır da aynı kalıba uyar, ama bir sayıyı bir dizgiye bölmenin anlamı yoktur:

double sonuc = 2.3 / "abc";
error: bad operand types for binary operator '/'
  first type:  double
  second type: String

İleti, / operatörünün bu iki tiple kullanılamayacağını söyler ve iki işlenenin tipini ayrı ayrı yazar: birincisi double, ikincisi String. Aynı türden bir başka hata, bir değişkene tipine uymayan bir değer atamaktır:

int x = "beş";
error: incompatible types:
  String cannot be converted to int

Bu iki satırın söz diziminde bir kusur yoktur. Hata, derleyicinin tip denetimi adımında bulunur. Java statik tipli bir dildir: derleyici her değişkenin ve her ifadenin tipini bilir, her operatörün işlenenlerinin tipini denetler. Bu yüzden bu tür hatalar program hiç çalışmadan yakalanır.

Kaynak kodHata.javaSöz dizimiint = x 5;Statik anlamint x = "beş";Çalışma (anlam)(a + b) / 2
Şekil 2.1.5: Kaynak kodun geçtiği denetimler. Kod önce söz dizimi (int = x 5; burada takılır), sonra statik anlam denetiminden (int x = "beş"; burada takılır) geçer; ikisi de derleme zamanındadır. Anlam hataları ((a + b) / 2 ile ortalama) ise ancak program çalışırken ortaya çıkar.

Şekil 2.1.5 üç katmanı bir boru hattı gibi gösterir. Her katman bir öncekinden geçmiş kodu alır. Söz dizimi hatası olan bir satır tip denetimine hiç ulaşmaz. Tip denetiminden geçen kod derlenir ve çalıştırılabilir. Son kutudaki hatalar ise derleyicinin göremeyeceği türdendir.

Statik anlam denetiminin gücü dilden dile değişir. Bu denetimi zayıf olan dillerde yakalanmayan hatalar programın davranışını öngörülemez kılar. C dilinde bir dizinin sınırı dışına yazmak derlemede yakalanmaz ve program belleğin başka bir yerini sessizce bozabilir. Java bu tür hataların bir kısmını derlemede, kalanını çalışma zamanında bir istisnayla yakalar; hiçbirini sessiz bırakmaz.

2.1.3.3Anlam

Tanım 2.1.10

Anlam (semantics), söz dizimi doğru bir ifadenin ne anlama geldiğini, yani çalıştığında ne yaptığını belirler.

Doğal dillerde bir cümlenin birden fazla anlamı olabilir. “They saw the man with the telescope.” cümlesi iki biçimde okunur: adamı teleskopla gördüler, ya da teleskobu olan adamı gördüler. Dinleyen kişi doğru anlamı bağlamdan çıkarır. Programlama dillerinde böyle bir çok anlamlılık yoktur. Her geçerli ifadenin tek bir anlamı vardır ve bu anlam dil belirtiminde yazılıdır.

Ancak bu tek anlam, programcının beklediği şey olmayabilir. Aşağıdaki kod 7 ile 8'in ortalamasını üç farklı biçimde hesaplamaya çalışır:

int a = 7, b = 8;
int ortalama = (a + b) / 2;
System.out.println(ortalama);       // 7.5 beklenirken
System.out.println(a + b / 2);      // öncelik
System.out.println((a + b) / 2.0);  // doğru
7
11
7.5

7 ile 8'in ortalaması 7,5'tir. Yalnız üçüncü satır bu değeri yazar. İlk iki satır derlenir, çalışır ve bir sayı yazar; ama sayı yanlıştır. Derleyici bu satırlarda bir hata görmez, çünkü dil açısından ortada bir hata yoktur. Her satır Java'nın kurallarına göre tam olarak ne yazılmışsa onu yapar.

  • (a + b) / 2: iki işlenen de int olduğu için Java tam sayı bölmesi yapar. 15 / 2'nin kesirli kısmı atılır ve 7 bulunur.
  • a + b / 2: parantez yoktur ve / işleminin önceliği +'dan yüksektir. Önce b / 2 = 4 hesaplanır, sonra 7 + 4 = 11 bulunur.
  • (a + b) / 2.0: 2.0 bir double sabittir. 15 de double'a genişletilir ve gerçek bölme yapılır: 7.5.

2.1.3.4Kod izleme: tam sayı bölmesi

Bir ifadenin nasıl değerlendirildiği, ifade ağacıyla açıkça görülür. Ağaçta her iç düğüm bir operatör, her yaprak bir değer ya da değişkendir. Bir operatör ancak alt düğümlerinin değerleri bulunduktan sonra uygulanır; bu yüzden değerlendirme yapraklardan köke doğru ilerler.

/+152a7b8 /7+152a7b8 +11a7/4b82 /7.5+152.0a7b8
Şekil 2.1.6: Ortalama programının ifade ağaçları. (a) (a + b) / 2: önce parantez, a + b = 15. (b) 15 / 2: iki işlenen de int, tam sayı bölmesiyle 7. (c) a + b / 2: parantez olmayınca bölme alta iner; önce b / 2 = 4, sonra 7 + 4 = 11. (d) (a + b) / 2.0: 2.0 double olduğu için sonuç 7.5.

Şekil 2.1.6'daki (a) ve (c) ağaçları aynı dört parçadan (a, b, +, /) ve aynı 2 sabitinden oluşur. Yalnız biçimleri farklıdır. Parantez + düğümünü alta, yani önce yapılacak yere taşır. Parantez olmadığında öncelik kuralı /'yi alta koyar. Aynı karakterlerle yazılan iki ifadenin farklı sonuç vermesinin nedeni budur.

Tablo 2.1.5: Ortalama programının satır satır izlemesi.
SatırHesapDeğişkenlerÇıktı
1—a = 7, b = 8
2(7 + 8) / 2 = 15 / 2 = 7ortalama = 7
3—7
47 + 8 / 2 = 7 + 4 = 1111
5(7 + 8) / 2.0 = 15.0 / 2.0 = 7.57.5

Programın ikinci satırı bu notun en sık rastlanacak tuzağını gösterir. ortalama değişkeni int olarak tanımlanmıştır. Sağ taraf 7.5 üretseydi bile Java bu değeri int'e atamaya izin vermezdi. Değişken double yapılsa da sorun çözülmez; sağ taraf yine tam sayı bölmesiyle 7 üretir, atama yalnız 7'yi 7.0'a çevirir.

Sık yapılan hata

Sonucu double bir değişkene yazmanın bölmeyi ondalıklı yaptığını sanmak. double ort = (a + b) / 2; satırı 7.5 değil 7.0 verir. Java önce sağ tarafı kendi tipleriyle hesaplar: iki int'in bölümü int'tir. Atama ancak bu hesap bittikten sonra yapılır ve kaybolan kesir geri gelmez. Doğrusu işlenenlerden birini double yapmaktır: (a + b) / 2.0.

2.1.4Hata türleri ve hata iletileri

Program yazmak, hata yapmayı ve hatayı bulmayı da içerir. Deneyimli programcılar bile ilk denemede hatasız kod yazmaz. Fark, hatanın türünü hızla tanımak ve onu nerede arayacağını bilmektir. Söz dizimi, statik anlam ve anlam ayrımı burada doğrudan işe yarar.

2.1.4.1Dört hata türü

Tablo 2.1.6: Hata türleri, fark edildikleri an ve Java'daki belirtileri.
Hata türüNe zaman fark edilir?Java örneğiBelirti
Söz dizimiderlemede (derleyici)System.out.println("Merhaba")';' expected
Statik anlamderlemede (tip denetimi)int x = "beş";incompatible types
Çalışma zamanıprogram çalışırkena / b (b sıfırken)çökme: ArithmeticException
Mantıkderleyici fark etmez; ancak testle(a + b) / 2 ile ortalamayanlış sonuç, sonsuz döngü

İlk iki tür derleme hatası diye birlikte anılır. İkisi de program çalışmadan bulunur ve hata varken .class dosyası oluşmaz. Söz dizimi hataları yazım hataları gibidir: açıkça tanımlanmış kurallara aykırılıktır ve derleyici bunları kolayca bulur. Statik anlam hataları ise dile özgü denetimlerle aranır; Java'da bu denetimin merkezinde tipler vardır.

Tanım 2.1.11

Çalışma zamanı hatası (runtime error), derlenmiş bir program çalışırken ortaya çıkan ve Java'da bir istisnaya (exception) yol açan hatadır. Mantık hatası (logic error) ise programın çalışıp yanlış sonuç üretmesidir; ne derleyici ne de JVM bunu fark eder.

Tablonun son satırı en tehlikeli durumu anlatır. Söz dizimi ve statik anlam açısından kusursuz bir program bile beklenen sonucu üretmeyebilir. Program çökebilir, sonsuz döngüye girebilir ya da hiçbir uyarı vermeden yanlış bir sonuç verebilir. Bu sonuncusu en zor bulunanıdır, çünkü bir şeylerin yanlış gittiğini gösteren bir belirti yoktur.

2.1.4.2Derleme hatası iletisini okumak

Derleyicinin hata iletisi korkutucu görünebilir, ama düzenli bir yapısı vardır. Aşağıdaki programda bir noktalı virgül eksiktir:

public class Selam {
    public static void main(String[] args) {
        System.out.println("Merhaba")
    }
}
Selam.java:3: error: ';' expected
        System.out.println("Merhaba")
                                     ^
1 error

İleti dört parçadan oluşur:

  • Selam.java:3: hatanın bulunduğu dosya ve satır numarası.
  • error: ';' expected: derleyicinin ne beklediği. Burada bir noktalı virgül bekleniyordu.
  • Satırın kopyası ve altındaki ^ işareti: hatanın satır içindeki konumu. İşaret, kapanan parantezden hemen sonrasını, yani ;'ün gelmesi gereken yeri gösterir.
  • 1 error: toplam hata sayısı.

Hata varsa .class dosyası oluşmaz ve program hiç çalışmaz. Önceki bir derlemeden kalmış eski bir .class dosyası varsa, java komutu yanlışlıkla o eski sürümü çalıştırabilir. Kodda yapılan değişikliğin etkisinin görülmemesi çoğu zaman bu yüzdendir: derleme başarısız olmuştur ama ileti gözden kaçmıştır.

Noktalı virgül eksikse javac hatayı, ;'ün eksik olduğu satırın sonunda gösterir. Aşağıdaki parçada ilk satırın sonundaki ; unutulmuştur:

int x = 5
int y = 6;
System.out.println(x + y);
Semi.java:3: error: ';' expected
        int x = 5
                 ^
1 error

İşaret ikinci satıra değil, 5'in hemen arkasına konmuştur. Derleyici 5'ten sonra gelen sözcüğü (int) görünce deyimin bitmesi gerektiğini anlar ve eksikliği bir önceki parçanın sonuna yazar. Dosyanın 3. satırı, kod parçasının ilk satırıdır; parçanın üstünde sınıf ve main başlıkları vardır.

Örnek 2.1.3

Bir programda int toplam = 0; tanımlanmış, yedinci satırda ise System.out.println(Toplam); yazılmıştır. Derleyici şu iletiyi verir: Hesap.java:7: error: cannot find symbol, altında da symbol: variable Toplam.

İleti okunur: hata 7. satırdadır; derleyici Toplam adında bir değişken bulamamıştır. Tanımlanan ad toplam'dır. Java büyük ve küçük harfi ayırır; bu iki ad farklıdır. Bu bir derleme (statik anlam) hatasıdır. Toplam yerine toplam yazılınca program derlenir.

2.1.4.3Çalışma zamanı hatası ve istisna

Aşağıdaki program sorunsuz derlenir. Hata ancak çalışırken ortaya çıkar:

public class Bolme {
    public static void main(String[] args) {
        int a = 10, b = 0;
        System.out.println(a / b);
    }
}
Exception in thread "main"
java.lang.ArithmeticException: / by zero
	at Bolme.main(Bolme.java:4)

Derleyici a / b ifadesinde bir sorun görmez: iki int'in bölümü geçerli bir ifadedir. b'nin o anda 0 olduğu ancak program çalışırken bilinir. Tam sayılar sıfıra bölünemez; JVM bu durumda bir istisna fırlatır. İletinin parçaları şöyledir:

  • Exception in thread "main": istisna main iş parçacığında oluştu.
  • java.lang.ArithmeticException: istisnanın türü, yani sınıfının tam adı.
  • / by zero: istisnanın açıklaması; tam sayı sıfıra bölünmeye çalışıldı.
  • at Bolme.main(Bolme.java:4): istisnanın oluştuğu metot, dosya ve satır.

Gerçek çıktıda ilk iki parça tek satırda yazılır; sunumda sayfaya sığsın diye bölünmüştür. İstisna bir try-catch bloğuyla yakalanmazsa program bu satırda sonlanır ve sonraki satırlar hiç çalışmaz.

Sıfıra bölmenin davranışı tipe bağlıdır. 10 / 0 istisna fırlatır. 10.0 / 0 ise istisna fırlatmaz ve Infinity değerini üretir; 0.0 / 0 da NaN (not a number, sayı değil) verir. Kayan noktalı sayıların bu özel değerleri vardır, tam sayıların yoktur. Bu yüzden double ile yapılan bir sıfıra bölme programı çökertmez, ama sonucu sessizce anlamsız bir değere dönüştürür.

Bir başka sık çalışma zamanı hatası, dizinin sınırı dışına çıkmaktır. int[] a = new int[3]; üç öğeli bir dizi oluşturur; geçerli indisler 0, 1 ve 2'dir. a[3] = 1; derlenir, ama çalışırken ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3 istisnasını fırlatır. İndisin değeri çoğu zaman bir hesabın sonucudur ve derleyici onu önceden bilemez.

2.1.4.4Mantık hatası ve test

Mantık hatasında program derlenir, çalışır ve bir sonuç verir; yalnız sonuç yanlıştır. Ortalamayı (a + b) / 2 ile almak, dairenin alanı için 2 * pi * r yazmak (bu çevredir), bir döngünün sınırını bir eksik ya da bir fazla kurmak bu türdendir. Dil açısından bu satırların hiçbirinde kusur yoktur.

Mantık hatalarını bulmanın yolu testtir: programı, sonucu önceden bilinen girdilerle çalıştırıp çıktıyı beklenen değerle karşılaştırmak. 7 ile 8'in ortalamasının 7,5 olduğu bilinir; program 7 yazıyorsa bir hata vardır. İyi seçilmiş birkaç test, hatanın yerini de daraltır. 6 ile 8 için program doğru sonucu (7) verir, 7 ile 8 için yanlış sonucu verir; bu fark, kesirli sonuçların kaybolduğunu düşündürür.

İstisna mekanizması mantık hatalarını yakalamaz. İstisna, geçersiz bir işlem yapıldığında fırlatılır; mantık hatasında ise yapılan her işlem geçerlidir. Bu ayrım önemlidir: programın istisna fırlatmadan çalışması, doğru çalıştığını göstermez.

Örnek 2.1.4

Aşağıdaki dört satırın her biri ayrı bir programda olsun. Hata türleri bulunsun.

(a) String s = 5; Statik anlam hatası. Derleyici incompatible types: int cannot be converted to String der. Satırın biçimi doğrudur, tipler uyuşmaz.

(b) System.out.println("Merhaba"; Söz dizimi hatası. Kapanan parantez eksiktir; derleyici ')' or ',' expected der.

(c) int k = 0; System.out.println(10 % k); Çalışma zamanı hatası. Kalan işlemi de bölme gibi sıfıra yapılamaz; ArithmeticException: / by zero fırlatılır.

(d) double cevre = pi * r * r; Mantık hatası. Kod derlenir ve çalışır, ama çevre yerine alanı hesaplar. Doğrusu 2 * pi * r'dir.

2.1.5İfadeler ve işlem önceliği

Bir programın hesap yapan her parçası bir ifadedir. Bir ifadenin değerini doğru bulmak için iki şey bilinmelidir: işlemlerin hangi sırayla yapıldığı ve her işlemin hangi tiple sonuç verdiği. Bu kesim birinciyi, yani öncelik kurallarını işler; tipler de bu kuralların içine yer yer girer.

2.1.5.1İfade, operatör, işlenen

Tanım 2.1.12

İfade (expression), değerlendirilince bir değer üreten yapıdır: 3 + 4, x > 0, pi * r * r. İfadedeki işlem işaretlerine operatör (operator), operatörün üzerinde çalıştığı değerlere işlenen (operand) denir.

3 + 4 ifadesinde + operatör, 3 ve 4 işlenendir; ifadenin değeri 7'dir. İşlenenler de birer ifade olabilir. (a + b) / 2 ifadesinde / operatörünün sol işleneni (a + b) ifadesidir. İfadeler böylece iç içe geçer ve bir ağaç oluşturur.

Java'daki temel aritmetik operatörler beştir: toplama +, çıkarma -, çarpma *, bölme / ve kalan %. Kalan operatörü, bölmenin kalanını verir: 7 % 3 = 1, çünkü 7 = 2 · 3 + 1'dir. Bunlara ek olarak karşılaştırma operatörleri (<, >, <=, >=, ==, !=) iki değeri karşılaştırır ve boolean bir değer, yani true ya da false üretir.

2.1.5.2Öncelik ve soldan sağa kuralı

Tanım 2.1.13

İşlem önceliği (operator precedence), bir ifadede operatörlerin hangi sırayla uygulanacağını belirleyen kuraldır. Aynı öncelikteki aritmetik operatörler soldan sağa uygulanır.

Java'nın aritmetik öncelikleri okulda öğrenilen matematik kurallarıyla aynıdır. Tablo 2.1.7 bu notta kullanılan operatörleri yüksek öncelikten düşüğe doğru sıralar.

Tablo 2.1.7: Bu notta kullanılan operatörlerin önceliği (yukarıdan aşağıya azalır).
DüzeyOperatörlerYön
1parantez ( )içten dışa
2* / %soldan sağa
3+ - (toplama, çıkarma, dizgi birleştirme)soldan sağa
4< > <= >=soldan sağa
5== !=soldan sağa
6= (atama)sağdan sola

Tablodaki iki ayrıntı sık gözden kaçar. Birincisi, *, / ve % aynı düzeydedir. Biri ötekinden önce gelmez; hangisi soldaysa o önce yapılır. 12 / 3 * 2 ifadesi 12 / 6 = 2 değil, (12 / 3) · 2 = 8 verir. İkincisi, atama en düşük önceliklidir ve sağdan sola işler. Bu yüzden x = y = 7; önce y'ye 7 atar, sonra bu atamanın değerini (7) x'e atar.

Aynı öncelikteki işlemlerin soldan sağa yapılması, çıkarmada sonucu doğrudan etkiler. 10 - 4 - 3 ifadesi (10 − 4) − 3 = 3 diye hesaplanır. Sağdan başlansaydı 10 − (4 − 3) = 9 bulunurdu. Toplama ve çarpmada sıra sonucu değiştirmez, ama tam sayı bölmesi devreye girince değiştirir: 7 / 2 * 2 = 3 · 2 = 6, oysa 7 * 2 / 2 = 14 / 2 = 7'dir.

Örnek 2.1.5

10 - 2 * 3 + 8 / 4 % 3 ifadesinin değeri bulunsun.

En yüksek öncelikli operatörler *, / ve %'dir. Soldan sağa: 2 * 3 = 6. Sonra 8 / 4 = 2, ardından 2 % 3 = 2. İfade 10 - 6 + 2 olur.

- ile + aynı düzeydedir, soldan sağa: 10 − 6 = 4, 4 + 2 = 6. Sonuç 6'dır ve tipi int'tir.

2.1.5.3İfade değerlendirme adım adım

Bilgisayar bir ifadeyi tek seferde değil, öncelik kurallarına göre adım adım sadeleştirir. Her adımda en yüksek öncelikli işlemlerden en soldakini yapar ve onun yerine sonucunu koyar. Fahrenhayttan Celsius'a dönüşüm formülünden alınan şu ifade bunu iyi gösterir: (72 - 32) / 9 * 5. Matematikte bu ifadenin değeri 40 · 5 / 9 ≈ 22,22'dir.

Tablo 2.1.8: (72 - 32) / 9 * 5 ve (72.0 - 32) / 9 * 5 ifadelerinin adım adım değerlendirilmesi.
AdımİfadeYapılan işlem
1(72 - 32) / 9 * 5önce parantez: 72 − 32 = 40
240 / 9 * 5/ ile * aynı düzeyde, soldan sağa: önce 40 / 9
34 * 5iki işlenen int: tam sayı bölmesi, 4,44… değil 4 (kalan 4 atıldı)
420sonuç 20; beklenen 22,22… idi
5(72.0 - 32) / 9 * 5double − int: 32, 32.0'a genişletilir; sonuç 40.0
640.0 / 9 * 5gerçek bölme: 4.444444444444445
74.444444444444445 * 5double × int → double
822.22222222222222doğru sonuç
×/5−972321.2.3.
Şekil 2.1.7: (72 - 32) / 9 * 5 ifadesinin ağacı. Alttaki işlem önce yapılır; numaralar işlem sırasıdır: 1. çıkarma, 2. bölme, 3. çarpma.

Şekil 2.1.7 ifadenin yapısını gösterir. Kökte çarpma vardır, çünkü en son o yapılır. Çarpmanın sol işleneni bölmedir, bölmenin sol işleneni de çıkarmadır. Soldan sağa kuralı ağaçta şu biçimde görünür: / ile * aynı öncelikte olduğu için soldaki (/) alta iner ve önce yapılır.

Tablo 2.1.8'in ilk dört adımı, öncelik kurallarının doğru uygulandığı hâlde sonucun yanlış çıkabileceğini gösterir. Hata öncelikte değil, tiptedir. Üçüncü adımda iki int bölünür ve kesir kaybolur. Bu kayıp sonraki çarpmayla büyür: 0,44'lük kayıp beşle çarpılır ve sonuç 2,22 eksik çıkar. Bu bir anlam (mantık) hatasıdır; derleyici uyarmaz.

Düzeltme için tek bir sabitin double yapılması yeterlidir. 72.0 yazılınca çıkarmanın sonucu double olur ve bu tip ifadenin geri kalanına yayılır. Bir işlemin işlenenlerinden biri double ise öteki de double'a genişletilir ve sonuç double olur. Ondalıklı sonuç gerekiyorsa işlenenlerden en az biri double olmalıdır; bu genişletme de bölmeden önce gerçekleşmelidir.

Son adımdaki 22.22222222222222'nin son basamağı da dikkat çekebilir. Matematikte 200 / 9 = 22,222… sonsuza kadar sürer. double yaklaşık 15–17 anlamlı basamak tutar; Java bu değeri yazarken onu tek başına belirleyen en kısa basamak dizisini seçer. Kayan noktalı sayılar bu yüzden her zaman yaklaşık değerdir.

Örnek 2.1.6

Aynı hesap işlem sırası değiştirilerek (72 - 32) * 5 / 9 diye yazılsın.

Parantez: 40. Soldan sağa, önce çarpma: 40 · 5 = 200. Sonra bölme: 200 / 9 = 22 (tam sayı bölmesi, kalan 2 atılır). Sonuç 22'dir.

Kayıp küçülmüştür: tam sayı bölmesi en sona kaldığı için yalnız son kesir (0,22…) atılır. Çarpmayı bölmeden önce yapmak tam sayılarla çalışırken hatayı azaltır, ama ortadan kaldırmaz. Tam sonuç için yine double gerekir: 200 / 9.0 = 22.22222222222222.

2.1.5.4Tam sayı bölmesi ve kalan

Tanım 2.1.14

Tam sayı bölmesi (integer division), iki tam sayının bölümünde kesirli kısmın atılmasıdır. Java'da / operatörünün iki işleneni de tam sayı tipindeyse (int, long gibi) sonuç da tam sayıdır. Sonuç sıfıra doğru kesilir: 7 / 2 = 3, -7 / 2 = −3.

“Sıfıra doğru kesilir” ifadesi negatif sayılarda önem kazanır. −7 / 2 = −3,5'tir. Kesir atılınca −3 kalır; sonuç sıfıra yaklaşır. Bazı diller (örneğin Python'un // operatörü) aşağı yuvarlar ve −4 verir. Java her zaman sıfıra doğru keser.

Kalan operatörü %, bölmeyle tutarlı çalışır. Her a ve sıfırdan farklı b için şu eşitlik sağlanır:

(a / b) * b + a % b == a

Bu eşitlikten kalanın işaretinin bölünenin işaretiyle aynı olduğu çıkar. -7 % 2 = −1'dir, çünkü (−3) · 2 + (−1) = −7'dir. -17 % 5 = −2'dir. Buna karşılık 7 % -2 = 1'dir; bölenin işareti kalanı etkilemez. Bir sayının çift olup olmadığı n % 2 == 0 ile sınanır. Negatif tek sayılarda kalan −1 olduğu için tek sayı sınaması n % 2 == 1 değil, n % 2 != 0 ile yapılmalıdır.

Tablo 2.1.9: Tam sayı bölmesi ve kalan örnekleri (JDK 21 ile çalıştırılarak doğrulanmıştır).
İfadeSonuçTipAçıklama
7 % 31int7 = 2 · 3 + 1
17 / 53int3,4'ün kesri atılır
17 % 52int17 = 3 · 5 + 2
-7 / 2-3intsıfıra doğru kesilir
-7 % 2-1intkalan bölünenin işaretini alır
1 / 3 * 30int1 / 3 = 0, sonra 0 · 3
1.0 / 3 * 31.0doublegerçek bölme
17 / 5.03.4doublebir işlenen double

Tam sayı bölmesi her zaman bir hata değildir. Birçok algoritma onu bilerek kullanır. Saniyeyi dakika ve saniyeye ayırmak bunun tipik örneğidir: 135 saniye için 135 / 60 = 2 dakika, 135 % 60 = 15 saniye. Bir sayının basamaklarını ayırmak da aynı yolla yapılır: n % 10 son basamağı verir, n / 10 son basamağı atar. Sorun, ondalıklı bir sonuç beklenen yerde tam sayı bölmesinin farkında olmadan yapılmasıdır.

Örnek 2.1.7

Bir öğrencinin üç sınav notu 70, 85 ve 90'dır. Ortalama üç biçimde hesaplansın:

int n1 = 70, n2 = 85, n3 = 90;
System.out.println((n1 + n2 + n3) / 3);     // 81
System.out.println((n1 + n2 + n3) / 3.0);   // 81.66666666666667
double ort = (n1 + n2 + n3) / 3;
System.out.println(ort);                    // 81.0

Toplam 245'tir. İlk satır 245 / 3'ü tam sayı bölmesiyle 81 bulur. İkinci satırda 3.0 bir double olduğu için gerçek bölme yapılır ve 81.66666666666667 yazılır. Üçüncü satır en yanıltıcı olanıdır: değişken double olsa da sağ taraf önce 81 olarak hesaplanır; atama bunu 81.0'a çevirir.

2.1.5.5Üs alma

Java'da üs alma operatörü yoktur. Matematikte 23 diye yazılan işlem, Math.pow(2, 3) metoduyla yapılır ve sonuç double'dır: 8.0. Math.pow(2, 10) = 1024.0, Math.pow(2, 0.5) = 1.4142135623730951'dir. Küçük tam sayı kuvvetleri için çarpma da kullanılır: x * x, x2'dir.

Başka dillerden gelen iki alışkanlık burada sorun çıkarır. Python'daki ** operatörü Java'da yoktur; 2 ** 3 yazılırsa derleyici illegal start of expression der. Daha sinsi olanı ^ işaretidir. Java'da ^ geçerli bir operatördür, ama üs değil, bit düzeyinde dışlayıcı veya (XOR) işlemidir:

System.out.println(2 ^ 3);           // 1: XOR
System.out.println(Math.pow(2, 3));  // 8.0

2'nin ikili gösterimi 010, 3'ünki 011'dir. XOR, iki sayının farklı olduğu bitlerde 1 üretir: 001, yani 1. Kod derlenir ve çalışır; yalnız sonuç yanlıştır. Derleme hatası vermediği için ^ ile üs almaya çalışmak bir mantık hatasıdır.

2.1.6Değişkenler, atama ve tipleme

Bir programın hesapladığı değerlerin bir yerde saklanması gerekir. Bu yer değişkendir. Değişkenin tipi, içinde ne tür bir değer saklanacağını ve o değerle hangi işlemlerin yapılabileceğini belirler.

2.1.6.1Değişken ve atama

Tanım 2.1.15

Değişken (variable), bir ada bağlı, değeri değişebilen bellek alanıdır. Java'da bir değişken kullanılmadan önce tipi ve adıyla bildirilir: int sayac;. Atama (assignment) ise = işaretiyle, sağdaki ifadenin değerini soldaki değişkene yazmaktır.

Atama iki adımda gerçekleşir ve bu iki adımın sırası önemlidir:

  1. Sağ taraftaki ifade, değişkenlerin şu anki değerleriyle hesaplanır.
  2. Bulunan değer soldaki değişkene yazılır; değişkenin eski değeri silinir.

Bu sıra, matematikte anlamsız görünen x = x + 1 satırını anlamlı kılar. Matematikte bu bir denklemdir ve çözümü yoktur. Java'da ise bir komuttur: “x'in şimdiki değerine 1 ekle, sonucu x'e yaz.” x 5 ise önce 5 + 1 = 6 hesaplanır, sonra 6 yazılır.

Örnek 2.1.8

Şu dört satır çalıştıktan sonra a ve b kaçtır? int a = 4; int b = a * 3; a = b - a; b = b / a;

1. a = 4. 2. Sağ taraf 4 · 3 = 12, b = 12. 3. Sağ taraf a'nın eski değeriyle hesaplanır: 12 − 4 = 8; sonra a = 8 olur. 4. 12 / 8 = 1 (tam sayı bölmesi, 0,5 atılır); b = 1.

Sonuç: a = 8, b = 1. Üçüncü satırda a sağda da solda da geçer. Sağdaki a okunurken değeri hâlâ 4'tür; yeni değer ancak hesap bittikten sonra yazılır.

Değişken adlarına tanımlayıcı (identifier) denir ve birkaç kurala uymaları gerekir. Ad bir harf, alt çizgi (_) ya da dolar işaretiyle ($) başlar; sonraki karakterler bunlara ek olarak rakam olabilir. Ad boşluk içeremez ve bir anahtar kelime olamaz. toplam2, _sayac, $fiyat ve ortalamaNot geçerlidir. 2toplam rakamla başladığı, ogrenci sayisi boşluk içerdiği, int de anahtar kelime olduğu için geçersizdir. Java geleneğinde değişken adları küçük harfle başlar ve sonraki sözcükler büyük harfle bitiştirilir: ortalamaNot, ogrenciSayisi.

Java büyük ve küçük harfi ayırır. Sayi ile sayi iki farklı addır:

int Sayi = 4;
System.out.println(sayi);
Harf.java:4: error: cannot find symbol
        System.out.println(sayi);
                           ^
  symbol:   variable sayi
  location: class Harf
1 error

İleti, derleyicinin sayi adlı bir değişken bulamadığını söyler. symbol satırı aranan adı, location satırı aramanın yapıldığı sınıfı verir. Aynı ileti, değişkeni hiç tanımlamamak ya da adını yanlış yazmak durumunda da çıkar.

Bir yerel değişken, değer verilmeden okunamaz:

int s;
System.out.println(s);
IlkDeger.java:4: error: variable s might not have been initialized
        System.out.println(s);
                           ^
1 error

Derleyici, s'ye ulaşan bütün yolları inceler ve hiçbirinde atama bulamaz. Bu denetim, rastgele bir bellek değerinin sessizce kullanılmasını önler. Bir nesnenin alanları ise (Dikdortgen'in en ve boy'u gibi) otomatik olarak varsayılan değer alır: sayılar için 0, boolean için false. Yerel değişkenler için böyle bir varsayılan değer yoktur.

2.1.6.2Atama ile karşılaştırma

Java'da = atamadır, == karşılaştırmadır. Matematikteki eşittir işareti her iki anlamda da kullanıldığı için bu iki operatör sık karıştırılır. Bir koşulun içine yanlışlıkla = yazmak tipik bir hatadır:

int x = 100;
if (x = 100) {
    System.out.println("yüz");
}
EsitMi.java:4: error: incompatible types: int cannot be converted to boolean
        if (x = 100) {
              ^
1 error

if deyiminin parantezi içinde bir boolean değer beklenir. x = 100 ise bir atamadır ve değeri atanan değer, yani int türünde 100'dür. Java bir int'i boolean'a çevirmez, bu yüzden satır derlenmez. Doğrusu if (x == 100)'dür. C dilinde tam sayılar koşul olarak kullanılabildiği için aynı hata derlenir ve koşul her zaman doğru çıkar; Java'nın tip denetimi bu hatayı derlemede yakalar.

Karşılaştırma operatörlerinin ürettiği değerler boolean değişkenlerde saklanabilir: boolean gecti = not >= 50;. Bu satırda önce not >= 50 karşılaştırması yapılır (öncelik tablosunda >=, ='den yüksektir), sonra sonucu gecti'ye atanır. Böyle bir değişken bir if deyiminde doğrudan kullanılabilir: if (gecti).

2.1.6.3Statik ve dinamik tipleme

Her programlama dili değerlerin tipleriyle ilgilenir; diller arasındaki fark, tiplerin ne zaman denetlendiğindedir.

Tanım 2.1.16

Statik tipli dillerde (Java, C, C#) değişkenin tipi açıkça belirtilir, değişmez ve tip uyumu derleme zamanında denetlenir. Dinamik tipli dillerde (Python, JavaScript) değişkenin tipi önceden belirtilmez; değer atanınca belirlenir ve çalışma zamanında değişebilir. Bu iki yaklaşıma statik tipleme ve dinamik tipleme (static / dynamic typing) denir.

Farkı aynı fikrin iki dildeki yazımı gösterir. Python'da bir değişkene önce sayı, sonra dizgi atanabilir:

x = 5          # x şimdi int
x = "beş"      # artık str: sorun yok
print(x + 1)   # çalışırken:
# TypeError: can only concatenate
#   str (not "int") to str

İlk iki satır sorunsuz çalışır. Hata ancak üçüncü satır çalıştırılınca, bir dizgiye sayı eklenmeye çalışılınca ortaya çıkar. Bu satır programın sonunda ya da nadiren girilen bir dalda olsaydı, hata belki haftalarca fark edilmezdi. Java'da aynı fikir derlenmez:

int x = 5;
x = "beş";     // derlenmez:
// error: incompatible types:
//   String cannot be converted to int

x, int olarak bildirilmiştir ve bu tip değişmez. İkinci satırdaki atama derlemede reddedilir; program hiç çalışmaz.

Tablo 2.1.10: Dinamik ve statik tiplemenin karşılaştırması.
Dinamik tipli (Python, JavaScript)Statik tipli (Java, C, C#)
Tip bildirimiÖnceden belirtilmez; değer atanınca belirlenirAçıkça belirtilir ve değişmez
Tip değişebilir mi?Çalışma zamanında değişebilirHayır
Uyumsuz işlemÇalışırken hata (TypeError)Derlemede hata
KazançEsneklik, kısa kodGüvenlik; derleyici hızlı kod üretebilir

Statik tiplemenin bir kazancı da hızdır. Derleyici a + b ifadesindeki iki değişkenin int olduğunu bildiği için doğrudan bir tam sayı toplama komutu üretir. Dinamik tipli bir dilde ise her toplamada işlenenlerin o anki tipine bakılması gerekir: iki sayı mı, iki dizgi mi, yoksa uyumsuz bir çift mi?

Java 10'dan bu yana yerel değişkenler var ile de bildirilebilir. Bu, Java'yı dinamik tipli yapmaz:

var adet = 3;          // tip: int
var oran = adet / 2;   // tip: int, değer 1
var ad = "Ali";        // tip: String
adet = "üç";           // derlenmez: String cannot be converted to int

var ile tip yazılmaz, ama derleyici tipi sağ taraftan çıkarır ve derlemede sabitler. adet'in tipi int'tir; ona daha sonra bir dizgi atanamaz. var yalnız yazımı kısaltır; tip denetimi aynen sürer. Bu yüzden var kullanılan bir değişkene ilk değer vermek zorunludur: var x; derlenmez, çünkü derleyici tipi çıkaracağı bir değer bulamaz.

Sık yapılan hata

var'ın değişkeni “her tipten değer alabilir” yaptığını sanmak. var oran = adet / 2; satırında adet 3 ise oran'ın tipi int, değeri 1'dir. var ondalıklı bir tip seçmez; sağ taraf ne üretiyorsa onu alır. Ondalıklı sonuç isteniyorsa ya double oran = adet / 2.0; yazılır ya da sağ taraf double üretecek biçimde düzenlenir.

2.1.7Dizgiler ve tipler

Bir program yalnız sayılarla çalışmaz. Kullanıcıya gösterilen her ileti, okunan her ad, ekrana yazılan her sonuç bir dizgidir. Sayılar ile dizgiler aynı ifadede buluştuğunda Java'nın kuralları kesin ama ilk bakışta şaşırtıcıdır. Bu kesim o kuralları işler.

Tanım 2.1.17

Dizgi (string), karakterlerden oluşan bir metindir. Java'da dizgiler String sınıfının nesneleridir ve kaynak kodda çift tırnak içinde yazılır: "Merhaba". Tek tırnak içindeki tek bir karakter ('a') ise dizgi değil, char tipinde bir değerdir.

2.1.7.1Farklı tipler karşılaştırılmaz

Farklı veri tiplerini doğrudan karşılaştırmak hataya yol açar. Bir tam sayı ile bir dizgi karşılaştırılamaz; 5 ile "5" insan gözüne aynı görünse de biri bir sayı, öteki bir karakter dizisidir.

int sayi = 5;
String metin = "5";
boolean b = sayi == metin;
error: bad operand types for binary operator '=='
  first type:  int
  second type: String

Diller, tiplerin karşılaştırılması için kurallar ve tip dönüşümü (type conversion) yolları tanımlar. Karşılaştırma ancak iki değer aynı tipe getirildikten sonra anlamlıdır. Bunun iki yönü vardır: dizgi sayıya çevrilir ya da sayı dizgiye çevrilir.

int sayi = 5;
String metin = "5";
System.out.println(Integer.parseInt(metin) == sayi);
System.out.println(metin.equals(String.valueOf(sayi)));
System.out.println(metin.equals(sayi));
true
true
false
  • Integer.parseInt(metin), "5" dizgisini int türündeki 5'e çevirir. İki int artık == ile karşılaştırılabilir: true.
  • String.valueOf(sayi), 5 sayısını "5" dizgisine çevirir. İki dizgi equals metoduyla karşılaştırılır: true.
  • metin.equals(sayi) derlenir, ama her zaman false döndürür. equals her türden nesneyi parametre olarak kabul eder; sayi burada otomatik olarak bir Integer nesnesine sarılır. Bir String hiçbir zaman bir Integer'a eşit değildir.

Üçüncü satır en tehlikeli olanıdır, çünkü derleyici uyarmaz. Program çalışır ve yanlış bir cevap verir; bu bir mantık hatasıdır. parseInt da dikkat ister: dizgi bir tam sayı değilse, örneğin "5a" ya da "5.0" ise, çalışma zamanında NumberFormatException fırlatır.

Dizgilerin karşılaştırılmasında bir kural daha vardır: dizgiler == ile değil equals ile karşılaştırılır. == iki referansın aynı nesneyi gösterip göstermediğine bakar; equals ise iki dizginin karakterlerinin aynı olup olmadığına. İki ayrı nesne aynı metni taşıyabilir. Bu durumda == false, equals true verir. Kaynak kodda yazılan aynı sabit dizgiler Java'da tek bir nesne olarak saklandığı için == bazen yanlışlıkla doğru sonuç da verir. Bu tesadüfe güvenilmemelidir.

2.1.7.2Farklı tiplerde işlemler

Aynı operatör farklı tipler üzerinde farklı davranır. En belirgin örnek +'dır: sayılar üzerinde toplama, dizgiler üzerinde birleştirmedir.

Tanım 2.1.18

Dizgi birleştirme (string concatenation), + ile dizgileri uç uca eklemektir. +'nın işlenenlerinden biri dizgiyse öteki de dizgiye çevrilir ve sonuç bir dizgidir. Genişletme dönüşümü (widening conversion) ise küçük bir sayı tipinin daha büyük bir tipe otomatik çevrilmesidir: int → double.

Tablo 2.1.11: Farklı tiplerde + işlemi (JDK 21 ile çalıştırılarak doğrulanmıştır).
İfadeJava sonucuTipAçıklama
3.0 + 4.07.0doubleiki ondalıklı sayı
3 + 47intiki tam sayı
3 + 4.07.0doubleint otomatik olarak double'a genişletilir
"3" + "4""34"Stringbirleştirme
3 + "4""34"Stringbir taraf dizgiyse öteki dizgiye çevrilir; hata değildir
3 + truederlenmez—bad operand types for binary operator '+'
'a' + 198intchar bir sayısal koddur ('a' = 97)

Tablonun her satırı bir kuralı gösterir. İki int'in toplamı int'tir. Bir int ile bir double toplanırken int önce double'a genişletilir; genişletmede bilgi kaybolmaz, çünkü her int değeri bir double'da tam olarak yazılabilir. Ters yön otomatik değildir. int n = 4.5; satırı derlenmez ve derleyici possible lossy conversion from double to int der: double'dan int'e geçişte kesir kaybolabilir ve Java bu kaybı programcının açıkça istemesini bekler.

3 + "4" satırı dillerin farklı tercihlerini gösterir. Java bunu hata saymaz ve 3'ü "3"'e çevirip birleştirir. Python'da 3 + "4" bir TypeError fırlatır; Python sayıyı kendiliğinden dizgiye çevirmez. JavaScript ise Java gibi birleştirir. Hiçbiri yanlış değildir; her dilin kuralı farklıdır ve programcı kullandığı dilin kuralını bilmelidir.

3 + true satırı Java'nın bir başka tercihidir: boolean bir sayı değildir. C'de doğru değeri 1 sayılır ve bu toplam 4 verir; Java ise satırı derlemez. Son satırdaki 'a' + 1 ise şaşırtıcı görünse de kurala uyar. char tipi bir karakteri Unicode kod numarasıyla saklar; 'a''nın kodu 97'dir. Aritmetik işlemde char, int'e genişletilir ve 97 + 1 = 98 bulunur. Sonucu karakter olarak görmek için açık dönüşüm gerekir: (char) ('a' + 1) ifadesi 'b' verir.

Farklı tipler arasında işlem, dilin kuralları bilinmiyorsa beklenmeyen sonuçlara ya da hatalara yol açar. Java'da bu kurallar kesindir ve derleyici onları her ifadeye aynı biçimde uygular. Kuralları bilen bir programcı için Java'nın davranışı her zaman öngörülebilirdir.

2.1.7.3Birleştirme soldan sağa

+ operatörü soldan sağa işler ve her adımda yalnız o adımın iki işlenenine bakar. Bir ifadede hem sayılar hem dizgiler varsa sonuç, dizginin hangi konumda olduğuna bağlıdır:

Tablo 2.1.12: Sayı ve dizgi karışık ifadeler.
İfadeAdımlarÇıktı
"Toplam: " + 2 + 3"Toplam: 2" + 3Toplam: 23
"Toplam: " + (2 + 3)parantez önce: 5; "Toplam: " + 5Toplam: 5
2 + 3 + " toplam"2 + 3 = 5; 5 + " toplam"5 toplam
1 + 2 + "3" + 4 + 53; "33"; "334"; "3345"3345
"" + 'A' + 'B'"A"; "AB"AB
'A' + 'B'65 + 66131

İlk satır, sık yapılan bir hatanın kaynağıdır. Programcı “Toplam: 5” yazdırmak ister, ama Java soldan başlar: "Toplam: " + 2 bir dizgidir. Bu dizgiye 3 eklenince yine birleştirme yapılır ve “Toplam: 23” çıkar. Sayıların önce toplanması için parantez gerekir. Üçüncü satırda ise dizgi en sondadır. Soldaki iki int toplanır ve 5 bulunur; birleştirme ancak son adımda yapılır.

Dördüncü satır kuralın genel hâlini gösterir. İlk dizgi görülene kadar sayılar toplanır; ilk dizgiden sonra her + birleştirmedir. Son iki satır char için aynı kuralı uygular. 'A' + 'B' ifadesinde dizgi yoktur; iki karakter sayı olarak toplanır ve 65 + 66 = 131 bulunur. Başa boş bir dizgi ("") eklemek ifadeyi en baştan birleştirmeye çevirir.

2.1.7.4Girdide ondalık ayırıcı

Tipler arası dönüşümün bir yüzü de kullanıcıdan okunan girdidir. Klavyeden gelen her şey önce metindir; Scanner sınıfı bu metni istenen tipe çevirir. nextDouble() metodu bir ondalıklı sayı okurken bilgisayarın yerel ayarını (locale) kullanır. Türkçe yerel ayarda ondalık ayırıcı virgüldür.

Scanner giris = new Scanner(System.in);   // yerel ayar: Türkçe
double boy = giris.nextDouble();
// girdi "1,75" -> 1.75
// girdi "1.75" -> java.util.InputMismatchException

Türkçe bölge ayarlı bir bilgisayarda kullanıcı 1,75 yazarsa değer 1.75 olarak okunur. 1.75 yazarsa Scanner bunu bir ondalıklı sayı olarak tanımaz ve InputMismatchException fırlatır; program durur. Kaynak koddaki sabitler (65.2 gibi) ve println'ın çıktısı ise yerel ayardan etkilenmez; her zaman nokta kullanılır. Bu yüzden aynı program girdide virgül ister, çıktıda nokta yazar.

Sorunu çözmenin iki yolu vardır. Birincisi, kullanıcıya gösterilen istemdeki örneği yerel ayara uydurmaktır: “Boy (örn: 1,75)”. İkincisi, Scanner'ın yerel ayarını sabitlemektir: giris.useLocale(Locale.US); satırından sonra girdi her bilgisayarda noktayla okunur. Bu ayrıntı, aynı programın bir bilgisayarda çalışıp ötekinde çökmesinin sık nedenlerinden biridir.

2.1.8Algoritmadan programa

Önceki kesimler bir programın yapı taşlarını tek tek ele aldı: değerler, tipler, ifadeler, değişkenler. Bu kesim onları bir araya getirir. Bir program bu yapı taşlarından kurulmuş bir talimatlar dizisidir; talimatların sırası da en az kendileri kadar önemlidir.

2.1.8.1Program bir talimatlar dizisidir

Bilgisayar talimatları birer birer ve sırayla işler. Bir talimat bittikten sonra bir sonraki başlar ve her talimat bir öncekinin bıraktığı durumdan devam eder. Bu yüzden işlem sırasını doğru belirlemek önemlidir. Klasik bir örnek, iki değişkenin değerlerini yer değiştirmektir; buna takas denir.

int a = 3, b = 5;
a = b;
b = a;
System.out.println(a + " " + b);

int c = 3, d = 5;
int gecici = c;
c = d;
d = gecici;
System.out.println(c + " " + d);

Program iki satır yazar: 5 5 ve 5 3. İlk takas başarısızdır, ikincisi başarılıdır. Tablo 2.1.13 iki parçayı satır satır izler.

Tablo 2.1.13: Takas programının izlemesi.
SatırDeyimDeğişkenlerÇıktı
1int a = 3, b = 5;a = 3, b = 5
2a = b;a = 5, b = 5 (eski 3 kayboldu)
3b = a;a = 5, b = 5
4println5 5
6int c = 3, d = 5;c = 3, d = 5
7int gecici = c;c = 3, d = 5, gecici = 3
8c = d;c = 5, d = 5, gecici = 3
9d = gecici;c = 5, d = 3, gecici = 3
10println5 3
a5b5 a5b5 c3d5gecici3 c5d3gecici3
Şekil 2.1.8: Takas izlemesinden dört an. (a) a = b: a'ya 5 yazılır, eski değer 3 kaybolur. (b) b = a: b'ye a'nın şimdiki değeri (5) yazılır; takas başarısızdır. (c) Doğru yöntem: c'nin değeri önce gecici'de saklanır. (d) d'ye saklanan eski c değeri (3) yazılır; takas başarılıdır.

İlk parçanın hatası, atamanın eski değeri silmesindedir. a = b; çalıştığında a'nın 3 değeri kaybolur ve hiçbir yerde saklanmamıştır. Sonraki b = a; satırı a'nın şimdiki değerini, yani 5'i kopyalar. Şekil 2.1.8'in (a) ve (b) kareleri bu kaybı gösterir. Satırların sırası değiştirilse de sonuç düzelmez: önce b = a; yazılırsa bu kez b'nin değeri kaybolur ve çıktı 3 3 olur.

Doğru yöntem, üzerine yazılacak değeri önceden üçüncü bir geçici değişkende saklamaktır. gecici = c satırı 3'ü güvene alır. Ardından c'nin üzerine güvenle yazılabilir. Son satır, saklanan değeri d'ye taşır. Takas üç atamayla yapılır ve sıraları değiştirilemez. Maliyeti sabittir: kaç değer olursa olsun bir takas üç atama ve bir ek değişken gerektirir.

Örnek 2.1.9

Geçici değişken olmadan da tam sayılar takas edilebilir. a = 4, b = 9 için şu üç satır izlensin: a = a + b; b = a - b; a = a - b;

1. a = 4 + 9 = 13, b = 9. 2. b = 13 − 9 = 4. 3. a = 13 − 4 = 9.

Sonuçta a = 9, b = 4'tür. Yöntem çalışır, ama iki sakıncası vardır. Okunması zordur ve a + b toplamı int'in sınırını aşarsa taşma olur. Geçici değişkenli yöntem her tip için çalışır ve niyeti açıkça gösterir.

2.1.8.2İfadeler, deyimler ve programlar

Bir programın parçaları iki türlüdür: değer üretenler ve etki yaratanlar.

Tanım 2.1.19

İfade bir değeri hesaplar ve sonucunu döndürür: 3 + 4 ifadesinin değeri 7'dir. Deyim (statement) ise bir etki yaratır: pi = 3.14159; bir değişkene değer atar, System.out.println(pi); ekrana yazar. Java'da deyimler genellikle ; ile biter. Bir program, sırayla çalışan deyimlerden oluşur.

İfadeler iç içe geçebilir ve deyimlerin içinde yer alabilir. double alan = pi * r * r; bir deyimdir; içinde pi * r * r ifadesi vardır, o ifade de pi * r ifadesini içerir. Tersi geçerli değildir: bir deyim, bir ifadenin içinde yer alamaz. println ekrana yazar, ama bir değer döndürmez; dönüş tipi void'dir. Bu yüzden onu bir hesabın içinde kullanmak anlamsızdır:

double d = 3 + System.out.println(pi);
error: 'void' type not allowed here

Tersine, değer üreten ama hiçbir etki yaratmayan bir ifade de tek başına deyim olamaz:

3 + 4;      // değer üretir ama etkisi yok
error: not a statement

Her ifade sonuna ; konarak deyime dönüşmez. Java buna yalnız dört ifade türünde izin verir: atama (x = 5;), artırma ve azaltma (i++;), metot çağrısı (println(x);) ve nesne oluşturma (new Dikdortgen(1, 2);). Bunların ortak yanı bir etki yaratmalarıdır. 3 + 4;, x * 2; ya da x > 3; hesaplanır ve sonuç hemen atılır; bu boşa yapılmış bir işlemdir ve derleyici onu bir yazım yanlışı sayar.

Burada ince bir nokta vardır. Java'da atama da bir değer üretir: atanan değeri. Bu yüzden şu satır derlenir:

int y = (x = 4) + 1; → x = 4, y = 5

Parantezdeki atama önce yapılır ve değeri 4 olur; buna 1 eklenir. Atamanın değer üretmesi, x = y = 7; gibi zincirleme atamaları da mümkün kılar. Yine de atamayı bir ifadenin ortasına gömmek okunmayı zorlaştırır ve tercih edilmez. Programın her satırında tek bir etki olması, izlemeyi de hata aramayı da kolaylaştırır.

Sık yapılan hata

println'ın yazdırdığı değeri bir değişkene almaya çalışmak: int s = System.out.println(5);. println ekrana yazar ama bir değer döndürmez; derleyici incompatible types: void cannot be converted to int iletisini verir. Bir hesabın sonucu hem kullanılacak hem yazdırılacaksa önce değişkene atanır, sonra yazdırılır.

2.1.8.3Fahrenhayttan Celsius'a

Bir program yazmanın sırası bellidir: önce algoritma, sonra kod. Algoritma problemi kesin adımlara ayırır; kod bu adımları dilin kurallarıyla yazar. Fahrenhayt cinsinden verilen bir sıcaklığı Celsius'a çeviren bir program bu sırayla kurulsun. Dönüşüm formülü C = (F − 32) · 5 / 9'dur.

Algoritma üç adımdır:

1. Fahrenhayt değeri f'yi oku.
2. c = (f − 32) · 5 / 9 değerini hesapla.
3. c'yi yazdır.

Her adımın Java'da bir karşılığı vardır. Okuma için Scanner, hesap için bir atama deyimi, yazdırma için println kullanılır:

import java.util.Scanner;

public class Fahrenhayt {
    public static void main(String[] args) {
        Scanner giris = new Scanner(System.in);
        System.out.print("Fahrenhayt: ");
        double f = giris.nextDouble();
        double c = (f - 32) * 5 / 9;
        System.out.println("Celsius: " + c);
    }
}

Kullanıcı 100 girdiğinde ekranda şu görünür:

Fahrenhayt: 100
Celsius: 37.77777777777778

Kod satır satır şöyle çalışır. import java.util.Scanner; satırı Scanner sınıfını kullanılabilir kılar. new Scanner(System.in) klavyeden okuyan bir nesne oluşturur. print, println'dan farklı olarak satır sonuna geçmez; bu yüzden kullanıcının girdiği 100 aynı satırda görünür. giris.nextDouble() bir sayı okur ve f'ye atar. Hesap satırında önce f - 32 = 68.0 bulunur; f double olduğu için bu sonuç da double'dır. Ardından 68.0 · 5 = 340.0 ve 340.0 / 9 = 37.77777777777778 hesaplanır.

Formülün yazılış biçimi burada da önemlidir. Aynı formül 5 / 9 * (f - 32) diye yazılırsa sonuç 0.0 olur. Öncelik kuralları değişmemiştir: 5 / 9 soldadır ve önce yapılır. İki işlenen de int olduğu için tam sayı bölmesi 0 verir ve 0 · 68.0 = 0.0 bulunur. Derleyici bunu bir hata olarak görmez; program 100 °F için 0 °C yazar. Düzeltmenin iki yolu vardır: bölmeyi ondalıklı yapmak (5.0 / 9 * (f - 32)) ya da çarpmayı öne almak ((f - 32) * 5 / 9). İkincisi, f double olduğu sürece doğru çalışır; f - 32 double ürettiği için sonraki bölme de gerçek bölmedir.

Tablo 2.1.14: Fahrenhayt-Celsius formülünün farklı yazılışları, F = 100 için.
Yazılışf doublef int
(f - 32) * 5 / 937.7777777777777837 (340 / 9, kesir atılır)
5 / 9 * (f - 32)0.00
5.0 / 9 * (f - 32)37.7777777777777837.77777777777778

Tablonun son sütunu, tipin sonucu nasıl değiştirdiğini gösterir. f int olsa bile 5.0 ifadeye bir double soktuğu için sonuç doğru çıkar. Program yazmanın sırası bu küçük örnekte bile görünür: algoritma doğrudur, Java koduna çeviri sırasında yapılan küçük bir yazım farkı sonucu bozar. Bu tür hataları bulmanın yolu, sonucu bilinen bir girdiyle denemektir. 212 °F'nin 100 °C, 32 °F'nin 0 °C olduğu bilinir; program bu iki değeri doğru çeviriyorsa formül büyük olasılıkla doğru yazılmıştır.

2.1.9Popüler programlama dilleri

Programlama dilleri farklı görevler için farklı avantajlar sunar ve programcılar projenin ihtiyacına göre en uygun dili seçer. Dillerin ne kadar kullanıldığı her yıl anketler ve dizinlerle izlenir. Stack Overflow'un geliştirici anketi, TIOBE dizini ve IEEE Spectrum'un sıralaması bunların bilinenleridir. Her biri farklı bir ölçüt kullanır: ankete katılanların beyanı, arama motoru sonuçları ya da iş ilanları. Bu yüzden sıralamalar birbirinden biraz farklıdır, ama ilk sıralarda hep aynı diller görülür.

Tablo 2.1.15: 2023'te öne çıkan dillerden bazıları ve öne çıktıkları alanlar.
DilÖne çıktığı alan
Pythonveri analizi, yapay zekâ, web geliştirme
Javabüyük ölçekli kurumsal uygulamalar, Android
JavaScriptweb tarayıcıları, web geliştirme
C#Windows uygulamaları, oyun geliştirme
C++oyun, sistem programlama, performans odaklı yazılım
Rustgüvenli ve hızlı sistem programlama
SwiftApple platformlarında uygulama geliştirme
KotlinAndroid uygulama geliştirme (JVM üzerinde çalışır)
Go (Golang)basit ve etkili; ağ ve bulut uygulamaları

Tablo önceki kesimlerdeki sınıflandırmalarla birlikte okunabilir. C, C++, Rust ve Go doğrudan makine koduna derlenir ve performansın önemli olduğu yerlerde öne çıkar. Java, Kotlin ve C# bir ara koda derlenip bir sanal makinede çalışır; taşınabilirlik ile hızı dengeler. Python ve JavaScript genellikle yorumlanır ve hızlı geliştirme gereken yerlerde tercih edilir. Kotlin'in Java ile aynı JVM'de çalışması, iki dilin aynı projede birlikte kullanılabilmesini sağlar.

Mühendislik açısından tek bir “en iyi dil” yoktur. Bir mikrodenetleyicinin yazılımı için C, bir sinyal işleme prototipi için MATLAB ya da Python, bir masaüstü ölçüm uygulaması için Java uygun düşebilir. Bir dilde öğrenilen kavramlar, yani tipler, ifadeler, öncelik, atama ve deyim sırası, öteki dillere büyük ölçüde taşınır. Yeni bir dil öğrenmek çoğu zaman bu kavramların o dildeki yazılışını ve küçük kural farklarını öğrenmektir.

2.1.10Sık yapılan hatalar

Programlamaya yeni başlayanların hataları büyük ölçüde aynı birkaç kaynaktan gelir. Aşağıdaki sekiz hata, bu notta ayrıntılı ele alınan kuralların özetidir. Her birinde derleyicinin ya da JVM'in tepkisi JDK 21 ile alınmıştır.

Tablo 2.1.16: Sık yapılan hatalar, belirtileri ve düzeltmeleri.
HataÖrnekBelirtiDüzeltme
Noktalı virgülü unutmakSystem.out.println("Merhaba")';' expected; ^ satırın sonunu gösterirdeyimin sonuna ;
= ile =='yi karıştırmakif (x = 100)int cannot be converted to booleanif (x == 100)
Tam sayı bölmesi(a + b) / 2 ile 7 ve 8'in ortalaması7 (beklenen 7,5); derleyici uyarmaz(a + b) / 2.0
Üs için ** ya da ^2 ** 3; 2 ^ 3illegal start of expression; ^ derlenir ama XOR'dur (1)Math.pow(2, 3)
Dizgi + sayı sırası"Toplam: " + 2 + 3Toplam: 23"Toplam: " + (2 + 3)
Büyük/küçük harfSayi tanımlayıp sayi kullanmakcannot find symboladı tanımlandığı gibi yazmak
Değer atamadan kullanmakint s; System.out.println(s);variable s might not have been initializedbildirimde ilk değer: int s = 0;
Ondalık ayırıcıTürkçe yerel ayarda nextDouble()'a 7.5 girmekInputMismatchException7,5 girmek ya da useLocale(Locale.US)

Tablodaki hatalar üç gruba ayrılır. Noktalı virgül, büyük/küçük harf, ilk değer ve =/== hataları derleme hatasıdır; derleyici yakalar ve iletisi hatanın yerini gösterir. Ondalık ayırıcı hatası bir çalışma zamanı hatasıdır; program derlenir ama belli bir girdiyle çöker. Tam sayı bölmesi, ^ ile üs ve birleştirme sırası ise mantık hatasıdır. Bu sonuncular en tehlikeli olanlardır, çünkü program hiçbir belirti göstermeden yanlış bir sonuç üretir. Onları bulmanın tek yolu, sonucu bilinen girdilerle denemektir.

Derleme hatalarıyla ilgili genel bir alışkanlık da işe yarar: ileti dikkatle okunur ve düzeltmeye ilk hatadan başlanır. int = x 5; örneğinde olduğu gibi, tek bir yanlış bir dizi iletiye yol açabilir. İlk hata düzelince ötekiler çoğu zaman kendiliğinden kaybolur. Hata iletisinin gösterdiği satırda bir sorun görünmüyorsa bir önceki satıra da bakılır; özellikle kapanmamış bir parantez ya da tırnak, hatayı birkaç satır ileriye taşıyabilir.

2.1.11Özet

  • Programlama dili, bir makinenin yapabileceği hesaplamaları ifade etmek için tasarlanmış yapay bir dildir. Program hem makineye talimat hem de programcılar arasında bir iletişim aracıdır.
  • Diller dört ölçüte göre sınıflandırılır: soyutlama düzeyi (düşük ya da yüksek seviye), uygulama alanı (genel amaçlı ya da hedefe odaklı), yürütme biçimi (derlenen ya da yorumlanan) ve paradigma.
  • Java'da javac kaynak kodu bytecode'a (.class) derler; JVM bytecode'u yorumlar ve sık çalışan kısımları JIT ile makine koduna çevirir. Aynı .class dosyası JVM olan her platformda çalışır.
  • Dört temel paradigma buyruksal, fonksiyonel, mantıksal ve nesne yönelimlidir. Java temelde nesne yönelimli ve buyruksaldır; Java 8'den bu yana fonksiyonel öğeler de içerir.
  • Söz dizimi biçim kurallarıdır. Statik anlam, söz dizimi doğru kodun derlemede denetlenen anlam kurallarıdır (tip uyumu). Anlam ise kodun ne yaptığıdır; tek ve kesindir, ama programcının beklediği olmayabilir.
  • Hatalar dört türlüdür: söz dizimi ve statik anlam hataları derlemede, çalışma zamanı hataları çalışırken istisnayla, mantık hataları ancak testle bulunur.
  • İfadeler önce parantez, sonra * / %, sonra + - sırasıyla, aynı düzeyde soldan sağa değerlendirilir. İki tam sayının bölümü tam sayıdır ve sıfıra doğru kesilir. Java'da üs operatörü yoktur; Math.pow kullanılır.
  • Atama iki adımdır: önce sağ taraf şu anki değerlerle hesaplanır, sonra soldaki değişkene yazılır. = atama, == karşılaştırmadır.
  • Java statik tiplidir: tipler derlemede denetlenir ve değişmez. var bile tipi derlemede sabitler. int ile double'ın işlemi double'dır. Bir işlenen dizgiyse + birleştirmedir ve soldan sağa işler.
  • Dizgiler equals ile karşılaştırılır. Farklı tipler önce aynı tipe dönüştürülür: Integer.parseInt, String.valueOf.
  • Program sırayla çalışan deyimlerden oluşur. İfade değer üretir, deyim etki yaratır. Deyimlerin sırası sonucu belirler; takas üçüncü bir geçici değişkenle yapılır.
  • Bir program önce algoritma olarak tasarlanır, sonra koda çevrilir. Çeviride tip ve öncelik ayrıntıları sonucu değiştirebilir.

2.1.12Kavram listesi

2.1.13Kendini Dene

  1. 17 / 5 * 5 + 17 % 5 ve 17 / 5.0 ifadelerinin değerleri ve tipleri nedir?

    Cevap

    Birincisi: 17 / 5 = 3 (tam sayı bölmesi), 3 · 5 = 15, 17 % 5 = 2, toplam 17, tipi int'tir. Bu sonuç rastlantı değildir: (a / b) * b + a % b her zaman a'ya eşittir. İkincisi: 5.0 double olduğu için gerçek bölme yapılır; 3.4, tipi double'dır.

  2. System.out.println(2 * 3 + "x" + 2 * 3); ne yazdırır?

    Cevap

    6x6. * operatörünün önceliği +'dan yüksektir; iki çarpma, birleştirmelerden önce yapılır ve ifade 6 + "x" + 6 olur. Soldan sağa: 6 + "x" = "6x", ardından "6x" + 6 = "6x6". Çarpmalar parantezsiz de doğru çalışır; sorun yalnız + ile toplama yapılmak istendiğinde çıkar.

  3. Her satır ayrı bir programda olsun. Hata türü (söz dizimi, statik anlam, çalışma zamanı, mantık) nedir? (a) boolean b = 1; (b) int n = 5; int y = 10 / (n - 5); (c) Dairenin çevresi için double cevre = 2 * pi + r;

    Cevap

    (a) Statik anlam: incompatible types: int cannot be converted to boolean. Java'da boolean bir sayı değildir. (b) Çalışma zamanı: kod derlenir, ama n - 5 = 0 olduğu için ArithmeticException: / by zero fırlatılır. (c) Mantık: kod derlenir ve çalışır, ama formül yanlıştır; doğrusu 2 * pi * r'dir.

  4. Şu satırlar çalıştıktan sonra x ve y kaçtır? int x = 6; int y = x / 4; x = x * y + x % 4; y = x - y;

    Cevap

    y = 6 / 4 = 1 (tam sayı bölmesi). Üçüncü satırın sağ tarafı eski x (6) ile hesaplanır: 6 · 1 + 6 % 4 = 6 + 2 = 8; x = 8. Son satır: 8 − 1 = 7. Sonuç x = 8, y = 7.

  5. a = 1, b = 2, c = 3 iken değerler sağa döndürülmek isteniyor: a ← c, b ← a, c ← b. Geçici bir değişkenle hangi sırayla yazılır? Sonuç ne olur?

    Cevap

    Önce üzerine ilk yazılacak değer saklanır. int g = c; c = b; b = a; a = g; sırası doğrudur: g = 3, c = 2, b = 1, a = 3. Sonuç 3 1 2'dir. Her atama, değeri daha önce başka bir yere kopyalanmış bir değişkenin üzerine yazar; hiçbir değer kaybolmaz.

  6. IntStream.range(2, 6).sum() ne verir? Aynı hesabın buyruksal karşılığı nasıl yazılır?

    Cevap

    range üst sınırı dahil etmez: 2, 3, 4, 5 sayıları toplanır ve 14 bulunur. Buyruksal karşılığı: int t = 0; for (int i = 2; i < 6; i++) t = t + i;. Döngü koşulunda <= değil < kullanılır; rangeClosed(2, 6) olsaydı 6 da dahil olur ve 20 bulunurdu.

  7. int c = 37; iken c * 9 / 5 + 32, 9 / 5 * c + 32 ve c * 9 / 5.0 + 32 ifadeleri ne verir?

    Cevap

    Birincisi: 37 · 9 = 333, 333 / 5 = 66 (kesir atılır), 66 + 32 = 98. İkincisi: 9 / 5 = 1 (tam sayı bölmesi), 1 · 37 + 32 = 69; büyük bir hata. Üçüncüsü: 333 / 5.0 = 66.6, 66.6 + 32 = 98.6 (double), doğru değer budur.

  8. x bir int değişken olsun. Aşağıdakilerden hangileri tek başına bir deyim olarak derlenir? x++;, x + 1;, Math.pow(x, 2);, x == 3;

    Cevap

    x++; derlenir: x'i bir artırır, yani bir etki yaratır. Math.pow(x, 2); de derlenir: metot çağrıları deyim olabilir, döndürülen değer atılır. x + 1; ve x == 3; derlenmez ve ikisi de not a statement iletisini verir; yalnız bir değer üretirler, etkileri yoktur.

2.1.14İleri okuma

  • Oracle, The Java Tutorials, “Language Basics” bölümü: değişkenleri, operatörleri, öncelik tablosunu, ifadeleri ve deyimleri kısa örneklerle anlatır. Java'nın resmî ve ücretsiz başlangıç kaynağıdır.
  • A. B. Downey ve C. Mayfield, Think Java (2. baskı, 2019), Bölüm 1–2: programlama dilini, derleme ve yorumlamayı, hata türlerini ve hata iletilerini okumayı anlatır. Anlatım yeni başlayan okura göredir.
  • J. V. Guttag, Introduction to Computation and Programming Using Python (3. baskı, 2021), Bölüm 2: söz dizimi, statik anlam ve anlam ayrımının kaynağıdır. Ayrımı Python örnekleriyle verdiği için dinamik tiplemeyle karşılaştırmaya da elverişlidir.
  • J. Gosling ve diğerleri, The Java Language Specification, Java SE 21 Edition, Bölüm 15: ifadelerin değerlendirme sırasını, tam sayı bölmesini ve dizgi birleştirmeyi kesin kurallarla tanımlar. Bir ifadenin değeri konusunda son söz bu belgededir.

2.1.15Kaynaklar

  1. P. Deitel ve H. Deitel, Java: How to Program, Early Objects, 11. baskı, Pearson, 2017 (Bölüm 1–2).
  2. A. B. Downey ve C. Mayfield, Think Java: How to Think Like a Computer Scientist, 2. baskı, O'Reilly, 2019 (Bölüm 1–2).
  3. J. Gosling ve diğerleri, The Java Language Specification, Java SE 21 Edition, Oracle, 2023 (Bölüm 3: Sözcük yapısı, Bölüm 15: İfadeler).
  4. J. Guttag, Introduction to Computation and Programming Using Python, 3. baskı, MIT Press, 2021 (Bölüm 2: söz dizimi, statik anlam ve anlam).
  5. C. S. Horstmann, Core Java, Volume I: Fundamentals, 12. baskı, Pearson, 2022 (Bölüm 2–3).
  6. Oracle, The Java Tutorials: “Language Basics” (değişkenler, operatörler, ifadeler, deyimler ve bloklar).
  7. K. Sierra ve B. Bates, Head First Java, 3. baskı, O'Reilly, 2022.

İlgili sayfalar: Bölüm 2.1 sunumu · Çalışma soruları · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.