Berk Akademi
Birebir ders başvurusu Ücretsiz ön görüşme Ana Sayfa

Java’da ConcurrentModificationException Nasıl Çözülür?

javada-concurrentmodificationexception-nasil-cozulur
Bu yazıda neler var?
  1. ConcurrentModificationException neden oluşur?
  2. Hatalı ArrayList örneğini yeniden üretme
  3. Güvenli silme ve filtreleme yöntemleri
  4. Tamamlanan görevleri güvenli biçimde silen mini program
  5. Hangi durumda hangi yöntemi seçmeliyim?
  6. Sık Sorulan Sorular

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

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

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:

  1. Döngü sırasında tek tek silme gerekiyor mu? Evetse Iterator.remove() kullan.
  2. Yalnızca belirli bir koşula uyan öğeleri temizlemek mi gerekiyor? Evetse removeIf() daha doğrudan bir çözümdür.
  3. Ö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.

Bu içerik aradığın cevabı verdi mi?
Yanıtın, hangi yazıları geliştirmemiz gerektiğini anlamamıza yardımcı olur.
Bu içeriğin üretilmesinde yapay zeka araçlarından destek alınmıştır.

Bu konudan sonra ne okuyabilirsin?

Tüm yazılar

İlgili Eğitimler

Berk Keskin, yazılım geliştirici ve eğitmen
Yazar

Berk Keskin Kimdir?

Yazılıma 12 yaşında başladı; İzmir Ekonomi Üniversitesi'ni bölüm birincisi ve yüksek şeref öğrencisi olarak tamamladı. Bugün yalnızca eğitim vermekle kalmıyor, sektörde aktif olarak yazılım projeleri geliştiriyor ve gerçek dünya deneyimini birebir derslerine taşıyor. Ezberden uzak, mühendislik zihniyetini merkeze alan sürdürülebilir öğrenme sistemleri tasarlayarak sorgulayan, üreten ve problem çözebilen yeni nesil yazılımcılar yetiştiriyor.

Sektörel Deneyim & Projeler

  • Ticarify Entegrasyon Yazılım logosu CEO Ticarify Entegrasyon YazılımPazaryerleri ve e-ticaret sitelerine otomatik e-fatura kesimi, sipariş ve kargo takibi hizmetleri sunan e-Dönüşüm platformunun API mimarisini ve yazılım ekibini yönetmektedir.
  • Benim Düğünüm logosu CEO Benim DüğünümDijital etkinlik ve anı paylaşım platformu.
  • Siberdizayn logosu Yazılım Ekibi Lideri SiberdizaynYüksek anlık oyuncu trafiğine sahip oyun kontrol panelleri ve sunucu altyapıları geliştiren yazılım ekibine liderlik etmektedir.
  • MEDYOGRAFYA 360° Dijital Çözümler logosu Dijital Strateji Lideri MEDYOGRAFYA 360° Dijital ÇözümlerŞirketlerin dijital çözümlerde uzun vadede nasıl ilerlemesi gerektiği ve dijital dönüşüm süreçlerinin yönetilmesine destek olmaktadır.
  • İzmir Ekonomi Üniversitesi logosu Danışma Kurulu Üyesi İzmir Ekonomi ÜniversitesiMezun olduğu üniversitesinde, Bilgisayar Programcılığı bölümünün akademik müfredatını güncel sektör ihtiyaçlarına göre şekillendirmek adına Danışma Kurulu'nda görev almaktadır.
WhatsApp Hemen Ara