برنامه نویسی

آموزش جامع Generic در جاوا: مزایا، مثال‌ها و بهترین روش‌ها

مقدمه

Genericهای جاوا به شما اجازه می‌دهند کلاس‌ها، اینترفیس‌ها و متدهایی بنویسید که روی تایپی مشخص‌شده در زمان کامپایل عمل کنند—نه در زمان اجرا. Genericها که در جاوا ۵ (JSR 14، سپتامبر ۲۰۰۴) معرفی شدند، بررسی تایپ زمان-کامپایل را اضافه کرده و Castهای صریح و ریسک ClassCastException—which در کد Collection قبل از جاوا ۵ رایج بود—را حذف می‌کنند. کل فریم‌ورک Java Collections برای امنیت تایپ بازنویسی شد تا از Genericها استفاده کند.

این آموزش کلاس‌های Generic، متدها و اینترفیس‌ها، پارامترهای تایپ محدودشده (Bounded Type Parameter)، Wildcardها و Type Erasure را—to‌همراه روشهایی که کد Generic را امن و خوانا نگه می‌دارند—پوشش می‌دهد. مثال‌ها از سینتکس جاوا ۸+ استفاده کرده و همان‌طور که نشان داده شده کامپایل می‌شوند.

نکات کلیدی

  • Genericهای جاوا امنیت تایپ زمان-کامپایل فراهم می‌کنند؛ پس کامپایلر خطاهای تایپ را قبل از اجرای برنامه می‌گیرد—به‌جای شکست با ClassCastException در زمان اجرا.
  • Genericها در جاوا ۵ (JSR 14) اضافه شدند و فریم‌ورک Java Collections هم‌زمان بازنویسی شد تا از آن‌ها استفاده کند.
  • کلاس یا اینترفیس Generic، پارامترهای تایپ را در گیومه زاویه‌دار (<T>) تعریف می‌کند؛ متد Generic هم پارامترهای تایپِ خودش را—محدود به همان متد—تعریف می‌کند.
  • پارامترهای تایپ محدودشده (<T extends Number>) تایپهای پذیرفته‌شده را محدود می‌کنند؛ Boundهای متعدد (<T extends A & B>) حداکثر یک کلاس در فهرست اجازه می‌دهند.
  • Wildcardها انعطاف اضافه می‌کنند: ? extends T برای خواندن از ساختار (Producer)، ? super T برای نوشتن در ساختار (Consumer) و ? برای دسترسی نامحدود. این همان قاعده PECS است: Producer Extends, Consumer Super.
  • کامپایلر همه پارامترهای تایپ را حین کامپایل—from طریق Type Erasure—حذف می‌کند؛ پس Genericها هیچ سربار زمان-اجرایی ندارند و List<String> و List<Integer> یک کلاس زمان-اجرایی را شریک‌اند.
  • از Raw Type در کد جدید اجتناب کنید، PECS را روی انتخابهای Wildcard در امضاهای API عمومی اعمال کنید و از نامهای استاندارد تک-حرفیِ پارامتر تایپ (یعنی E، K، V، N، T) استفاده کنید.

Generic در جاوا

Genericها در جاوا ۵ اضافه شدند تا بررسی تایپ زمان-کامپایل را فراهم کرده و ریسک ClassCastException—which هنگام کار با کلاسهای Collection رایج بود—را حذف کنند. کل فریم‌ورک Collection برای امنیت تایپ بازنویسی شد. مثالهای زیر مسئله‌ای را که Genericها حل می‌کنند نشان می‌دهند:

List list = new ArrayList();
list.add("abc");
list.add(Integer.valueOf(5));
// مجاز: raw type هیچ بررسی تایپ زمان-کامپایلی انجام نمی‌دهد

for(Object obj : list){
 //type casting منجر به ClassCastException در زمان اجرا
    String str=(String) obj; 
}

این کد کامپایل می‌شود اما در زمان اجرا ClassCastException پرتاب می‌کند؛ چون یکی از عناصر Integer است در حالی که حلقه هر عنصر را به String تبدیل می‌کند. با Collection تایپ‌شده، همان خطا به شکست زمان-کامپایل تبدیل می‌شود:

List<String> list1 = new ArrayList<>();
// عملگر diamond (جاوا 7+): تایپ از سمت چپ استنباط می‌شود
list1.add("abc");
//list1.add(new Integer(5)); //خطای کامپایلر

for(String str : list1){
     //بدون نیاز به type casting، از ClassCastException اجتناب می‌شود
}

تعیین String به‌عنوان آرگومان تایپ هنگام ساخت list یعنی هر فراخوانی list1.add() با آرگومان غیر-String در زمان کامپایل شکست می‌خورد. هیچ Castی در حلقه لازم نیست—چون کامپایلر از قبل تضمین کرده هر عنصری String است؛ که ClassCastException را از منبع حذف می‌کند.

Generic در جاوا چیست؟

Genericهای جاوا به شما اجازه می‌دهند کلاس، اینترفیس یا متد منفردی تعریف کنید که روی تایپی مشخص‌شده توسط Caller در زمان کامپایل عمل کند. پارامتر تایپ به‌عنوان Placeholderای درون گیومه زاویه‌دار (<>) عمل می‌کند. کامپایلر تایپ واقعی را جایگزین کرده و سازگاری تایپ را در همه استفاده‌ها الزامی می‌کند.

چرا Genericها به جاوا اضافه شدند

قبل از جاوا ۵، Collectionها Untyped بودند. یک List می‌توانست هر Objectای را نگه دارد و بازیابی عنصر همیشه Cast صریحی لازم داشت. اگر Cast اشتباه بود، برنامه در زمان اجرا ClassCastException پرتاب می‌کرد—رده‌ای از باگ که گرفتنش از طریق تست دشوار بود. مثال امنیت Collection در ابتدای همین مقاله، مسئله را مستقیم نشان می‌دهد.

Genericها آن بررسی تایپ را به زمان کامپایل منتقل کردند. کامپایلر عدم تطابق تایپ را قبل از اجرای کد رد می‌کند؛ که دسته کاملی از خطاهای زمان-اجرا را حذف و Castهای دفاعی را در سراسر کد اپلیکیشن از بین می‌برد.

Genericها در زمان کامپایل چگونه کار می‌کنند

وقتی می‌نویسید List<String> names = new ArrayList<>()، کامپایلر String را به‌عنوان آرگومان تایپ ثبت می‌کند. هر فراخوانی names.add() که String پاس ندهد در زمان کامپایل رد می‌شود. پارامتر تایپ سپس از Bytecode پاک می‌شود (بخش Type Erasure را ببینید)؛ پس کلاس کامپایل‌شده از نظر Binary با کد قبل از جاوا ۵ (Raw Typeها) سازگار می‌ماند—even though برای اجرا همچنان به JVM جاوا ۵+ لازم دارد.

مزایای استفاده از Generic در جاوا

Genericها چهار مزیت مشخص ارائه می‌کنند: امنیت تایپ زمان-کامپایل، حذف Castهای صریح، استفاده مجدد کد بین تایپها و مقصود شفاف‌تر. مثال زیر دو مورد اول را مستقیم اثبات می‌کند. نسخه Raw-Type کامپایل می‌شود اما در زمان اجرا شکست می‌خورد؛ نسخه Generic همان شکست را به زمان کامپایل منتقل کرده و Cast را کاملاً حذف می‌کند:

import java.util.*;

public class GenericsBenefits {
    public static void main(String[] args) {
        // Raw type: کامپایل می‌شود، cast دستی لازم دارد، در زمان اجرا شکست می‌خورد
        List rawList = new ArrayList();
        rawList.add("hello");
        rawList.add(42);                      // کامپایلر اعتراضی نمی‌کند
        String raw = (String) rawList.get(1); // ClassCastException در زمان اجرا

        // تایپ Generic: add ناسازگار قبل از اجرای برنامه رد می‌شود
        List<String> typedList = new ArrayList<>();
        typedList.add("hello");
        // typedList.add(42);                 // خطای کامپایل: فوراً گرفته می‌شود
        String typed = typedList.get(0);      // بدون نیاز به cast
    }
}

در عمل، حذف Cast مهم‌تر از چیزی است که در نگاه اول به نظر می‌رسد: هر Castِ حذف‌شده، ClassCastExceptionای است که دیگر نمی‌تواند به پروداکشن برسد؛ و Boilerplateِ ذخیره‌شده در Codebase بزرگ انباشته می‌شود.

امنیت تایپ زمان-کامپایل

Genericها عدم تطابق تایپها را حین کامپایل می‌گیرند—نه در زمان اجرا؛ همان‌طور که خط کامنت‌شدهٔ typedList.add(42) در بالا نشان می‌دهد. خطا لحظه نوشته شدن در IDE ظاهر می‌شود؛ نه بعداً در Stack Trace پروداکشن.

حذف Castهای صریح

بدون Genericها، هر بازیابی Cast لازم دارد: String s = (String) list.get(0). با Genericها کامپایلر از قبل نوع عنصر را می‌داند؛ پس String s = list.get(0) کافی است. Cast کمتر یعنی جاهای کمتر که Cast اشتباه می‌تواند در آن پنهان شود.

استفاده مجدد کد بین تایپها

کلاس یا متد Generic منفردی با هر تایپ Referenceای کار می‌کند. منطق را یک‌بار می‌نویسید و کامپایلر امنیت تایپ را جداگانه برای هر استفاده عینی الزامی می‌کند؛ پس Box<String> و Box<Integer> یک پیاده‌سازی را شریک‌اند—بدون از دست دادن امنیت.

خوانایی و نگهداری‌پذیری بهبودیافته

تایپهای پارامتری مقصود را صریح می‌کنند. Map<String, List<Integer>> شکل دقیق داده را در یک نگاه بیان می‌کند؛ در حالی که Map خام، خواننده را وادار به ردیابی نحوه پر شدنش می‌کند تا بفهمد چه چیزی درون دارد.

کلاس Generic در جاوا

تعریف کلاس Generic با پارامتر تایپ

کلاس Generic یک یا چند پارامتر تایپ را در گیومه زاویه‌دار بعد از نام کلاس تعریف می‌کند. هر Instance از کلاس می‌تواند در نقطه ساخت، مستقل تایپ شود. برای درک مزیت، کانتینر سادهٔ غیر-Generic زیر را در نظر بگیرید:

package com.example.generics;

public class GenericsTypeOld {

    private Object t;

    public Object get() {
        return t;
    }

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

    public static void main(String[] args) {
        GenericsTypeOld type = new GenericsTypeOld();
        type.set("Sammy");
        String str = (String) type.get();
        // cast صریح لازم: مستعد خطا، می‌تواند ClassCastException پرتاب کند
    }
}

با این کلاس غیر-Generic، هر بازیابی Cast صریحی لازم دارد و هر Castِ تایپ-اشتباه، ClassCastException در زمان اجرا تولید می‌کند. نسخه Generic هر دو مشکل را حذف می‌کند:

package com.example.generics;

public class GenericsType<T> {

    private T t;

    public T get() {
        return this.t;
    }

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

    public static void main(String[] args) {
        GenericsType<String> type = new GenericsType<>();
        type.set("Sammy"); // معتبر

        GenericsType type1 = new GenericsType(); // raw type: هشدار کامپایلر
        type1.set("Sammy"); // معتبر
        type1.set(10);      // معتبر: autoboxing، اما بدون امنیت تایپ
    }
}

دقت کنید GenericsType<String> هنگام صدا زدن get() به Cast نیاز ندارد. اگر آرگومان تایپ را حذف کنید (متغیر type1)، کامپایلر هشدار Raw-Type صادر می‌کند. وقتی آرگومان تایپی فراهم نشده، تایپ Object می‌شود؛ که هر مقداری را مجاز دانسته و بازیابی را به Cast مجبور می‌کند—which همان ریسک ClassCastException را که Genericها برای حذفش طراحی شده بودند، دوباره معرفی می‌کند.

استفاده از چند پارامتر تایپ

کلاس Generic می‌تواند بیش از یک پارامتر تایپِ جدا شده با ویرگول تعریف کند. اینترفیس Map<K, V> در کتابخانه استاندارد جاوا، مثال کلاسیک است:

package com.example.generics;

// کلاس Generic با دو پارامتر تایپ
public class Pair<K, V> {

    private K key;
    private V value;

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

    public K getKey()   { return key; }
    public V getValue() { return value; }
}

مثال کلاس Generic: Pair تایپ‌شده

مثال زیر Pair را با دو تایپ عینی Instantiate می‌کند. کامپایلر هر تایپ را مستقل الزامی می‌کند:

public class PairDemo {
    public static void main(String[] args) {
        Pair<String, Integer> entry = new Pair<>("score", 42);

        String label = entry.getKey();   // بدون نیاز به cast
        int    score = entry.getValue(); // بدون نیاز به cast

        System.out.println(label + ": " + score);
    }
}

خروجی:

score: 42

نکته: می‌توانیم از انوتیشن @SuppressWarnings("rawtypes") برای سرکوب هشدار کامپایلر استفاده کنیم.

هم‌چنین دقت کنید که از autoboxing جاوا پشتیبانی می‌کند.

اینترفیس Generic در جاوا

تعریف اینترفیس Generic

اینترفیس Generic، پارامترهای تایپ را درست مثل کلاس Generic تعریف می‌کند. اینترفیس Comparable<T> از کتابخانه استاندارد جاوا، مثال شناخته‌شده‌ای است:

package java.lang;

public interface Comparable<T> {
    public int compareTo(T o);
}

می‌توانید اینترفیسهای Generic خودتان را به همین شکل تعریف کنید. مثال زیر اینترفیس Repository<T> را برای قرارداد دسترسی-به-دادهٔ ساده‌ای تعریف می‌کند:

package com.example.generics;

import java.util.List;

// اینترفیس Genericِ پارامتری‌شده روی تایپ موجودیت
public interface Repository<T> {
    void save(T entity);
    T findById(int id);
    List<T> findAll();
}

پیاده‌سازی اینترفیس Generic با تایپ عینی

وقتی اینترفیس Genericای را با تایپ شناخته‌شده‌ای پیاده می‌کنید، پارامتر تایپ را با کلاس مشخصی جایگزین کنید:

package com.example.generics;

import java.util.ArrayList;
import java.util.List;

public class UserRepository implements Repository<String> {

    private final List<String> store = new ArrayList<>();

    @Override
    public void save(String entity) { store.add(entity); }

    @Override
    public String findById(int id) { return store.get(id); }

    @Override
    public List<String> findAll() { return store; }
}

پیاده‌سازی اینترفیس Generic با کلاس Generic

کلاس پیاده‌کننده می‌تواند Generic بمانده و پارامتر تایپ را عبور بدهد:

package com.example.generics;

import java.util.ArrayList;
import java.util.List;

// کلاس پیاده‌کننده هم Generic است
public class InMemoryRepository<T> implements Repository<T> {

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

    @Override
    public void save(T entity) { store.add(entity); }

    @Override
    public T findById(int id) { return store.get(id); }

    @Override
    public List<T> findAll() { return store; }
}

هم‌چنین می‌توانید چند پارامتر تایپ تعریف کنید—مثل Map<K, V>—و مقادیر پارامتری‌شده به تایپهای پارامتری بدهید: new HashMap<String, List<String>>() معتبر است.

قراردادهای نام‌گذاری پارامتر تایپ

نامهای پارامتر تایپ طبق قرارداد، حروف بزرگ منفردی‌اند؛ که آن‌ها را از نظر بصری از نامهای کلاس و متغیر متمایز می‌کند. پیروی از این قراردادها، کد Generic را با کتابخانه استاندارد و APIهای شخص ثالث سازگار نگه می‌دارد.

پارامتر تایپمعنیاستفاده رایج
TType (تایپ)پارامتر تایپ همه-منظوره
EElement (عنصر)Collectionها: ArrayList<E>، Set<E>
KKey (کلید)کلیدهای Map: Map<K, V>
VValue (مقدار)مقادیر Map: Map<K, V>
NNumber (عدد)عملیات عددی
RResult (نتیجه)تایپهای بازگشتی تابع: Function<T, R>
S، U، Wتایپهای اضافیپارامترهای ۲م، ۳م و ۴م وقتی T در Scope است

متد Generic در جاوا

سینتکس تعریف متد Generic

متد Generic، پارامتر تایپ خودش را قبل از تایپ بازگشتی در امضای متد تعریف می‌کند. پارامتر تایپ فقط به همان متد محدود است—فارغ از اینکه کلاس احاطه‌کننده خودش Generic باشد یا نه. چون Constructor نوع خاصی از متد است، Constructorها هم می‌توانند پارامتر تایپ تعریف کنند.

شکل کلی این است:

public static <T> ReturnType methodName(T param) { ... }

متد Generic در مقابل کلاس Generic

بُعدکلاس Genericمتد Generic
Scope پارامتر تایپکل کلاسمتد منفرد
مورد استفادهشیء کانتینری تایپ‌شدهیک عملیات تایپ-متجاهل
Instantiate شدنهنگام ساخت شیءبه‌ازای هر فراخوانی استنباط می‌شود

مثال متد Generic: جابه‌جایی عناصر آرایه

متد Generic زیر دو عنصر را در هر آرایه تایپ‌شده‌ای جابه‌جا می‌کند. تایپ T از آرگومان آرایه استنباط می‌شود؛ هیچ تعیین تایپ صریحی از Caller لازم نیست:

package com.example.generics;

import java.util.Arrays;

public class ArrayUtils {

    // T در محل فراخوانی از آرگومان آرایه استنباط می‌شود
    public static <T> void swap(T[] array, int i, int j) {
        T temp   = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    public static void main(String[] args) {
        String[] words = {"alpha", "beta", "gamma"};
        System.out.println("Before: " + Arrays.toString(words));

        swap(words, 0, 2); // T به‌عنوان String استنباط می‌شود

        System.out.println("After:  " + Arrays.toString(words));
    }
}

خروجی:

Before: [alpha, beta, gamma]
After:  [gamma, beta, alpha]

اینجا مثال اضافی‌ای با الگوی isEqual آمده—که هم تعیین تایپ صریح و هم استنباط تایپ را نمایش می‌دهد:

package com.example.generics;

public class GenericsMethods {

    // متد Generic: T قبل از تایپ بازگشتی تعریف شده
    public static <T> boolean isEqual(GenericsType<T> g1, GenericsType<T> g2) {
        return java.util.Objects.equals(g1.get(), g2.get());
    }

    public static void main(String[] args) {
        GenericsType<String> g1 = new GenericsType<>();
        g1.set("Sammy");

        GenericsType<String> g2 = new GenericsType<>();
        g2.set("Sammy");

        // type witness صریح
        boolean isEqual = GenericsMethods.<String>isEqual(g1, g2);
        // استنباط تایپ: ترجیح‌داده‌شده
        isEqual = GenericsMethods.isEqual(g1, g2);
    }
}

Constructorها هم از همان سینتکس پیروی می‌کنند. Constructor Generic، پارامتر تایپ خودش را قبل از نام کلاس در امضا تعریف می‌کند—مستقل از هر پارامتر تایپی روی کلاس احاطه‌کننده:

public class Wrapper {
    private final Object value;

    // Constructor Generic: T را از آرگومان استنباط، به‌عنوان Object ذخیره می‌کند
    public <T> Wrapper(T value) {
        this.value = value;
    }

    public Object get() { return value; }
}

<String> در GenericsMethods.<String>isEqual(g1, g2) یک Type Witness صریح است. فراخوانی دوم آن را کاملاً حذف کرده و به استنباط تایپ تکیه می‌کند: کامپایلر T را از تایپهای g1 و g2 استنباط می‌کند. استنباط تایپ برای فراخوانی متدهای Generic از جاوا ۵ موجود بوده و Releaseهای بعدی استنباط را بیشتر بهبود دادند؛ پس حذف Type Witness صریح، سبک ترجیح‌داده‌شده است.

پارامترهای تایپ محدودشده (Bounded Type Parameters)

پارامترهای تایپ محدودشده، اینکه کدام تایپهای عینی می‌توانند جایگزین پارامتر تایپ شوند را محدود می‌کنند.

پارامترهای تایپ با Bound بالایی (extends)

از extends برای محدود کردن پارامتر تایپ به کلاس یا اینترفیس مشخصی و همه زیرتایپهایش استفاده کنید. متد زیر هر تایپی را می‌پذیرد که Comparable<T> را پیاده کند—و تضمین می‌کند compareTo موجود است:

public static <T extends Comparable<T>> int compare(T t1, T t2) {
    return t1.compareTo(t2);
}

اگر تایپی را پاس بدهید که Comparable را پیاده نکرده، کامپایلر فراخوانی را رد می‌کند. پارامترهای تایپ محدودشده به‌طور مساوی روی کلاسها، اینترفیسها و متدها اعمال می‌شوند.

پارامترهای تایپ با Bound پایینی (super)

جاوا اجازه Bound پایینی روی تعریف پارامتر تایپ نمی‌دهد: <T super Number> سینتکس معتبری نیست. تعریف پارامتر تایپ فقط Bound بالایی (extends) را پشتیبانی می‌کند. وقتی «این تایپ یا هر Supertypeهایش» لازم دارید، از پارامتر Wildcard یعنی ? super T استفاده کنید—that در بخش Wildcardها پوشش داده شده.

Boundهای متعدد

پارامتر تایپ می‌تواند چند اینترفیس و حداکثر یک کلاس را Extend کند. کلاس باید اول ظاهر شود:

// T باید زیرتایپ Number باشد که هم‌زمان Comparable را پیاده کند
public static <T extends Number & Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}

در <T extends A & B & C>، اگر A کلاس باشد، B و C باید اینترفیس باشند. بیش از یک کلاس در فهرست Bound مجاز نیست.

Genericها و ارث‌بری

تایپهای Generic، رابطه زیرتایپِ آرگومانهای تایپ خود را به ارث نمی‌برند. حتی though String زیرتایپ Object است، MyClass<String> زیرتایپ MyClass<Object> نیست؛ این دو تایپ پارامتری‌شده نامرتبط‌اند و کامپایلر انتساب بین آن‌ها را رد می‌کند. این عمدی است. اگر انتساب مجاز بود، کد زیر کامپایل می‌شد اما امنیت تایپ را می‌شکست:

package com.example.generics;

public class GenericsInheritance {

    public static void main(String[] args) {
        String str = "abc";
        Object obj = new Object();
        obj = str; // معتبر: String زیرتایپ Object است

        MyClass<String> myClass1 = new MyClass<>();
        MyClass<Object> myClass2 = new MyClass<>();
        // myClass2 = myClass1; // خطای کامپایل: MyClass<String> یک MyClass<Object> نیست
        obj = myClass1;         // معتبر: MyClass<T> خودش زیرتایپ Object است
    }

    public static class MyClass<T> {}
}

MyClass<String> همچنان شیء معمولی‌ای از کلاس MyClass است؛ پس می‌تواند به Object انتساب شود. کاری که نمی‌تواند بکند این است که جای MyClass<Object> بایستد. این را با آرایههای جاوا مقایسه کنید—which Covariant هستند (String[] به Object[] قابل انتساب است). همان Covariance دقیقاً دلیل این است که آرایهها می‌توانند در زمان اجرا ArrayStoreException پرتاب کنند. Genericها با Invariant بودن، عمداً از این اجتناب می‌کنند. وقتی به انعطاف بین آرگومانهای تایپ مرتبط نیاز دارید، به‌جای تلاش برای گستر دادن آرگومان تایپ، از Wildcard محدودشده استفاده کنید:

// بیش‌ازحد محدود: Callerهای MyClass<String> و MyClass<Integer> را رد می‌کند
// public static void process(MyClass<Object> obj) { }

// کامپایل می‌شود: MyClass<String>، MyClass<Integer>، هر MyClass<?>ای را می‌پذیرد
public static void process(MyClass<?> obj) { }

کلاسهای Generic و Subtyping

رابطه زیرتایپ بین تایپهای پارامتری‌شده را با Extend یا پیاده کردن تایپ Generic—هم‌زمان با ثابت نگه داشتن آرگومان تایپ—ایجاد می‌کنید. چون ArrayList<E> اینترفیس List<E> را پیاده می‌کند—which Collection<E> را Extend می‌کند، ArrayList<String> زیرتایپ List<String> است؛ که خودش زیرتایپ Collection<String> است. رابطه تا وقتی برقرار است که آرگومان تایپ تغییر نکند.

// MyList<E, T> برای E ثابت، زیرتایپ List<E> است
interface MyList<E, T> extends List<E> {
}

هم MyList<String, Object> و هم MyList<String, Integer> زیرتایپ List<String> هستند؛ چون آرگومان تایپ اول یعنی E، String می‌ماند.

Wildcardها در Generic جاوا

علامت سوال (?)، کاراکتر Wildcard در Genericهای جاواست. این کاراکتر تایپی ناشناخته را نمایندگی می‌کند. Wildcardها می‌توانند به‌عنوان تایپهای پارامتر متد، تایپهای فیلد یا تایپهای متغیر محلی ظاهر شوند. هنگام فراخوانی متد Generic یا Instantiate کردن کلاس Generic نمی‌توانید از Wildcardها استفاده کنید.

Wildcardنامخواندننوشتنمورد استفاده معمول
?نامحدودبله، به‌عنوان Objectنه—به‌جز nullچاپ/شمارش هر تایپ
? extends TBound بالاییبله، به‌عنوان Tنه—به‌جز nullProducer
? super TBound پایینیبله، به‌عنوان Objectبلهنوشتن (Consumer)

قاعده PECS: وقتی متدی فقط از ساختار پارامتری‌شده می‌خواند (مقادیری از تایپ T تولید می‌کند) از ? extends T استفاده کنید. وقتی متدی فقط در ساختار پارامتری‌شده می‌نویسد (مقادیری از تایپ T مصرف می‌کند) از ? super T استفاده کنید. اگر متد هر دو را لازم دارد، به‌جای Wildcard از پارامتر تایپ صریح <T> استفاده کنید.

Wildcard با Bound بالایی (? extends T)

از ? extends T وقتی استفاده کنید که متدی فقط لازم است از Collection بخواند و باید هر زیرتایپِ T را بپذیرد. بدون آن، متدی که به List<Number> تایپ شده، List<Integer> و List<Double> را در محل فراخوانی رد می‌کند—even though هر دو مقادیر Number معتبر نگه می‌دارند. تلاش اول ممکن است پارامتر را List<Number> تایپ کند:

public static double sum(List<Number> list){
  double sum = 0;
  for(Number n : list){
   sum += n.doubleValue();
  }
  return sum;
 }

بدون Wildcard، List<Number> یعنی List<Integer> و List<Double> رد می‌شوند—چون تایپهای Generic Invariant هستند. Wildcard آن انعطاف را برای خواندن بازیابی می‌کند. نسخه اصلاح‌شده:

package com.example.generics;

import java.util.ArrayList;
import java.util.List;

public class GenericsWildcards {

 public static void main(String[] args) {
  List<Integer> ints = new ArrayList<>();
  ints.add(3); ints.add(5); ints.add(10);
  double sum = sum(ints);
  System.out.println("Sum of ints="+sum);
 }

 public static double sum(List<? extends Number> list){
  double sum = 0;
  for(Number n : list){
   sum += n.doubleValue();
  }
  return sum;
 }
}

خروجی:

Sum of ints=18.0

درون sum، هر عنصر به‌عنوان Number قابل استفاده است؛ پس doubleValue() روی هر کدام موجود است. نمی‌توانید—to‌جز null—چیزی به List<? extends Number> اضافه کنید. کامپایلر می‌داند لیست یکی از زیرتایپهای مشخصِ Number را نگه می‌دارد اما نمی‌داند کدام؛ پس نمی‌تواند اجازه دهد Integerای در چیزی که ممکن است List<Double> باشد درج شود. همین محدودیت فقط-خواندنی دقیقاً دلیل این است که ? extends در PECS نشانه Producer است.

Wildcard نامحدود (?)

از ? وقتی استفاده کنید که متد روی عناصر فقط—from طریق متدهای Object (مانند toString یا equals)—عمل می‌کند و لازم نیست تایپ عنصر مشخص را بداند. این معادل ? extends Object است:

 public static void printData(List<?> list){
  for(Object obj : list){
   System.out.print(obj + "::");
  }
 }

printData یعنی List<String>، List<Integer> یا هر لیست دیگری را می‌پذیرد. مثل ? extends T، نمی‌توانید—to‌جز null—چیزی اضافه کنید.

نقطه سردرگمی رایج، List<?> در مقابل List<Object> است. List<Object> فقط لیستی که List<Object> تعریف شده را می‌پذیرد و List<String> را رد می‌کند—چون تایپهای Generic Invariant هستند. List<?> لیستی با هر تایپ عنصری را می‌پذیرد اما عناصرش را به‌عنوان مقدارهای Objectِ فقط-خواندنی رفتار می‌کند. از List<?> برای پذیرش هر لیست تایپ‌شده‌ای در زمینه فقط-خواندنی استفاده کنید؛ از List<Object> فقط وقتی که مشخصاً لیست قابل-نوشتنِ اشیاء دلخواه لازم دارید.

// List<Object> فقط List<Object> را می‌پذیرد
public static void printObjects(List<Object> list) { }
// printObjects(new ArrayList<String>());
// خطای کامپایل: List<String> یک List<Object> نیست

// List<?> هر لیست پارامتری‌شده‌ای را می‌پذیرد
public static void printAny(List<?> list) { }
// printAny(new ArrayList<String>()); -- OK: کامپایل و اجرا می‌شود

Wildcard با Bound پایینی (? super T)

از ? super T وقتی استفاده کنید که متدی فقط در Collection می‌نویسد و باید هر لیستی را بپذیرد که به‌طور قانونی می‌تواند Tای را نگه دارد. پارامتر List<Integer> فقط Callerهای List<Integer> را می‌پذیرد. پارامتر List<? super Integer> یعنی List<Integer>، List<Number> و List<Object> را می‌پذیرد؛ چون هر سه می‌توانند مقدار Integer نگه دارند:

public static void addIntegers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    list.add(3);
}

نوشتنها امن‌اند؛ چون هر لیستِ تایپ‌شده به ? super Integer تضمین‌شده Integer را می‌پذیرد. همچنان می‌توانید از لیست بخوانید، اما عناصر به‌عنوان Object برمی‌گردند—چون کامپایلر فقط می‌داند تایپ عنصر یکی از Supertypeهای Integer است. همین رفتارِ قابل-نوشتن/خواندنی-به‌عنوان-Object دلیل این است که ? super در PECS نشانه Consumer است.

Wildcardها در عمل: Producer و Consumer با هم

PECS وقتی ارزشش را اثبات می‌کند که متدی از ساختاری می‌خواند و در ساختار دیگری می‌نویسد. Collections.copy در کتابخانه استاندارد، مورد کلاسیک است: از منبعی می‌خواند (Producer یعنی ? extends T) و در مقصدی می‌نویسد (Consumer یعنی ? super T):

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (int i = 0; i < src.size(); i++) {
        dest.set(i, src.get(i)); // خواندن T از src، نوشتن T در dest
    }
}

این، مقصد List<Object> با منبع List<Integer> را می‌پذیرد؛ چون Object یک Supertype و Integer یک زیرتایپ از تایپ عنصر T است. هیچ‌کدام از پارامترها نمی‌توانست List<T> ساده‌ای باشد—بدون مجبور کردن هر دو لیست به تایپ عنصر یکسان. یک محدودیت اعمال می‌شود: dest باید از قبل حداقل به‌اندازه src عنصر داشته باشد؛ چون List.set() وجود Index را لازم دارد. پاس دادن ArrayList خالی به‌عنوان dest، IndexOutOfBoundsException پرتاب می‌کند. Collections.copy واقعی همین الزام را دارد.

چه زمانی از Wildcard در مقابل پارامتر تایپ استفاده کنیم

از Wildcard محدودشده وقتی استفاده کنید که پارامری مستقل از بقیه پارامترها و از تایپ بازگشتی است. از پارامتر تایپ صریح <T> وقتی استفاده کنید که متد باید رابطه‌ای بین آرگومانها یا بین آرگومان و مقدار بازگشتی را الزامی کند:

// <T> لازم دارد: تایپ بازگشتی به تایپ عنصر گره خورده
public static <T> T firstElement(List<T> list) {
    return list.get(0);
}

اگر firstElement از List<?> استفاده می‌کرد، فقط می‌توانست Object برگرداند و هر Caller را به Cast مجبور کند. پارامتر تایپ صریح، تایپ عنصر را در سراسر امضای متد حفظ می‌کند.

Wildcard Capture

امضای Wildcardِ خالی نمی‌تواند عنصری را در-جای خودش تغییر دهد؛ چون کامپایلر نامی برای تایپ Capture-شده ندارد. راه‌حل، Helper خصوصیِ Genericای است که Wildcard را در پارامتر تایپِ نام‌دار Capture می‌کند:

public static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j); // wildcard درون helper به‌عنوان T capture می‌شود
}

private static <T> void swapHelper(List<T> list, int i, int j) {
    T temp = list.get(i);
    list.set(i, list.get(j));
    list.set(j, temp);
}

کامپایلر در محل فراخوانی، تایپ عینی اما بی‌نامی را برای T استنباط می‌کند—فرایندی که Wildcard Capture نامیده می‌شود. متد عمومی، امضای انعطاف‌پذیر List<?> را افشا می‌کند؛ Helper خصوصی، پارامتر تایپِ نام‌داری را که عملیات swap لازم دارد فراهم می‌کند.

Subtyping با Wildcard مربوط به Genericها

تایپهای پارامتری‌شدهٔ Wildcard، سلسله‌مراتب زیرتایپِ خودشان را تشکیل می‌دهند. چون Integer زیرتایپ Number است، List<? extends Integer> زیرتایپ List<? extends Number> است. این، انعطاف Wildcard را در طول سلسله‌مراتب تایپ گسترش می‌دهد—بدون نیاز به مرتبط بودن پارامتری‌سازیهای خام:

List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList;
// OK: List<? extends Integer> زیرتایپ List<? extends Number> است

Type Erasure در Genericهای جاوا

کامپایلر در زمان اجرا چگونه با پارامترهای تایپ رفتار می‌کند

Type Erasure، سازوکاری است که طی آن کامپایلر جاوا همه اطلاعات تایپ Generic را قبل از تولید Bytecode حذف می‌کند. این تصمیم طراحی عمدی برای حفظ سازگاری Binary با فایلهای کلاسِ قبل از جاوا ۵ بود. در زمان اجرا، List<String> و List<Integer> هر دو به‌عنوان Raw Type یعنی List نمایش داده می‌شوند.

حین کامپایل، کامپایلر:

۱. هر پارامتر تایپ را با Boundش—یا اگر پارامتر نامحدود است با Object—جایگزین می‌کند.
۲. در صورت نیاز، Castهایی برای حفظ امنیت تایپ—from دید Caller—درج می‌کند.
۳. متدهای Bridge تولید می‌کند تا Polymorphism در زیرکلاسها حفظ شود (بالا را ببینید).

هیچ کلاس جدیدی برای تایپهای پارامتری‌شده ساخته نمی‌شود؛ پس Genericها هیچ سربار حافظه یا Class-Loading زمان-اجرایی ندارند. مثلاً، با این کلاس Generic:

public class Test<T extends Comparable<T>> {

    private T data;
    private Test<T> next;

    public Test(T d, Test<T> n) {
        this.data = d;
        this.next = n;
    }

    public T getData() { return this.data; }
}

کامپایلر جاوا پارامتر تایپ محدودشدهٔ T را با اولین اینترفیس Bound یعنی Comparable جایگزین کرده و این را تولید می‌کند:

public class Test {

    private Comparable data;
    private Test next;

    public Test(Comparable d, Test n) {
        this.data = d;
        this.next = n;
    }

    public Comparable getData() { return data; }
}

متدهای Bridge

وقتی کلاس Genericای Extend شود یا اینترفیس Genericای پیاده شود، کامپایلر می‌تواند متد Syntheticای که Bridge Method نامیده می‌شود را برای حفظ Polymorphism بعد از Erasure تولید کند. کلاسی را در نظر بگیرید که Comparable<Node> را پیاده می‌کند:

public class Node implements Comparable<Node> {
    public int compareTo(Node other) {
        return 0; // منطق مقایسه
    }
}

بعد از Erasure، Comparable متد compareTo(Object) را تعریف می‌کند. برای معتبر ماندن Override، کامپایلر متد Bridgeای درج می‌کند که به نسخه تایپ‌شده تفویض می‌کند:

// متد bridge ساختگیِ تولیدشده توسط کامپایلر، هرگز دستی نوشته نمی‌شود
public int compareTo(Object other) {
    return compareTo((Node) other);
}

متدهای Bridge در سورس کد نامرئی اما در Bytecode کامپایل‌شده ظاهر شده و با javap -verbose ClassName قابل مشاهده‌اند.

پیامدهای عملی Type Erasure

  • چکهای instanceof روی تایپهای پارامتری‌شده کامپایل نمی‌شوند: obj instanceof List<String> خطای کامپایل است.
  • نمی‌توانید پارامتر تایپ را Instantiate کنید: new T() کامپایل نمی‌شود.
  • نمی‌توانید آرایه‌ای از تایپ پارامتری‌شده بسازید: new T[10] کامپایل نمی‌شود.
  • Overloadهایی که فقط بر اساس آرگومان تایپ تفاوت دارند، حین Erasure تصادم می‌کنند: process(List<String>) و process(List<Integer>) هر دو به process(List) تبدیل شده و نمی‌توانند در همان کلاس هم‌زیستی کنند.
  • فیلدهای Static بین همه پارامتری‌سازی‌ها مشترک‌اند؛ چون فقط یک کلاس در زمان اجرا وجود دارد.

برخلاف C# و Kotlin—which از Genericهای Reified استفاده می‌کنند (آرگومانهای تایپ در زمان اجرا حفظ و از طریق Reflection در دسترس‌اند)، جاوا از Erasure استفاده می‌کند: آرگومانهای تایپ در زمان کامپایل حذف شده و روی Instanceها در دسترس نیستند. Project Valhalla تلاش جاری برای افزودن Genericهای تخصصی روی Value Typeها به جاواست—which اجازه می‌دهد Genericها بدون Boxing روی Primitiveها عمل کرده و برخی محدودیتهای فعلی Erasure را کاهش بدهند. تا وقتی عرضه شود، Erasure و محدودیتهایش مدلِ طراحی باقی می‌مانند.

Genericها با Collectionهای جاوا

استفاده از Genericها با List، Set و Map

فریم‌ورک Java Collections کاملاً روی Genericها بنا شده. مثال زیر استفاده امن-از-نظر-تایپ از سه اینترفیس اصلی Collection را—بدون هیچ Cast صریحی—نشان می‌دهد:

import java.util.*;

public class CollectionsDemo {
    public static void main(String[] args) {

        // List: دنباله مرتب، اجازه تکرار
        List<String> names = new ArrayList<>();
        names.add("Alice");
        names.add("Bob");

        // Set: نامرتب، بدون تکرار
        Set<Integer> ids = new HashSet<>();
        ids.add(101);
        ids.add(102);

        // Map: جفتهای کلید-مقدار
        // LinkedHashMap ترتیب درج را برای خروجی پایدار حفظ می‌کند
        Map<String, Integer> scores = new LinkedHashMap<>();
        scores.put("Alice", 95);
        scores.put("Bob", 87);

        for (Map.Entry<String, Integer> entry : scores.entrySet()) {
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }
    }
}

خروجی:

Alice -> 95
Bob -> 87

مقایسه کنار-همِ Raw Type در مقابل تایپ Generic

بُعدRaw Type (یعنی List)تایپ Generic (یعنی List)
بررسی تایپ زمان-کامپایلهیچتوسط کامپایلر الزامی
Cast صریح هنگام بازیابی لازمبلهخیر
ریسک ClassCastExceptionبالا (زمان اجرا)حذف‌شده
هشدار کامپایلرunchecked / rawtypesهیچ
سازگاری عقبیکد قبل از جاوا ۵جاوا ۵ و بعدتر
توصیه‌شده در کد جدیدخیربله

Raw Typeها صرفاً برای سازگاری عقبی با کدهای نوشته‌شده قبل از جاوا ۵ وجود دارند. Java Language Specification صریحاً از استفاده‌شان در کد جدید منع می‌کند.

بهترین روشهای Generic در جاوا

در متدهای API عمومی از Wildcard محدودشده استفاده کنید

وقتی متدی می‌نویسید که پارامترش فقط لازم است از او خوانده شود، از ? extends T استفاده کنید. وقتی پارامتر فقط لازم است در او نوشته شود، از ? super T. این، دامنه Callerهایی که می‌توانند متد را استفاده کنند را—بدون فدا کردن امنیت تایپ—به حداکثر می‌رساند (قاعده PECS در عمل):

// Producer: از Numberها می‌خواند؛ پس List<Integer>، List<Double> و... را می‌پذیرد
public static double sum(List<? extends Number> list) {
    double total = 0;
    for (Number n : list) {
        total += n.doubleValue();
    }
    return total;
}

// Consumer: Integer می‌نویسد؛ پس List<Integer>، List<Number>، List<Object> را می‌پذیرد
public static void addIntegers(List<? super Integer> list) {
    list.add(42);
}

در کد جدید از Raw Type اجتناب کنید

هر استفاده از Raw Type، بررسی زمان-کامپایل را دور زده و ClassCastException بالقوه‌ای را از زمان کامپایل به زمان اجرا منتقل می‌کند. Raw Typeها را با تایپهای پارامتری‌شده یا Wildcard محدودشده جایگزین کنید:

List rawNames      = new ArrayList();    // اجتناب کنید
List<String> names = new ArrayList<>();  // ترجیح بدهید

از قراردادهای استاندارد نام‌گذاری پارامتر تایپ پیروی کنید

از حروف بزرگ منفرد (T، E، K، V، N) استفاده کنید تا پارامترهای تایپ از نظر بصری از نامهای کلاس متمایز بمانند.

تا جای امکان، متدهای Generic را به کلاسهای Generic ترجیح بدهید

اگر فقط یک متد در کلاسی به پارامتری‌سازی تایپ نیاز دارد، پارامتر تایپ را روی متد تعریف کنید—نه روی کل کلاس. این Scope تایپ را محدود و کلاس را ساده‌تر برای Instantiate نگه می‌دارد:

// ترجیح: پارامتر تایپ محدود به متد
public static <T extends Comparable<T>> T findMax(List<T> list) {
    return Collections.max(list);
}

از تایپهای Primitive به‌عنوان آرگومان تایپ استفاده نکنید

Genericهای جاوا فقط با تایپهای Reference کار می‌کنند. به‌جایش از کلاس Wrapper استفاده کنید:

// کامپایل نمی‌شود
// List<int> numbers = new ArrayList<>();

// درست: autoboxing به‌طور خودکار بین int و Integer تبدیل می‌کند
List<Integer> numbers = new ArrayList<>();

اشتباهات رایج با Genericها

حتی توسعه‌دهندگان باتجربه هنگام استفاده از Genericها اشتباه می‌کنند. اینجا برخی دامهای رایج آمده:

استفاده از Raw Type

استفاده از Raw Typeها هدف Genericها را شکست می‌دهد:

List list = new ArrayList(); // از این اجتناب کنید
list.add("Hello");
list.add(123); // بدون امنیت تایپ

به‌جایش از تایپ پارامتری‌شده استفاده کنید:

List<String> list = new ArrayList<>();

ترکیب Genericها با کد Legacy

پاس دادن Collection پارامتری‌شده به کدِ قبل از Generic—which از Raw Type استفاده می‌کند—هشدارهای Unchecked تولید کرده و می‌تواند به Heap Pollution منجر شود؛ جایی که List<String> در نهایت عنصری غیر-String نگه می‌دارد. خطا پنهان می‌ماند تا وقتی چیزی عنصر آلوده را بخواند و Castِ درج‌شدهٔ کامپایلر شکست بخورد:

public class LegacyDemo {

    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        addRaw(strings);            // List<String> را به پارامتر raw یعنی List پاس می‌دهد
        String s = strings.get(0); // ClassCastException در زمان اجرا
    }

    static void addRaw(List list) { // پارامتر raw: بدون بررسی تایپ
        list.add(42); // هشدار unchecked این‌جا: add خام، بررسی تایپ را دور می‌زند
    }
}

وقتی مجبورید APIهای Raw-Typeِ Legacy را صدا بزنید، آن‌ها را پشت Wrapper تایپ‌شدهٔ کوچکی ایزوله کرده و @SuppressWarnings("unchecked") را فقط بعد از تأیید—with بازرسی—اعمال کنید که فراخوانی واقعاً امن-از-نظر-تایپ است. دلیل را در محل سرکوب مستند کنید.

استفاده نادرست از Wildcardها

استفاده از Wildcard با Bound بالایی (? extends T) روی پارامتری که لازم است در Collection بنویسد، رایج‌ترین اشتباه Wildcard است. کامپایلر نوشتن را بلاک می‌کند؛ چون نمی‌تواند تأیید کند لیست در زمان اجرا کدام زیرتایپ مشخص را نگه می‌دارد:

public void addItem(List<? extends Number> list) {
    // list.add(10); // خطای کامپایل: ممکن است List<Double> باشد، نه List<Integer>
}

وقتی متد لازم است عنصر اضافه کند، به‌جایش از ? super T استفاده کنید:

public void addItem(List<? super Integer> list) {
    list.add(10);
}

استفاده بیش‌ازحد از Wildcardها

Wildcardها به پارامترهای متد تعلق دارند—نه تایپهای بازگشتی. متدی که List<? extends Number> برمی‌گرداند، هر Caller را مجبور به مدیریت Wildcard می‌کند؛ که محدودیت را به بیرون منتقل کرده و معمولاً Castهای بیشتری—نه کمتری—تولید می‌کند:

// اجتناب: تایپ بازگشتی wildcard، عدم قطعیت را به Callerها تحمیل می‌کند
public List<? extends Number> getNumbers() { ... }

// ترجیح: تایپ بازگشتی پارامتری‌شدهٔ عینی
public List<Integer> getNumbers() { ... }

از Wildcardها برای گستر دادنِ آنچه متد می‌پذیرد استفاده کنید؛ از تایپهای پارامتری‌شدهٔ عینی برای آنچه برمی‌گرداند. هم‌چنین از تودرتوی Wildcardها عمیق‌تر از یک سطح (List<? extends List<?>>) اجتناب کنید—مگر اینکه طراحی واقعاً لازمش داشته باشد. Wildcardهای تودرتو قانونی اما به‌ندرت ضروری‌اند و بار شناختیِ خواندن امضا را به‌طور قابل توجهی زیاد می‌کنند.

سوالات متداول

Generic در جاوا چیست؟

Genericها به شما اجازه می‌دهند کلاس، اینترفیس یا متدی با پارامتر تایپی تعریف کنید که کامپایلر آن را در زمان کامپایل با تایپ عینی جایگزین می‌کند. این امنیت تایپ را—بدون Cast صریح—فراهم کرده و ClassCastException زمان-اجرا را حذف می‌کند.

چرا از Generic در جاوا استفاده کنیم؟

Genericها امنیت تایپ زمان-کامپایل (گرفتن ClassCastException قبل از اجرای برنامه)، حذف Castهای صریح، امکان استفاده مجدد کد بین تایپها و ساختارهای پارامتریِ خود-مستند را فراهم می‌کنند.

می‌توانید مثالی از متد Generic بدهید؟

متد Generic می‌تواند پارامتر تایپ بگیرد:

public static <T> void printArray(T[] elements) {
    for (T element : elements) {
        System.out.println(element);
    }
}

این متد عناصر آرایه‌ای با هر تایپی را چاپ می‌کند.

List<?> در جاوا چه معنایی دارد؟

علامت ? در Genericهای جاوا Wildcard است و نمایندگی تایپی ناشناخته:

List<?> list = new ArrayList<String>();

یعنی لیست می‌تواند هر تایپی را نگه دارد، اما تایپ دقیقش در زمان کامپایل ناشناخته است.

انواع Wildcard در Genericهای جاوا چیستند؟

دو فرم محدودشده به‌همراه Wildcard نامحدود:

  • ? extends T: نمایندگی تایپی ناشناخته که زیرتایپ T است:
public void processElements(List<? extends Number> numbers) {
    for (Number num : numbers) {
        System.out.println(num);
    }
}
  • ? super T: نمایندگی تایپی ناشناخته که Superclassِ T است:
public void addNumbers(List<? super Integer> numbers) {
    numbers.add(42);
}

Wildcardها انعطاف فراهم کرده اما امنیت تایپ را حفظ می‌کنند.

آیا می‌توانم در جاوا آرایه‌ای از Generic بسازم؟

نه. ساخت آرایه Generic خطای زمان-کامپایل است: Type Erasure تایپ عنصری را که آرایه باید در زمان اجرا الزامی کند حذف می‌کند. به‌جایش از List<T> استفاده کنید:

List<String> list = new ArrayList<>();

چرا Genericها در جاوا امن‌اند؟

Genericها بررسی تایپ را از زمان اجرا به زمان کامپایل منتقل می‌کنند. کامپایلر الزامی می‌کند فقط مقادیرِ تایپ تعریف‌شده بتوانند به ساختار پارامتری‌شده اضافه یا از آن بازیابی شوند؛ که دسته ClassCastExceptionهایی که Raw Typeها اجازه می‌دهند را حذف می‌کند.

قوانین Generic در جاوا چیستند؟

  • نمی‌توان Instanceهای پارامتر تایپ را مستقیم ساخت:
class Container<T> {
    // T obj = new T(); // غیرقانونی: پارامتر تایپ قابل instantiation نیست
}
  • نمی‌توان فیلد Static از پارامتر تایپ Generic تعریف کرد:
class Holder<T> {
    // static T instance; // غیرقانونی: بین همه پارامتری‌سازی‌ها مشترک است
}
  • نمی‌توان از Primitiveها به‌عنوان پارامتر تایپ استفاده کرد (به‌جایش از کلاس Wrapper):
// List<int> list = new ArrayList<>(); // غیرقانونی
List<Integer> list = new ArrayList<>();
  • کلاس Generic نمی‌تواند Throwable را مستقیم یا غیرمستقیم Extend کند:
// class MyException<T> extends Exception { } // غیرقانونی

یعنی Exceptionهای Checked و Uncheckedِ Generic مجاز نیستند. می‌توانید از پارامتر تایپ در بند throws متد استفاده کنید، اما خود تعریف کلاس نمی‌تواند روی Throwable پارامتری شود.

Genericها کِی به جاوا معرفی شدند؟

جاوا ۵ (JSR 14، سپتامبر ۲۰۰۴). فریم‌ورک Java Collections هم‌زمان بازنویسی شد.

تفاوت ? extends T و ? super T چیست؟

? extends T لیستی از T یا هر زیرتایپش را می‌پذیرد؛ وقتی استفاده‌اش کنید که متد فقط می‌خواند (Producer). ? super T لیستی از T یا هر Supertypeاش را می‌پذیرد؛ وقتی استفاده‌اش کنید که متد فقط می‌نویسد (Consumer). يادآوری: PECS یعنی Producer Extends, Consumer Super.

تفاوت متد Generic و کلاس Generic چیست؟

کلاس Generic، تایپی را در زمان ساخت برای همه متدها و فیلدهایش ثابت می‌کند. متد Generic، پارامتر تایپ خودش را—محدود به همان متد، مستقل از کلاس احاطه‌کننده—تعریف می‌کند. وقتی فقط یک عملیات لازم دارد تایپ-متجاهل باشد، از متد Generic استفاده کنید.

Genericهای جاوا در مقایسه با C# چگونه‌اند؟

C# از Genericهای Reified استفاده می‌کند: آرگومانهای تایپ در زمان اجرا موجودند—which امکان new T() و Reflection یعنی typeof(T) زمان-اجرا را می‌دهد. جاوا از Type Erasure استفاده می‌کند: آرگومانهای تایپ در زمان کامپایل حذف می‌شوند؛ پس new T() و instanceof List<String> کامپایل نمی‌شوند. جاوا اطلاعات تایپ زمان-اجرا را با سازگاری عقبی با Bytecode قبل از جاوا ۵ معامله کرد.

نتیجه‌گیری

این آموزش Genericهای جاوا را از اصول اولیه تا استفاده پیشرفته پوشش داد. کاوش کردید که کلاسها، متدها و اینترفیسهای Generic چگونه تعریف و پارامتری می‌شوند؛ پارامترهای تایپ محدودشده چگونه با extends و super تایپهای قابل قبول را محدود می‌کنند؛ Wildcardها و قاعده PECS چگونه طراحی API را هدایت می‌کنند؛ و Type Erasure چگونه در سطح Bytecode—from جمله نقش متدهای Bridge—کار می‌کند. هم‌چنین تایپهای Generic را با Raw Typeها مقایسه کرده و روشهایی که کد Generic را امن و خوانا نگه می‌دارند را مرور کردید.

حالا می‌توانید کلاسها و اینترفیسهای Generic با پارامترهای تایپ منفرد و متعدد بنویسید، متدهای Generic با تایپهای استنباط‌شده توسط کامپایلر تعریف کنید، Boundهای بالایی و پایینی را برای محدود کردن تایپهای پذیرفته‌شده اعمال کنید، بین Wildcardها و پارامترهای تایپ صریح در متدهای API عمومی انتخاب کنید؛ و محدودیتهای عملی تحمیل‌شده توسط Type Erasure را بشناسید.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا