تعد الأسئلة المتعلقة بـ OOP جزءًا لا يتجزأ من المقابلة الفنية لوظيفة مطور Java في إحدى شركات تكنولوجيا المعلومات. في هذه المقالة، سنتحدث عن مبدأ واحد من مبادئ OOP – تعدد الأشكال. سنركز على الجوانب التي يتم السؤال عنها غالبًا أثناء المقابلات، وسنقدم أيضًا بعض الأمثلة للتوضيح.
ما هو تعدد الأشكال في جافا؟
تعدد الأشكال هو قدرة البرنامج على التعامل مع الكائنات ذات الواجهة نفسها بنفس الطريقة، دون معلومات حول نوع الكائن المحدد. إذا أجبت على سؤال حول ماهية تعدد الأشكال، فمن المرجح أن يُطلب منك توضيح ما تقصده. دون إثارة مجموعة من الأسئلة الإضافية، اعرض كل شيء على القائم بإجراء المقابلة مرة أخرى. يمكنك البدء بحقيقة أن نهج OOP يتضمن بناء برنامج Java يعتمد على التفاعل بين الكائنات، والتي تعتمد على الفئات. الفصول عبارة عن مخططات (قوالب) مكتوبة مسبقًا تُستخدم لإنشاء كائنات في البرنامج. علاوة على ذلك، دائمًا ما يكون للفصل نوع محدد، والذي، بأسلوب برمجة جيد، له اسم يوحي بالغرض منه. علاوة على ذلك، يمكن ملاحظة أنه بما أن Java مكتوبة بقوة، فيجب أن يحدد كود البرنامج دائمًا نوع الكائن عند الإعلان عن المتغيرات. أضف إلى ذلك حقيقة أن الكتابة الصارمة تعمل على تحسين أمان الكود وموثوقيته، وتجعل من الممكن، حتى أثناء التجميع، منع الأخطاء الناجمة عن أنواع عدم التوافق (على سبيل المثال، محاولة تقسيم سلسلة على رقم). بطبيعة الحال، يجب على المترجم "معرفة" النوع المعلن - يمكن أن يكون فئة من JDK أو فئة أنشأناها بأنفسنا. وضح للقائم بالمقابلة أن الكود الخاص بنا لا يمكنه استخدام الكائنات من النوع المشار إليه في الإعلان فحسب، بل أيضًا العناصر التابعة له. هذه نقطة مهمة: يمكننا العمل مع العديد من الأنواع المختلفة كنوع واحد (بشرط أن تكون هذه الأنواع مشتقة من نوع أساسي). هذا يعني أيضًا أنه إذا أعلنا عن متغير يكون نوعه فئة فائقة، فيمكننا تعيين مثيل لأحد أحفاده لهذا المتغير. سوف يعجب القائم بالمقابلة إذا أعطيت مثالاً. حدد بعض الفئات التي يمكن مشاركتها بواسطة (فئة أساسية) لعدة فئات واجعل اثنين منهم يرثونها. الطبقة الأساسية:public class Dancer {
private String name;
private int age;
public Dancer(String name, int age) {
this.name = name;
this.age = age;
}
public void dance() {
System.out.println(toString() + " I dance like everyone else.");
}
@Override
public String toString() {
Return "I'm " + name + ". I'm " + age + " years old.";
}
}
في الفئات الفرعية، تجاوز طريقة الفئة الأساسية:
public class ElectricBoogieDancer extends Dancer {
public ElectricBoogieDancer(String name, int age) {
super(name, age);
}
// Override the method of the base class
@Override
public void dance() {
System.out.println(toString () + " I dance the electric boogie!");
}
}
public class Breakdancer extends Dancer {
public Breakdancer(String name, int age) {
super(name, age);
}
// Override the method of the base class
@Override
public void dance() {
System.out.println(toString() + " I breakdance!");
}
}
مثال على تعدد الأشكال وكيفية استخدام هذه الكائنات في البرنامج:
public class Main {
public static void main(String[] args) {
Dancer dancer = new Dancer("Fred", 18);
Dancer breakdancer = new Breakdancer("Jay", 19); // Widening conversion to the base type
Dancer electricBoogieDancer = new ElectricBoogieDancer("Marcia", 20); // Widening conversion to the base type
List<dancer> disco = Arrays.asList(dancer, breakdancer, electricBoogieDancer);
for (Dancer d : disco) {
d.dance(); // Call the polymorphic method
}
}
}
في الطريقة الرئيسية ، تبين أن الخطوط
Dancer breakdancer = new Breakdancer("Jay", 19);
Dancer electricBoogieDancer = new ElectricBoogieDancer("Marcia", 20);
أعلن عن متغير من فئة فائقة وقم بتعيين كائن يمثل مثيلًا لأحد أحفاده. من المرجح أن يتم سؤالك عن سبب عدم انقلاب المترجم بسبب عدم تناسق الأنواع المعلنة على الجانبين الأيسر والأيمن لمشغل المهمة - بعد كل شيء، تتم كتابة Java بقوة. اشرح أن تحويل نوع التوسيع يعمل هنا - يتم التعامل مع الإشارة إلى كائن كمرجع إلى فئته الأساسية. والأكثر من ذلك، بعد أن واجه مثل هذا البناء في الكود، يقوم المترجم بإجراء التحويل تلقائيًا وضمنيًا. يوضح نموذج التعليمات البرمجية أن النوع المعلن على الجانب الأيسر من عامل التعيين ( Dancer ) له أشكال (أنواع) متعددة، والتي تم الإعلان عنها على الجانب الأيمن ( Breakdancer ، ElectricBoogieDancer ). يمكن أن يكون لكل شكل سلوكه الفريد فيما يتعلق بالوظيفة العامة المحددة في الطبقة الفائقة ( طريقة الرقص ). وهذا يعني أن الطريقة المعلنة في فئة فائقة قد يتم تنفيذها بشكل مختلف في أحفادها. في هذه الحالة، نحن نتعامل مع تجاوز الطريقة، وهو بالضبط ما يخلق أشكالًا (سلوكيات) متعددة. يمكن ملاحظة ذلك عن طريق تشغيل الكود بالطريقة الرئيسية: إخراج البرنامج: أنا فريد. عمري 18 سنة. أنا أرقص مثل أي شخص آخر. أنا جاي. انا عمري 19 سنة. أنا أرقص! أنا مارسيا. عمري 20 سنة. أنا أرقص الرقصة الكهربائية! إذا لم نتجاوز الطريقة الموجودة في الفئات الفرعية، فلن نحصل على سلوك مختلف. على سبيل المثال، إذا قمنا بالتعليق على طريقة الرقص في فصول Breakdancer و ElectricBoogieDancer ، فإن نتيجة البرنامج ستكون كما يلي: أنا فريد. عمري 18 سنة. أنا أرقص مثل أي شخص آخر. أنا جاي. انا عمري 19 سنة. أنا أرقص مثل أي شخص آخر. أنا مارسيا. عمري 20 سنة. أنا أرقص مثل أي شخص آخر. وهذا يعني أنه ببساطة ليس من المنطقي إنشاء فئتي Breakdancer و ElectricBoogieDancer . أين يتجلى مبدأ تعدد الأشكال على وجه التحديد؟ أين يتم استخدام الكائن في البرنامج دون معرفة نوعه المحدد؟ في مثالنا، يحدث ذلك عندما يتم استدعاء الأسلوب Dance() على الكائن Dancer d . في Java، يعني تعدد الأشكال أن البرنامج لا يحتاج إلى معرفة ما إذا كان الكائن هو Breakdancer أو ElectricBoogieDancer . المهم أنه من سلالة الراقصين . وإذا ذكرت أحفادًا، فيجب أن تلاحظ أن الوراثة في Java لا تقتصر على الامتداد فحسب ، بل تنفذ أيضًا. الآن هو الوقت المناسب للإشارة إلى أن Java لا تدعم الوراثة المتعددة - يمكن أن يكون لكل نوع أصل واحد (فئة فائقة) وعدد غير محدود من المتحدرين (فئات فرعية). وفقًا لذلك، يتم استخدام الواجهات لإضافة مجموعات متعددة من الوظائف إلى الفئات. بالمقارنة مع الفئات الفرعية (الميراث)، تكون الواجهات أقل اقترانًا بالفئة الأصلية. يتم استخدامها على نطاق واسع جدا. في Java، تعد الواجهة نوعًا مرجعيًا، لذلك يمكن للبرنامج الإعلان عن متغير من نوع الواجهة. الآن حان الوقت لإعطاء مثال. إنشاء واجهة:
public interface CanSwim {
void swim();
}
من أجل الوضوح، سنأخذ العديد من الفئات غير ذات الصلة ونجعلها تنفذ الواجهة:
public class Human implements CanSwim {
private String name;
private int age;
public Human(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void swim() {
System.out.println(toString()+" I swim with an inflated tube.");
}
@Override
public String toString() {
return "I'm " + name + ". I'm " + age + " years old.";
}
}
public class Fish implements CanSwim {
private String name;
public Fish(String name) {
this.name = name;
}
@Override
public void swim() {
System.out.println("I'm a fish. My name is " + name + ". I swim by moving my fins.");
}
public class UBoat implements CanSwim {
private int speed;
public UBoat(int speed) {
this.speed = speed;
}
@Override
public void swim() {
System.out.println("I'm a submarine that swims through the water by rotating screw propellers. My speed is " + speed + " knots.");
}
}
الطريقة الرئيسية :
public class Main {
public static void main(String[] args) {
CanSwim human = new Human("John", 6);
CanSwim fish = new Fish("Whale");
CanSwim boat = new UBoat(25);
List<swim> swimmers = Arrays.asList(human, fish, boat);
for (Swim s : swimmers) {
s.swim();
}
}
}
توضح لنا نتائج استدعاء الطريقة متعددة الأشكال المحددة في الواجهة الاختلافات في سلوك الأنواع التي تنفذ هذه الواجهة. في حالتنا، هذه هي السلاسل المختلفة التي تعرضها طريقة السباحة . بعد دراسة مثالنا، قد يتساءل القائم بالمقابلة عن سبب تشغيل هذا الرمز في الطريقة الرئيسية
for (Swim s : swimmers) {
s.swim();
}
هل يؤدي إلى استدعاء الأساليب المهيمنة المحددة في فئاتنا الفرعية؟ كيف يتم اختيار التنفيذ المطلوب للطريقة أثناء تشغيل البرنامج؟ للإجابة على هذه الأسئلة، تحتاج إلى شرح الربط المتأخر (الديناميكي). الربط يعني إنشاء تعيين بين استدعاء الأسلوب وتنفيذ الفئة المحددة الخاصة به. في جوهر الأمر، يحدد الكود أي من الطرق الثلاث المحددة في الفئات سيتم تنفيذها. تستخدم Java الربط المتأخر بشكل افتراضي، أي أن الربط يحدث في وقت التشغيل وليس في وقت الترجمة كما هو الحال مع الربط المبكر. هذا يعني أنه عندما يقوم المترجم بترجمة هذا الرمز
for (Swim s : swimmers) {
s.swim();
}
لا يعرف أي فئة ( Human أو Fish أو Uboat ) لديها الكود الذي سيتم تنفيذه عند استدعاء أسلوب السباحة . يتم تحديد ذلك فقط عند تنفيذ البرنامج، وذلك بفضل آلية الربط الديناميكي (التحقق من نوع الكائن في وقت التشغيل واختيار التنفيذ الصحيح لهذا النوع). إذا تم سؤالك عن كيفية تنفيذ ذلك، فيمكنك الإجابة على أنه عند تحميل الكائنات وتهيئتها، يقوم JVM ببناء الجداول في الذاكرة وربط المتغيرات بقيمها والكائنات بطرقها. عند القيام بذلك، إذا تم توريث فئة ما أو تطبيق واجهة، فإن أول أمر في العمل هو التحقق من وجود الأساليب التي تم تجاوزها. إذا كان هناك أي منها، فهي ملزمة بهذا النوع. إذا لم يكن الأمر كذلك، فإن البحث عن طريقة المطابقة ينتقل إلى الفئة التي تكون أعلى بخطوة واحدة (الأصل) وهكذا حتى الجذر في تسلسل هرمي متعدد المستويات. عندما يتعلق الأمر بتعدد الأشكال في OOP وتنفيذه في التعليمات البرمجية، نلاحظ أنه من الممارسات الجيدة استخدام الفئات والواجهات المجردة لتوفير تعريفات مجردة للفئات الأساسية. تتبع هذه الممارسة مبدأ التجريد - تحديد السلوك والخصائص الشائعة ووضعها في فئة مجردة، أو تحديد السلوك الشائع فقط ووضعه في واجهة. يلزم تصميم وإنشاء تسلسل هرمي للكائنات استنادًا إلى الواجهات ووراثة الفئة لتنفيذ تعدد الأشكال. فيما يتعلق بتعدد الأشكال والابتكارات في Java، نلاحظ أنه بدءًا من Java 8، عند إنشاء فئات وواجهات مجردة، من الممكن استخدام الكلمة الأساسية الافتراضية لكتابة تطبيق افتراضي للطرق المجردة في الفئات الأساسية. على سبيل المثال:
public interface CanSwim {
default void swim() {
System.out.println("I just swim");
}
}
في بعض الأحيان يسأل القائمون على المقابلات عن كيفية الإعلان عن الأساليب في الفئات الأساسية حتى لا يتم انتهاك مبدأ تعدد الأشكال. الجواب بسيط: يجب ألا تكون هذه الأساليب ثابتة أو خاصة أو نهائية . الخاص يجعل الطريقة متاحة فقط داخل الفصل الدراسي، لذلك لن تتمكن من تجاوزها في فئة فرعية. تقوم خاصية Static بربط الطريقة بالفئة بدلاً من أي كائن، لذلك سيتم دائمًا استدعاء طريقة الفئة الفائقة. والنهائي يجعل الطريقة غير قابلة للتغيير ومخفية عن الفئات الفرعية.
ماذا يعطينا تعدد الأشكال؟
من المرجح أيضًا أن يتم سؤالك عن مدى فائدة تعدد الأشكال لنا. يمكنك الإجابة على هذا السؤال بإيجاز دون التورط في التفاصيل المعقدة:- يجعل من الممكن استبدال تطبيقات الفصل. الاختبار مبني عليه.
- إنه يسهل التوسعة، مما يجعل من الأسهل بكثير إنشاء أساس يمكن البناء عليه في المستقبل. تعد إضافة أنواع جديدة بناءً على الأنواع الموجودة هي الطريقة الأكثر شيوعًا لتوسيع وظائف برامج OOP.
- فهو يتيح لك دمج الكائنات التي تشترك في نوع أو سلوك مشترك في مجموعة أو مصفوفة واحدة والتعامل معها بشكل موحد (كما في الأمثلة لدينا، حيث أجبرنا الجميع على الرقص () أو السباحة () :)
- المرونة في إنشاء أنواع جديدة: يمكنك اختيار تنفيذ الوالدين لطريقة ما أو تجاوزها في فئة فرعية.
بعض كلمات الفراق
تعدد الأشكال هو موضوع مهم جدا وواسع النطاق. إنه موضوع ما يقرب من نصف هذه المقالة حول OOP في Java ويشكل جزءًا كبيرًا من أساس اللغة. لن تتمكن من تجنب تحديد هذا المبدأ في المقابلة. إذا كنت لا تعرف ذلك أو لا تفهمه، فمن المحتمل أن تنتهي المقابلة. لذا لا تكن متكاسلاً – قم بتقييم معرفتك قبل المقابلة وقم بتحديثها إذا لزم الأمر.
المزيد من القراءة: |
---|
GO TO FULL VERSION