تخطَّ إلى المحتوى

الوراثة والواجهات في Java: abstract و interface

الدرس 9 من 11 · Java

في درس الكلاسات رأيت كيف ترث القطة من الحيوان بكلمة extends. لكن الوراثة في المشاريع الحقيقية أعمق من ذلك: أحياناً نريد كلاساً أباً لا يُنشأ منه كائن أصلاً، وأحياناً نريد أن تتفق كلاسات مختلفة تماماً على قدرة مشتركة. في هذا الدرس ستتعلم الكلاسات المجردة والواجهات، وهما أساس تصميم أي برنامج Java كبير.

class Employee {
protected String name;
protected double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public double bonus() {
return salary * 0.10;
}
public String describe() {
return name + " - المكافأة: " + bonus();
}
}
class Manager extends Employee {
public Manager(String name, double salary) {
super(name, salary);
}
@Override
public double bonus() {
return super.bonus() + 500;
}
}
Employee e = new Employee("هدى", 4000);
Employee m = new Manager("كريم", 6000);
System.out.println(e.describe());
System.out.println(m.describe());
الناتج
هدى - المكافأة: 400.0
كريم - المكافأة: 1100.0

الجديد هنا هو super.bonus(): استدعاء نسخة الأب من الدالة ثم البناء عليها، بدل إعادة كتابة الحساب كله. ولاحظ أن describe مكتوبة مرة واحدة في الأب، ومع ذلك استخدمت مكافأة المدير الصحيحة. هذا هو تعدد الأشكال (Polymorphism): الكائن نفسه يحدد أي نسخة من الدالة تعمل.

ما مساحة «شكل»؟ السؤال بلا معنى حتى نعرف هل هو دائرة أم مستطيل. الكلاس Shape فكرة عامة فقط، ولا يصح إنشاء كائن منه. نعبّر عن ذلك بكلمة abstract:

abstract class Shape {
protected String name;
public Shape(String name) {
this.name = name;
}
public abstract double area();
public void print() {
System.out.println(name + ": " + area());
}
}
class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
super("مستطيل");
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
class Square extends Rectangle {
public Square(double side) {
super(side, side);
this.name = "مربع";
}
}
Shape[] shapes = { new Rectangle(4, 5), new Square(3) };
for (Shape s : shapes) {
s.print();
}
الناتج
مستطيل: 20.0
مربع: 9.0
  • abstract class: كلاس لا يمكن إنشاء كائن منه. السطر new Shape("شكل") يرفضه المترجم.
  • abstract double area();: دالة بلا جسم. كل كلاس وارث ملزم بكتابتها، وإلا لن يُترجم.
  • الكلاس المجرد يستطيع مع ذلك أن يحمل حقولاً ودوالاً عادية مثل print.

الكلاس المجرد مثل نموذج طلب فيه خانات فارغة: الهيكل جاهز، لكن لا يُقبل النموذج حتى يملأ صاحبه الخانات الإلزامية.

الطائر يطير، والطائرة تطير، لكنهما لا ينتميان لعائلة واحدة. ما يجمعهما قدرة وليست قرابة. الواجهة (Interface) تصف قدرة: قائمة دوال يتعهد الكلاس بتنفيذها:

interface Printable {
void print();
}
interface Payable {
double TAX = 0.14;
double total();
default String receipt() {
return "الإجمالي: " + total();
}
}
class Invoice implements Printable, Payable {
private double amount;
public Invoice(double amount) {
this.amount = amount;
}
@Override
public void print() {
System.out.println("فاتورة بمبلغ " + amount);
}
@Override
public double total() {
return amount + amount * TAX;
}
}
Invoice inv = new Invoice(200);
inv.print();
System.out.println(inv.receipt());
Payable p = inv;
System.out.println(p.total());
الناتج
فاتورة بمبلغ 200.0
الإجمالي: 228.0
228.0
  • implements: الكلاس يتعهد بتنفيذ كل دوال الواجهة.
  • الكلاس يرث من أب واحد فقط بـ extends، لكنه ينفّذ أي عدد من الواجهات.
  • دوال الواجهة public و abstract تلقائياً، وأي متغير فيها ثابت (public static final).
  • default: دالة لها جسم جاهز داخل الواجهة، يرثها الكلاس كما هي أو يعيد كتابتها.

الواجهة مثل رخصة القيادة: لا يهم من أنت ولا من أي عائلة، المهم أنك أثبتّ قدرتك على القيادة.

instanceof: ما النوع الحقيقي؟

Section titled “instanceof: ما النوع الحقيقي؟”

متغير من نوع الأب قد يحمل كائناً من أي ابن. للتحقق من النوع الحقيقي والوصول لدواله الخاصة نستخدم instanceof:

Object[] items = { "نص", 42, 3.5 };
for (Object item : items) {
if (item instanceof String s) {
System.out.println("نص طوله " + s.length());
} else if (item instanceof Integer n) {
System.out.println("عدد صحيح ضعفه " + (n * 2));
} else {
System.out.println("نوع آخر: " + item);
}
}
الناتج
نص طوله 2
عدد صحيح ضعفه 84
نوع آخر: 3.5

الصيغة item instanceof String s تفحص النوع وتعرّف المتغير s جاهزاً في خطوة واحدة. ولاحظ أن كل كلاس في Java يرث تلقائياً من الكلاس Object، ولذلك تقبل المصفوفة أي شيء.

الكلاس المجرد الواجهة
العلاقة «هو نوع من» «يستطيع أن»
العدد المسموح أب واحد أي عدد
الحقول حقول عادية ومُنشئ ثوابت فقط
مثال Shape و Rectangle Comparable و Runnable

القاعدة العملية: ابدأ بالواجهة. واختر الكلاس المجرد فقط إذا كانت الكلاسات الوارثة تشترك في حقول وكود فعلي.

كلمة final أمام كلاس تمنع الوراثة منه، وأمام دالة تمنع إعادة كتابتها:

final class Token {
public final String value() {
return "abc123";
}
}

الكلاس String نفسه final، ولهذا لا يستطيع أحد أن يرث منه ويغيّر سلوكه.

  1. أنشئ abstract class Vehicle فيه حقل name ودالة مجردة wheels() ودالة عادية info() تطبع الاسم وعدد العجلات.
  2. أنشئ Car و Bike يرثان منه.
  3. أنشئ interface Electric فيها الدالة charge()، واجعل كلاساً جديداً ElectricCar يرث من Car وينفّذ Electric.
  4. ضع المركبات في مصفوفة واطبع معلوماتها، واشحن فقط ما ينفّذ Electric باستخدام instanceof.
  • إنشاء كائن من كلاس مجرد: new Shape(...) يعطي الخطأ Shape is abstract; cannot be instantiated.
  • نسيان تنفيذ دالة: إذا لم ينفّذ الكلاس كل الدوال المجردة ستقرأ: is not abstract and does not override abstract method.
  • إضعاف الوصول: دوال الواجهة public، فتنفيذها يجب أن يُكتب public أيضاً. نسيان الكلمة خطأ ترجمة.
  • نسيان @Override: بدونها قد تخطئ في اسم الدالة فتنشئ دالة جديدة دون أن تدري.

سؤال

كم واجهة يستطيع الكلاس الواحد أن ينفّذ بكلمة implements؟

سؤال

ماذا يحدث إذا ورث كلاس عادي من كلاس مجرد ولم يكتب دالته المجردة؟

  • super.method() تستدعي نسخة الأب، وتعدد الأشكال يختار نسخة الكائن الحقيقي.
  • abstract class: كلاس لا يُنشأ منه كائن، وقد يحوي دوالاً مجردة يلتزم بها الأبناء.
  • interface تصف قدرة، والكلاس ينفّذها بـ implements وبأي عدد.
  • instanceof تفحص النوع الحقيقي للكائن وتعرّف متغيراً منه.
  • final تمنع الوراثة من الكلاس أو إعادة كتابة الدالة.