Java'da ConcurrentModificationException hatası, bir koleksiyon (List, Set veya Map) for-each döngüsüyle veya bir Iterator ile geziliyorken, o koleksiyonun yapısı aynı döngü içinde doğrudan değiştirildiğinde fırlatılır. Yani sorun genelde "veri bozuldu" değil, "gezinme sırasında listenin boyutu veya içeriği beklenmedik şekilde değişti" durumudur. En klasik örneği, bir listeyi for-each ile dolaşırken içindeki elemanlardan birini list.remove() ile silmeye çalışmaktır. Bu yazıda hatanın neden oluştuğunu, Java'nın bunu nasıl tespit ettiğini ve kök nedenini ortadan kaldıracak doğru çözüm yollarını örneklerle göreceksiniz.
ConcurrentModificationException Nedir ve Ne Zaman Oluşur?
ConcurrentModificationException, Java Collections Framework'e ait bir runtime istisnasıdır ve bir koleksiyon üzerinde iterasyon devam ederken o koleksiyonun yapısal olarak değiştirildiğini fark eden iterator tarafından fırlatılır. "Yapısal değişiklik" burada anahtar kavramdır: eleman ekleme, silme veya listenin boyutunu değiştiren her işlem bu kapsama girer; bir elemanın değerini set() ile güncellemek genelde yapısal sayılmaz ve hataya yol açmaz.
Bu davranış sadece ArrayList ile sınırlı değildir. List, Set ve Map implementasyonlarının büyük çoğunluğu aynı mantıkla çalışır: siz bir HashMap'in anahtarlarını dolaşırken haritaya yeni bir kayıt eklerseniz ya da bir HashSet'i for-each ile gezerken elemanlarından birini silerseniz, sonuç aynıdır. Hata, koleksiyonun kendisinden değil, o koleksiyonun ürettiği iterator'ün iç kontrol mekanizmasından kaynaklanır.
Bu noktada birçok yeni başlayan geliştirici hatayı "rastgele" ya da "bazen olan bir şey" olarak algılar; oysa mantığı anlaşıldığında tamamen öngörülebilir ve önlenebilir bir durumdur. Koleksiyonların iç işleyişini, iterator kavramını ve bu tür yapısal detayları sağlam bir temelle öğrenmek isteyenler için 1-1 özel Java dersleri kapsamında bu konular pratik örneklerle, gerçek hata senaryoları üzerinden adım adım işlenir.
Özetle bu hatayı görmek için üç koşulun bir arada gerçekleşmesi gerekir: bir koleksiyon üzerinde iterasyon yapılıyor olmalı, bu iterasyon sırasında koleksiyonun yapısı değiştirilmeli ve bu değişiklik iterator'ün kendi kontrollü metotları (örneğin Iterator.remove()) dışında yapılmalıdır. Sonraki bölümde bu kontrolün Java tarafından tam olarak nasıl yapıldığını inceleyeceğiz.
Fail-Fast Davranışı ve modCount Mantığı

Java'nın standart koleksiyonlarının (ArrayList, HashMap, HashSet vb.) çoğu fail-fast olarak tasarlanmıştır. Fail-fast, bir hatanın sessizce ilerlemesine izin vermek yerine, hatalı durum tespit edildiği anda mümkün olan en erken noktada istisna fırlatarak geliştiriciyi uyarma felsefesidir. Buradaki amaç, tutarsız bir veri durumunun fark edilmeden ilerleyip daha sonra tanısı zor bir hataya dönüşmesini engellemektir.
Bu davranışın arkasındaki teknik mekanizma modCount adı verilen bir sayaçtır. Koleksiyonun kendi içinde tuttuğu bu sayaç, listeye eleman eklendiğinde, çıkarıldığında veya yapısını değiştiren başka bir işlem yapıldığında bir artar. Koleksiyon üzerinde bir iterator oluşturulduğunda, iterator o anki modCount değerini kendi içinde bir anlık görüntü (expectedModCount) olarak saklar.
Döngü her next() çağrısında iterator, kendi sakladığı expectedModCount ile koleksiyonun güncel modCount değerini karşılaştırır. İkisi eşitse her şey yolundadır ve iterasyon devam eder. Ancak koleksiyon, iterator'ün kendi remove() metodu dışında bir yolla (örneğin doğrudan list.remove(eleman) çağrısıyla) değiştirilmişse, koleksiyonun modCount değeri artar ama iterator'ün elindeki expectedModCount aynı kalır. Bu uyumsuzluk fark edildiği anda ConcurrentModificationException fırlatılır.
Burada dikkat edilmesi gereken önemli bir nokta, bu kontrolün her zaman garanti bir kilit mekanizması olmamasıdır; Java dokümantasyonu bu davranışın "best-effort" (elden geldiğince) bir kontrol olduğunu belirtir, yani her koşulda hatayı yakalayacağının garantisi yoktur ama pratikte büyük çoğunlukla yakalar. İterator'ün kendi remove() metodu ise özel davranır: bu metot hem koleksiyondan elemanı siler hem de expectedModCount değerini güncel modCount ile eşitler, böylece iterasyon güvenle devam edebilir. İşte doğru çözümün temeli tam olarak burada yatar.
Hatayı Üreten Örnek: For-Each İçinde list.remove()
Teoriyi somutlaştırmak için en sık karşılaşılan senaryoyu koda dökelim. Aşağıdaki örnekte bir tamsayı listesi for-each döngüsüyle geziliyor ve belirli bir koşulu sağlayan eleman doğrudan listenin remove() metoduyla silinmeye çalışılıyor:
import java.util.ArrayList;
import java.util.List;
public class HataliOrnek {
public static void main(String[] args) {
List<Integer> sayilar = new ArrayList<>();
sayilar.add(10);
sayilar.add(15);
sayilar.add(20);
sayilar.add(25);
for (Integer sayi : sayilar) {
if (sayi == 15) {
sayilar.remove(sayi); // ConcurrentModificationException burada fırlar
}
}
}
}
Bu kod çalıştırıldığında ilk elemanlarda sorun çıkmaz, ancak koşulu sağlayan eleman silinir silinmez program ConcurrentModificationException ile çöker. Sebebi tam olarak önceki bölümde anlatılan mekanizmadır: for-each döngüsü arka planda otomatik olarak bir Iterator oluşturur ve o iterator başlangıçtaki modCount değerini saklar. sayilar.remove(sayi) çağrısı ise iterator'den habersiz, doğrudan listenin kendi metodunu kullanarak yapısal bir değişiklik yapar ve listenin modCount değerini artırır. Döngü bir sonraki elemana geçmek için next() çağırdığında, iterator kendi elindeki eski değerle koleksiyonun yeni değerinin uyuşmadığını fark eder ve hatayı fırlatır.
Burada yaşanan "aha anı" şudur: hata, silme işleminin kendisinden değil, silme işleminin for-each'in kullandığı iterator'ün bilgisi dışında yapılmasından kaynaklanır. Aynı silme işlemi, iterator'ün kendi kontrol mekanizmasından geçirilerek yapılsaydı hiçbir sorun yaşanmayacaktı. Bir sonraki bölümde bu doğru yaklaşımın nasıl uygulandığını, Iterator.remove() ve removeIf() yöntemleriyle birlikte ele alacağız.
Doğru Çözüm: Iterator.remove() ve removeIf()
Önceki bölümde gördüğümüz hatanın kökeninde, koleksiyonu for-each ile gezerken aynı anda list.remove() çağırmak vardı. Çözüm aslında oldukça basit: koleksiyonu değiştirme işini for-each döngüsüne değil, doğrudan Iterator nesnesine bırakmak. Iterator'ın kendi remove() metodu, iç sayaçları senkronize tuttuğu için ConcurrentModificationException fırlatmaz.
List<String> isimler = new ArrayList<>(List.of("Ali", "Veli", "Ayşe", "Can"));
Iterator<String> it = isimler.iterator();
while (it.hasNext()) {
String isim = it.next();
if (isim.startsWith("A")) {
it.remove(); // Güvenli silme
}
}
System.out.println(isimler); // [Veli, Can]
Burada döngü artık while (it.hasNext()) ile klasik for-each'in arkasında gizlenen mantığı elle yönetiyor. Silme işlemi list.remove() yerine it.remove() üzerinden yapıldığı için liste ile iteratör senkron kalıyor.
Java'nın koleksiyon API'sine sonradan eklenen removeIf() metodu ise aynı işi çok daha kısa şekilde yapar. Metot, bir Predicate alır ve koşulu sağlayan tüm elemanları döngü yazmanıza gerek kalmadan güvenli biçimde çıkarır:
List<String> isimler = new ArrayList<>(List.of("Ali", "Veli", "Ayşe", "Can"));
isimler.removeIf(isim -> isim.startsWith("A"));
System.out.println(isimler); // [Veli, Can]
Tek satırlık bu çözüm, iç mantıkta yine iteratör tabanlı güvenli silmeyi kullanır; siz sadece koşulu tanımlarsınız. Koleksiyonlarla çalışırken bu tür ince ayrıntıları pratikte defalarca uygulamak, hatayı ezbere değil mantığına oturtarak öğrenmenin en hızlı yoludur. Bire bir ilerleyen bir çalışma isteyenler için Java özel ders randevusu bu tarz koleksiyon hatalarını canlı kod üzerinde görmek isteyenlere pratik bir seçenek sunar.
Doğru kullanım için izlenecek adımlar şöyle özetlenebilir:
- Koleksiyon üzerinde döngü kurarken, silme ihtiyacı olup olmadığını baştan belirleyin.
- Silme gerekiyorsa for-each yerine açık bir
Iteratornesnesi oluşturun. - Elemanı
iterator.next()ile aldıktan sonra, koşul sağlanıyorsaiterator.remove()çağırın. - Basit koşullu silmelerde döngü yazmak yerine doğrudan
removeIf()tercih edin. - Listeyi değil elemanları değiştirmek istiyorsanız (silme değil güncelleme)
ListIterator.set()kullanmayı değerlendirin.
Yöntemleri Karşılaştırma: Hangi Durumda Ne Kullanılır?

Koleksiyon üzerinde eleman silme veya değiştirme ihtiyacı, projeden projeye farklı yaklaşımlar gerektirebilir. Aşağıdaki tablo, en sık karşılaşılan senaryolarda hangi yöntemin tercih edilmesi gerektiğini ve nelere dikkat edilmesi gerektiğini özetliyor.
| Yöntem | Ne Zaman Kullanılır | Dikkat Edilmesi Gereken Nokta |
|---|---|---|
| Iterator.remove() | Döngü içinde koşullu ve elemanlara tek tek erişim gerektiren karmaşık silme işlemlerinde | Silme işlemi mutlaka iteratör üzerinden yapılmalı, listenin kendi remove() metodu çağrılmamalı |
| removeIf() | Tek bir koşula göre toplu silme yapılacak basit ve okunabilir senaryolarda | Predicate içinde yan etkili (side-effect) kod çalıştırmaktan kaçınılmalı |
| ListIterator.set() | Elemanları silmek değil, döngü sırasında güncellemek gerektiğinde | Sadece List arayüzünü destekleyen koleksiyonlarda kullanılabilir |
| Yeni liste oluşturma (stream/filter) | Orijinal koleksiyonu değiştirmeden filtrelenmiş bir kopya elde etmek istendiğinde | Bellek kullanımı artar; büyük koleksiyonlarda performans etkisi göz önünde bulundurulmalı |
| CopyOnWriteArrayList | Birden fazla thread'in aynı anda okuma yaptığı, yazmanın nadir olduğu senaryolarda | Her yazma işleminde dizinin tamamı kopyalanır, sık yazımda maliyetlidir |
Bu tablodaki seçim aslında tek bir soruya indirgenebilir: Amacınız silme mi, güncelleme mi, yoksa çoklu erişim güvenliği mi? Bu soruyu netleştirdiğinizde doğru yöntem kendiliğinden ortaya çıkar.
CopyOnWriteArrayList ve Map Üzerinde Benzer Durum
CopyOnWriteArrayList, ArrayList'in thread-safe bir alternatifi olarak tasarlanmıştır; her yazma işleminde (ekleme, silme) dizinin yeni bir kopyasını oluşturur, okuma işlemleri ise mevcut kopya üzerinden kesintisiz devam eder. Bu yapı sayesinde bir thread listeyi gezerken başka bir thread eleman eklese veya silse bile ConcurrentModificationException oluşmaz. Ancak bu güvenlik bedelsiz değildir: her değişiklikte tüm dizi kopyalanır, dolayısıyla sık yazma yapılan senaryolarda performans belirgin şekilde düşer.
Burada altı çizilmesi gereken önemli bir nokta var: eğer uygulamanız tek thread üzerinde çalışıyorsa, yani koleksiyona birden fazla iş parçacığı aynı anda erişmiyorsa, CopyOnWriteArrayList kullanmak gereksiz bir maliyettir. Tek thread'de yaşanan ConcurrentModificationException zaten eşzamanlılıktan değil, aynı thread içinde döngü sırasında koleksiyonu değiştirmekten kaynaklanır; bu durumda çözüm bir önceki bölümde anlatılan Iterator.remove() veya removeIf()'dur, thread-safe bir koleksiyona geçmek değildir.
Aynı fail-fast davranış Map yapılarında da karşımıza çıkar. Bir HashMap üzerinde keySet() veya entrySet() ile döngü kurup, döngü içinde doğrudan map.remove(key) çağırmak aynı hatayı üretir; çünkü keySet() ve entrySet() map'in kendi iç yapısına bağlı görünümlerdir ve map değiştiğinde bu görünümlerin modCount'u da senkron bozulur. Çözüm burada da aynıdır: keySet().iterator() üzerinden remove() çağırmak veya map.entrySet().removeIf(...) kullanmak. Koleksiyonlar ve Map yapıları arasındaki bu paralel mantığı pekiştirmek isteyenler, konuyu ne kadar özümsediklerini Java bilgi seviyesi testi ile hızlıca ölçebilir.
Özetle, hem List hem Map için kural aynıdır: koleksiyonu gezerken değiştirme işlemini her zaman iteratöre veya iteratör tabanlı bir metoda (removeIf) devretmek, hatayı kaynağında ortadan kaldırır.
Bu Hatayı Try-Catch ile Yamamak Yerine Kök Nedeni Çözmek
ConcurrentModificationException ile karşılaşan pek çok geliştiricinin ilk refleksi, hatayı bir try-catch bloğuyla yakalayıp programı "çökmesin" diye susturmaktır. Bu yaklaşım burada yanlıştır, çünkü bu istisna genel anlamda beklenmedik bir çalışma zamanı hatası değil, kodun koleksiyonu yanlış kullandığının doğrudan bir göstergesidir. Dosya bulunamadı ya da ağ bağlantısı koptu türünden, dışarıdan gelen ve programın kontrolü dışındaki bir durumu ele alan klasik istisna yönetimiyle bu senaryoyu aynı kefeye koymak, sorunu gizlemekten öteye gitmez.
Bir catch (ConcurrentModificationException e) bloğu eklediğinizde, döngü yarıda kesilir, bazı elemanlar işlenmeden kalır ve hata sessizce yutulur. Sonuç olarak program çalışmaya devam eder ama artık yanlış veya eksik bir veri kümesiyle işlem yapıyordur. Bu, gerçek bir çözüm değil, semptomu maskeleyen bir yama olur. Aşağıdaki karşılaştırma bu farkı somutlaştırır:
// Yanlış yaklaşım: hatayı yutmak
try {
for (String s : liste) {
if (s.equals("sil")) liste.remove(s);
}
} catch (ConcurrentModificationException e) {
// hiçbir şey yapılmıyor, eksik silme fark edilmiyor
}
// Doğru yaklaşım: kök nedeni ortadan kaldırmak
liste.removeIf(s -> s.equals("sil"));
İkinci örnekte hata yönetimine hiç ihtiyaç yoktur, çünkü sorun kaynağında çözülmüştür. Bu ayrımı erken fark etmek, ileride daha büyük ve iç içe koleksiyon yapılarında saatler süren hata ayıklama seanslarından kurtarır. Bu tür yapısal hataları try-catch ile kapatmak yerine neden oluştuğunu anlayarak ilerlemek, Java'da sağlam kod yazmanın temel alışkanlıklarından biridir. Koleksiyonlar, iterator mantığı ve fail-fast davranışları gibi konuları uygulamalı örneklerle, canlı geri bildirim alarak pekiştirmek isteyenler için birebir Java özel dersi içeriği bu tür pratik senaryoları adım adım ele alır.
Özetle, ConcurrentModificationException gördüğünüzde yapılması gereken ilk şey hatayı bastırmak değil, döngü sırasında koleksiyonun neden değiştirildiğini sorgulamaktır. Kök neden çözüldüğünde istisna zaten hiç oluşmaz; bu da kodu hem daha okunabilir hem de daha güvenilir hale getirir.
Sık Sorulan Sorular
ConcurrentModificationException her zaman çoklu thread kullanımında mı oluşur?
Hayır. Bu istisna en sık tek thread'li kodlarda, bir for-each döngüsü içinde koleksiyonun yapısını doğrudan değiştirmeye çalışırken görülür. Çoklu thread ortamında da benzer bir durum oluşabilir, ancak asıl tetikleyici thread sayısı değil, iterator'ın beklediği yapı ile koleksiyonun gerçek durumu arasındaki uyumsuzluktur.
For-each yerine normal for döngüsü kullanırsam bu hatadan kurtulur muyum?
Endeks tabanlı bir for döngüsüyle listeden eleman silmek hatayı bazen gizler ama elemanların atlanmasına yol açar, çünkü silme sonrası indeksler kayar. Bu, hatayı çözmek değil farklı bir hataya dönüştürmektir; doğru yol yine Iterator.remove() veya removeIf() kullanmaktır.
Iterator.remove() ile list.remove() arasındaki fark nedir?
list.remove() koleksiyonun yapısını doğrudan değiştirir ve iterator'ın bundan haberi olmaz, bu yüzden fail-fast mekanizması devreye girer. Iterator.remove() ise iterator'ın kendi iç durumunu da güncelleyerek silme işlemini güvenli hale getirir.
removeIf() her koleksiyon türünde çalışır mı?
removeIf(), Collection arayüzünü uygulayan List, Set gibi yapılarda kullanılabilir. Map üzerinde doğrudan çalışmaz; Map için genellikle entrySet().removeIf() gibi bir yaklaşım tercih edilir.
CopyOnWriteArrayList performans açısından her senaryoda tercih edilmeli mi?
Hayır. Bu yapı her değişiklikte listenin bir kopyasını oluşturduğu için sık yazma işlemi yapılan senaryolarda maliyetlidir. Okuma işleminin yazmadan çok daha fazla olduğu, eşzamanlı erişim gerektiren durumlarda anlamlıdır; genel amaçlı kullanımda standart liste yapıları yeterlidir.
ConcurrentModificationException, aslında Java'nın koleksiyonları güvenli kullanmanız için size verdiği erken bir uyarıdır; onu bastırmak yerine anlamak, daha sağlam kod yazmanın ilk adımıdır. Bu tür koleksiyon ve iterator mantıklarını ne kadar iyi kavradığınızı görmek isterseniz Java bilgi testi ile kendinizi kısa sürede ölçebilirsiniz.