JAVA COLLECTİONS PERFORMANS: ARRAYLİST VS LİNKEDLİST VS HASHMAP (NE ZAMAN HANGİSİ?)
Java’da “performans sorunu” diye gelen pek çok vakanın altında, yanlış koleksiyon seçimi yatar: veri yapısı doğru değilse en iyi algoritma bile yorulur. ArrayList mi, LinkedList mi, yoksa HashMap mi? Bu soru aslında “hangi operasyon benim için kritik?” sorusuna verilen cevaptır.
Bu makalede üç popüler koleksiyonu; okuma/gezme, ekleme-silme, bellek maliyeti, CPU önbelleği etkisi ve gerçekçi ölçüm pratikleriyle ele alacağız. Amacımız ezber değil: senaryoya göre hızlı ve güvenli seçim yapabilmeniz.
Eğer temeli sağlamlaştırmak, Collections API’nin inceliklerini ve performans tuzaklarını daha sistematik öğrenmek isterseniz Java eğitimi sayfasına da göz atabilirsiniz.

Java Collections performansını belirleyen ölçütler
Big O önemli, ama tek başına yeterli değil
“O(1) hızlıdır” demek cazip; ancak gerçek dünyada veri boyutu, CPU önbelleği, branch prediction, nesne tahsisi ve GC baskısı gibi etkiler sonucu değiştirir. Örneğin LinkedList’te ortadan ekleme teoride “O(1) bağlantı güncelle” gibi görünür; ama doğru konuma gitmek için önce düğümleri tek tek gezersiniz: bu da “O(n)” ve üstelik bellek erişimi dağınıktır.
Operasyon profili: okuma mı, ekleme mi, arama mı?
Bir koleksiyon seçiminde önce baskın operasyonları yazın: “%70 iterasyon + %20 okuma + %10 ekleme” gibi. Bu yaklaşım, “genel olarak en hızlı” yerine iş yüküne en uygun seçim yapmanızı sağlar.
Bellek ve GC maliyeti performansın gizli çarpanıdır
ArrayList çoğunlukla bitişik bir Object[] üzerinde çalışır; bu, daha az nesne demektir. LinkedList ise her eleman için ayrı bir düğüm (node) nesnesi tutar; bu hem bellek tüketimini artırır hem de GC’yi daha sık tetikleyebilir. HashMap’te de her giriş için ek yapı (entry/node) bulunduğundan, kapasite ve yük faktörü doğru ayarlanmazsa bellek artışı performansa geri döner.
ArrayList performansı: ne zaman parlıyor?
Rastgele erişim ve iterasyonda avantaj
ArrayList’in en güçlü tarafı rastgele erişim (get(i)) ve hızlı iterasyondur. Bitişik dizi yerleşimi sayesinde CPU önbelleği dostudur; ardışık dolaşımda (for-each) çoğunlukla çok iyi performans verir. Eğer listenin büyük kısmı “oku/gez” operasyonuysa, ArrayList genellikle ilk adaydır.
Sona ekleme hızlı; ama kapasite büyümesi sürpriz yaratabilir
add(e) çoğu zaman amortize “O(1)” kabul edilir. Ancak iç dizi dolarsa yeni bir dizi ayrılır ve elemanlar kopyalanır. Bu kopyalama anlık gecikmelere yol açabilir. Büyük listelerde (özellikle gecikmeye hassas servislerde) başlangıç kapasitesini bilerek vermek, beklenmedik büyüme maliyetini azaltır.
Ortadan ekleme/silme: kaydırma maliyeti
ArrayList’te araya ekleme veya ortadan silme, elemanların kaydırılmasını gerektirir. Bu yüzden yoğun “insert/remove at index” iş yüklerinde maliyet hızla artabilir. Yine de “çok az sayıda orta operasyon” varsa, basitlik ve cache avantajı ArrayList’i önde tutabilir.
import java.util.*;
public class ListOps {
public static void main(String[] args) {
List<Integer> a = new ArrayList<>(1_000_000);
List<Integer> l = new LinkedList<>();
for (int i = 0; i < 1_000_000; i++) {
a.add(i);
l.add(i);
}
// Sık okuma
int sum = 0;
for (int i = 0; i < 100_000; i++) {
sum += a.get(500_000); // ArrayList genelde daha hızlı
}
// Araya ekleme (örnek)
a.add(10, -1); // kaydırma
l.add(10, -1); // konuma gitmek için dolaşma
System.out.println(sum);
}
}LinkedList performansı: doğru yerde kullanılırsa anlamlı
Nesne düğümleri ve dağınık bellek erişimi
LinkedList, her eleman için ayrı bir düğüm taşıdığı için bellek tüketimi artar. Bu düğümler heap’te dağınık durduğundan iterasyon sırasında cache miss oranı yükselir; pratikte “sadece dolaş” senaryosunda ArrayList’e göre sıkça daha yavaş görülür. Bu yüzden LinkedList, “liste” gibi görünse de çoğu iş yükünde varsayılan tercih olmamalıdır.
Deque olarak güçlü: baştan/sondan işlemlerde istikrarlı
LinkedList’in güçlü olduğu yer, Deque davranışıdır: baştan ekle/sil veya sondan ekle/sil. Queue/stack benzeri kullanımda (özellikle çok büyük olmayan veri setlerinde) okunabilir bir seçenek olabilir. Yine de modern Java’da bu rol için çoğunlukla ArrayDeque daha performanslıdır; çünkü daha az nesne üretir.
Iterator ile konumlandıysan ekleme/silme pratik olabilir
Liste üzerinde bir iterator ile zaten doğru konuma gelmişseniz, o noktada ekleme/silme bağlantı güncelleme açısından pratiktir. Ama kritik kısım “konuma gelme” maliyetidir; bu maliyeti göz ardı etmek, yanlış bir performans beklentisi doğurur.

HashMap performansı: hızlı arama, doğru ayarla daha da hızlı
Anahtar bazlı erişimde doğru araç
“Bir öğeyi indekse göre değil, anahtara göre bulmalıyım” dediğiniz anda HashMap doğal adaydır. Ortalama durumda get/put operasyonları hızlıdır; ancak bu hız, iyi bir hash dağılımı ve doğru kapasite yönetimi varsayar. Özellikle yoğun erişim senaryosunda HashMap performansı, kapasite büyümeleri ve çakışma davranışıyla dalgalanabilir.
Yük faktörü ve kapasite: yeniden boyutlandırma maliyeti
HashMap belirli bir doluluk eşiğinde yeniden boyutlandırma (rehash) yapar. Bu süreçte iç tablo büyür ve girişler yeniden yerleştirilir. Büyük haritalarda bu, gözle görülür gecikmelere dönüşebilir. Veri boyutunu tahmin edebiliyorsanız, başlangıç kapasitesi vererek bu maliyeti azaltabilirsiniz.
Çakışmalar, equals/hashCode kalitesi ve anahtar seçimi
İyi bir hashCode dağılımı yoksa, aynı kovaya yığılan girişler aramayı pahalılaştırır. Bu yüzden anahtar tiplerinin equals/hashCode sözleşmesine uygun ve dengeli olması kritiktir. String anahtarlar çoğu senaryoda iyi çalışır; ama özel sınıflarda “alan seçimi” ve “immutability” gibi konular doğrudan performansa yansır.
import java.util.*;
public class MapTuning {
public static void main(String[] args) {
int expected = 200_000;
// Varsayılan yük faktörü 0.75; kapasiteyi tahmini boyuta göre ayarlamak rehash'i azaltır
int capacity = (int) (expected / 0.75f) + 1;
Map<String, Integer> counts = new HashMap<>(capacity, 0.75f);
// computeIfAbsent ile gereksiz get/put azaltılır
String key = "istanbul";
counts.put(key, counts.getOrDefault(key, 0) + 1);
counts.compute(key, (k, v) -> v == null ? 1 : v + 1);
// Sık arama senaryosu
int total = 0;
for (int i = 0; i < 1_000_000; i++) {
total += counts.getOrDefault(key, 0);
}
System.out.println(total);
}
}ArrayList vs LinkedList vs HashMap: operasyon bazlı karşılaştırma
Hızlı seçim için pratik kontrol listesi
Aşağıdaki özet, “ne zaman hangisi?” sorusuna hızlı bir çerçeve verir. Yine de nihai karar; veri boyutu, erişim paterni ve JVM ayarları gibi değişkenlerle şekillenir. En güvenlisi, kritik noktaları küçük bir ölçümle doğrulamaktır.
- Sık iterasyon + indeksle okuma: ArrayList genelde en iyi başlangıç noktasıdır.
- Baş/son ekle-sil (kuyruk/stack gibi): LinkedList kullanılabilir; ama çoğu durumda ArrayDeque daha verimlidir.
- Anahtarla arama (lookup): HashMap doğal seçimdir; kapasiteyi öngörmek dalgalanmayı azaltır.
- Ortadan yoğun ekleme/silme: LinkedList teoride uygun görünür; pratikte konuma gitme maliyeti yüzünden dikkat ister.
- Bellek hassasiyeti: ArrayList çoğu zaman daha az nesne ürettiği için avantajlıdır.
Cache locality ve modern CPU davranışı
ArrayList’in çoğu senaryoda “beklenenden” hızlı olmasının sebebi yalnızca Big O değil, cache locality’dir. Ardışık belleğe yakın erişim, CPU’nun ön getirme (prefetch) mekanizmalarını daha iyi kullanır. LinkedList’te ise her adımda başka bir düğüme atlamak, çoğu zaman bellek gecikmesini öne çıkarır.
Ölçümleme: JMH ile doğru benchmark nasıl yapılır?
Neden basit System.nanoTime yanıltabilir?
JIT derleyici optimizasyonları, dead code elimination ve ısınma (warmup) etkileri yüzünden “tek sefer ölç, sonuç çıkar” yaklaşımı yanıltıcıdır. Basit ölçümlerde JVM henüz optimize etmemiş olabilir veya kod tamamen elenebilir. Bu nedenle mikro-benchmark için JMH tercih edilir.
JMH senaryosu tasarlarken dikkat edilecekler
Benchmark’ta gerçek iş yükünü taklit edin: veri boyutunu, erişim dağılımını ve concurrency durumunu doğru kurgulayın. Ayrıca test verisini her iterasyonda yeniden üretmek yerine setup aşamasında hazırlamak, ölçümü daha temiz yapar. Sonuçları değerlendirirken ortalama süre kadar sapma ve yüzdelikleri de önemseyin.

Gerçek dünya senaryoları: hangi yapı, hangi tip veri?
Okuma ağırlıklı sayfalandırma ve listeleme
API katmanında sayfalandırma yapıyor, veriyi filtreleyip sıraladıktan sonra liste olarak dönüyorsanız, çoğu zaman ArrayList idealdir. İndeksle erişim kolaydır, serialize ederken iterasyon hızlıdır. Ayrıca kapasiteyi yaklaşık sayfa boyutuna göre ayarlamak (ör. 50, 100) küçük ama düzenli bir kazanç sağlayabilir.
Lookup ağırlıklı caching ve sayaçlar
“KullanıcıId → oturum bilgisi” ya da “ürünKodu → stok” gibi senaryolarda HashMap öne çıkar. Burada kritik nokta; anahtarların stabil olması ve hashCode/equals davranışının tutarlı kalmasıdır. Eğer map büyüyorsa, başlangıç kapasitesi vermek ve gereksiz boxing’i azaltmak, gecikme dalgalanmalarını düşürür.
Sıra mantığı: queue ve job işleme
İşleri sırayla alıp tükettiğiniz yapılarda (producer/consumer) LinkedList yerine çoğunlukla ArrayDeque daha uygundur; ancak “Collections dünyasında LinkedList nerede mantıklı?” sorusunun cevabı da buraya yakındır. Eğer API gereği List arayüzüyle çalışmak zorundaysanız ve baştan/sondan operasyonlar baskınsa LinkedList düşünülebilir.
Performans tuzakları ve ince ayarlar
Boxing maliyeti: Integer yerine primitive koleksiyonlar
List<Integer> gibi kullanımlarda boxing/unboxing maliyeti oluşur. Çok sıcak (hot path) kodlarda bu maliyet hissedilebilir. Standart kütüphanede primitive koleksiyonlar yoktur; ancak bu noktada tasarımı yeniden düşünmek, diziler veya özel yapılar kullanmak performansı belirgin iyileştirebilir.
Yanlış paylaşım: gereksiz kopyalar ve dönüşümler
Sürekli “List → Set → List” gibi dönüşümler yapmak, hem CPU hem bellek maliyeti getirir. En başta doğru yapıyı seçmek, sonraki dönüşümleri minimize eder. Ayrıca koleksiyonları gereksiz yere synchronized yapmak ya da thread-safe beklentisiyle yanlış yapı seçmek de performansı düşürür; concurrency ihtiyacı varsa ConcurrentHashMap gibi alternatifleri değerlendirmek gerekir.
Kapalı kutu: ölçmeden kesin konuşma
Son söz: “ArrayList her zaman daha hızlı” veya “LinkedList araya eklemede iyidir” gibi genellemeler, ancak operasyon profiliyle birlikte anlamlıdır. Küçük bir JMH testiyle kendi veri boyutunuzda ölçmek; tahmin yürütmekten daha güvenlidir. Yine de çoğu uygulamada, başlangıç için sağlam varsayım şudur: okuma/iterasyon için ArrayList, anahtarla erişim için HashMap, deque davranışı için (çoğu zaman) ArrayDeque.
Özet: ne zaman hangisi?
- ArrayList: Okuma ve dolaşım ağırlıklı işlerde; indeksle erişim gereken yerlerde.
- LinkedList: Baş/son operasyonları ve iterator ile konumlu ekleme/silme gereken sınırlı senaryolarda.
- HashMap: Anahtar bazlı arama ve ilişkilendirme gereken durumlarda; kapasiteyi öngörerek daha stabil gecikme için.
Bu çerçeveyi bir başlangıç rehberi olarak kullanın; ardından kendi yükünüzü ölçün. Doğru koleksiyon, yalnızca hız değil; kodun bakım kolaylığı ve sistemin öngörülebilirliği için de kritik bir kaldıraçtır.


