menusearch
javapro.ir

معرفی کلاس های Generic در زبان برنامه نویسی جاوا

جستجو
شنبه ۱ دی ۱۴۰۳ | ۲۰:۲۴:۱۱
۱۴۰۲/۱۱/۲۵ چهارشنبه
(3)
(1)
معرفی کلاس های Generic در زبان برنامه نویسی جاوا
معرفی کلاس های Generic در زبان برنامه نویسی جاوا

Generic

 

 

مدرس: پارسا کاویانپور

 

 

آموزش Generic در دوره پیشرفته زبان برنامه نویسی جاوا

 

 

 

جنریک در JDK5 به پلت فرم جاوا اضافه شد.استفاده از جنریک ها این امکان را فراهم میکند که کلاس ها, واسط ها و توابعی ایجاد کنید که بتوانند با هر نوع داده ای کار کنند.در بسیاری از الگوریتم های منطقی, نوع داده ی به کار رفته اهمیت چندانی ندارد. بعنوان مثال برای الگوریتمی که یک Stack را پیاده سازی میکند اهمیتی ندارد که داده ی ذخیره شده در آن عدد صحیح باشد یا رشته, شی یا نخ .جنریک ها میتوانند گزینه ی مناسبی برای پیاده سازی چنین دست الگوریتم هایی باشد.

 

فرق استفاده از Object با جنریک چیست؟

میدانید که به واسطه ی اینکه در جاوا تمام کلاس ها بصورت مستقیم و غیر مستقیم فرزند Object هستند میتوانیم متد ها و کلاس هایی بسازیم که با نوع داده ای Object سر و کار داشته باشند بدین شکل با دیگر نوع داده ها نیز کار خواهند کرد.درواقع قبل از Jdk 5 برنامه نویسان بدین شکل متد های عمومی ایجاد میکردند.اما این روش مشکلاتی را مربوط به type casting در کد بوجود می‌اورد و با امدن جنریک از فرایند کستینگ جلوگیری شد (حداقل جلوی چشم کاربر. اما در سطح کامپایلر همان casting انجام میگیرد)

 

برای مثال کد زیر را در نظر بگیرید:

نمونه
 public static Object doSomething(Object obj){
return null;
}
public static<T> T doSomething(T t){
return null;
}  

 

 

 

هنگام کار با doSomething نسخه ی جنریک خبری از تبدیل نوع یا کست نیست.

 

;int i=1

;int i2= doSomething(i)

 

اما اگر از متدی که با کلاس Object سر و کار دارد استفاده کنیم:

 

;int i=1

;int i2=(int) doSomething(i)

 

 

از کجا معلوم که در نسخه ی Object واقعا همان نوعی سازگار با نوع ورودی ریترن شود؟
باید این مسئله را مورد بررسی قرار دهیم که به ClassCastException بر نخوریم.

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

یک مثال ساده از جنریک

} <class Stats<T

; T[] items

 

} Stats(T[] o) 

;items[]=o     

{   

 

 

 

فرض کنید میخواهیم به کد بالا متدی اضافه کنیم که میانگین داده های nums را محاسبه کند.در نگاه اول چنین کدی به نظر ما خواهد رسید :

یک مثال ساده از جنریک

} <class Stats<T

; T[] items

 

} Stats(T[] o) 

;items[]=o     

{   

} ()double average   

double sum=0.0       

} for(int i=0;i<nums.length;i++)       

;sum+=nums[i]         

{       

; return su    

{

 

 

 

اما کد فوق کامپایل نمیشود. از کجا معلوم پارامتر T یک داده ی عددی باشد؟ شاید String پاس داده شود:

;()<>Stats<String> s = new Stats

;()s.average

 

در چنین مواقعی باید از جنریک های کران دار (bounded types) استفاده کنیم.

میدانیم که تمام کلاس های Short ,Byte,Double,Float,Long,Integer از یک کلاسی به نام Number ارث بری میکنند. باید به نوعی مشخص کنیم تمام T هایی که برای ما می‌آیند زیر کلاسی از Number باشند. چگونه ؟به کد زیر توجه کنید:

 

جنریک کراندار
class Stats<T extends Number>{
   T[] nums;
   Stats(T[] o){
   nums=o;
   }
   double average(){
   double sum=0.0;
   for(int i=0;i<nums.length;i++)
       sum+=nums[i].doubleValue();        
   return sum;    
}

 

نکته: متد doubleValue در کلاس Number تعریف شده است. وقتی میگوییم T حتما کلاسی باشد که از Number ارث بری کرده است طبعا تمام متد های کلاس Number را دارد. بنابراین ما میتوانیم از متد های کلاس Number استفاده کنیم.

 

نکته : جنریک ها میتوانند کراندار به اینترفیس هم باشند:

جنریک کراندار به اینترفیس

class ClassName<T extends ClassOrInterface>{

}

 

و یا هردو با هم :

جنریک کراندار به اینترفیس و کلاس

class ClassName<T extends Class & Interface>{

}

 

حال فرض کنید متدی میخواهیم اضافه کنیم به نام sameAverage که بررسی میکند ایا دو تا کلاس Stats مقادیر average یکسانی دارند یا خیر. در ابتدا شاید پیاده سازی زیر به نظرتان معقول بیاید:

 

class Stats<T extends Number>{
   T[] nums;
   Stats(T[] o){
       nums=o;
   }
   double average(){
       double sum=0.0;
       for(int i=0;i<nums.length;i++)
           sum+=nums[i].doubleValue();        
       return sum;
   boolean sameAverage(Stats<T> ob){
       return average()==ob.average();
}

 

هنگام کامپایل کد فوق خطایی مشاهده نخواهید کرد. تنها یک ایراد وجود دارد. متد sameAverage تنها میتواند با نوعی از Stats کار کند که پارامتر T ان با پارامتر T خودش یکسان باشد(فرضا هردو double باشند) و این یک راهکار عمومی نیست چرا که اگر Stats اولی double باشد و حاوی 2.0 و 3.0 باشد و Stats دومی Integer باشد و حاوی 2و 3 باشد باید average یکسانی داشته باشند.

راه حل : استفاده از Wildcard

آرگومان wildcard چیست؟

در وضعیت هایی مشابه کد فوق wildcard مورد استفاده قرار میگیرد. wildcard با استفاده از علامت سوال <?> مشخص میشود و نشاندهنده ی یک نوع ناشناخته است. بنابراین کد فوق را به شکل زیر بازنویسی میکنیم:

 

wildcard
class Stats<T extends Number>{
   T[] nums;
   Stats(T[] o){
       nums=o;
   }
   double average(){
       double sum=0.0;
       for(int i=0;i<nums.length;i++)
           sum+=nums[i].doubleValue();        
       return sum;
   boolean sameAverage(Stats<?> ob){
       return average()==ob.average();
}

 

کراندار کردن Wildcard ها

مشابه همان روش محدود کردن پارامتر هاست :

 

methodOrClass<? extends superclass>

 

اما میتوانیم کران پایین هم تعریف کنیم:

 

methodOrClass<? super subclass>

 

 

یعنی تنها کلاس هایی مورد پذیرش هستند که superclass ای برای subclass باشند.


مثال دیگر از wildcard ها:

 

فرض کنید متدی بر مبنای الگوریتم insert ionSort برای مرتب سازی داریم که جنریک است:

 

public static <T extends Comparable> void sort(List<T> list) {

    for (int i = 0; i < list.size(); i++) {

       T key = list.get(i);

       int z = i;

      while (z > 0 && list.get(z - 1).compareTo( key) > 0) {

           list.set(z, list.get(z - 1));

          z--;

       }

       list.set(z, key);

   }

}

در مقاله ای جدا به بررسی عملکرد مرتب سازی درجی میپردازیم.

 

کد بالا درواقع عالی است و کار میکند اما یک مشکل ریز دارد. ما نگفتیم Comparable چه چیزی باشد (زیر خود جنریک است). وقتی نمیگوییم مجددا داریم از Object استفاده میکنیم. هدف ما از Generic استفاده نکردن از کلاس Object و درگیر نشدن با مشکلات casting است.درواقع کد بالا معادل کد زیر است:

 

    public static <T extends Comparable<Object>> void sort(List<T> list) {
        for (int i = 0; i < list.size(); i++) {
            T key = list.get(i);
            int z = i;
           while (z > 0 && list.get(z - 1).compareTo( key) > 0) {
                list.set(z, list.get(z - 1));
                z--;
            }
            list.set(z, key);
        }
    }

 

شاید راه حلی که اول به ذهنمان میرسد عاقلانه باشد. به جای Object همان T بگذاریم:

 

    public static <T extends Comparable<T>> void sort(List<T> list) {
        for (int i = 0; i < list.size(); i++) {
            T key = list.get(i);
            int z = i;
           while (z > 0 && list.get(z - 1).compareTo( key) > 0) {
                list.set(z, list.get(z - 1));
                z--;
            }
            list.set(z, key);
        }
    }

 

 

اما این نیز کاملا Generic نیست. در ارث بری ما میدانیم متد های کلاس پدر باید بتوانند
بر روی اشیایی از کلاس فرزند فراخوانی شده و رفتار مناسبی انجام دهند. با در نظر گرفتن این حالت اگر برای T سوپر کلاسی داشتیم و آن super class اینترفیس Comparable را پیاده سازی کرده بود. باید بتوانیم از آن پیاده سازی برای T که sub class است نیز استفاده نماییم:

 

       public static <T extends Comparable<? super T>> void sort(List<T> list) {
        for (int i = 0; i < list.size(); i++) {
            T key = list.get(i);
            int z = i;
           while (z > 0 && list.get(z - 1).compareTo( key) > 0) {
                list.set(z, list.get(z - 1));
                z--;
            }
            list.set(z, key);
        }
    }

 

کد فوق یک کد کاملا Generic است.

Erasure

به منظور جلوگیری از عدم سازگاری کد های قدیمی با جدید (میدانید که کد های jdk های پایینتر روی jdk های بالاتر قابل اجرا هستند) جاوا از Erasure استفاده میکند.

به هنگام کامپایل کردن کد های جاوا تمام اطلاعات مربوط به جنریک ها حذف میشود (erase میشود).

مثال :

 

class Stats<T>{
   T[] nums;
   Stats(T[] o){
   nums=o;
   }
}

 

کلاس فوق درواقع پس از کامپایل با کلاس زیر جایگزین میشود:

 

class Stats{
   Object[] nums;
   Stats(Object[] o){
   nums=o;
   }
}

 

البته اگر T کراندار به کلاسی باشد به جای ابجکت ان کلاس مینشیند:

 

class Stats<T extends Number>{
   T[] nums;
   Stats(T[] o){
   nums=o;
   }
}

 

به شکل زیر در می‌آید:

 

class Stats{
   Number[] nums;
   Stats(Number[] o){
   nums=o;
   }
}

 

و اما بریم سراغ محدودیت های Generic:

نمیتوان پارامتر نوع را new کرد:

 

class Stats<T>{
   T[] nums;
   Stats(T[] o){
   nums=new T();// Illegal!!!
   }
}

 

دلیل ان ساده است .هنگام new شدن یک شی باید اطلاعاتی از اینکه این شی واقعا چه شی ای است در دست باشد اما وقتی توسط Erasure با Object جایگزین میشود دیگر اطلاعاتی از این دست که T واقعا چی بوده از بین میرود و بنابراین نمیتوان انرا new کرد و همچنین نمیتوان از ان آرایه نیز ساخت!به مثال زیر توجه کنید:

 

  class Stats<T>{
   T[] nums= new T[5];
}
 

 

دلیل: اطلاعات مربوط به نوع T در زمان اجرا توسط Erasure از بین میرود درحالی که ایجاد یک آرایه در جاوا نیازمند دانستن اطلاعی درمورد نوع ان داده است.

راه حل: استفاده از ارایه ای از Object

راه حل دوم : استفاده از رفلکشن. کد زیر مثالی در این باره است:

 

class Array<E> {
private final E[] arr;
public final int length;
public Array(Class<E> type, int length) {
      this.arr = (E[]) java.lang.reflect.Array.newInstance(type, length);
      this.length = length;
    }
}

 

خطاهای مبهم (Ambiguity Error)

ورود مفهوم جنریک به دنیای جاوا با خود نوع جدیدی از خطاها با عنوان Ambiguity به همراه اورد. این نوع خطا مربوط به ویژگی erasure در جاواست :

هنگام کامپایل کد فوق با خطا مواجه خواهید شد:

 

 

class Stats<T,V>{
   T t;
   V v;

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

 

دلیل ان ساده است...هنگام حذف پارامتر نوع در فرایند Erasure هر دوی V و T با کلاس Object جایگزین میشوند و در متد های set ابهام بوجود می‌اید(متد های overload شده نباید پارامتر از نوع یکسان داشته باشند).

 

چرا Generic ها با داده های primitive نمیتوانند کار کنند ؟

چون Erasure پارامتر را با Object جایگزین میکند (با این استدلال که پدر تمام کلاس هاست) اما primitive type ها اصلا کلاس نیستند !بنابراین در bounded types ها به مشکل خواهند خورد.

 

موفق باشید.

نظرات کاربران
*نام و نام خانوادگی
* پست الکترونیک
* متن پیام

بستن
*نام و نام خانوادگی
* پست الکترونیک
* متن پیام

0 نظر
هدر سایت
دوره برنامه نویسی Spring Core
مشاهده سرفصل ها و ثبت نام در دوره Spring Boot جاواپرو  [کلیک کنید]
آموزش پروژه محور اسپرینگ بوت(Spring Boot)-سیستم دانشگاه
ثبت نام در دوره آموزش Spring security
دوره معماری میکروسرویس ها (Microservices) با استفاده از Spring Boot و Spring Cloud
دوره پرتاب | آموزش پیش نیازهای برنامه نویسی
دوره آموزش مبانی زبان برنامه نویسی جاوا
دوره آموزش مفاهیم پیشرفته زبان برنامه نویسی جاوا
مقدمه ای از زبان برنامه نویسی جاوا(java)
آموزش زبان برنامه نویسی جاوا
آموزش گرافیک در زبان برنامه نویسی جاوا
آموزش مدیریت چیدمان گرافیکی در زبان جاوا
آموزش ساخت بازی دوبعدی در زبان جاوا
Collection ها در زبان برنامه نویسی جاوا
آموزش پروژه محور ساخت برنامه مدیریت بانک با JavaFX
نمونه پروژه های رایگان زبان جاوا
آموزش دیتابیس در زبان برنامه نویسی جاوا
نمونه مثال پایه ای زبان برنامه نویسی جاوا
نمونه مثال String در زبان برنامه نویسی جاوا
آموزش جامع برنامه نویسی JavaFX
آموزش ساخت برنامه آزمون تستی در JavaFX
آموزش برنامه نویسی سوکت در جاوا
آموزش ساخت برنامه دفترچه تلفن با JavaFX
آموزش ساخت ربات ساده تلگرام با زبان جاوا
آموزش ساخت برنامه ماشین حساب با JavaFX
آموزش ساخت برنامه ساده مدیریت ایمیل ها با JavaFX
دوره آموزش Spring Boot
سفارش انجام پروژه زبان برنامه نویسی جاوا(JAVA)
سفارش انجام پروژه برنامه نویسی متلب(MATLAB) با قیمت منصفانه و تحویل به موقع
سفارش انجام پروژه زبان برنامه نویسی سی شارپ (#C)
سفارش انجام پروژه زبان برنامه نویسی سی(C)
سفارش انجام پروژه زبان برنامه نویسی پایتون(Python)
سفارش انجام پروژه زبان برنامه نویسی PHP (پی اچ پی)
سفارش انجام پروژه زبان برنامه نویسی اسمبلی(Assembly)
سفارش انجام پروژه زبان برنامه نویسی جاوا اسکریپت (Javascript)
سفارش انجام پروژه هوش مصنوعی
سفارش انجام پروژه طراحی الگوریتم
سفارش انجام پروژه ساختمان داده ها
سفارش انجام پروژه مهندسی نرم افزار
سفارش انجام پروژه شبکه های کامپیوتری
سفارش انجام پروژه پایگاه داده: دیتابیس (database)
 سفارش انجام پروژه سیستم عامل
سفارش انجام پروژه پاورپوینت(PowerPoint)
سفارش انجام پروژه اکسل (Excel)
سفارش انجام تحقیق و تهیه مقاله
سوالات متداول برنامه نویسی
جدیدترین مطالب