Roadmap’e dön
Ders 2 · Java’nın yapı taşları

Immutability, String, String Pool ve records

Değişmez nesneleri doğru tasarlamayı, String Pool’un neden mümkün olduğunu ve record’ların hangi noktada gerçekten immutable olmadığını adım adım öğren.

Java Core2 Eylül 2026~17 dk okuma

Roadmap’taki “Java’nın yapı taşları” bölümünün ikinci konusu, değişmezlik modelini String, String Pool ve record’lar üzerinden derinleştiriyor. İlk dersteki referans, equals ve hashCode bilgisi burada doğrudan kullanılacak.

Ana fikir: Referans değişebilir; referansın gösterdiği immutable nesne değişemez.

01 Immutability nedir?

Bir nesne oluşturulduktan sonra gözlemlenebilir durumu değişmiyorsa o nesne immutable, yani değişmezdir.

JAVAString name = "Ali";
name = name.toUpperCase();

Burada "Ali" nesnesi "ALI" hâline dönüşmedi. toUpperCase() yeni bir nesne üretti ve referans değişkeni bu yeni nesneyi göstermeye başladı.

name ───────► "Ali"

name.toUpperCase() yeni nesne üretir:

name ───────► "ALI"
              "Ali" eski nesne olarak kalır
Nesnenin değişmesi

Mevcut nesnenin gözlemlenebilir durumu değişir.

Referansın değişmesi

Değişken başka bir nesneyi göstermeye başlar.

02 final ile immutable aynı şey değildir

En sık yapılan hata bu iki kavramı aynı kabul etmektir.

JAVAfinal List<String> names = new ArrayList<>();

names.add("Ali");          // Geçerli
names.add("Ayşe");         // Geçerli
names = new ArrayList<>(); // Derleme hatası

final, names referansının başka bir listeyi göstermesini engeller. Listenin içeriğinin değişmesini engellemez.

final referans

Başka nesne atanamaz.

final field

Constructor sonrasında field’a başka değer atanamaz.

Unmodifiable collection

O referans üzerinden değiştirme işlemleri yasaktır.

Immutable object

Nesnenin gözlemlenebilir durumu hiçbir yoldan değişmez.

Şu sınıf immutable değildir:

JAVApublic final class User {

    private final List<String> roles;

    public User(List<String> roles) {
        this.roles = roles;
    }

    public List<String> getRoles() {
        return roles;
    }
}

Sınıf final, field private final ve setter yok. Yine de iki ayrı açık bulunuyor.

Constructor üzerinden değiştirilebilir

JAVAList<String> roles = new ArrayList<>();
roles.add("USER");

User user = new User(roles);
roles.add("ADMIN");

System.out.println(user.getRoles());
// [USER, ADMIN]

User, dışarıdaki listenin referansını doğrudan sakladı.

Getter üzerinden değiştirilebilir

JAVAuser.getRoles().add("SUPER_ADMIN");

Getter, içerideki mutable nesneyi dışarı sızdırdı.

03 Doğru immutable sınıf nasıl yazılır?

  1. Sınıfın kontrolsüz biçimde genişletilmesini engelle.
  2. Field’ları private final yap.
  3. Setter sağlama.
  4. Constructor’da mutable girdilerin kopyasını al.
  5. Getter’dan mutable iç durumu doğrudan döndürme.
  6. Nesne oluşturulurken doğrulama yap.
  7. Değişiklik gerekiyorsa yeni nesne döndür.

Oracle’ın önerdiği strateji de private final alanlar, alt sınıflamanın engellenmesi ve mutable nesnelerin referanslarının paylaşılmaması üzerine kuruludur. Ayrıntılar için Oracle immutable object stratejisine bakabilirsin.

JAVAimport java.util.List;
import java.util.Objects;

public final class User {

    private final String username;
    private final List<String> roles;

    public User(String username, List<String> roles) {
        this.username = Objects.requireNonNull(username);
        this.roles = List.copyOf(roles);
    }

    public String username() {
        return username;
    }

    public List<String> roles() {
        return roles;
    }

    public User withRole(String role) {
        var newRoles = new java.util.ArrayList<>(roles);
        newRoles.add(role);

        return new User(username, newRoles);
    }
}

Değişiklik yerine yeni nesne

JAVAUser ali = new User("ali", List.of("USER"));
User adminAli = ali.withRole("ADMIN");

System.out.println(ali.roles());
// [USER]

System.out.println(adminAli.roles());
// [USER, ADMIN]

withRole() mevcut nesneyi değiştirmez. Yeni bir User oluşturur. Constructor’daki List.copyOf(roles)sayesinde roles accessor’ı değiştirilemeyen listeyi güvenle döndürebilir.

JAVAuser.roles().add("ADMIN");
// UnsupportedOperationException
List.copyOf() shallow copy, yani yüzeysel kopyadır. Listenin elemanlarını otomatik olarak immutable yapmaz.

04 Shallow ve deep immutability

JAVApublic final class Address {

    private String city;

    public Address(String city) {
        this.city = city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getCity() {
        return city;
    }
}
JAVAList<Address> addresses =
        List.of(new Address("İstanbul"));

List<Address> copy = List.copyOf(addresses);

addresses.get(0).setCity("Ankara");

System.out.println(copy.get(0).getCity());
// Ankara

Listenin kendisine eleman eklenemez veya listeden eleman çıkarılamaz. Ancak içindeki Address nesnesi hâlâ mutable’dır.

addresses ──► immutable liste ──► mutable Address
copy      ──► başka liste ──────► aynı mutable Address
  • Immutable collection, elemanlarını immutable yapmaz.
  • Deep immutability için nesne grafiğinin tamamı değişmez olmalıdır.
  • List<String> genellikle güvenlidir.
  • List<MutableAddress> derin immutable değildir.

05 unmodifiableList ile immutable kopya farkı

JAVAList<String> source = new ArrayList<>();
source.add("A");

List<String> view =
        Collections.unmodifiableList(source);

source.add("B");

System.out.println(view);
// [A, B]

unmodifiableList, mevcut listenin salt-okunur görünümünü oluşturur. Kaynak liste değişirse görünüm de değişir.

JAVAList<String> snapshot = List.copyOf(source);

source.add("C");

System.out.println(snapshot);
// [A, B]

List.copyOf, eleman referanslarının anlık kopyasını alır. İkisi de deep copy değildir; immutable sınıf tasarlarken genellikle List.copyOf() daha doğru seçimdir.

unmodifiableList

Kaynağa bağlı salt-okunur görünüm.

List.copyOf

Eleman referanslarının bağımsız snapshot’ı.

Array için defensive copy

JAVApublic final class FileHash {

    private final byte[] value;

    public FileHash(byte[] value) {
        this.value = value.clone();
    }

    public byte[] value() {
        return value.clone();
    }
}

Hem içeri alırken hem dışarı verirken kopya gerekir.

06 Immutability neden değerlidir?

Değişmez hashCode

Bir nesneyi HashMap anahtarı yaptıktan sonra equals veya hashCode hesabına katılan alanları değiştirirsen nesne yanlış bucket’ta kalabilir.

JAVAMap<MutableUser, String> map = new HashMap<>();

MutableUser user = new MutableUser("ali");
map.put(user, "data");

user.setUsername("ayse");

System.out.println(map.get(user));
// null çıkabilir

Nesne eklenirken hashCode “ali” üzerinden, arama sırasında “ayse” üzerinden hesaplanınca başka bucket aranabilir. Immutable anahtarların equals/hashCode durumu değişmediği için bu problem oluşmaz.

Thread-safety

JAVArecord Coordinate(int x, int y) {}

Bin thread aynı Coordinate nesnesini okuyabilir. Değişen ortak durum olmadığı için thread’ler birbirinin durumunu bozamaz. Oracle da immutable nesnelerin thread interference ve tutarsız durum risklerini azalttığını vurgular. Ayrıntılar için Oracle Immutable Objects dokümanına bakabilirsin.

record Team(List<String> members) {} tek başına thread-safe değildir. Record referansı değişmez; içindeki liste hâlâ değişebilir.

Diğer avantajlar

  • Yan etkiler azalır.
  • Kodun davranışını takip etmek kolaylaşır.
  • Nesneler güvenli biçimde paylaşılabilir.
  • Cache ve memoization daha güvenilir olur.
  • Hash-based collection anahtarı olmaya uygundur.
  • Hata ayıklamak kolaylaşır.

Bedeli değişikliklerde yeni nesne üretilmesidir. Ancak kısa ömürlü nesne oluşturmak JVM’in oldukça optimize olduğu bir alandır; sırf bundan korkarak mutable tasarıma yönelmek çoğu zaman gereksizdir.

07 String neden immutable?

String üzerinde değişiklik yapan metotlar yeni String döndürür.

JAVAString value = "java";

value.toUpperCase();

System.out.println(value);
// java

Sonuç kullanılmadı. Doğru kullanım:

JAVAvalue = value.toUpperCase();

System.out.println(value);
// JAVA

value += " developer"; satırı da “java” nesnesini değiştirmez. Yeni bir String üretilir ve value yeni nesneyi göstermeye başlar.

String’in immutable olması sayesinde:

  • Aynı literal güvenle paylaşılabilir.
  • String Pool kullanılabilir.
  • Hash değeri güvenle cache edilebilir.
  • String, HashMap anahtarı olarak güvenlidir.
  • Dosya yolu, class adı ve URL gibi değerler sonradan değiştirilemez.

08 String Pool nasıl çalışır?

JVM, intern edilmiş String’ler için benzersiz örneklerin tutulduğu mantıksal bir havuz yönetir. Literal’ler ve compile-time String sabit ifadeleri intern edilir. Ayrıntılar için String.intern() dokümantasyonuna bakabilirsin.

JAVAString a = "java";
String b = "java";

System.out.println(a == b);
// true
a ─┐
   ├──► "java"
b ─┘

new String(...) ise açıkça yeni nesne oluşturur:

JAVAString a = "java";
String b = new String("java");

System.out.println(a == b);      // false
System.out.println(a.equals(b)); // true

Compile-time ve runtime birleştirme

JAVAString a = "java";
String b = "ja" + "va";

System.out.println(a == b);
// true

“ja” + “va” compile-time’da hesaplanabilen sabit ifadedir ve pool’daki nesneyi kullanır. Değişken içeren birleştirme ise runtime’da yapılır.

JAVAString suffix = "va";
String c = "ja" + suffix;

System.out.println(a == c);          // false
System.out.println(a == c.intern()); // true
String içeriği karşılaştırırken her zaman equals()kullan. == sonucunu String Pool bilgisine dayandırmak kırılgan bir programlama biçimidir.
JAVA// Yanlış
if (username == "ali") { }

// Doğru
if ("ali".equals(username)) { }

Literal ve sabit String ifadelerinin intern edilmesi JLS §3.10.5 içinde tanımlanır.

Döngülerde String birleştirme

JAVAString result = "";

for (int i = 0; i < 10_000; i++) {
    result += i;
}

Her iterasyonda yeni sonuç üretildiği için çok sayıda ara nesne oluşabilir. Bunun yerine mutable yardımcı kullanılır:

JAVAStringBuilder result = new StringBuilder();

for (int i = 0; i < 10_000; i++) {
    result.append(i);
}

String finalResult = result.toString();

StringBuilder mutable’dır; sonuç olarak üretilen String immutable’dır.

09 Record nedir?

Record, sabit bir değer kümesini taşıyan veri sınıflarını kısa biçimde yazmamızı sağlar.

JAVApublic record Point(int x, int y) {}

Compiler kabaca şunları sağlar:

  • private final int x ve y
  • Canonical constructor
  • x() ve y() accessor’ları
  • Component tabanlı equals() ve hashCode()
  • toString()
  • Implicitly final record sınıfı
JAVAPoint first = new Point(10, 20);
Point second = new Point(10, 20);

System.out.println(first.equals(second)); // true
System.out.println(first); // Point[x=10, y=20]

Bu nedenle record’lar value object yazmak için kullanışlıdır. JLS, record sınıflarının final olduğunu ve component field’larının private final üretildiğini belirtir. Ayrıntılar için JLS §8.10 bölümüne bakabilirsin.

10 Record otomatik olarak deep immutable değildir

JAVApublic record User(
        String username,
        List<String> roles
) {}
JAVAList<String> roles = new ArrayList<>();
roles.add("USER");

User user = new User("ali", roles);
roles.add("ADMIN");

System.out.println(user.roles());
// [USER, ADMIN]

Record yalnızca roles field’ına başka bir liste atanmasını engeller. Listenin değiştirilmesini engellemez. Java API, record’ları shallowly immutable olarak tanımlar. Ayrıntılar için Java Record API dokümanına bakabilirsin.

Compact constructor ile savunmacı kopya

JAVAimport java.util.List;
import java.util.Objects;

public record User(
        String username,
        List<String> roles
) {
    public User {
        username = Objects.requireNonNull(username);
        roles = List.copyOf(roles);

        if (username.isBlank()) {
            throw new IllegalArgumentException(
                    "username boş olamaz"
            );
        }
    }
}

Compact constructor sonunda compiler, doğrulanmış ve kopyalanmış parametreleri component field’larına atar. Roles bir List<String> olduğu için bu record pratikte deep immutable kabul edilebilir:

  • Record component referansı değişemez.
  • Liste değiştirilemez.
  • Listenin elemanı olan String nesneleri immutable’dır.

Ancak List<MutableAddress> olsaydı yine deep immutable olmazdı.

11 Record içinde array kullanma tuzağı

JAVArecord Image(byte[] pixels) {}

Burada iki problem vardır:

  1. pixels() gerçek array’i dışarı verir; içerik değiştirilebilir.
  2. Array equals() içerik yerine referans karşılaştırır; record eşitliği beklenmedik davranabilir.

Record component’larında mümkünse şunları kullan:

  • Primitive değerler
  • String
  • BigDecimal
  • LocalDate
  • Başka immutable record veya value object’ler
  • Immutable kopyası alınmış koleksiyonlar
Akılda kalacak özet

Değişmeyen referans ile değişmeyen nesneyi ayır

final reference

Referans değişmez; nesne değişebilir.

immutable object

Nesnenin gözlemlenebilir durumu değişmez.

unmodifiable view

Bu referans değiştiremez; kaynak değişebilir.

List.copyOf

Snapshot alır; elemanları deep copy yapmaz.

record

Component referansları final; varsayılan olarak shallow immutable.

String Pool

Literal’leri paylaşır; içerik yine equals() ile karşılaştırılır.

Pratik hedef

Gerçekten immutable bir value object yaz

Money, UserProfile veya Order isminde, içinde en az bir koleksiyon bulunan immutable bir sınıf oluştur. Ardından iki açığı test et:

  1. Constructor’a verdiğin koleksiyonu sonradan değiştir.
  2. Accessor’dan dönen koleksiyona eleman eklemeyi dene.
Her iki durumda da oluşturduğun nesnenin durumu aynı kalıyorsa defensive copy sınırlarını doğru kurmuşsun demektir.