Java’da ConcurrentModificationException hatasını çözmenin temel yolu, üzerinde gezinilen koleksiyonu döngü devam ederken doğrudan değiştirmemektir. Özellikle for-each içinde ArrayList.remove() veya HashSet.remove() çağırmak yerine, işlemi Iterator.remove(), removeIf() ya da ayrı bir filtreleme adımıyla yapmak gerekir.
Bu hata adında “concurrent” kelimesi geçmesine rağmen her zaman çoklu thread probleminden kaynaklanmaz. Tek bir thread içinde de bir koleksiyon üzerinde gezinirken, aynı koleksiyonun yapısını doğrudan değiştirirsen bu istisnayla karşılaşabilirsin.
ConcurrentModificationException neden oluşur?
Bir koleksiyonun elemanlarını sırayla okumak için kullanılan nesneye Iterator denir. Iterator, gezinme sırasında koleksiyonun yapısının değişmediğini varsayar. Buradaki yapısal değişiklik; koleksiyona eleman eklemek, eleman silmek veya koleksiyonun eleman sayısını değiştiren benzer işlemler anlamına gelir.
Örneğin bir ArrayList üzerinde Iterator etkin durumdayken listeyi doğrudan remove() ile değiştirirsen Iterator, kendi takip ettiği durum ile koleksiyonun yeni durumu arasında uyumsuzluk olduğunu fark edebilir. Bunun sonucunda ConcurrentModificationException fırlatılır.
for-each döngüsünü düşünmenin pratik yolu şudur: Iterable olan bir koleksiyonun elemanları arasında dolaşırken arka planda bir Iterator kullanılıyormuş gibi davranılır.
for (String gorev : gorevler) {
System.out.println(gorev);
}
Bu kodu yazarken Iterator’ı kendin oluşturmuyor olsan da döngünün ilerleyebilmesi için koleksiyonun bir sonraki elemanını bulan bir gezinme mekanizması çalışır. Bu nedenle döngü gövdesinde koleksiyonun kendisini yapısal olarak değiştirmek risklidir.
Java koleksiyonlarında görülen bu davranış fail-fast olarak adlandırılır. Fail-fast yaklaşımı, Iterator’ın beklenmedik bir koleksiyon değişikliğini mümkün olduğunca erken fark edip sessizce yanlış sonuç üretmek yerine hata vermesini amaçlar. Ancak bu mekanizma kesin bir güvenlik garantisi değildir. Java API dokümantasyonunda fail-fast davranışının “best effort” temelinde çalıştığı ve program doğruluğunun bu istisnanın kesinlikle oluşacağı varsayımına dayandırılmaması gerektiği belirtilir. Yani ConcurrentModificationException yakalamak, koleksiyonu eşzamanlı erişime karşı güvenli hâle getirmez; öncelikle hatalı değiştirme biçimini düzeltmek gerekir.
Hatalı ArrayList örneğini yeniden üretme

Aşağıdaki örnekte liste, for-each döngüsüyle okunurken doğrudan liste üzerinden değiştiriliyor:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> gorevler = new ArrayList<>(
List.of("Raporu yaz", "Toplantıya katıl", "Dosyaları arşivle")
);
for (String gorev : gorevler) {
if (gorev.equals("Toplantıya katıl")) {
gorevler.remove(gorev);
}
}
System.out.println(gorevler);
}
}
gorevler listesi oluşturulur ve for-each döngüsü ilk elemandan başlayarak listeyi dolaşır. Döngü değişkeni gorev, o an Iterator tarafından döndürülen elemanı temsil eder. Koşul sağlandığında yapılan gorevler.remove(gorev) çağrısı ise Iterator üzerinden değil, doğrudan koleksiyon üzerinden gerçekleştirilir.
Bu doğrudan silme işlemi listenin yapısını değiştirir. Iterator bir sonraki adımda ilerlemeye çalıştığında çoğu yaygın çalışma senaryosunda ConcurrentModificationException görülür. Bununla birlikte fail-fast mekanizması kesin çıktı garantisi sunmadığı için, örneğin davranışını yalnızca “istisna kesinlikle şu satırda oluşur” şeklinde yorumlamamak gerekir.
Burada önemli bir ayrım vardır: gorev değişkenine yeni bir değer atamak ile gorevler listesinden eleman silmek aynı şey değildir. İlki yalnızca döngü değişkenini etkiler; ikincisi ise Iterator’ın üzerinde çalıştığı koleksiyonun yapısını değiştirir.
Aynı temel kural HashSet için de geçerlidir. Ancak HashSet elemanların dolaşım sırasını garanti etmez. Bu nedenle HashSet ile yapılan örneklerde hangi elemanın önce yazdırılacağına veya çıktının belirli bir sırada görüneceğine güvenmemelisin. Koleksiyon değiştirme sorununu daha sistematik biçimde öğrenmek isteyenler, Java koleksiyonları ve algoritmik düşünme üzerine birebir çalışma yaklaşımından yararlanabilir.
Güvenli silme ve filtreleme yöntemleri

ConcurrentModificationException hatasını önlemenin temel kuralı, döngü devam ederken koleksiyonu hangi mekanizmayla dolaşıyorsan değişikliği de aynı mekanizma üzerinden yapmaktır. Bunun için üç yaygın yaklaşım vardır: Iterator.remove(), removeIf() ve yeni bir koleksiyon oluşturarak filtreleme.
Iterator.remove() ile kontrollü silme
Iterator.remove(), yineleme sırasında silme yapmak için kullanılan güvenli yöntemdir. Silinen öğe, doğrudan liste üzerinden değil, o öğeyi son next() çağrısıyla döndüren Iterator üzerinden kaldırılır.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
List<String> isimler = new ArrayList<>(
List.of("Ali", "Veli", "Ayşe"));
Iterator<String> iterator = isimler.iterator();
while (iterator.hasNext()) {
String isim = iterator.next();
if (isim.equals("Veli")) {
iterator.remove();
}
}
System.out.println(isimler);
Burada remove() yalnızca geçerli bir next() çağrısıyla alınan son öğe için kullanılabilir. Aynı öğe üzerinde arka arkaya iki kez çağrılamaz; yeni bir next() çağrısı yapılmadan tekrar kullanılırsa IllegalStateException oluşur. Ayrıca aynı döngü içinde iterator.remove() ile isimler.remove(...) yöntemlerini karıştırmamalısın. Iterator ilerlerken koleksiyonu doğrudan değiştirmek, yineleme durumunu bozabilir. Bu çağrı kısıtları ve Iterator üzerinden silmenin tanımlı davranışı Java API belgelerinde açıkça belirtilir.
removeIf() ile koşula uyanları topluca temizleme
Bir koşula uyan tüm öğeleri silmek istediğinde removeIf() daha kısa ve okunabilir bir seçenek olabilir:
List<Integer> sayilar = new ArrayList<>(
List.of(3, 8, 11, 14, 20));
sayilar.removeIf(sayi -> sayi % 2 == 0);
System.out.println(sayilar); // [3, 11]
removeIf(), koşulu true döndüren öğeleri kaldırır ve Java’da Collection arayüzüne Java 8 ile eklenmiştir. Varsayılan uygulama, koleksiyonu Iterator ile dolaşarak eşleşen öğeleri Iterator üzerinden siler. Buna rağmen removeIf() çalışırken koleksiyona ayrıca doğrudan add() veya remove() çağrısı yapmamalısın.
Yeni koleksiyon oluşturarak filtreleme
Özgün listeyi korumak, eski ve yeni veriyi karşılaştırmak ya da filtrelenmiş veriyi başka bir işlemde kullanmak istiyorsan ayrı bir koleksiyon oluşturabilirsin:
List<String> roller = List.of("admin", "user", "editor");
List<String> yetkiliRoller = new ArrayList<>();
for (String rol : roller) {
if (!rol.equals("user")) {
yetkiliRoller.add(rol);
}
}
System.out.println(yetkiliRoller); // [admin, editor]
Bu yaklaşımda dolaşılan koleksiyon değiştirilmediği için yineleme sırasında yapısal değişiklik yaşanmaz. Özellikle özgün verinin başka bir bölümde kullanılacağı, filtreleme sonucunun yeni bir liste olarak döndürüleceği veya dönüşüm işlemi yapılacağı durumlarda daha güvenlidir.
| Yöntem | Uygun senaryo | Güçlü yön | Dikkat edilmesi gereken |
|---|---|---|---|
| Iterator.remove() | Tek geçerli öğeyi döngü içinde silme | Yinelemeyle uyumlu kontrollü silme | Önce next(), sonra en fazla bir remove() |
| removeIf() | Koşula uyan tüm öğeleri temizleme | Kısa ve okunabilir filtreleme | Aynı dolaşım sırasında doğrudan değişiklik yapmama |
| Yeni koleksiyon | Özgün veriyi koruma veya dönüşüm | Yan etkisi düşük ve anlaşılır yapı | Ek koleksiyon için bellek gerekir |
Kısa karar çerçevesi şöyledir: Tek bir geçerli öğeyi döngü sırasında silmek için Iterator.remove(), belirli bir koşula uyanları doğrudan temizlemek için removeIf(), özgün koleksiyonu korumak veya veriyi dönüştürmek için yeni koleksiyon yaklaşımını seç.
Tamamlanan görevleri güvenli biçimde silen mini program
Aşağıdaki program, görev adını ve tamamlanma durumunu bir sınıfla temsil eder. Iterator, tamamlanan görevleri güvenli biçimde siler; döngü tamamlandıktan sonra kalan görevler yazdırılır.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
class Gorev {
String ad;
boolean tamamlandi;
Gorev(String ad, boolean tamamlandi) {
this.ad = ad;
this.tamamlandi = tamamlandi;
}
@Override
public String toString() {
return ad;
}
}
public class GorevProgrami {
public static void main(String[] args) {
List<Gorev> gorevler = new ArrayList<>();
gorevler.add(new Gorev("Raporu hazırla", true));
gorevler.add(new Gorev("Kod incelemesi yap", false));
gorevler.add(new Gorev("Testleri çalıştır", true));
gorevler.add(new Gorev("Dokümantasyonu güncelle", false));
Iterator<Gorev> iterator = gorevler.iterator();
while (iterator.hasNext()) {
Gorev gorev = iterator.next();
if (gorev.tamamlandi) {
iterator.remove();
}
}
System.out.println(gorevler);
}
}
Kontrol noktaları sırasıyla şöyledir: Iterator oluşturulur, hasNext() ile sırada öğe olup olmadığı kontrol edilir, next() mevcut görevi verir, koşul sağlanıyorsa iterator.remove() çağrılır ve liste döngü tamamlandıktan sonra yazdırılır.
Beklenen çıktı şöyledir:
[Kod incelemesi yap, Dokümantasyonu güncelle]
Bu sıralama ArrayList kullanıldığı için ekleme sırasını korur. Farklı koleksiyon türlerinde öğelerin dolaşım sırası değişebilir; bu nedenle çıktıyı değerlendirirken kullanılan koleksiyonun sıra garantisini dikkate almalısın.
Aynı işlem removeIf() ile daha kısa yazılabilir:
gorevler.removeIf(gorev -> gorev.tamamlandi);
Ancak iki yöntemi aynı işlemde birlikte kullanma. Ya Iterator ile adım adım silme yap ya da koşullu temizliği removeIf() yöntemine bırak.
Hangi durumda hangi yöntemi seçmeliyim?
Yöntem seçimini şu kısa akışla yapabilirsin:
- Döngü sırasında tek tek silme gerekiyor mu? Evetse
Iterator.remove()kullan. - Yalnızca belirli bir koşula uyan öğeleri temizlemek mi gerekiyor? Evetse
removeIf()daha doğrudan bir çözümdür. - Özgün koleksiyon korunmalı veya yeni bir sonuç mu üretilmeli? Evetse filtrelenmiş yeni bir koleksiyon oluştur.
| İhtiyaç | Uygun yöntem | Temel gerekçe |
|---|---|---|
| Döngü içinde karar vererek tek tek silme | Iterator.remove() |
Silme işlemi, dolaşımı yöneten iterator üzerinden yapılır. |
| Bir koşula uyan tüm öğeleri temizleme | removeIf() |
Filtreleme niyetini kısa ve açık biçimde ifade eder. |
| Kaynak koleksiyonu değiştirmeden sonuç alma | Yeni koleksiyon | Özgün verinin korunmasını sağlar. |
Iterator.remove() kullanıyorsan önce next() ile bir öğe alınmış olmalıdır. Aynı next() çağrısından sonra remove() metodunu iki kez çağırmak geçerli değildir; ayrıca her iterator bu işlemi desteklemek zorunda değildir. Iterator dışındaki koleksiyon değiştirme işlemleri, dolaşım devam ederken tanımsız davranışa veya fail-fast iterator’larda ConcurrentModificationException hatasına yol açabilir.
Pratik kontrol listesi:
- Döngü içinde doğrudan
collection.remove()çağırma. - Iterator kullanıyorsan silme işlemini onun üzerinden yap.
removeIf()seçtiğinde aynı işi manuel iterator dolaşımıyla gereksiz yere karıştırma.HashSetüzerinde çalışırken elemanların belirli bir sırada geleceğini varsayma.- Silme koşulunu açık yaz: örneğin
task.isCompleted()gibi anlamlı bir koşul kullan. - Küçük bir test verisiyle beklenen sonucun gerçekten oluştuğunu doğrula.
Başka bir thread koleksiyonu değiştirirken sen de aynı koleksiyonu dolaşıyorsan sorun artık yalnızca for-each sözdizimi değildir. Genel amaçlı koleksiyonlarda eşzamanlı erişim için erişim tasarımı, dışarıdan senkronizasyon veya uygun eşzamanlı veri yapısı ayrıca değerlendirilmelidir. ConcurrentHashMap görünümlerinin iterator’ları weakly consistent davranabilir; ancak bu, her koleksiyon için otomatik çözüm anlamına gelmez.
CopyOnWriteArrayList de her durumda önerilecek bir kaçış yolu değildir. Yazma işlemlerinde temel dizinin kopyalanması nedeniyle okumanın çok, yazmanın az olduğu ve eşzamanlı gezinmenin önemli olduğu senaryolarda değerlendirilebilir; seçim yaparken okuma-yazma dengesi ve uygulamanın eşzamanlılık gereksinimleri birlikte incelenmelidir.
Java koleksiyonları ve iterator temellerini pekiştirmek için Java bilgi testi ile küçük bir kontrol yapabilirsin.
Sık Sorulan Sorular
ConcurrentModificationException adı neden çoklu thread varmış gibi görünüyor?
Adındaki “concurrent” ifadesi yanıltıcı olabilir. Hata, farklı thread’ler olmadan da tek bir thread’in fail-fast iterator ile dolaşırken koleksiyonu doğrudan değiştirmesi sonucunda oluşabilir.
for-each içinde neden doğrudan ArrayList.remove() kullanmamalıyım?
Çünkü for-each, arka planda iterator kullanır. ArrayList.remove() iterator’ın beklediği yapısal değişiklik takibini bozar ve sonraki kontrolde ConcurrentModificationException oluşabilir.
Iterator.remove() her döngüde kullanılabilir mi?
Yalnızca iterator’ın desteklediği durumlarda kullanılabilir. Önce next() çağrılmalı ve aynı öğe için ikinci kez çağrılmamalıdır; aksi hâlde IllegalStateException veya desteklenmiyorsa UnsupportedOperationException alınabilir.
removeIf() ile Iterator.remove() arasında nasıl seçim yapmalıyım?
Tüm silme mantığın tek bir koşula dayanıyorsa removeIf() daha sade olur. Her öğe için farklı işlemler yapıyor veya silme kararını adım adım veriyorsan Iterator.remove() daha uygundur.
HashSet üzerinde eleman silerken ArrayList’e göre nelere dikkat etmeliyim?
HashSet üzerinde sıra varsaymamalısın. Silme kararını elemanın değerine veya açıkça yazılmış koşula bağla; dolaşım sırasında doğrudan seti değil, destekleniyorsa iterator’ı kullan.
Doğru yöntem, hatayı bastırmaktan çok koleksiyonun nasıl dolaşıldığını ve ne zaman değiştirildiğini netleştirmeye dayanır.