Java'da generics, bir sınıfın, arayüzün veya metodun hangi veri tipiyle çalışacağını nesne oluşturulmadan önce, derleme aşamasında belirlemenizi sağlayan bir dil özelliğidir. Bu sayede kod hem tip güvenliği kazanır hem de aynı işlevi farklı veri tipleri için ayrı ayrı yazma zorunluluğu ortadan kalkar. Kısacası generics, "bu metot ya da sınıf hangi tiple çalışacak" sorusunun cevabını derleyiciye önceden bildirmenin yoludur; derleyici de yanlış tip kullanımını siz programı çalıştırmadan yakalar.
Bu yazıda generics kavramını sıfırdan ele alıyoruz: önce generics olmadan yaşanan problemlere bakacak, ardından derleme zamanı kontrolünün ne işe yaradığını ve generic metot yazımının pratik adımlarını göreceğiz.
Generics Olmadan Java'da Neler Yaşanır?
Generics Java'ya eklenmeden önce, koleksiyonlar (ArrayList, HashMap gibi yapılar) elemanları Object tipinde tutardı. Mantığı basitti: Object her tipin üst sınıfı olduğu için listeye istediğiniz tipte veriyi ekleyebilirdiniz. Ama bu esneklik ciddi bir bedel getiriyordu, çünkü listeden veri okurken elinizdeki şeyin gerçekte ne tip olduğunu bilme garantiniz kalmıyordu. Bu yüzden veriyi kullanmadan önce onu istediğiniz tipe casting yapmak zorundaydınız ve bu casting işlemi tamamen sizin varsayımınıza dayanıyordu.
Sorun şurada başlıyordu: derleyici, listeye hangi tipte eleman koyduğunuzu kontrol etmediği için, farklı tiplerdeki nesneleri aynı listeye rahatça ekleyebiliyordunuz. Aşağıdaki örnek bu riski net gösteriyor:
List liste = new ArrayList(); // Generics olmadan, ham (raw) tip kullanımı
liste.add("Merhaba");
liste.add(42); // Farklı tipte bir eleman da sorunsuzca eklenir
String metin = (String) liste.get(1); // Derleyici burada hata vermez
System.out.println(metin); // Program çalışırken ClassCastException fırlar
Görüldüğü gibi kod sorunsuz derlenir, ama programı çalıştırdığınız anda ClassCastException ile karşılaşırsınız. En kötü yanı, bu tür hataların genellikle küçük test verileriyle fark edilmemesi, üretim ortamında beklenmedik bir noktada patlak vermesidir. Bu tür casting kaynaklı riskleri baştan öğrenip önlemek isteyenler için, koleksiyonlar ve tip güvenliği konularının uygulamalı işlendiği birebir Java özel dersi bu aşamada sağlam bir temel oluşturur.
Derleme Zamanı Tip Kontrolü Nedir?

Derleme zamanı (compile-time) tip kontrolü, kodunuzu çalıştırmadan önce, derleyicinin tip uyumsuzluklarını tespit etmesi anlamına gelir. Generics'in asıl gücü tam olarak burada ortaya çıkar: bir koleksiyona veya metoda hangi tipin gireceğini baştan belirttiğinizde, derleyici o sözleşmeye uymayan her satırı derleme aşamasında reddeder. Bir önceki bölümdeki örneği generics ile yazsaydınız, liste.add(42) satırı hiç derlenmezdi; hata size çalıştırma anında değil, kodu yazarken bildirilirdi.
Bu fark pratikte çok şey değiştirir. Runtime hatası, programın gerçek kullanıcıda veya canlı ortamda ortaya çıkabilecek bir hatadır; oysa derleme zamanı hatası, sizin geliştirme ortamınızda, kodu teslim etmeden önce görünür. Yani generics kullanmak, hata bulma sürecini olabildiğince erken bir noktaya, yani en ucuz ve en kolay düzeltilebilir noktaya taşır.
Bunun arkasındaki mantık şudur: generics ile bir yapıyı tanımlarken (örneğin List<String> gibi) siz aslında derleyiciye "bu liste sadece String tutacak" diye bir söz veriyorsunuz. Derleyici de bu sözü referans alarak, listeye eklenen her elemanı, listeden okunan her değeri bu tipe göre denetler. Elle yaptığınız casting işlemleri de büyük ölçüde ortadan kalkar, çünkü derleyici zaten hangi tipin geleceğini bildiği için otomatik tip çözümlemesi yapar.
Bu yaklaşımın en somut faydası, hata mesajlarının daha anlaşılır ve daha erken gelmesidir. Bir tip uyuşmazlığı olduğunda derleyici size tam olarak hangi satırda, hangi tipin beklendiğini söyler. Bu da özellikle büyüyen ve birden fazla kişinin çalıştığı projelerde, kod tabanının tutarlılığını korumak açısından kritik bir avantaj sağlar. Kısacası derleme zamanı kontrolü, generics'in sadece bir sözdizimi kolaylığı değil, kod kalitesini doğrudan etkileyen bir güvenlik katmanı olduğunu gösterir.
Generic Metot Yazımı: <T> Kullanımı
Generic metot yazmak, aslında bir metodun hangi tiplerle çalışabileceğini esnek ama güvenli biçimde tanımlamak demektir. Bunun için metodun dönüş tipinden hemen önce bir tip parametresi tanımlarsınız; bu parametre genellikle T harfiyle gösterilir, ancak istediğiniz herhangi bir harf ya da kelime de olabilir. Önemli olan, bu sembolün metot içinde gerçek bir tip gibi kullanılabilmesidir.
Adım adım bakacak olursak:
- Metot imzasında, erişim belirleyicisinden sonra ve dönüş tipinden önce
<T>tip parametresini tanımlayın. - Metodun parametrelerinde ve dönüş tipinde bu
Tsembolünü gerçek bir tip gibi kullanın. - Metodu çağırırken tip parametresini elle belirtmenize genelde gerek kalmaz; derleyici gönderdiğiniz argümana bakarak tipi kendisi çıkarır.
Aşağıdaki örnek, bir dizinin ilk elemanını döndüren basit bir generic metodu gösteriyor. Dikkat edilmesi gereken nokta, aynı metodun hem Integer hem String dizisiyle sorunsuz çalışmasıdır:
public class Yardimci {
public static <T> T ilkElemani(T[] dizi) {
if (dizi == null || dizi.length == 0) {
return null;
}
return dizi[0];
}
public static void main(String[] args) {
Integer[] sayilar = {10, 20, 30};
String[] kelimeler = {"elma", "armut"};
System.out.println(ilkElemani(sayilar)); // 10
System.out.println(ilkElemani(kelimeler)); // elma
}
}
Burada tek bir metot tanımıyla hem sayı dizisi hem metin dizisi için çalışan, tip güvenliğinden ödün vermeyen bir çözüm elde ettik. Eğer bu tip parametreli yapıları örneklerle, adım adım pekiştirmek isterseniz, kayıtlı video ders içerikleri generic metot ve sınıf konularını uygulamalı biçimde ele alan bir kaynak sunar. Bir sonraki bölümde bu mantığı bir adım öteye taşıyıp, generic sınıf yazımına geçeceğiz.
Generic Sınıf Örneği: Basit Bir Kutu<T>
Generics'in en çok akılda kalan örneği, aslında en basit örnektir: bir kutu. Elinizde bir Kutu sınıfı olduğunu düşünün; bu kutu bazen bir String, bazen bir Integer, bazen de kendi yazdığınız bir sınıf tutacak. Generics olmadan bunu yapmanın tek yolu Object tipini kullanmaktı ve bunun getirdiği casting derdini bir önceki bölümde konuştuk. Generic bir sınıf yazdığınızda ise aynı kutuyu, hangi tipi tutacağını dışarıdan belirleyerek defalarca kullanabilirsiniz.
Mantık şu: sınıf tanımının hemen yanına <T> yazarsınız ve sınıfın içindeki alan, parametre ve dönüş tiplerinde T'yi gerçek bir tipmiş gibi kullanırsınız. Nesneyi oluştururken siz T'nin ne olacağını söylersiniz, derleyici gerisini halleder.
public class Kutu<T> {
private T icerik;
public void koy(T deger) {
this.icerik = deger;
}
public T al() {
return icerik;
}
}
// Kullanımı:
Kutu<String> metinKutusu = new Kutu<>();
metinKutusu.koy("Merhaba");
String s = metinKutusu.al(); // casting yok, doğrudan String
Kutu<Integer> sayiKutusu = new Kutu<>();
sayiKutusu.koy(42);
Integer i = sayiKutusu.al();
Burada dikkat edilmesi gereken nokta, Kutu sınıfının kodunu bir kez yazmış olmanız, ama onu istediğiniz kadar farklı tip için kullanabilmenizdir. metinKutusu.al() çağrısının sonucu doğrudan String olarak gelir; siz hiçbir yerde (String) yazmazsınız çünkü derleyici bu kutunun sadece String tutacağını zaten bilir. Aynı şekilde sayiKutusu'na yanlışlıkla bir String koymaya çalışırsanız, kod çalışmadan, derleme aşamasında hata alırsınız.
Gerçek projelerde bu desenin daha karmaşık hâllerini sık sık görürsünüz: önbellek sınıfları, sonuç sarmalayıcıları (wrapper), API yanıt nesneleri gibi birçok yapı aslında bu basit Kutu<T> mantığının üzerine kuruludur. Tek bir sınıf tanımı, sonsuz sayıda tip kombinasyonuna hizmet eder.
Sınırlı Tipler: <T extends Number>

Bazen T'nin her tipi kabul etmesini istemezsiniz; belirli bir aile içinden gelmesini isterseniz. Örneğin bir metodun içinde T üzerinde toplama işlemi yapacaksanız, T tamamen serbest bırakılırsa bu imkansızdır çünkü Java, rastgele bir tipin toplanabilir olduğunu bilemez. İşte tam bu noktada sınırlı tipler (bounded types) devreye girer.
<T extends Number> yazdığınızda, derleyiciye "bu metot ya da sınıf yalnızca Number'dan türeyen tiplerle çalışsın" dersiniz. Böylece Integer, Double, Long gibi sayısal tipler kabul edilirken, String gibi ilgisiz bir tip parametre olarak verilmeye çalışıldığında derleme hatası alınır.
public class SayiKutusu<T extends Number> {
private T deger;
public SayiKutusu(T deger) {
this.deger = deger;
}
public double ikiKatı() {
return deger.doubleValue() * 2;
}
}
SayiKutusu<Integer> kutu = new SayiKutusu<>(10);
System.out.println(kutu.ikiKatı()); // 20.0
Burada deger.doubleValue() çağrısını yapabilmemizin sebebi, T'nin Number sınırına bağlı olması; Number sınıfının her alt tipi doubleValue() metodunu barındırır, dolayısıyla derleyici bu çağrının güvenli olduğunu bilir. Sınır koymasaydık, T için hiçbir varsayım yapamaz, bu metodu yazamazdınız.
Sınırlı tipler, aslında generics'in "her şeyi kabul et" mantığını biraz daraltıp "belirli bir kategoriyi kabul et" mantığına çeviren pratik bir araçtır. Kendi tip hiyerarşinizi kurarken de aynı yaklaşımı kullanabilir, örneğin <T extends Comparable<T>> gibi sınırlamalarla sıralanabilir nesnelerle çalışan generic metotlar yazabilirsiniz. Bu tür tip ilişkilerini kâğıt üzerinde değil, küçük kod denemeleriyle pekiştirmek en kalıcı öğrenme yoludur; isterseniz mevcut seviyenizi görmek için ücretsiz kodlama bilgisi testi ile generics ve tip sistemi konusundaki hakimiyetinizi ölçebilirsiniz.
Generics ve Koleksiyonlar: List, Map İlişkisi
Generics'i günlük hayatta en sık, kendi yazdığınız sınıflarda değil, Java'nın koleksiyon yapılarında kullanırsınız. List<String>, Map<String, Integer> gibi tanımlar aslında az önce anlattığımız Kutu<T> mantığının, dilin kendi kütüphanesine uygulanmış hâlidir. List arayüzü içinde neyin tutulacağını bilmez; siz List<String> dediğinizde bu listenin yalnızca String nesneleri barındıracağını derleyiciye bildirmiş olursunuz.
Bunun neden standart hâline geldiğini anlamak için eski usul koleksiyon kullanımını hatırlamak yeterli. Generics öncesinde bir ArrayList'e istediğiniz her tipte nesne ekleyebiliyordunuz; bu esneklik gibi görünse de pratikte listeyi dolaşırken her elemanı manuel olarak cast etmeniz, üstelik yanlış tipte bir eleman eklenmişse bunu ancak çalışma zamanında fark etmeniz anlamına geliyordu. Map<String, Integer> gibi anahtar-değer yapılarında da aynı risk hem anahtar hem değer için iki katına çıkıyordu.
| Kriter | Generics Öncesi (Object tabanlı) | Generics Sonrası (List<T>, Map<K,V>) |
|---|---|---|
| Tip güvenliği | Yok, her tip karışabilir | Var, tek tip zorunlu |
| Casting ihtiyacı | Her okuma işleminde manuel cast | Casting gerekmez |
| Hata zamanı | Çalışma zamanı (ClassCastException) | Derleme zamanı |
| Kod tekrarı | Her tip için ayrı kontrol/cast kodu | Tek tanım, tüm tipler için geçerli |
Bu tablodaki fark, günlük kodlamada oldukça belirgin bir şekilde hissedilir: for (String s : liste) yazdığınızda döngü değişkeninin doğrudan String olması, IDE'nin size doğru metotları önermesi, yanlış tipte bir ekleme yapıldığında derleyicinin sizi anında uyarması gibi küçük ama birikimli kazanımlar sağlar. Map<String, Integer> üzerinde get() çağırdığınızda dönen değerin Integer olduğunu bilmeniz, üzerinde doğrudan aritmetik işlem yapabilmenizi sağlar; bu da yine cast derdini ortadan kaldırır.
Koleksiyonlarla generics ilişkisini kavramak, Java'da nesne yönelimli düşünmenin önemli bir parçasıdır ve bu konudaki hakimiyetinizi netleştirmek isterseniz Java bilgi testi üzerinden kendinizi kısa sürede değerlendirebilirsiniz. Generics'in koleksiyonlarla bu kadar sıkı bağlantılı olmasının temel sebebi, koleksiyonların doğası gereği "bir şeylerin listesi" veya "bir şeyle başka bir şey arasındaki eşleşme" gibi soyut kavramları temsil etmesidir; bu soyutluk, tam da generics'in çözmek için tasarlandığı problemle örtüşür.
Kod Tekrarını Azaltma: Tek Metotla Çoklu Tip
Generics'in gündelik hayattaki en somut faydası, aynı mantığı taşıyan ama farklı tipler için yazılmış metotları teke indirmesidir. Bunu en iyi, generics olmadan yazılmış bir örnekle görebilirsin. Diyelim ki bir listedeki elemanları ekrana yazdıran iki ayrı metodun olduğunu düşün: biri Integer listesi için, diğeri String listesi için. İçerikleri neredeyse birebir aynıdır, tek fark parametre tipidir. Bu tekrar, kod tabanı büyüdükçe bakım kabusuna dönüşür; bir hatayı düzeltmek istediğinde her kopyayı tek tek bulup güncellemen gerekir.
Generic metot bu sorunu kökten çözer. Tek bir metot tanımlarsın, tip parametresi metodun içine değil imzasına taşınır:
public static <T> void listeYazdir(List<T> liste) {
for (T eleman : liste) {
System.out.println(eleman);
}
}
// Kullanımı
List<Integer> sayilar = List.of(1, 2, 3);
List<String> kelimeler = List.of("java", "generics", "tip");
listeYazdir(sayilar);
listeYazdir(kelimeler);
Burada aynı metot, hem sayı listesi hem metin listesi için doğrudan çalışır. Derleyici her çağrıda hangi tipin kullanıldığını kendisi çözer; sen bunun için ayrı ayrı overload metot yazmak zorunda kalmazsın. Bu yaklaşımın günlük iş akışına kattığı avantajları şöyle sıralayabiliriz:
- Daha az kod, daha az bakım: Aynı işi yapan onlarca metot yerine tek bir generic versiyon yeterli olur.
- Tutarlılık: Mantık tek bir yerde yaşadığı için, bir hatayı düzelttiğinde bu düzeltme tüm tipler için otomatik geçerli olur.
- Okunabilirlik: Metodun ne iş yaptığı, farklı tip varyantları arasında boğulmadan tek bakışta anlaşılır.
- Genişletilebilirlik: Yeni bir tip eklemek istediğinde, yeni bir metot yazmana gerek kalmaz; mevcut generic metot zaten o tipi de kabul eder.
Bu mantık, gerçek projelerde repository katmanlarından yardımcı sınıflara kadar pek çok yerde karşına çıkar. Bu tür kalıpları küçük örneklerle görmek bir şey, kendi projende ne zaman generic metot yazıp ne zaman ayrı metotlarla devam edeceğine karar vermek başka bir şeydir; bu ayrımı pratikte oturtmak isteyenler için birebir Java dersleri kapsamında kod üzerinden ilerleyen bir çalışma daha kalıcı sonuç verir.
Sık Sorulan Sorular
Generics kullanmak zorunlu mudur?
Hayır, zorunlu değildir. Java'da tip belirtmeden de koleksiyon ve metot yazabilirsin ama bu durumda tip güvenliğini derleyiciden kendi sorumluluğuna almış olursun. Modern Java kodunda generics kullanmamak, hataları çalışma zamanına ertelemek anlamına gelir; bu yüzden tercih edilen yaklaşım değildir.
Generics performansı etkiler mi?
Pratikte fark edilir bir performans maliyeti yoktur. Generics, derleme aşamasında tip kontrolü sağlayan bir mekanizmadır; çalışma zamanında ekstra bir yük getirmez. Yani generics'i performans kaygısıyla es geçmene gerek yok, asıl kazanç zaten derleme anında yakalanan hatalarda.
Generic metot ile generic sınıf arasındaki fark nedir?
Generic metot, tip parametresini yalnızca kendi imzasında taşır ve o metot çağrıldığında geçerli olur; sınıfın geri kalanını etkilemez. Generic sınıf ise tip parametresini sınıfın tamamına yayar; sınıftan nesne oluştururken belirlediğin tip, o nesnenin tüm alan ve metotlarında geçerli olur.
ClassCastException hatası generics ile tamamen ortadan kalkar mı?
Generics kullanıldığında bu hatayla karşılaşma ihtimali büyük ölçüde azalır çünkü tip uyumsuzlukları derleme anında yakalanır. Ancak eski tip kontrolsüz koleksiyonlarla karışık kullanım ya da bilinçli tip dönüşümleri yapıldığında hata yine de ortaya çıkabilir; yani generics riski azaltır ama sıfırlamaz.
Bounded type ne zaman kullanılmalı?
Generic bir metot veya sınıf içinde, tip parametresi üzerinde belirli bir davranışa (örneğin sayısal işlemlere) ihtiyaç duyduğunda bounded type tercih edilir. Herhangi bir tipi kabul etmek yerine belirli bir üst sınıftan türeyen tiplerle sınırlamak, o tipe ait metotları güvenle çağırmanı sağlar.
Generics, Java'da yazdığın kodu hem daha güvenli hem daha az tekrarlı hale getiren temel bir araçtır; bir kez mantığını kavradığında koleksiyonlardan kendi sınıflarına kadar her yerde doğal bir refleks haline gelir. Bu kavramı kalıcı biçimde oturtmak istiyorsan Java yazılım kursu içeriğine göz atarak konuyu daha geniş bir müfredat içinde ilerletebilirsin.