← Kursa Dön
📄 Text · 12 min

Records ve Text Blocks

Giriş

Java'nın en büyük eleştirilerinden biri her zaman boilerplate oldu. Bir veri taşıyan sınıf yazmak istiyorsun, ama constructor, getter, equals, hashCode, toString yazman gerekiyor. 50 satır kod, 5 satırlık iş için.

Java 16 ile gelen Record ve Java 15 ile gelen Text Block tam bu acıyı çözmek için var. İkisi de "daha az yaz, daha çok iş yap" felsefesinin ürünü.

Record Nedir?

Record, immutable veri taşıyıcı sınıf yazmak için kısayol. Eskiden 40 satırda yazdığın şeyi tek satırda yazıyorsun.

Analoji: Kimlik Kartı

Record'u kimlik kartı gibi düşün. Üzerinde isim, TC no, doğum tarihi yazıyor. Bu bilgileri değiştiremezsin — yeni kimlik kartı çıkartman gerekir. Aynı bilgilere sahip iki kimlik kartı eşittir. Ve kartı birine gösterdiğinde tüm bilgiler okunabilir. İşte record tam olarak bu: değiştirilemez, karşılaştırılabilir, okunabilir veri taşıyıcı.

Eski Yol vs Record

// ESKİ YOL — 35+ satır boilerplate
public class Ogrenci {
    private final String isim;
    private final int yas;

    public Ogrenci(String isim, int yas) {
        this.isim = isim;
        this.yas = yas;
    }

    public String getIsim() { return isim; }
    public int getYas() { return yas; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Ogrenci ogrenci = (Ogrenci) o;
        return yas == ogrenci.yas && Objects.equals(isim, ogrenci.isim);
    }

    @Override
    public int hashCode() {
        return Objects.hash(isim, yas);
    }

    @Override
    public String toString() {
        return "Ogrenci[isim=" + isim + ", yas=" + yas + "]";
    }
}
// RECORD — tek satır, aynı iş
public record Ogrenci(String isim, int yas) { }

Bu tek satır sana şunları otomatik verir:

  • Constructor: new Ogrenci("Ali", 20)

  • Accessor method'lar: ogrenci.isim(), ogrenci.yas() (get prefix'i yok!)

  • equals(): Tüm field'lar karşılaştırılır

  • hashCode(): Tüm field'lardan hesaplanır

  • toString(): Ogrenci[isim=Ali, yas=20]

Kullanım

public record Ogrenci(String isim, int yas) { }

public class Main {
    public static void main(String[] args) {
        Ogrenci o1 = new Ogrenci("Ali", 20);
        Ogrenci o2 = new Ogrenci("Ali", 20);

        System.out.println(o1.isim());    // Ali (getter değil, accessor)
        System.out.println(o1.yas());     // 20
        System.out.println(o1);           // Ogrenci[isim=Ali, yas=20]
        System.out.println(o1.equals(o2)); // true — aynı değerler
    }
}

Record'un Kuralları

Record'lar bazı kısıtlamalara sahip:

  1. Field'lar final — Oluşturulduktan sonra değiştirilemez (immutable)

  2. Başka sınıfı extend edemez — Zaten java.lang.Record'u extend eder

  3. Instance field ekleyemezsin — Sadece header'da tanımlanan component'ler var

  4. Abstract olamaz — Record somut bir veri taşıyıcı

// BUNLAR OLMAZ:
public record Ogrenci(String isim, int yas) {
    private String ekAlan;  // ❌ Ek instance field yasak

    // o1.isim = "Veli"; // ❌ Setter yok, field'lar final
}

// BUNLAR OLUR:
public record Ogrenci(String isim, int yas) {
    // Static field olur
    static int sayac = 0;

    // Static method olur
    static Ogrenci olustur(String isim) {
        return new Ogrenci(isim, 18);
    }

    // Instance method olur
    String buyukHarf() {
        return isim.toUpperCase();
    }
}

💡 Not: Record interface implement edebilir. public record Ogrenci(String isim, int yas) implements Serializable { } geçerli.

Compact Constructor

Bazen constructor'da validasyon yapmak istersin. Record'da bunu compact constructor ile yaparsın:

public record Ogrenci(String isim, int yas) {

    // Compact constructor — parametre listesi yok, this.x = x atama yok
    public Ogrenci {
        if (isim == null || isim.isBlank()) {
            throw new IllegalArgumentException("İsim boş olamaz");
        }
        if (yas < 0 || yas > 150) {
            throw new IllegalArgumentException("Geçersiz yaş: " + yas);
        }
        // isim ve yas otomatik atanır, sen sadece validasyon yap
        isim = isim.trim(); // Atamadan önce değiştirebilirsin
    }
}

Compact constructor'ın güzelliği: parametre ataması (this.isim = isim) otomatik yapılır. Sen sadece kontrol ve dönüşüm kodunu yazarsın.

Normal (canonical) constructor da yazabilirsin ama compact daha temiz:

// Canonical constructor — explicit
public record Ogrenci(String isim, int yas) {
    public Ogrenci(String isim, int yas) {
        if (yas < 0) throw new IllegalArgumentException("Negatif yaş");
        this.isim = isim;  // Manuel atama gerekir
        this.yas = yas;
    }
}

Record ve Override

Otomatik oluşturulan method'ları override edebilirsin:

public record Nokta(double x, double y) {

    @Override
    public String toString() {
        return String.format("(%.2f, %.2f)", x, y);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Nokta n)) return false;
        // Kayan nokta karşılaştırması — epsilon ile
        return Math.abs(x - n.x) < 0.001 && Math.abs(y - n.y) < 0.001;
    }
}

⚠️ Dikkat: equals() override edersen hashCode() da override et. İkisi tutarlı olmalı.

Record Ne Zaman Kullanılır?

Kullan:

  • DTO (Data Transfer Object) — API response, request body

  • Value object — Para, Koordinat, Adres

  • Map key — equals/hashCode otomatik geldiği için güvenli

  • Method'dan birden fazla değer döndürme

// Method'dan iki değer döndür
public record Sonuc(boolean basarili, String mesaj) { }

public Sonuc kaydet(Ogrenci o) {
    if (o.yas() < 18) return new Sonuc(false, "Yaş yetersiz");
    // ... kaydet
    return new Sonuc(true, "Kaydedildi");
}

Kullanma:

  • JPA Entity — Entity'ler mutable olmalı, default constructor gerekir

  • Mutable state gereken yerler — Record immutable

  • Kalıtım gereken yerler — Record extend edilemez

Text Blocks (Java 15)

Birden fazla satırlık String yazmak Java'da hep sıkıntıydı. JSON, SQL, HTML yazarken kaçış karakterleri (\", \n) ile boğuşurdun.

Text Block bu sorunu çözüyor: """ ile başla, """ ile bitir, aradaki her şey olduğu gibi String.

Eski Yol vs Text Block

// ESKİ YOL — kaçış karakteri cehennemi
String json = "{\n" +
    "  \"isim\": \"Ali\",\n" +
    "  \"yas\": 20,\n" +
    "  \"aktif\": true\n" +
    "}";

// TEXT BLOCK — temiz, okunabilir
String json = """
        {
          "isim": "Ali",
          "yas": 20,
          "aktif": true
        }
        """;

SQL Örneği

// Eski yol
String sql = "SELECT o.isim, o.yas " +
    "FROM ogrenciler o " +
    "JOIN dersler d ON o.id = d.ogrenci_id " +
    "WHERE o.yas > 18 " +
    "ORDER BY o.isim";

// Text Block
String sql = """
        SELECT o.isim, o.yas
        FROM ogrenciler o
        JOIN dersler d ON o.id = d.ogrenci_id
        WHERE o.yas > 18
        ORDER BY o.isim
        """;

HTML Örneği

String html = """
        <html>
            <body>
                <h1>Merhaba Dünya</h1>
                <p>Java Text Blocks harika!</p>
            </body>
        </html>
        """;

Text Block Girintileme (Indentation)

Text Block'larda girintileme akıllıca çalışır. Kapanış """ ifadesinin konumu, sol kenar boşluğunu belirler.

// Kapanış """ sola yaslanmış — tüm girinti korunur
String a = """
        Merhaba
            Dünya
        """;
// Sonuç:
// Merhaba
//     Dünya

// Kapanış """ sağa kaydırılmış — daha az girinti
String b = """
            Merhaba
                Dünya
            """;
// Sonuç aynı — göreceli girinti korunur

Kuralı basit: JVM tüm satırlardaki ortak baştaki boşlukları siler. stripIndent() bunu otomatik yapar.

Text Block Özel Karakterler

// Satır sonundaki \ ile satır birleştirme
String tek = """
        Bu çok uzun bir cümlenin \
        tek satırda birleştirilmiş hali""";
// Sonuç: "Bu çok uzun bir cümlenin tek satırda birleştirilmiş hali"

// \s ile satır sonundaki boşlukları koruma
String korunmus = """
        isim:  Ali   \s
        yas:   20    \s
        """;

// formatted() ile değişken kullanımı
String sablon = """
        Merhaba %s,
        Yaşınız: %d
        """.formatted("Ali", 20);

💡 İpucu: Text Block'ta " karakteri kaçış gerektirmez. """ gerektiğinde \""" yaz.

Record + Text Block Birlikte

Bu iki özellik birlikte çok güçlü:

public record ApiResponse(int status, String body) {

    String toJson() {
        return """
                {
                  "status": %d,
                  "body": "%s"
                }
                """.formatted(status, body);
    }
}

public class Main {
    public static void main(String[] args) {
        var response = new ApiResponse(200, "Başarılı");
        System.out.println(response.toJson());
    }
}
// Record ile config tanımlama
public record DbConfig(String host, int port, String dbName) {

    String connectionUrl() {
        return """
                jdbc:postgresql://%s:%d/%s\
                ?sslmode=require\
                &connectTimeout=30
                """.formatted(host, port, dbName);
    }
}

Local Record (Java 16)

Record'u method içinde de tanımlayabilirsin. Geçici veri gruplamak için mükemmel:

public List<String> enYuksekNotlar(List<Ogrenci> ogrenciler) {
    // Sadece bu method'da kullanılacak geçici veri yapısı
    record OgrenciNot(String isim, double ortalama) { }

    return ogrenciler.stream()
        .map(o -> new OgrenciNot(o.isim(), hesaplaOrtalama(o)))
        .filter(on -> on.ortalama() > 85)
        .map(OgrenciNot::isim)
        .toList();
}

Bu pattern özellikle Stream işlemlerinde ara veri taşımak için çok kullanışlı.

Record ve Collections

Record'ların otomatik equals/hashCode'u, koleksiyonlarda kullanımı çok güvenli yapar.

Map Key Olarak

public record Koordinat(int x, int y) { }

Map<Koordinat, String> harita = new HashMap<>();
harita.put(new Koordinat(0, 0), "Başlangıç");
harita.put(new Koordinat(5, 3), "Hedef");

// equals/hashCode otomatik — aynı değerle sorgulayabilirsin
String deger = harita.get(new Koordinat(5, 3)); // "Hedef" ✅

Normal sınıfta equals/hashCode override etmeyi unutsan, get() null dönerdi. Record'da bu sorun yok.

Set'te Kullanım

Set<Koordinat> ziyaretEdilen = new HashSet<>();
ziyaretEdilen.add(new Koordinat(1, 2));
ziyaretEdilen.add(new Koordinat(1, 2)); // Aynı değer, eklenmez

System.out.println(ziyaretEdilen.size()); // 1

Sıralama

Record Comparable implement edebilir:

public record Ogrenci(String isim, double ortalama)
    implements Comparable<Ogrenci> {

    @Override
    public int compareTo(Ogrenci other) {
        return Double.compare(other.ortalama, this.ortalama); // Büyükten küçüğe
    }
}

List<Ogrenci> liste = new ArrayList<>(List.of(
    new Ogrenci("Ali", 85.5),
    new Ogrenci("Veli", 92.0),
    new Ogrenci("Ayşe", 88.3)
));
Collections.sort(liste);
// Veli(92.0), Ayşe(88.3), Ali(85.5)

Record ve Serialization

Record'lar Serializable implement edebilir ve sıradan sınıflara göre daha güvenli serialization sağlar:

public record Mesaj(String gonderen, String icerik, LocalDateTime zaman)
    implements Serializable { }

// Serialization
Mesaj mesaj = new Mesaj("Ali", "Merhaba", LocalDateTime.now());
try (var oos = new ObjectOutputStream(new FileOutputStream("mesaj.ser"))) {
    oos.writeObject(mesaj);
}

// Deserialization — canonical constructor kullanılır
try (var ois = new ObjectInputStream(new FileInputStream("mesaj.ser"))) {
    Mesaj okunan = (Mesaj) ois.readObject();
    System.out.println(okunan);
}

Normal sınıflarda deserialization constructor'ı atlar ve field'ları doğrudan set eder — bu güvenlik açığı yaratabilir. Record'larda deserialization her zaman canonical constructor'dan geçer, bu yüzden compact constructor'daki validasyonlar çalışır.

Record ve Annotation

Record component'lerine annotation eklenebilir. Bu özellikle framework'lerle çalışırken işe yarar:

// Jackson JSON serialization
public record Urun(
    @JsonProperty("urun_adi") String adi,
    @JsonProperty("fiyat") double fiyat,
    @JsonIgnore String dahiliKod
) { }

// Validation
public record KayitFormu(
    @NotBlank String isim,
    @Email String email,
    @Min(18) int yas
) { }

Annotation, component'e eklenince otomatik olarak constructor parametresine, field'a ve accessor method'a uygulanır. Hangi hedeflere uygulanacağını @Target ile kontrol edebilirsin.

Record ve Generic

Record'lar generic olabilir:

public record Cift<A, B>(A birinci, B ikinci) { }

Cift<String, Integer> isimYas = new Cift<>("Ali", 20);
Cift<Double, Double> koordinat = new Cift<>(41.0, 29.0);

System.out.println(isimYas.birinci());  // Ali
System.out.println(koordinat.ikinci()); // 29.0
// Generic result wrapper
public record Sonuc<T>(boolean basarili, T deger, String hata) {

    public static <T> Sonuc<T> basarili(T deger) {
        return new Sonuc<>(true, deger, null);
    }

    public static <T> Sonuc<T> basarisiz(String hata) {
        return new Sonuc<>(false, null, hata);
    }
}

Sonuc<Kullanici> s1 = Sonuc.basarili(new Kullanici("Ali"));
Sonuc<Kullanici> s2 = Sonuc.basarisiz("Kullanıcı bulunamadı");

Record Neden Immutable?

Immutability'nin faydaları:

  1. Thread-safety — Değiştirilemez nesneyi birden fazla thread güvenle paylaşabilir

  2. Güvenli paylaşım — Bir method'a record geçtiğinde, method onu değiştiremez

  3. HashMap key — equals/hashCode değişmeyeceği için Map'te güvenli

  4. Reasoning kolaylığı — Nesnenin durumunu takip etmek kolay, her zaman aynı

Ama dikkat — immutability sığ (shallow):

public record Ogrenci(String isim, List<String> dersler) { }

var dersler = new ArrayList<>(List.of("Java", "SQL"));
var ogrenci = new Ogrenci("Ali", dersler);

// Record immutable ama içindeki liste değil!
ogrenci.dersler().add("Spring"); // ❌ Bunu engellemez!

⚠️ Çözüm: Mutable koleksiyonları compact constructor'da kopyala:

public record Ogrenci(String isim, List<String> dersler) {
    public Ogrenci {
        dersler = List.copyOf(dersler); // Unmodifiable kopya
    }
}

Text Block ve Raw String Karşılaştırma

Text Block'lar aslında normal String'dir. Derleme zamanında işlenirler:

// Bu ikisi tam olarak aynı String'i üretir
String a = "Satır 1\nSatır 2\nSatır 3";

String b = """
        Satır 1
        Satır 2
        Satır 3
        """;

// Ama b'nin sonunda ekstra \n var (kapanış """ yeni satırda olduğu için)
// a.equals(b) → false!

// Sonunda \n olmasın istersen:
String c = """
        Satır 1
        Satır 2
        Satır 3"""; // Kapanış """ son satırın sonunda

Text Block'ta Regex

Regular expression'lar text block ile yazıldığında çok daha okunabilir:

// Eski yol — backslash cehennemi
String regex = "\\d{2}\\.\\d{2}\\.\\d{4}"; // dd.MM.yyyy

// Text block — biraz daha temiz
// (yine de \\d gerekir çünkü regex engine'e \d gitmeli)
String regex = """
        \\d{2}\\.\\d{2}\\.\\d{4}""";

Text block backslash sayısını yarıya indirmez çünkü \ hâlâ Java escape karakteri. Ama çok satırlı regex'lerde okunabilirlik artar:

String emailRegex = """
        ^[a-zA-Z0-9._%+-]+\
        @[a-zA-Z0-9.-]+\
        \\.[a-zA-Z]{2,}$""";

Sealed Class + Record

Record'lar sealed class hiyerarşisinde de kullanılabilir:

public sealed interface Sekil permits Daire, Dikdortgen, Ucgen { }

public record Daire(double yaricap) implements Sekil { }
public record Dikdortgen(double en, double boy) implements Sekil { }
public record Ucgen(double a, double b, double c) implements Sekil { }

// Pattern matching ile kullanım (Java 21)
public double alanHesapla(Sekil sekil) {
    return switch (sekil) {
        case Daire d -> Math.PI * d.yaricap() * d.yaricap();
        case Dikdortgen d -> d.en() * d.boy();
        case Ucgen u -> {
            double s = (u.a() + u.b() + u.c()) / 2;
            yield Math.sqrt(s * (s - u.a()) * (s - u.b()) * (s - u.c()));
        }
    };
}

⚠️ Not: Record ile sealed class + pattern matching üçlüsü Java'nın fonksiyonel programlama tarafını çok güçlendiriyor. İleriki derste pattern matching'i detaylı göreceğiz.

Record ve Reflection

Record'ların component bilgilerine reflection ile erişebilirsin. Bu, framework geliştirirken veya generic utility method'lar yazarken işe yarar:

public record Ogrenci(String isim, int yas) { }

// Record component bilgilerine erişim
RecordComponent[] components = Ogrenci.class.getRecordComponents();
for (RecordComponent c : components) {
    System.out.println(c.getName() + " : " + c.getType().getSimpleName());
}
// isim : String
// yas : int
// Generic record → Map dönüştürücü
public static Map<String, Object> recordToMap(Record record) {
    Map<String, Object> map = new LinkedHashMap<>();
    for (RecordComponent c : record.getClass().getRecordComponents()) {
        try {
            Object value = c.getAccessor().invoke(record);
            map.put(c.getName(), value);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    return map;
}

// Kullanım
Ogrenci o = new Ogrenci("Ali", 20);
Map<String, Object> map = recordToMap(o);
// {isim=Ali, yas=20}

Record ve Interface Pattern'ları

Record'lar interface implement ederek güçlü pattern'lar oluşturabilir:

// Doğrulanabilir (validatable) record'lar
public interface Dogrulanabilir {
    void dogrula();
}

public record Email(String adres) implements Dogrulanabilir {
    public Email {
        adres = adres.trim().toLowerCase();
    }

    @Override
    public void dogrula() {
        if (!adres.matches("^[\\w.-]+@[\\w.-]+\\.[a-z]{2,}$")) {
            throw new IllegalArgumentException("Geçersiz email: " + adres);
        }
    }
}

public record Telefon(String numara) implements Dogrulanabilir {
    @Override
    public void dogrula() {
        if (!numara.matches("^\\+?[0-9]{10,13}$")) {
            throw new IllegalArgumentException("Geçersiz telefon: " + numara);
        }
    }
}
// Toplu doğrulama
List<Dogrulanabilir> alanlar = List.of(
    new Email("ali@example.com"),
    new Telefon("+905551234567")
);
alanlar.forEach(Dogrulanabilir::dogrula); // Hepsi doğrulanır

Record vs Lombok @Data

Lombok @Data annotation'ı da boilerplate azaltır. Fark ne?

ÖzellikRecordLombok @Data
ImmutabilityZorunlu (final)İsteğe bağlı
SetterYokVar
KalıtımExtend edemezEdebilir
JPA EntityUyumsuzUyumlu
Dil özelliğiEvet (native)Hayır (ek kütüphane)
IDE desteğiTamLombok plugin gerekli

Kural: Veri immutable olacaksa → Record. Mutable entity gerekiyorsa → Lombok veya elle yaz.

Text Block ve Diğer Diller

Text Block benzeri özellikler diğer dillerde de var:

  • Python"""triple quotes"""

  • Kotlin"""trimIndent()"""

  • JavaScript → ` template literals `

  • C#"""raw string literals"""

Java'nın text block'u Python'unkine en çok benzer, ama Java otomatik indentation stripping yapar.

Text Block ile Template Oluşturma

Text Block'lar template engine olmadan basit template işlemleri için kullanılabilir:

public record HtmlSayfa(String baslik, String icerik) {

    String render() {
        return """
                <!DOCTYPE html>
                <html lang="tr">
                <head>
                    <meta charset="UTF-8">
                    <title>%s</title>
                </head>
                <body>
                    <h1>%s</h1>
                    <div>%s</div>
                </body>
                </html>
                """.formatted(baslik, baslik, icerik);
    }
}

// Kullanım
var sayfa = new HtmlSayfa("Ana Sayfa", "<p>Hoş geldiniz!</p>");
System.out.println(sayfa.render());
// Email template
public record EmailTemplate(String alici, String konu, String mesaj) {

    String olustur() {
        return """
                Sayın %s,

                %s

                Saygılarımızla,
                Uygulama Ekibi

                ---
                Bu otomatik bir mesajdır. Lütfen yanıtlamayın.
                """.formatted(alici, mesaj);
    }
}

Özet

  • Record immutable veri taşıyıcı sınıf kısayolu — constructor, accessor, equals, hashCode, toString otomatik gelir

  • Record'da accessor method'lar getIsim() değil isim() şeklinde — get prefix'i yok

  • Compact constructor ile validasyon yapılır, atama otomatik

  • Record interface implement edebilir ama sınıf extend edemez, ekstra instance field alamaz

  • Text Block ("""...""") çok satırlı String'leri temiz yazmanı sağlar — JSON, SQL, HTML için ideal

  • Record + Text Block + sealed class birlikte kullanıldığında modern, temiz ve tip-güvenli Java kodu ortaya çıkar