Java Programlama


Java programlama dilini sözdizimi, türler, kontrol akışı, metotlar, nesne yönelimli programlama, diziler, istisnalar ve dosya G/Ç’den generics, koleksiyonlar, eşzamanlılık, JVM, JDBC ve üretim davranışına kadar örnek kodlarla ele alan kapsamlı ders notu.
Java, kaynak kodun doğrudan belirli bir işlemci mimarisine bağlanmasından çok, dil belirtimi ile sanal makine modelinin ayrıldığı bir çalışma düzeni sunar. Bu ayrım taşınabilirliğin temelidir; ancak “bir kez yaz, her yerde çalıştır” ifadesi donanım, işletim sistemi, yerel kütüphane, karakter kodlaması ve dış servis bağımlılıklarının ortadan kalktığı anlamına gelmez. Sağlam bir Java programı için dil kurallarını, Java Sanal Makinesi'ni (JVM), standart kütüphaneyi ve çalışma zamanı davranışını birlikte değerlendirmek gerekir.
Java programlama; problem çözme, veri türleri, giriş/çıkış, denetim akışı, metot, sınıf, dizi, kalıtım, istisna ve dosya G/Ç temellerinden JVM, koleksiyonlar, genel türler, eşzamanlılık, veri erişimi ve üretim davranışına uzanan tek bir bütün olarak ele alınmalıdır. Böylece amaç yalnız sözdizimini ezberlemek değil, kaynak koddan çalışma zamanına kadar davranışın nedenini açıklayabilmektir.
Ünite 1: Derleme, Bytecode ve JVM
.java kaynak dosyası javac ile derlendiğinde platformdan bağımsız JVM komutlarını taşıyan .class dosyaları oluşur. JVM sınıf dosyasını yükler, doğrular, bağlar ve çalıştırır. Sık yürütülen kod bölgeleri çalışma sırasında JIT derleyicisi tarafından yerel makine koduna çevrilebilir. Bu nedenle Java ne yalnız yorumlanan ne de yalnız geleneksel anlamda önceden yerel koda derlenen bir dil olarak sınıflandırılmalıdır.
.java kaynak
|
v
javac
|
v
.class / bytecode
|
v
class loading -> doğrulama -> bağlama -> başlatma
|
+--> interpreter
|
+--> JIT derleme -> yerel makine koduJDK geliştirme araçlarını, derleyiciyi ve çalışma zamanını kapsar. Güncel Java dağıtımlarında eski “ayrı JRE kurulumu” yaklaşımı pratikte merkezi değildir; uygulama çalışma zamanı jlink gibi araçlarla da özelleştirilebilir. Sürüm bağımlı özellikler kullanılırken kaynak sürümü, hedef çalışma zamanı ve kullanılan kütüphaneler birlikte yönetilmelidir.
15 Eylül 2026 itibarıyla JDK 27 en yeni özellik sürümü, JDK 25 ise güncel LTS çizgisidir. Üretim için sürüm seçimi yalnız “en yeni” olmasına göre değil destek ömrü, bağımlılık uyumluluğu, güvenlik güncellemeleri ve kurumun yükseltme politikasına göre yapılmalıdır.
JVM çalışma zamanı veri alanlarının temel ayrımı şunlardır:
- Her iş parçacığına ait program sayacı ve JVM yığını,
- Nesnelerin yer aldığı heap,
- Sınıf üst verisı ve çalışma zamanı sabit havuzu,
- Yerel yöntemler için gerekli çalışma zamanı yapıları.
Java dilindeki yerel değişken, nesne ve referans kavramlarını fiziksel RAM adresleriyle bire bir özdeşleştirmek hatalıdır. JVM gerçekleştirmesi kaçış analizi, scalar replacement veya başka optimizasyonlarla kaynak kodda görülen nesne yaşamını farklı biçimde gerçekleştirebilir.
Java kodunun yürütülmesini incelerken kaynak kod, bayt kodu ve çalışma zamanı davranışı birbirinden ayrılmalıdır. Aynı metot farklı çağrı örüntülerinde yorumlanmış veya derlenmiş kod üzerinden çalışabilir; bu yüzden tek bir kısa süre ölçümü genel başarım sonucu değildir. Nesne oluşturma, sanal çağrı ve bellek erişimi maliyetleri ilgili iş yüküyle birlikte değerlendirilir. İyileştirmenin doğruluğu korunurken ısınma, çöp toplama ve ölçüm değişkenliği de deney düzenine katılmalıdır.
1.1. İlk Program, Derleme ve Çalıştırma
Java programının yürütme giriş noktası public static void main(String[] args) metodudur. main özel bir JVM talimatı değil, uygulama başlatıcısının belirli imzayla aradığı static metottur. Java büyük/küçük harfe duyarlıdır; toplam, Toplam ve TOPLAM farklı tanımlayıcılardır.
public class MerhabaJava {
public static void main(String[] args) {
System.out.println("Merhaba Java");
}
}Kaynak dosyanın adı MerhabaJava.java ise temel komut zinciri şöyledir:
javac MerhabaJava.java
java MerhabaJavaimport, C/C++ anlamında bir önişlemci direktifi değildir. Başka paketlerdeki türlerin adlarını kaynak kodda nitelemesiz kullanabilmek için derleme zamanı ad çözümlemesine katılır. java.lang paketindeki türler örtük olarak kullanılabilir; diğer paketler için tam nitelikli ad veya import gerekir. Wildcard import (java.util.*) alt paketleri içeri almaz.
Açıklama satırları //, /* ... */ ve API belgelemesi için /** ... */ biçimlerinde yazılabilir. Kaynak biçimlendirme çalışma zamanının semantiğini değiştirmez; ancak blok yapısını görünür kılan tutarlı girintileme bakım maliyetini doğrudan etkiler.
Ünite 2: Tür Sistemi, Değişkenler ve İfadeler
Java statik ve güçlü tür denetimi uygular. Temel türler byte, short, int, long, float, double, char ve booleandır. Bunların nesne karşılıkları sarmalayıcı sınıflardır; autoboxing kullanım kolaylığı sağlasa da ek nesne üretimi, null davranışı ve karşılaştırma semantiği göz ardı edilmemelidir.
Java'da tüm argüman aktarımı değer ile yapılır. Bir nesne “referansla geçirilmez”; nesneyi gösteren referansın değeri kopyalanır. Metot bu referans üzerinden nesnenin durumunu değiştirebilir, fakat çağıranın referans değişkenini başka bir nesneye bağlayamaz.
Sayısal dönüşümlerde genişletici dönüşümler çoğunlukla örtük, daraltıcı dönüşümler açık cast gerektirir. Tam sayı taşması varsayılan olarak istisna üretmez; kritik hesaplarda Math.addExact, Math.multiplyExact gibi denetimli işlemler veya uygun yüksek kesinlikli türler kullanılabilir. Parasal hesaplarda ikili kayan nokta yerine gereken durumda BigDecimal tercih edilir; BigDecimal ölçeği ve yuvarlama kuralı da iş sözleşmesinin parçasıdır.
String değişmezdir. Birleştirme işlemlerinin derleyici ve çalışma zamanı tarafından nasıl optimize edildiği bağlama göre değişse de döngü içinde çok sayıda dinamik birleştirme için StringBuilder daha öngörülebilir bir seçimdir. == referans kimliğini, equals ise sınıfın tanımladığı mantıksal eşitliği sınar.
2.1. Değişkenler, Sabitler, Operatörler ve Dönüşümler
Bir değişken kullanılmadan önce türüyle tanımlanır. Yerel değişkenler otomatik olarak başlangıç değeri almaz; kesin atama kuralları derleyici tarafından denetlenir. Nesne alanları ise türlerinin varsayılan değerleriyle başlatılır. final bir değişkene yeniden atamayı sınırlar; nesnenin kendisini otomatik olarak değişmez yapmaz.
Aritmetik işlemlerde tam sayı bölmesi, taşma ve tür yükseltme kuralları önemlidir. 5 / 2 sonucu 2, 5.0 / 2 sonucu 2.5 olur. % kalan işlemini üretir. ++ ve -- işleçlerinin prefix/postfix farkı, ifadenin değerinin aynı deyimde kullanıldığı durumda görünür hale gelir; okunabilirlik açısından yan etkili karmaşık ifadelerden kaçınmak daha güvenlidir.
public class TurVeDonusum {
public static void main(String[] args) {
int adet = 7;
double birimFiyat = 12.50;
double toplam = adet * birimFiyat;
int yuvarlanmis = (int) Math.round(toplam);
int guvenliToplam = Math.addExact(1_500_000_000, 500_000_000);
System.out.println(toplam);
System.out.println(yuvarlanmis);
System.out.println(guvenliToplam);
}
}2.2. Konsol G/Ç ve Scanner
System.out.print satır sonu eklemez; System.out.println ekler. Konsoldan biçimlendirilmiş veri okumada Scanner öğretici ve pratik bir araçtır. nextInt() gibi token tabanlı okumalar ile nextLine() satır tabanlı okuma farklı davranır; ikisini birlikte kullanırken satır sonunun tamponda kalabileceği bilinmelidir.
import java.util.Scanner;
public class KonsolGiris {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(System.in)) {
System.out.print("Ad: ");
String ad = scanner.nextLine().trim();
System.out.print("Birinci sayı: ");
double a = Double.parseDouble(scanner.nextLine());
System.out.print("İkinci sayı: ");
double b = Double.parseDouble(scanner.nextLine());
System.out.printf("%s için toplam = %.2f%n", ad, a + b);
}
}
}2.3. String: İçerik ile Kimliği Ayırmak
String bir temel tür değil sınıftır ve değişmezdir. length, charAt, substring, indexOf, startsWith, endsWith, replace, trim/strip, toLowerCase ve toUpperCase gibi işlemler yeni sonuçlar üretir. Metin karşılaştırmasında içerik için equals/equalsIgnoreCase, sözlüksel sıralama için compareTo kullanılır; == referans kimliğini sınar.
public class MetinIsleme {
public static void main(String[] args) {
String ham = " Java Programlama ";
String temiz = ham.strip();
String kucuk = temiz.toLowerCase();
StringBuilder builder = new StringBuilder(32);
builder.append(kucuk).append(" | uzunluk=").append(temiz.length());
System.out.println(builder);
System.out.println("java programlama".equalsIgnoreCase(temiz));
}
}Ünite 3: Karar Yapıları ve Döngüler
Kontrol akışı bir algoritmanın hangi deyimleri hangi sırada çalıştıracağını belirler. if/else genel koşullar, switch ayrık değer kümeleri, koşullu işleç (?:) ise kısa değer seçimi için kullanılabilir. Modern switch expression, her dalın bir değer üretmesini sağlayarak bazı hata sınıflarını azaltır.
public class KararYapisi {
static String harfNotu(int puan) {
if (puan < 0 || puan > 100) {
throw new IllegalArgumentException("Puan 0..100 aralığında olmalı");
}
int grup = puan / 10;
return switch (grup) {
case 10, 9 -> "A";
case 8 -> "B";
case 7 -> "C";
case 6 -> "D";
default -> "F";
};
}
public static void main(String[] args) {
System.out.println(harfNotu(86));
}
}while koşulu başta, do-while sonda denetler. En az bir kez çalışması gereken giriş döngülerinde do-while anlamlı olabilir. for başlangıç, koşul ve güncellemeyi aynı yapıda toplar; enhanced for diziler ve Iterable yapıları üzerinde indeks gerektirmeyen dolaşım sağlar. break döngüyü sonlandırır, continue bir sonraki yinelemeye geçer.
Sentinel değeriyle veri toplama, giriş sayısının önceden bilinmediği klasik bir akıştır:
import java.util.Scanner;
public class SentinelOrtalama {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(System.in)) {
long toplam = 0;
int adet = 0;
while (true) {
int deger = scanner.nextInt();
if (deger == -999) {
break;
}
toplam += deger;
adet++;
}
if (adet > 0) {
System.out.println((double) toplam / adet);
}
}
}
}Rastgele sayı üretilirken test edilebilirlik önemlidir. Basit tek-iş parçacığı örneklerinde Random, eşzamanlı kodda ThreadLocalRandom, güvenlik amacıyla öngörülemez değer gerektiğinde SecureRandom farklı ihtiyaçlara karşılık gelir; Math.random() her kullanım için evrensel tercih değildir.
Karar yapısı veya döngü tasarlanırken geçerli girişlerin kümesi, sınır değerler ve değişmezin korunması kontrol edilir. Bir döngünün sonlanacağı, her adımda sonlanma ölçüsünün uygun yönde değişmesiyle gerekçelendirilmelidir. Koşul sırasının değişmesi yan etkili ifadelerde farklı sonuç doğurabilir; kısa devre değerlendirmesi bu nedenle semantik bir ayrıntıdır. Birim testlerde boş koleksiyon, tek eleman ve en büyük beklenen veri boyutu mutlaka düşünülmelidir.
Ünite 4: Metotlar, Static, Math ve Overloading
Metot bir davranışı adlandırır, parametrelerle veri alabilir ve void değilse sonuç döndürebilir. Java'da argümanlar daima değer ile aktarılır. Bir nesne parametresinde kopyalanan değer referanstır; bu nedenle metot nesnenin durumunu değiştirebilir fakat çağıranın değişkenini başka nesneye yeniden bağlayamaz.
public class MetotOrnegi {
static int maksimum(int a, int b) {
return a >= b ? a : b;
}
static double ortalama(int... degerler) {
if (degerler.length == 0) {
throw new IllegalArgumentException("En az bir değer gerekli");
}
long toplam = 0;
for (int deger : degerler) {
toplam += deger;
}
return (double) toplam / degerler.length;
}
public static void main(String[] args) {
System.out.println(maksimum(7, 11));
System.out.println(ortalama(10, 20, 30, 40));
}
}static üye sınıfa aittir; örnek üyeye erişmek için nesne gerekir. Math.sqrt, Math.pow, Math.abs, Math.min ve Math.max gibi yardımcılar statik metotlardır. Sarmalayıcı sınıflar (Integer, Long, Double, Character vb.) primitive değerleri nesne API'leriyle buluşturur. Integer.parseInt("42") gibi dönüşümler metin girdisini sayıya çevirmede yaygındır.
Overloading aynı metot adını farklı parametre listeleriyle kullanır ve derleme zamanında çözülür:
public class OverloadOrnegi {
static long alan(int kenar) {
return (long) kenar * kenar;
}
static long alan(int genislik, int yukseklik) {
return (long) genislik * yukseklik;
}
static double alan(double yaricap) {
return Math.PI * yaricap * yaricap;
}
public static void main(String[] args) {
System.out.println(alan(5));
System.out.println(alan(4, 7));
System.out.println(alan(2.5));
}
}Ünite 5: Sınıflar ve Nesne Yönelimli Programlama
Java sınıfları alan, metot, kurucu, iç içe tür ve başlatma blokları içerebilir. Nesne yönelimli tasarımın dört yaygın kavramı soyutlama, kapsülleme, kalıtım ve çok biçimliliktir; ancak iyi tasarım bunları mekanik olarak kullanmak değil, değişim sınırlarını doğru kurmaktır.
Erişim denetimi private, paket erişimi, protected ve public düzeyleriyle yapılır. En geniş erişimi varsayılan tercih olarak kullanmak kapsüllemeyi zayıflatır. Değişmez nesnelerde durum kurucuda doğrulanır, dışarıya değiştirilebilir iç temsil verilmez ve nesne yaşamı boyunca gözlenebilir durum değiştirilmez.
Metot overloading aynı adın farklı parametre listeleriyle derleme zamanında çözümlenmesidir. Overriding ise alt sınıfın üst sınıftan gelen örnek metodun davranışını değiştirmesidir ve çalışma zamanı çok biçimliliğinin temelidir. static, private ve kurucuların davranışı overriding ile karıştırılmamalıdır.
Soyut sınıf durum ve gerçekleşmiş davranış taşıyabilir. Arayüz bir sözleşme tanımlar; modern Java arayüzleri default, static ve özel yardımcı metotlar da içerebilir. Bir sınıf tek bir sınıftan kalıtım alırken birden çok arayüzü gerçekleştirebilir. Kalıtım yalnız “is-a” ilişkisi gerçekten korunuyorsa kullanılmalı; davranışın bileşimle kurulabildiği durumlarda composition çoğu zaman daha düşük bağımlılık üretir.
record veri taşıyan türlerde bileşenler, erişim metotları, equals, hashCode ve toString için standart bir model sağlar. enum yalnız sayısal sabit değildir; alan ve metot içerebilen sınıf benzeri bir türdür. Sealed sınıflar bir tür hiyerarşisinin hangi alt türlere izin verdiğini dil düzeyinde sınırlar.
5.1. Kapsülleme, Kurucular ve This
Kaynak sunumdaki public/private, getter/setter ve kurucu örneklerinin kalıcı fikri şudur: sınıfın iç durumu doğrudan alan erişimiyle değil, sınıfın koruduğu invariants üzerinden yönetilmelidir. Setter yazmak kapsülleme için zorunlu değildir; değişmez veya kontrollü değişen sınıflarda yalnız gerekli operasyonların açılması daha güçlü bir sözleşme verir.
public class HesapOrnegi {
static final class BankaHesabi {
private final String iban;
private long bakiyeKurus;
BankaHesabi(String iban, long ilkBakiyeKurus) {
if (iban == null || iban.isBlank()) {
throw new IllegalArgumentException("IBAN boş olamaz");
}
if (ilkBakiyeKurus < 0) {
throw new IllegalArgumentException("Bakiye negatif olamaz");
}
this.iban = iban;
this.bakiyeKurus = ilkBakiyeKurus;
}
void yatir(long tutarKurus) {
if (tutarKurus <= 0) {
throw new IllegalArgumentException("Tutar pozitif olmalı");
}
bakiyeKurus = Math.addExact(bakiyeKurus, tutarKurus);
}
long bakiyeKurus() {
return bakiyeKurus;
}
String iban() {
return iban;
}
}
public static void main(String[] args) {
BankaHesabi hesap = new BankaHesabi("TR0001", 10_000);
hesap.yatir(2_500);
System.out.println(hesap.iban() + " " + hesap.bakiyeKurus());
}
}Bir sınıfta hiç kurucu yazılmazsa derleyici parametresiz varsayılan kurucuyu üretir. Programcı herhangi bir kurucu tanımladığında bu otomatik parametresiz kurucu artık üretilmez. this(...) aynı sınıftaki başka bir kurucuyu, super(...) üst sınıf kurucusunu çağırır ve kurucu gövdesinin ilk deyimi olmalıdır.
5.2. Kalıtım, Soyut Sınıf, Arayüz ve Çok Biçimlilik
Kalıtımda extends, arayüz gerçekleştirmede implements kullanılır. Overriding sırasında parametre sözleşmesi korunur ve dinamik dispatch, çağrılacak örnek metodu çalışma zamanındaki gerçek nesne türüne göre seçer. @Override anotasyonu derleyicinin niyet denetimine yardım eder.
public class CokBicimlilikOrnegi {
interface EnerjiTuketir {
double tuketimKWh(double km);
}
static abstract class Tasit {
private final String marka;
Tasit(String marka) {
this.marka = marka;
}
String marka() {
return marka;
}
abstract String tur();
}
static final class ElektrikliOtomobil extends Tasit implements EnerjiTuketir {
ElektrikliOtomobil(String marka) {
super(marka);
}
@Override
String tur() {
return "Elektrikli otomobil";
}
@Override
public double tuketimKWh(double km) {
return km * 0.17;
}
}
public static void main(String[] args) {
Tasit tasit = new ElektrikliOtomobil("Örnek");
System.out.println(tasit.marka() + " - " + tasit.tur());
}
}Üst sınıfın private alanları alt sınıfa görünmez; erişim sınıfın sunduğu sözleşme üzerinden yapılmalıdır. final metot override edilemez, final sınıftan kalıtım alınamaz. Kalıtım hiyerarşisini yalnız kod tekrarını azaltmak için kurmak kırılgan tasarım üretir; substitutability korunmuyorsa composition tercih edilmelidir.
Ünite 6: Object Sözleşmesi, Eşitlik ve Kimlik
Object, sınıf hiyerarşisinin ortak temelidir. equals mantıksal eşitliği tanımlıyorsa hashCode ile sözleşmesi korunmalıdır: eşit iki nesnenin aynı hash kodunu üretmesi gerekir. Tersi zorunlu değildir; hash çakışmaları mümkündür.
referans a ----+
+----> nesne X
referans b ----+
== -> referans kimliği
.equals(...) -> tanımlanmış mantıksal eşitlik
.hashCode() -> eşit nesneler için uyumlu karma değeriHash tabanlı koleksiyonda anahtar olarak kullanılan nesnenin eşitliği veya hash değeri koleksiyondayken değişirse kayıt erişilemezmiş gibi görünebilir. Bu nedenle anahtarların değişmez olması veya en azından eşitlikte kullanılan alanların değiştirilmemesi güvenli yaklaşımdır.
toString tanılama açısından yararlıdır fakat kalıcı veri biçimi olarak görülmemelidir. Parola, token, kimlik numarası ve kişisel veri gibi hassas alanlar otomatik toString, log veya hata mesajlarına taşınmamalıdır.
String değişmezdir. Çok sayıda ardışık metin birleştirmede StringBuilder tek iş parçacığı içinde değiştirilebilir bir tampon sağlar. StringBuffer benzer tarihsel API'yi senkronize metotlarla sunar; senkronizasyon gerekmeyen yeni kodda yalnız iş parçacığı-safe olduğu için otomatik tercih edilmemelidir.
6.1. Equals, hashCode ve toString için Somut Örnek
== nesne kimliğiyle, equals ise tanımlanan mantıksal eşitlikle ilişkilidir. Üretim kodunda mantıksal eşitlik tanımlanıyorsa hashCode da aynı alanlarla uyumlu olmalıdır.
import java.util.Objects;
public final class Kesir {
private final int pay;
private final int payda;
public Kesir(int pay, int payda) {
if (payda == 0) {
throw new IllegalArgumentException("Payda sıfır olamaz");
}
int gcd = gcd(Math.abs(pay), Math.abs(payda));
int isaret = payda < 0 ? -1 : 1;
this.pay = isaret * pay / gcd;
this.payda = isaret * payda / gcd;
}
private static int gcd(int a, int b) {
while (b != 0) {
int t = a % b;
a = b;
b = t;
}
return a == 0 ? 1 : a;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Kesir other)) return false;
return pay == other.pay && payda == other.payda;
}
@Override
public int hashCode() {
return Objects.hash(pay, payda);
}
@Override
public String toString() {
return pay + "/" + payda;
}
}Ünite 7: İstisnalar ve Kaynak Yönetimi
Java istisnaları Throwable hiyerarşisi altında Error ve İstisna ana dallarına ayrılır. Checked istisna ile unchecked istisna arasındaki ayrım derleyicinin yakalama/bildirme zorunluluğuyla ilgilidir; “checked her zaman kurtarılabilir, unchecked her zaman programlama hatasıdır” gibi genellemeler güvenilir değildir.
İstisna sınırı katman sorumluluğuna göre seçilmelidir. Düşük seviyeli bir istisnayı anlamsız bir genel istisnaya çevirmek kök nedeni kaybettirir; buna karşılık alt sistem ayrıntısını dış API'ye sızdırmak da soyutlamayı bozar. Yeniden fırlatmada cause zincirinin korunması hata teşhisini kolaylaştırır.
Dosya, soket ve JDBC nesnelerinde try-with-resources, AutoCloseable kaynaklarını istisna olsa bile kapatır. finally hâlâ genel temizleme davranışı için kullanılabilir; ancak kaynak yönetiminde try-with-resources daha güvenlidir.
final, finally ve tarihsel finalize() kavramları farklıdır. finalize() güvenilir bir kaynak kapatma mekanizması değildir ve modern Java'da kullanımdan kaldırılmış yaklaşım olarak ele alınmalıdır. Kaynak yaşamı deterministik olarak kapatılmalıdır.
Sık karşılaşılan çalışma zamanı istisnalarının adları hatanın kök nedenini tarif eder: NullPointerİstisna null referans kullanımını, Arithmeticİstisna belirli aritmetik hataları, NumberFormatİstisna geçersiz metin-sayı dönüşümünü, ClassCastİstisna geçersiz tür dönüşümünü ve ArrayIndexOutOfBoundsİstisna dizi sınırı ihlalini gösterebilir. Interruptedİstisna ise yalnız “bir hata” değildir; iş parçacığının kesilme isteğini taşıdığı için iptal ve kapanış protokolünün bir parçası olarak ele alınmalıdır. Runtimeİstisna ailesini topluca yakalayıp yutmak bu ayrımı kaybettirir.
7.1. Özel İstisna ve Çoklu Yakalama
İstisnalar yalnız beklenmeyen teknik hataları değil, API sözleşmesinin ihlalini de temsil edebilir. İstisna türü anlamlı seçilmeli ve mümkün olduğunca doğru soyutlama sınırında ele alınmalıdır.
public class IstisnaOrnegi {
static final class GecersizBolmeException extends Exception {
GecersizBolmeException(String mesaj) {
super(mesaj);
}
}
static int bol(int bolunen, int bolen) throws GecersizBolmeException {
if (bolen == 0) {
throw new GecersizBolmeException("Bölen sıfır olamaz");
}
return bolunen / bolen;
}
public static void main(String[] args) {
try {
System.out.println(bol(10, 2));
} catch (GecersizBolmeException e) {
System.err.println(e.getMessage());
}
}
}Bir try birden çok catch ile kullanılabilir; alt türler üst türlerden önce yakalanmalıdır. Multi-catch (catch (IOİstisna | SQLİstisna e)) ortak işlem uygulanacak ilgisiz istisna türlerinde tekrarları azaltabilir. İç içe try-catch yapısı mümkündür ancak kontrol akışını zorlaştırıyorsa metot sınırlarını yeniden tasarlamak daha temizdir.
Ünite 8: Genel Türler ve Tür Güvenliği
Genel türler, koleksiyon ve API'lerde çalışma zamanı cast ihtiyacını azaltıp tür hatalarını derleme zamanına taşır. Java genel türler gerçekleştirmesi büyük ölçüde type erasure kullanır; bu nedenle List<String> ile List<Integer> çalışma zamanında ayrı reified türler değildir.
? extends T üretici gibi okunan yapılarda güvenli okuma, ? super T tüketici gibi kullanılan yapılarda güvenli yazma sağlar. “PECS: producer extends, consumer super” sezgisi yararlıdır; ancak API'nin gerçek varyans gereksinimi ayrıca değerlendirilmelidir.
Raw type kullanımı eski kodla uyumluluk dışında kaçınılması gereken bir tür güvenliği açığıdır. Generic varargs, unchecked cast ve reflection ile genel türler bir araya geldiğinde heap pollution riski ortaya çıkabilir.
8.1. Diziler ve Çok Boyutlu Diziler
Dizi sabit uzunluklu, aynı bileşen türünden değerleri indeksle erişilebilir biçimde tutar. length alanı eleman sayısını verir. Bir dizi değişkeni nesne referansı taşıdığı için b = a elemanları kopyalamaz; iki değişkeni aynı diziye bağlar. Gerçek kopya için Arrays.copyOf, clone veya açık eleman kopyalama gerekir.
import java.util.Arrays;
public class DiziOrnegi {
static double ortalama(int[] degerler) {
if (degerler.length == 0) {
return Double.NaN;
}
long toplam = 0;
for (int deger : degerler) {
toplam += deger;
}
return (double) toplam / degerler.length;
}
public static void main(String[] args) {
int[] notlar = {72, 91, 64, 88, 79};
int[] kopya = Arrays.copyOf(notlar, notlar.length);
Arrays.sort(kopya);
int[][] matris = {
{1, 2, 3},
{4, 5, 6}
};
System.out.println(ortalama(notlar));
System.out.println(Arrays.toString(kopya));
System.out.println(matris[1][2]);
}
}Java'da int[][] gibi çok boyutlu görünen diziler gerçekte dizi referansları dizisidir; satırların aynı uzunlukta olması zorunlu değildir. Bu özellik düzensiz (jagged) yapı kurmaya izin verir. Büyük sayısal matrislerde ise nesne/dizi düzeni, locality ve bellek maliyeti ayrıca değerlendirilmelidir.
8.2. Basit Generic Metot Örneği
import java.util.List;
public class GenericOrnegi {
static <T> T ilk(List<T> liste) {
if (liste.isEmpty()) {
throw new IllegalArgumentException("Liste boş");
}
return liste.get(0);
}
public static void main(String[] args) {
System.out.println(ilk(List.of("Java", "JVM")));
System.out.println(ilk(List.of(10, 20, 30)));
}
}Ünite 9: Koleksiyonlar
Collection hiyerarşisi liste, küme ve kuyruk türlerini; Map ise anahtar-değer yapısını temsil eder. Veri yapısı seçiminde yalnız API kolaylığı değil, işlem karmaşıklığı, bellek yerleşimi, sıralama gereksinimi ve eşzamanlı erişim biçimi dikkate alınır.
ArrayList: rastgele erişim güçlü, sona ekleme amortize sabit zamanlı; ortadan ekleme/silme eleman kaydırabilir.LinkedList: çift bağlı listedir veDequesözleşmesini de uygular; indeksle erişim doğrusal maliyetlidir.HashSet/HashMap: doğru hash dağılımıyla genel amaçlı üyelik ve anahtar erişimi için uygundur; mantıksal sıralama garantisi vermez.LinkedHashMap: ekleme veya erişim sırasını koruyabilir.TreeSet/TreeMap: sıralı ağaç yapısıyla karşılaştırmaya dayalı düzen sağlar.ArrayDeque: çoğu yığın ve çift uçlu kuyruk kullanımında eskiStacksınıfına göre daha uygun genel tercihtir.PriorityQueue: öncelik sırasına göre baş elemanı sunar; bütün elemanların her an sıralı bir liste olarak tutulduğu varsayılmamalıdır.
HashMap iş parçacığı-safe değildir. Hashtable tarihsel senkronize bir sınıftır; çok iş parçacıklı yeni tasarımlarda erişim modeline göre ConcurrentHashMap veya dış senkronizasyon değerlendirilmelidir. ConcurrentHashMap, bütün tabloyu tek kilitle yönetiyormuş gibi düşünülmemelidir.
Iterator davranışı koleksiyon türüne göre değişebilir. Iterator tek yönlü dolaşımı, ListIterator liste üzerinde iki yönlü dolaşım ve konumsal güncellemeleri destekler. Birçok klasik koleksiyon fail-fast kontrol uygular; eşzamanlı koleksiyonların bazıları weakly consistent dolaşım sağlar. ConcurrentModificationİstisna bir eşzamanlılık güvenliği mekanizması değildir.
Daha özel koleksiyonlar da gerçek kullanım amacıyla seçilmelidir: LinkedHashSet hash tabanlı tekilliği öngörülebilir dolaşım sırasıyla birleştirir; EnumSet enum değerleri için kompakt bir küme gerçekler; NavigableMap sıralı anahtar uzayında alt/üst komşu sorgularını destekler; IdentityHashMap normal equals yerine referans kimliği kullanır ve genel amaçlı map yerine özel kimlik-semantikli problemler içindir. CopyOnWriteArrayList, okumanın çok, yazmanın seyrek olduğu ve kopyalama maliyetinin kabul edilebildiği eşzamanlı senaryolarda anlamlıdır; sık yazılan büyük listelerde pahalıdır.
9.1. Koleksiyon Seçimini Kod Üzerinde Görmek
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class KoleksiyonOrnegi {
public static void main(String[] args) {
List<String> diller = List.of("Java", "C", "C++", "C#");
Map<String, Integer> uzunluk = new HashMap<>();
for (String dil : diller) {
uzunluk.put(dil, dil.length());
}
ArrayDeque<String> kuyruk = new ArrayDeque<>();
kuyruk.addLast("birinci");
kuyruk.addLast("ikinci");
System.out.println(uzunluk.get("Java"));
System.out.println(kuyruk.removeFirst());
}
}Ünite 10: Lambda, Fonksiyonel Arayüzler ve Stream API
Lambda ifadeleri tek soyut metoda sahip fonksiyonel arayüzlere hedeflenir. @FunctionalInterface anotasyonu bu niyeti derleyici tarafından denetlenebilir kılar; anotasyon bulunmasa da tek soyut metot şartını sağlayan arayüz fonksiyonel olabilir. Predicate, Function, Consumer, Supplier ve bunların ilkel tür özelleştirmeleri yaygın örneklerdir. Metot referansı, uygun imzalı mevcut metodun fonksiyonel arayüz bağlamında kullanılmasını sağlar.
Stream bir veri deposu değildir; bir kaynak üzerinde ara ve son işlemlerden oluşan değerlendirme hattıdır. Ara işlemler çoğunlukla lazy'dir. map, filter, flatMap, sorted, distinct gibi işlemler son işlem çağrılıncaya kadar çalışmayabilir.
Parallel stream, CPU çekirdeği bulunduğu için otomatik olarak daha hızlı değildir. İş bölme maliyeti, ortak havuz kullanımı, veri miktarı, işlem başına maliyet, yan etkiler ve sıralama gereksinimleri ölçülmelidir. G/Ç ağırlıklı veya ortak kaynakta kilitlenen işlemler için kör paralelleştirme gecikmeyi artırabilir.
10.1. Stream Örneği
import java.util.List;
public class StreamOrnegi {
public static void main(String[] args) {
List<Integer> degerler = List.of(3, 8, 11, 14, 21, 30);
int toplam = degerler.stream()
.filter(x -> x % 2 == 0)
.mapToInt(Integer::intValue)
.sum();
System.out.println(toplam);
}
}Bu kodun aynı işini basit bir for döngüsü de yapabilir. Stream kullanımının gerekçesi yalnız daha kısa görünmesi değil, dönüşüm zincirinin veri akışı olarak okunabilir olmasıdır; sıcak performans yolunda tahsis, boxing ve paralellik maliyetleri ayrıca ölçülmelidir.
Ünite 11: Giriş/Çıkış, NIO ve Serileştirme
java.io akış temelli klasik API'leri; java.nio ve java.nio.file buffer, channel, charset ve modern dosya sistemi erişimlerini sağlar. FileInputStream/FileOutputStream bayt, FileReader/FileWriter karakter akışına tarihsel örneklerdir; BufferedReader küçük okumaları tamponlayabilir, PrintStream ve PrintWriter biçimlendirilmiş metin çıktısı sağlar. Bu sınıfların adı karakter kodlaması problemini çözmez; metin okurken/yazarken charset açıkça yönetilmelidir. Bayt dizisini varsayılan platform charset'i ile metne çevirmek farklı ortamlarda sessiz veri bozulmasına yol açabilir.
XML işleme tarafında SAX olay/akış tabanlı ayrıştırma yaklaşımıdır ve tüm belge ağacını bellekte tutmak zorunda değildir. DOM ise ağaç modeli sağlar. Seçim belge boyutu, rastgele erişim gereksinimi ve işleme biçimine göre yapılmalıdır.
Path ve Files, dosya sistemi işlemleri için temel modern API'lerdir. Büyük dosyada bütün içeriği belleğe almak yerine akış, channel veya parça tabanlı işleme daha güvenli olabilir. Dosya yazımında kalıcı çıktının görünürlüğü gerekiyorsa geçici dosya + atomik taşıma gibi yayınlama desenleri düşünülmelidir.
Java yerleşik nesne serileştirmesi tarihsel sistemlerde bulunur; güvenilmeyen verinin deserialization işlemi ciddi güvenlik riski doğurabilir. Yeni dış veri sözleşmelerinde açık şema ve sınırlandırılmış veri biçimleri tercih edilmelidir.
11.1. Metin Dosyası okuma/yazma: Klasik Akımdan Modern Files API'sine
Sunum PrintWriter, BufferedReader, FileReader ve File sınıflarıyla metin dosyası G/Ç'sini öğretir. Bu sınıflar hâlâ geçerlidir. Modern kodda Path ve Files API'leri dosya sistemi işlemlerini daha açık bir modelle sunar; karakter kümesi de örtük platform varsayımına bırakılmamalıdır.
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class DosyaOrnegi {
public static void main(String[] args) throws Exception {
Path yol = Path.of("ornek.txt");
try (BufferedWriter writer = Files.newBufferedWriter(yol, StandardCharsets.UTF_8)) {
writer.write("Java");
writer.newLine();
writer.write("JVM");
}
try (BufferedReader reader = Files.newBufferedReader(yol, StandardCharsets.UTF_8)) {
String satir;
while ((satir = reader.readLine()) != null) {
System.out.println(satir);
}
}
}
}File tarihsel API'de bir yol/dosya soyutlamasıdır; Path/Files daha zengin hata ve dosya sistemi semantiği sunar. İkili veri için InputStream/OutputStream, karakter verisi için Reader/Writer ayrımı temel kalır. Büyük dosyalarda tüm içeriği tek seferde belleğe almak yerine akış, kanal veya parça tabanlı yaklaşım gerekir.
Ünite 12: Annotation, Reflection ve Modüller
Annotation kaynak, class dosyası veya çalışma zamanı saklama politikasına sahip üst veridir. Framework'ler annotation'ları yapılandırma, eşleme ve doğrulama için yoğun kullanabilir. Annotation'ın varlığı tek başına davranış üretmez; onu işleyen derleyici, annotation processor, framework veya reflection kodu gerekir.
Reflection çalışma zamanında tür, alan, metot ve annotation bilgisine erişir. Esneklik sağlasa da statik tür güvenliği, kapsülleme, performans ve native/AOT uyumluluğu üzerinde maliyet oluşturabilir. Çalışma zamanı keşfi yerine açık kayıt veya üretim zamanı üst verisi bazı sistemlerde daha öngörülebilirdir.
Java Platform Module System, module-info.java üzerinden modül bağımlılıklarını ve dışarı açılan paketleri tanımlar. Modül, package ve JAR aynı kavram değildir.
@Deprecated, bir API'nin kullanımının artık önerilmediğini ve çoğu durumda alternatif/migrasyon bilgisinin incelenmesi gerektiğini belirtir; API'nin o anda çalışmadığı anlamına gelmez. Lombok gibi harici araçlar derleme zamanı kod üretimi yapabilir; örneğin @SuperBuilder Lombok'a özgüdür, Java veya Jakarta standardı değildir. Bu tür anotasyonların kaynağı ve ürettiği kod özellikle IDE, annotation processing ve build zincirinde açıkça bilinmelidir.
Ünite 13: Eşzamanlılık ve Java Bellek Modeli
Thread oluşturmak yalnız paralellik sağlamak değildir; görünürlük, atomiklik ve sıralama problemlerini de getirir. Java Bellek Modeli, bir iş parçacığındaki yazmanın başka bir iş parçacığı tarafından hangi koşullarda görüleceğini tanımlar. happens-before ilişkisi bu modelin merkezindedir.
Thread A Thread B
-------- --------
yazma x=1
|
+-- synchronization / happens-before --> okuma x
|
v
görünürlük garantisi
sıra + görünürlük + atomiklik birbirinden farklıdırsynchronized karşılıklı dışlama yanında monitör giriş/çıkışları üzerinden bellek görünürlüğü sağlar. volatile bir değişkenin okumaları ve yazmaları arasında görünürlük/sıralama garantileri getirir; fakat count++ gibi bileşik read-modify-write işlemini atomik yapmaz. Atomik sayaçta AtomicInteger, yüksek çekişmeli istatistik sayaçlarında LongAdder gibi araçlar uygun olabilir.
Lock ailesi zaman aşımı, interruptible kilit alma ve birden çok Condition gibi ek yetenekler sunar. Kilit sayısı arttıkça global sıra kuralı, minimum kilit süresi ve bloklayan G/Ç'nin kilit altında tutulmaması deadkilit riskini azaltır.
ExecutorService, görev sunma ile iş parçacığı yaşam döngüsünü ayırır. Havuz boyutunu “CPU sayısı kadar” veya “ne kadar büyük o kadar iyi” biçiminde tek formülle belirlemek doğru değildir; görevlerin CPU/G/Ç oranı, dış kaynak kapasiteleri, kuyruk ve hedef gecikme birlikte değerlendirilir.
CompletableFuture asenkron iş akışlarını bileştirebilir. Hangi executor üzerinde çalıştığı, istisna propagation ve timeout davranışı açıkça yönetilmelidir. Virtual iş parçacıkları çok sayıda bloklayan görevin iş parçacığı başına düşük maliyetle modellenmesini kolaylaştırır; ancak veritabanı bağlantısı, uzak servis kotası veya kilit gibi gerçek kıt kaynakların kapasitesini artırmaz.
Structured concurrency gibi bazı yeni eşzamanlılık API'leri Java sürümüne göre preview olabilir. Preview özelliği kalıcı API sözleşmesi gibi kullanılmamalı; kullanılan JDK sürümünün belirtimi kontrol edilmelidir.
13.1. Atomik Sayaç ve Virtual İş Parçacığı Örneği
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
public class EszamanlilikOrnegi {
public static void main(String[] args) throws Exception {
AtomicInteger sayac = new AtomicInteger();
List<Future<?>> gorevler = new ArrayList<>();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000; i++) {
gorevler.add(executor.submit(sayac::incrementAndGet));
}
for (Future<?> gorev : gorevler) {
gorev.get();
}
}
System.out.println(sayac.get());
}
}Örnekte atomik sayaç veri yarışını önler. Virtual iş parçacığı, görevin temsil maliyetini düşürür; aynı anda 1.000 veritabanı sorgusunun güvenli olduğu anlamına gelmez. Dış kaynak kapasitesi ayrıca sınırlandırılmalıdır.
13.2. Virtual Thread ile Kapasite Kontrolünü Ayırmak
Virtual thread, bloklayan iş yüklerinde her görev için platform thread ayırma maliyetini azaltır; fakat paylaşılan kaynağın kapasitesini büyütmez. Bu ayrım Java eşzamanlılığında önemlidir çünkü çok sayıda görevi başlatabilmek ile çok sayıda görevin aynı anda veritabanına, uzak servise veya diske girebilmesi aynı garanti değildir.
çok sayıda virtual thread
|
v
concurrency sınırı
|
v
kıt dış kaynakBu sınır için Semaphore gibi basit bir izin mekanizması kullanılabilir. Thread yaşam döngüsü ile kaynak izni farklı kavramlardır: görev virtual thread üzerinde bekleyebilir, ancak yalnız izin aldıktan sonra pahalı kaynağa girer. Böylece yürütme modeli okunabilir bloklayan kod olarak kalırken kaynak tüketimi açık bir bütçeye bağlanır.
Burada Java Memory Model ile kapasite kontrolü de karıştırılmamalıdır. volatile, kilit veya atomik sınıflar görünürlük ve atomiklik problemi çözer; semaphore veya bağlantı havuzu ise aynı anda kaç işin bir sınıra gireceğini düzenler. Birincisi doğruluk, ikincisi kaynak yönetimi problemidir; bazı tasarımlarda ikisine birden ihtiyaç vardır.
İptal ve kesme davranışı da sözleşmenin parçasıdır. İzin bekleyen veya bloklayan görevin zaman aşımında nasıl sonlandırıldığı bilinmiyorsa yük altında kaynak sızıntısı ve uzun kuyruk oluşabilir. Bu nedenle concurrency tasarımı yalnız “hangi thread modeli?” sorusuyla değil, “hangi kaynak sınırlı, bekleme ne kadar, iptal nasıl yayılıyor?” sorularıyla tamamlanmalıdır. Daha geniş performans bağlamı Java Tabanlı Veri Sistemlerinde Yüksek Başarım notunda ele alınır.
Kapasite sınırı koyulacak yer, işlemin başlatıldığı thread türü değil pahalı kaynağa giriş noktasıdır. Bir görev veritabanına erişmeden önce izin alır, işlemini tamamladığında veya hata aldığında izni bırakır; zaman aşımı durumunda izin sızıntısı olmamalıdır. Kabul sınırının dışındaki işler sınırsız bellekte sıraya alınırsa semaphore kullanılması tek başına yük koruması sağlamaz. Bekleyen görev sayısı veya bekleme süresi için ayrıca bütçe belirlenmeli ve sistem doygunken açık bir geri dönüş davranışı tanımlanmalıdır.
Sanal iş parçacığı, sınırsız eşzamanlılık izni değildir. Her gelen iş için virtual thread açmak, bekleyen işleri ucuz biçimde temsil edebilir; ancak veritabanı bağlantısı veya harici hizmet gibi sınırlı kaynaklara olan talebi azaltmaz. Bu nedenle görevin başlatılması ile pahalı kaynağa erişim farklı sınırlarla kontrol edilmelidir. Semaphore gibi bir admission-control mekanizması, seçilen concurrency limitinin üzerinde sıraya alma veya reddetme politikasıyla birlikte tasarlanmalıdır; yalnız semaphore eklemek, sınırsız bekleyen iş birikimini önlemez.
Platform-thread pinning tanısı güncel JVM gözlem araçlarıyla yapılmalı; synchronized kullanımının her sürümde aynı pinning davranışını oluşturduğu varsayılmamalıdır. Bekleme süresi azalırken toplam tamamlanan iş sayısı değişmeyebilir. Karar, thread sayısına göre değil CPU doygunluğu, kuyruk derinliği, downstream limitleri ve timeout oranına göre verilmelidir.
İzin muhasebesi için yararlı bir değişmez, boş izin + edinilmiş izin = toplam izin ilişkisidir. Henüz izin edinmemiş bir görevin finally bloğunda koşulsuz release() çağırması toplam kapasiteyi yanlışlıkla büyütebilir. İptal; izin beklerken, izin alındıktan sonra ve kaynak kullanımının ortasında ayrı sınanır. Görev sonlandığında yalnız gerçekten edinilmiş izin bırakılır. Zaman aşımı beklemeyi sınırlasa da alttaki işlemin durduğunu kendiliğinden kanıtlamaz; kaynak kullanımının bitişi ayrıca doğrulanır.
13.3. Java Bellek Modeli ve Eşzamanlı Doğruluk
Çok çekirdekli programlamada doğruluk yalnız veri yarışının bulunmaması değildir. Java'da eşzamanlı koleksiyonun sonuçlarının geçerli bir sıralı geçmişle açıklanabilmesi, blocking veya lock-free ilerleme özelliği, monitor/atomic contention maliyeti ve Java Memory Model kapsamındaki happens-before görünürlüğü birbirinden ayrı doğruluk ve performans boyutlarıdır. Linearizability doğruluk için güçlü bir model sunarken lock-free ve wait-free gibi ilerleme sınıfları, sistemin gecikme davranışının yalnız mutex sayısıyla açıklanamayacağını gösterir.
- happens-before ilişkisini yalnız synchronized anahtar kelimesine indirgememek.
- Atomic sınıfların composite invariantları otomatik olarak korumadığını göstermek.
- Concurrent queue/map seçiminde contention ve progress guarantee ölçmek.
Bir alanın volatile olması, o alanın görünürlük ve sıralama semantiğini değiştirir; iki adımlı güncellemeyi tek atomik işlem yapmaz. count++ ifadesi okuma, toplama ve yazma aşamalarına ayrılabildiği için birden fazla iş parçacığı aynı eski değeri okuyup artışı kaybedebilir. Bağımsız atomik sayaç gerekliyse atomik okuma-değiştirme-yazma sağlayan bir yapı kullanılabilir; birkaç alan birlikte tutarlı kalacaksa tek sayaç atomikliği yeterli değildir. Doğruluk koşulu, paylaşılmış durumun bütün geçişlerini kapsayan bir senkronizasyon protokolüyle tanımlanmalıdır.
13.4. Kesme, İptal ve Kaynak Sahipliği
Java eşzamanlılığında iş parçacığı kesmesi zorlayıcı bir durdurma komutu değildir; bir iptal isteğinin işbirlikçi olarak iletilmesidir. Bloke edici bazı işlemler kesme durumunu algılayıp InterruptedException üretir. Bu istisnanın yakalanıp hiçbir işlem yapılmadan yutulması, üst katmanın iptal talebini görmesini engeller. Kesmenin yeniden iletilmediği bir sınırda Thread.currentThread().interrupt() ile bayrak geri kurulabilir; ancak bu yaklaşım, istisnanın doğrudan üst katmana aktarılmasıyla aynı kontrol akışına sahip değildir. Seçim, metot sözleşmesinde açık olmalıdır.
İptal anında tutulan kaynağın kapanması ayrı bir yükümlülüktür. Bir görev veritabanı bağlantısı, dosya tanıtıcısı veya eşzamanlılık izni edinmişse, normal tamamlanma, hata ve iptal yollarının tümünde sahipliği sonlandırmalıdır. try-with-resources kapanabilir kaynakları leksik kapsamda yönetir; semaphore izni ise aynı kapsamda finally içinde bırakılır. İzin ancak başarılı edinimden sonra serbest bırakılmalıdır. Bu ayrım, yüksek yük altında beklenmedik izin artışını önler.
Virtual thread kullanımı eşzamanlı beklemeyi ucuzlatabilir; ancak aynı anda açılabilecek JDBC bağlantıları, dış hizmet çağrıları veya dosya tanıtıcılarının üst sınırını artırmaz. Örneğin 40 bağlantılı bir havuza karşı 400 sanal iş parçacığının sorgu başlatması, veritabanında 400 paralel işlem yürütüldüğü anlamına gelmez. Kabul kontrolü, bağlantı sınırını dikkate alarak kuyruk birikimi ve iptal gecikmesini ayrıca yönetmelidir.
13.5. Atomik Yayınlama, Bellek Görünürlüğü ve ABA
Java Bellek Modeli açısından doğru yayınlama, nesneye ait alanların başka bir iş parçacığı tarafından hangi happens-before ilişkisi altında gözlenebileceğini belirler. volatile alan veya uygun senkronizasyon, erişilen referansın görünürlüğüne katkı sağlar; ancak birden fazla değişken üzerinde kendiliğinden atomik transaction sağlamaz. AtomicReference ile CAS döngüsü kurulduğunda, karşılaştırma anından sonra gözlenen nesnenin tekrar aynı referansa dönüşmüş olması ara durumların göz ardı edilebileceği anlamına gelmez.
ABA riski referans değiştirme algoritmasının mantıksal sürümüyle ilgilidir; AtomicStampedReference belirli kullanım senaryolarında sürüm bilgisini eşleştirmeye katabilir. Kilitsizliğin ilerleme garantisi, atomik bir türün kullanılmasından değil algoritmanın tüm adımlarından çıkarılır. Çok çekirdekli testlerde görünürlük, doğrusal hale getirilebilirlik, uzun süreli contention ve GC yükü ayrı doğrulama boyutlarıdır.
Ünite 14: JVM Belleği, Çöp Toplama ve JIT
Heap'te ulaşılabilir olmayan nesneler çöp toplama için aday olur; “değişkeni null yapmak nesneyi hemen siler” ifadesi doğru değildir. GC algoritması, heap boyutu, tahsis oranı, canlı veri miktarı ve gecikme hedefi birlikte değerlendirilir.
JVM süreci
|
+-------------+-------------+
| |
iş parçacığına özel paylaşılan
| |
stack / frame heap
PC / native stack nesneler
|
garbage collector
class metadata ----------------> metaspace
hot code ----------------------> JIT -> native codeBellek sızıntısı Java'da da mümkündür: bir nesne artık iş açısından gerekli olmadığı hâlde güçlü referans zinciriyle erişilebilir kalıyorsa GC onu toplayamaz. Sınırsız önbellek, listener kaydı, ThreadLocal yaşamı ve uzun ömürlü koleksiyonlar yaygın nedenlerdir.
JIT optimizasyonları ısınma davranışı oluşturur. Bu nedenle mikro benchmark System.nanoTime() çevresinde birkaç kez metot çağırmaktan ibaret olmamalıdır. Dead-code elimination, constant folding, tiered compilation ve GC etkileri hesaba katılmalı; Java mikro ölçümlerinde JMH gibi bu sorunları modelleyen araçlar kullanılmalıdır.
Eski JVM kaynaklarında görülen PermGen (Permanent Generation) güncel HotSpot bellek düzeninin bir parçası değildir; Java 8 ile sınıf üst verisi için native bellek tabanlı Metaspace yaklaşımına geçilmiştir. Eski PermGen space hata ve ayarlarını güncel JVM'e aynen taşımak sürüm hatasıdır.
Allocation, canlı küme ve GC duraklamasını ayrı gözlemek. Bir JVM sürecinin bellek tüketimi heap kullanımından ibaret değildir: thread stack'leri, metaspace, JIT code cache, direct buffer'lar ve yerel kütüphanelerin ayırdığı alan süreç belleğine katkı verir. Konteyner belleği sınırına ulaşmak, heap grafiğinde yeterli boş alan göründüğünde de mümkündür. -Xmx ayarı ile RSS ya da cgroup bellek tüketimini aynı metrik olarak yorumlamak bu nedenle yanıltıcıdır.
JFR üzerinden allocation örüntüsü, GC olayı ve uygulama evresi ilişkilendirilirken warm-up sonrası kararlı iş yükü tercih edilir. Genç nesilde çok sayıda nesnenin kısa ömürlü olması tek başına bellek sızıntısı kanıtı değildir. Sızıntı hipotezi, tamamlama sonrası canlı kalan nesnelere, tutulan referanslara ve tekrarlanan kararlı döngülerdeki retained-size eğilimine dayanmalıdır. JIT optimizasyonunu sınamak için de derleme/warm-up etkisi ile gerçekten daha az yapılan işi birbirinden ayırmak gerekir.
Ünite 15: JDBC ve Veri Erişimi
JDBC sürücüden bağımsız bir Java veri tabanı API'sidir. Connection, PreparedStatement ve ResultSet temel yapı taşlarıdır. CallableStatement, saklı yordam/fonksiyon çağrılarında kullanılan JDBC sözleşmesidir; ürün bağımlı prosedürel davranışın taşınabilir SQL ile karıştırılmaması gerekir. Parametreli SQL, hem veri türü bağlama hem SQL injection riskini azaltma açısından string birleştirmeden üstündür.
Transaction sınırı iş atomikliğini temsil etmelidir. Auto-commit davranışı bilinmeden yapılan çok adımlı yazmalar kısmi sonuç üretebilir. Bağlantı havuzu bağlantı yaratma maliyetini amorti eder; havuz kapasitesi veritabanının gerçek eşzamanlı işlem kapasitesinden bağımsız düşünülemez.
ORM/JPA, SQL ve transaction bilgisinin yerine geçmez. Hibernate gibi sağlayıcılarda SessionFactory/Session ve HQL gibi sağlayıcıya özgü kavramlar bulunurken JPA taşınabilir tarafta EntityManager, JPQL ve standart eşleme anotasyonlarını tanımlar. @ManyToOne, @OneToMany, @GeneratedValue ve FetchType.LAZY/EAGER gibi ayarlar veri erişim planını etkiler. Persistence context kapandıktan sonra lazy ilişkiye erişim Hibernate tarafında LazyInitializationİstisna gibi hatalar üretebilir; çözüm her ilişkiyi eager yapmak değildir. N+1 sorgu, gereksiz entity yükleme, geniş transaction, hatalı fetch stratejisi ve sınırsız sonuç kümesi uygulama tarafında ciddi maliyet oluşturabilir. Bu konunun performans tarafı Java Tabanlı Veri Sistemlerinde Yüksek Başarım notunda ayrıntılıdır.
Eski JDBC örneklerinde sürücüyü dinamik yüklemek için Class.forName(...) ve sorgu yürütmek için Statement/PreparedStatement.executeQuery(...) kalıpları görülebilir. Modern JDBC sürücüleri çoğunlukla service-provider mekanizmasıyla otomatik keşfedilir; Class.forName her uygulamada zorunlu başlangıç adımı değildir. executeQuery sonuç kümesi döndüren sorgular için kullanılırken güncelleme ve DDL gibi işlemlerin farklı yürütme kontratları vardır.
15.1. PreparedStatement ile Parametreli Sorgu
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class JdbcOrnegi {
static String kullaniciAdi(Connection connection, long id) throws SQLException {
String sql = "select ad from kullanici where id = ?";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getString(1) : null;
}
}
}
}Parametre bağlama yalnız SQL injection azaltımı değildir; tür dönüşümü ve sorgu sözleşmesini de belirginleştirir. Transaction ve timeout yönetimi bu örneğin dışında bırakılmıştır; gerçek uygulamada bağlantı yaşam döngüsü ve iş atomikliği ayrıca tanımlanmalıdır.
Ünite 16: Ağ, HTTP ve Kurumsal Java Ekosistemi
Java standart kütüphanesi soket ve HTTP istemci API'leri sunar. TCP akış, UDP datagram modelidir; uygulama protokolü tasarlarken mesaj sınırı, framing, timeout, yeniden deneme ve kimlik doğrulama ayrıca belirlenir.
Servlet/JSP, Java web uygulamalarının tarihsel ve hâlen bazı sistemlerde kullanılan temel teknolojileridir. HttpServlet HTTP odaklı servlet tabanını; ServletConfig tek servlet'e, ServletContext ise web uygulaması kapsamına ilişkin bağlamı temsil eder. JSP tarafında JSTL gibi etiket kütüphaneleri görünüm mantığını Java scriptlet kodundan ayırmak için kullanılmıştır. Güncel Jakarta EE terminolojisinde bunların karşılıkları Jakarta Servlet ve Jakarta Pages belirtimleridir; eski javax.* paketleriyle güncel jakarta.* paketleri aynı namespace değildir. Modern servislerde Spring MVC, Spring Boot, Jakarta REST veya benzeri çerçeveler daha yüksek seviyeli programlama modeli sağlar. Framework bilgisi Java dil bilgisinin yerine geçmez; class loading, istisna, iş parçacığı, collection ve I/O davranışı üretim problemlerinde doğrudan görünür hâle gelir.
Masaüstü arayüz tarafında AWT ve Swing Java SE içinde yer alan köklü araç takımlarıdır. JavaFX ise modern sürümlerde JDK'dan ayrı geliştirilen OpenJFX projesi olarak kullanılabilir; Scene, layout ve control bileşenleriyle grafik arayüz oluşturur. Eski bir Swing/JavaFX uygulamasının varlığı, web veya servis mimarisine taşınması gerektiği anlamına gelmez; arayüz teknolojisi dağıtım, bakım ve kullanıcı etkileşimi gereksinimine göre seçilmelidir.
Spring ekosistemi ayrı olarak Spring Boot, protokol ve tarayıcı tarafı ise Web Programlama notunda ele alınır.
Servlet tabanlı eski Java web uygulamalarında web.xml dağıtım descriptor; servlet, filter, listener ve mapping gibi yapılandırmaları XML ile tanımlamak için merkezi bir mekanizmaydı. Anotasyon tabanlı yapılandırma birçok kullanım alanını azaltmış olsa da web.xml mevcut Jakarta Servlet/JSP sistemlerinde ve belirli container ayarlarında hâlâ karşılaşılabilen tarihsel/uyumlu bir yapılandırma biçimidir.
16.1. Standart HttpClient Örneği
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class HttpOrnegi {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2))
.build();
HttpRequest request = HttpRequest.newBuilder(URI.create("https://example.com"))
.timeout(Duration.ofSeconds(3))
.GET()
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println(response.statusCode());
}
}Gerçek servis istemcisinde timeout yalnız bağlantı için değil istek için de tanımlanmalı; yeniden deneme yalnız idempotency ve hata sınıfı bilinerek uygulanmalıdır. HTTP istemci örneği ağ programlama ilkelerinin yerine geçmez; bağlantı havuzu, DNS, TLS, proxy ve sunucu kapasitesi uçtan uca davranışın parçasıdır.
Ünite 17: Derleme, Bağımlılık ve Test
Maven ve Gradle yalnız “kütüphane indiren araçlar” değildir; bağımlılık çözümleme, derleme yaşam döngüsü, test, paketleme ve eklenti yürütme gibi görevleri yönetir. Transitive dependency sürümleri kontrolsüz bırakıldığında farklı ortamlarda davranış değişikliği veya güvenlik açığı oluşabilir.
JUnit birim ve entegrasyon testlerinin temel araçlarından biridir. Mockito gibi mock kütüphaneleri dış bağımlılığın davranışını kontrollü biçimde taklit etmek için kullanılabilir; her sınıfı mock'lamak iyi test tasarımı anlamına gelmez. Saf hesaplama kodunda gerçek nesneler daha düşük bakım maliyetine sahiptir.
Git kaynak sürüm kontrolünü dağıtık modelle sağlar. Commit, branch ve merge; derlenmiş çıktı veya veri tabanı yedeği yerine kaynak geçmişini ve değişim bağlamını yönetmelidir. CI/CD otomasyonu test ve paketleme sürecini tekrarlanabilir hâle getirir, fakat hatalı test veya hatalı dağıtım politikasını kendiliğinden düzeltmez.
Klasik JavaFX kodunda VBox dikey yerleşim kapsayıcısı, ScrollBar kaydırma denetimi ve NumberConverter sayısal değerlerle metin arasındaki dönüşümlerde kullanılan converter ailesinin bir parçasıdır. Bunlar JavaFX API örnekleridir; Java dil özelliği değildir. Benzer şekilde belirli widget sınıfını bilmek, layout/iş parçacığıing/lifecycle modelini anlamanın yerine geçmez.
Ünite 18: Güvenli ve Ölçülebilir Java
Güvenli Java kodu yalnız istisna yakalamakla sağlanmaz. Girdi doğrulama, yetkilendirme, sorgu parametreleme, güvenli deserialization, kriptografik API'lerin doğru kullanımı, gizli bilgilerin loglanmaması ve bağımlılık yönetimi aynı güvenlik sınırının parçalarıdır.
Performans çalışmasında önce sistem hedefi tanımlanmalıdır: gecikme mi, işlem hacmi mi, CPU mu, bellek mi, GC pause mu, dış servis kapasitesi mi? Bir ArrayList yerine başka koleksiyon seçmenin etkisi, yanlış SQL veya kilit altında uzak çağrı gibi makro darboğazların yanında önemsiz kalabilir. Ölçüm profilleyici, JFR, GC çıktısı, iş parçacığı dump ve sistem metrikleriyle gerçek darboğazı göstermelidir.
Java'nın dili, JVM'i ve ekosistemi ayrı katmanlardır. Dil kuralını framework davranışıyla, framework varsayımını JVM garantisiyle karıştırmamak üretim sorunlarını anlamada temel disiplindir.
18.1. Değer Odaklı Modelleme: Record, Sealed Türler ve Örüntü Eşleme
Java'nın nesne yönelimli modeli yalnız mutable sınıf hiyerarşilerinden oluşmaz. record, veri taşıyan küçük modellerde constructor, accessor, equals, hashCode ve toString sözleşmesini kompakt biçimde kurar. sealed türler ise hangi alt türlerin bulunabileceğini sınırlar.
Bu iki özellik örüntü eşlemeyle birlikte kullanıldığında kapalı domain modelleri daha açık ifade edilebilir. Örneğin ödeme sonucunun Approved, Rejected ve Pending türlerinden biri olduğu biliniyorsa switch bütün olasılıkları kapsayacak biçimde yazılabilir.
Bunun amacı daha kısa kod değil, durum uzayını derleyicinin görebileceği biçimde ifade etmektir. Domain gerçekten açık uçluysa sealed hiyerarşi zorlanmamalı; modelin değişim yönü API tasarımını belirlemelidir.
18.2. Java Davranışını Dil ve JVM Katmanında Ayırmak
Java Language Specification ile JVM Specification farklı sözleşmeleri tanımlar. Bir dil özelliğinin derleme zamanı kuralı ile bytecode veya HotSpot optimizasyonu aynı kanıt düzeyinde değildir.
Eşzamanlılık örneklerinde Java Memory Model esas alınmalıdır. Bir kodun birkaç çalıştırmada doğru görünmesi happens-before ilişkisini kanıtlamaz; görünürlük ve atomiklik sözleşmesi dil/JVM kurallarıyla kurulmalıdır.
Performans örneğinde JIT ısınması, GC ve escape analysis sonucu değiştirebilir. Mikrobenchmark için ölçüm harness'i, üretim davranışı için JFR/GC logları ve gerçek iş yükü birbirini tamamlayan araçlardır.
Mikrobenchmark'ın ölçüm nesnesi. Döngüde çalıştırılan iki Java metodu yalnız kaynak kod uzunluklarına bakılarak karşılaştırılamaz. JIT, sonucun kullanılmadığını kanıtladığında hesabı kaldırabilir; sabit girdileri önceden hesaplayabilir, çağrıyı inline edebilir veya nesne tahsisini escape analysis ile ortadan kaldırabilir. Bu nedenle JMH gibi amaca uygun bir harness kullanılsa bile sonuç tüketimi, parametre çeşitliliği, warm-up, fork sayısı ve ölçüm çevresinin davranışı belgelenmelidir.
Mikrobenchmark kazancını bir HTTP isteğine taşırken Amdahl sınırı yeniden hesaplanmalıdır: isteğin yalnız küçük bir bölümünü oluşturan serileştirme adımını iki kat hızlandırmak, tüm istek süresini iki kat iyileştirmez. Gerçek iş yükünün veri dağılımı, GC davranışı ve cache yerelliği ayrıca doğrulanmalıdır. Yerel yöntem hızlanması ile üretim throughput'u farklı başarı ölçütleridir.
Ünite 19: JVM Yaşam Döngüsü, Kaynak Semantiği ve Modern Java Derinleşmeleri
Java programını yalnız kaynak kod, nesne ve metot düzeyinde okumak bazı çalışma zamanı davranışlarını görünmez bırakır. Sınıfın JVM'e ne zaman ve hangi yükleyiciyle geldiği, kaynağın hangi sırayla kapatıldığı, asenkron zincirde hatanın nasıl taşındığı veya bir referansın çöp toplayıcı açısından ne kadar güçlü olduğu doğrudan program davranışını etkiler.
19.1. Sınıf Yükleme ve Sınıf Kimliği
JVM'de bir sınıfın yaşam döngüsü kabaca:
loading
↓
linking
├─ verification
├─ preparation
└─ resolution
↓
initializationaşamalarıyla düşünülebilir.
Yükleme (loading), sınıf tanımının bulunup JVM içindeki sınıf nesnesine dönüştürülmesidir. Bağlama (linking) sırasında bytecode doğrulanır, statik alanlar için gerekli temel hazırlık yapılır ve sembolik başvurular gerektiğinde çözümlenir. Başlatma (initialization) ise sınıf başlatıcılarının ve statik başlangıç ifadelerinin çalıştığı aşamadır.
Bir sınıfın kimliği yalnız tam sınıf adı değildir. JVM açısından pratik olarak:
sınıf kimliği
=
binary name
+
onu tanımlayan sınıf yükleyicibirlikte önem taşır.
Aynı adlı sınıf iki farklı sınıf yükleyici tarafından tanımlandığında bunlar birbirinin yerine kullanılabilir iki tür olmak zorunda değildir. Uygulama sunucuları, eklenti sistemleri, yeniden yükleme mekanizmaları ve modüler çalışma zamanları incelenirken ClassCastException benzeri şaşırtıcı hataların bir bölümü bu sınırdan doğar.
Sınıf yükleyici (class loader) mimarisini öğrenmenin amacı özel yükleyici yazmak değildir. Amaç, kodun bulunması ile tür kimliğinin oluşması arasındaki bağı görmektir.
19.2. Başlatma Zamanı ve Statik Durum
Bir sınıfın bytecode'unun bulunmuş olması, bütün statik başlatıcılarının daha önce çalıştığı anlamına gelmez. Sınıf başlatma kuralları; sabit alan, statik metot çağrısı, nesne oluşturma ve yansıma gibi erişim biçimlerine göre farklı tetiklenebilir.
Bu nedenle ağır iş yapan statik başlatıcılar:
- uygulama başlangıç süresini,
- hata yüzeyini,
- test izolasyonunu,
- class loading sırasında kilitlenme davranışını
etkileyebilir.
Statik global durumun testte veya yeniden yüklemede kalıcı yan etki üretmesi de sınıf yaşam döngüsü ile ilgilidir. static yalnız erişim sözdizimi değil, çalışma zamanı yaşam süresi kararıdır.
19.3. try-with-resources, Kapanış Sırası ve Bastırılmış İstisnalar
try-with-resources yalnız close() çağrısını otomatik yazdıran bir sözdizimi kısaltması değildir. Birden fazla kaynak tanımlandığında kaynaklar oluşturulduklarının ters sırasıyla kapatılır.
try (var first = openFirst();
var second = openSecond()) {
use(first, second);
}normal veya hatalı çıkışta kapanış sırası kavramsal olarak:
second.close()
first.close()şeklindedir.
Asıl işlem bir istisna üretmişken close() da hata üretirse kaynak kapatma hatası ana hatayı ezmemelidir. Java bu ikincil hataları bastırılmış istisna (suppressed exception) olarak ana istisnaya ekleyebilir:
catch (Exception e) {
for (Throwable suppressed : e.getSuppressed()) {
System.err.println(suppressed.getMessage());
}
}Bu semantik özellikle dosya, soket, JDBC ve benzeri kaynaklarda hata teşhisi için önemlidir.
Garbage collector nesnenin belleğini geri alabilir; fakat açık dosya tanıtıcısı, veritabanı bağlantısı veya soket gibi dış kaynakların zamanında serbest bırakılmasının yerini tutmaz. Bellek yaşam süresi ile işletim sistemi kaynağı yaşam süresi ayrı sözleşmelerdir.
19.4. Güçlü, Yumuşak, Zayıf ve Phantom Referanslar
Java'da olağan nesne başvurusu güçlü referanstır. Nesneye güçlü bir erişim yolu kaldığı sürece nesne çöp toplama için erişilemez sayılmaz.
java.lang.ref ailesi farklı referans güçleri sunar:
strong
↓
SoftReference
↓
WeakReference
↓
PhantomReferenceBu çizim kesin bir "toplanma sırası" algoritması değildir; referansın GC karşısındaki kuvvetini düşünmek için kavramsal bir eksendir.
**SoftReference**, bellek duyarlı nesneler için tarihsel olarak kullanılmıştır; fakat genel amaçlı ve öngörülebilir bir önbellek politikası sayılmamalıdır. Önbellek kapasitesi, TTL ve tahliye politikasını açık yönetmek çoğu üretim sisteminde daha denetlenebilirdir.
**WeakReference**, başka güçlü referans kalmadığında nesnenin yaşamını uzatmamak istenen yapılarda kullanılabilir. WeakHashMap belirli anahtar yaşam döngülerinde bunun örneğidir; ancak "zayıf referans kullanınca memory leak olmaz" sonucu çıkmaz.
**PhantomReference**, nesneye yeniden erişmek için değil, toplama sonrası yaşam döngüsü koordinasyonu ve ReferenceQueue tabanlı temizleme düzenekleri için kullanılır. Deterministik kaynak kapatma gerektiğinde yine AutoCloseable ve açık yaşam döngüsü tercih edilmelidir.
19.5. Bellek Sızıntısı ile GC Yetersizliğini Ayırmak
Java'da "GC var, memory leak olmaz" doğru değildir.
iş açısından gereksiz nesne
+
hâlâ güçlü referansla erişilebilir
=
GC açısından canlı nesneSınırsız map, listener kaydı, uzun ömürlü ThreadLocal, yanlış cache anahtarı veya statik koleksiyon bu durumu oluşturabilir.
Sorun her zaman collector seçimi değildir. Heap dump veya Java Flight Recorder ile hangi referans zincirinin nesneyi canlı tuttuğu bulunmadan GC ayarı değiştirmek semptomla uğraşmak olabilir.
19.6. CompletableFuture: Görev Başlatmaktan Bileşime
CompletableFuture yalnız runAsync() ile "arka planda iş çalıştırmak" değildir. Asıl gücü, sonuç ve hata akışlarını yapılandırılmış biçimde bileştirebilmesidir.
Temel ayrım:
runAsync(Runnable)
-> sonuç değeri yok
-> CompletableFuture<Void>
supplyAsync(Supplier<T>)
-> T sonucu üretir
-> CompletableFuture<T>Zincirleme aşamada:
thenApply
-> T değerini U değerine dönüştürür
thenCompose
-> T değerinden yeni async aşama üretir
-> iç içe future oluşmasını düzleştirir
thenCombine
-> bağımsız iki sonucu birleştirirgibi farklı semantik vardır.
CompletableFuture<User> user =
CompletableFuture.supplyAsync(() -> loadUser(id), executor);
CompletableFuture<Account> account =
user.thenCompose(u -> loadAccountAsync(u.accountId()));thenApply(u -> loadAccountAsync(...)) kullanılsaydı sonuç CompletableFuture<CompletableFuture<Account>> biçiminde iç içe kalabilirdi.
19.7. Hata Zinciri ve Executor Sahipliği
Asenkron akışta hata yalnız try/catch ile okunmaz. exceptionally, handle, whenComplete ve tamamlanma istisnaları farklı amaçlara sahiptir.
Önemli soru yalnız "hangi metot?" değil:
hata nerede çevrilecek?
hangi katmanda loglanacak?
fallback gerçekten güvenli mi?
iptal nasıl yayılacak?
timeout kime ait?sorularıdır.
Executor belirtilmeyen bazı async aşamalar ortak havuz gibi varsayılan yürütme kaynaklarını kullanabilir. Kütüphane kodunun uygulamanın haberi olmadan pahalı veya bloklayan işi ortak kaynağa taşıması kapasite problemi yaratabilir. Kritik iş akışında executor sahipliği açık tutulmalıdır.
Virtual thread kullanımı da CompletableFuture ihtiyacını otomatik ortadan kaldırmaz. Virtual thread, bloklayan görevin temsilini ucuzlatır; CompletableFuture ise bağımlı asenkron aşamaları veri akışı olarak birleştirir. Hangi modelin daha okunabilir ve ölçülebilir olduğu iş yüküne göre seçilir.
19.8. Reflection, MethodHandle Ve Dinamik Çağrı
Yansıtma (reflection), tür ve üyeleri çalışma zamanında keşfetmeyi sağlar. MethodHandle ise JVM'in dinamik çağrı altyapısına daha doğrudan bağlanan, türü açık bir çağrılabilir temsil sunar.
Kavramsal ayrım:
Reflection
-> keşif + genel amaçlı dinamik erişim
MethodHandle
-> önceden çözümlenmiş dinamik çağrı davranışıinvokedynamic ise dil ve çalışma zamanı uygulayıcılarının çağrı bağlantısını çalışma zamanında özelleştirebilmesi için JVM bytecode düzeyinde bir mekanizmadır. Lambda gerçekleştirimleri ve dinamik diller bu altyapıdan yararlanabilir.
Bu araçlar normal statik çağrının "daha hızlı alternatifi" olarak değil, framework, dil çalışma zamanı ve ileri metaprogramlama ihtiyaçları bağlamında değerlendirilmelidir.
19.9. Yerleşik Nesne Serileştirmesinin Sınırı
Java nesne serileştirmesi (ObjectInputStream / ObjectOutputStream) tarihsel sistemlerde önemlidir; ancak yeni dış veri sözleşmesi için varsayılan seçim olmamalıdır.
Temel riskler:
- sınıf yapısına sıkı bağ,
- sürümleme zorluğu,
- güvenilmeyen girdide nesne grafı oluşturma riski,
- gizli alanların istemeden taşınması,
- başka dillerle düşük birlikte çalışabilirlik.
Güvenilmeyen nesne akışının doğrudan deserialization işlemi güvenlik yüzeyidir. Açık şema, sınırlı DTO ve JSON/CBOR/Protobuf benzeri sözleşmeler çoğu dış sınırda daha denetlenebilirdir.
Eski sistemde Java serialization zorunluysa izin verilen sınıfları ve nesne grafı sınırlarını filtrelemek, girdiyi kimlik doğrulamasına tabi tutmak ve yalnız gerekli sınıfları deserialise etmek savunma katmanlarıdır.
19.10. Security Manager Tarihsel bir Mekanizmadır
Eski Java kitapları ve mülakat dokümanları SecurityManager, policy dosyaları ve uygulama içi sandbox modelini güncel güvenlik çözümü gibi anlatabilir.
Bu bilgi tarihsel bağlamda değerlidir; fakat güncel JDK çizgisinde Security Manager kalıcı olarak devre dışıdır. Yeni güvenlik mimarisi:
process / container sınırı
işletim sistemi izinleri
modül ve paket sınırları
uygulama yetkilendirmesi
girdi doğrulama
güvenli serialization
ağ ve dosya izinlerigibi katmanlarda kurulmalıdır.
Eski soruya doğru cevap bazen API'nin tarihini bilmek değil, API'nin artık geçerli güvenlik modeli olmadığını fark etmektir.
19.11. Java 27: GA Özellik ile Preview Özelliği Ayırmak
JDK 27, Eylül 2026'da yayımlanan güncel Java SE sürümüdür. Ancak bir özelliğin JDK 27 dağıtımında bulunması onun kalıcı Java SE sözleşmesi olduğu anlamına gelmez.
Örneğin bazı dil özellikleri preview statüsünde bulunabilir. Preview özellik:
- açık derleyici/çalıştırma seçeneği gerektirebilir,
- sonraki sürümde değişebilir veya kaldırılabilir,
- uzun ömürlü kütüphane API'sinde dikkatle kullanılmalıdır.
Sürüm değerlendirmesinde üç ayrı soru sorulmalıdır:
özellik hangi JDK'da mevcut?
standart mı, preview mı, incubator mı?
kullandığım framework bu JDK'yı resmen destekliyor mu?Java'yı güncel biçimde öğrenmek, eski mülakat ezberlerini çoğaltmak değil; dil belirtimi, JVM semantiği, kütüphane sözleşmesi ve çalışma zamanı sürümünü birbirinden ayırabilmektir.
Ünite 20: Java'da Metin Kimliği, Değişmezlik ve Eşzamanlılık Protokolleri
Eski Java mülakat notlarında sık geçen bazı başlıklar hâlâ değerlidir; ancak güncel Java'da bunları API ezberi yerine dil/JVM sözleşmesi üzerinden okumak gerekir.
20.1. String Pool, Literal ve intern()
String literal'leri ve compile-time constant string ifadeleri JVM tarafından canonical string havuzuyla ilişkilendirilebilir.
String a = "iz";
String b = "iz";
System.out.println(a == b); // çoğu durumda aynı interned literal
System.out.println(a.equals(b)); // içerik eşitliğiBuradan "== String için kullanılabilir" sonucu çıkarılmamalıdır.
== -> referans kimliği
equals -> içerik/eşitlik sözleşmesiintern() aynı içeriğin canonical temsilini döndürür; fakat her uygulama string'i otomatik olarak intern etmek bellek ve performans açısından doğru politika değildir.
20.2. Değişmez Sınıf Tasarımı
Bir alanı final yapmak nesne grafını otomatik olarak immutable yapmaz.
final List<String> values;referansın başka listeye atanmasını engeller; listenin kendisinin değişmesini engellemez.
Gerçek immutability için:
- mutable iç nesnelere defensive copy,
- dışarı mutable koleksiyon sızdırmama,
- constructor'da geçerli state kurma,
- setter benzeri mutasyon yüzeyini sınırlama,
- subclass'ın invariant'ı bozmasını önleme
gibi kararlar gerekir.
record da bileşeninin kendisi mutable ise derin immutability garantisi vermez.
20.3. Comparable Ve Comparator
Comparable<T> tipe doğal sıralama sözleşmesi kazandırır:
class Version implements Comparable<Version> {
public int compareTo(Version other) { ... }
}Comparator<T> ise dışarıdan bir veya birden fazla alternatif sıralama tanımlar:
Comparator<Person> byName = Comparator.comparing(Person::name);
Comparator<Person> byAge = Comparator.comparingInt(Person::age);Sıralama sözleşmesinin eşitlik (equals) ile tutarlılığı özellikle sorted set/map davranışını etkileyebilir.
20.4. wait, notify Ve notifyAll
Bu yöntemler Object monitor protokolünün düşük seviyeli parçalarıdır.
Temel kural:
wait/notify çağrısı
-> ilgili monitor sahipliği gerektirirCondition her zaman döngü içinde tekrar kontrol edilmelidir:
synchronized (queue) {
while (queue.isEmpty()) {
queue.wait();
}
consume(queue.removeFirst());
}if yerine while kullanılmasının nedeni yalnız spurious wakeup değildir; thread uyandığında koşul başka thread tarafından tekrar bozulmuş olabilir.
notify() bir waiter seçerken, notifyAll() bütün waiter'ları uyandırır. Karmaşık üretim kodunda BlockingQueue, Lock/Condition, synchronizer'lar veya daha yüksek seviyeli structured concurrency yaklaşımları çoğu zaman daha açık sözleşme sunar.
20.5. Native Serialization: transient Ve serialVersionUID
Java native object serialization tarihsel mülakatlarda sık görülür.
transient alan normal serial form'a dahil edilmez. serialVersionUID, serializable class sürümü ile stream'deki sınıf tanımı arasındaki compatibility kontrolünde rol oynar.
Ancak bu mekanizma:
- genel amaçlı servis veri sözleşmesi,
- güvenilmeyen girdiyi deserialize etme yolu,
- uzun ömürlü platform-bağımsız format
olarak varsayılan tercih değildir.
Mevcut güvenlik ve bakım gereksinimlerinde JSON/CBOR/Protobuf benzeri açık veri sözleşmeleri veya uygulamaya özel formatlar çoğu zaman daha kontrollüdür.
20.6. Interface Default Method
Modern Java interface yalnız abstract instance method listesi değildir. default method, implementasyon sağlayarak interface evrimini kolaylaştırabilir.
Bir sınıf iki interface'den aynı default imzayı alırsa conflict açık biçimde çözülmelidir.
Bu özellik inheritance hiyerarşisini keyfi olarak interface'e taşımak için değil, ortak sözleşmenin geriye uyumlu evrimini desteklemek için kullanılmalıdır.
20.7. Tarihsel Collection Sınıflarını Doğru Konumlandırmak
Vector, Stack ve Hashtable eski Java API'lerinde önemlidir ve mevcut sistemlerde görülebilir.
Yeni tasarımda çoğu kullanım için daha açık alternatifler vardır:
Vector
-> ArrayList + gerekli concurrency stratejisi
Stack
-> Deque / ArrayDeque
Hashtable
-> HashMap
-> ConcurrentHashMap (eşzamanlı erişim gerekiyorsa)Buradaki amaç "eski sınıf çalışmaz" demek değildir; senkronizasyon ve API tasarımı açısından yeni kodda daha uygun soyutlamaları seçmektir.
20.8. Eski Mülakat Bilgisini Güncelleme İlkesi
Java mülakatında bir ifade gördüğünüzde üç ayrı soru sorun:
- Bu davranış Java dil specification'ına mı ait?
- JVM implementasyon ayrıntısı mı?
- Tarihsel bir API/sürüm davranışı mı?
Bu ayrım, ezberlenmiş cevabın yeni JDK sürümünde yanlış genellenmesini önler.
Kaynakça
- A. Yazıcı, E. Doğdu, M. Özbayoğlu, M. Erten, O. Ergin. Java Bilgisayar Programlamaya Giriş.
- Benjamin J. Evans; Jason Clark; David Flanagan. Java in a Nutshell: A Desktop Quick Reference, 8th ed. O'Reilly Media, 2023.
- Benjamin J. Evans; Martijn Verburg. The Well-Grounded Java Developer. Manning, 2013.
- Brendan Gregg. Systems Performance: Enterprise and the Cloud. Pearson Education, 2014.
- Brian Goetz et al. Java Concurrency in Practice. Addison-Wesley, 2006.
- Eclipse Foundation. Jakarta Pages Specification. https://jakarta.ee/specifications/pages/
- Eclipse Foundation. Jakarta Servlet Specification. https://jakarta.ee/specifications/servlet/
- Eclipse Foundation. Standard Widget Toolkit (SWT). https://www.eclipse.org/swt/
- Joshua Bloch. Effective Java, 3rd ed. Addison-Wesley, 2018.
- Maurice Herlihy; Nir Shavit. The Art of Multiprocessor Programming. Morgan Kaufmann, 2008.
- OpenJDK. Java Microbenchmark Harness (JMH). https://openjdk.org/projects/code-tools/jmh/
- OpenJDK. JEP 444: Virtual Threads. https://openjdk.org/jeps/444
- OpenJDK. JEP 486: Permanently Disable the Security Manager. https://openjdk.org/jeps/486
- OpenJFX. JavaFX Documentation. https://openjfx.io/
- Oracle. Java Language Specification. https://docs.oracle.com/javase/specs/
- Oracle. Java Platform, Standard Edition 27 & JDK 27 Specifications. https://docs.oracle.com/en/java/javase/27/docs/specs/index.html
- Oracle. Java Platform, Standard Edition Documentation. https://docs.oracle.com/en/java/javase/
- Oracle. Java SE API Documentation. https://docs.oracle.com/en/java/javase/27/docs/api/
- Oracle. Java Virtual Machine Specification. https://docs.oracle.com/javase/specs/
- Oracle. JDK 27 Release Notes. https://www.oracle.com/java/technologies/javase/27all-relnotes.html
- Oracle. String (Java SE 27 & JDK 27). https://docs.oracle.com/en/java/javase/27/docs/api/java.base/java/lang/String.html
- P. Deitel, H. Deitel. Java How to Program. Pearson Education.
- Walter Savitch, F. M. C. Java: An Introduction to Problem Solving & Programming, 5th ed. Pearson Education.