Press "Enter" to skip to content

Effective Java Madde 58: Klasik for Döngüleri Yerine for-each Tercih Edin

Last updated on July 17, 2022

Madde 45’de anlatıldığı üzere, bazı durumlarda stream kullanmak avantajlı olurken, bazen de yinelemeye (iteration) başvurmak daha mantıklı olmaktadır. Aşağıda, bir koleksiyonun elemanlarını tarayan bir döngü görmekteyiz:

// Koleksiyon elemanlarını tarayan klasik döngü kodu
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
    Element e = i.next();
    ... // e ile bir işlem yapabilirsiniz
}

Bu da dizi elemanlarını tarayan klasik döngü kodu:

// Dizi elemanlarını tarayan klasik döngü kodu
for (int i = 0; i < a.length; i++) {
    ... // a[i] ile bir işlem yapın
}

Yukarıdaki kullanımlar Madde 57’deki while döngülerinden daha iyi olsa da mükemmel değiller. Iterator ve döngü değişkenleri son derece gereksiz, tek ihtiyacımız olan elemanların kendileri. Bu gereksiz değişkenler döngüyü kurarken ve elemanlara erişirken hata yapma olasılığı doğurmakta ve kodun okunabilirliğini azaltmaktadır. Ayrıca, dizi ve koleksiyon elemanlarına erişim için döngüler farklı şekilde kuruluyor. Bunun yerine for-each döngülerini kullanarak bu problemleri aşabiliriz. Bu döngülerde Iterator ve döngü değişkenleri gizlenmiş olduğu için hata yapma olasılığı ortadan kalkar ve kodun okunabilirliği artar.

// Tercih edilen döngü kullanımı
for (Element e : elements) {
    ... // e elemanını kullanabilirsiniz..
}

Yukarıdaki döngü kodu hem koleksiyonlar hem de diziler için çalışacaktır. Bu kullanımın performans açısından bir dezavantajı da yoktur.

for-each döngülerinin avantajı iç içe döngüler kullanıldığında çok daha belirgin olmaktadır. Aşağıda, iç içe döngüler kullanılırken sıkça yapılan bir hata gösterilmektedir:

// Hatayı bulabilir misiniz?
enum Suit { CLUB, DIAMOND, HEART, SPADE }
enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,
            NINE, TEN, JACK, QUEEN, KING }
...
static Collection<Suit> suits = Arrays.asList(Suit.values());
static Collection<Rank> ranks = Arrays.asList(Rank.values());
List<Card> deck = new ArrayList<>();

for (Iterator<Suit> i = suits.iterator(); i.hasNext(); ) {
    for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); ) {
        deck.add(new Card(i.next(), j.next()));
    }
}

Buradaki hatayı bulamadıysanız üzülmeyin. Birçok uzman yazılımcı bile bu hatayı yapmış olabilir. Bu koddaki problem dıştaki koleksiyonu (suits) gezen iteratörün next metodunun yanlış yerde çağrılıyor olmasıdır. Her bir suit için çağrılması gereken bu metot, içteki döngüde çağrıldığından iteretor her bir rank için ilerletilmektedir. Bu da suit elemanlarının erkenden tükenmesine ve kodun NoSuchElementException fırlatmasına sebep olmaktadır.

Eğer çok şanssızsanız ve dıştaki koleksiyonun eleman sayısı içtekinin bir veya daha fazla katıysa (mesela aynı koleksiyonu hem içteki hem de dıştaki döngüde kullanıyorsanız), döngü aykırı durum fırlatmadan tamamlanacak ancak istediğiniz sonucu üretmeyecektir. Örneğin, bir çift zar atıldığında gelebilecek tüm kombinasyonları yazdırmaya çalışan aşağıdaki döngü koduna bakalım:

// Aynı hata, farklı sonuç!
enum Face { ONE, TWO, THREE, FOUR, FIVE, SIX }
...
Collection<Face> faces = EnumSet.allOf(Face.class);
for (Iterator<Face> i = faces.iterator(); i.hasNext(); ) {
    for (Iterator<Face> j = faces.iterator(); j.hasNext(); ) {
        System.out.println(i.next() + " " + j.next());
    }
}

Bu program hata üretmez, ancak istediğimiz gibi 36 kombinasyonu yazdırmak yerine "ONE ONE" ile başlayıp "SIX SIX"‘e kadar sadece 6 tane çıktı üretir.

Yukarıdaki örneklerde bulunan hatayı düzeltmek için, dıştaki elemanı tutacak şekilde bir değişken kullanmalıyız:

// Hata çözüldü, ancak daha iyisini yapabiliriz!
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); ) {
    Suit suit = i.next();
    for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); ) {
        deck.add(new Card(suit, j.next()));
    }
}

Bunun yerine for-each döngüsü kullanırsak hata otomatik olarak ortadan kaybolmuş olur:

// İç içe döngülerlerde tercih edilmesi gereken for-each kullanımı
for (Suit suit : suits) {
    for (Rank rank : ranks) {
        deck.add(new Card(suit, rank));
    }
}

Maalesef, for-each döngülerini kullanmanın mümkün olmadığı üç tane durum bulunmaktadır:

  • Eğer bir koleksiyon üzerinde gezerken aynı zamanda eleman silmeniz gerekiyorsa, bir iterator kullanmalı ve remove metodunu çağırmalısınız. Ancak birçok durumda Java 8’le Collections‘a eklenen removeIf metodunu kullanarak döngüye gerek kalmadan da bunu yapabilirsiniz.
  • Eğer bir liste veya dizi elemanları üzerinde gezip, bir veya daha çok elemanın değerini değiştirmek istiyorsanız, bir liste iteratörü veya dizi indisi kullanmanız gerekecektir.
  • Eğer birden fazla koleksiyon üzerinde aynı anda iterasyon yapacaksanız, bir iteratöre veya dizi indisine gerek duyarsınız çünkü koleksiyonlar üzerinde ilerleme yaparken bunların birbiriyle uyumlu bir biçimde gerçekleşmesi gerekir. Yukarıdaki kart ve zar örneklerinde bunun hatalı bir kullanımını görmüştük.

Eğer bu durumlardan birisiyle karşılaşırsanız klasik for döngüsü kullanın ancak yukarıda belirttiğimiz hatalara düşmeyin!

for-each kullanarak sadece koleksiyonlar ve diziler üzerinde değil, Iterable arayüzünü gerçekleştiren bütün nesneler üzerinde tarama yapabiliriz. Bu arayüzün tek bir metodu vardır ve aşağıdaki gibi tanımlanmıştır:

public interface Iterable<E> {
    // Bu tür üzerinde tarama yapan bir iterator döndürür    
    Iterator<E> iterator();
}

Eğer sıfırdan bir Iterator gerçekleştirimi yapıyorsanız bu arayüzü uygulamak biraz zahmetli olabilir. Ancak bir grup elemanı temsil eden bir tür yazıyorsanız Iterator arayüzünü uygulamayı ciddi ciddi düşünün. Böylece kullanıcılarınız tür üzerinde for-each döngüsüyle tarama yapabilirler ve bunun için size minnet duyacaklardır.

Özetle, for-each döngüleri klasik for döngülerine göre daha okunaklıdır, hata yapma olasılığını düşürürler ve daha esnektirler. Herhangi bi performans kaybına da sebep olmadıklarından, mümkün olan her yerde kullanılmalıdırlar.

Share

Leave a Reply

%d bloggers like this: