الوراثة وتعدّد الأشكال

الكلمة المفتاحية final للأصناف والتوابع

15 دقيقة الدرس 7 من 14

الكلمة المفتاحية final للأصناف والتوابع

حتى الآن في هذه السلسلة تعلّمت كيف تُوسّع الأصناف وتُعيد تعريف التوابع لبناء تسلسلات هرمية مرنة. لكن أحيانًا تكون المرونة هي آخر ما تحتاجه. أحيانًا تريد أن تقول: "لا يجوز توسيع هذا الصنف أبدًا" أو "لا يجوز استبدال هذا التابع أبدًا." تمنحك Java كلمة مفتاحية واحدة لكلا الغرضين: final.

معنى final على التوابع

حين تُضيف final إلى تابع، لا يستطيع أي صنف فرعي إعادة تعريفه. التنفيذ الذي كتبته ثابت على طول سلسلة الوراثة بأكملها.

class BankAccount { private double balance; public BankAccount(double initialBalance) { this.balance = initialBalance; } // يجوز للأصناف الفرعية تغيير طريقة حساب الفائدة... public double calculateInterest() { return balance * 0.02; } // ...لكن لا يجوز أبدًا تغيير كيفية التحقق من عملية السحب. public final boolean withdraw(double amount) { if (amount <= 0 || amount > balance) { return false; } balance -= amount; return true; } } class SavingsAccount extends BankAccount { public SavingsAccount(double initialBalance) { super(initialBalance); } @Override public double calculateInterest() { // مسموح — ليس final return 0.05; } // @Override // public boolean withdraw(double amount) { ... } // خطأ تصريفي: لا يمكن إعادة تعريف التابع النهائي withdraw() }

منطق السحب يُطبّق قاعدة عمل: لا يمكن سحب مبلغ سالب أو أكثر من الرصيد. جعله final يضمن ألا يتحايل أي صنف فرعي على تلك القاعدة.

الفكرة الجوهرية: استخدم final على تابع حين يعتمد السلوك الصحيح على تنفيذه تحديدًا — وأي إعادة تعريف من صنف فرعي ستُدخل أخطاءً أو ثغرات أمنية.

معنى final على الأصناف

حين تُضيف final إلى صنف بأكمله، لا يمكن توسيعه على الإطلاق. لا أحد يستطيع كتابة class Foo extends YourFinalClass.

public final class ImmutablePoint { private final double x; private final double y; public ImmutablePoint(double x, double y) { this.x = x; this.y = y; } public double getX() { return x; } public double getY() { return y; } public ImmutablePoint translate(double dx, double dy) { return new ImmutablePoint(x + dx, y + dy); // يُعيد نقطة جديدة } } // class MutablePoint extends ImmutablePoint { ... } // خطأ تصريفي: لا يمكن الوراثة من الصنف النهائي ImmutablePoint

لأن الصنف final وجميع حقوله private final، يُضمن أن كائن ImmutablePoint ثابت تمامًا — لا يمكن تغيير إحداثياته بعد الإنشاء، ولا يمكن لأي صنف فرعي إضافة حالة قابلة للتغيير.

أصناف final في Java — String

String هو المثال الأشهر على صنف final في مكتبة Java القياسية. لا يمكن توسيعه:

// class MyString extends String { ... } // خطأ تصريفي: لا يمكن الوراثة من الصنف النهائي String

لماذا جعل مصمّمو Java صنف String نهائيًا؟ تتضافر عدة أسباب:

  • الأمان. تتلقّى أجزاء كثيرة من JVM والمكتبة القياسية قيمة String وتثق بها (مسارات الملفات، أسماء الأصناف، كلمات المرور). لو كان String قابلًا للتوسيع، يمكن لصنف فرعي خبيث إعادة تعريف equals أو toString للكذب بشأن قيمته بعد فحص أمني.
  • الثبات (Immutability). كائنات String ثابتة — لا يتغيّر مصفوفة البايتات الداخلية أبدًا. هذا الثبات مضمون فقط إذا تعذّر على الأصناف الفرعية إضافة حقول قابلة للتغيير.
  • String interning وتجمّع الثوابت. يُخزّن JVM حرفيات String مؤقتًا. هذا التحسين آمن فقط لأن جميع السلاسل تتصرّف بالطريقة ذاتها؛ الأصناف الفرعية ستُفسد هذا الضمان.
أصناف final أخرى بارزة في JDK: Integer وLong وDouble وسائر أصناف التغليف البدائية final لأسباب الثبات والصحة ذاتها. وكذلك صنف Math.

الحقول final — تذكير سريع

رأيت مسبقًا final على الحقول (متغيّر لا يُسنَد إليه إلا مرة واحدة). هذا استخدام منفصل — لكنه مرتبط — للكلمة المفتاحية ذاتها. الاستخدامات الثلاثة تشترك في فلسفة واحدة: التزم بقيمة أو سلوك وامنع تغييره.

class Circle { private final double radius; // حقل: يُسنَد مرة واحدة في المُنشئ، لا يتغيّر أبدًا public Circle(double radius) { this.radius = radius; } public final double area() { // تابع: لا يجوز لأي صنف فرعي إعادة تعريف هذه الصيغة return Math.PI * radius * radius; } }

متى تستخدم final؟

قائمة تحقق ذهنية مفيدة:

  • ضع final على التابع حين يُطبّق قاعدة أمنية، أو خطوة بروتوكول، أو خوارزمية تعتمد عليها الأصناف الفرعية لكنها لا يجب أن تُعدّلها.
  • ضع final على الصنف حين يُمثّل نوع قيمة يجب أن يبقى ثابتًا (مثل String أو Integer)، أو حين يكون صنف أدوات مساعدة (مثل Math) لا يجب تهيئته عبر صنف فرعي.
  • لا تُفرط في استخدام final. جعل كل شيء final يجعل الكود أصعب في الاختبار (لا يمكن إنشاء بدائل للاختبار) وأصعب في التوسيع حين تتغيّر المتطلبات.
خطأ شائع: يضع بعض المطوّرين final على صنف لمنع الإساءة في استخدامه، ثم يكتشفون لاحقًا أن حالة استخدام مشروعة تحتاج إلى التوسيع. افضّل التصميم الجيد بمُحدّدات الوصول والتغليف أولًا. الجأ إلى final حين يكون لديك سبب وجيه — لا مجرد عادة.

التحقق وقت التصريف

final يُفحص بالكامل وقت التصريف — لا يوجد أي عبء وقت تشغيل. إذا حاولت توسيع صنف final أو إعادة تعريف تابع final، يرفض المُصرِّف الكود فورًا برسالة خطأ واضحة. هذا يجعله شبكة أمان بدون أي تكلفة.

الخلاصة

الكلمة المفتاحية final على تابع تمنع أي صنف فرعي من إعادة تعريف ذلك التابع، وتُثبّت سلوكًا بعينه. وعلى صنف تمنع توسيعه كليًا، وهكذا تضمن Java أن String وInteger وأنواع مشابهة تبقى ثابتة وجديرة بالثقة. استخدم final بوعي: لإنفاذ عقد، أو للحفاظ على الثبات، أو لحماية تنفيذ حساس أمنيًا.