جنریک (Generic) ها در جاوا

جنریک (Generic) ها در جاوا

تعریف جنریک (Generic)

جنریک (Generic) ها باعث میشن خطا ها بجای زمان اجرا در زمان کامپایل دیده بشن.

فرض کنید یک فیلد میخوایم برای کلاس تعریف کنیم و بستگی به جایی که میخوایم از کلاس استفاده کنیم، گاهی این فیلد باید Integer باشه گاهی String و گاهی یک کلاس دیگه.

میتونیم کلاسمونو برای نوع (type) های مختلفی که نیاز داریم، چند بار باز طراحی کنیم، اما این کار کاملا وقت گیر، تکراری و خسته کننده میشه.

میتونیم فیلد رو به صورت Object تعریف کرده و بعدا بر اساس نیاز فیلد رو کست (cast) کنیم.

public class MyClass {

  private Object myField;

  public MyClass(Object myField){

    this.myField = myField;

  }

  public Object getMyField(){
    return this.myField();
  }

}
        

استفاده از آبجکت به عنوان فیلد فکر خوبیه ولی یه مشکل وجود داره! اگه فیلد رو اشتباه به یک نوع دیگه cast کنیم، زمان اجرا دچار خطای ClassCastException میشیم.

MyClass mc = new MyClass("This is a string field");
String v0 = (String) mc.getMyField();  //Correct
File f = (File) mc.getMyField(); //ClassCastException
        

هنگام استفاده از جنریک دیگه نیازی به کست کردن نداریم و خطاها زمان کامپایل مشخص میشن.

جنریک مثل یک قالب میمونه که میتونیم انواع مایعات رو داخلش بریزیم بدون اینکه فرم قالب تغییر کنه.

در واقع با Generic میتونیم نوعی که داخل کلاس یا متد (تابع) داریم استفاده میکنیم رو عمومی نگه داریم و زمان استفاده از کلاس یا متد نوعی (Type) که لازم داریم رو مشخص کنیم.

استفاده از جنریک (Generic) کد رو از خطاهای زمان اجرا ایمن میکنه و فقط زمان کامپایل مصرف داره، بعد از کامپایل کد توسط کامپایلر جاوا، جنریک ها پاک میشن و جای خودشونو به نوعی که برای کلاس مشخص کردیم میدن اصطلاحا بهش Type Ereasure میگیم.

مثالی که با Object نوشتیم رو میخوایم با Generic بنویسیم.

public class MyClass<E> {

  private E myField;

  public MyClass(E myField){

    this.myField = myField;

  }

  public E getMyField(){
    return this.myField();
  }

}
        

حالا میخوایم از کلاسمون استفاده کنیم.

MyClass<String> mc = new MyClass<>("This is a string field");
String v0 = mc.getMyField();  //Correct
File f =  mc.getMyField(); //Compile Error
        

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

هنگام استفاده از کلاس Generic در جاوا، نوعی که میخوایم برای جنریک تعیین کنیم باید آبجکت (Object Type) باشه و نوع Primitive نمیشه.

MyClass<int> mc = new MyClass<>(89); //Error
MyClass<Integer> mc1 = new MyClass<>(89) //Correct
        

کلاس های جنریک (Generic)

میتونیم یک کلاس رو به صورت جنریک تعریف کنیم. برای این کار کافیه بعد از نام کلاس یک حرف لاتین معمولا بزرگ رو به صورت <T> بنویسیم؛ سپس میتونیم فیلد ها، متغیر های داخل متد (تابع) ها، نوع مقداری که متد (تابع) بر میگردونه، پارامتر متدها و کانستراکتور ها رو از نوع T تعریف کنیم.

public class MyClass<T> {
  T myField;

  public MyClass(T t){

  }

  void myFirstMethod(T t){

    T myLocalVariable;

  }

  T mySecondMethod(T t){

    T myLocalVariable;

  }

}
        

بسیاری از کلاس ها در کتابخونه های استاندارد جاوا به صورت Generic تعریف شدن، مثل ArrayList که یکی از کلاس های پیاده سازی شده ی ساختار داده در جاواست.

در ArrayList چون نوع عناصر تا زمان استفاده از کلاس معلوم نیست، بنابراین با Generic پیاده سازی شده است.

در زیر دو کلاس ArrayList تعریف کردیم یکی برای File و یکی برای String.

ArrayList<String> strings = new ArrayList<>();
ArrayList<File> files = new ArrayList<>();
        

جنریک های نوع خام (Raw Types)

هنگام استفاده از کلاس جنریک اگه نوع جنریک رو مشخص نکنیم، کامپایلر جاوا به طور پیشفرض نوع جنریک رو Object در نظر میگیره؛ که بهشون جنریک های نوع خام (Raw Type) میگیم.

در زیر هردو ArrayList یکسان هستند.

ArrayList list = new ArrayList()
ArrayList<Object> list = new ArrayList<>();
          

اینترفیس های جنریک (Generic)

میتونیم اینترفیس ها رو به صورت جنریک (Generic) تعریف کنیم؛ مانند کلاس کافیه بعد از اسم اینترفیس یک حرف لاتین معمولا بزرگ رو به صورت <T> بزاریم؛ سپس میتونیم پارامتر و نوعی که متدهای (توابع) داخل اینترفیس برمیگردونن رو از نوع T تعریف کنیم.

public interface MyInterface<T> {
  
  void myFirstMethod(T t);

  T mySecondMethod(T t);

}
        

به عنوان مثال اینترفیس Comparable به صورت Generic در جاوا تعریف شده است، اینترفیس Comparable به صورت پیشفرض در اکثر کلاس های استاندارد جاوا پیاده سازی شده و هدفش مقایسه کردن آبجکت ایجاد شده از کلاس با یه آبجکت دیگه از نوع خودشه؛ این مقایسه با متد compareTo تعریف شده داخل اینترفیس صورت میگیره.

public interface Comparable<T> {

  int compareTo(T t);

}
        

متد (تابع) استاتیک جنریک (Generic)

میتونیم متد های استاتیک رو مستقل از کلاس به صورت جنریک تعریف کنیم، برای این کار یک حرف دلخواه از حروف بزرگ لاتین رو بعد از کلیدواژه ی static ، به صورت <T> تعریف میکنیم.

public static <T> myStaticMethod(T t){
  ...
}
        

مثال

میخوایم یک متد (تابع) جنریک (Generic) تعریف کنیم که عناصر آرایه ها رو نمایش بده.

public static void main(String[] args){
  
  Integer[] numbers = { 2, 5, 9, 4}

  String[] colors = {"Red", "Green", "Blue", "Orange"}

    System.out.println("Printing numbers...");
    printArray(numbers);

    System.out.println("Printing colors....");
    printArray(colors);

}
        
public static <E> void printArray(E[] elements){

  for(int i=0; i<elements.length; i++)
    System.out.print(elements[i] + " ");

    System.out.println();

}
        

نوع محدود شده (Bounded Type)

به طور پیشفرض جنریک هایی که با <T> تعریف میکنیم در واقع <T extends Object> هستند، یعنی هر نوعی که از Object ارث بری کنه رو میتونیم هنگام استفاده از کلاس یا متد براش تعیین کنیم.

میتونیم نوعمون رو محدود به یک کلاس خاص و ساب کلاس های اون کلاس کنیم تا هنگام استفاده از کلاس یا متد جنریک، به صورت مشخص شده تری بدونیم چه نوع هایی رو میتونیم به عنوان نوع جنریک (Generic) تعیین کنیم.

میخوایم یک تابع جنریک تعریف کنیم که max بین دو عدد رو نمایش بده؛ در مثال زیر کامپایلر میدونه که Number قراره سوپر کلاس تمام نوع هایی باشه که میخوایم در متد max استفاده کنیم، بنابراین متد های Number رو در دسترس قرار میده.

static <T extends Number> T max(T t0, T t1){
  
  return t0.doubleValue() > t1.doubleValue() ? t0 : t1;
}
        

تولید کننده (Producer) و مصرف کننده (Consumer)

هنگامی که بخوایم از یک کلاس جنریک به عنوان متغیر استفاده کنیم، میتونیم نوع جنریک کلاس رو به صورت WildCard مشخص کنیم. با استفاده از WildCard ها میتونیم از کلاس های جنریک متغیر های مصرف کننده (Consumer) و تولید کننده (Producer) تعریف کنیم.

کلاس جنریک زیر رو برای ادامه ی مطلب در نظر بگیرید.

public class MyClass<T>{

  private T t;

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

  public T getT(){
    return t;
  }

}
          

تعریف متغیر تولید کننده (Producer) با کلاس جنریک

هنگام ایجاد متغیر از کلاس، با تعیین نوع جنریک کلاس به صورت <? extends Type>، به متغیر های جنریک تعریف شده در کلاس نمیتونیم مقداری رو اختصاص بدیم اما میتونیم متغیر ها رو به عنوان ساب تایپی از Type صدا بزنیم و نوع متغیر های تعریف شده در داخل کلاس باید ساب تایپی (ساب کلاس) از Type باشن؛ به این متغیر ها Producer میگیم.

چون کامپایلر نمیتونه تشخیص بده متغیری که به کلاس اضافه میکنیم چه تایپی است، بنابراین هنگام استفاده از این نوع WildCard کامپایلر اختصاص دادن مقدار به این فیلد ها رو مسدود میکنه و تنها میتونیم فیلد های مقداردهی شده ی داخل کلاس رو صدا بزنیم.

در زیر میتونیم با متغیر mc0 فیلد t رو مقداردهی کنیم، چون در mc به طور واضح برای کامپایل مشخص کردیم نوع مورد نظرمون Integer است و کامپایلر هر عدد از نوع Integer رو به عنوان مقدار t قبول می کنه.

اما هنگام مقداردهی فیلد t با متغیر mc1 دچار خطای کامپایل میشیم چون کامپایلر نمیتونه تشخیص بده فیلد از چه نوعی است و فقط میدونه که فیلد یک ساب تایپ (ساب کلاس) از Number است.

با این WildCard میتونیم مقادیر رو از کلاس بخونیم و مقدار رو به نوع واقعی خودش کست کنیم.

public static void main(String[] args){

  MyClass<Integer> mc0 = new MyClass<>();
  mc0.setT(5); //Correct

  MyClass<? extends Number> mc = new MyClass<>();
  mc.setT(2); //Compile Error
  int t = (int) mc.t; //Correct

}
          

اگه نوع جنریک کلاس رو هر نوعی که ساب تایپ Object است (<? extends Object>) تعیین کرده باشیم، میتونیم به صورت خلاصه از <?> استفاده کنیم.

به طور خلاصه اگه از extends استفاده کنیم متغیری که به آبجکت اشاره می کنه Read-Only است.

تعریف متغیر مصرف کننده (Consumer) با کلاس جنریک

هنگام ایجاد متغیر از کلاس، با تعیین نوع جنریک به صورت <? super Type> میتونیم فیلد های داخل کلاس رو مقداردهی کنیم و آبجکت ایجاد شده از کلاس باید از نوع Type یا یکی از سوپرتایپ (سوپرکلاس) های Type باشه و برای مقداردهی فیلد های داخل این کلاس ها، نوع مقدار فیلد باید یکی از ساب تایپ (ساب کلاس) های Type باشه. به این نوع متغیر ها مصرف کننده (Consumer) گفته میشه.

public static void main(String[] args){
  
  MyClass<? super Number> mc = new MyClass<>();
  mc.setT(Double.valueOf(5.2));
  mc.setT(Integer.valueOf(2));

          

به طور خلاصه اگه از super استفاده کنیم، متغیری که به آبجکت اشاره میکنه Write-Only است.

در زیر میخوایم یک متد کپی با دو پارامتر تولید کننده (Producer) و مصرف کننده (Consumer) برای کپی کردن اعداد تعریف کنیم.

private static void copy(List<? extends Number> producer, List<? super Number> consumer){
        for (int i=0; i<producer.size(); i++)
            consumer.add(producer.get(i));
    }
        

محدودیت های موجود در جنریک (Generic) ها

برای استفاده از متغیر و فیلد های جنریک داخل کلاس یک سری محدودیت ها وجود داره میخوایم اونا رو بررسی کنیم.

  1. از فیلد ها و متغیر های جنریک داخل کلاس نمیتونیم نمونه ایجاد کنیم، چون نوع متغیر ها مشخص نیست و کامپایلر نمیتونه تشخیص بده آبجکت ایجاد شده از چه کلاسی است.

    public class MyClass<E>{
      
      E myGenericField = new E(); //خطای کامپایل
    
      public void myMethod(){
        E myGenericVar = new E(); //خطای کامپایل
      }
    
    }
                
  2. اگه فیلد یا متغیر جنریک به صورت آرایه تعریف شده باشه نمیتونیم یک آرایه با مقادیر پیش فرض ازشون ایجاد کنیم.

    public class MyClass<E>{
    
      E[] myGenericArrayField; 
    
      public MyClass(){
        myGenericArrayField = new E[10]; //خطای کامپایل
      }
    
      public void myMethod(){
        E[] myGenericArrayVar = new E[15]; //خطای کامپایل
      }
    
    }
            
  3. اگه فیلد یا متغیر جنریک به صورت آرایه تعریف شده باشه برای ایجاد آرایه با مقادیر پیشفرض باید آرایه رو با Object ایجاد کنیم و سپس به نوع جنریک کست کنیم.

    public class MyClass<E>{
    
      E[] myGenericArrayField; 
    
      public MyClass(){
        myGenericArrayField = (E[]) new Object[10]; //صحیح
      }
    
      public void myMethod(){
        E[] myGenericArrayVar =(E[]) new Object[15]; //صحیح
      }
    
    }
                
  4. پارامتر های تعریف شده برای کلاس جنریک قابل استفاده در استاتیک ها نیستن.

    public  class MyClass<E> {
    
       public static E e0; //خطای کامپایل
    
       static {
         E e1; //خطای کامپایل
       }
    
       public  static void myStaticMethod(E e2){ //خطای کامپایل
             ...
       }
    
    
                
  5. کلاس های اکسپشن نمیتونن جنریک باشن

  6. public class MyException<T> extends Exception{ //خطای کامپایل
            ....
    }
              

خلاصه

  • هنگام تعریف یک کلاس، تا زمان استفاده از کلاس نخوایم نوع فیلد و متغیر های داخل کلاس مشخص باشه از جنریک (Generic) به عنوان پارامتر کلاس استفاده می کنیم.
  • به کلاس های دارای پارامتر جنریک کلاس جنریک میگیم.
  • میتونیم کلاس، اینترفیس و متد های استاتیک رو با یک یا چند پارامتر جنریک تعریف کنیم.
  • هنگام تعریف پارامتر جنریک برای کلاس، میتونیم پارمتر کلاس رو محدود به یک کلاس خاص و ساب کلاس های اون کنیم که به نوع پارامتر ها Bounded Type میگیم.
  • با استفاده از WildCard ها میتونیم تولید کننده (Producer) و مصرف کننده (Consumer) تعریف کنیم.

برای اطلاع از جدیدترین مطالب یا پرسش و پاسخ عضو کانال و گروه تلگرامی ما شوید.

arrow_drop_up
کپی شد!