EnumSet, Java 5'te Collections Framework geliştirmelerinin bir parçası olarak tanıtıldı ve Joshua Bloch tarafından enum türleri için yüksek performanslı, hafıza verimli bir Set uygulaması sağlamak amacıyla özel olarak tasarlandı. Tanıtımından önce geliştiriciler, gerektiğinden fazla hashing algoritmaları, kutu yönetimi ve nesne kutulama yükü ile HashSet<EnumType> kullanıyordu, oysa ki bu aslında sonlu, indekslenmiş bir koleksiyondu. Tasarım ekibi, enum sabitlerinin etkili bir şekilde derleme zamanı sabitleri olduğunu ve atanan ordinal değerleri ile mükemmel şekilde ikili vektör temsilleri için uygun adaylar olduğunu fark etti. Bu içgörü, bir soyut sınıfın, enum türünün kardinalitesine uyum sağlayacak iki farklı somut uygulama ile oluşturulmasına yol açtı.
Bir enum türü 64 veya daha az sabit içeriyorsa, tek bir 64 bit long ilkel tipi mükemmel bir bit vektörü görevi görür; bu sayede add(), remove() ve contains() gibi işlemler O(1) karmaşıklığı ile tek bit düzeyinde talimatlar olarak yürütülür. Ancak, bir enum 64 sabiti aştığında (Java long'un bit genişliği), bu tek kelimelik temsil taşar; çok kelimeli bir yapıya ihtiyaç duyulur, bu da teorik olarak performansı düşürebilir veya API sözleşmelerini bozabilir. Mimari zorluk, soyut EnumSet API'sini korurken, bir alanlı uygulama (RegularEnumSet) ile dizi tabanlı bir uygulama (JumboEnumSet) arasında kesintisiz geçişi sağlamak ve çağırana uygulama detaylarını açığa çıkarmamaktı. Ayrıca, addAll() ve retainAll() gibi toplu işlemlerin her iki temsilin de verimli olmasını sağlamak, geleneksel hash tabanlı koleksiyonlarla ilişkili O(n) karmaşıklığını önlemek gerekiyordu.
JDK, EnumSet.noneOf() aracılığıyla bir fabrika modeli kullanır; bu, enum sınıfının getEnumConstants() uzunluğunu çalışma zamanında inceleyerek ya RegularEnumSet (≤64 sabit için) ya da JumboEnumSet (>64 sabit için) oluşturur. RegularEnumSet, bit düzeyindeki işlemleri (eklemek için |= 1L << ordinal, çıkarmak için &= ~(1L << ordinal)) tek bir long elements alanında saklayarak, tek CPU talimatlarına derlenir. JumboEnumSet, index ordinal >>> 6 kelimeyi seçerken long[] elements dizisini korur ve 1L << ordinal kelimenin içindeki biti seçer, bu da O(1) tek öğe işlemleri ve O(n/64) toplu işlemler sağlar—pratik enum boyutları için etkili bir şekilde O(1). Her iki sınıf da soyut EnumSet<E>'i genişletir ve addAll() gibi soyut yöntemleri geçersiz kılar; JumboEnumSet, CPU önbellek satırlarını verimli bir şekilde kullanmak için kelime düzeyinde yineleme yoluyla toplu işlemleri gerçekleştirir.
public enum SmallPlanet { MERCURY, VENUS, EARTH, MARS } // 4 sabit public enum LargeStatus { S0, S1, S2, /* ... */ S63, S64, S65 // 66 sabit } // Fabrika yöntemi uygulamayı şeffaf bir şekilde seçer EnumSet<SmallPlanet> smallSet = EnumSet.allOf(SmallPlanet.class); // Tek bir long alanla desteklenir EnumSet<LargeStatus> largeSet = EnumSet.allOf(LargeStatus.class); // long[2] dizisi ile desteklenir
Yüksek frekanslı bir ticaret platformu, piyasa verisi olaylarını 50 farklı olay türü içeren bir enum MarketDataEvent olarak modellemektedir (teklifler, işlemler, iptaller vb.). Sistem, her müşteri bağlantısı için abonelik ilgi alanlarını korumak amacıyla EnumSet<MarketDataEvent> kullanır ve gelen olayları müşteri tercihleriyle filtrelemek için küme kesişim işlemleri (retainAll) gerçekleştirir.
Problem tanımı: Yönetmeliklerin getirdiği 20 yeni egzotik türev olay türüyle enum, 70 sabite yükselmiştir. Operasyon ekibi, olay dağıtımı gecikmesinin, hangi müşterilerin hangi güncellemeleri alacağını belirleyen küme kesişim aşamasında %15 arttığını gözlemlemiştir. Profiling, EnumSet'in hala kullanılmasına rağmen, uygulamanın sessizce RegularEnumSet'den JumboEnumSet'e geçtiğini açığa çıkarmıştır ve toplu retainAll işlemi iki long kelime üzerinden yineleme yapmaktadır; bu da tek bir bit düzeyinde AND yapmaktan çok daha yavaştır.
Çözüm 1: HashSet<MarketDataEvent>‘e geçiş yapın
Bu yaklaşım, enum boyutuna bakılmaksızın kod yolunu birleştirecektir. HashSet, tutarlı performans özellikleri ve basit uygulama sağlar. Ancak, profiling HashSet'in, hashCode() hesaplaması (enums için bile önbelleğe alınmış olsa da), kutu gezintisi ve düğüm nesne yükü nedeniyle %40 daha fazla gecikme sunduğunu göstermiştir. Ayrıca, setteki bellek ayak izi de önemli ölçüde artmış ve sistemin 100,000 eş zamanlı bağlantıları için sorunlu hale gelmiştir.
Çözüm 2: Özelleştirilmiş bir BitSet sargısı uygulamak
Ekip, enum ordinal'ları ile karşılık gelen bit indekslerini manuel olarak yönetmek için java.util.BitSet'i sarmalamayı düşündü. Bu, EnumSet'in otomatik uygulama geçişini önleyecektir. BitSet'in toplu işlemler için mükemmel ham performans sunduğu halde, tür güvenliğinden yoksundu ve MarketDataEvent örnekleri ile tam sayı indeksleri arasında manuel çeviri gerektiriyordu. Bu, bakım yükü ve enum sıralaması değiştiğinde indeks bozulma potansiyeli getirdi; bu da en az sürpriz ilkesini ihlal eder.
Çözüm 3: EnumSet ile kesişim algoritmasını optimize etmek
JumboEnumSet'in hala HashSet'ten daha iyi performans gösterdiğini gözlemleyerek, ekip, olay yönlendirmelerini kesişim sonuçlarını önbelleğe alacak şekilde optimize etti. Her gelen olay için retainAll hesaplamak yerine, yaygın abonelik desenleri için önceden bit düzeyinde maskeler hesaplayarak EnumSet.complementOf() ve bit düzeyinde mantık kullandılar. Bu, JumboEnumSet'in yedek dizileri üzerindeki toplu işlemlerin sıklığını minimize etti.
Seçilen çözüm ve nedeni: Çözüm 3, EnumSet'in tür güvenliğini ve hafıza verimliliğini korurken RegularEnumSet ve JumboEnumSet arasındaki performans farkını azaltmayı amaçladığı için seçildi. Ekip, %15 gecikme artışının HashSet ile gözlemlenen %400 kötüleşmeyle karşılaştırıldığında önemsiz olduğu sonucuna vardı; önbellekleme stratejisi etkisini %2'ye düşürdü. Sonuç, platformun yeni düzenleyici olayları mimari değişiklik olmadan başarıyla işlediği ve genişletilmiş enum kardinalitesi ile birlikte sub-mikrosaniye olay filtreleme gecikmesini koruduğu oldu.
EnumSet neden açıkça null öğeleri yasaklıyor ve bu kısıtlama bit vektörü optimizasyonunu nasıl sağlıyor?
EnumSet, bit vektöründeki doğrudan indeks olarak enum'un ordinal() değerini kullanarak optimize edilmiştir. Null referansların ordinal değeri yoktur; bu da, her long kelimede belirli bir bekçi biti ayırmaksızın bir bit konumunda kodlanmalarını imkansız kılar. Ayrıca, contains(Object) yöntemi, hemen ordinal çıkarımı ile birlikte bir instanceof kontrolü yapar; null değerine izin verilmesi, sıcak yolda açık bir null kontrolü gerektirir ve bu, sıfır maliyetli soyutlama ilkesini bozan şube tahmin cezası getirebilir. Bu kısıtlama, RegularEnumSet'in contains yöntemini basitçe return (elements & (1L << ((Enum<?>)e).ordinal())) != 0; şeklinde implementasyonunu sağlar, tek bir CPU talimatı ile güvenlik kontrolleri olmaksızın.
EnumSet, bir değişiklik sayımı alanı olmadan nasıl başarısız hızlı yineleme gerçekleştiriyor?
HashSet'in, modifikasyonları bir int modCount alanı ile izlediği gibi, EnumSet yineleyicileri içsel durumun bir anlık görüntüsünü yakalar. RegularEnumSet'te, yineleyici oluşturulduğunda elements alanının başlangıç değeri saklanır. Her next() veya remove() çağrısında, mevcut elements değerini bu anlık görüntü ile karşılaştırır; herhangi bir fark, eşzamanlı bir değişikliği gösterir ve ConcurrentModificationException'ı tetikler. JumboEnumSet, dizi referansını klonlayarak veya kelime bazında kontrol ederek benzer bir strateji izler. Bu yaklaşım, ayrı bir sayıcı alanının bellek yükünü önlerken başarısız hızlı sözleşmeyi korur; ancak sadece geçişteki belirli kelimelere yapılan değişiklikleri, dizinin yapısal değişikliklerini değil, tespit eder.
EnumSet neden soyut ve kullanıcı tanımlı alt sınıfların önlenmesi için hangi mekanizma var?
EnumSet, fabrika tabanlı instantiation'u sağlamak için soyut olarak belirtilmiştir. Bu, JDK'nın enum kardinalitesine dayalı olarak RegularEnumSet ve JumboEnumSet arasında seçim yapmasına olanak tanır; bu uygulama sınıflarını genel API'de açığa çıkarmaksızın sağlar. Sınıf, tüm yapıcıları paket özel (varsayılan erişim) olarak belirleyerek dış alt sınıflamayı engeller. EnumSet, java.util içinde yer aldığından, kullanıcı kodları bu paket içinde yer alamaz (Java modül sistemi kapsülleme ve güvenlik kısıtlamaları nedeniyle), bu nedenle dış kod onu oluştaramaz veya genişletemez. Bu tasarım deseni, "kontrol edilen alt sınıflama" olarak bilinir ve platformun uygulama stratejisini (örneğin, yeni bit vektörü şemaları tanıtmak gibi) evrim geçirebilmesini sağlarken mevcut dağıtımların milyonlarca için ikili uyumluluğu bozmaz.