← Kursa Dön
📄 Text · 12 min

Optional Kullanımı

Giriş

NullPointerException — Java geliştiricilerinin en sık karşılaştığı hata. Tony Hoare, null referansı icat ettiği için bunu "milyar dolarlık hata" olarak nitelendirdi. Her yerde null kontrolü yapmak kodu çirkinleştirir, bir tane unutmak uygulamayı çökertir.

Java 8 ile gelen Optional sınıfı bu soruna zarif bir çözüm sunuyor. "Bu değer olmayabilir" bilgisini tip sistemine taşıyarak, null kontrollerini daha okunabilir ve güvenli hale getiriyor.

Optional Nedir?

Analoji: Hediye Kutusu

Optional'ı hediye kutusu gibi düşün. Kutunun içinde bir hediye olabilir veya kutu boş olabilir. Kutuyu açmadan önce "içinde bir şey var mı?" diye kontrol edersin. Kutunun kendisi hiçbir zaman null değil — kutu her zaman var, ama içi dolu veya boş olabilir.

İşte Optional tam olarak bu: bir değeri saran (wrap eden) konteyner. İçinde değer var (present) veya yok (empty).

Optional Oluşturma

Üç yol var:

import java.util.Optional;

// 1. Optional.of() — değer kesinlikle null değilse
Optional<String> isim = Optional.of("Ali");

// Optional.of(null) → NullPointerException fırlatır!
// Optional.of(null); // ❌ BOOM!

// 2. Optional.ofNullable() — değer null olabilirse
String deger = kullanicidanAl(); // null dönebilir
Optional<String> belki = Optional.ofNullable(deger);

// 3. Optional.empty() — boş Optional
Optional<String> bos = Optional.empty();

Hangisini ne zaman kullanırsın?

  • of() — Null olmadığından eminsen. Null gelirse hata istersin (fail-fast).

  • ofNullable() — Null gelebilir, sorun değil.

  • empty() — Bilinçli olarak "değer yok" demek istiyorsun.

// Tipik kullanım: bir method null dönebilir
public Optional<Kullanici> kullaniciBul(int id) {
    Kullanici k = veritabaninaBak(id);
    return Optional.ofNullable(k); // null ise empty, değilse wrapped
}

Değere Erişim

isPresent() ve isEmpty()

Optional<String> opt = Optional.of("Ali");

if (opt.isPresent()) {
    System.out.println("Değer: " + opt.get());
}

if (opt.isEmpty()) { // Java 11+
    System.out.println("Değer yok");
}

get() — Tehlikeli

Optional<String> opt = Optional.empty();
String deger = opt.get(); // ❌ NoSuchElementException!

⚠️ Dikkat: get() metodunu direkt kullanma. Boş Optional'da NoSuchElementException fırlatır. Bu, null kontrolü yapmadan erişmekle aynı şey — Optional'ın amacını yok eder. Aşağıdaki alternatifleri kullan.

ifPresent() — Güvenli Erişim

Optional<String> isim = Optional.of("Ali");

// Değer varsa çalıştır, yoksa hiçbir şey yapma
isim.ifPresent(i -> System.out.println("Merhaba " + i));

// Java 9+ — değer yoksa da bir şey yap
isim.ifPresentOrElse(
    i -> System.out.println("Merhaba " + i),
    () -> System.out.println("İsim bulunamadı")
);

Varsayılan Değer Stratejileri

orElse() — Sabit Varsayılan

Optional<String> opt = Optional.empty();

// Değer yoksa varsayılanı kullan
String isim = opt.orElse("Misafir");
System.out.println(isim); // Misafir

orElseGet() — Lazy Varsayılan

Optional<String> opt = Optional.empty();

// Değer yoksa Supplier çalıştır
String isim = opt.orElseGet(() -> veritabanindanVarsayilanGetir());

orElse vs orElseGet — Önemli Fark:

// orElse: Değer OLSA BİLE sağ taraf her zaman çalışır
String isim1 = Optional.of("Ali").orElse(pahaliIslem()); // pahaliIslem() ÇALIŞIR!

// orElseGet: Değer varsa sağ taraf ÇALIŞMAZ
String isim2 = Optional.of("Ali").orElseGet(() -> pahaliIslem()); // pahaliIslem() çalışmaz

orElse basit sabit değerler için, orElseGet pahalı işlemler (DB sorgusu, API çağrısı) için kullan.

orElseThrow() — Hata Fırlat

Optional<Kullanici> opt = kullaniciBul(42);

// Değer yoksa özel exception fırlat
Kullanici k = opt.orElseThrow(
    () -> new KullaniciBulunamadiException("ID: 42")
);

// Java 10+ — parametresiz versiyon (NoSuchElementException fırlatır)
Kullanici k2 = opt.orElseThrow();
// Pratik kullanım: Service katmanında
public Kullanici kullaniciGetir(int id) {
    return kullaniciRepository.findById(id)
        .orElseThrow(() -> new NotFoundException("Kullanıcı bulunamadı: " + id));
}

Dönüşüm: map() ve flatMap()

Optional'ın asıl gücü burada. Zincirleme dönüşümlerle temiz, okunabilir kod yazabilirsin.

map() — Değeri Dönüştür

Optional<String> isim = Optional.of("ali veli");

// İçindeki değeri dönüştür
Optional<String> buyukHarf = isim.map(String::toUpperCase);
System.out.println(buyukHarf.get()); // ALI VELI

// Boş Optional'da map hiçbir şey yapmaz
Optional<String> bos = Optional.<String>empty().map(String::toUpperCase);
System.out.println(bos.isPresent()); // false
// Zincirleme dönüşüm
Optional<String> sonuc = Optional.of("  ali veli  ")
    .map(String::trim)
    .map(String::toUpperCase)
    .map(s -> s.replace(" ", "_"));

System.out.println(sonuc.get()); // ALI_VELI

flatMap() — İç İçe Optional'ı Düzleştir

// Bir method Optional döndürüyorsa ve sen de Optional içindeysen
public Optional<String> emailBul(int kullaniciId) {
    return Optional.ofNullable(veritabanindanEmailAl(kullaniciId));
}

// map kullanırsan: Optional<Optional<String>> — iç içe!
Optional<Optional<String>> icice = Optional.of(42).map(this::emailBul);

// flatMap kullanırsan: Optional<String> — düz!
Optional<String> duz = Optional.of(42).flatMap(this::emailBul);
// Pratik örnek: Zincirleme navigation
public record Adres(String sehir) { }
public record Kullanici(String isim, Optional<Adres> adres) { }

Optional<Kullanici> kullanici = kullaniciBul(1);

// Kullanıcının şehrini güvenle al
String sehir = kullanici
    .flatMap(Kullanici::adres)         // Optional<Adres>
    .map(Adres::sehir)                 // Optional<String>
    .orElse("Bilinmiyor");             // String

Bu kodu Optional olmadan yazsaydın:

// Optional olmadan — null kontrol cehennemi
String sehir = "Bilinmiyor";
if (kullanici != null) {
    Adres adres = kullanici.getAdres();
    if (adres != null) {
        if (adres.getSehir() != null) {
            sehir = adres.getSehir();
        }
    }
}

Hangisi daha okunabilir? 🙂

filter() — Koşullu Filtreleme

Optional<Integer> sayi = Optional.of(42);

// Koşulu sağlıyorsa Optional döndür, sağlamıyorsa empty
Optional<Integer> cift = sayi.filter(n -> n % 2 == 0);
System.out.println(cift.isPresent()); // true

Optional<Integer> buyuk = sayi.filter(n -> n > 100);
System.out.println(buyuk.isPresent()); // false — 42 > 100 değil
// Pratik: Kullanıcı aktifse getir
Optional<Kullanici> aktifKullanici = kullaniciBul(1)
    .filter(Kullanici::isAktif);

or() — Alternatif Optional (Java 9)

Optional<String> birincil = Optional.empty();
Optional<String> yedek = Optional.of("Yedek değer");

// Birincil boşsa yedek Optional'ı dene
Optional<String> sonuc = birincil.or(() -> yedek);
System.out.println(sonuc.get()); // Yedek değer
// Zincirleme kaynak deneme
Optional<String> ayar = sistemdenOku()
    .or(() -> dosyadanOku())
    .or(() -> varsayilanDegerOku());

Stream ile Entegrasyon

// Optional → Stream (Java 9)
Optional<String> opt = Optional.of("Ali");
Stream<String> stream = opt.stream(); // 0 veya 1 elemanlı stream

// Listeden Optional'ları düzleştir
List<Optional<String>> optListesi = List.of(
    Optional.of("Ali"),
    Optional.empty(),
    Optional.of("Veli"),
    Optional.empty()
);

List<String> isimler = optListesi.stream()
    .flatMap(Optional::stream)
    .toList();
// [Ali, Veli] — boşlar otomatik atılır

Anti-Patterns — Yapma!

1. Optional'ı Method Parametresi Olarak Kullanma

// ❌ KÖTÜ — Optional parametre olarak
public void kaydet(Optional<String> isim) {
    String deger = isim.orElse("Varsayılan");
    // ...
}
kaydet(Optional.of("Ali"));
kaydet(Optional.empty());

// ✅ İYİ — Method overloading veya nullable parametre
public void kaydet(String isim) {
    // ...
}
public void kaydet() {
    kaydet("Varsayılan");
}

Neden? Optional parametre kullanmak API'yi karmaşıklaştırır. Çağıran taraf Optional.of() yazmak zorunda kalır. Optional dönüş tipi için tasarlandı, parametre için değil.

2. Optional'ı Field Olarak Kullanma

// ❌ KÖTÜ
public class Kullanici {
    private Optional<String> email; // Serialization sorunları + gereksiz overhead
}

// ✅ İYİ — Field nullable olsun, getter Optional döndürsün
public class Kullanici {
    private String email; // null olabilir

    public Optional<String> getEmail() {
        return Optional.ofNullable(email);
    }
}

3. isPresent() + get() Kullanmak

// ❌ KÖTÜ — Optional'ın amacını öldürüyor
Optional<String> opt = methodCagir();
if (opt.isPresent()) {
    String deger = opt.get();
    System.out.println(deger);
}

// ✅ İYİ
methodCagir().ifPresent(System.out::println);

// VEYA
String deger = methodCagir().orElse("varsayılan");

4. Optional.of() ile null Sarma

// ❌ KÖTÜ — NPE!
String deger = null;
Optional<String> opt = Optional.of(deger); // NullPointerException!

// ✅ İYİ
Optional<String> opt = Optional.ofNullable(deger);

5. Koleksiyonları Optional ile Sarma

// ❌ KÖTÜ — boş liste yeterli
public Optional<List<String>> isimleriGetir() {
    List<String> isimler = bul();
    return isimler.isEmpty() ? Optional.empty() : Optional.of(isimler);
}

// ✅ İYİ — boş liste döndür
public List<String> isimleriGetir() {
    List<String> isimler = bul();
    return isimler != null ? isimler : List.of();
}

💡 Altın Kural: Optional'ı sadece method dönüş tipi olarak kullan. Parametre, field veya koleksiyon sarmalayıcı olarak kullanma.

Tam Örnek: Service Katmanı

public class KullaniciService {
    private final KullaniciRepository repo;

    public KullaniciService(KullaniciRepository repo) {
        this.repo = repo;
    }

    // Optional döndür — "bulunamayabilir" mesajını tip sistemiyle ver
    public Optional<Kullanici> idIleBul(int id) {
        return repo.findById(id);
    }

    // Kesinlikle bulunmalı — exception fırlat
    public Kullanici idIleGetir(int id) {
        return repo.findById(id)
            .orElseThrow(() -> new NotFoundException("Kullanıcı bulunamadı: " + id));
    }

    // Zincirleme işlem
    public String kullaniciSehri(int id) {
        return repo.findById(id)
            .flatMap(Kullanici::getAdres)
            .map(Adres::getSehir)
            .orElse("Belirtilmemiş");
    }

    // Filtreleme + dönüşüm
    public Optional<String> aktifKullaniciEmail(int id) {
        return repo.findById(id)
            .filter(Kullanici::isAktif)
            .flatMap(Kullanici::getEmail);
    }
}

Pratik Örnek: E-Ticaret Sepet

Gerçek bir senaryoda Optional'ın zincirleme gücünü görelim:

public record Urun(String adi, double fiyat) { }
public record SepetItem(Urun urun, int adet) { }

public class Sepet {
    private final Map<String, SepetItem> items = new HashMap<>();

    public void ekle(String urunId, Urun urun, int adet) {
        items.put(urunId, new SepetItem(urun, adet));
    }

    public Optional<SepetItem> itemBul(String urunId) {
        return Optional.ofNullable(items.get(urunId));
    }

    // Ürünün toplam fiyatını al
    public double urunToplam(String urunId) {
        return itemBul(urunId)
            .map(item -> item.urun().fiyat() * item.adet())
            .orElse(0.0);
    }

    // Ürün adını al
    public String urunAdi(String urunId) {
        return itemBul(urunId)
            .map(SepetItem::urun)
            .map(Urun::adi)
            .orElse("Bilinmeyen ürün");
    }

    // Belirli tutarın üstündeki ürünleri bul
    public Optional<SepetItem> pahaliUrunBul(String urunId, double esik) {
        return itemBul(urunId)
            .filter(item -> item.urun().fiyat() > esik);
    }
}

Kullanım

Sepet sepet = new Sepet();
sepet.ekle("P001", new Urun("Laptop", 25000), 1);
sepet.ekle("P002", new Urun("Mouse", 350), 2);

System.out.println(sepet.urunToplam("P001"));  // 25000.0
System.out.println(sepet.urunToplam("P999"));  // 0.0 (ürün yok)
System.out.println(sepet.urunAdi("P002"));      // Mouse
System.out.println(sepet.urunAdi("P999"));      // Bilinmeyen ürün

sepet.pahaliUrunBul("P001", 10000)
    .ifPresent(item -> System.out.println("Pahalı: " + item.urun().adi()));

Pratik Örnek: Yapılandırma Okuyucu

Birden fazla kaynaktan yapılandırma okuma — Optional zincirleme:

public class YapilandirmaOkuyucu {

    // Sırasıyla: çevre değişkeni → sistem özelliği → varsayılan dosya
    public String degerOku(String anahtar) {
        return cevredenOku(anahtar)
            .or(() -> sistemOzelligindenOku(anahtar))
            .or(() -> dosyadanOku(anahtar))
            .orElseThrow(() ->
                new IllegalStateException("Yapılandırma bulunamadı: " + anahtar));
    }

    private Optional<String> cevredenOku(String anahtar) {
        return Optional.ofNullable(System.getenv(anahtar));
    }

    private Optional<String> sistemOzelligindenOku(String anahtar) {
        return Optional.ofNullable(System.getProperty(anahtar));
    }

    private Optional<String> dosyadanOku(String anahtar) {
        // Dosyadan oku — yoksa empty
        return Optional.empty(); // Basitleştirilmiş
    }
}

Bu pattern çok güçlü — fallback zincirleri oluşturabilirsin.

Optional ve Primitive Tipler

Optional<Integer> kullanmak boxing/unboxing maliyeti getirir. Java bunu çözmek için primitive Optional sınıfları sunar:

OptionalInt sayi = OptionalInt.of(42);
OptionalDouble ondalik = OptionalDouble.of(3.14);
OptionalLong buyuk = OptionalLong.of(1_000_000_000L);

// Kullanım
int deger = sayi.orElse(0);
sayi.ifPresent(s -> System.out.println("Sayı: " + s));

// Stream'den dönen
OptionalInt max = IntStream.of(3, 1, 4, 1, 5, 9)
    .max();
System.out.println(max.orElse(-1)); // 9
// OptionalInt'in map'i yok! getAsInt() veya orElse() kullanman gerekir
OptionalInt opt = OptionalInt.of(42);
// opt.map(...) // ❌ Yok!
int deger = opt.orElse(0); // ✅

⚠️ Not: OptionalInt, OptionalDouble, OptionalLong sınıflarında map(), flatMap(), filter() metotları yok. Bu yüzden zincirleme dönüşümler gerekiyorsa Optional<Integer> kullanmak zorunda kalabilirsin. Performans kritik döngülerde primitive versiyonları tercih et.

Optional ve Exceptions

Optional ile exception fırlatma stratejileri:

// Farklı exception tipleri
Kullanici k1 = optKullanici
    .orElseThrow(() -> new NotFoundException("Bulunamadı"));

Kullanici k2 = optKullanici
    .orElseThrow(() -> new BusinessException("İşlem yapılamaz"));

Kullanici k3 = optKullanici
    .orElseThrow(IllegalStateException::new); // Method reference
// Optional'dan checked exception fırlatma — biraz uğraştırır
public Kullanici kullaniciGetir(int id) throws KullaniciBulunamadiException {
    return repo.findById(id)
        .orElseThrow(() -> new KullaniciBulunamadiException(id));
    // orElseThrow Supplier alır, checked exception sorunsuz çalışır
}

Method Tasarımı: Ne Zaman Optional Döndür?

// ✅ Tek sonuç, olmayabilir
public Optional<Kullanici> emailIleBul(String email) { ... }

// ✅ İlk eşleşme, olmayabilir
public Optional<Urun> enUcuzUrunBul(String kategori) { ... }

// ❌ Liste döndürüyorsan Optional gereksiz — boş liste döndür
public List<Urun> urunleriGetir(String kategori) {
    // return Optional.ofNullable(sonuclar); // ❌
    return sonuclar != null ? sonuclar : List.of(); // ✅
}

// ❌ Boolean döndürüyorsan Optional gereksiz
public boolean aktifMi(int id) { ... } // ✅ Basit boolean yeter

// ❌ Primitive döndürüyorsan dikkatli ol
// OptionalInt veya varsayılan değer kullan
public OptionalInt enYuksekNotu(int ogrenciId) { ... }

Optional Cheat Sheet

// Oluşturma
Optional.of(deger)          // null → NPE
Optional.ofNullable(deger)  // null → empty
Optional.empty()            // Boş

// Değer alma
opt.get()                   // ❌ Tehlikeli
opt.orElse(varsayilan)      // Sabit varsayılan
opt.orElseGet(supplier)     // Lazy varsayılan
opt.orElseThrow(supplier)   // Exception

// Kontrol
opt.isPresent()             // Boolean kontrol
opt.isEmpty()               // Java 11+
opt.ifPresent(consumer)     // Varsa çalıştır
opt.ifPresentOrElse(c, r)   // Java 9+

// Dönüşüm
opt.map(fonksiyon)          // T → U
opt.flatMap(fonksiyon)      // T → Optional<U>
opt.filter(predicate)       // Koşullu filtreleme
opt.or(supplier)            // Java 9+ — alternatif Optional
opt.stream()                // Java 9+ — 0 veya 1 elemanlı Stream

Optional ve Spring / Framework Entegrasyonu

Modern framework'ler Optional'ı doğal olarak destekler:

Spring Data JPA

public interface KullaniciRepository extends JpaRepository<Kullanici, Long> {
    // Spring Data otomatik Optional döndürür
    Optional<Kullanici> findByEmail(String email);
    Optional<Kullanici> findByKullaniciAdi(String kullaniciAdi);
}
@Service
public class KullaniciService {
    private final KullaniciRepository repo;

    public KullaniciDTO profilGetir(String email) {
        return repo.findByEmail(email)
            .filter(Kullanici::isAktif)
            .map(this::toDTO)
            .orElseThrow(() -> new NotFoundException("Kullanıcı bulunamadı"));
    }
}

Spring MVC Controller

@GetMapping("/kullanicilar/{id}")
public ResponseEntity<KullaniciDTO> kullaniciGetir(@PathVariable Long id) {
    return repo.findById(id)
        .map(this::toDTO)
        .map(ResponseEntity::ok)
        .orElse(ResponseEntity.notFound().build());
}

Optional ve Diğer Diller

Java'nın Optional'ı diğer dillerdeki benzer yapılardan ilham almış:

  • ScalaOption[T] (Some / None)

  • Kotlin? nullable types (String?)

  • RustOption<T> (Some / None)

  • SwiftOptional<T> (String?)

Kotlin'in yaklaşımı farklı: dil seviyesinde null safety var, ayrı bir tip gerekmez. Java'da Optional bir sınıf olduğu için overhead var, ama mevcut ekosisteme uyum sağlıyor.

⚠️ Performans Notu: Optional her kullanıldığında Heap'te bir nesne oluşturur. Çok sıcak döngülerde (milyonlarca iterasyon) bu overhead fark edilebilir. Ama normal iş mantığında endişelenme — okunabilirlik performanstan daha önemli.

Refactoring: null → Optional

Mevcut kodu Optional'a nasıl geçirirsin?

Adım 1: Null döndüren method'ları Optional'a çevir

// ÖNCE
public Kullanici kullaniciBul(int id) {
    Kullanici k = db.query(id);
    return k; // null olabilir
}

// SONRA
public Optional<Kullanici> kullaniciBul(int id) {
    Kullanici k = db.query(id);
    return Optional.ofNullable(k);
}

Adım 2: Çağıran kodu güncelle

// ÖNCE
Kullanici k = service.kullaniciBul(42);
if (k != null) {
    System.out.println(k.getIsim());
}

// SONRA
service.kullaniciBul(42)
    .ifPresent(k -> System.out.println(k.getIsim()));

Adım 3: Null check zincirleri temizle

// ÖNCE — iç içe null kontrol
String sehir = null;
Kullanici k = service.kullaniciBul(42);
if (k != null) {
    Adres a = k.getAdres();
    if (a != null) {
        sehir = a.getSehir();
    }
}
if (sehir == null) sehir = "Bilinmiyor";

// SONRA — zincirleme Optional
String sehir = service.kullaniciBul(42)
    .map(Kullanici::getAdres)
    .map(Adres::getSehir)
    .orElse("Bilinmiyor");

Özet

  • Optional "bu değer olmayabilir" bilgisini tip sisteminde ifade eder — Optional.of(), Optional.ofNullable(), Optional.empty() ile oluşturulur

  • orElse sabit varsayılan, orElseGet lazy varsayılan (pahalı işlemlerde tercih et), orElseThrow hata fırlatma

  • map() değeri dönüştürür, flatMap() iç içe Optional'ı düzleştirir, filter() koşullu filtreleme yapar

  • get() yerine her zaman orElse, orElseThrow veya ifPresent kullan

  • Optional'ı sadece method dönüş tipi olarak kullan — parametre, field veya koleksiyon sarmalayıcı olarak kullanma

  • isPresent() + get() yerine fonksiyonel method'ları tercih et — daha temiz, daha güvenli