Madde 35’de çoğu programcının enum türleri için tanımlanan ordinal
metoduna ihtiyacı olmayacağını ve bu sebeple de çok nadir durumlar haricinde kullanılmaması gerektiğini söylemiştik. Bu maddede ise bazı programcıların ordinal kullanmayı tercih ettiği bir iki özel durum üzerinde duracağız.
Bazen ordinal
metodunun bir dizi veya liste için indis (index) olarak kullanıldığını görebilirsiniz. Örneğin bitkileri temsil etmek için tasarlanmış aşağıdaki sınıfa bakalım:
class Plant {
enum LifeCycle { ANNUAL, BIENNIAL, PERENNIAL }
final String name;
final LifeCycle lifeCycle;
Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
@Override
public String toString() {
return name;
}
}
Bu sınıfta bitkilerin yaşam döngülerini temsil eden bir de LifeCycle
isimli enum türü bulunmaktadır. ANNUAL
yaşam döngüsüne sahip bitkiler tek yıl yaşayıp ölürken BIENNIAL
iki yıl, PERENNIAL
ise daha uzun yıllar yaşayabilen bitki türlerini temsil etmektedir.
Şimdi de elemanları bu bitki türünün nesneleri olan garden
isimli bir dizimiz olsun ve biz bu dizideki bitkileri yaşam döngülerine (life cycle) göre gruplayıp yazdırmak isteyelim. Bunu yapabilmek adına her bir yaşam döngüsü için bir tane olacak şekilde toplam 3 küme (Set
) oluşturup garden
dizisindeki bitki nesnelerini yaşam döngüsü değerlerine göre bu kümelere ekleyebiliriz. Bazı programcılar bunun için oluşturdukları Set
nesnelerini bir diziye koyup LifeCycle
enum sabitlerinin ordinal değerleri ile erişme yoluna giderler.
// ordinal değerini diziye indis olarak kullanmak - YAPMAYIN!
Set<Plant>[] plantsByLifeCycle =
(Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycle.length; i++) {
plantsByLifeCycle[i] = new HashSet<>();
}
for (Plant p : garden) {
plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
}
// sonuçları yazdır
for (int i = 0; i < plantsByLifeCycle.length; i++) {
System.out.printf("%s: %s%n",
Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
}
Burada plantsByLifeCycle
dizisinin 0. elemanı yaşam döngüsü ANNUAL
olan bitkileri, 1. elemanı BIENNIAL
, 2. elemanı ise PERENNIAL
bitkileri tutan Set
nesnelerini referans etmektedir.
Bu kod çalışacaktır ama problemlerle doludur. Diziler ile üreysel türler (generics) uyumlu olmadığı için (Madde 28) program kontrolsüz bir tür dönüşümü yapmak zorundadır. Bu sebeple de derleyici uyarı verecektir. Ancak daha ciddi bir problem vardır o da dizi indisleri için bir int
değeri üreten ordinal
metodunu kullandığımız için, programın her yerinde doğru int
değerilerini kullanmak sizin sorumluluğunuzdadır. Temel int
türü enumlar gibi tür güvenliği sağlayamaz. Yanlış bir değer kullanırsanız program sessiz bir biçimde yanlış sonuç üretebilir ama şanslıysanız ArrayIndexOutOfBoundsException
fırlatır.
Aynı amaca ulaşmak için kullanabileceğimiz çok daha iyi bir yol var. Buradaki dizi aslında enum sabitlerini başka verilerle eşleştirmek için kullanılmaktadır. Bunun için bir Map
kullanabiliriz. Hatta bu iş için özel yazılmış, anahtar olarak enum sabitleri kullanan java.util.EnumMap
adında bir sınıf vardır. EnumMap
kullanarak bu programı yeniden yazarsak aşağıdaki gibi olacaktır:
// EnumSet kullanarak enum sabitlerini başka verilerle eşleştirme
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle =
new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
plantsByLifeCycle.put(lc, new HashSet<>());
}
for (Plant p : garden) {
plantsByLifeCycle.get(p.lifeCycle).add(p);
}
System.out.println(plantsByLifeCycle);
Bu program daha kısa, daha anlaşılır ve daha güvenlidir. Performans olarak da önceki versiyonla benzerdir. Güvensiz bir tür dönüşümü içermez. Yazdırmak için özel bir çaba sarfetmemize gerek yoktur çünkü plantsByLifeCycle
anahtarları enum sabitleri olduğu için birer String
olarak ifade edilebilirler. En önemlisi de ortada bir dizi olmadığı için yanlış indis kullanımı yüzünden hatalar oluşmayacaktır. Bu gerçekleştirimin öncekiyle benzer performans sergilemesinin sebebi de EnumMap
sınıfının kendisinin arka planda önceki örnekteki gibi bir dizi kullanmasıdır. Tabi bu bir gerçekleştirim detayı olduğu için sınıfın kullanıcılarına yansıtılmaz. Böylece EnumMap
bize kullanım kolaylığı ve tür güvenliği vermesinin yanında kendi içinde kullandığı dizi sayesinde yüksek performans da sağlamaktadır. Ayrıca, EnumMap
türünün yapıcı metodunda anahtar olarak kullanılmak amacıyla bir Class
nesnesi kabul edilmektedir. Bu Madde 33’de gördüğümüz sınırlandırılmış tür belirtecine bir örnektir.
Aynı programı çok daha kısa bir şekilde yazmak stream kullanarak mümkündür. (Madde 45) Aşağıdaki program büyük ölçüde aynı işlevselliği taşımaktadır ve stream kullanarak yazılmıştır:
// EnumMap üretmeyen stream tabanlı yaklaşım
System.out.println(Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle)));
Bu koddaki problem kendi Map
gerçekleştirim türünü seçmesidir ve pratikte bu tür EnumMap
olmayacaktır. Bu sebeple de EnumMap
ile yakaladığımız yüksek performans ve düşük bellek kullanımı gibi avantajlar kaybolmaktadır. Bu problemi düzeltmek içinse aşağıdaki gibi Collectors.groupingBy
metodunun 3 parametreli halini kullanarak istediğimiz Map
türünü belirtebiliriz:
// Stream ve EnumMap kullanarak enum ile veri ilişkilendirme
System.out.println(Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class), toSet())));
Böyle bir optimizasyonu yapmak bizimki gibi basit bir örnek için çok önemli olmasa da bu veri yapılarını çok sık kullanan bir program için faydalı olabilir.
Bazen de ordinal değerinin iki boyutlu dizilerin indislerini ifade etmek için kullanıldığını görebilirsiniz. Örneğin, aşağıdaki program maddenin halleri arasındaki geçişleri ifade etmek için iki boyutlu bir dizi kullanmaktadır:
// iki boyutlu dizilerin indisleri için ordinal kullanımı - YAPMAYIN!
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
private static final Transition[][] TRANSITIONS = {
{ null, MELT, SUBLIME },
{ FREEZE, null, BOIL },
{ DEPOSIT, CONDENSE, null }
};
// Maddenin halleri arasındaki geçişleri temsil eden
// Transition enum sabitlerinden birini döndürüyor
public static Transition from(Phase from, Phase to) {
return TRANSITIONS[from.ordinal()][to.ordinal()];
}
}
}
Burada maddenin 3 halini temsil eden Phase
enum türü ve onun içinde de maddenin halleri arasındaki geçişleri temsil etmek üzere Transition
enum türü yazılmıştır. from
metoduna maddenin ilk ve son halini verdiğimizde bize geçişi temsil eden Transition
sabitlerinden birini döndürecektir. Örneğin SOLID
(katı) ve LIQUID
(sıvı) geçildiği zaman bu elemanların ordinal değerlerinden TRANSITIONS
iki boyutlu dizisi içerisinde katıdan sıvıya geçişi temsil eden MELT
(erime) bulunup döndürülecektir. Burada dikkat ederseniz iki enum sabiti anahtar olarak kullanılmakta ve başka bir enum sabiti değer olarak döndürülmektedir.
Bu program çalışır hatta gözünüze hoş bile gelebilir ancak görünüşler aldatıcı olabilir. Önceki bitki örneğinde olduğu gibi burada da int
türünden dizi indisleri kullanıldığı için, Phase
veya Phase.Transition
enumlarında bir değişiklik yapıp iki boyutlu diziyi olması gerektiği gibi güncellemezseniz programınız çalışma zamanında çökecektir. Bu noktada ArrayIndexOutOfBoundsException
veya NullPointerException
alabilirsiniz veya program hiç çökmeden yanlış sonuçlar da üretebilir.
Burada da EnumMap
kullanarak çok daha iyisini yapabiliriz. Her bir geçiş maddenin iki hali ile ilişkili olduğu için, bunu iç içe iki EnumMap
ile ifade edebiliriz:
// EnumMap kullanarak enum çiftleri ile veri ilişkilendirmek
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
private final Phase from;
private final Phase to;
Transition(Phase from, Phase to) {
this.from = from;
this.to = to;
}
// geçişleri temsil eden map nesnesinin yaratılması
private static final Map<Phase, Map<Phase, Transition>> m =
Stream.of(values()).collect(
groupingBy(t -> t.from,() ->
new EnumMap<>(Phase.class),
toMap(t -> t.to, t -> t,
(x, y) -> y, () -> new EnumMap<>(Phase.class))));
public static Transition from(Phase from, Phase to) {
return m.get(from).get(to);
}
}
}
Burada ilk dikkati çeken şey Transition
enum sabitlerinin yapıcı metotlarına maddenin hangi iki hali arasındaki geçişi temsil ettiğini göstermek üzere from
ve to
isimli Phase
sabitleri geçmesidir.
Geçişleri temsil eden veri yapısının yaratılması biraz karmaşık. Buradaki m
nesnesinin türü Map<Phase, Map<Phase, Transition>>
olarak belirlenmiştir. Bu veri yapısında dıştaki Phase
anahtarı from
(maddenin ilk hali), içteki Phase
anahtarı to
(maddenin son hali), Transition
ise geçiş sabitini temsil etmektedir. Bu iç içe Map
yapısını kurmak için stream ve kademeli toplayıcılar (collector) kullanılmıştır. Stream kullanmak istemiyorsanız veya karmaşık geliyorsa aynı veri yapısını aşağıdaki gibi döngüler kullanarak da oluşturabilirsiniz:
// geçişleri temsil eden veri yapısını yaratmanın alternatif yolu
private static final Map<Phase, Map<Phase, Transition>> m =
new EnumMap<>(Phase.class);
static {
for (Phase p : Phase.values()) {
m.put(p, new EnumMap<>(Phase.class));
}
for (Transition trans : Transition.values()) {
m.get(trans.src).put(trans.dst, trans);
}
}
Bu yöntem her ne kadar daha çok kod yazmayı gerektirse de bazı kişiler tarafından daha anlaşılır bulunabilir.
Şimdi varsayalım ki sisteme maddenin bir başka hali olan plazmayı eklemek istiyoruz. Bu yeni hal için iki tane daha geçiş tanımlamak gerekecektir. Gaz galden plazmaya geçiş iyonlaşma, plazmadan gaz hale geçişse iyonsuzlaşma adını alır. Bu eklemeyi iki boyutlu dizi kullandığımız örnekte yapıyor olsaydık Phase
enumuna bir tane, Transition
enumuna ise iki tane ekleme yapardık ancak 9 elemanlı diziyi (3×3) 16 elemana (4×4) çıkarmamız gerekirdi. Burada yapılacak en ufak bir hatada programımız yanlış sonuçlar üretebilir veya çalışma zamanında çökebilirdi. EnumMap
kullandığımız versiyonda ise tek yapmamız gereken Phase
enum türüne PLASMA
adında bir yeni değer, Transition
içinse IONIZE(GAS, PLASMA)
ve DEIONIZE(PLASMA, GAS)
olacak şekilde iki yeni değer eklemek olacaktır.
// EnumMap gerçekleştiriminde yeni bir Phase eklemek
public enum Phase {
SOLID, LIQUID, GAS, PLASMA;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID),
IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS);
... // Geri kalan kısımlarda değişiklik yok
}
}
Programın geri kalanı hiçbir değişikliğe ihtiyaç duymadan doğru olarak çalışmaya devam edecektir ve böylece size hata yapmak için neredeyse hiç imkan vermemektedir.
Yukarıdaki örneklerde Transition
döndüren from
metodu eğer maddenin iki hali arasında bir geçiş bulamazsa null
döndürecektir (örneğin from
ve to
parametreleri aynı olursa). Bu çok doğru bir yaklaşım olmasa da kodu basit tutmak için yapılmıştır. Gerçek sistemlerde bu gibi metotlar yüzünden sıklıkla NullPointerException
ile karşılaşabilirsiniz.
Özetle, ordinal değerlerini dizi indisi olarak kullanmak çok nadir durumlar haricinde önerilmez, bunun yerine EnumMap
kullanın. Eğer ifade etmek istediğiniz ilişkiler çok boyutlu ise iç içe EnumMap<..., EnumMap<...>>
kullanın.