← Kursa Dön
📄 Text · 12 min

Sealed Classes ve Records

Sealed Class Nedir?

Sealed class, kendisinden kimlerin türeyebileceğini kontrol eden bir sınıftır. Normalde bir sınıfı final yapmazsan herkes extend edebilir. Sealed class, "sadece şu sınıflar benden türeyebilir" demenin yolu.

Bunu bir apartman kapısı gibi düşün. Normal sınıf: kapı sonuna kadar açık, herkes girebilir. Final sınıf: kapı kilitli, kimse giremez. Sealed sınıf: kapıda güvenlik var, sadece listede olanlar girebilir.

Sealed Class Sözdizimi

sealed class Shape permits Circle, Rectangle, Triangle {
    String color;

    Shape(String color) {
        this.color = color;
    }
}

final class Circle extends Shape {
    double radius;

    Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }
}

final class Rectangle extends Shape {
    double width, height;

    Rectangle(String color, double w, double h) {
        super(color);
        this.width = w;
        this.height = h;
    }
}

final class Triangle extends Shape {
    double base, height;

    Triangle(String color, double base, double height) {
        super(color);
        this.base = base;
        this.height = height;
    }
}

// class Pentagon extends Shape { } // HATA! permits listesinde yok

sealed keyword'ü sınıfın mühürlü olduğunu, permits ise hangi sınıfların türeyebileceğini belirtir.

Permits Alt Sınıf Kuralları

permits listesindeki her sınıf şu üç modifier'dan birini kullanmak zorunda:

  1. final — Bu sınıftan artık kimse türeyemez (zincir burada biter)

  2. sealed — Bu sınıf da kendi alt sınıflarını kontrol eder (zincir devam)

  3. non-sealed — Bu sınıftan herkes türeyebilir (zincir açılır)

sealed class Vehicle permits Car, Truck, Motorcycle {
}

final class Motorcycle extends Vehicle {
    // Kimse Motorcycle'dan türeyemez
}

sealed class Car extends Vehicle permits Sedan, SUV {
    // Sadece Sedan ve SUV türeyebilir
}

non-sealed class Truck extends Vehicle {
    // Herkes Truck'tan türeyebilir
}

final class Sedan extends Car { }
final class SUV extends Car { }

class PickupTruck extends Truck { }  // OK! Truck non-sealed
class MonsterTruck extends Truck { } // OK!

Sealed Class Neden Kullanışlı?

1. Tüm alt tipleri bilmek:

Sealed class ile compiler tüm olası alt tipleri biliyor. Bu, switch ifadelerinde exhaustiveness (tamlık) kontrolü sağlar.

sealed interface PaymentMethod permits CreditCard, Cash, BankTransfer { }
record CreditCard(String number) implements PaymentMethod { }
record Cash(double amount) implements PaymentMethod { }
record BankTransfer(String iban) implements PaymentMethod { }
// Java 21+ pattern matching switch
String describe(PaymentMethod pm) {
    return switch (pm) {
        case CreditCard cc -> "Card: " + cc.number();
        case Cash c -> "Cash: " + c.amount();
        case BankTransfer bt -> "Transfer: " + bt.iban();
        // default gerekmiyor! Compiler tüm case'lerin kapsandığını biliyor
    };
}

Yeni bir PaymentMethod eklersen, bu switch ifadesi derleme hatası verir — hiçbir case'i kaçırmazsın.

2. Domain modelleme:

sealed interface HttpResponse permits Success, ClientError, ServerError { }
record Success(int code, String body) implements HttpResponse { }
record ClientError(int code, String message) implements HttpResponse { }
record ServerError(int code, String message) implements HttpResponse { }

3. Güvenlik:

Kütüphane yazarı olarak alt sınıflandırmayı kontrol edebilirsin. Kimsenin beklenmedik bir alt sınıf oluşturarak sistemi bozmasını önlersin.

💡 İpucu: Sealed class'lar "algebraic data types" kavramının Java karşılığıdır. Fonksiyonel programlama dillerindeki (Haskell, Rust, Kotlin) enum + data class kombinasyonuna benzer.

Sealed Interface

Interface'ler de sealed olabilir:

sealed interface Result<T> permits Success, Failure {
}

record Success<T>(T value) implements Result<T> { }
record Failure<T>(String error) implements Result<T> { }
Result<String> result = new Success<>("Hello");

String output = switch (result) {
    case Success<String> s -> "OK: " + s.value();
    case Failure<String> f -> "Error: " + f.error();
};

System.out.println(output); // OK: Hello

Record Sınıflar Nedir?

Record, sadece veri taşıyan sınıflar için kısa yol sözdizimidir. Java 16 ile geldi. Getter, constructor, equals(), hashCode() ve toString() otomatik oluşturulur.

Düşün ki bir veri kartı yapıyorsun — ad, soyad, numara. Bunun için 50 satır boilerplate kod yazmak yerine, record ile 1 satırda halledersin.

Record Sözdizimi

// Eski yol — ~40 satır
class Point {
    private final int x;
    private final int y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    int x() { return x; }
    int y() { return y; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point p)) return false;
        return x == p.x && y == p.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point[x=" + x + ", y=" + y + "]";
    }
}
// Record ile — 1 satır!
record Point(int x, int y) { }

Bu tek satır, yukarıdaki tüm kodu otomatik oluşturur. Aynı işlevsellik, minimum yazım.

Point p1 = new Point(3, 5);
Point p2 = new Point(3, 5);

System.out.println(p1.x());        // 3 (getter — x() şeklinde)
System.out.println(p1.y());        // 5
System.out.println(p1);            // Point[x=3, y=5]
System.out.println(p1.equals(p2)); // true (değer karşılaştırması)
System.out.println(p1.hashCode() == p2.hashCode()); // true

Record'un Özellikleri

Record'lar immutable (değiştirilemez). Alanlar private final'dır.

record Student(String name, int id, double gpa) { }

Student s = new Student("Ali", 101, 3.75);
// s.name = "Veli"; // HATA! final alan
// Setter yok — değiştirmek istersen yeni nesne oluştur

Record'ların kısıtlamaları:

  • Başka sınıftan extend edemez (implicit olarak Record sınıfından türer)

  • Instance alanı eklenemez (sadece header'daki parametreler)

  • abstract olamaz

  • Ama interface implement edebilir

interface Printable {
    void print();
}

record Product(String name, double price) implements Printable {
    @Override
    public void print() {
        System.out.println(name + ": " + price + " TL");
    }
}

Compact Constructor

Record'larda validasyon yapmak için compact constructor kullanabilirsin. Parametreleri ve atamaları tekrar yazmana gerek yok.

record Age(int value) {
    // Compact constructor — parametre listesi yok
    Age {
        if (value < 0 || value > 150) {
            throw new IllegalArgumentException("Invalid age: " + value);
        }
    }
}
Age valid = new Age(25);    // OK
Age invalid = new Age(-5);  // IllegalArgumentException!

Compact constructor'da this.value = value; yazmana gerek yok — Java bunu otomatik yapar. Sen sadece validasyon veya dönüşüm mantığını yazarsın.

record Email(String address) {
    Email {
        // Validasyon
        if (address == null || !address.contains("@")) {
            throw new IllegalArgumentException("Invalid email: " + address);
        }
        // Dönüşüm — normalizasyon
        address = address.toLowerCase().trim();
    }
}
Email e = new Email("  Ali@Mail.COM  ");
System.out.println(e.address()); // ali@mail.com

⚠️ Dikkat: Compact constructor'da parametreye yeniden atama yapabilirsin (address = address.toLowerCase()). Java, constructor sonunda bu değeri this.address'e otomatik atar.

Record ile Custom Metotlar

Record'lara kendi metotlarını ekleyebilirsin:

record Rectangle(double width, double height) {
    double area() {
        return width * height;
    }

    double perimeter() {
        return 2 * (width + height);
    }

    Rectangle scale(double factor) {
        return new Rectangle(width * factor, height * factor);
    }
}
Rectangle r = new Rectangle(4, 5);
System.out.println(r.area());      // 20.0
System.out.println(r.perimeter()); // 18.0

Rectangle bigger = r.scale(2);
System.out.println(bigger);        // Rectangle[width=8.0, height=10.0]

Sealed + Record Birlikte

Sealed class/interface ve record birlikte kullanıldığında çok güçlü domain modelleri oluşur.

sealed interface Expression permits
        Literal, Add, Multiply, Negate {
}

record Literal(double value) implements Expression { }
record Add(Expression left, Expression right) implements Expression { }
record Multiply(Expression left, Expression right) implements Expression { }
record Negate(Expression expr) implements Expression { }
double evaluate(Expression expr) {
    return switch (expr) {
        case Literal l -> l.value();
        case Add a -> evaluate(a.left()) + evaluate(a.right());
        case Multiply m -> evaluate(m.left()) * evaluate(m.right());
        case Negate n -> -evaluate(n.expr());
    };
}
// (3 + 5) * 2
Expression expr = new Multiply(
    new Add(new Literal(3), new Literal(5)),
    new Literal(2)
);

System.out.println(evaluate(expr)); // 16.0

Bu pattern, compiler-yazımı, matematiksel ifade değerlendirme, AST (Abstract Syntax Tree) gibi alanlarda çok kullanılır.

Ne Zaman Record Kullanılır?

Record kullan:

  • Sadece veri taşıyan sınıflar (DTO, Value Object)

  • Immutability istediğin durumlar

  • equals() / hashCode() / toString() boilerplate'inden kurtulmak istediğinde

  • API response/request modelleri

Record kullanma:

  • Değiştirilebilir (mutable) duruma ihtiyaç varsa

  • Kalıtım gerekiyorsa (başka sınıftan extend etmek)

  • Karmaşık iş mantığı olan sınıflar

// İyi record kullanımı
record Coordinate(double lat, double lon) { }
record Money(BigDecimal amount, String currency) { }
record ApiResponse(int status, String body) { }

// Kötü record kullanımı — bunlar record olmamalı
// record UserService(...) { } — servis sınıfı
// record DatabaseConnection(...) { } — durum yönetimi var

Ne Zaman Sealed Class Kullanılır?

Sealed class kullan:

  • Sabit sayıda alt tip varsa (enum yetmediğinde)

  • Pattern matching ile exhaustive switch istiyorsan

  • Kütüphane/framework yazıyorsan ve alt sınıflandırmayı kontrol etmek istiyorsan

  • Domain modelinde olası tipleri kısıtlamak istiyorsan

// Enum yetmiyor çünkü her variant farklı veri taşıyor
sealed interface Shape permits Circle, Rectangle {
}
record Circle(double radius) implements Shape { }
record Rectangle(double w, double h) implements Shape { }

// Enum yeterli — her değer aynı yapıda
enum Color { RED, GREEN, BLUE }

💡 İpucu: Sealed class'lar ve record'lar Java'nın modern yüzü. Java 17+ projelerde bu özellikler DTO'lar, domain modeller ve pattern matching ile birlikte yoğun şekilde kullanılıyor.

Gerçekçi Örnek: JSON Parse Sonucu

sealed interface JsonValue permits
        JsonString, JsonNumber, JsonBoolean, JsonNull, JsonArray, JsonObject {
}

record JsonString(String value) implements JsonValue { }
record JsonNumber(double value) implements JsonValue { }
record JsonBoolean(boolean value) implements JsonValue { }
record JsonNull() implements JsonValue { }
record JsonArray(List<JsonValue> elements) implements JsonValue { }
record JsonObject(Map<String, JsonValue> fields) implements JsonValue { }
String stringify(JsonValue value) {
    return switch (value) {
        case JsonString s -> "\"" + s.value() + "\"";
        case JsonNumber n -> String.valueOf(n.value());
        case JsonBoolean b -> String.valueOf(b.value());
        case JsonNull ignored -> "null";
        case JsonArray a -> "[" +
                a.elements().stream()
                        .map(e -> stringify(e))
                        .reduce((x, y) -> x + ", " + y)
                        .orElse("") + "]";
        case JsonObject o -> "{...}";
    };
}

Bu örnekte sealed interface tüm olası JSON tiplerini tanımlıyor, record'lar her tip için veriyi taşıyor, pattern matching ile güvenli bir şekilde işleniyor. Yeni bir JSON tipi eklersen (olası değil ama), tüm switch ifadeleri seni uyarır.


Özet

  • Sealed class (sealed ... permits), kendisinden türeyebilecek sınıfları kısıtlar; alt sınıflar final, sealed veya non-sealed olmalıdır.

  • Sealed class'lar compiler'a tüm alt tipleri bildirdiği için exhaustive switch (tamlık kontrolü) mümkün olur.

  • Record (record Name(params)) sadece veri taşıyan immutable sınıflar için kısa yol sözdizimidir; constructor, getter, equals(), hashCode(), toString() otomatik oluşturulur.

  • Compact constructor ile record'larda validasyon ve normalizasyon yapılabilir.

  • Sealed + record birlikte kullanıldığında güçlü algebraic data types oluşur — domain modelleme ve pattern matching için ideal.

  • Record kullan: DTO, value object, API modelleri. Kullanma: mutable state, karmaşık iş mantığı, kalıtım gereken durumlar.