سلام! ما مجموعه درس های خود را در مورد ژنریک ادامه می دهیم. ما قبلاً
یک ایده کلی از چیستی آنها و چرایی نیاز آنها به دست آوردیم. امروز در مورد برخی از ویژگی های ژنریک ها و کار با آنها بیشتر خواهیم آموخت. بیا بریم!
در درس گذشته
در مورد تفاوت انواع ژنریک و انواع خام صحبت کردیم . نوع خام یک کلاس عمومی است که نوع آن حذف شده است.
مستندات می گوید: "T - نوع کلاسی که توسط این شی Class مدل شده است." با ترجمه این از زبان مستندسازی به گفتار ساده، متوجه میشویم که کلاس شی

List list = new ArrayList();
به عنوان مثال. در اینجا ما نشان نمی دهیم که چه نوع اشیایی در ما قرار می گیرد List
. اگر بخواهیم چنین چیزی ایجاد کنیم List
و چند آبجکت به آن اضافه کنیم، یک هشدار در IDEA خواهیم دید:
"Unchecked call to add(E) as a member of raw type of java.util.List".
اما ما همچنین در مورد این واقعیت صحبت کردیم که ژنریک ها فقط در جاوا 5 ظاهر می شوند. در زمان انتشار این نسخه، برنامه نویسان قبلاً مجموعه ای از کدها را با استفاده از انواع خام نوشته بودند، بنابراین این ویژگی زبان نمی توانست کار را متوقف کند و توانایی ایجاد انواع خام در جاوا حفظ شد. با این حال، معلوم شد که این مشکل گسترده تر است. همانطور که می دانید کد جاوا به فرمت کامپایل شده خاصی به نام بایت کد تبدیل می شود که سپس توسط ماشین مجازی جاوا اجرا می شود. اما اگر در طی فرآیند تبدیل اطلاعات مربوط به پارامترهای نوع را در بایت کد قرار دهیم، تمام کدهای نوشته شده قبلی را می شکند، زیرا قبل از جاوا 5 هیچ پارامتر نوع وجود نداشت! هنگام کار با ژنریک، یک مفهوم بسیار مهم وجود دارد که باید آن را به خاطر بسپارید. به آن پاک کردن نوع می گویند . به این معنی که یک کلاس حاوی هیچ اطلاعاتی در مورد پارامتر نوع نیست. این اطلاعات فقط در حین کامپایل موجود است و قبل از زمان اجرا پاک می شود (غیرقابل دسترسی می شود). اگر بخواهید نوع اشتباهی از شی را در خود قرار دهید List<String>
، کامپایلر یک خطا ایجاد می کند. این دقیقاً همان چیزی است که سازندگان زبان هنگام ایجاد ژنریک میخواهند به آن دست یابند: بررسیهای زمان کامپایل. اما وقتی همه کدهای جاوا شما به بایت کد تبدیل می شوند، دیگر حاوی اطلاعاتی در مورد پارامترهای نوع نیستند. در بایت کد، لیست گربه های شما List<Cat>
تفاوتی با رشته ها ندارد List<String>
. در بایت کد، چیزی نمی گوید که cats
لیستی از Cat
اشیا است. چنین اطلاعاتی در طول کامپایل پاک می شوند - فقط این واقعیت که شما یک List<Object> cats
لیست دارید در بایت کد برنامه قرار می گیرد. بیایید ببینیم این چگونه کار می کند:
public class TestClass<T> {
private T value1;
private T value2;
public void printValues() {
System.out.println(value1);
System.out.println(value2);
}
public static <T> TestClass<T> createAndAdd2Values(Object o1, Object o2) {
TestClass<T> result = new TestClass<>();
result.value1 = (T) o1;
result.value2 = (T) o2;
return result;
}
public static void main(String[] args) {
Double d = 22.111;
String s = "Test String";
TestClass<Integer> test = createAndAdd2Values(d, s);
test.printValues();
}
}
ما کلاس عمومی خود را ایجاد کردیم TestClass
. این بسیار ساده است: در واقع یک "مجموعه" کوچک از 2 شی است که بلافاصله پس از ایجاد شی ذخیره می شوند. دارای 2 T
فیلد زمانی که createAndAdd2Values()
متد اجرا شد، دو شیء ارسال شده ( Object a
و Object b
باید به T
type ریخته شوند و سپس به TestClass
شی اضافه شوند. در main()
متد یک را ایجاد می کنیم TestClass<Integer>
، یعنی Integer
آرگومان type جایگزین پارامتر type می شود. همچنین a و a را به آن Integer
پاس می دهیم. آیا فکر می کنید برنامه ما کار می کند؟ بالاخره ما به عنوان آرگومان نوع مشخص کردیم، اما قطعا نمی توان یک را به یک فرستاد ! بیایید روش را اجرا کنیم و بررسی کنیم. خروجی کنسول: Double
String
createAndAdd2Values()
Integer
String
Integer
main()
22.111
Test String
این غیر منتظره بود! چرا این اتفاق افتاد؟ این نتیجه پاک کردن نوع است. اطلاعات مربوط به Integer
نوع آرگومان مورد استفاده برای نمونه سازی TestClass<Integer> test
شیء ما هنگام کامپایل شدن کد پاک شد. میدان می شود TestClass<Object> test
. آرگومان های ما Double
و ما String
به راحتی به اشیا تبدیل شدند ( آنطور که انتظار داشتیم Object
به اشیا تبدیل نمی شوند !) و بی سر و صدا به . در اینجا یک مثال ساده اما بسیار آشکار دیگر از پاک کردن نوع است: Integer
TestClass
import java.util.ArrayList;
import java.util.List;
public class Main {
private class Cat {
}
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
List<Cat> cats = new ArrayList<>();
System.out.println(strings.getClass() == numbers.getClass());
System.out.println(numbers.getClass() == cats.getClass());
}
}
خروجی کنسول:
true
true
به نظر می رسد ما مجموعه هایی را با سه نوع آرگومان مختلف ایجاد کردیم - String
, Integer
و کلاس خودمان Cat
. اما در هنگام تبدیل به بایت کد، هر سه لیست تبدیل به List<Object>
.
هنگام کار با آرایه ها و ژنریک ها، پاک کردن را تایپ کنید
نکته بسیار مهمی وجود دارد که باید هنگام کار با آرایه ها و کلاس های عمومی (مانندList
) به وضوح درک شود. همچنین باید هنگام انتخاب ساختارهای داده برای برنامه خود به آن توجه کنید. ژنریک ها در معرض پاک کردن نوع هستند. اطلاعات مربوط به پارامترهای نوع در زمان اجرا در دسترس نیست. در مقابل، آرایه ها می دانند و می توانند از اطلاعات مربوط به نوع داده خود در هنگام اجرای برنامه استفاده کنند. تلاش برای قرار دادن یک نوع نامعتبر در یک آرایه باعث ایجاد یک استثنا می شود:
public class Main2 {
public static void main(String[] args) {
Object x[] = new String[3];
x[0] = new Integer(222);
}
}
خروجی کنسول:
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
از آنجا که تفاوت زیادی بین آرایه ها و ژنریک ها وجود دارد، ممکن است مشکلات سازگاری داشته باشند. مهمتر از همه، شما نمی توانید یک آرایه از اشیاء عمومی یا حتی فقط یک آرایه پارامتری ایجاد کنید. آیا این کمی گیج کننده به نظر می رسد؟ بیا یک نگاهی بیندازیم. به عنوان مثال، شما نمی توانید هیچ یک از این موارد را در جاوا انجام دهید:
new List<T>[]
new List<String>[]
new T[]
اگر بخواهیم آرایه ای از List<String>
اشیاء ایجاد کنیم، یک خطای کامپایل دریافت می کنیم که از ایجاد آرایه عمومی شکایت می کند:
import java.util.List;
public class Main2 {
public static void main(String[] args) {
// Compilation error! Generic array creation
List<String>[] stringLists = new List<String>[1];
}
}
اما چرا این کار انجام می شود؟ چرا ایجاد چنین آرایه هایی مجاز نیست؟ این همه برای تامین ایمنی نوع است. اگر کامپایلر به ما اجازه دهد چنین آرایه هایی از اشیاء عمومی ایجاد کنیم، می توانیم مشکلات زیادی برای خود ایجاد کنیم. در اینجا یک مثال ساده از کتاب "جاوای موثر" جاشوا بلوخ آورده شده است:
public static void main(String[] args) {
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = Arrays.asList(42, 65, 44); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)
}
بیایید تصور کنیم که ایجاد یک آرایه مانند List<String>[] stringLists
مجاز است و خطای کامپایل ایجاد نمی کند. اگر این درست بود، در اینجا چند کار وجود دارد که میتوانیم انجام دهیم: در خط 1، آرایهای از فهرستها ایجاد میکنیم: List<String>[] stringLists
. آرایه ما شامل یک است List<String>
. در خط 2، لیستی از اعداد ایجاد می کنیم: List<Integer>
. در خط 3، ما را List<String>[]
به یک Object[] objects
متغیر اختصاص می دهیم. زبان جاوا این اجازه را می دهد: آرایه ای از X
اشیا می توانند X
اشیاء و اشیاء همه زیر کلاس ها را ذخیره کنند X
. بر این اساس، شما می توانید هر چیزی را در یک Object
آرایه قرار دهید. در خط 4، تنها عنصر آرایه objects()
(a List<String>
) را با یک جایگزین می کنیم List<Integer>
. بنابراین، ما a را List<Integer>
در آرایه ای قرار می دهیم که فقط برای ذخیره List<String>
اشیاء در نظر گرفته شده است! تنها زمانی که خط 5 را اجرا می کنیم با خطا مواجه می شویم. A ClassCastException
در زمان اجرا پرتاب می شود. بر این اساس، ممنوعیت ایجاد چنین آرایه هایی به جاوا اضافه شد. این به ما امکان می دهد از چنین موقعیت هایی اجتناب کنیم.
چگونه می توانم از پاک کردن نوع استفاده کنم؟
خوب، ما در مورد پاک کردن نوع یاد گرفتیم. بیایید سعی کنیم سیستم را فریب دهیم! :) وظیفه: ما یک کلاس عمومی داریمTestClass<T>
. می خواهیم برای این کلاس متدی بنویسیم createNewT()
که یک شی جدید ایجاد و برگرداند T
. اما این غیر ممکن است، درست است؟ تمام اطلاعات مربوط به T
نوع در طول کامپایل پاک می شود و در زمان اجرا نمی توانیم تعیین کنیم که چه نوع شی را باید ایجاد کنیم. در واقع یک راه دشوار برای انجام این کار وجود دارد. احتمالاً به یاد دارید که جاوا Class
کلاس دارد. می توانیم از آن برای تعیین کلاس هر یک از اشیاء خود استفاده کنیم:
public class Main2 {
public static void main(String[] args) {
Class classInt = Integer.class;
Class classString = String.class;
System.out.println(classInt);
System.out.println(classString);
}
}
خروجی کنسول:
class java.lang.Integer
class java.lang.String
اما در اینجا یک جنبه است که ما در مورد آن صحبت نکرده ایم. در اسناد Oracle، خواهید دید که کلاس Class عمومی است! 
https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html
Integer.class
نه فقط Class
، بلکه بیشتر است Class<Integer>
. نوع شی String.class
فقط نیست Class
، بلکه بیشتر Class<String>
و غیره است. اگر هنوز مشخص نیست، سعی کنید یک پارامتر نوع را به مثال قبلی اضافه کنید:
public class Main2 {
public static void main(String[] args) {
Class<Integer> classInt = Integer.class;
// Compilation error!
Class<String> classInt2 = Integer.class;
Class<String> classString = String.class;
// Compilation error!
Class<Double> classString2 = String.class;
}
}
و اکنون، با استفاده از این دانش، می توانیم پاک کردن نوع را دور بزنیم و وظیفه خود را انجام دهیم! بیایید سعی کنیم اطلاعاتی در مورد پارامتر نوع بدست آوریم. نوع استدلال ما این خواهد بود MySecretClass
:
public class MySecretClass {
public MySecretClass() {
System.out.println("A MySecretClass object was created successfully!");
}
}
و در اینجا نحوه استفاده از راه حل خود در عمل آمده است:
public class TestClass<T> {
Class<T> typeParameterClass;
public TestClass(Class<T> typeParameterClass) {
this.typeParameterClass = typeParameterClass;
}
public T createNewT() throws IllegalAccessException, InstantiationException {
T t = typeParameterClass.newInstance();
return t;
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
MySecretClass secret = testString.createNewT();
}
}
خروجی کنسول:
A MySecretClass object was created successfully!
ما فقط آرگومان کلاس مورد نیاز را به سازنده کلاس عمومی خود منتقل کردیم:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
این به ما اجازه داد تا اطلاعات مربوط به نوع آرگومان را ذخیره کنیم و از پاک شدن کامل آن جلوگیری کنیم. در نتیجه توانستیم یک T
شی بسازیم! :) با آن، درس امروز به پایان می رسد. هنگام کار با ژنریک ها همیشه باید پاک کردن نوع را به خاطر بسپارید. این راهحل چندان راحت به نظر نمیرسد، اما باید بدانید که در زمان ایجاد، ژنریکها بخشی از زبان جاوا نبودند. این ویژگی، که به ما کمک میکند مجموعههای پارامتری شده و خطاها را در حین کامپایل ایجاد کنیم، بعداً مورد استفاده قرار گرفت. در برخی از زبان های دیگر که شامل ژنریک های نسخه اول بودند، هیچ گونه پاک کردنی وجود ندارد (مثلاً در سی شارپ). در ضمن، ما هنوز مطالعه ژنریک را تمام نکرده ایم! در درس بعدی با چند ویژگی دیگر ژنریک آشنا خواهید شد. فعلاً خوب است که یکی دو کار را حل کنیم! :)
GO TO FULL VERSION