Hangisi daha verimli, her biri için bir döngü veya bir yineleyici?


208

Bir koleksiyondan geçmenin en etkili yolu hangisidir?

List<Integer>  a = new ArrayList<Integer>();
for (Integer integer : a) {
  integer.toString();
}

veya

List<Integer>  a = new ArrayList<Integer>();
for (Iterator iterator = a.iterator(); iterator.hasNext();) {
   Integer integer = (Integer) iterator.next();
   integer.toString();
}

Son sorunun yanıtlarından biri yaklaşsa da , bunun bunun , şu , bu veya bunun tam bir kopyası olmadığını lütfen unutmayın . Bunun bir dupe olmamasının nedeni, bunların çoğunun get(i)yineleyiciyi kullanmak yerine döngü içinde çağırdığınız döngüleri karşılaştırmasıdır .

Üzerinde önerildiği gibi Meta bu soruya cevabım gönderme olacak.


Java ve templating mekanizması sözdizimsel şekerden biraz daha fazla olduğu için bir fark yaratmadığını düşünürdüm
Hassan Syed


2
@OMG Ponies: Bunun bir yineleme olduğuna inanmıyorum, çünkü bu döngü döngüyü yineleyici ile karşılaştırmaz, aksine koleksiyonların neden yineleyicileri doğrudan sınıfın kendisinde yapmak yerine yineleyicileri döndürdüğünü sorar.
Paul Wagland

Yanıtlar:


266

Tüm değerleri okumak için koleksiyon üzerinde dolaşıyorsanız, yeni sözdizimi sadece sualtı yineleyiciyi kullandığından, yineleyici veya döngü sözdizimi için yeni kullanmak arasında bir fark yoktur.

Ancak, eski "c-style" döngüsünü kastediyorsanız:

for(int i=0; i<list.size(); i++) {
   Object o = list.get(i);
}

Daha sonra, döngü veya yineleyici için yeni olan, temeldeki veri yapısına bağlı olarak çok daha verimli olabilir. Bunun sebebi, veri yapıları için, yani get(i)döngü, bir O (n yapan bir O (n) işlemi, bir 2 ) işlemi. Geleneksel bir bağlantılı liste böyle bir veri yapısına bir örnektir. Tüm yineleyicilerin, next()O (n) döngüsünü yapan bir O (1) işlemi olması gereken temel bir gereksinimi vardır .

Yineleyicinin yeni döngü sözdizimi tarafından su altında kullanıldığını doğrulamak için, aşağıdaki iki Java parçacığından oluşturulan bayt kodlarını karşılaştırın. İlk önce for döngüsü:

List<Integer>  a = new ArrayList<Integer>();
for (Integer integer : a)
{
  integer.toString();
}
// Byte code
 ALOAD 1
 INVOKEINTERFACE java/util/List.iterator()Ljava/util/Iterator;
 ASTORE 3
 GOTO L2
L3
 ALOAD 3
 INVOKEINTERFACE java/util/Iterator.next()Ljava/lang/Object;
 CHECKCAST java/lang/Integer
 ASTORE 2 
 ALOAD 2
 INVOKEVIRTUAL java/lang/Integer.toString()Ljava/lang/String;
 POP
L2
 ALOAD 3
 INVOKEINTERFACE java/util/Iterator.hasNext()Z
 IFNE L3

İkincisi, yineleyici:

List<Integer>  a = new ArrayList<Integer>();
for (Iterator iterator = a.iterator(); iterator.hasNext();)
{
  Integer integer = (Integer) iterator.next();
  integer.toString();
}
// Bytecode:
 ALOAD 1
 INVOKEINTERFACE java/util/List.iterator()Ljava/util/Iterator;
 ASTORE 2
 GOTO L7
L8
 ALOAD 2
 INVOKEINTERFACE java/util/Iterator.next()Ljava/lang/Object;
 CHECKCAST java/lang/Integer
 ASTORE 3
 ALOAD 3
 INVOKEVIRTUAL java/lang/Integer.toString()Ljava/lang/String;
 POP
L7
 ALOAD 2
 INVOKEINTERFACE java/util/Iterator.hasNext()Z
 IFNE L8

Gördüğünüz gibi, oluşturulan bayt kodu etkili bir şekilde aynıdır, bu nedenle her iki formu da kullanmak için performans cezası yoktur. Bu nedenle, daha estetik olarak çekici olan döngü biçimini seçmelisiniz, çünkü her döngü için olacak çoğu insan için, daha az kazan plakası kodu vardır.


4
Tam tersini söylediğine inanıyorum, foo.get (i) çok daha az verimli olabilir. LinkedList'i düşünün. LinkedList'in ortasında bir foo.get (i) yaparsanız, i'ye ulaşmak için önceki tüm düğümleri geçmesi gerekir. Öte yandan bir yineleyici, temeldeki veri yapısını tutacak ve düğümler üzerinde birer birer dolaşmanıza izin verecektir.
Michael Krauklis

1
Büyük bir şey değil, bir for(int i; i < list.size(); i++) {stil döngüsü de list.size()her yinelemenin sonunda değerlendirilmelidir , eğer kullanılırsa, list.size()ilkinin sonucunu önbelleğe almak bazen daha etkilidir .
Brett Ryan

3
Aslında, orijinal ifade ArrayList ve RandomAccess arayüzünü uygulayan diğer tüm durumlar için de geçerlidir. "C stili" döngü, Yineleyici tabanlı olandan daha hızlıdır. docs.oracle.com/javase/7/docs/api/java/util/RandomAccess.html
andresp

4
İster foreach ister desugar'd versiyonu olsun, Iterator yaklaşımı yerine eski C-tarzı döngüyü kullanmanın bir nedeni çöptür. Birçok veri yapısı .iterator () çağrıldığında yeni bir Yineleyici başlatır, ancak C stili döngü kullanılarak bunlara ayırmadan erişilebilir. Bu, (a) ayırıcıya veya (b) çöp koleksiyonlarına çarpmamaya çalışılan bazı yüksek performanslı ortamlarda önemli olabilir.
Dan

3
Başka bir yorumda olduğu gibi, ArrayLists için for (int i = 0 ....) döngüsü, yineleyici veya for (:) yaklaşımını kullanmaktan yaklaşık 2 kat daha hızlıdır, bu yüzden gerçekten temel yapıya bağlıdır. Bir yan not olarak, HashSets'i yinelemek de çok pahalıdır (bir Dizi Listesinden çok daha fazlası), bu nedenle veba gibi olanlardan kaçının (mümkünse).
Leo

106

Fark performansta değil, yetenekte. Bir referansı doğrudan kullanırken, çoğu durumda aynı uygulamayı döndürseler de, açıkça bir yineleyici türü (örn. List.iterator () ve List.listIterator () ile karşılaştırıldığında) üzerinde daha fazla güce sahipsiniz. Ayrıca, döngünüzdeki Yineleyiciye başvurabilirsiniz. Bu, ConcurrentModificationException almadan koleksiyonunuzdan öğe kaldırma gibi şeyler yapmanızı sağlar.

Örneğin

Tamamdır:

Set<Object> set = new HashSet<Object>();
// add some items to the set

Iterator<Object> setIterator = set.iterator();
while(setIterator.hasNext()){
     Object o = setIterator.next();
     if(o meets some condition){
          setIterator.remove();
     }
}

Bu, eşzamanlı bir değişiklik istisnası atacağı için değildir:

Set<Object> set = new HashSet<Object>();
// add some items to the set

for(Object o : set){
     if(o meets some condition){
          set.remove(o);
     }
}

13
Bilgilendirici olduğu ve mantıklı takip sorusunu cevapladığım için +1 verdiğim soruyu doğrudan cevaplamasa da bu çok doğrudur.
Paul Wagland

1
Evet, toplama öğelerine foreach döngüsü ile erişebiliriz, ancak bunları kaldıramayız, ancak öğeleri Iterator ile kaldırabiliriz.
Akash5288

23

Paul'ün kendi cevabını genişletmek için, o belirli derleyicide (muhtemelen Sun'ın javac?) Bayt kodunun aynı olduğunu gösterdi, ancak farklı derleyicilerin aynı bayt kodunu üreteceği garanti edilmedi , değil mi? İkisi arasındaki gerçek farkın ne olduğunu görmek için, doğrudan kaynağa gidelim ve Java Dil Spesifikasyonunu, özellikle 14.14.2, "Geliştirilmiş ifade için" kontrol edelim :

Geliştirilmiş forifade, forformun temel bir ifadesine eşdeğerdir :

for (I #i = Expression.iterator(); #i.hasNext(); ) {
    VariableModifiers(opt) Type Identifier = #i.next();    
    Statement 
}

Başka bir deyişle, JLS tarafından ikisinin eşdeğer olması gerekmektedir. Teoride, bayt kodundaki marjinal farklılıklar anlamına gelebilir, ancak gerçekte döngü için gelişmiş olması gerekir:

  • .iterator()Yöntemi çağırın
  • kullanım .hasNext()
  • Yerel değişkeni, .next()

Diğer bir deyişle, tüm pratik amaçlar için bayt kodu aynı veya neredeyse aynı olacaktır. Bu ikisi arasında önemli bir fark yaratacak herhangi bir derleyici uygulamasını öngörmek zordur.


Aslında, yaptığım test Eclipse derleyicisindeydi, ancak genel noktanız hala geçerli. +1
Paul Wagland

3

foreachUnderhood yaratıyoriterator hasNext () 'u çağırarak ve değeri elde etmek için () aşağıdaki çağrı; Performansla ilgili sorun, yalnızca RandomomAccess'i uygulayan bir şey kullanıyorsanız gelir.

for (Iterator<CustomObj> iter = customList.iterator(); iter.hasNext()){
   CustomObj custObj = iter.next();
   ....
}

Yineleyici tabanlı döngü ile ilgili performans sorunları şu şekildedir:

  1. liste boş olsa bile bir nesne ayırma (Iterator<CustomObj> iter = customList.iterator(); );
  2. iter.hasNext() döngünün her yinelemesi sırasında bir invokeInterface sanal çağrısı vardır (tüm sınıfları gözden geçirin, ardından atlamadan önce yöntem tablosu araması yapın).
  3. yineleyici uygulaması yapmak için en az 2 alan araması yapmak zorundadır hasNext() çağrıyı değer zorundadır: # 1 geçerli sayımı al ve # 2 toplam sayımı al
  4. body loop içinde, başka bir invokeInterface sanal çağrısı var iter.next(yani: tüm sınıfları gözden geçirin ve atlamadan önce yöntem tablosu araması yapın) ve ayrıca alan araması yapmak zorunda: # 1 dizini alın ve # 2, (her yinelemede) ofseti yapmak için bir dizi.

Potansiyel bir optimizasyon,index iteration önbelleğe alınmış boyut aramasıyla bir a'ya geçmektir:

for(int x = 0, size = customList.size(); x < size; x++){
  CustomObj custObj = customList.get(x);
  ...
}

İşte burada:

  1. bir invokeInterface sanal yöntem çağrısı customList.size() için for döngüsünün ilk oluşturulmasında boyut
  2. customList.get(x)diziye bir alan araması olan ve daha sonra diziye ofset yapabilen döngü için gövde sırasında get yöntemi çağrısı

Bir ton yöntem çağrısını, alan aramalarını azalttık. Bu LinkedList, bir RandomAccesskoleksiyon objesi olmayan bir şeyle veya onunla yapmak istemezsiniz , aksi takdirde customList.get(x),LinkedList her yinelemede .

Bu herhangi bir RandomAccesstabanlı liste koleksiyonu olduğunu bildiğinizde mükemmeldir .


1

foreachyine de kaputun altında yineleyiciler kullanır. Gerçekten sadece sözdizimsel şekerdir.

Aşağıdaki programı düşünün:

import java.util.List;
import java.util.ArrayList;

public class Whatever {
    private final List<Integer> list = new ArrayList<>();
    public void main() {
        for(Integer i : list) {
        }
    }
}

Hadi derleyelim javac Whatever.java,
Ve demonte bayt kodunu main()kullanarak şunu okuyalım javap -c Whatever:

public void main();
  Code:
     0: aload_0
     1: getfield      #4                  // Field list:Ljava/util/List;
     4: invokeinterface #5,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
     9: astore_1
    10: aload_1
    11: invokeinterface #6,  1            // InterfaceMethod java/util/Iterator.hasNext:()Z
    16: ifeq          32
    19: aload_1
    20: invokeinterface #7,  1            // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
    25: checkcast     #8                  // class java/lang/Integer
    28: astore_2
    29: goto          10
    32: return

Görebiliyoruz foreach bir programda derlendiğini :

  • Kullanarak yineleyici oluşturur List.iterator()
  • Eğer Iterator.hasNext(): Iterator.next()döngüyü çağırır ve devam ettirir

"Neden bu işe yaramaz döngü derlenmiş koddan optimize edilmiyor? Liste öğesiyle hiçbir şey yapmadığını görebiliyoruz": Evet, tekrarlanabilir kodunuzu kodlamanız mümkün .iterator() yan etkilere sahip veya bunun .hasNext()yan etkileri veya anlamlı sonuçları vardır.

Bir veritabanından kaydırılabilir bir sorguyu temsil eden yinelenebilir bir öğenin, .hasNext() (veritabanıyla iletişim kurmak veya sonuç kümesinin sonuna ulaştığınız için bir imleci kapatmak gibi) .

Dolayısıyla, döngü gövdesinde hiçbir şeyin olmadığını kanıtlasak bile… yinelediğimizde anlamlı / sonuçsuz hiçbir şeyin olmadığını kanıtlamak daha pahalıdır (inatçı?). Derleyici bu boş döngü gövdesini programda bırakmalıdır.

Umabileceğimiz en iyi şey derleyici uyarısı olacaktır . Bu çok ilginç javac -Xlint:all Whatever.javageliyor değil bu boş döngü gövdesinin hakkında bizi uyarmak. IntelliJ IDEA bunu yapıyor. Kuşkusuz IntelliJ'i Eclipse Derleyici'yi kullanacak şekilde yapılandırdım, ancak bunun nedeni olmayabilir.

resim açıklamasını buraya girin


0

Yineleyici, Java Koleksiyonlar çerçevesindeki bir koleksiyon üzerinde geçiş veya yineleme için yöntemler sağlayan bir arabirimdir.

Nedeniniz yineleyici ve for döngüsü, öğelerinizi okumak için bir koleksiyonun üzerinden geçmek olduğunda benzer şekilde davranır.

for-each Koleksiyonu yinelemenin bir yoludur.

Örneğin:

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

//using for-each loop
for(String msg: messages){
    System.out.println(msg);
}

//using iterator 
Iterator<String> it = messages.iterator();
while(it.hasNext()){
    String msg = it.next();
    System.out.println(msg);
}

Ve for-each döngüsü sadece yineleyici arabirimini uygulayan nesnelerde kullanılabilir.

Şimdi for döngüsü ve yineleyici durumuna geri dönelim.

Fark, bir koleksiyonu değiştirmeye çalıştığınızda gelir. Bu durumda, yineleyici, arıza hızlı özelliği nedeniyle daha verimlidir . yani. bir sonraki eleman üzerinde tekrar etmeden önce altta yatan koleksiyonun yapısındaki herhangi bir değişikliği kontrol eder. Bulunan herhangi bir değişiklik varsa, ConcurrentModificationException özel durumunu atar .

(Not: Yineleyicinin bu işlevselliği yalnızca java.util paketindeki toplama sınıfları için geçerlidir. Eşzamanlı koleksiyonlar için doğası gereği arıza korumalı oldukları için geçerli değildir)


1
Fark hakkındaki ifadeniz doğru değil, her döngü için su altında bir yineleyici de kullanıyor ve bu nedenle aynı davranışa sahip.
Paul Wagland

@Pault Wagland, cevabımı değiştirdim hatayı işaret ettiğiniz için teşekkür ederim
eccentricCoder

güncellemeleriniz hala doğru değil. Sahip olduğunuz iki kod parçacığı, dil ile aynı olacak şekilde tanımlanır. Uygulamada bir hata olan davranışta herhangi bir fark varsa. Tek fark, yineleyiciye erişiminizin olup olmadığıdır.
Paul Wagland

@ Paul Wagland Yineleyici kullanan her döngü için varsayılan uygulamasını kullansanız bile, eşzamanlı işlemler sırasında remove () yöntemini kullanmaya çalıştığınızda yine de istisna oluşturur. Daha fazla bilgi için burayı inceleyin
eccentricCoder

1
her döngü için, yineleyiciye erişemezsiniz, bu nedenle kaldırmayı çağıramazsınız. Ancak bu, konunun yanında, cevabınızda birinin güvenli olduğunu, diğerinin güvenli olmadığını iddia ediyorsunuz. Dil spesifikasyonuna göre eşdeğerdirler, bu nedenle her ikisi de sadece temel koleksiyonlar kadar iş parçacığı kadar güvenlidir.
Paul Wagland

-8

Koleksiyonlarla çalışırken geleneksel for döngüsü kullanmaktan kaçınmalıyız. Ne vereceğim basit nedeni döngü için karmaşıklığı O (sqr (n)) sırası ve Iterator karmaşıklığı veya hatta gelişmiş için döngü sadece O (n) olmasıdır. Bu yüzden bir performans farkı verir .. Sadece 1000 öğenin bir listesini alın ve her iki yolu kullanarak yazdırın. ve ayrıca yürütme için zaman farkını yazdırın. Farkı görebilirsiniz.


lütfen beyanlarınızı desteklemek için bazı açıklayıcı örnekler ekleyin.
Rajesh Pitty

@Chandan Üzgünüm ama yazdıklarınız yanlış. Örneğin: std :: vector da bir koleksiyon ancak erişim maliyeti O (1). Yani bir vektör üzerinde döngü için geleneksel olan sadece O (n) 'dir. Eğer altlık kapsayıcı erişim O (n) erişim maliyeti varsa, bu yüzden O (n ^ 2) bir karmaşıklık daha std :: liste için olduğunu söylemek istiyorum düşünüyorum. Bu durumda yineleyicilerin kullanılması maliyeti O (n) 'ye düşürür, çünkü yineleyiciler öğelere doğrudan erişime izin verir.
kaiser

Zaman farkı hesaplaması yaparsanız, her iki kümenin de sıralandığından (veya oldukça dağıtılmamış rasgele sıralanmamış) emin olun ve testi her set için iki kez çalıştırın ve her birinin ikinci çalışmasını hesaplayın. Bu ile zamanlamalarınızı tekrar kontrol edin (testi neden iki kez çalıştırmanız gerektiğine dair uzun bir açıklama). Bunun nasıl doğru olduğunu göstermeniz gerekir (belki kodla). Aksi takdirde, bildiğim kadarıyla her ikisi de performans açısından aynıdır, ancak yetenek değildir.
ydobonebi
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.