جنریک در جاوا

جنریک در JDK5 به پلت فرم جاوا اضافه شد.استفاده از جنریک ها این امکان را فراهم میکند که کلاس ها , واسط ها و توابعی ایجاد کنید که بتوانند با هر نوع داده ای کار کنند.در بسیاری از الگوریتم های منطقی , نوع داده ی به کار رفته اهمیت چندانی ندارد. بعنوان مثال برای الگوریتمی که یک Stack را پیاده سازی میکند اهمیتی ندارد که داده ی ذخیره شده در آن عدد صحیح باشد یا رشته , شی یا نخ .جنریک ها میتوانند گزینه ی مناسبی برای پیاده سازی چنین دست الگوریتم هایی باشد.فرق استفاده از Object با جنریک چیست؟میدانید که به واسطه ی اینکه در جاوا تمام کلاس ها بصورت مستقیم و غیر مستقیم فرزند Object هستند میتوانیم متد ها و کلاس هایی بسازیم که با نوع داده ای Object سر و کار داشته باشند بدین شکل با دیگر نوع داده ها نیز کار خواهند کرد.درواقع قبل از Jdk 5برنامه نویسان بدین شکل متد های عمومی ایجاد میکردند.اما این روش مشکلاتی را مربوط به casting در کد ما بوجود می اورد و با امدن جنریک از فرایند کستینگ جلوگیری شد (حداقل جلوی چشم کاربر.. اما در سطح کامپایلر همان casting انجام میگیرد) مثال : کد زیر را در نظر بگیریدpublic static Object doSomething(Object obj) {....} public static<T> T doSomething(T t) {....}هنگام کار با doSomething نسخه ی جنریک خبری از تبدیل نوع یا کست نیست :int i=1;int i2= doSomething(i);اما اگر از متدی که با کلاس Object سر و کار دارد استفاده کنیم:int i=1;int i2= (ُint) doSomething(i);مورد اول :از کجا معلوم که در نسخه ی Object واقعا همان نوعی سازگار با نوع ورودی ریترن شود؟باید این مسئله را مورد بررسی قرار دهیم که به ClassCastException بر نخوریم.اما با جنریک از تمام این گرفتاری ها خلاص میشویم.یک مثال ساده از جنریک :class Stats<T>{ T[] nums; Stats(T[] o){ nums=o; } }فرض کنید میخواهیم به کد بالا متدی اضافه کنیم که میانگین داده های nums را محاسبه کند.در نگاه اول چنین کدی به نظر ما خواهد رسید :class Stats<T>{ T[] nums; Stats(T[] o){ nums=o; } double average(){ double sum=0.0; for(int i=0;i<nums.length;i++) sum+=nums[i]; return sum; }اما کد فوق کامپایل نمیشود . از کجا معلوم پارامتر 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 استفاده کنیم.نکته : جنریک ها میتوانند کراندار به اینترفیس هم باشند:<T extends ClassOrInterface>و یا هردو با هم :<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 با استفاده از علامت سوال <?> مشخص میشود و نشاندهنده ی یک نوع ناشناخته است. بنابراین کد فوق را به شکل زیر بازنویسی میکنیم: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 ها: فرض کنید متدی بر مبنای الگوریتم insertionSort برای مرتب سازی داریم که جنریک است: 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 ها به مشکل خواهند خورد.