Immutable (değişmez) sınıf, bir kez oluşturulduktan sonra iç durumu hiçbir şekilde değiştirilemeyen nesne demektir; immutable class java tasarımında bu durum final alanlar, defensive copy ve setter'sız yapı ile sağlanır. Java'da final anahtar kelimesi ve dikkatli kurucu metotlarla, Python'da ise tuple, frozenset ve dataclass(frozen=True) gibi araçlarla aynı hedefe farklı yollarla ulaşılır. Bu yazıda hem Java'da değişmez sınıf yazmanın somut kuralları hem de Python'daki karşılıkları, kod örnekleriyle birlikte ele alınıyor.
Immutability Nedir? Mutable ve Immutable Nesne Farkı
Bir nesnenin mutable (değişebilir) mi yoksa immutable (değişmez) mi olduğu, o nesnenin durumunun oluşturulduktan sonra değiştirilip değiştirilemeyeceğine bağlıdır. Örneğin bir Java ArrayList nesnesine eleman eklendiğinde nesnenin kendisi aynı kalır ama içeriği değişir; bu mutable bir davranıştır. Buna karşılık bir String nesnesi üzerinde birleştirme işlemi yapıldığında aslında yeni bir String nesnesi oluşturulur, orijinal nesne hiç değişmez. Python tarafında da benzer bir ayrım vardır: bir liste (list) elemanları değiştirilebilirken, bir demet (tuple) oluşturulduktan sonra elemanları değiştirilemez.
Bu ayrım, nesne yönelimli programlamada sadece teorik bir detay değil, doğrudan tasarım kararlarını etkileyen bir noktadır. Bir nesnenin durumu dışarıdan kontrolsüzce değiştirilebiliyorsa, o nesneyi birden fazla yerde paylaşmak riskli hale gelir; bir yerde yapılan değişiklik, beklenmedik şekilde başka bir bölümü etkileyebilir. Immutable nesneler ise oluşturulduğu andaki durumunu garanti ettiği için, özellikle çok parçalı ve eşzamanlı çalışan sistemlerde güvenli bir referans noktası sunar. Bu yüzden bir sınıf tasarlarken "bu nesnenin durumu zamanla değişecek mi, yoksa sabit bir değeri mi temsil ediyor?" sorusu, sınıfın mutable mı immutable mı olacağını belirleyen temel karar noktasıdır.
Java ve Python bu kavrama farklı dilsel araçlarla yaklaşır. Java'da immutability genellikle geliştiricinin bilinçli olarak uyguladığı bir disiplin gerektirir: final sınıflar, final alanlar ve defensive copy teknikleri bir araya gelir. Python'da ise dil, tuple ve frozenset gibi doğası gereği değişmez veri yapılarını hazır sunar; ayrıca dataclass dekoratörünün frozen=True parametresiyle kendi immutable sınıflarınızı kısa yoldan tanımlayabilirsiniz. İlerleyen bölümlerde her iki dildeki bu yaklaşımlar somut kod örnekleriyle karşılaştırılacak.
Java'da Immutable Sınıf Oluşturmanın Kuralları
Java'da gerçek anlamda değişmez bir sınıf tasarlamak, birkaç kuralın eksiksiz uygulanmasını gerektirir. Öncelikle sınıfın kendisi final olarak tanımlanmalıdır; aksi halde bir alt sınıf türetilip metotlar ezilerek immutability garantisi kırılabilir. İkinci olarak, sınıfın tüm alanları final olmalı ve yalnızca kurucu (constructor) içinde bir kez atanmalıdır. Üçüncüsü, sınıfta durumu değiştirecek herhangi bir setter metodu bulunmamalıdır; dışarıdan yalnızca okuma amaçlı getter metotlarına izin verilir.
Bu üç kural tek başına yeterli değildir; alan tiplerinden biri liste, tarih veya dizi gibi kendisi mutable olan bir referans tipiyse, o alan için mutlaka defensive copy (savunmacı kopyalama) uygulanmalıdır. Aksi halde kurucuya verilen ya da getter ile dışarı çıkarılan referans üzerinden nesnenin iç durumu dolaylı yoldan değiştirilebilir. Aşağıdaki örnek, hem kurucuda hem de getter'da defensive copy uygulanan bir Java sınıfını göstermektedir:
import java.util.ArrayList;
import java.util.List;
public final class Siparis {
private final String musteriAdi;
private final List<String> urunler;
public Siparis(String musteriAdi, List<String> urunler) {
this.musteriAdi = musteriAdi;
this.urunler = new ArrayList<>(urunler); // kurucuda defensive copy
}
public String getMusteriAdi() {
return musteriAdi;
}
public List<String> getUrunler() {
return new ArrayList<>(urunler); // getter'da defensive copy
}
}
Bu örnekte dışarıdan verilen liste doğrudan saklanmaz, kopyası tutulur; getter da orijinal listeyi değil onun bir kopyasını döndürür. Böylece çağıran kod elindeki listeyi değiştirse bile Siparis nesnesinin iç durumu etkilenmez. Bu kuralın eksik uygulanması — örneğin yalnızca alanların final yapılıp defensive copy'nin atlanması — pratikte "yarı immutable" bir sınıfa yol açar: alan referansı değişmez ama referansın gösterdiği nesnenin içeriği dışarıdan serbestçe değiştirilebilir, bu da immutability garantisini fiilen geçersiz kılar. Bu tür ince detaylar, konuyu ilk öğrenirken gözden kaçabilir; birebir Java dersleri kapsamında bu kurallar örnek üzerinden adım adım uygulanarak pekiştirilir.
Java'nın Kendi Immutable Sınıfları: String ve Integer Üzerinden Kavramsal Bakış
String ve Integer gibi Java'nın kendi standart sınıfları, tasarım gereği immutable olacak şekilde geliştirilmiştir. Bir String nesnesi üzerinde concat ya da replace gibi bir işlem çağrıldığında orijinal nesne değişmez, her seferinde yeni bir String nesnesi üretilir. Aynı mantık Integer, Long, Double gibi sarmalayıcı (wrapper) sınıflar için de geçerlidir: bir kez oluşturulan değer nesnesi, yaşam süresi boyunca hep aynı değeri taşır.
Bu tasarım tercihinin somut bir faydası, Java'nın string pool mekanizmasında görülür. Aynı metinsel değere sahip birden fazla String literal'i, arka planda tek bir nesne olarak paylaşılabilir; çünkü nesnenin değişmeyeceği garanti altındadır, bu paylaşım hiçbir riske yol açmaz. Eğer String mutable olsaydı, bir referans üzerinden yapılan değişiklik pool içindeki diğer tüm referansları da etkileyeceğinden bu tür bir paylaşım mekanizması güvenli olmazdı. Aynı güven ilişkisi, immutable nesnelerin önbellekleme (caching) ve nesne paylaşımı senaryolarında genel olarak tercih edilmesinin de temel nedenidir.
Kendi sınıflarımızı tasarlarken buradan çıkarılacak ders nettir: bir nesnenin değeri sabit kalması gereken bir bilgiyi temsil ediyorsa — bir para birimi tutarı, bir tarih aralığı, bir koordinat çifti gibi — o sınıfı String ve Integer örneklerinde olduğu gibi baştan immutable tasarlamak, ileride ortaya çıkabilecek yan etkili hataları daha en başından ortadan kaldırır. Bu tür temel dil yapıtaşlarının nasıl çalıştığını kavramak, ileri seviye konulara geçişte büyük fark yaratır; canlı yazılım eğitimi sürecinde bu kavramlar hem teorik hem uygulamalı örneklerle işlenir. Ayrıca öğrenci portalındaki kategori bazlı soru bankaları, bu tür kavramsal konuları pekiştirmek isteyenler için düzenli pratik imkânı sunar.
Python'da Immutability Yaklaşımı: tuple, frozenset, dataclass(frozen=True)
Python, Java'nın aksine immutability'yi sınıf düzeyinde manuel kurallarla değil, dilin kendi veri tipleri ve dekoratörleri üzerinden sağlar. En temel örnek tuple'dır: bir liste (list) her zaman değiştirilebilirken, aynı verileri tutan bir tuple oluşturulduktan sonra elemanlarına yeni değer atanamaz, yalnızca yeni bir tuple üretilebilir. Benzer şekilde set mutable bir koleksiyonken, frozenset bu kümenin donmuş, değiştirilemez halidir ve bu özelliği sayesinde başka bir kümenin veya sözlük anahtarının içine güvenle yerleştirilebilir. Bu iki yapı, Java'daki final alanların sağladığı garantiyi dil seviyesinde, ekstra kod yazmadan sunar.
Python'da gerçek anlamda private alan yoktur; bir alanı isimlendirirken başına çift alt çizgi koymak (name mangling) yalnızca dışarıdan erişimi zorlaştıran bir kurallar bütünüdür, derleyici düzeyinde bir engel değildir. Bu durum immutability tasarımını doğrudan etkiler: Java'da private alan + final + defensive copy üçlüsüyle kurulan sıkı koruma, Python'da disiplin ve dekoratör kullanımına dayanır. Yani Python'da bir nesnenin değişmez kalması, çoğunlukla geliştiricinin sözleşmeye uymasına ve dilin sunduğu araçları doğru kullanmasına bağlıdır.
Bu noktada dataclass(frozen=True) devreye girer. Bu dekoratör, sınıfın tüm alanlarına atama yapılmasını engelleyerek nesneyi oluşturulduktan sonra değiştirilemez hale getirir:
from dataclasses import dataclass
@dataclass(frozen=True)
class Nokta:
x: int
y: int
def oteleme(self, dx: int, dy: int) -> "Nokta":
return Nokta(self.x + dx, self.y + dy)
n1 = Nokta(1, 2)
n2 = n1.oteleme(3, 4)
print(n1, n2)
try:
n1.x = 10
except Exception as hata:
print("Değiştirme engellendi:", hata)
Bu örnekte n1.x = 10 satırı çalıştırıldığında dataclass modülü bir hata fırlatarak alanın değiştirilmesine izin vermez; tıpkı Java'da final alana ikinci kez atama yapılamaması gibi bir güvence sağlar. Farkı ise şu şekilde özetlemek mümkündür: Java'da bu güvenceyi kurmak için her alanı final yapmak, constructor'da gelen mutable referansları kopyalamak ve getter'larda savunmacı kopya döndürmek gerekirken, Python'da tek bir dekoratör parametresiyle benzer bir koruma elde edilir. Ancak bu kolaylık, Python tarafında mutable bir alan (örneğin bir liste) frozen dataclass içine konursa iç içerik yine değiştirilebilir kalabileceğinden, dikkatli tasarım gerektirmeye devam eder. Bu tür ince farkları pratikle görmek isteyenler birebir Python dersleri kapsamında örnek üzerinden çalışabilir.
Immutable Sınıfların Avantajları: Thread-Safety, hashCode/equals ve Önbellekleme
Immutable nesnelerin en somut faydalarından biri, birden fazla iş parçacığının (thread) aynı nesneye eriştiği senaryolarda ortaya çıkar. Bir nesnenin durumu oluşturulduktan sonra hiç değişmiyorsa, iki thread aynı anda o nesneyi okusa bile aralarında bir yarış durumu (race condition) oluşmaz; çünkü değiştirilecek bir "durum" yoktur. Bu, immutable sınıfları doğal olarak thread-safe kılar ve senkronizasyon bloklarına, kilitlere (lock) ihtiyaç duymadan güvenli paylaşım sağlar. Java'da çok iş parçacıklı uygulamalarda bu özellik, karmaşık senkronizasyon kodunu tamamen ortadan kaldırabilir.
İkinci önemli avantaj hashCode ve equals tutarlılığıdır. Bir nesne HashMap veya HashSet gibi hash tabanlı bir koleksiyonda anahtar olarak kullanıldığında, o nesnenin hash değeri koleksiyona eklendiği andan itibaren sabit kalmalıdır. Eğer nesne mutable ise ve koleksiyona eklendikten sonra alanlarından biri değişirse, hash değeri de değişir ve nesne artık kendi eklendiği kovada (bucket) bulunamaz hale gelir; bu durum sessiz ama ciddi bir hataya yol açar. Immutable bir sınıfta ise alanlar hiç değişmediği için hesaplanan hash değeri kalıcıdır, dolayısıyla nesne bir kez HashMap anahtarı olarak eklendiğinde güvenle bulunabilir kalır. Bu konudaki kavramsal doğruluğunu Java bilgi seviyeni ölçen test ile hızlıca sınayabilirsin.
Üçüncü avantaj önbellekleme (caching) senaryolarında görülür. Bir nesne değişmeyeceği garantisi taşıdığında, o nesneyi bir kez hesaplayıp bellekte veya bir önbellek yapısında saklamak güvenlidir; çünkü nesnenin daha sonra başka bir kod parçası tarafından değiştirilip önbelleği bozması söz konusu değildir. Bu yüzden sabit değer nesneleri, konfigürasyon nesneleri ve paylaşılan referans verileri sıklıkla immutable olarak tasarlanır. Aynı mantık, tekrar hesaplanması maliyetli olan nesnelerin güvenle yeniden kullanılmasını da mümkün kılar, çünkü önbellekten dönen referans hiçbir zaman beklenmedik şekilde değişmez.
Yaygın Hatalar ve Builder Pattern İlişkisi
Immutable sınıf tasarlarken görünüşte doğru ama pratikte immutability'yi kıran birkaç tekrarlayan hata vardır. Bu hataların çoğu, sınıfın dışarıdan "değişmez" görünmesine rağmen içeriden hâlâ değiştirilebilir kalmasına neden olur:
- Alanı
finaltanımlayıp ama bu alan mutable bir nesneye (örneğin bir liste veya tarih nesnesi) referans veriyorsa, getter metodunda bu referansı doğrudan döndürmek; çağıran kod bu referans üzerinden nesnenin iç durumunu değiştirebilir. - Constructor'da parametre olarak gelen mutable referansı kopyalamadan doğrudan alana atamak; dışarıdaki kod, nesne oluşturulduktan sonra bile o referans üzerinden sınıfın iç durumunu değiştirebilir.
- Koleksiyon türündeki alanları immutable bir sarmalayıcı (örneğin değiştirilemez bir liste görünümü) olmadan olduğu gibi paylaşmak; koleksiyonun kendisi final olsa da içeriği dışarıdan eklenip çıkarılabilir kalır.
- Yalnızca setter metotlarını kaldırıp sınıfı "immutable" ilan etmek, ancak iç durumu değiştiren başka public metotlar (örneğin bir listeye ekleme yapan yardımcı metot) bırakmak; bu, immutability'nin yalnızca görünüşte sağlandığı en sinsi hatalardan biridir.
Bu hataların özellikle çok alanlı sınıflarda sıklaşmasının bir nedeni de karmaşık constructor'lardır: alan sayısı arttıkça hem okunabilirlik düşer hem de her alanı doğru şekilde kopyalayıp doğrulamak zorlaşır. Bu noktada builder pattern devreye girer. Builder, nesnenin tüm alanlarını adım adım, isimlendirilmiş metotlarla toplayan ayrı bir yardımcı sınıftır; asıl immutable sınıfın constructor'ı yalnızca builder'dan gelen değerleri final alanlara atar ve gerekiyorsa savunmacı kopyalarını alır. Böylece hem nesne oluşturma okunabilir hale gelir hem de immutable sınıfın kendisi hiçbir ara değişikliğe açık kalmaz; nesne yalnızca builder'ın build() metodu çağrıldığında, tüm alanlarıyla birlikte tek seferde ve tamamlanmış olarak var olur. Bu tür tasarım desenlerini örnekler üzerinden birebir uygulamalı görmek isteyenlerin 1-1 özel ders randevusu alarak kendi kod tabanları üzerinde çalışması, hatayı soyut anlatımdan çok daha hızlı fark etmelerini sağlar.
Immutable mi Mutable mi? Karşılaştırma Tablosu ve Karar Kriterleri
Bir sınıfı immutable mı yoksa mutable mı tasarlayacağınıza karar verirken tek bir doğru cevap yoktur; karar, nesnenin kullanım bağlamına, performans gereksinimlerine ve eşzamanlılık (concurrency) ihtiyaçlarına göre değişir. Aşağıdaki tablo, iki yaklaşımı temel kriterler üzerinden karşılaştırarak karar sürecini somutlaştırır.
| Kriter | Immutable Sınıf | Mutable Sınıf |
|---|---|---|
| Thread-safety | Doğası gereği güvenlidir, ek senkronizasyon gerekmez | Paylaşılan durum için kilit (lock) veya senkronize blok gerekir |
| Bellek kullanımı | Her değişiklikte yeni nesne oluştuğu için daha fazla nesne üretilebilir | Aynı nesne üzerinde güncelleme yapıldığı için nesne sayısı azdır |
| Performans (küçük nesneler) | Sık değişen küçük veriler için ek nesne yaratma maliyeti oluşabilir | Yerinde güncelleme daha az kaynak tüketir |
| hashCode/equals güvenilirliği | HashMap, HashSet gibi koleksiyonlarda güvenle anahtar olarak kullanılabilir | Nesne değişirse hash tutarsızlığı ve kayıp anahtar riski oluşur |
| API tasarımı ve okunabilirlik | Yan etkisiz, tahmin edilebilir davranış sağlar | Durum değişimini takip etmek daha fazla dikkat gerektirir |
| Nesne yaşam döngüsü | Oluşturulduğu andan itibaren sabit kalır, geçmiş durumu korur | Zaman içinde durumu değişir, geçmiş hâli izlenmez |
Karar verirken sorulması gereken pratik sorular şunlardır:
- Nesne birden fazla iş parçacığı (thread) tarafından aynı anda okunacak mı? Öyleyse immutable tasarım ek senkronizasyon yükünü ortadan kaldırır.
- Nesne bir Map anahtarı ya da Set elemanı olarak kullanılacak mı? Bu durumda immutability, hash tutarlılığını garanti eder.
- Nesnenin durumu saniyede binlerce kez mi değişiyor, yoksa nadiren mi güncelleniyor? Sık değişim senaryolarında mutable sınıf daha az nesne üretimiyle daha verimli olabilir.
- Nesne bir değer (value) mi temsil ediyor (para tutarı, tarih, koordinat) yoksa bir varlık (entity) mi (kullanıcı hesabı, sipariş kaydı)? Değerler genellikle immutable, varlıklar genellikle mutable tasarlanır.
Bu tür tasarım kararlarını gerçek proje senaryolarında pratik yaparak içselleştirmek, teoriyi ezberlemekten çok daha kalıcıdır. Hangi yönde ilerlemeniz gerektiğini netleştirmek isteyenler için ücretsiz kariyer yönelim testi ilgi alanınıza uygun teknoloji ve öğrenme yolunu belirlemede yol gösterici olabilir. Sonuç olarak immutable tasarım güvenlik ve öngörülebilirlik öncelikli senaryolarda, mutable tasarım ise performans ve sık güncelleme öncelikli senaryolarda tercih edilmelidir.
Sık Sorulan Sorular
Bir sınıfı gerçek anlamda immutable yapmak için hangi kurallara uyulmalıdır?
Sınıfın final olması, tüm alanların final ve private tanımlanması, kurucu dışında alan değerini değiştiren bir metot bulunmaması ve mutable referans tipi alanlar için defensive copy uygulanması gerekir. Bu kurallardan biri eksik kalırsa sınıf yalnızca "değişmez gibi görünür" ama gerçek anlamda immutable olmaz.
Java'da bir alanı final yapmak tek başına sınıfı immutable yapmaya yeter mi?
Hayır. final anahtar kelimesi yalnızca referansın yeniden atanmasını engeller; alanın işaret ettiği nesnenin iç durumunun değişmesini engellemez. Örneğin final bir List referansı, listeye eleman eklenmesini önlemez; bu yüzden mutable referans tipleri için ayrıca defensive copy uygulanmalıdır.
Python'da gerçek private erişim belirleyici olmadan immutability nasıl sağlanır?
Python'da erişim belirleyicileri Java'daki gibi zorlayıcı değildir; bunun yerine dil, immutability'yi tuple, frozenset gibi değişmez veri yapıları ve dataclass(frozen=True) dekoratörüyle sağlar. frozen=True kullanıldığında alan ataması denemesi çalışma zamanında hataya yol açar, böylece disiplin sözleşme (convention) yerine dil düzeyinde uygulanır.
Immutable sınıflar neden thread-safe kabul edilir?
Bir nesnenin durumu oluşturulduktan sonra hiç değişmediği için birden fazla iş parçacığı aynı nesneyi eşzamanlı okusa bile veri yarışı (race condition) oluşmaz. Yazma işlemi olmadığından kilitleme veya senkronizasyon mekanizmasına ihtiyaç duyulmaz.
dataclass(frozen=True) normal bir Python sınıfından farklı olarak ne sağlar?
frozen=True, sınıfın __setattr__ ve __delattr__ metotlarını otomatik olarak devre dışı bırakarak alan atamasını engeller ve nesneyi hashlenebilir hâle getirir. Normal bir sınıfta bu davranışı elde etmek için geliştiricinin manuel olarak özel metotlar yazması gerekirken, frozen=True bunu tek satırlık bir dekoratör parametresiyle sunar.
Builder pattern immutable nesne oluşturmaya nasıl katkı sağlar?
Builder pattern, çok sayıda alanı olan immutable bir nesneyi adım adım, okunabilir şekilde inşa etmeyi sağlar; nesne yalnızca build() çağrıldığında, tüm alanlar hazır olduğunda oluşturulur. Bu sayede uzun parametre listeli kurucular yerine daha esnek ve hatasız bir nesne oluşturma süreci elde edilir.
Bir sınıf tasarlarken immutable mı mutable mı tercih etmeliyim, nasıl karar veririm?
Nesnenin bir değeri mi yoksa değişken durumlu bir varlığı mı temsil ettiğine, thread-safety ihtiyacına ve güncelleme sıklığına bakılmalıdır. Sık değişmeyen, koleksiyon anahtarı olarak kullanılacak veya paylaşılan durumu olan nesneler için immutable tasarım; sık güncellenen, performans kritik nesneler için mutable tasarım daha uygundur.
Immutable sınıf tasarımı, hem Java hem de Python'da yalnızca sözdizimsel bir tercih değil, güvenilir ve öngörülebilir yazılım üretmenin temel prensiplerinden biridir. Bu tür temel tasarım kalıplarını uygulamalı örneklerle, geri bildirim alarak pekiştirmek isteyenler canlı yazılım eğitimi sürecinde konuyu daha derinlemesine çalışabilir.