Iterasi melalui Koleksi, menghindari ConcurrentModificationException saat menghapus objek dalam satu lingkaran


1194

Kita semua tahu Anda tidak dapat melakukan hal berikut karena ConcurrentModificationException:

for (Object i : l) {
    if (condition(i)) {
        l.remove(i);
    }
}

Tapi ini tampaknya bekerja kadang-kadang, tetapi tidak selalu. Ini beberapa kode spesifik:

public static void main(String[] args) {
    Collection<Integer> l = new ArrayList<>();

    for (int i = 0; i < 10; ++i) {
        l.add(4);
        l.add(5);
        l.add(6);
    }

    for (int i : l) {
        if (i == 5) {
            l.remove(i);
        }
    }

    System.out.println(l);
}

Ini, tentu saja, menghasilkan:

Exception in thread "main" java.util.ConcurrentModificationException

Meskipun beberapa utas tidak melakukannya. Bagaimanapun.

Apa solusi terbaik untuk masalah ini? Bagaimana saya bisa menghapus item dari koleksi dalam satu lingkaran tanpa membuang pengecualian ini?

Saya juga menggunakan arbitrer di Collectionsini, tidak harus ArrayList, jadi Anda tidak bisa mengandalkan get.


Catatan untuk para pembaca: jangan membaca docs.oracle.com/javase/tutorial/collections/interfaces/… , mungkin ada cara yang lebih mudah untuk mencapai apa yang ingin Anda lakukan.
GKFX

Jawaban:


1601

Iterator.remove() aman, Anda bisa menggunakannya seperti ini:

List<String> list = new ArrayList<>();

// This is a clever way to create the iterator and call iterator.hasNext() like
// you would do in a while-loop. It would be the same as doing:
//     Iterator<String> iterator = list.iterator();
//     while (iterator.hasNext()) {
for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
    String string = iterator.next();
    if (string.isEmpty()) {
        // Remove the current element from the iterator and the list.
        iterator.remove();
    }
}

Perhatikan bahwa Iterator.remove()satu-satunya cara aman untuk mengubah koleksi selama iterasi; perilaku tidak ditentukan jika koleksi yang mendasarinya dimodifikasi dengan cara lain saat iterasi sedang berlangsung.

Sumber: docs.oracle> The Collection Interface


Dan demikian pula, jika Anda memiliki ListIteratordan ingin menambahkan item, Anda dapat menggunakan ListIterator#add, untuk alasan yang sama Anda dapat menggunakan Iterator#remove - itu dirancang untuk memungkinkannya.


Dalam kasus Anda Anda mencoba untuk menghapus dari daftar, tapi pembatasan yang sama berlaku jika mencoba untuk putmenjadi Mapsementara iterasi isinya.


19
Bagaimana jika Anda ingin menghapus elemen selain elemen yang dikembalikan dalam iterasi saat ini?
Eugen

2
Anda harus menggunakan .remove di iterator dan itu hanya dapat menghapus elemen saat ini, jadi tidak ada :)
Bill K

1
Perlu diketahui bahwa ini lebih lambat dibandingkan dengan menggunakan ConcurrentLinkedDeque atau CopyOnWriteArrayList (setidaknya dalam kasus saya)
Dan

1
Apakah tidak mungkin untuk melakukan iterator.next()panggilan ke-loop? Jika tidak, bisakah seseorang menjelaskan alasannya?
Blake

1
@GonenI Ini diterapkan untuk semua iterator dari koleksi yang tidak dapat diubah. List.add"opsional" dalam arti yang sama juga, tetapi Anda tidak akan mengatakan itu "tidak aman" untuk ditambahkan ke daftar.
Radiodef

345

Ini bekerja:

Iterator<Integer> iter = l.iterator();
while (iter.hasNext()) {
    if (iter.next() == 5) {
        iter.remove();
    }
}

Saya berasumsi bahwa karena foreach loop adalah gula sintaksis untuk iterasi, menggunakan iterator tidak akan membantu ... tetapi memberi Anda .remove()fungsi ini .


43
foreach loop adalah gula sintaksis untuk iterasi. Namun seperti yang Anda tunjukkan, Anda perlu memanggil remove pada iterator - yang sebelumnya tidak memberi Anda akses. Oleh karena itu alasan mengapa Anda tidak dapat menghapus dalam loop foreach (meskipun Anda benar - benar menggunakan iterator di bawah tenda)
madlep

36
+1 sebagai kode contoh untuk menggunakan iter.remove () dalam konteks, yang tidak dimiliki oleh Bill K.
Diubah

202

Dengan Java 8 Anda dapat menggunakan metode baruremoveIf . Diterapkan pada contoh Anda:

Collection<Integer> coll = new ArrayList<>();
//populate

coll.removeIf(i -> i == 5);

3
Ooooo! Saya berharap sesuatu di Java 8 atau 9 dapat membantu. Ini sepertinya masih agak bertele-tele bagi saya, tetapi saya masih menyukainya.
James T Snell

Apakah penerapan equals () direkomendasikan dalam kasus ini juga?
Anmol Gupta

by the way removeIfmenggunakan Iteratordan whileloop. Anda dapat melihatnya di java 8java.util.Collection.java
omerhakanbilici

3
@omerhakanbilici Beberapa implementasi seperti menimpanya ArrayListkarena alasan kinerja. Yang Anda maksud hanyalah implementasi standar.
Didier L

@AnmolGupta: Tidak, equalstidak digunakan sama sekali di sini, jadi tidak harus diimplementasikan. (Tapi tentu saja, jika Anda menggunakan equalsdalam tes Anda maka itu harus dilaksanakan dengan cara yang Anda inginkan.)
Lii

42

Karena pertanyaan telah dijawab yaitu cara terbaik adalah menggunakan metode hapus dari objek iterator, saya akan pergi ke spesifik tempat di mana kesalahan "java.util.ConcurrentModificationException"dilemparkan.

Setiap kelas koleksi memiliki kelas pribadi yang mengimplementasikan antarmuka Iterator dan menyediakan metode seperti next(), remove()dan hasNext().

Kode untuk selanjutnya terlihat seperti ini ...

public E next() {
    checkForComodification();
    try {
        E next = get(cursor);
        lastRet = cursor++;
        return next;
    } catch(IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
    }
}

Di sini metode checkForComodificationdiimplementasikan sebagai

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

Jadi, seperti yang Anda lihat, jika Anda secara eksplisit mencoba menghapus elemen dari koleksi. Ini menghasilkan modCountperbedaan dari expectedModCount, menghasilkan pengecualian ConcurrentModificationException.


Sangat menarik. Terima kasih! Saya sering tidak memanggil remove () sendiri, saya lebih suka membersihkan koleksi setelah mengulanginya. Bukan untuk mengatakan itu adalah pola yang baik, hanya apa yang saya lakukan belakangan ini.
James T Snell

26

Anda dapat menggunakan iterator secara langsung seperti yang Anda sebutkan, atau menyimpan koleksi kedua dan menambahkan setiap item yang ingin Anda hapus ke koleksi baru, lalu hapus semua di bagian akhir. Hal ini memungkinkan Anda untuk tetap menggunakan jenis-keamanan loop untuk-masing-masing dengan biaya peningkatan penggunaan memori dan waktu cpu (seharusnya tidak menjadi masalah besar kecuali Anda memiliki daftar yang benar-benar besar atau komputer yang benar-benar tua)

public static void main(String[] args)
{
    Collection<Integer> l = new ArrayList<Integer>();
    Collection<Integer> itemsToRemove = new ArrayList<>();
    for (int i=0; i < 10; i++) {
        l.add(Integer.of(4));
        l.add(Integer.of(5));
        l.add(Integer.of(6));
    }
    for (Integer i : l)
    {
        if (i.intValue() == 5) {
            itemsToRemove.add(i);
        }
    }

    l.removeAll(itemsToRemove);
    System.out.println(l);
}

7
ini adalah apa yang biasanya saya lakukan, tetapi iterator eksplisit adalah solusi yang lebih elgant yang saya rasakan.
Claudiu

1
Cukup adil, selama Anda tidak melakukan hal lain dengan iterator - setelah diekspos membuatnya lebih mudah untuk melakukan hal-hal seperti panggilan .next () dua kali per loop dll. Bukan masalah besar, tetapi dapat menyebabkan masalah jika Anda melakukan sesuatu yang lebih rumit dari sekadar menelusuri daftar untuk menghapus entri.
RodeoClown

@RodeoClown: dalam pertanyaan awal, Claudiu menghapus dari Koleksi, bukan iterator.
matt b

1
Menghapus dari iterator menghapus dari koleksi yang mendasarinya ... tapi apa yang saya katakan di komentar terakhir adalah bahwa jika Anda melakukan sesuatu yang lebih rumit daripada hanya mencari penghapusan dalam loop (seperti memproses data yang benar) menggunakan iterator dapat membuat beberapa kesalahan lebih mudah dibuat.
RodeoClown

Jika itu adalah nilai hapus sederhana yang tidak diperlukan dan loop hanya melakukan satu hal, menggunakan iterator secara langsung dan memanggil .remove () benar-benar baik-baik saja.
RodeoClown

17

Dalam kasus seperti itu, trik umum adalah (dulu?) Untuk mundur:

for(int i = l.size() - 1; i >= 0; i --) {
  if (l.get(i) == 5) {
    l.remove(i);
  }
}

Yang mengatakan, saya sangat senang bahwa Anda memiliki cara yang lebih baik di Java 8, misalnya removeIfatau filterdi stream.


2
Ini trik yang bagus. Tapi itu tidak akan berfungsi pada koleksi yang tidak diindeks seperti set, dan itu akan sangat lambat pada daftar yang ditautkan.
Claudiu

@Claudiu Ya, ini pasti hanya untuk ArrayListkoleksi s atau sejenisnya.
Landei

Saya menggunakan ArrayList, ini berfungsi dengan baik, terima kasih.
StarSweeper

2
indeksnya bagus. Jika itu sangat umum mengapa Anda tidak menggunakannya for(int i = l.size(); i-->0;) {?
John

16

Jawaban yang sama dengan Claudius dengan for for:

for (Iterator<Object> it = objects.iterator(); it.hasNext();) {
    Object object = it.next();
    if (test) {
        it.remove();
    }
}

12

Dengan Eclipse Collections , metode yang removeIfdidefinisikan pada MutableCollection akan berfungsi:

MutableList<Integer> list = Lists.mutable.of(1, 2, 3, 4, 5);
list.removeIf(Predicates.lessThan(3));
Assert.assertEquals(Lists.mutable.of(3, 4, 5), list);

Dengan sintaks Java 8 Lambda ini dapat ditulis sebagai berikut:

MutableList<Integer> list = Lists.mutable.of(1, 2, 3, 4, 5);
list.removeIf(Predicates.cast(integer -> integer < 3));
Assert.assertEquals(Lists.mutable.of(3, 4, 5), list);

Panggilan ke Predicates.cast()diperlukan di sini karena removeIfmetode default telah ditambahkan pada java.util.Collectionantarmuka di Java 8.

Catatan: Saya pengendara untuk Eclipse Collections .


10

Buat salinan dari daftar yang ada dan ulangi salinan baru.

for (String str : new ArrayList<String>(listOfStr))     
{
    listOfStr.remove(/* object reference or index */);
}

19
Membuat salinan terdengar seperti pemborosan sumber daya.
Antzi

3
@ Antzi Itu tergantung pada ukuran daftar dan kepadatan objek di dalamnya. Masih solusi yang berharga dan valid.
mre

Saya telah menggunakan metode ini. Dibutuhkan lebih banyak sumber daya, tetapi jauh lebih fleksibel dan jelas.
Tao Zhang

Ini adalah solusi yang baik ketika Anda tidak bermaksud untuk menghapus objek di dalam loop itu sendiri, tetapi mereka agak "secara acak" dihapus dari utas lainnya (misalnya operasi jaringan memperbarui data). Jika Anda mendapati diri Anda banyak melakukan salinan ini, bahkan ada implementasi java yang melakukan hal ini: docs.oracle.com/javase/8/docs/api/java/util/concurrent/…
A1m

8

Orang-orang menyatakan seseorang tidak dapat menghapus dari Koleksi yang diulangi oleh loop foreach. Saya hanya ingin menunjukkan bahwa secara teknis tidak benar dan menjelaskan dengan tepat (saya tahu pertanyaan OP sangat canggih untuk menghindari mengetahui ini) kode di belakang asumsi itu:

for (TouchableObj obj : untouchedSet) {  // <--- This is where ConcurrentModificationException strikes
    if (obj.isTouched()) {
        untouchedSet.remove(obj);
        touchedSt.add(obj);
        break;  // this is key to avoiding returning to the foreach
    }
}

Ini bukan berarti Anda tidak dapat menghapus dari iterasi Colletiondaripada Anda tidak dapat melanjutkan iterasi setelah melakukannya. Karenanya breakdalam kode di atas.

Mohon maaf jika jawaban ini agak khusus untuk kasus penggunaan dan lebih cocok untuk utas asli tempat saya tiba di sini, yang ditandai sebagai duplikat (meskipun utas ini tampak lebih bernuansa) dan dikunci.


8

Dengan loop tradisional

ArrayList<String> myArray = new ArrayList<>();

for (int i = 0; i < myArray.size(); ) {
    String text = myArray.get(i);
    if (someCondition(text))
        myArray.remove(i);
    else
        i++;   
}

Ah, jadi ini benar-benar peningkatan -untuk-loop yang melempar Pengecualian.
cellepo

FWIW - kode yang sama akan tetap berfungsi setelah dimodifikasi menjadi penambahan i++di loop guard daripada di dalam loop body.
cellepo

Koreksi ^: Itu adalah jika i++penambahan itu tidak bersyarat - saya mengerti sekarang itu sebabnya Anda melakukannya di dalam tubuh :)
cellepo

2

A ListIteratormemungkinkan Anda untuk menambah atau menghapus item dalam daftar. Misalkan Anda memiliki daftar Carobjek:

List<Car> cars = ArrayList<>();
// add cars here...

for (ListIterator<Car> carIterator = cars.listIterator();  carIterator.hasNext(); )
{
   if (<some-condition>)
   { 
      carIterator().remove()
   }
   else if (<some-other-condition>)
   { 
      carIterator().add(aNewCar);
   }
}

Metode tambahan di antarmuka ListIterator (ekstensi Iterator) menarik - khususnya previousmetodenya.
cellepo

1

Saya punya saran untuk masalah di atas. Tidak perlu daftar sekunder atau waktu tambahan. Silakan temukan contoh yang akan melakukan hal yang sama tetapi dengan cara yang berbeda.

//"list" is ArrayList<Object>
//"state" is some boolean variable, which when set to true, Object will be removed from the list
int index = 0;
while(index < list.size()) {
    Object r = list.get(index);
    if( state ) {
        list.remove(index);
        index = 0;
        continue;
    }
    index += 1;
}

Ini akan menghindari Pengecualian Konkurensi.


1
Pertanyaannya secara eksplisit menyatakan, bahwa OP tidak perlu menggunakan ArrayListdan dengan demikian tidak dapat diandalkan get(). Kalau tidak, mungkin pendekatan yang baik.
kaskelotti

(Klarifikasi ^) OP menggunakan arbitrary Collection- Collectionantarmuka tidak termasuk get. (Meskipun Listantarmuka FWIW tidak termasuk 'dapatkan').
cellepo

Saya baru saja menambahkan Jawaban terpisah, lebih rinci di sini juga untuk while-looping a List. Tapi +1 untuk Jawaban ini karena itu yang lebih dulu.
cellepo

1

ConcurrentHashMap atau ConcurrentLinkedQueue atau ConcurrentSkipListMap dapat menjadi pilihan lain, karena mereka tidak akan pernah membuang ConcurrentModificationException, bahkan jika Anda menghapus atau menambahkan item.


Yap, dan perhatikan bahwa semuanya ada dalam java.util.concurrentpaket. Beberapa kelas kasus serupa / penggunaan umum lainnya dari paket itu adalah CopyOnWriteArrayList & CopyOnWriteArraySet [tetapi tidak terbatas pada itu].
cellepo

Sebenarnya, saya baru belajar bahwa meskipun objek-objek struktur data menghindari ConcurrentModificationException , menggunakannya dalam loop -for- ditingkatkan masih dapat menyebabkan masalah pengindeksan (yaitu: masih melewatkan elemen, atau IndexOutOfBoundsException...)
cellepo

1

Saya tahu pertanyaan ini terlalu tua untuk tentang Java 8, tetapi bagi mereka yang menggunakan Java 8 Anda dapat dengan mudah menggunakan removeIf ():

Collection<Integer> l = new ArrayList<Integer>();

for (int i=0; i < 10; ++i) {
    l.add(new Integer(4));
    l.add(new Integer(5));
    l.add(new Integer(6));
}

l.removeIf(i -> i.intValue() == 5);

1

Cara lain adalah dengan membuat salinan arrayList Anda:

List<Object> l = ...

List<Object> iterationList = ImmutableList.copyOf(l);

for (Object i : iterationList) {
    if (condition(i)) {
        l.remove(i);
    }
}

Catatan: ibukan objek indexmelainkan objek. Mungkin memanggilnya objakan lebih pas.
luckydonald

1

Cara terbaik (Disarankan) adalah menggunakan paket java.util.Concurrent. Dengan menggunakan paket ini Anda dapat dengan mudah menghindari Pengecualian ini. lihat Kode yang Dimodifikasi

public static void main(String[] args) {
    Collection<Integer> l = new CopyOnWriteArrayList<Integer>();

    for (int i=0; i < 10; ++i) {
        l.add(new Integer(4));
        l.add(new Integer(5));
        l.add(new Integer(6));
    }

    for (Integer i : l) {
        if (i.intValue() == 5) {
            l.remove(i);
        }
    }

    System.out.println(l);
}

0

Dalam hal ArrayList: hapus (indeks int) - jika (indeks adalah posisi elemen terakhir) ia menghindari tanpa System.arraycopy()dan tidak membutuhkan waktu untuk ini.

waktu arraycopy meningkat jika (indeks berkurang), dengan cara elemen-elemen daftar juga berkurang!

cara menghapus efektif terbaik adalah- menghapus elemen-elemennya dalam urutan menurun: while(list.size()>0)list.remove(list.size()-1);// take O (1) while(list.size()>0)list.remove(0);// take O (factorial (n))

//region prepare data
ArrayList<Integer> ints = new ArrayList<Integer>();
ArrayList<Integer> toRemove = new ArrayList<Integer>();
Random rdm = new Random();
long millis;
for (int i = 0; i < 100000; i++) {
    Integer integer = rdm.nextInt();
    ints.add(integer);
}
ArrayList<Integer> intsForIndex = new ArrayList<Integer>(ints);
ArrayList<Integer> intsDescIndex = new ArrayList<Integer>(ints);
ArrayList<Integer> intsIterator = new ArrayList<Integer>(ints);
//endregion

// region for index
millis = System.currentTimeMillis();
for (int i = 0; i < intsForIndex.size(); i++) 
   if (intsForIndex.get(i) % 2 == 0) intsForIndex.remove(i--);
System.out.println(System.currentTimeMillis() - millis);
// endregion

// region for index desc
millis = System.currentTimeMillis();
for (int i = intsDescIndex.size() - 1; i >= 0; i--) 
   if (intsDescIndex.get(i) % 2 == 0) intsDescIndex.remove(i);
System.out.println(System.currentTimeMillis() - millis);
//endregion

// region iterator
millis = System.currentTimeMillis();
for (Iterator<Integer> iterator = intsIterator.iterator(); iterator.hasNext(); )
    if (iterator.next() % 2 == 0) iterator.remove();
System.out.println(System.currentTimeMillis() - millis);
//endregion
  • untuk loop indeks: 1090 msec
  • untuk indeks desc: 519 msec --- yang terbaik
  • untuk iterator: 1043 msec

0
for (Integer i : l)
{
    if (i.intValue() == 5){
            itemsToRemove.add(i);
            break;
    }
}

Tangkapannya adalah setelah menghapus elemen dari daftar jika Anda melewatkan panggilan iterator.next internal (). masih bekerja! Meskipun saya tidak mengusulkan untuk menulis kode seperti ini, ada baiknya memahami konsep di baliknya :-)

Bersulang!


0

Contoh modifikasi pengumpulan aman thread:

public class Example {
    private final List<String> queue = Collections.synchronizedList(new ArrayList<String>());

    public void removeFromQueue() {
        synchronized (queue) {
            Iterator<String> iterator = queue.iterator();
            String string = iterator.next();
            if (string.isEmpty()) {
                iterator.remove();
            }
        }
    }
}

0

Saya tahu pertanyaan ini mengasumsikan hanya Collection, dan tidak lebih spesifik List. Tetapi bagi mereka yang membaca pertanyaan ini yang memang bekerja dengan Listreferensi, Anda dapat menghindari ConcurrentModificationExceptiondengan while-loop (sambil memodifikasi di dalamnya) sebagai gantinya jika Anda ingin menghindariIterator (baik jika Anda ingin menghindarinya secara umum, atau menghindarinya secara khusus untuk mencapai urutan pengulangan berbeda dari berhenti dari awal hingga akhir di setiap elemen [yang saya percaya adalah satu-satunya urutan yang Iteratordapat dilakukan sendiri)):

* Pembaruan: Lihat komentar di bawah ini yang memperjelas analog juga dapat dicapai dengan loop tradisional .

final List<Integer> list = new ArrayList<>();
for(int i = 0; i < 10; ++i){
    list.add(i);
}

int i = 1;
while(i < list.size()){
    if(list.get(i) % 2 == 0){
        list.remove(i++);

    } else {
        i += 2;
    }
}

Tidak ada ConcurrentModificationException dari kode itu.

Di sana kita melihat perulangan tidak dimulai di awal, dan tidak berhenti di setiap elemen (yang saya percaya Iteratortidak bisa lakukan).

FWIW kita juga melihat getdipanggil list, yang tidak dapat dilakukan jika referensi itu hanya Collection(bukan Listtipe -lebih spesifik dari Collection) - Listantarmuka termasuk get, tetapi Collectionantarmuka tidak. Jika bukan karena perbedaan itu, maka listrujukannya bisa menjadi Collection[dan karena itu secara teknis Jawaban ini kemudian akan menjadi Jawaban langsung, bukan Jawaban tangensial].

FWIWW kode yang sama masih berfungsi setelah dimodifikasi untuk mulai dari awal di berhenti di setiap elemen (seperti Iteratorpesanan):

final List<Integer> list = new ArrayList<>();
for(int i = 0; i < 10; ++i){
    list.add(i);
}

int i = 0;
while(i < list.size()){
    if(list.get(i) % 2 == 0){
        list.remove(i);

    } else {
        ++i;
    }
}

Namun, ini masih membutuhkan perhitungan indeks yang sangat hati-hati untuk dihapus.
OneCricketeer

Juga, ini hanya penjelasan yang lebih terinci tentang jawaban ini stackoverflow.com/a/43441822/2308683
OneCricketeer

Senang tahu - terima kasih! Jawaban yang lain membantu saya memahami bahwa itu adalah peningkatan -untuk-loop yang akan melempar ConcurrentModificationException, tetapi bukan - untuk-loop tradisional (yang menggunakan Jawaban lain) - tidak menyadari bahwa sebelumnya adalah mengapa saya termotivasi untuk menulis Jawaban ini (saya keliru berpikir kemudian bahwa itu semua untuk loop yang akan membuang Pengecualian).
cellepo

0

Salah satu solusinya adalah dengan memutar daftar dan menghapus elemen pertama untuk menghindari ConcurrentModificationException atau IndexOutOfBoundsException

int n = list.size();
for(int j=0;j<n;j++){
    //you can also put a condition before remove
    list.remove(0);
    Collections.rotate(list, 1);
}
Collections.rotate(list, -1);

0

Coba yang ini (hapus semua elemen dalam daftar yang sama i):

for (Object i : l) {
    if (condition(i)) {
        l = (l.stream().filter((a) -> a != i)).collect(Collectors.toList());
    }
}

0

Anda juga dapat menggunakan Rekursi

Rekursi dalam java adalah suatu proses di mana suatu metode menyebut dirinya secara terus menerus. Metode dalam java yang menyebut dirinya disebut metode rekursif.


-2

ini mungkin bukan cara terbaik, tetapi untuk sebagian besar kasus kecil ini harus dapat diterima:

"buat array kosong kedua dan tambahkan hanya yang ingin Anda simpan"

Saya tidak ingat di mana saya membaca ini dari ... untuk keadilan saya akan membuat wiki ini dengan harapan seseorang menemukannya atau hanya untuk tidak mendapatkan perwakilan saya tidak pantas.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.