اسم واحد، عدة نُسخ
في الصفحة السابقة رأيت كيف تحدد معاملات الدالة ما الذي تقبله. والتحميل الزائد للدوال يمضي بهذا أبعد: يمكنك أن تمنح عدة دوال الاسم نفسه ما دامت قوائم معاملاتها مختلفة. يتعامل المُصرِّف معها كدوال منفصلة ويختار الصحيحة بناءً على الوسائط التي تمررها.
ولهذا تطبع System.out.println بلا أي عناء قيمة من نوع int أو String أو boolean أو double — فليست هناك دالة println واحدة، بل نسخ كثيرة تتشارك الاسم. تكتب الاستدعاء الذي تقصده ويربطه المُصرِّف بالنسخة المناسبة.
كلتا الدالتين تحملان الاسم square، لكن إحداهما تأخذ int والأخرى double. القيمة الحرفية 5 من نوع int، لذا تُنفَّذ النسخة الأولى؛ و2.5 من نوع double، لذا تُنفَّذ الثانية.
ما الذي يُعدّ نسخة مختلفة
يجب أن تختلف النسخ في قائمة المعاملات — أي في واحد على الأقل مما يأتي:
- عدد مختلف من المعاملات،
- أنواع معاملات مختلفة، أو
- ترتيب مختلف للأنواع.
كل استدعاء له قائمة معاملات تطابق تمامًا واحدة من دوال join الثلاث، فلا يقع أي لبس.
نوع القيمة المُعادة لا يُحسب
مزلق شائع للمبتدئين: محاولة التحميل الزائد بناءً على نوع القيمة المُعادة وحده. نوع القيمة المُعادة ليس جزءًا من التوقيع الذي يستخدمه المُصرِّف، لذا فإن هذا لا يُصرَّف:
// لا يُصرَّف - الاسم نفسه والمعاملات نفسها، ويختلف نوع القيمة المُعادة فقط
static int value() { return 1; }
static double value() { return 1.0; } // خطأ: value() معرّفة بالفعل
لا يستطيع المُصرِّف التمييز بينهما، لأنه عند موضع استدعاء مثل value() لا يوجد في الوسائط ما يلمّح إلى أيهما تريد. يمكنك أن تمنح النسخ أنواع قيم مُعادة مختلفة، لكن فقط حين تكون قوائم معاملاتها مختلفة أصلًا.
كيف تختار Java النسخة المناسبة
عندما تستطيع أكثر من نسخة قبول وسائطك، تختار Java النسخة الأكثر تحديدًا، وتفضّل المطابقة التامة للنوع على التحويل التوسيعي. لاحظ ما يحدث مع وسيط من نوع int:
يطابق show(7) نوع int تمامًا، رغم أن long وdouble بإمكانهما أيضًا استيعاب 7 بعد توسيع. ولن يوسّع المُصرِّف int إلى long ثم إلى double إلا إذا أُزيلت النسخة المطابقة تمامًا. يُحسم هذا الاختيار بالكامل في وقت التصريف، بناءً على الأنواع المُصرَّح بها لوسائطك.
احذر الاستدعاءات الملتبسة
إن لم تكن أي نسخة هي الأفضل مطابقةً بوضوح، يرفض المُصرِّف التخمين ويُبلّغ عن خطأ. ويحدث هذا غالبًا مع null الذي يلائم أي نوع مرجعي:
static void handle(String s) { }
static void handle(StringBuilder b) { }
handle(null); // خطأ: الإشارة إلى handle ملتبسة
كلتا النسختين تقبلان null، وليست إحداهما أكثر تحديدًا، لذا لن يُصرَّف الاستدعاء. أصلح ذلك بجعل النوع صريحًا عبر تحويل (cast) - handle((String) null) - أو بإعادة التصميم بحيث لا تتصادم النسخ. وينطبق الحذر نفسه عند خلط التغليف التلقائي (autoboxing) مع التوسيع؛ فاحرص على إبقاء مجموعات النسخ بسيطة بما يكفي ليكون لكل استدعاء فائز واحد واضح.
التحميل الزائد للبواني
لا يقتصر التحميل الزائد على الدوال العادية — فالبواني تستخدمه باستمرار لتقديم عدة طرق لبناء كائن. يمكن لبانٍ بلا وسائط أن يفوّض إلى بانٍ أكمل عبر this(...):
يتشارك البانيان الاسم Point لكنهما يختلفان في عدد المعاملات، تمامًا كالدوال المُحمَّلة زائدًا. والتفويض عبر this(...) يُبقي منطق التهيئة في مكان واحد.
التحميل الزائد مقابل إعادة التعريف
يتشابه المصطلحان في الوقع لكن لا علاقة بينهما:
- التحميل الزائد - الاسم نفسه، قوائم معاملات مختلفة، في الصنف نفسه. يختار المُصرِّف النسخة في وقت التصريف. الغرض منه تقديم نُسخ متنوعة لعملية ما.
- إعادة التعريف - يعيد صنف فرعي تعريف دالة موروثة بالاسم نفسه والمعاملات نفسها. تختار Java النسخة في وقت التشغيل بناءً على النوع الحقيقي للكائن. والغرض منه استبدال السلوك (ستلتقي به ضمن الوراثة وتعدد الأشكال).
إذا كانت قوائم المعاملات متطابقة فأنت تُعيد التعريف (أو تُحدث خطأ تكرار دالة في الصنف نفسه)؛ وإذا اختلفت فأنت تُحمِّل زائدًا.
التالي: Varargs
يتيح لك التحميل الزائد كتابة join(a, b) وjoin(a, b, c) كدالتين منفصلتين — لكن ماذا لو أردت قبول أي عدد من الوسائط دون التصريح بنسخة لكل عدد؟ صيغة varargs في Java تتيح لدالة واحدة أن تأخذ قائمة وسائط متغيّرة الطول، وهذا موضوع الصفحة التالية.
الأسئلة الشائعة
ما هو التحميل الزائد للدوال في Java؟
التحميل الزائد للدوال يعني تعريف عدة دوال بالاسم نفسه داخل الصنف نفسه، بحيث يكون لكل منها قائمة معاملات مختلفة (عدد مختلف من المعاملات، أو أنواع مختلفة، أو ترتيب مختلف للأنواع). يقرر المُصرِّف أي نسخة يستدعي عبر مطابقة الوسائط التي تمررها مع معاملات كل نسخة. وهذا قرار يُتخذ في وقت التصريف، لا في وقت التشغيل.
هل يمكن أن تختلف دالتان في Java بنوع القيمة المُعادة فقط؟
لا. نوع القيمة المُعادة ليس جزءًا من توقيع الدالة لأغراض التحميل الزائد، لذا فإن وجود int total() وdouble total() في الصنف نفسه يسبب خطأ تصريف. يجب أن تختلف النسخ في قائمة معاملاتها: في العدد أو الأنواع أو ترتيب المعاملات. يمكن أن يختلف نوع القيمة المُعادة، لكن فقط إضافةً إلى اختلاف في المعاملات، لا وحده.
ما الفرق بين التحميل الزائد (overloading) وإعادة التعريف (overriding) في Java؟
التحميل الزائد هو عدة دوال بالاسم نفسه لكن بمعاملات مختلفة داخل الصنف نفسه، يحلّها المُصرِّف في وقت التصريف. أما إعادة التعريف فهي أن يعيد صنف فرعي تعريف دالة موروثة بالاسم نفسه والمعاملات نفسها، ويُحلّ ذلك في وقت التشغيل بناءً على النوع الفعلي للكائن. التحميل الزائد يتعلق بتقديم نُسخ متنوعة، أما إعادة التعريف فتتعلق باستبدال السلوك.