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ı veremove
metodunu çağırmalısınız. Ancak birçok durumda Java 8’leCollections
‘a eklenenremoveIf
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.