Bir koleksiyondaki yineleme, döngüdeki nesneleri kaldırırken ConcurrentModificationException öğesinden kaçınma


1194

Hepimiz aşağıdakiler nedeniyle aşağıdakileri yapamayacağınızı biliyoruz ConcurrentModificationException:

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

Ama bu görünüşe göre bazen çalışıyor, ama her zaman değil. İşte bazı özel kodlar:

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);
}

Bu, elbette, aşağıdakilerle sonuçlanır:

Exception in thread "main" java.util.ConcurrentModificationException

Birden fazla iş parçacığı bunu yapmasa da. Neyse.

Bu sorunun en iyi çözümü nedir? Bu istisnayı atmadan bir öğeyi koleksiyondan bir döngüde nasıl kaldırabilirim?

Ben de Collectionburada keyfi olarak değil, keyfi bir kullanıyorum ArrayList, bu yüzden güvenemezsin get.


Okuyucular için not: docs.oracle.com/javase/tutorial/collections/interfaces/… 'ı okuyun, yapmak istediğiniz şeyi elde etmenin daha kolay bir yolu olabilir.
GKFX

Yanıtlar:


1601

Iterator.remove() güvenlidir, şöyle kullanabilirsiniz:

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();
    }
}

Iterator.remove()Bir koleksiyonu yineleme sırasında değiştirmenin tek güvenli yolunun bu olduğuna dikkat edin; yineleme devam ederken temel alınan koleksiyon başka bir şekilde değiştirilirse davranış belirtilmez .

Kaynak: docs.oracle> Koleksiyon Arayüzü


Ve benzer şekilde, bir öğeniz varsa ListIteratorve öğe eklemek istiyorsanız, kullanabilirsiniz ListIterator#add, aynı nedenle kullanabilirsiniz Iterator#remove - buna izin vermek için tasarlanmıştır.


Sizin durumunuzda bir listeden kaldırmayı denediniz, ancak içeriğini kısıtlarken putbir Mapsüre geçmeye çalışıyorsanız aynı kısıtlama geçerlidir .


19
Geçerli yinelemede döndürülen öğeden başka bir öğeyi kaldırmak isterseniz ne olur?
Eugen

2
Yineleyicideki .remove kullanmanız gerekir ve bu sadece geçerli öğeyi kaldırabilir, bu yüzden hayır :)
Bill K

1
ConcurrentLinkedDeque veya CopyOnWriteArrayList (en azından benim durumumda) kullanmaya kıyasla daha yavaş olduğunu unutmayın
Dan

1
iterator.next()Çağrıyı for-loop'a koymak mümkün değil mi? Değilse, birisi nedenini açıklayabilir mi?
Blake

1
@GonenI Değişmeyen koleksiyonların tüm yineleyicileri için uygulanır. List.addaynı anlamda "isteğe bağlı" dir, ancak bir listeye eklemenin "güvensiz" olduğunu söylemezsiniz.
Radiodef

345

Bu çalışıyor:

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

Bir foreach döngüsünün yineleme için sözdizimsel şeker olduğu için bir yineleyici kullanmanın yardımcı olmayacağını varsaydım ... ama size bu .remove()işlevi veriyor .


43
foreach döngüsü olan iterating sözdizimsel şeker. Ancak, işaret ettiğiniz gibi, yineleyiciden kaldır çağrmanız gerekir - bu foreach size erişim vermez. Dolayısıyla (eğer olsa bile, bir foreach döngüsü kaldıramazsınız nedeni vardır aslında kaput altında bir yineleyici kullanarak)
madlep

36
+1, örneğin Bill K'nin cevabının [doğrudan] sahip olmadığı bağlamda iter.remove () işlevini kullanmak için kod.
Eddified

202

Java 8 ile yeni removeIfyöntemi kullanabilirsiniz . Örneğinize uygulanır:

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

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

3
Ooooo! Java 8 veya 9'da bir şeyin yardımcı olabileceğini umuyordum. Bu hala bana çok ayrıntılı geliyor, ama yine de hoşuma gidiyor.
James T Snell

Bu durumda da eşittir () uygulanması tavsiye edilir mi?
Anmol Gupta

Bu arada removeIfkullanır Iteratorve whiledöngü. Java 8'de görebilirsinizjava.util.Collection.java
omerhakanbilici

3
@omerhakanbilici Gibi bazı uygulamalar ArrayListperformans nedenleriyle geçersiz kılar. Bahsettiğiniz kişi yalnızca varsayılan uygulamadır.
Didier L

@AnmolGupta: Hayır, equalsburada hiç kullanılmıyor, bu yüzden uygulanması gerekmiyor. (Ama elbette, eğer equalstestinizde kullanırsanız , istediğiniz şekilde uygulanmalıdır.)
Lii

42

Soru zaten yanıtlandığından, yani en iyi yol yineleyici nesnesinin remove yöntemini kullanmak olduğundan, hatanın olduğu yerin özelliklerine girerim "java.util.ConcurrentModificationException" atıldığı girerim.

Her koleksiyon sınıfının, Iterator arabirimini uygulayan ve next(), remove()ve gibi yöntemler sağlayan özel bir sınıfı vardır hasNext().

Bir sonraki kod böyle bir şeye benziyor ...

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

Burada yöntem checkForComodificationşu şekilde uygulanır:

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

Gördüğünüz gibi, açıkça bir öğeyi koleksiyondan kaldırmaya çalışırsanız. Bu sonuçlanır modCountfarklı oluyor expectedModCountistisna sonuçlanan ConcurrentModificationException.


Çok ilginç. Teşekkür ederim! Sık sık kendimi remove () olarak adlandırmıyorum, bunun yerine koleksiyonu yineledikten sonra temizlemeyi tercih ediyorum. Bunun iyi bir desen olduğunu söylemek değil, sadece son zamanlarda yaptığım şey.
James T Snell

26

Yineleyiciyi doğrudan belirttiğiniz gibi kullanabilirsiniz veya ikinci bir koleksiyon saklayabilir ve kaldırmak istediğiniz her öğeyi yeni koleksiyona ekleyebilir, ardından sonunda All'u kaldırabilirsiniz. Bu, her bir döngü için tür güvenliğini artırılmış bellek kullanımı ve işlemci zamanı pahasına kullanmaya devam etmenizi sağlar (gerçekten büyük listeleriniz veya gerçekten eski bir bilgisayarınız yoksa büyük bir sorun olmamalıdır)

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
Bu normalde yaptığım şey, ama açık yineleyici hissediyorum daha elgant bir çözümdür.
Claudiu

1
Yineleyici ile başka bir şey yapmadığınız sürece yeterince açıktır - maruz kalması döngü başına iki kez .next () gibi şeyler yapmayı kolaylaştırır. Büyük bir sorun değil, ancak sorun yaşıyorsanız girişleri silmek için sadece bir listeden geçmekten daha karmaşık bir şey.
RodeoClown

@RodeoClown: orijinal soruda Claudiu, yineleyiciden değil, Koleksiyondan kaldırılıyor.
matt b

1
Yineleyiciden kaldırma, alttaki koleksiyondan kaldırılır ... ancak son yorumda söylediğim şey, yineleyiciyi kullanarak döngüdeki silmeleri (doğru verileri işlemek gibi) aramaktan daha karmaşık bir şey yapıyorsanız, hata yapmak daha kolay.
RodeoClown

Basit bir silme değeri yoksa ve döngü sadece bir şeyi yapıyorsa, yineleyiciyi doğrudan kullanmak ve .remove () öğesini çağırmak kesinlikle iyidir.
RodeoClown

17

Bu gibi durumlarda ortak bir hile geriye gitmek için: (oldu?):

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

Bununla birlikte, Java 8'de removeIfveya filterakışlarda daha iyi yollara sahip olduğunuz için çok mutluyum .


2
Bu iyi bir numara. Ancak kümeler gibi dizine eklenmemiş koleksiyonlarda işe yaramaz ve bağlantılı listeler söz konusu olduğunda gerçekten yavaş olur.
Claudiu

@Claudiu Evet, bu kesinlikle sadece ArrayListbenzer koleksiyonlar içindir.
Landei

Bir ArrayList kullanıyorum, bu mükemmel çalıştı, teşekkürler.
StarSweeper

2
dizinler harika. Çok yaygınsa neden kullanmıyorsunuz for(int i = l.size(); i-->0;) {?
John

16

For döngüsü ile Claudius ile aynı cevap :

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

12

İle Eclipse Koleksiyonları , yöntem removeIfüzerinde tanımlı MutableCollection çalışacaktır:

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);

Java 8 Lambda sözdizimi ile bu şöyle yazılabilir:

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);

Java 8 arabiriminde Predicates.cast()varsayılan bir removeIfyöntem eklendiğinden burada çağrı gereklidir java.util.Collection.

Not: Eclipse Collections için bir komisyoncuyum .


10

Mevcut listenin bir kopyasını oluşturun ve yeni kopya üzerinde tekrarlayın.

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

19
Kopya yapmak kaynak israfı gibi geliyor.
Antzi

3
@Antzi Bu, listenin boyutuna ve içindeki nesnelerin yoğunluğuna bağlıdır. Yine de değerli ve geçerli bir çözüm.
mre

Bu yöntemi kullanıyorum. Biraz daha kaynak gerektirir, ancak çok daha esnek ve açıktır.
Tao Zhang

Döngünün içindeki nesneleri kaldırmak istemediğinizde, ancak diğer iş parçacıklarından (örneğin verileri güncelleştiren ağ işlemleri) "rastgele" kaldırıldıklarında bu iyi bir çözümdür. Kendinizi bu kopyaları çok fazla bulursanız, tam olarak bunu yapan bir java uygulaması bile vardır: docs.oracle.com/javase/8/docs/api/java/util/concurrent/…
A1m

8

İnsanlar bir iddia edilmektedir edemez bir Koleksiyonu varlık bir foreach döngüsü tarafından iterated kaldırmak. Ben sadece teknik olarak yanlış işaret ve tam olarak açıklamak istedim (OP'nin sorununun bunu bilmek için çok gelişmiş olduğunu biliyorum) bu varsayımın arkasındaki kodu:

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
    }
}

Bu iterated dan kaldırmak olamaz değil Colletionbunu bir kez daha sonra yinelemeyi devam edemez ziyade söyledi. Dolayısıylabreak yukarıdaki kodda.

Bu cevap biraz uzman bir kullanım durumu ve buraya geldiğim orijinal konuya daha uygunsa özür dilerim , bunun bir kopyası olarak işaretlenir (bu konu daha nüanslı görünmesine rağmen) ve kilitlenir.


8

Geleneksel bir döngü için

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, bu gerçekten sadece İstisna atan gelişmiş döngü için.
cellepo

FWIW - Aynı kod, i++döngü gövdesi yerine döngü koruyucusunda artırılmak üzere değiştirildikten sonra da çalışmaya devam eder.
cellepo

Düzeltme ^: i++Artan koşullu olmasaydı - şimdi görüyorum, bu yüzden vücutta
yapıyorsun

2

A ListIterator, listeye öğe eklemenizi veya listeden öğe kaldırmanızı sağlar. Bir Carnesne listeniz olduğunu varsayalım :

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);
   }
}

ListIterator arabirimindeki ek yöntemler (Iterator'ın uzantısı) ilginçtir - özellikle previousyöntemi.
cellepo

1

Yukarıdaki sorun için bir önerim var. İkincil listeye veya ekstra zamana gerek yok. Lütfen aynı şeyleri ancak farklı bir şekilde yapacak bir örnek bulun.

//"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;
}

Bu, Eşzamanlılık İstisnası'ndan kaçınır.


1
Soru açıkça OP'nin kullanılması gerekmediğini ArrayListve bu nedenle güvenilemeyeceğini açıkça belirtmektedir get(). Aksi halde muhtemelen iyi bir yaklaşım.
kaskelotti

(Açıklama ^) OP keyfi kullanıyor Collection- Collectionarayüz içermiyor get. (FWIW Listarayüzü 'get' içermesine rağmen ).
cellepo

Buraya a- whiledöngüsü için de ayrı, daha ayrıntılı bir cevap ekledim List. Ama bu cevap için +1, çünkü ilk geldi.
cellepo

1

ConcurrentHashMap veya ConcurrentLinkedQueue veya ConcurrentSkipListMap başka bir seçenek olabilir, çünkü öğeyi kaldırsanız veya ekleseniz bile hiçbir ConcurrentModificationException oluşturmazlar.


Evet ve hepsinin java.util.concurrentpakette olduğunu unutmayın . Bu paketteki diğer bazı benzer / yaygın kullanım örneği sınıfları CopyOnWriteArrayList & CopyOnWriteArraySet [ancak bunlarla sınırlı değildir].
cellepo

Aslında, bu veri yapısı Nesnelerin kaçınmasına rağmen ConcurrentModificationException, bunları gelişmiş bir döngü için kullanmanın yine de dizin oluşturma sorunlarına neden olabileceğini öğrendim (yani: hala öğeleri atlıyor veya IndexOutOfBoundsException...)
cellepo

1

Bu sorunun Java 8 ile ilgili olamayacak kadar eski olduğunu biliyorum, ancak Java 8 kullananlar için kolayca removeIf () kullanabilirsiniz:

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

Başka bir yol, arrayList'inizin bir kopyasını oluşturmaktır:

List<Object> l = ...

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

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

Not: ibir indexdeğil, nesnedir. Belki onu objaramak daha uygun olur.
luckydonald

1

En iyi yol (Önerilen) java.util.Concurrent paketini kullanmaktır. Bu paketi kullanarak bu İstisnadan kolayca kaçınabilirsiniz. Değiştirilmiş Kod'a bakın

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

ArrayList durumunda : remove (int index) - (index son elemanın konumuysa) onsuz kaçınır System.arraycopy()ve bunun için zaman almaz.

dizilim süresi (indeks azalırsa), liste elemanları da azalır!

en etkili kaldırma yolu, elemanlarını azalan sırada kaldırmaktır: while(list.size()>0)list.remove(list.size()-1);// O (1) while(list.size()>0)list.remove(0);alır // O (faktöriyel (n)) alır

//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
  • dizin döngüsü için: 1090 msn
  • desc indeksi için: 519 msn - en iyisi
  • yineleyici için: 1043 ms

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

Yakalama, dahili iterator.next () çağrısını atlarsanız öğeyi listeden kaldırdıktan sonra olur. hala çalışıyor! Böyle bir kod yazmayı önermememe rağmen arkasındaki kavramı anlamaya yardımcı olur :-)

Şerefe!


0

İş parçacığı güvenli toplama değişikliği örneği:

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

Bu sorunun sadece a olduğunu Collectionve daha spesifik olmadığını varsayıyorum List. Ama gerçekten çalışıyorsanız bu soruyu okuyanlar için Listreferans, sen önleyebilirsiniz ConcurrentModificationExceptionbir ile while(içindeki değiştirirken) kaçınmak istediğiniz yerine eğer -loopIterator genel olarak bunu önlemek veya elde etmek için özel olarak bunu önlemek isteyen ya if ( her öğede baştan sona durdurma farklı bir döngü sırası [ki tek sipariş Iteratoryapabilirsiniz inanıyorum ]):

* Güncelleme: Geleneksel -for-loop ile benzerliğin de elde edilebilir olduğunu açıklayan aşağıdaki yorumlara bakın .

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;
    }
}

Bu kodda ConcurrentModificationException yok.

Orada döngü başlangıcında başlamıyor ve her öğede durmuyor (ki buna inanıyorum)Iterator kendisinin yapamayacağına ).

FWIW da getçağrıldığını görüyoruz list, eğer referansı sadece Collection(daha spesifik- Listtürü yerine Collection) - Listarayüz içeriyorsa get, ancak Collectionarayüz içermiyorsa yapılamadı . Eğer bu fark için değilse, listreferans bunun yerine bir Collection[olabilir ve bu nedenle teknik olarak bu cevap daha sonra teğetsel bir cevap yerine doğrudan bir cevap olacaktır].

FWIWW aynı kod, her öğede başlangıçta başlayacak şekilde değiştirildikten sonra da çalışır (tıpkı Iteratorsipariş gibi ):

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;
    }
}

Bununla birlikte, yine de çıkarılması gereken göstergelerin çok dikkatli bir şekilde hesaplanması gerekir.
OneCricketeer

Ayrıca, bu cevap daha ayrıntılı bir açıklaması stackoverflow.com/a/43441822/2308683
OneCricketeer

Bilmek güzel - teşekkürler! Öbür Cevap beni o oluyor anlamak yardımcı gelişmiş için--döngü atmak istiyorum ConcurrentModificationException, ama değil geleneksel (diğer Cevap kullanımlar) yerleştirebilir döngü - Bunu daha önce fark etmeden ben yanlışlıkla (bu cevap yazmak için motive neden olduğu bu olduğunu sonra düşündüm bütün ) İstisna demek olur-döngüler.
cellepo

0

Bir çözüm, ConcurrentModificationException veya IndexOutOfBoundsException öğesinden kaçınmak için listeyi döndürmek ve ilk öğeyi kaldırmak olabilir

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

Bunu deneyin (listedeki eşit olan tüm öğeleri kaldırır i):

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

0

Özyinelemeyi de kullanabilirsiniz

Java'da özyineleme, bir yöntemin kendini sürekli çağırdığı bir süreçtir. Java'da kendisini çağıran bir yönteme özyinelemeli yöntem denir.


-2

bu en iyi yol olmayabilir, ancak küçük vakaların çoğu için bu kabul edilebilir olmalıdır:

"ikinci bir boş dizi oluşturun ve yalnızca tutmak istediğiniz dizileri ekleyin"

Bunu nereden okuduğumu hatırlamıyorum ... adalet için bu wiki'yi birisinin bulduğu ya da sadece hak etmediğim temsilcisi kazanamayacağım.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.