Roadmap’e dön
Ders 3 · Java’nın yapı taşları

Generics: generic class, generic method ve bounds

Aynı kodu farklı tiplerle çalıştırırken derleme zamanında tip güvenliğini nasıl koruyacağını; type parameter, bounds ve invariance üzerinden öğren.

Java Core2 Eylül 2026~20 dk okuma
Ana fikir: Generics, aynı kodu farklı tiplerle çalıştırırken derleme zamanında tip güvenliğini korumamızı sağlar.

01 Generics neden var?

Generics olmasaydı genel amaçlı bir kutuyu şöyle yazabilirdik:

JAVApublic class Box {

    private Object value;

    public void set(Object value) {
        this.value = value;
    }

    public Object get() {
        return value;
    }
}
JAVABox box = new Box();
box.set("Java");

String value = (String) box.get();

Bu yaklaşımda iki önemli problem vardır:

  • get() sonucu için cast gerekir.
  • Kutunun içine yanlışlıkla her tip konabilir.
JAVAbox.set(42);

String value = (String) box.get();
// Runtime'da ClassCastException

Generic sürümünde kabul edilen tip önceden belirlenir:

JAVApublic class Box<T> {

    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}
JAVABox<String> box = new Box<>();

box.set("Java");
String value = box.get();

box.set(42);
// Compile error
Generics hatayı runtime’dan compile-time’a taşır. En büyük faydası budur.

02 T nedir?

JAVApublic class Box<T> {}

Buradaki T, bir type parameter, yani tip parametresidir. T özel bir keyword değildir; aynı sınıfı X ile de yazabilirdik. Ancak Java’da yaygın isimlendirmeler vardır.

T

Type

E

Element

K

Key

V

Value

R

Result

N

Number

HashMap kabaca iki tip parametresi kullanır:

JAVAMap<K, V>

Map<String, Integer> ages = new HashMap<>();
// K → String
// V → Integer

Generic class kullanımı

JAVABox<String> stringBox = new Box<>();
Box<Integer> integerBox = new Box<>();
Box<User> userBox = new Box<>();

stringBox.set("Java");
integerBox.set(42);
userBox.set(new User("Ali"));

Aynı Box kodu farklı tiplerle kullanılabilir. Her nesne kendi belirlenen tipiyle çalışır.

03 Generic class yazmak

Birden fazla tip parametresi taşıyan immutable Pair örneği:

JAVAimport java.util.Objects;

public class Pair<K, V> {

    private final K key;
    private final V value;

    public Pair(K key, V value) {
        this.key = Objects.requireNonNull(key);
        this.value = Objects.requireNonNull(value);
    }

    public K key() {
        return key;
    }

    public V value() {
        return value;
    }
}
JAVAPair<String, Integer> age =
        new Pair<>("Ali", 29);

String name = age.key();
Integer value = age.value();

Pair<Long, User> userEntry =
        new Pair<>(100L, new User("Ali"));

Type parameter sınıf içinde farklı konumlarda kullanılabilir:

JAVApublic class Container<T> {

    private T value;               // Field tipi

    public Container(T value) {    // Parametre tipi
        this.value = value;
    }

    public T get() {               // Return tipi
        return value;
    }

    public void set(T value) {     // Metot parametresi
        this.value = value;
    }
}

04 Diamond operator <>

new Box<String>()

Eski ve tekrarlı kullanım.

new Box<>()

Modern diamond operator kullanımı.

JAVABox<String> box = new Box<>();

Sağ taraftaki tip sol taraftan çıkarılabilir. Buna type inference denir. Compiler diamond içindeki T’nin String olması gerektiğini anlar.

JAVAvar first = new Box<String>();

// Tip bilgisi olmadığı için çoğunlukla Box<Object>
var second = new Box<>();
Tip bilgisinin nereden geldiğine dikkat et. Box<String> first = new Box<>(); ve var second = new Box<String>(); açık ve güvenlidir.

05 Raw type nedir?

Generic bir sınıfı tip parametresi vermeden kullanırsak raw type oluşur. Bu kullanım geriye dönük uyumluluk için vardır; yeni kodda kullanılmamalıdır.

JAVABox box = new Box();

box.set("Java");
box.set(42);
box.set(new User("Ali"));

// Compiler: unchecked call uyarısı

Doğrusu:

JAVABox<String> box = new Box<>();
Raw type gördüğünde generics’in compile-time güvenliğinin devre dışı bırakılmış olabileceğini düşün.

06 Generic tiplerde primitive kullanılamaz

JAVABox<int> box = new Box<>();
// Compile error

Generics yalnızca reference type’larla çalışır. Primitive yerine wrapper class kullanılır.

JAVABox<Integer> integerBox = new Box<>();
Box<Double> doubleBox = new Box<>();
Box<Boolean> booleanBox = new Box<>();

Autoboxing ve unboxing

JAVABox<Integer> box = new Box<>();

box.set(10);
// Compiler: box.set(Integer.valueOf(10));

int number = box.get();
// Integer → int unboxing

Wrapper null olduğunda unboxing yapılamaz:

JAVABox<Integer> box = new Box<>();

Integer wrapper = box.get(); // null
int primitive = box.get();   // NullPointerException

07 Generic method

Sınıfın tamamını generic yapmak zorunda değiliz. Tek bir metot kendi tip parametresine sahip olabilir.

JAVApublic class Printer {

    public static <T> void print(T value) {
        System.out.println(value);
    }
}
JAVAPrinter.print("Java");
Printer.print(42);
Printer.print(new User("Ali"));
Generic method bildiriminde <T>, return type’tan önce yazılır: public static <T> void print(...).
JAVApublic static <T> T first(T first, T second) {
    return first;
}

String result = first("Java", "Spring");
Integer number = first(10, 20);

Compiler parametrelere ve beklenen dönüş tipine bakarak T’yi çıkarır. İstersek tipi açıkça verebiliriz ancak çoğunlukla gerekmez.

JAVAString result = Printer.<String>first(
        "Java",
        "Spring"
);

08 Generic class ile generic method farkı

Generic class

JAVApublic class Box<T> {

    private T value;

    public void set(T value) {
        this.value = value;
    }
}

Box<String> box = new Box<>();

T, nesne oluşturulurken belirlenir. Bu box için T, nesnenin kullanım ömrü boyunca String’dir.

Generic method

JAVApublic static <T> T identity(T value) {
    return value;
}

String text = identity("Java"); // T = String
Integer number = identity(42);  // T = Integer
Generic class

Tip, nesnenin kullanım ömrü boyunca belirlenir.

Generic method

Tip, her metot çağrısında yeniden belirlenir.

09 Static üyeler ve class type parameter

Şu kod geçersizdir:

JAVApublic class Box<T> {

    private static T value;
    // Compile error
}

Static alan sınıfa aittir, belirli bir nesneye ait değildir. Aynı sınıftan Box<String> veBox<Integer> oluşturulabildiğine göre tek bir static value’nun hangi tipe ait olacağı belirsizdir.

JAVApublic class Box<T> {

    public static T getSomething() {
        // Compile error
    }
}

Static metot kendi bağımsız generic parametresini tanımlayabilir:

JAVApublic class Box<T> {

    public static <U> U identity(U value) {
        return value;
    }
}

Buradaki U, sınıfın T parametresinden bağımsızdır.

10 Neden bounds gerekir?

JAVApublic static <T> double doubleValue(T value) {
    return value.doubleValue();
    // Compile error
}

Compiler T hakkında hiçbir şey bilmez. T; String, User, LocalDate veya Thread olabilir ve hepsinde doubleValue metodu yoktur. Compiler’a bir garanti vermek için bound kullanılır.

JAVApublic static <T extends Number>
double doubleValue(T value) {
    return value.doubleValue();
}
<T extends Number>, “T, Number veya Number’ın alt türlerinden biri olmalı” anlamına gelir.
JAVAdoubleValue(10);     // Integer
doubleValue(5.5);    // Double
doubleValue(100L);   // Long

doubleValue("Java");
// Compile error

11 Upper bound

<T extends Number> ile yazılan sınır upper bound olarak adlandırılır.

Object
  └── Number
       ├── Integer
       ├── Long
       ├── Double
       ├── Float
       ├── Short
       └── Byte

T, Number sınırının aşağısındaki tiplerden biri olabilir. Bound sayesinde metot içinde Number’ın metotları çağrılabilir.

JAVApublic static <T extends Number>
double calculate(T value) {
    double first = value.doubleValue();
    int second = value.intValue();

    return first + second;
}

Compiler value değişkeninin en azından Number olduğunu bilir.

12 Interface ile bound

extends yalnızca sınıflar için kullanılmaz. Interface bound için de extends yazılır.

JAVApublic static <T extends Comparable<T>>
T greater(T first, T second) {
    if (first.compareTo(second) >= 0) {
        return first;
    }

    return second;
}
JAVASystem.out.println(greater(10, 20));
// 20

System.out.println(greater("Java", "Spring"));
// Spring

Comparable garantisi sayesinde first.compareTo(second)çağrılabilir. Bound olmasaydı compiler compareTo metodunun varlığını bilemezdi.

Buradaki extends, kalıtım türünden bağımsız olarak “Bu tipe uyumlu olmalı” anlamına gelir.

13 Multiple bounds

Bir tipin hem bir sınıfı genişletmesini hem de interface’leri uygulamasını isteyebiliriz.

JAVApublic static
<T extends Number & Comparable<T>>
T max(T first, T second) {
    return first.compareTo(second) >= 0
            ? first
            : second;
}

Integer result = max(10, 20);

Multiple bound kuralları:

  • En fazla bir sınıf bound olabilir.
  • Sınıf varsa ilk sırada yazılmalıdır.
  • Sonrasında birden fazla interface gelebilir.
JAVA<T extends SomeClass
        & FirstInterface
        & SecondInterface>
<T extends Comparable<T> & Number>

Hatalı: class bound ilk sırada değil.

<T extends Number & Comparable<T>>

Doğru sıralama.

14 Generic bir Stack yazalım

JAVAimport java.util.ArrayList;
import java.util.EmptyStackException;
import java.util.List;

public class Stack<T> {

    private final List<T> elements =
            new ArrayList<>();

    public void push(T value) {
        elements.add(value);
    }

    public T pop() {
        if (elements.isEmpty()) {
            throw new EmptyStackException();
        }

        return elements.remove(
                elements.size() - 1
        );
    }

    public T peek() {
        if (elements.isEmpty()) {
            throw new EmptyStackException();
        }

        return elements.get(
                elements.size() - 1
        );
    }

    public boolean isEmpty() {
        return elements.isEmpty();
    }

    public int size() {
        return elements.size();
    }
}
JAVAStack<String> stack = new Stack<>();

stack.push("Java");
stack.push("Spring");

String top = stack.pop();
// Spring

Stack<Integer> numbers = new Stack<>();
numbers.push(10);
numbers.push(20);

int number = numbers.pop();
// 20

Aynı algoritma farklı tiplerle çalışır ancak tip güvenliği korunur.

15 Generics invariant’tır

Integer, Number’ın alt tipi olsa da List<Integer>,List<Number>ın alt tipi değildir.

JAVAList<Integer> integers = List.of(1, 2, 3);

List<Number> numbers = integers;
// Compile error

Neden izin verilmediğini düşünelim:

JAVAList<Integer> integers =
        new ArrayList<>();

List<Number> numbers = integers;

numbers.add(3.14);
// List<Integer> içine Double eklenmiş olurdu
Integer, Number’ın alt tipidir. AncakList<Integer>, List<Number>ın;Box<Integer> da Box<Number>ın alt tipi değildir.

Bu ihtiyaç wildcard ile çözülür: List<? extends Number>ve List<? super Integer>. Bu, roadmap’in dördüncü konusu olan Wildcards, PECS ve type erasure bölümünde ele alınacak.

16 Generics’in önemli kısıtlamaları

new T() oluşturamazsın

JAVApublic class Factory<T> {

    public T create() {
        return new T();
        // Compile error
    }
}

Runtime’da T’nin somut olarak ne olduğunu doğrudan bilemeyiz. Bir çözüm factory veya Supplier almaktır.

JAVAimport java.util.function.Supplier;

public class Factory<T> {

    private final Supplier<T> supplier;

    public Factory(Supplier<T> supplier) {
        this.supplier = supplier;
    }

    public T create() {
        return supplier.get();
    }
}

Factory<ArrayList<String>> factory =
        new Factory<>(ArrayList::new);

ArrayList<String> list = factory.create();

Generic array oluşturamazsın

JAVAT[] values = new T[10];
// Compile error

instanceof T kullanamazsın

JAVAif (value instanceof T) {
    // Compile error
}

T.class kullanamazsın

JAVAClass<T> type = T.class;
// Compile error
Bu kısıtlamaların ana sebebi type erasuredır. Bir sonraki derste ayrıntılı olarak ele alınacak.

17 Ne zaman hangisini kullanmalı?

Generic class kullan

JAVAclass Repository<T> {}
class Stack<T> {}
class Box<T> {}
class Cache<K, V> {}

Nesnenin genel davranışı belirli bir tipe bağlıysa sınıfı generic yap.

Generic method kullan

JAVAstatic <T> T first(List<T> values)
static <T> void swap(...)
static <T> T identity(T value)

Yalnızca tek bir işlem tipten bağımsız çalışacaksa metodu generic yap.

Bound kullan

JAVA<T extends Number>
<T extends Comparable<T>>

Metodun T üzerinde belirli operasyonları yapabilmesi gerekiyorsa sınır koy.

18 En önemli zihinsel model

Object kullanmak

Her şeyi kabul eder; tip bilgisi kaybolur, cast ve runtime hatası doğabilir.

Generic kullanmak

Tip kontrollüdür; bilgi korunur ve hata compile-time’da yakalanır.

Bound kullanmak

T serbest değildir; compiler hangi davranışlara sahip olduğunu bilir.

Generics kodu farklı tipler için yeniden kullanılabilir yapar; bounds ise bu tiplerin hangi yeteneklere sahip olması gerektiğini compiler’a anlatır.
Kontrol soruları

Zihinsel modeli kontrol et

Box<Number> = new Box<Integer> neden hatalı?

Box<Integer>, Box<Number>ın alt tipi değildir.

Generic method’da <T> neden gerekir?

Metodun kendi tip parametresini tanımlaması gerekir.

<T extends Number> neden kullanılır?

T üzerinde Number metotlarının çağrılabileceğini garanti eder.

List values = new ArrayList() neden kötüdür?

Raw type kullanımı compile-time tip güvenliğini kaybettirir.

Bu konunun pratiği

Basitten bounded generics’e ilerle

  1. Box<T>: set, get ve isEmpty
  2. Pair<K, V>: immutable iki değer
  3. Stack<T>: push, pop ve peek
  4. <T> T first(List<T>): generic method
  5. <T extends Number> double sum(List<T>)
  6. <T extends Comparable<T>> T max(T, T)