أهلاً! نواصل سلسلة دروسنا حول الأدوية الجنيسة. لقد حصلنا سابقًا
على فكرة عامة عن ماهيتها وسبب الحاجة إليها. سنتعلم اليوم المزيد عن بعض ميزات الأدوية الجنيسة وعن العمل معها. دعنا نذهب! تحدثنا في الدرس الأخير عن الفرق بين
الأنواع العامة والأنواع الخام . النوع الخام هو فئة عامة تمت إزالة نوعها.
تقول الوثائق، "T - نوع الفئة التي تم تصميمها بواسطة كائن الفئة هذا." بترجمة هذا من لغة التوثيق إلى كلام عادي، نفهم أن فئة الكائن
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 إلى تنسيق مترجم خاص يسمى bytecode، والذي يتم تنفيذه بعد ذلك بواسطة جهاز 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
. الأمر بسيط جدًا: إنها في الواقع "مجموعة" صغيرة من كائنين، يتم تخزينهما فورًا عند إنشاء الكائن. لديها 2 T
الحقول. عند createAndAdd2Values()
تنفيذ الطريقة، يجب تحويل الكائنين اللذين تم تمريرهما ( Object a
ويجب Object b
تحويلهما إلى T
النوع ثم إضافتهما إلى TestClass
الكائن. في main()
الطريقة، نقوم بإنشاء ملف TestClass<Integer>
، أي أن Integer
وسيطة النوع تحل محل Integer
معلمة النوع. كما نقوم أيضًا بتمرير a Double
و a 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
نظرًا لوجود فرق كبير بين المصفوفات والأسماء العامة، فقد يكون لديهم مشكلات في التوافق. قبل كل شيء، لا يمكنك إنشاء مصفوفة من الكائنات العامة أو حتى مجرد مصفوفة ذات معلمات. هل يبدو هذا مربكا بعض الشيء؟ لنلقي نظرة. على سبيل المثال، لا يمكنك القيام بأي من هذا في Java:
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
. تسمح لغة Java بذلك: يمكن لمجموعة من X
الكائنات تخزين X
الكائنات والكائنات من جميع الفئات الفرعية X
. وفقا لذلك، يمكنك وضع أي شيء على الإطلاق في Object
مجموعة. في السطر الرابع، نستبدل العنصر الوحيد في المصفوفة objects()
(a List<String>
) بـ List<Integer>
. وهكذا، قمنا بوضع List<Integer>
مصفوفة كانت مخصصة فقط لتخزين List<String>
الكائنات! سنواجه خطأ فقط عندما ننفذ السطر 5. ClassCastException
سيتم طرح A في وقت التشغيل. وفقا لذلك، تمت إضافة حظر على إنشاء مثل هذه المصفوفات إلى جافا. وهذا يتيح لنا تجنب مثل هذه المواقف.
كيف يمكنني التغلب على محو الكتابة؟
حسنًا، لقد تعلمنا عن محو الكتابة. دعونا نحاول خداع النظام! :) المهمة: لديناTestClass<T>
فئة عامة. نريد أن نكتب createNewT()
طريقة لهذه الفئة من شأنها إنشاء T
كائن جديد وإرجاعه. لكن هذا مستحيل، أليس كذلك؟ يتم مسح جميع المعلومات المتعلقة بالنوع T
أثناء التجميع، وفي وقت التشغيل لا يمكننا تحديد نوع الكائن الذي نحتاج إلى إنشائه. هناك في الواقع طريقة واحدة صعبة للقيام بذلك. ربما تتذكر أن 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
ولكن هنا جانب واحد لم نتحدث عنه. في وثائق أوراكل، سترى أن فئة الفصل عامة!
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
كائن! :) وبهذا ينتهي درس اليوم. يجب أن تتذكر دائمًا مسح الكتابة عند العمل مع الأدوية الجنيسة. لا يبدو هذا الحل مناسبًا جدًا، لكن يجب أن تفهم أن الأدوية العامة لم تكن جزءًا من لغة Java عند إنشائها. هذه الميزة، التي تساعدنا في إنشاء مجموعات ذات معلمات واكتشاف الأخطاء أثناء التجميع، تم تناولها لاحقًا. في بعض اللغات الأخرى التي تضمنت أسماء عامة من الإصدار الأول، لا يوجد مسح للنوع (على سبيل المثال، في C#). بالمناسبة، لم ننتهي من دراسة الأدوية الجنيسة! في الدرس التالي، سوف تتعرف على بعض الميزات الإضافية للأدوية الجنيسة. في الوقت الحالي، سيكون من الجيد حل بعض المهام! :)
GO TO FULL VERSION