جنریک ( Generic )

جنریک یعنی کدی را بنویسیم که توسط انواع مختلف استفاده شود. برای مثال شما نمی خواهید کلاسهای جداگانه ای برای جمع آوری آبجکت های file و string بنویسید. کلاس مجزا ArrayList آبجکتهای هر کلاسی را جمع آوری می کند. این مثالی از برنامه نویسی جنریک می باشد.
قبل از JDK 5.0 ، برنامه نویسی جنریک در جاوا از طریق ارث بری انجام می شد. کلاس ArrayList در زیر به طور ساده از آرایه ای از اشیا پشتیبانی می کند:
1
2 package Example;
3
4 public class ArrayList // before JDK 5.0
5 {
6 public Object get(int i) { }
7 public void add(Object o) { }
8 //. . .
9 private Object[] elementData;
10 }
11
این روش دو مشکل دارد. زمانیکه شما می خواهید مقداری را بازیابی کنید، یک تبدیل(cast) ضروری می شود:
ArrayList files = new ArrayList();
. . .
String filename = (String) names.get(0);
علاوه بر این، هیچگونه بررسی خطا ای وجود ندارد. شما می توانید مقادیری از هر کلاسی را به آن اضافه کنید:
files.add(new File(". . ."));
این فراخوانی بدون خطا کامپایل و اجرا می شود. در جای دیگر، تبدیل نتیجه ی get به یک string باعث خطا خواهد شد.
JDK 5.0 راه حل بهتری را پیشنهاد کرد: نوع پارامترها. اکنون کلاس ArrayList نوع پارامتری دارد که نوع اجزا را نشان می دهد:
ArrayList<String> files = new ArrayList<String>();
این روش کدها را برای خواندن آسانتر کرده است. کامپایلر از این اطلاعات استفاده بهتری خواهد کرد و هیچ تبدیلی برای فراخوانی get نیاز نیسیت.کامپایلر می داند که نوع string را برگرداند، نه object :
String filename = files.get(0);
همچنین کامپایلر می داند که متد add از یک ArrayList<String> پارامتری از نوع string دارد که امن تر داشتن یک پارامتر object است. اکنون کامپایلر می تواند چک کند که شما آبجکتی با نوع غلط وارد نکنید. برای مثال دستور زیر را در نظر بگیرید:
files.add(new File(". . .")); // can only add String objects to an ArrayList<String>
این دستور کامپایل نخواهد شد. یک خطای کامپایلری رخ دهد بهتر است تا یک استثنا در زمان اجرا رخ دهد.
استفاده ازکلاس جنریک مانند ArrayList آسان است. بیشتر برنامه نویسیان جاوا به سادگی از انواع مانند ArrayList<String> درست مشابه آرایه String[] استفاده می کنند. (البته لیستی از آرایه ها بهتر از آرایه ها است زیرا آنها می توانند به طور اتوماتیک گسترش یابند.)
به هر حال، implement کردن یک کلاس جنریک آسان نیست. برنامه نویسانی که از کد های شما استفاده می کنند، می خواهند همه ی انواع کلاسها برای انواع پارامتر های شما را پلاگین کنند. آنها همه چیز را بدون محدودیت و سردرگمی در پیام خطا برای کار در نظر می گیرند. کار شما به عنوان برنامه نویس جنریک، این است که همه ی پتانسیل های مورد استفاده درآینده را برای کلاس تان پیش بینی کنید.
این کار چقدر می تواند سخت باشد؟ در اینجا موضوعی است که طراحان کلاسهای کتابخانه ای استاندارد باید با آن گلاویز شوند. کلاس ArrayList متد addAll برای اضافه کردن تمامی اجزا مجموعه های دیگر دارد. یک برنامه نویس ممکن است بخواهد همه ی اجزا را از یک ArrayList<Manager> به ArrayList<Employee> اضافه کند. اما انجام این کار با روش دیگر نباید مجاز باشد. چگونه شما به یک فراخوانی اجازه می دهید و به دیگری اجازه نمی دهید؟ طراحان زبان جاوا یک راهکار هوشمندانه جدید را اختراع کردند. نوع wildcard (علامت جانشین)، این مشکل را حل کرده است. نوع wildcard نسبتا خلاصه است، اما آنها به سازنده ی کتابخانه اجازه می دهند تا متدهایی تاحد امکان انعطاف پذیرایجاد کنند.
برنا.مه نویسی جنریک به سه سطح مهارت تقسیم می شود. در سطح اول، شما باید از کلاسهای جنریک – به عنوان نمونه، مجموعه هایی مانند ArrayList - استفاده کنید بدون اینکه فکر کنید چرا وچگونه کار می کنند. بیشتر برنامه نویسان اپلیکیشن، می خواهند در این سطح بمانند تا بعضی چیز ها اشتباه شود. ممکن است زمانیکه کلاسهای جنریک مختلفی را باهم ترکیب می کنید یا وقتی از کدی ارث بری می کند که در مورد انواع پارامتر ها چیزی نمی داند، با پیام خطای گیج کننده ای مواجه شوید. در پایان ممکن است بخواهید از کلاس یا متد جنریک خود implement کنید.
احتمالا برنامه نویسان اپلیکیشن نمی خواهند کدهای زیاد جنریک را بنویسند. گروه Sun اکنون پیشرفت های زیادی داشتند و انواع پارامتر ها را برای کلاسهای مجموعه ارائه دادند. فقط کدهایی که شامل cast های زیادی از انواع عمومی(جنرال) زیادی می باشند(مانند object یا اینترفیس comparable) استفاده ی پارامتر های نوع سودمند خواهد بود.
استفاده از جنریک مفهومی را معرفی می کند بنام type variable یک type variable بر طبق زبان جاوا در واقع یک شناسه ی بی حد و حصر یا unqualified identifier است که بصورت زیر دسته بندی می شود:
Generic class declarations •(کلاس جنریک)
Generic interface declarations •(اینترفیس جنریک)
Generic method declarations •(متد جنریک)
Generic constructor declarations •(کانستراکتور جنریک)

کلاس یا اینترفیس جنریک

کلاس یا اینترفیسی که یک یا چندین type variable دارد
public interface List<t> extends Collection<t>
{
} </t></t>
به نحوی type variable مانند یک پارامتر عمل می کند و اطلاعاتی را که کامپایلر نیاز دارد فراهم می کند.

متد و کانستراکتور جنریک

متد و کانستراکتور هم مانند بالا با داشتن یک یا چند type variable می تواند جنریک تعریف شود
public static <t> T getFirst(List<t> list) </t></t>

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

یک کلاس جنریک کلاسی با یک یا چند نوع متغییر می باشد. به عنوان مثال کلاس ساده ی Pair را درنظر بگیرید. این کلاس به ما اجازه می دهد که بدون توجه به جزئیات ذخیره سازی داده، روی جنریک فوکوس کنیم:
1
2 package Example;
3
4 public class Pair<T> {
5 public Pair() { first = null; second = null; }
6 public Pair(T first, T second) { this.first = first; this.second = second; }
7 public T getFirst() { return first; }
8 public T getSecond() { return second; }
9 public void setFirst(T newValue) { first = newValue; }
10 public void setSecond(T newValue) { second = newValue; }
11 private T first;
12 private T second;
13 }
14
یک نمونه از کلاس Pair به نام T از طریق <> که بعد از نام کلاس آمده، معرفی شده است. کلاس جنریک می تواند بیشتر از یک نوع داشته باشد مانند زیر:
public class Pair<T, U> { . . . }
انواع متغییر ها می توانند در سراسر تعریف کلاس برای تعیین نوع مقدار بازگشتی متد و انواع فیلد ها و متغییر های محلی استفاده شوند. برای مثال:
private T first; // uses type variable

نکته:

معمولا برای متغییر ها از حروف بزرگ استفاده می شود. در کتابخانه جاوا از متغییر E برای نوع عناصر(element) مجموعه(collection) ، K وV برای کلید(key) و مقدار(value) نوع متغییر جدول استفاده می شود. . از T (اگر نیاز بود از S , U) برای "هر نوع آن".
شما می توانید جنریک را با استفاده از یک جایگزین برای متغییر معرفی کنید، مانند زیر:
Pair<String>
می توانید نتیجه ی یک کلاس عادی با سازنده ها را در نظر بگیرید:
Pair<String>()
Pair<String>(String, String)
و متدها :
String getFirst()
String getSecond(
void setFirst(String)
void setSecond(String)
به عبارت دیگر، کلاس جنریک به عنوان یک تولید کننده برای کلاس عادی عمل می کند.
در مثال زیر کلاس PairTest1 را ایجاد کرده، متد استاتیک minmax یک آرایه را مرور می کند و همزمان مقدار minimum و maximum را محاسبه می کند و از آبجکت Pair برای برگرداندن هردو استفاده می کند. فراخوانی متد comparTo دو رشته را با هم مقایسه می کند و اگر رشته یکسان بود مقدار 0 را برمی گرداند و اگر رشته ی اول قبل از رشته ی دوم (بر اساس حروف الفبا) آمده باشد مقدار منفی و در غیر اینصورت مقدار مثبت را بر می گرداند:
1 package Example;
2
3 public class PairTest1 {
4 public static void main(String[] args) {
5 String[] words = { "Mary", "had", "a", "little", "lamb" };
6 Pair<String> mm = ArrayAlg.minmax(words);
7 System.out.println("min = " + mm.getFirst());
8 System.out.println("max = " + mm.getSecond());
9 } }
10
11 class ArrayAlg
12 {
13 /**
14 * Gets the minimum and maximum of an array of strings.
15 * @param a an array of strings
16 * @return a pair with the min and max value, or null if a is
17 * null or empty
18 * */
19 public static Pair<String> minmax(String[] a)
20 {
21 if (a == null || a.length == 0) return null;
22 String min = a[0];
23 String max = a[0];
24 for (int i = 1; i < a.length; i++)
25 {
26 if (min.compareTo(a[i]) > 0) min = a[i];
27 if (max.compareTo(a[i]) < 0) max = a[i];
28 }
29 return new Pair<String>(min, max);
30 }
31 }
و خروجی آن به صورت زیر می باشد:

متد جنریک

در بخش قبلی دیدید که چگونه یک کلاس جنریک تعریف می شود. شما می توانید یک متد با انواع پارامتر ها داشته باشید:
class ArrayAlg
{
public static <T> T getMiddle(T[] a)
{
return a[a.length / 2]);
}
}
این متد در یک کلاس معمولی تعریف شده است و در کلاس جنریک تعریف نشده است. به هرحال یک متد جنریک است. که شما می توانید از <> و متغیر نوع ببینید. توجه داشته باشید که متغیر نوع پس از اصلاح (در اینجا public static) و قبل از نوع return وارد شده. شما می توانید متد جنریک را در داخل کلاس های معمولی و یا در داخل کلاس های جنریک تعریف کنید. هنگامیکه شما یک متد جنریک را فراخوانی می کنید، می توانید انواع واقعی را محصور در <> بعد از نام متد قرار دهید:
String[] names = { "John", "Q.", "Public" };
String middle = ArrayAlg.<String>getMiddle(names);
در این مورد و در واقع در اغلب موارد، شما می توانید پارامتر نوع <string> را از فراخوانی متد حذف کنید. کامپایلر اطلاعات کافی مربوط به متدی که شما می خواهید را دارد. این مقایسه ها ی نوع names (که String[] است) با نوع جنریکT[] که در نتیجه T باید String باشد. این است که، شما به سادگی می توانید فراخوانی کنید:
String middle = ArrayAlg.getMiddle(names);

نکته:

در C++ می توانید پارامترهای نوع را بعد از نام متد قرار دهید که می تواند به ابهامات تجزیه کننده منجر شود. برای مثال g(f<a,b>(c)) که می تواند به دو صورت زیر معنی دهد: "فراخوانی g با بازگشتی f<a,b>(c) " و یا "فراخوانی g با دو مقدار منتطقی f<a و b>(c) " .

محدوده برای نوع متغیر

گاهی اوقات یک کلاس و یا یک متد نیاز دارند روی انواع متغییرها محدودیت قرار دهند. در اینجا یک مثال ساده ارائه می کنیم. ما می خواهیم کوچکترین عضو آرایه را محاسبه کنیم:
class ArrayAlg
{
public static <T> T min(T[] a) // almost correct
{
if (a == null || a.length == 0) return null;
T smallest = a[0];
for (int i = 1; i < a.length; i++)
if (smallest.compareTo(a[i]) > 0) smallest = a[i];
return smallest;
}
}
اما یک مشکل وجود دارد. به کد درون متد min نگاه کنید. متغییر smallest نوع T را دارد، که به معنی این است که آن می تواند یک شی از یک کلاس دلخواه باشد. چگونه ما میدانیم که کلاسی که T را تعلق دارد، متد compareTo را دارد؟
این راه حل، محدودیت T به کلاسی که اینترفیس Comparable را implement می کند، است. یک اینترفیس استاندارد با یک متد مجزا compareT.
public static <T extends Comparable> T min(T[] a) . . .
درحقیقت اینترفیس Comparable خودش یک نوع جنریک می باشد. ما فعلا این پیچیدگی را نادیده می گیریم.
اکنون متد جنریک min می تواند فقط با آرایه ای از کلاسهایی که اینترفیس Comparable را implement می کند، فراخوانی شود. مانند String , Date و.... فراخوانی min با یک آرایه ی Rectangle ، خطای زمان کامپایل دارد زیرا کلاس Rectangle ، اینترفیس Comparable را implement نمی کند.
ممکن است تعجب کنید که چرا از کلمه ی کلیدی extends بجای کلمه ی کلیدی implement در این وضعیت استفاده می کنید. Comparable بک اینترفیس است. نماد زیر (BoundingType یعنی نوع محدود)
<T extends BoundingType>
بیان می کند که T باید یک زیر گروه از نوع محدوده باشد. هم T و هم نوع محدوده می توانند به صورت یک کلاس و یا یک اینترفیس باشند. کلمه ی کلیدی extends استفاده شده زیرا این یک تقریب مناسب از مفهوم زیر نوع است و طراحان جاوا نمی خواهند کلمه ی کلیدی جدیدی(مانند sub) را به زبان اضافه کنند. یک متغییر نوع و یا wildcard می تواند محدودیتها و مرز های چندگانه داشته باشدکه با علامت "&" از هم جدا می شود، مانند:
T extends Comparable & Serializable
Subtype
در جاوا به عنوان یک زبان شی گرا شما می توانید سلسله مراتبی از type های مختلف داشته باشید یک subtype از نوع T نوع یا type است که T را extends می کند یا اگر T اینترفیس باشد آن را implements می کند.
اگر می خواهید آبجکتی از ساختار داده بدست آوید از extends استفاده کنید.
اگر می خواهید آبجکتی داخل ساختار داده قرار دهید از superاستفاده کنید.
اگر هر دو را لازم دارید از هیچ wildcards استفاده نکنید.
با ارث بری در جاوا، می توانید چند اینترفیس super که دوست دارید، داشته باشید. اما در بسیاری از محدودیتها می تواند کلاس باشد. اگر شما یک کلاس را به عنوان محدودیت داشته باشید، باید درلیست محدودیت ها، آن را اولین قرار دهیم.
در مثال زیر ما متد minmax را به جنریک، بازنویسی کردیم. این متد minimum و maximum یک آرایه جنریک را محاسبه می کند و یک Pair<T> را برمیگرداند.
1
2 package Example;
3
4 import java.util.*;
5 public class PairTest2
6 {
7 public static void main(String[] args)
8 {
9 GregorianCalendar[] birthdays =
10 {
11 new GregorianCalendar(1906, Calendar.DECEMBER, 9),// G. Hopper
12 new GregorianCalendar(1815, Calendar.DECEMBER, 10), // A. Lovelace
13 new GregorianCalendar(1903, Calendar.DECEMBER, 3), // J. von Neumann
14 new GregorianCalendar(1910, Calendar.JUNE, 22), // K. Zuse
15 };
16
17 Pair<GregorianCalendar> mm = PairTest2.minmax(birthdays);
18 System.out.println("min = " + mm.getFirst().getTime());
19 System.out.println("max = " + mm.getSecond().getTime());
20 }
21 }
22
23 class ArrayAlg
24 {
25 /**
26 Gets the minimum and maximum of an array of objects of type T.
27 @param a an array of objects of type T
28 @return a pair with the min and max value, or null if a is
29 null or empty
30 */
31 public static <T extends Comparable> Pair<T> minmax(T[] a)
32
33 {
34 if (a == null || a.length == 0) return null;
35 T min = a[0];
36 T max = a[0];
37 for (int i = 1; i < a.length; i++)
38 {
39 if (min.compareTo(a[i]) > 0) min = a[i];
40 if (max.compareTo(a[i]) < 0) max = a[i];
41 }
42 return new Pair<T>(min, max);
43 }
44 }

مثالی را در intellij

اکنون مثالی را در intellij بررسی می کنیم. کلاسی با نام sample می سازیم و type1,type2,type3 را داخل <> در جلوی نام کلاس می تویسیم سپس سه نوع از آن با نامهای i,y,z ایجاد می کنیم:
1 package first;
2
3 public class sample <type1,type2,type3> {
4 private type1 i;
5 private type2 y;
6 private type3 z;
7 }
8
سپس روی صفحه کلیک راست می کنیم و گزینه ی Generic را انتخاب می کنیم:
بعد از آن گزینه ی Getter and Setter را انتخاب می کنیم:
هر سه فیلد را انتخاب می کنیم تا برای آنها متد های get و set قرار داده شود:
کد های زیر اضافه می شود:
به عنوان مثال مقدار بازگشتی متد getI و ورودی setI از نوع type1 می باشد.
به این کار جنریک گفته می شود که در اصل نوع خصوصیات در زمانی که می خواهیم از آن آبجکت بسازیم مشخص می شود.
اکنون کلاس main را می سازیم و در آن از کلاس sample آبجکت می سازیم (در خط 8 ) و داخل < > باید از نوع کلاس (Integer) باشد نه دیتا تایپ(int). کد های زیر را در آن وارد می کنیم:
1 package first;
2 import static java.lang.System.*;
3
4 public class Main {
5
6 public static void main(String[] args) {
7
8 sample <Integer,String,Boolean> sam1 =new sample();
9 sam1.setI(100);
10 sam1.setY(" Rasekhoon ");
11 sam1.setZ(true);
12
13 sample <Integer,Integer,Integer> sam2 =new sample();
14 sam2.setI(100);
15 sam2.setY(200);
16 sam2.setZ(300);
17
18 out.println(sam1.getY());
19 out.println(sam2.getY());
20
21 }
22 }
در این مثال ما دو آبجکت به نام های sam1 و sam2 از روی کلاس sample ساختیم که نوع خصوصیات آن را در زمان ایجاد آن مشخص کردیم. برای sam1 I از نوع کلاس Integer ، Y از نوع کلاس Stringو Z هم از نوع کلاس Boolean می باشد. برای مقدار دادن ازمتد set استفاده کردیم. به I باید integer و به y باید string و به z باید Boolean بدهیم. برای sam2 ، IوYوZ همه از نوع کلاس Integer می باشد. که خروجی آن به صورت زیر می باشد: