أهلاً! دعونا نواصل دراستنا للأدوية. لقد اكتسبت بالفعل قدرًا كبيرًا من المعرفة عنها من الدروس السابقة (حول استخدام varargs عند العمل مع الأدوية العامة
وحول محو الكتابة
)، ولكن هناك موضوعًا مهمًا لم نأخذه بعين الاعتبار بعد - أحرف البدل . هذه ميزة مهمة جدًا للأدوية العامة. لدرجة أننا خصصنا لها درسا منفصلا! ومع ذلك، لا يوجد شيء معقد بشكل خاص فيما يتعلق بأحرف البدل. سترى ذلك على الفور :) دعونا نلقي نظرة على مثال:
public class Main {
public static void main(String[] args) {
String str = new String("Test!");
// No problem
Object obj = str;
List<String> strings = new ArrayList<String>();
// Compilation error!
List<Object> objects = strings;
}
}
ما الذي يحدث هنا؟ نرى حالتين متشابهتين للغاية. في هذه الحالة، نلقي String
كائنًا على Object
كائن. لا توجد مشاكل هنا – كل شيء يعمل كما هو متوقع. ولكن في الحالة الثانية، يقوم المترجم بإنشاء خطأ. لكننا نفعل نفس الشيء، أليس كذلك؟ هذه المرة نحن ببساطة نستخدم مجموعة من الكائنات المتعددة. ولكن لماذا يحدث الخطأ؟ ماهو الفرق؟ هل نقوم بإلقاء String
كائن واحد على Object
20 كائنًا؟ هناك فرق مهم بين كائن ومجموعة من الكائنات . إذا B
كان الفصل تابعًا للفئة A
، فهو Collection<B>
ليس تابعًا لـ Collection<A>
. ولهذا السبب لم نتمكن من إرسالنا List<String>
إلى ملف List<Object>
. String
هو طفل Object
، ولكن List<String>
ليس طفلا List<Object>
. قد لا يبدو هذا بديهيًا للغاية. لماذا جعلها مبدعو اللغة بهذه الطريقة؟ لنتخيل أن المترجم لا يعطينا خطأ:
List<String> strings = new ArrayList<String>();
List<Object> objects = strings;
وفي هذه الحالة يمكننا، على سبيل المثال، القيام بما يلي:
objects.add(new Object());
String s = strings.get(0);
نظرًا لأن المترجم لم يعطنا أي خطأ وسمح لنا بإنشاء مرجع List<Object>
يشير إلى strings
، فيمكننا إضافة أي Object
كائن قديم إلى strings
المجموعة! وبالتالي، فقد فقدنا الضمان بأن مجموعتنا تحتوي فقط على الكائنات String
المحددة بواسطة وسيطة النوع في استدعاء النوع العام . وبعبارة أخرى، فقد فقدنا الميزة الرئيسية للأدوية الجنيسة، ألا وهي سلامة النوع. ولأن المترجم لم يمنعنا من القيام بذلك، فلن نحصل على خطأ إلا في وقت التشغيل، وهو دائمًا أسوأ بكثير من خطأ الترجمة. لمنع مثل هذه المواقف، يعطينا المترجم خطأ:
// Compilation error
List<Object> objects = strings;
... ويذكرنا أنه List<String>
ليس من نسل List<Object>
. هذه قاعدة صارمة بالنسبة للأدوية الجنيسة، ويجب تذكرها عند التعامل معها. هيا لنذهب. لنفترض أن لدينا تسلسل هرمي فئة صغيرة:
public class Animal {
public void feed() {
System.out.println("Animal.feed()");
}
}
public class Pet extends Animal {
public void call() {
System.out.println("Pet.call()");
}
}
public class Cat extends Pet {
public void meow() {
System.out.println("Cat.meow()");
}
}
يعلو التسلسل الهرمي فئة حيوانية بسيطة، والتي ورثها الحيوان الأليف. الحيوانات الأليفة لديها فئتان فرعيتان: الكلب والقط. لنفترض الآن أننا بحاجة إلى إنشاء iterateAnimals()
طريقة بسيطة. يجب أن تأخذ الطريقة مجموعة من أي حيوانات ( Animal
،،،،، )، وتكررها على جميع العناصر، وتعرض رسالة على وحدة التحكم أثناء كل تكرار Pet
. دعونا نحاول كتابة مثل هذه الطريقة: Cat
Dog
public static void iterateAnimals(Collection<Animal> animals) {
for(Animal animal: animals) {
System.out.println("Another iteration in the loop!");
}
}
يبدو أن المشكلة قد تم حلها! ومع ذلك، كما علمنا مؤخرًا List<Cat>
، List<Dog>
وليسوا List<Pet>
من نسل List<Animal>
! هذا يعني أنه عندما نحاول استدعاء iterateAnimals()
الطريقة مع قائمة القطط، نحصل على خطأ في الترجمة:
import java.util.*;
public class Main3 {
public static void iterateAnimals(Collection<Animal> animals) {
for(Animal animal: animals) {
System.out.println("Another iteration in the loop!");
}
}
public static void main(String[] args) {
List<Cat> cats = new ArrayList<>();
cats.add(new Cat());
cats.add(new Cat());
cats.add(new Cat());
cats.add(new Cat());
// Compilation error!
iterateAnimals(cats);
}
}
الوضع لا يبدو جيداً بالنسبة لنا! هل علينا أن نكتب طرقًا منفصلة لتعداد كل نوع من الحيوانات؟ في الواقع، لا، لا نفعل ذلك :) وكما يحدث، تساعدنا أحرف البدل في هذا! يمكننا حل المشكلة بطريقة واحدة بسيطة باستخدام البنية التالية:
public static void iterateAnimals(Collection<? extends Animal> animals) {
for(Animal animal: animals) {
System.out.println("Another iteration in the loop!");
}
}
هذا هو البدل. بتعبير أدق، هذا هو الأول من عدة أنواع من أحرف البدل. يُعرف باسم أحرف البدل ذات الحدود العليا ويتم التعبير عنه بـ ? يمتد . ماذا يخبرنا هذا البناء؟ هذا يعني أن الطريقة تقبل مجموعة من Animal
الكائنات أو مجموعة كائنات من أي فئة تنحدر من Animal
(؟ يمتد الحيوان). بمعنى آخر، يمكن للطريقة أن تقبل مجموعة من الكائنات Animal
، Pet
أو Dog
، أو، Cat
ولا فرق. دعونا نقنع أنفسنا بأنه يعمل:
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
animals.add(new Animal());
animals.add(new Animal());
List<Pet> pets = new ArrayList<>();
pets.add(new Pet());
pets.add(new Pet());
List<Cat> cats = new ArrayList<>();
cats.add(new Cat());
cats.add(new Cat());
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
iterateAnimals(animals);
iterateAnimals(pets);
iterateAnimals(cats);
iterateAnimals(dogs);
}
إخراج وحدة التحكم:
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
لقد أنشأنا إجمالي 4 مجموعات و8 كائنات، وهناك 8 إدخالات بالضبط على وحدة التحكم. كل شيء يعمل بشكل رائع! :) أتاح لنا حرف البدل إمكانية ملاءمة المنطق الضروري المرتبط بأنواع معينة بسهولة في طريقة واحدة. لقد ألغينا الحاجة إلى كتابة طريقة منفصلة لكل نوع من الحيوانات. تخيل عدد الطرق التي سنحتاجها إذا تم استخدام تطبيقنا من قبل حديقة حيوان أو مكتب بيطري :) ولكن الآن دعونا نلقي نظرة على موقف مختلف. يبقى التسلسل الهرمي للميراث الخاص بنا دون تغيير: فئة المستوى الأعلى هي Animal
، مع Pet
فئة أدناه مباشرة، وفئات Cat
و Dog
في المستوى التالي. أنت الآن بحاجة إلى إعادة كتابة iterateAnimals()
الطريقة بحيث تعمل مع أي نوع من الحيوانات، باستثناء الكلاب . أي أنه يجب أن يقبل Collection<Animal>
، Collection<Pet>
أو Collection<Car>
، لكن لا ينبغي أن يعمل معه Collection<Dog>
. كيف نستطيع إنجاز هذا؟ يبدو أننا نواجه مرة أخرى احتمال كتابة طريقة منفصلة لكل نوع:/ وإلا كيف نشرح للمترجم ما نريد أن يحدث؟ انها في الواقع بسيطة جدا! مرة أخرى، تأتي أحرف البدل لمساعدتنا هنا. ولكن هذه المرة سوف نستخدم نوعًا آخر من أحرف البدل - أحرف البدل ذات الحدود المنخفضة ، والتي يتم التعبير عنها باستخدام super .
public static void iterateAnimals(Collection<? super Cat> animals) {
for(int i = 0; i < animals.size(); i++) {
System.out.println("Another iteration in the loop!");
}
}
هنا المبدأ مشابه. يخبر البناء <? super Cat>
المترجم أن iterateAnimals()
الطريقة يمكن أن تقبل كمدخلات مجموعة من Cat
الكائنات أو أي سلف للفئة Cat
كمدخل. في هذه الحالة، فإن Cat
الفصل وأصله Pet
وأصل أصله Animal
يتطابقون جميعًا مع هذا الوصف. Dog
لا يتطابق الفصل مع القيود التي وضعناها، لذا فإن محاولة استخدام الطريقة مع وسيطة ستؤدي List<Dog>
إلى خطأ في الترجمة:
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
animals.add(new Animal());
animals.add(new Animal());
List<Pet> pets = new ArrayList<>();
pets.add(new Pet());
pets.add(new Pet());
List<Cat> cats = new ArrayList<>();
cats.add(new Cat());
cats.add(new Cat());
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
iterateAnimals(animals);
iterateAnimals(pets);
iterateAnimals(cats);
// Compilation error!
iterateAnimals(dogs);
}
لقد قمنا بحل مشكلتنا، ومرة أخرى تبين أن أحرف البدل مفيدة للغاية :) وبهذا يكون الدرس قد انتهى. الآن ترى مدى أهمية الأدوية الجنيسة في دراستك للغة Java - لقد تلقينا 4 دروس كاملة عنها! لكنك الآن على دراية جيدة بالموضوع ويمكنك إثبات مهاراتك في مقابلات العمل :) والآن، حان الوقت للعودة إلى المهام! أفضل النجاح في دراستك! :)
GO TO FULL VERSION