Eğitim Portalı/Java/Sürüm Geçişi: Riskler ve Kazanımlar
Java01-java/21-surum-gecisi-riskler-ve-kazanimlar

Sürüm Geçişi: Riskler ve Kazanımlar

Yeni bir Java sürümüne geçmek genellikle kârlıdır — ama "derle, çalıştır, bitti" kadar basit değildir. Her büyük geçişin somut kazanımları (performans, güvenlik, dil özellikleri) olduğu kadar riskleri de (kaldırılan A…

Sürüm Geçişi: Riskler ve Kazanımlar

Yeni bir Java sürümüne geçmek genellikle kârlıdır — ama "derle, çalıştır, bitti" kadar basit değildir. Her büyük geçişin somut kazanımları (performans, güvenlik, dil özellikleri) olduğu kadar riskleri de (kaldırılan API'ler, kütüphane uyumsuzlukları, davranış değişiklikleri) vardır. Bu bölümde önce genel geçiş prensiplerini, sonra bugün en yaygın iki taban olan 17 ve 21'den en yeni sürümlere geçişte seni nelerin beklediğini konuşuyoruz.

Kazanımlar (neden geçilir?)

  • Güvenlik: Eski sürümler bir noktada güvenlik yamaları almayı bırakır. Desteklenen bir LTS'te kalmak, bilinen açıklara karşı en temel korumadır.
  • Performans: Her sürüm JIT ve çöp toplayıcı (GC) iyileştirmeleri getirir. 17→21 geçişi generational ZGC ile düşük gecikme sağlar; genel olarak daha az bellek, daha hızlı başlangıç.
  • Eşzamanlılık: Java 21'in sanal thread'leri, G/Ç ağırlıklı sunucularda kodu neredeyse hiç değiştirmeden çok daha yüksek eşzamanlılık sağlar (Örnek 3'te ~50x fark gördük).
  • Dil ergonomisi: record, sealed, pattern matching, switch expression... daha az kod, daha az hata. Modern API'ler (java.time, HttpClient, Stream.toList()) eski, hatalı olanların yerini alır.

Örnek 1 (./Ornek1.java) Runtime.version() ile çalışılan JDK'yı tanır ve hangi özelliklerin mevcut olduğunu bir "feature flag" mantığıyla raporlar — geçiş sırasında kodun sürüme göre nazikçe uyum sağlaması (graceful degradation) için kullanışlı bir desen.

Riskler (geçişte neye dikkat?)

1) Kaldırılan / kapsüllenen API'ler

  • Java 11: Java EE/CORBA modülleri kaldırıldı (JAXB, JAX-WS, CORBA). Bunlara bağlı eski projeler, kütüphaneleri ayrıca eklemeden derlenemez.
  • Java 16/17: JDK iç bileşenleri güçlü kapsüllendi (JEP 396). sun.misc.*, --illegal-access ile JDK içine sızan kütüphaneler (eski Hibernate, bazı serileştirme/proxy araçları) kırıldı. Geçişten önce --add-opens/--add-exports ihtiyacını ve kütüphane sürümlerini gözden geçirmek şart.
  • Deprecation (kullanımdan kaldırma): SecurityManager (17'den itibaren kaldırılmaya işaretli), Applet, finalization, new Integer()/new Double() (kaldırılmaya işaretli)... Bugün "uyarı" olan şey, yarın "derlenmiyor" olabilir.

2) Davranış değişiklikleri (sessiz kırılmalar)

Bunlar en sinsileridir; kod derlenir ama farklı davranır:

  • Java 18: Varsayılan karakter kümesi UTF-8 oldu. Platform varsayılanına güvenen dosya okuma/yazma kodu, farklı sonuç üretebilir.
  • GC varsayılanları ve değerleri sürümlerle değişir; performans profili kayabilir.
  • Bazı tarih/sayı biçimlendirme yerelleştirmeleri (CLDR güncellemeleri) çıktıları değiştirebilir.

3) Üçüncü parti uyumluluğu (en yaygın gerçek engel)

Uygulamanın kendisi geçişe hazır olsa bile, bağımlılıkların yeni JDK'yı desteklemesi gerekir. Özellikle:

  • Eski Spring Boot 2.x yalnızca Java 8–17 destekler; Java 21 için Spring Boot 3.2+, güvenli sanal thread desteği için 3.2+ gerekir. Java 25 için en güncel sürümlere bakılmalı.
  • Hibernate, Lombok, bytecode üreten/işleyen araçlar (ASM, ByteBuddy, mockito) sıklıkla JDK'ya duyarlıdır; eski sürümleri yeni JDK'da kırılır.
  • Build araçları da güncellenmeli: yeni JDK genelde yeni bir Gradle/Maven sürümü ister.

4) Güvensiz/eski desenlerin gün yüzüne çıkması

Geçiş, mevcut gizli hataları görünür kılabilir. Örnek 2 (./Ornek2.java) bunun klasiğini gösterir: paylaşımlı SimpleDateFormat thread-safe değildir ve çok thread'li ortamda sessizce yanlış sonuç üretir veya hata fırlatır (örnekte on binlerce anomali!). Modern java.time (DateTimeFormatter, LocalDate) değişmezdir ve güvenle paylaşılır. Geçiş, bu tür eski API'leri modernleriyle değiştirmek için iyi bir fırsattır.

Bugünkü tablo: 17 ve 21'den geçiş

Java 17 → 21 (düşük risk, yüksek kazanım)

  • Risk: Genelde düşüktür; 17'de zaten güçlü kapsülleme yapıldığı için büyük kırılmalar arkada kaldı. Asıl dikkat edilecek: framework sürümleri (Spring Boot 3.2+), build aracı sürümü ve sanal thread kullanacaksan synchronized bloklarındaki "pinning" (sanal thread'in taşıyıcı thread'e sabitlenmesi) — kritik bölümlerde ReentrantLock tercih et.
  • Kazanım: Sanal thread'ler, record patterns, sequenced collections, generational ZGC. Çoğu projede 17→21 "neredeyse bedava" bir yükseltmedir.

Java 21 → 25 (ölç, kütüphaneleri doğrula)

  • Risk: Önizleme (preview) olarak gelen bazı özellikler 25'te standartlaştı; davranışları ince ayar görmüş olabilir. Kütüphane/araç uyumu yine en büyük belirleyicidir — geçmeden önce Spring/Hibernate/build aracı sürümlerinin 25'i desteklediğini doğrula. Kaldırılmaya işaretli API'ler (SecurityManager vb.) bu hatta tamamen gidebilir.
  • Kazanım: Stream Gatherers (özel akış işlemleri), sadeleştirilmiş giriş noktası ve modül import bildirimleri, scoped values, sürekli iyileşen GC/JIT performansı, en güncel güvenlik.

Java 8/11 → 17+ (en büyük sıçrama)

Hâlâ 8 veya 11'deyse bir proje, en zorlu ama en kârlı geçiş budur: kaldırılan EE modülleri, güçlü kapsülleme ve eski kütüphanelerin hepsi aynı anda karşına çıkar. Adım adım (8→11→17→21) ilerlemek, riski yönetilebilir parçalara böler.

Pratik geçiş kontrol listesi

  1. Bağımlılıkları yükselt: Önce kütüphaneleri/framework'ü hedef JDK'yı destekleyen sürümlere çıkar (çoğu kırılma buradan gelir).
  2. Build aracını güncelle: Gradle/Maven'ı yeni JDK'yı destekleyen sürüme al; --release <hedef> ile derle.
  3. Uyarıları temizle: -Xlint:all -Werror ile deprecation/removal uyarılarını ciddiye al.
  4. Test, test, test: Kapsamlı test paketi geçişin sigortasıdır; davranış değişikliklerini (UTF-8, biçimlendirme, GC) testlerle yakala.
  5. Aşamalı geçir: Önce staging/canary ortamında çalıştır, performans ve hata metriklerini izle, sonra üretime al.
  6. --add-opens/--add-exports ihtiyacını gözden geçir ve mümkünse bu geçici çözümleri kütüphane yükseltmesiyle ortadan kaldır.

Özet

Sürüm geçişinin kazanımlarını (güvenlik, performans, sanal thread'ler, dil ergonomisi) ve risklerini (kaldırılan/kapsüllenen API'ler, davranış değişiklikleri, kütüphane uyumu, gizli hataların açığa çıkması) gerçek örneklerle gördük; bugün kullanılan 17 ve 21'den en yeni sürümlere geçişte ne beklemen gerektiğini ve bir kontrol listesini ele aldık. Çıkarım net: desteklenen bir LTS'te kal, bağımlılıkları önce yükselt, testlere güven, aşamalı geç.

Bu, Java bölümünün son konusudur. Artık dile hâkimsin, ekosistemini ve evrimini biliyorsun. Sırada, tüm bu temellerin üzerine kurulu kurumsal dünya: Spring ve Spring Boot.

Kod Örnekleri(3)

Ornek1

çalıştırılabilir
Ornek1.java
1// Ornek1: Çalışma zamanı sürümünü tanıma ve sürüme göre davranış.
2// Geçişlerde "hangi JDK üzerinde çalışıyorum?" sorusunu programatik yanıtlamak gerekir.
3// Çalıştırma: java Ornek1.java
4public class Ornek1 {
5
6    public static void main(String[] args) {
7        // Runtime.version() (Java 9+): yapısal sürüm bilgisi.
8        Runtime.Version v = Runtime.version();
9        int major = v.feature(); // ana sürüm: 17, 21, 25...
10
11        System.out.println("Çalışan JDK");
12        System.out.println("  Tam sürüm   : " + v);
13        System.out.println("  Ana sürüm   : " + major);
14        System.out.println("  java.version: " + System.getProperty("java.version"));
15        System.out.println("  Sağlayıcı   : " + System.getProperty("java.vendor"));
16
17        // LTS mi? (8, 11, 17, 21, 25 ...)
18        boolean lts = switch (major) {
19            case 8, 11, 17, 21, 25 -> true;
20            default -> false;
21        };
22        System.out.println("  LTS sürüm   : " + (lts ? "evet" : "hayır (kısa destekli)"));
23
24        // Sürüme göre özellik kullanılabilirliği (feature flag mantığı).
25        System.out.println("\nBu JDK'da kullanılabilir özellikler:");
26        ozellik("Lambda / Stream / Optional (8+)", major >= 8);
27        ozellik("var, HttpClient, tek dosya çalıştırma (11+)", major >= 11);
28        ozellik("record, sealed, pattern matching (17+)", major >= 17);
29        ozellik("Virtual threads, record patterns (21+)", major >= 21);
30        ozellik("Stream Gatherers, sade main, modül import (25+)", major >= 25);
31
32        // Pratik: yeni bir API yoksa eski yola düş (graceful degradation).
33        if (major >= 21) {
34            System.out.println("\nÖneri: Sanal thread'leri kullanabilirsin (yüksek eşzamanlılık).");
35        } else {
36            System.out.println("\nÖneri: Sabit boyutlu thread havuzu kullan (sanal thread henüz yok).");
37        }
38    }
39
40    static void ozellik(String ad, boolean varMi) {
41        System.out.println("  [" + (varMi ? "✓" : "×") + "] " + ad);
42    }
43}
Çıktı yerel JDK 21 ile yakalandı — tarayıcıda JVM çalışmaz.

Ornek2

çalıştırılabilir
Ornek2.java
1// Ornek2: Geçişte karşılaşılan RİSK — eski/güvensiz API'ler ve modern, güvenli karşılıkları.
2// Klasik örnek: SimpleDateFormat thread-safe DEĞİLDİR; java.time değişmez ve güvenlidir.
3// Çalıştırma: java Ornek2.java
4import java.text.ParseException;
5import java.text.SimpleDateFormat;
6import java.time.LocalDate;
7import java.time.format.DateTimeFormatter;
8import java.util.Date;
9import java.util.concurrent.atomic.AtomicInteger;
10
11public class Ornek2 {
12
13    static final String[] TARIHLER = {
14            "2020-01-15", "1999-12-31", "2010-06-20", "2024-03-08",
15            "1985-11-02", "2001-09-11", "2015-07-19", "1970-01-01"
16    };
17
18    public static void main(String[] args) throws Exception {
19        System.out.println("=== RİSK: paylaşımlı SimpleDateFormat (thread-safe değil) ===");
20        anomaliTesti();
21
22        System.out.println("\n=== Diğer eski -> modern karşılıklar ===");
23
24        // 1) new Integer(...) DEPRECATED (kaldırılmaya işaretli) -> Integer.valueOf kullan.
25        @SuppressWarnings("removal")
26        Integer eski = new Integer(42);
27        System.out.println("new Integer(42) (deprecated) -> Integer.valueOf(42) = " + Integer.valueOf(42)
28                + " | eşit mi: " + eski.equals(42));
29
30        // 2) Date + SimpleDateFormat yerine -> java.time (değişmez, okunaklı, güvenli).
31        String eskiStr = new SimpleDateFormat("yyyy-MM-dd").format(new Date(0L));
32        String modernStr = LocalDate.of(1970, 1, 1).format(DateTimeFormatter.ISO_DATE);
33        System.out.println("SimpleDateFormat: " + eskiStr + "  |  java.time: " + modernStr);
34
35        // 3) Collectors.toList() yerine -> Stream.toList() (Java 16+, daha kısa).
36        var liste = java.util.stream.Stream.of(3, 1, 2).sorted().toList();
37        System.out.println("Stream.toList() (16+): " + liste);
38
39        System.out.println("\nKural: Geçiş öncesi derleyici uyarılarını (deprecation) ciddiye al;");
40        System.out.println("kaldırılan API'ler bir sonraki sürümde kodu DERLENEMEZ hale getirebilir.");
41    }
42
43    // Tek bir SimpleDateFormat'ı çok sayıda thread'de PARSE için paylaşmak iç durumu bozar.
44    static void anomaliTesti() throws ParseException, InterruptedException {
45        // Referans (paylaşılmayan) parse ile beklenen değerleri hesapla.
46        SimpleDateFormat ref = new SimpleDateFormat("yyyy-MM-dd");
47        long[] beklenen = new long[TARIHLER.length];
48        for (int i = 0; i < TARIHLER.length; i++) beklenen[i] = ref.parse(TARIHLER[i]).getTime();
49
50        // 1) Güvensiz: TÜM thread'ler AYNI SimpleDateFormat'ı paylaşır.
51        SimpleDateFormat paylasimli = new SimpleDateFormat("yyyy-MM-dd");
52        AtomicInteger sdfAnomali = new AtomicInteger();
53        calistir(t -> {
54            for (int i = 0; i < 3000; i++) {
55                try {
56                    if (paylasimli.parse(TARIHLER[t]).getTime() != beklenen[t]) sdfAnomali.incrementAndGet();
57                } catch (Exception e) {
58                    sdfAnomali.incrementAndGet(); // güvensizlik exception da fırlatabilir
59                }
60            }
61        });
62
63        // 2) Güvenli: DateTimeFormatter değişmezdir, paylaşılması sorun değil.
64        DateTimeFormatter dtf = DateTimeFormatter.ISO_DATE;
65        AtomicInteger dtfAnomali = new AtomicInteger();
66        calistir(t -> {
67            for (int i = 0; i < 3000; i++) {
68                LocalDate d = LocalDate.parse(TARIHLER[t], dtf);
69                if (!d.toString().equals(TARIHLER[t])) dtfAnomali.incrementAndGet();
70            }
71        });
72
73        System.out.println("Paylaşımlı SimpleDateFormat  -> " + sdfAnomali.get() + " anomali "
74                + (sdfAnomali.get() > 0 ? "❗ (sessiz veri bozulması / hata!)" : "(bu sefer görünmedi ama GÜVENSİZ)"));
75        System.out.println("Paylaşımlı DateTimeFormatter -> " + dtfAnomali.get() + " anomali (güvenli)");
76    }
77
78    // 8 thread'i çalıştırır; her thread'e kendi indeksini (t) verir.
79    interface Is { void calistir(int t); }
80    static void calistir(Is is) throws InterruptedException {
81        Thread[] threadler = new Thread[TARIHLER.length];
82        for (int i = 0; i < threadler.length; i++) {
83            final int t = i;
84            threadler[i] = new Thread(() -> is.calistir(t));
85            threadler[i].start();
86        }
87        for (Thread x : threadler) x.join();
88    }
89}
Çıktı yerel JDK 21 ile yakalandı — tarayıcıda JVM çalışmaz.

Ornek3

çalıştırılabilir
Ornek3.java
1// Ornek3: Geçişin SOMUT KAZANCI — platform thread'leri vs sanal thread'ler (Java 21).
2// G/Ç bekleyen çok sayıda görevde sanal thread'ler çok daha ölçeklenir.
3// Çalıştırma: java Ornek3.java   (JDK 21 gerektirir)
4import java.util.concurrent.ExecutorService;
5import java.util.concurrent.Executors;
6import java.util.concurrent.TimeUnit;
7import java.util.concurrent.atomic.AtomicInteger;
8
9public class Ornek3 {
10
11    static final int GOREV = 1000; // her biri ~50 ms G/Ç bekleyen görev
12
13    public static void main(String[] args) throws InterruptedException {
14        System.out.println(GOREV + " adet, her biri ~50 ms bekleyen G/Ç görevi.\n");
15
16        // 1) Eski yaklaşım: küçük, sabit boyutlu platform thread havuzu.
17        // Görevler kuyruğa girer; aynı anda yalnızca havuz boyutu kadarı çalışır -> yavaş.
18        try (ExecutorService platform = Executors.newFixedThreadPool(16)) {
19            long ms = sureOlc(platform);
20            System.out.printf("Platform thread havuzu (16) : ~%5d ms%n", ms);
21        }
22
23        // 2) Java 21: her göreve bir sanal thread. Milyonlarcası ucuzdur; G/Ç beklerken
24        // işletim sistemi thread'ini bloke etmezler -> neredeyse hepsi aynı anda ilerler.
25        try (ExecutorService sanal = Executors.newVirtualThreadPerTaskExecutor()) {
26            long ms = sureOlc(sanal);
27            System.out.printf("Sanal thread'ler (21)       : ~%5d ms%n", ms);
28        }
29
30        System.out.println("\nKazanım: G/Ç ağırlıklı yükte sanal thread'ler, kod neredeyse aynı kalırken");
31        System.out.println("çok daha yüksek eşzamanlılık sağlar. Spring Boot 3.2+ bunu destekler.");
32    }
33
34    static long sureOlc(ExecutorService exec) throws InterruptedException {
35        AtomicInteger biten = new AtomicInteger();
36        long t0 = System.currentTimeMillis();
37        for (int i = 0; i < GOREV; i++) {
38            exec.submit(() -> {
39                try { Thread.sleep(50); } catch (InterruptedException ignored) {}
40                biten.incrementAndGet();
41            });
42        }
43        exec.shutdown();
44        exec.awaitTermination(60, TimeUnit.SECONDS);
45        return System.currentTimeMillis() - t0;
46    }
47}
Çıktı yerel JDK 21 ile yakalandı — tarayıcıda JVM çalışmaz.