مقدمة
بدءًا من JSE 5.0، تمت إضافة الأدوية العامة إلى ترسانة لغة Java.ما هي الأدوية العامة في جافا؟
الأدوية العامة هي آلية Java الخاصة لتنفيذ البرمجة العامة — وهي طريقة لوصف البيانات والخوارزميات التي تتيح لك العمل مع أنواع بيانات مختلفة دون تغيير وصف الخوارزميات. يحتوي موقع Oracle على برنامج تعليمي منفصل مخصص للأدوية العامة: " درس ". لفهم الأدوية العامة، عليك أولاً معرفة سبب الحاجة إليها وما الذي تقدمه. يقول قسم " لماذا تستخدم الأدوية العامة؟ " في البرنامج التعليمي أن هناك غرضين أقوى هما التحقق من النوع في وقت الترجمة وإلغاء الحاجة إلى قوالب صريحة. دعونا نستعد لبعض الاختبارات في مترجم Java الخاص بنا على الإنترنت Tutorialspoint . لنفترض أن لديك الكود التالي:import java.util.*;
public class HelloWorld {
public static void main(String []args) {
List list = new ArrayList();
list.add("Hello");
String text = list.get(0) + ", world!";
System.out.print(text);
}
}
سيتم تشغيل هذا الرمز بشكل جيد تمامًا. ولكن ماذا لو جاء إلينا الرئيس ويقول "مرحبًا أيها العالم!" عبارة مفرطة الاستخدام ويجب عليك إرجاع "مرحبًا" فقط؟ سنقوم بإزالة الكود الذي يربط "،world!" هذا يبدو غير ضار بما فيه الكفاية، أليس كذلك؟ لكننا في الواقع حصلنا على خطأ في وقت الترجمة:
error: incompatible types: Object cannot be converted to String
المشكلة هي أن قائمتنا تخزن الكائنات. السلسلة هي سليل Object (نظرًا لأن جميع فئات Java ترث Object ضمنيًا )، مما يعني أننا بحاجة إلى طاقم تمثيل واضح، لكننا لم نضيف واحدًا. أثناء عملية التسلسل، سيتم استدعاء أسلوب String.valueOf(obj) الثابت باستخدام الكائن. في النهاية، سيتم استدعاء أسلوب toString الخاص بفئة الكائن . بمعنى آخر، تحتوي قائمتنا على كائن . هذا يعني أنه عندما نحتاج إلى نوع معين (وليس Object )، سيتعين علينا إجراء تحويل النوع بأنفسنا:
import java.util.*;
public class HelloWorld {
public static void main(String []args) {
List list = new ArrayList();
list.add("Hello!");
list.add(123);
for (Object str : list) {
System.out.println("-" + (String)str);
}
}
}
ومع ذلك، في هذه الحالة، نظرًا لأن القائمة تأخذ كائنات، فيمكنها تخزين ليس فقط String s، ولكن أيضًا Integer s. لكن أسوأ شيء هو أن المترجم لا يرى أي خطأ هنا. والآن سنحصل على خطأ في وقت التشغيل (المعروف باسم "خطأ وقت التشغيل"). الخطأ سيكون:
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
يجب أن توافق على أن هذا ليس جيدًا جدًا. وكل هذا لأن المترجم ليس ذكاءً اصطناعيًا قادرًا دائمًا على تخمين نية المبرمج بشكل صحيح. قدمت Java SE 5 أدوية عامة للسماح لنا بإخبار المترجم عن نوايانا - وعن الأنواع التي سنستخدمها. نقوم بإصلاح الكود الخاص بنا عن طريق إخبار المترجم بما نريده:
import java.util.*;
public class HelloWorld {
public static void main(String []args) {
List<String> list = new ArrayList<>();
list.add("Hello!");
list.add(123);
for (Object str : list) {
System.out.println("-" + str);
}
}
}
كما ترون، لم نعد بحاجة إلى تحويل إلى سلسلة . بالإضافة إلى ذلك، لدينا أقواس زاوية تحيط بوسيطة النوع. الآن لن يسمح لنا المترجم بتجميع الفصل حتى نزيل السطر الذي يضيف 123 إلى القائمة، لأن هذا عدد صحيح . وسوف يخبرنا بذلك. كثير من الناس يطلقون على الأدوية العامة اسم "السكر النحوي". وهم على حق، لأنه بعد تجميع الأدوية العامة، تصبح بالفعل نفس نوع التحويلات. دعونا نلقي نظرة على الكود الثانوي للفئات المترجمة: واحدة تستخدم قالبًا صريحًا وأخرى تستخدم الأدوية العامة: بعد التجميع، يتم مسح جميع الأدوية العامة. وهذا ما يسمى " محو النوع
". تم تصميم محو الكتابة والأسماء العامة لتكون متوافقة مع الإصدارات القديمة من JDK مع السماح للمترجم في نفس الوقت بالمساعدة في تعريفات النوع في الإصدارات الجديدة من Java.
أنواع خام
عند الحديث عن الأدوية العامة، لدينا دائمًا فئتان: الأنواع ذات المعلمات والأنواع الأولية. الأنواع الأولية هي أنواع تحذف "توضيح النوع" بين قوسين زاوية: الأنواع ذات المعلمات، من ناحية، تتضمن "توضيحًا": كما ترون، استخدمنا بنية غير عادية، تم تمييزها بسهم في لقطة الشاشة. هذا هو بناء الجملة الخاص الذي تمت إضافته إلى Java SE 7. ويسمى " الماسة ". لماذا؟ تشكل الأقواس الزاوية المعينة: <> . يجب أن تعلم أيضًا أن بناء الجملة الماسي مرتبط بمفهوم " استدلال النوع ". بعد كل شيء، المترجم، الذي يرى <> على اليمين، ينظر إلى الجانب الأيسر من عامل التعيين، حيث يجد نوع المتغير الذي تم تعيين قيمته. وبناء على ما يجده في هذا الجزء، فهو يفهم نوع القيمة الموجودة على اليمين. في الواقع، إذا تم إعطاء نوع عام على اليسار، ولكن ليس على اليمين، فيمكن للمترجم استنتاج النوع:import java.util.*;
public class HelloWorld {
public static void main(String []args) {
List<String> list = new ArrayList();
list.add("Hello, World");
String data = list.get(0);
System.out.println(data);
}
}
لكن هذا يمزج بين النمط الجديد والأدوية العامة والنمط القديم بدونها. وهذا غير مرغوب فيه للغاية. عند تجميع الكود أعلاه، نحصل على الرسالة التالية:
Note: HelloWorld.java uses unchecked or unsafe operations
في الواقع، يبدو السبب الذي يجعلك بحاجة إلى إضافة الماسة هنا غير مفهوم. ولكن هنا مثال:
import java.util.*;
public class HelloWorld {
public static void main(String []args) {
List<String> list = Arrays.asList("Hello", "World");
List<Integer> data = new ArrayList(list);
Integer intNumber = data.get(0);
System.out.println(data);
}
}
ستتذكر أن ArrayList يحتوي على مُنشئ ثانٍ يأخذ المجموعة كوسيطة. وهنا يكمن شيء شرير مخفي. بدون بناء الجملة الماسي، لا يفهم المترجم أنه تم خداعه. مع بناء الجملة الماسي، يتم ذلك. لذا، القاعدة رقم 1 هي: استخدم دائمًا الصيغة الماسية مع الأنواع ذات المعلمات. وإلا فإننا نخاطر بفقدان المكان الذي نستخدم فيه الأنواع الخام. لإزالة التحذيرات "يستخدم عمليات غير محددة أو غير آمنة"، يمكننا استخدام التعليق التوضيحي @SuppressWarnings("unchecked") على طريقة أو فئة. لكن فكر في سبب قرارك باستخدامه. تذكر القاعدة رقم واحد. ربما تحتاج إلى إضافة وسيطة نوع.
طرق جافا العامة
تتيح لك الأدوية العامة إنشاء طرق يتم تحديد معلمات أنواع المعلمات ونوع الإرجاع فيها. تم تخصيص قسم منفصل لهذه الإمكانية في برنامج Oracle التعليمي: " الطرق العامة ". من المهم أن تتذكر بناء الجملة الذي تم تدريسه في هذا البرنامج التعليمي:- ويتضمن قائمة بمعلمات النوع داخل قوسين زاوية؛
- تظهر قائمة معلمات النوع قبل نوع الإرجاع الخاص بالأسلوب.
import java.util.*;
public class HelloWorld {
public static class Util {
public static <T> T getValue(Object obj, Class<T> clazz) {
return (T) obj;
}
public static <T> T getValue(Object obj) {
return (T) obj;
}
}
public static void main(String []args) {
List list = Arrays.asList("Author", "Book");
for (Object element : list) {
String data = Util.getValue(element, String.class);
System.out.println(data);
System.out.println(Util.<String>getValue(element));
}
}
}
إذا نظرت إلى فئة Util ، سترى أن لديها طريقتين عامتين. بفضل إمكانية استنتاج النوع، يمكننا إما الإشارة إلى النوع مباشرة للمترجم، أو يمكننا تحديده بأنفسنا. يتم عرض كلا الخيارين في المثال. بالمناسبة، بناء الجملة منطقي للغاية إذا فكرت فيه. عند الإعلان عن طريقة عامة، نحدد معلمة النوع قبل الطريقة، لأنه إذا أعلنا عن معلمة النوع بعد الطريقة، فلن يتمكن JVM من معرفة النوع الذي يجب استخدامه. وبناءً على ذلك، نعلن أولاً أننا سنستخدم معلمة النوع T ، ثم نقول إننا سنعيد هذا النوع. بطبيعة الحال، سوف يفشل Util.<Integer>getValue(element, String.class) بسبب خطأ: الأنواع غير المتوافقة: لا يمكن تحويل Class<String> إلى Class<Integer> . عند استخدام الطرق العامة، يجب أن تتذكر دائمًا مسح الكتابة. لنلقي نظرة على مثال:
import java.util.*;
public class HelloWorld {
public static class Util {
public static <T> T getValue(Object obj) {
return (T) obj;
}
}
public static void main(String []args) {
List list = Arrays.asList(2, 3);
for (Object element : list) {
System.out.println(Util.<Integer>getValue(element) + 1);
}
}
}
هذا سوف يعمل على ما يرام. ولكن فقط طالما أن المترجم يفهم أن نوع الإرجاع للطريقة التي يتم استدعاؤها هو عدد صحيح . استبدل بيان إخراج وحدة التحكم بالسطر التالي:
System.out.println(Util.getValue(element) + 1);
نحصل على خطأ:
bad operand types for binary operator '+', first type: Object, second type: int.
بمعنى آخر، حدث محو الكتابة. يرى المترجم أنه لم يحدد أحد النوع، لذلك تتم الإشارة إلى النوع على أنه كائن وتفشل الطريقة مع حدوث خطأ.
فئات عامة
ليس فقط الأساليب يمكن تحديد معلماتها. يمكن للفصول كذلك. قسم "الأنواع العامة" في برنامج Oracle التعليمي مخصص لهذا الغرض. دعونا نفكر في مثال:public static class SomeType<T> {
public <E> void test(Collection<E> collection) {
for (E element : collection) {
System.out.println(element);
}
}
public void test(List<Integer> collection) {
for (Integer element : collection) {
System.out.println(element);
}
}
}
كل شيء بسيط هنا. إذا استخدمنا الفئة العامة، تتم الإشارة إلى معلمة النوع بعد اسم الفئة. لنقم الآن بإنشاء مثيل لهذه الفئة في الطريقة الرئيسية :
public static void main(String []args) {
SomeType<String> st = new SomeType<>();
List<String> list = Arrays.asList("test");
st.test(list);
}
سيتم تشغيل هذا الرمز بشكل جيد. يرى المترجم أن هناك قائمة أرقام ومجموعة من السلاسل . ولكن ماذا لو قمنا بإزالة معلمة النوع وقمنا بما يلي:
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
نحصل على خطأ:
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
مرة أخرى، هذا هو محو الكتابة. نظرًا لأن الفصل لم يعد يستخدم معلمة النوع، قرر المترجم أنه بما أننا مررنا قائمة ، فإن الطريقة مع List<Integer> هي الأكثر ملاءمة. ونفشل بخطأ. لذلك، لدينا القاعدة رقم 2: إذا كان لديك فئة عامة، فحدد دائمًا معلمات النوع.
قيود
يمكننا تقييد الأنواع المحددة في الأساليب العامة والفئات. على سبيل المثال، لنفترض أننا نريد أن تقبل الحاوية رقمًا فقط كوسيطة النوع. تم توضيح هذه الميزة في قسم معلمات النوع المحدود في برنامج Oracle التعليمي. لنلقي نظرة على مثال:import java.util.*;
public class HelloWorld {
public static class NumberContainer<T extends Number> {
private T number;
public NumberContainer(T number) { this.number = number; }
public void print() {
System.out.println(number);
}
}
public static void main(String []args) {
NumberContainer number1 = new NumberContainer(2L);
NumberContainer number2 = new NumberContainer(1);
NumberContainer number3 = new NumberContainer("f");
}
}
كما ترون، قمنا بتقييد معلمة النوع على فئة/واجهة الرقم أو أحفادها. لاحظ أنه لا يمكنك تحديد فئة فحسب، بل يمكنك أيضًا تحديد الواجهات. على سبيل المثال:
public static class NumberContainer<T extends Number & Comparable> {
تدعم الأدوية العامة أيضًا أحرف البدل
وهي مقسمة إلى ثلاثة أنواع:
- أحرف البدل ذات الحدود العليا — < ? يمتد الرقم >
- أحرف البدل غير المحدودة - < ? >
- أحرف البدل ذات الحدود السفلية — < ? عدد صحيح فائق >
- استخدم حرف بدل ممتد عندما تحصل فقط على قيم من البنية.
- استخدم حرف البدل الفائق عندما تضع القيم في البنية فقط.
- ولا تستخدم حرف البدل عندما يرغب كلاكما في الانتقال من/إلى البنية.
public static class TestClass {
public static void print(List<? extends String> list) {
list.add("Hello, World!");
System.out.println(list.get(0));
}
}
public static void main(String []args) {
List<String> list = new ArrayList<>();
TestClass.print(list);
}
ولكن إذا قمت باستبدال الامتدادات بـ super ، فكل شيء على ما يرام. لأننا نملأ القائمة بقيمة قبل عرض محتوياتها، فهي مستهلكة . وبناء على ذلك، نستخدم السوبر.
ميراث
الأدوية الجنيسة لها ميزة أخرى مثيرة للاهتمام: الميراث. تم توضيح طريقة عمل الوراثة للأدوية العامة ضمن " الأدوية العامة والوراثة والأنواع الفرعية " في برنامج Oracle التعليمي. الشيء المهم هو أن تتذكر وتتعرف على ما يلي. لا نستطيع أن نفعل هذا:List<CharSequence> list1 = new ArrayList<String>();
لأن الميراث يعمل بشكل مختلف مع الأدوية العامة: وإليك مثال جيد آخر سيفشل بسبب وجود خطأ:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
مرة أخرى، كل شيء بسيط هنا. List<String> ليس من سلالة List<Object> ، على الرغم من أن String هو من سلالة Object . لتعزيز ما تعلمته، نقترح عليك مشاهدة درس فيديو من دورة Java الخاصة بنا
المزيد من القراءة: |
---|
GO TO FULL VERSION