مرحبًا! نواصل سلسلة محاضراتنا حول الأدوية الجنيسة. في السابق ، اكتشفنا بشكل عام ما هو ولماذا هو مطلوب. سنتحدث اليوم عن بعض ميزات الأدوية الجنيسة ونلقي نظرة على بعض المخاطر عند العمل معها. يذهب! تحدثنا في المحاضرة الأخيرة عن الفرق بين الأنواع العامة والأنواع الخام . في حالة نسيانك، فإن Raw Type هو فئة عامة تمت إزالة نوعها منها.
List list = new ArrayList();
هنا مثال. نحن هنا لا نحدد نوع الكائنات التي سيتم وضعها في ملف List
. إذا حاولنا إنشاء واحد List
وإضافة بعض الكائنات إليه، فسنرى تحذيرًا في IDea:
“Unchecked call to add(E) as a member of raw type of java.util.List”.
ولكننا تحدثنا أيضًا عن حقيقة أن الأدوية العامة ظهرت فقط في إصدار Java 5 من اللغة. وبحلول وقت إصدارها، كان المبرمجون قد كتبوا الكثير من التعليمات البرمجية باستخدام الأنواع الخام، وحتى لا تتوقف عن العمل، كانت القدرة على تم الحفاظ على إنشاء الأنواع الخام والعمل بها في Java. ومع ذلك، تبين أن هذه المشكلة أوسع من ذلك بكثير. كما تعلم، يتم تحويل كود Java إلى كود ثانوي خاص، والذي يتم تنفيذه بعد ذلك بواسطة جهاز Java الظاهري. وإذا قمنا أثناء عملية الترجمة بوضع معلومات حول أنواع المعلمات في الكود الثانوي، فسيؤدي ذلك إلى كسر جميع التعليمات البرمجية المكتوبة مسبقًا، لأنه قبل Java 5 لم تكن هناك أنواع معلمات! عند العمل مع الأدوية الجنيسة، هناك ميزة واحدة مهمة جدًا عليك أن تتذكرها. يطلق عليه محو النوع. يكمن جوهرها في حقيقة أنه لا يتم تخزين أي معلومات حول نوع المعلمة داخل الفصل. تتوفر هذه المعلومات فقط في مرحلة التجميع ويتم مسحها (يصبح غير قابل للوصول) في وقت التشغيل. إذا حاولت وضع كائن من نوع خاطئ في ملفك List<String>
، فسيقوم المترجم بإلقاء خطأ. هذا هو بالضبط ما حققه منشئو اللغة من خلال إنشاء فحوصات عامة في مرحلة التجميع. ولكن عندما يتحول كل كود Java الذي تكتبه إلى كود ثانوي، فلن تكون هناك معلومات حول أنواع المعلمات. داخل الكود الثانوي، 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
. الأمر بسيط جدًا: فهو في الأساس عبارة عن "مجموعة" صغيرة من كائنين، يتم وضعهما هناك فورًا عند إنشاء الكائن. يحتوي على كائنين كحقول T
. عند تنفيذ الطريقة، createAndAdd2Values()
يجب تحويل الكائنين اللذين تم تمريرهما Object a
إلى Object b
النوع الخاص بنا T
، وبعد ذلك سيتم إضافتهما إلى الكائن TestClass
. في الطريقة main()
التي ننشئ بها TestClass<Integer>
، أي من حيث الجودة T
سيكون لدينا Integer
. لكن في الوقت نفسه، createAndAdd2Values()
نقوم بتمرير رقم Double
وكائن إلى الطريقة String
. هل تعتقد أن برنامجنا سينجح؟ بعد كل شيء، لقد حددنا كنوع معلمة Integer
، ولكن String
بالتأكيد لا يمكن إرسالها إلى Integer
! لنقم بتشغيل الطريقة main()
والتحقق. إخراج وحدة التحكم: سلسلة اختبار 22.111 نتيجة غير متوقعة! لماذا حدث هذا؟ على وجه التحديد بسبب محو النوع. أثناء تجميع التعليمات البرمجية، تم مسح المعلومات حول نوع المعلمة 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());
}
}
إخراج وحدة التحكم: صحيح صحيح يبدو أننا أنشأنا مجموعات بثلاثة أنواع مختلفة من المعلمات - 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
نظرًا لوجود فرق كبير بين المصفوفات والأسماء العامة، فمن الممكن أن يكون لديهم مشكلات في التوافق. أولاً، لا يمكنك إنشاء مصفوفة من الكائنات العامة أو حتى مجرد مصفوفة مكتوبة. يبدو مربكا بعض الشيء؟ دعونا نلقي نظرة فاحصة. على سبيل المثال، لا يمكنك القيام بأي من هذا في Java:
new List<T>[]
new List<String>[]
new T[]
إذا حاولنا إنشاء مصفوفة من القوائم List<String>
، فسنحصل على خطأ تجميعي لإنشاء مصفوفة عامة:
import java.util.List;
public class Main2 {
public static void main(String[] args) {
//ошибка компиляции! 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
سيتم السماح بإنشاء المصفوفة، ولن يشتكي المترجم. إليك ما يمكننا فعله في هذه الحالة: في السطر الأول، قمنا بإنشاء مصفوفة من الأوراق List<String>[] stringLists
. تحتوي مجموعتنا على واحد List<String>
. في السطر 2 نقوم بإنشاء قائمة بالأرقام List<Integer>
. في السطر 3 نقوم بتعيين مصفوفتنا List<String>[]
لمتغير Object[] objects
. تتيح لك لغة Java القيام بذلك: X
يمكنك وضع كل من الكائنات X
والكائنات من جميع الفئات الفرعية في مصفوفة من الكائنات Х
. وفقا لذلك، Objects
يمكنك وضع أي شيء على الإطلاق في المصفوفة. في السطر الرابع نستبدل العنصر الوحيد للمصفوفة objects (List<String>)
بقائمة List<Integer>
. ونتيجة لذلك، وضعنا List<Integer>
في مصفوفتنا، والتي كانت مخصصة للتخزين فقط List<String>
! سنواجه خطأ فقط عندما يصل الكود إلى السطر 5. سيتم طرح استثناء أثناء تنفيذ البرنامج ClassCastException
. لذلك، تم تقديم الحظر على إنشاء مثل هذه المصفوفات في لغة جافا - وهذا يسمح لنا بتجنب مثل هذه المواقف.
كيف يمكنني تجاوز محو الكتابة؟
حسنًا، لقد تعلمنا عن محو الكتابة. دعونا نحاول خداع النظام! :) المهمة: لدينا فئة عامةTestClass<T>
. نحن بحاجة إلى إنشاء طريقة من شأنها createNewT()
إنشاء وإرجاع كائن جديد من النوع Т
. ولكن هذا من المستحيل القيام به، أليس كذلك؟ سيتم مسح جميع المعلومات المتعلقة بالنوع Т
أثناء التجميع، وأثناء تشغيل البرنامج، لن نتمكن من معرفة نوع الكائن الذي نحتاج إلى إنشائه. في الواقع، هناك طريقة واحدة صعبة. ربما تتذكر أن هناك فصلًا دراسيًا في Java 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
ولكن هنا ميزة واحدة لم نتحدث عنها. في وثائق أوراكل سترى أن الفئة هي فئة عامة! تقول الوثائق: "T هو نوع الفئة التي تم تصميمها بواسطة كائن الفئة هذا." إذا قمنا بترجمة هذا من لغة التوثيق إلى لغة بشرية، فهذا يعني أن فئة الكائن Integer.class
ليست فقط Class
، بل Class<Integer>
. نوع الكائن string.class
ليس فقط Class
، Class<String>
وما إلى ذلك. إذا كان الأمر لا يزال غير واضح، فحاول إضافة معلمة نوع إلى المثال السابق:
public class Main2 {
public static void main(String[] args) {
Class<Integer> classInt = Integer.class;
//ошибка компиляции!
Class<String> classInt2 = Integer.class;
Class<String> classString = String.class;
//ошибка компиляции!
Class<Double> classString2 = String.class;
}
}
والآن، باستخدام هذه المعرفة، يمكننا تجاوز محو الكتابة وحل مشكلتنا! دعونا نحاول الحصول على معلومات حول نوع المعلمة. سيتم لعب دورها من قبل الفصل MySecretClass
:
public class MySecretClass {
public MySecretClass() {
System.out.println("Объект секретного класса успешно создан!");
}
}
إليك كيفية استخدام حلنا عمليًا:
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();
}
}
إخراج وحدة التحكم:
Объект секретного класса успешно создан!
لقد قمنا ببساطة بتمرير معلمة الفئة المطلوبة إلى مُنشئ فئتنا العامة:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
وبفضل هذا، قمنا بحفظ المعلومات حول نوع المعلمة وحمايتها من المسح. ونتيجة لذلك، تمكنا من إنشاء كائن T
! :) وبهذا تنتهي محاضرة اليوم. يعد محو الكتابة أمرًا يجب مراعاته دائمًا عند العمل مع الأدوية الجنيسة. لا يبدو هذا مناسبًا جدًا، لكن عليك أن تفهم أن الأدوية العامة لم تكن جزءًا من لغة Java عند إنشائها. هذه ميزة تمت إضافتها لاحقًا وتساعدنا في إنشاء مجموعات مكتوبة ورصد الأخطاء في مرحلة التجميع. بعض اللغات الأخرى التي كانت الأدوية العامة موجودة منذ الإصدار 1 لا تحتوي على محو الكتابة (على سبيل المثال، C#). ومع ذلك، فإننا لم ننتهي من دراسة الأدوية الجنيسة! في المحاضرة التالية سوف تتعرف على العديد من الميزات الأخرى للعمل معهم. وفي غضون ذلك، سيكون من الجميل حل بعض المشاكل! :)
GO TO FULL VERSION