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.
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ırNesnenin değişmesiMevcut nesnenin gözlemlenebilir durumu değişir.
Referansın değişmesiDeğ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.
Başka nesne atanamaz.
Constructor sonrasında field’a başka değer atanamaz.
O referans üzerinden değiştirme işlemleri yasaktır.
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?
- Sınıfın kontrolsüz biçimde genişletilmesini engelle.
- Field’ları
private finalyap. - Setter sağlama.
- Constructor’da mutable girdilerin kopyasını al.
- Getter’dan mutable iç durumu doğrudan döndürme.
- Nesne oluşturulurken doğrulama yap.
- 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");
// UnsupportedOperationExceptionList.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());
// AnkaraListenin 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.
unmodifiableListKaynağa bağlı salt-okunur görünüm.
List.copyOfEleman 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 çıkabilirNesne 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);
// javaSonuç kullanılmadı. Doğru kullanım:
JAVAvalue = value.toUpperCase();
System.out.println(value);
// JAVAvalue += " 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);
// truea ─┐ ├──► "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)); // trueCompile-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()); // trueequals()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 xvey- Canonical constructor
x()vey()accessor’ları- Component tabanlı
equals()vehashCode() 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:
pixels()gerçek array’i dışarı verir; içerik değiştirilebilir.- 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
StringBigDecimalLocalDate- Başka immutable record veya value object’ler
- Immutable kopyası alınmış koleksiyonlar
Değişmeyen referans ile değişmeyen nesneyi ayır
Referans değişmez; nesne değişebilir.
Nesnenin gözlemlenebilir durumu değişmez.
Bu referans değiştiremez; kaynak değişebilir.
Snapshot alır; elemanları deep copy yapmaz.
Component referansları final; varsayılan olarak shallow immutable.
Literal’leri paylaşır; içerik yine equals() ile karşılaştırılır.
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:
- Constructor’a verdiğin koleksiyonu sonradan değiştir.
- Accessor’dan dönen koleksiyona eleman eklemeyi dene.