أفضل الممارسات من «Effective Java (3rd Edition)»
مقتطف من «Effective Java (3rd Edition) » لمؤلفه Joshua Bloch
1. أعِد مجموعات أو مصفوفات فارغة، لا null
إذا كانت الدالة تُعيد Set، فلا تكتب return null; عند النتيجة الفارغة – بل return Set.of();.
2. فكّر في دوال المصنع الساكنة بدلًا من المُنشئات
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
ليست أفضل دائمًا، لكنها في بعض حالات الاستخدام تقدّم مزايا عديدة:
- من مزايا دوال المصنع الساكنة أن لها أسماء، على عكس المُنشئات.
- ليست مضطرة إلى إنشاء كائن جديد في كل مرة تُستدعى فيها.
- يمكنها أن تُعيد كائنًا من أي نوع فرعي لنوع القيمة المُعادة.
- يمكن أن يختلف صنف الكائن المُعاد من استدعاء إلى آخر تبعًا لمعاملات الإدخال.
- لا يلزم أن يكون صنف الكائن المُعاد موجودًا حين يُكتب الصنف الذي يحتوي على الدالة.
العيوب:
- القيد الأساسي لتوفير دوال مصنع ساكنة فقط هو أن الأصناف التي لا تملك مُنشئات public أو protected لا يمكن اشتقاق أصناف فرعية منها.
- والعيب الثاني لدوال المصنع الساكنة أن المبرمجين يجدون صعوبة في العثور عليها.
3. فكّر في نمط الباني (Builder) حين تواجه معاملات مُنشئ كثيرة
تشترك المصانع الساكنة والمُنشئات في قيد واحد: إنها لا تتكيّف جيدًا مع عدد كبير من المعاملات الاختيارية. وقد عانت من ذلك بشدة بعض المشاريع التي عملت عليها.
4. افرض خاصية المفرد (Singleton) بمُنشئ خاص أو بنوع enum
كل ما لا يحتاج إلى نسخ متعددة ينبغي أن يُنشأ مرة واحدة فقط – سواء عبر @ApplicationScoped أو عبر نمط Singleton آخر.
5. امنع إنشاء النسخ بمُنشئ خاص
ستحتاج أحيانًا إلى كتابة صنف ليس سوى تجميع لدوال ساكنة وحقول ساكنة. اكتسبت مثل هذه الأصناف سمعة سيئة لأن بعضهم يسيء استخدامها هربًا من التفكير بمنطق الكائنات، لكن لها استخدامات مشروعة.
private MyStaticClass() {
// private constructor to prevent instantiation
}
6. تجنّب إنشاء كائنات غير ضرورية
String s = new String("bikini"); // DON'T DO THIS!
أو:
// Hideously slow! Can you spot the object creation?
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
الحل: يعطي هذا البرنامج الإجابة الصحيحة، لكنه أبطأ بكثير مما ينبغي، بسبب خطأ مطبعي في حرف واحد. فالمتغير sum مُعرَّف على أنه Long بدلًا من long، مما يعني أن البرنامج يُنشئ نحو 2^31 نسخة غير ضرورية من Long (تقريبًا نسخة لكل مرة يُضاف فيها long i إلى Long sum).
7. تجنّب الـ Finalizers والـ Cleaners
الـ Finalizers لا يمكن التنبؤ بها، وكثيرًا ما تكون خطيرة، وهي غير ضرورية عمومًا. وقد يؤدي استخدامها إلى سلوك غير منتظم وأداء ضعيف ومشكلات في قابلية النقل.
8. فضّل try-with-resources على try-finally
// try-with-resources - the best way to close resources!
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
9. فكّر في تطبيق Comparable
بتطبيق Comparable تتيح لصنفك العمل مع كل الخوارزميات العامة الكثيرة وتطبيقات المجموعات التي تعتمد على هذه الواجهة.
10. قلّل إمكانية الوصول إلى الأصناف والأعضاء
العامل الأهم على الإطلاق الذي يميّز المكوّن جيد التصميم عن سيئ التصميم هو مدى إخفاء المكوّن لبياناته الداخلية وتفاصيل تنفيذه الأخرى عن المكونات الأخرى. فالمكوّن جيد التصميم يُخفي كل تفاصيل تنفيذه، ويفصل الـ API الخاص به عن تنفيذه فصلًا نظيفًا. وعندها تتواصل المكونات فيما بينها عبر واجهاتها البرمجية فقط، ولا تعلم شيئًا عن طريقة عمل بعضها الداخلية. هذا المفهوم، المعروف بإخفاء المعلومات أو التغليف، مبدأ أساسي في تصميم البرمجيات.
11. في الأصناف العامة استخدم دوال الوصول، لا الحقول العامة
قد تُغرى أحيانًا بكتابة أصناف منحطّة لا غرض لها سوى تجميع حقول النسخة:
// Degenerate classes like this should not be public!
class Point {
public double x;
public double y;
}
ولأن حقول البيانات في مثل هذه الأصناف يُوصَل إليها مباشرة، فإن هذه الأصناف لا تقدّم مزايا التغليف. فلا يمكنك تغيير التمثيل من دون تغيير الـ API، ولا يمكنك فرض الثوابت (invariants)، ولا يمكنك اتخاذ إجراء مساعد عند الوصول إلى حقل ما. ويرى المبرمجون المتشددون في البرمجة كائنية التوجه أن مثل هذه الأصناف مكروهة، وأنه يجب دائمًا استبدالها بأصناف ذات حقول خاصة ودوال وصول عامة (getters)، ودوال تعديل (setters) في حالة الأصناف القابلة للتغيير:
// Encapsulation of data by accessor methods and mutators
class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public void setX(double x) { this.x = x; }
public void setY(double y) { this.y = y; }
}
لا شك أن المتشددين محقّون فيما يخص الأصناف العامة: إذا كان الصنف قابلًا للوصول من خارج حزمته، فوفّر دوال وصول للحفاظ على المرونة في تغيير التمثيل الداخلي للصنف. فإذا كشف صنف عام حقول بياناته، ضاع كل أمل في تغيير تمثيله، لأن كود العملاء قد يكون منتشرًا في كل مكان.
أما إذا كان الصنف package-private أو صنفًا متداخلًا خاصًا، فلا عيب جوهريًا في كشف حقول بياناته – بشرط أن تؤدي دورها جيدًا في وصف التجريد الذي يقدّمه الصنف.
12. قلّل قابلية التغيير
الصنف غير القابل للتغيير (immutable) هو ببساطة صنف لا يمكن تعديل نسخه. فكل المعلومات التي تحتويها كل نسخة ثابتة طوال عمر الكائن، فلا يمكن ملاحظة أي تغيير أبدًا. تحتوي مكتبات منصة Java على كثير من الأصناف غير القابلة للتغيير، منها String، وأصناف الأنواع الأولية المغلّفة، وBigInteger وBigDecimal. ولذلك أسباب وجيهة كثيرة: فالأصناف غير القابلة للتغيير أسهل في التصميم والتنفيذ والاستخدام من الأصناف القابلة للتغيير. وهي أقل عرضة للأخطاء وأكثر أمانًا. ولجعل صنف ما غير قابل للتغيير، اتبع هذه القواعد الخمس:
- لا توفّر دوال تعدّل حالة الكائن (المعروفة بـدوال التعديل (mutators)).
-
تأكّد من أن الصنف لا يمكن توسيعه. هذا يمنع الأصناف الفرعية المهملة أو الخبيثة من الإخلال بسلوك الصنف غير القابل للتغيير، بأن تتصرف كما لو أن حالة الكائن
قد تغيّرت. ويتحقق منع الاشتقاق عادةً بجعل الصنف
finalمثلًا. - اجعل كل الحقول final. هذا يعبّر بوضوح عن نيتك بطريقة يفرضها النظام. كما أنه ضروري لضمان السلوك الصحيح إذا مُرِّر مرجع إلى نسخة أُنشئت حديثًا من خيط إلى آخر من دون مزامنة، كما هو موضّح في نموذج الذاكرة.
- اجعل كل الحقول private. هذا يمنع العملاء من الوصول إلى الكائنات القابلة للتغيير التي تشير إليها الحقول ومن تعديلها مباشرة. ومع أنه مسموح تقنيًا للأصناف غير القابلة للتغيير أن تملك حقولًا public final تحتوي على قيم أولية أو مراجع إلى كائنات غير قابلة للتغيير، فإن ذلك غير مستحسن لأنه يحول دون تغيير التمثيل الداخلي في إصدار لاحق.
-
اضمن الوصول الحصري إلى أي مكوّنات قابلة للتغيير. إذا كان في صنفك حقول تشير إلى كائنات قابلة للتغيير، فتأكد من أن عملاء الصنف لا يمكنهم الحصول على مراجع إلى
هذه الكائنات. لا تهيّئ مثل هذا الحقل أبدًا بمرجع كائن قدّمه العميل، ولا تُعِد الحقل من دالة وصول. أنشئ نسخًا دفاعية في المُنشئات ودوال الوصول ودوال
readObject.
13. فضّل التركيب على الوراثة
الوراثة وسيلة قوية لتحقيق إعادة استخدام الكود، لكنها ليست دائمًا الأداة الأنسب للمهمة. فإذا استُخدمت في غير موضعها، أدّت إلى برمجيات هشّة. من الآمن استخدام الوراثة داخل الحزمة الواحدة، حيث يكون تنفيذ الصنف الفرعي والصنف الأب تحت سيطرة المبرمجين أنفسهم. ومن الآمن أيضًا استخدام الوراثة عند توسيع أصناف صُمّمت ووُثّقت خصيصًا للتوسيع. أما الوراثة من أصناف ملموسة عادية عبر حدود الحزم فخطيرة.
على عكس استدعاء الدوال، تنتهك الوراثة مبدأ التغليف. قد تظن أن توسيع صنف آمن إذا اكتفيت بإضافة دوال جديدة وامتنعت عن إعادة تعريف الدوال الموجودة. ومع أن هذا النوع من التوسيع أكثر أمانًا بكثير، فإنه لا يخلو من المخاطر. فإذا اكتسب الصنف الأب دالة جديدة في إصدار لاحق، وكان من سوء حظك أنك أعطيت الصنف الفرعي دالة بالتوقيع نفسه ونوع إرجاع مختلف، فلن يُصرَّف صنفك الفرعي بعد الآن. وإذا كنت قد أعطيت الصنف الفرعي دالة بالتوقيع ونوع الإرجاع نفسيهما للدالة الجديدة في الصنف الأب، فأنت الآن تعيد تعريفها، وبالتالي تتعرض للمشكلات الموصوفة سابقًا. وفوق ذلك، من المشكوك فيه أن تفي دالتك بعقد دالة الصنف الأب الجديدة، لأن ذلك العقد لم يكن قد كُتب بعد حين كتبت دالة الصنف الفرعي.
لحسن الحظ، توجد طريقة لتجنّب كل المشكلات الموصوفة أعلاه. فبدلًا من توسيع صنف موجود، امنح صنفك الجديد حقلًا خاصًا يشير إلى نسخة من الصنف الموجود. يُسمّى هذا التصميم التركيب (composition)، لأن الصنف الموجود يصبح مكوّنًا في الصنف الجديد. وكل دالة نسخة في الصنف الجديد تستدعي الدالة المقابلة على النسخة المحتواة من الصنف الموجود وتُعيد النتائج. يُعرف هذا بالتمرير (forwarding)، وتُعرف دوال الصنف الجديد بدوال التمرير. وسيكون الصنف الناتج متينًا كالصخر، بلا أي اعتماد على تفاصيل تنفيذ الصنف الموجود. بل إن إضافة دوال جديدة إلى الصنف الموجود لن يكون لها أي أثر على الصنف الجديد.
14. فضّل الواجهات على الأصناف المجردة
تملك Java آليتين لتعريف نوع يسمح بتطبيقات متعددة: الواجهات والأصناف المجردة. ومنذ إدخال الدوال الافتراضية (default methods) للواجهات في Java 8، تتيح الآليتان كلتاهما توفير تطبيقات لبعض دوال النسخة.
من الفروق الكبرى أنه لتطبيق النوع الذي يعرّفه صنف مجرد، يجب أن يكون الصنف صنفًا فرعيًا من ذلك الصنف المجرد. ولأن Java لا تسمح إلا بالوراثة المفردة، فإن هذا القيد على الأصناف المجردة يحدّ بشدة من استخدامها كتعريفات للأنواع. أما الواجهة فيُسمح لأي صنف بتطبيقها ما دام يعرّف كل الدوال المطلوبة ويلتزم بالعقد العام، بصرف النظر عن موقع الصنف في التسلسل الهرمي للأصناف. ويمكن بسهولة تعديل الأصناف الموجودة لتطبيق واجهة جديدة.
- الواجهات مثالية لتعريف الـ Mixins.
- الواجهات تتيح بناء أُطر أنواع غير هرمية.
- الواجهات تتيح تحسينات وظيفية آمنة وقوية عبر أسلوب صنف التغليف (wrapper class). أما إذا استخدمت الأصناف المجردة لتعريف الأنواع، فلن تترك للمبرمج الذي يريد إضافة وظائف أي بديل سوى الوراثة.
15. Generics: لا تستخدم الأنواع الخام (raw types)
لن تكتشف الخطأ إلا وقت التشغيل، بعد وقوعه بزمن طويل، وفي كود قد يكون بعيدًا عن الكود الذي يحتوي على الخطأ. وحين ترى ClassCastException، سيتعيّن عليك البحث في قاعدة الكود عن استدعاء الدالة الذي وضع العملة المعدنية في مجموعة الطوابع. ولن يستطيع المُصرِّف مساعدتك.
16. Generics: تخلّص من التحذيرات غير المفحوصة (unchecked)
حين تبرمج باستخدام Generics، سترى كثيرًا من تحذيرات المُصرِّف:
- تحذيرات التحويل غير المفحوص (unchecked cast)
- تحذيرات استدعاء الدوال غير المفحوص
- تحذيرات أنواع vararg ذات المعاملات غير المفحوصة
- تحذيرات التحويل الضمني غير المفحوص.
كلما اكتسبت خبرة أكبر مع Generics قلّت التحذيرات التي تحصل عليها، لكن لا تتوقع أن يُصرَّف الكود المكتوب حديثًا بلا تحذيرات. كثير من التحذيرات غير المفحوصة سهل الإزالة. فمثلًا، لنفترض أنك كتبت هذا التصريح عن طريق الخطأ:
Set<Lark> exaltation = new HashSet();
غيّره إلى:
Set<Lark> exaltation = new HashSet<>();
بعض التحذيرات سيكون التخلّص منه أصعب بكثير. وحين تحصل على تحذيرات تتطلب بعض التفكير، فثابر! تخلّص من كل تحذير غير مفحوص تستطيع التخلص منه.
إذا تخلّصت من كل التحذيرات، فأنت مطمئن إلى أن كودك آمن من حيث الأنواع (typesafe)، وهذا أمر جيد جدًا. فهو يعني أنك لن تحصل على ClassCastException وقت التشغيل، ويزيد ثقتك
بأن برنامجك سيتصرف كما تريد. إذا لم تستطع التخلّص من تحذير، لكنك تستطيع إثبات أن الكود الذي أثاره آمن من حيث الأنواع، فعندها (وعندها فقط) اكتم
التحذير بالتعليق التوضيحي @SuppressWarnings("unchecked"). وإذا كتمت التحذيرات من دون أن تثبت أولًا أن الكود آمن من حيث الأنواع، فأنت تمنح نفسك شعورًا زائفًا
بالأمان.
فقد يُصرَّف الكود من دون أي تحذير، لكنه قد يرمي مع ذلك ClassCastException وقت التشغيل.
في كل مرة تستخدم فيها التعليق التوضيحي @SuppressWarnings("unchecked")، أضف تعليقًا يوضّح لماذا يُعدّ ذلك آمنًا.
17. Generics: حين يكون لها معنى
آخ – صنف في أمسّ الحاجة إلى Generics:
مثل هذا يجب اكتشافه في مراجعة طلب الدمج (Merge Request) على أبعد تقدير!
18. Generics: كيف لا تفعلها
عذرًا، لا حاجة إلى الكلام. أرجوك لا تدمج هذا أبدًا:
public abstract class JpaLearningCardBaseContentService<LC extends LearningCardBaseContent<L, LC, AC>, J extends JpaLearningCardBaseContent<L, LC, AC>, L extends
LearningCardBase<LC, A>, A extends AnswerOptionBase<L, AC>, AC extends AnswerOptionBaseContent<LC, AC, A>, AS extends AnswerOptionBaseService<A, AC, L, LC>, ACS extends
AnswerOptionBaseContentService<AC, A, L, LC>, LS extends LearningCardBaseService<L>>
extends JpaLightPersistenceService<LC, J, Long> implements LearningCardBaseContentService<LC, L, A, AC> {
// ...
}
لا ينبغي أبدًا أن يستغرق فهم مجرد تصريح صنف 5 دقائق.
19. فضّل القوائم على المصفوفات
تختلف المصفوفات عن الأنواع العامة في أمرين مهمين. أولًا، المصفوفات متغايرة (covariant). وهذه الكلمة المخيفة تعني ببساطة أنه إذا كان Sub نوعًا فرعيًا من Super، فإن
نوع المصفوفة Sub[] نوع فرعي من نوع المصفوفة Super[]. أما Generics فهي، على العكس، ثابتة (invariant): فلأي نوعين مختلفين Type1 وType2،
لا يكون List<Type1> نوعًا فرعيًا ولا نوعًا أعلى من List<Type2>.
20. فضّل تعابير Lambda على الأصناف المجهولة
// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
بدلًا من ذلك، افعل هذا:
// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
21. قلّل نطاق المتغيرات المحلية
أقوى أسلوب لتقليل نطاق متغير محلي هو التصريح عنه في المكان الذي يُستخدم فيه أول مرة...
22. فضّل حلقات for-each على حلقات for التقليدية
// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
... // Do something with e
}
23. اعرف المكتبات واستخدمها
لنفترض أنك تريد توليد أعداد صحيحة عشوائية بين الصفر وحدٍّ أعلى ما. أمام هذه المهمة الشائعة، سيكتب كثير من المبرمجين دالة صغيرة تبدو على هذا النحو تقريبًا:
// Common but deeply flawed!
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}
قد تبدو هذه الدالة جيدة، لكن فيها ثلاثة عيوب. الأول أنه إذا كان n قوة صغيرة للعدد اثنين، فإن متتالية الأعداد العشوائية ستكرر نفسها بعد فترة قصيرة نسبيًا. والعيب الثاني أنه إذا لم يكن n قوة للعدد اثنين، فإن بعض الأعداد ستُعاد في المتوسط أكثر من غيرها. وإذا كان n كبيرًا، فقد يكون هذا التأثير واضحًا جدًا.
أما العيب الثالث في دالة random فهو أنها قد تفشل فشلًا ذريعًا في حالات نادرة، فتُعيد عددًا خارج النطاق المحدد. ويحدث ذلك لأن
الدالة تحاول تحويل القيمة التي تُعيدها rnd.nextInt() إلى int غير سالب باستدعاء Math.abs. فإذا أعادت nextInt()
القيمة Integer.MIN_VALUE، فستُعيد Math.abs أيضًا Integer.MIN_VALUE، وسيُعيد عامل باقي القسمة (%) عددًا سالبًا، على افتراض
أن n ليس قوة للعدد اثنين. وهذا سيؤدي على الأرجح إلى فشل برنامجك، وقد يصعب إعادة إنتاج هذا الفشل.
24. تجنّب float وdouble إذا كانت الإجابات الدقيقة مطلوبة
استخدم BigDecimal للمبالغ المالية. فـ float وdouble تُنتجان أخطاء تقريب.
على سبيل المثال، لنفترض أن في جيبك 1.03 دولار، وأنفقت 42 سنتًا. كم بقي معك من المال؟ إليك جزء برنامج ساذج يحاول الإجابة عن هذا السؤال:
System.out.println(1.03 - 0.42);
للأسف، يطبع 0.6100000000000001. وهذه ليست حالة معزولة. لنفترض أن في جيبك دولارًا واحدًا، واشتريت تسع حلقات معدنية سعر الواحدة عشرة سنتات. كم
يتبقى لك؟
System.out.println(1.00 - 9 * 0.10);
وفقًا لجزء البرنامج هذا، يتبقى لك $0.09999999999999998.
قد تظن أن المشكلة يمكن حلّها بمجرد تقريب النتائج قبل طباعتها، لكن هذا للأسف لا ينجح دائمًا. فمثلًا، لنفترض أن في جيبك دولارًا واحدًا، وترى رفًا عليه صف من الحلوى اللذيذة بأسعار 10 سنتات و20 و30، وهكذا حتى دولار واحد. تشتري قطعة من كل نوع، بدءًا بالتي سعرها 10 سنتات، حتى لا تعود قادرًا على شراء القطعة التالية على الرف.
25. فضّل الأنواع الأولية على الأنواع الأولية المغلّفة
// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
26. أشِر إلى الكائنات عبر واجهاتها
مثال جيد:
Set<Son> sonSet = new LinkedHashSet<>();
مثال سيئ:
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
27. فضّل الواجهات على الانعكاس (Reflection)
وإلا:
- ستخسر كل مزايا فحص الأنواع وقت التصريف، بما في ذلك فحص الاستثناءات. فإذا حاول برنامج أن يستدعي بالانعكاس دالة غير موجودة أو غير قابلة للوصول، فسيفشل وقت التشغيل ما لم تكن قد اتخذت احتياطات خاصة.
- الكود اللازم للوصول بالانعكاس ثقيل ومُسهِب. كتابته مملة وقراءته صعبة.
- يتأثر الأداء سلبًا. فاستدعاء الدوال بالانعكاس أبطأ بكثير من الاستدعاء العادي.
28. التزم باصطلاحات التسمية المتعارف عليها
تملك منصة Java مجموعة راسخة من اصطلاحات التسمية، يرد كثير منها في The Java Language Specification [JLS, 6.1].
29. استخدم الاستثناءات للحالات الاستثنائية فقط
// Horrible abuse of exceptions. Don't ever do this!
try {
int i = 0;
while (true) {
range[i++].climb();
}
} catch (ArrayIndexOutOfBoundsException e) {
// catch it
}
ماذا يفعل هذا الكود؟ الأمر ليس واضحًا على الإطلاق عند النظر إليه، وهذا سبب كافٍ لعدم استخدامه. ويتبيّن أنه أسلوب سيئ التصور للغاية للمرور على عناصر
مصفوفة. فالحلقة اللانهائية تنتهي برمي ArrayIndexOutOfBoundsException والتقاطه وتجاهله حين
تحاول الوصول إلى أول عنصر خارج حدود المصفوفة. والمفترض أن يكون مكافئًا للأسلوب القياسي للمرور على مصفوفة، الذي يعرفه فورًا
أي مبرمج Java:
for (Mountain m : range) {
m.climb();
}
الاستثناءات، كما يدل اسمها، يجب ألا تُستخدم إلا للحالات الاستثنائية؛ ولا ينبغي أبدًا استخدامها للتحكم العادي في سير البرنامج.
30. استخدم الاستثناءات المفحوصة للحالات القابلة للتعافي، واستثناءات وقت التشغيل لأخطاء البرمجة
استخدم الاستثناءات المفحوصة (checked) للحالات التي يُتوقع بشكل معقول أن يتعافى منها المستدعي. فبمواجهة المستخدم باستثناء مفحوص، يفرض مصمم الـ API عليه التعافي من الحالة. ويمكن للمستخدم تجاهل هذا الفرض بالتقاط الاستثناء وتجاهله، لكن هذا عادةً فكرة سيئة.
استخدم استثناءات وقت التشغيل للإشارة إلى أخطاء البرمجة. الغالبية العظمى من استثناءات وقت التشغيل تشير إلى انتهاك الشروط المسبقة. وانتهاك الشرط المسبق هو ببساطة إخفاق
عميل الـ API في الالتزام بالعقد الذي تحدده مواصفات الـ API. فمثلًا، ينص عقد الوصول إلى المصفوفة على أن فهرس المصفوفة يجب أن يكون بين الصفر وطول المصفوفة
ناقص واحد، شاملًا الطرفين. ويشير ArrayIndexOutOfBoundsException إلى انتهاك هذا الشرط المسبق.
31. تجنّب الاستخدام غير الضروري للاستثناءات المفحوصة
خلاصة القول، حين تُستخدم الاستثناءات المفحوصة باعتدال يمكنها أن تزيد موثوقية البرامج؛ وحين يُفرط في استخدامها تجعل الواجهات البرمجية مؤلمة الاستخدام.
Pair<Boolean, LearningCardGroup> answerLearningCard(@NotNull LearningCardGroup learningCardGroup,
LearningCardAssignment learningCardAssignment,
@NotNull Collection<AnswerOption> answers)
throws InvalidTrainingStateException, PersistenceException, LearningCardGroupAlreadyCompletedException,
ConsecutiveCorrectCountExceededException,
PreviousLearningCardGroupUncompletedException, LearningCardGroupCooldownException,
LearningCardGroupHasUncompletedLearningCardsException, InvalidParticipantException, TrainingNotSignedException; // but why ???
// this is a mistake seen a lot with junior devs
// exceptions are exceptionally slow and should be avoided for mere validation
// the performance gets even worse if a stacktrace is printed, since logging and printing is one of the slowest possible actions
32. فضّل استخدام الاستثناءات القياسية
من السمات التي تميّز المبرمجين الخبراء عن الأقل خبرة أن الخبراء يسعون إلى درجة عالية من إعادة استخدام الكود، ويحققونها عادةً.
والاستثناءات ليست استثناءً من قاعدة أن إعادة استخدام الكود أمر جيد. توفر مكتبات Java مجموعة من الاستثناءات تغطي معظم احتياجات رمي الاستثناءات في معظم الواجهات البرمجية. ولإعادة استخدام الاستثناءات القياسية فوائد عدة. أهمها أنها تجعل الـ API الخاص بك أسهل في التعلّم والاستخدام، لأنها توافق الاصطلاحات الراسخة التي يألفها المبرمجون أصلًا. وتليها مباشرة أن البرامج التي تستخدم الـ API الخاص بك أسهل قراءة، لأنها غير مزدحمة باستثناءات غير مألوفة. وأخيرًا (وأقلها أهمية)، فإن قلة أصناف الاستثناءات تعني استهلاكًا أقل للذاكرة ووقتًا أقل في تحميل الأصناف.
أكثر أنواع الاستثناءات إعادةً للاستخدام هو IllegalArgumentException. وهو عمومًا الاستثناء الذي يُرمى حين يمرّر المستدعي وسيطًا قيمته غير مناسبة. فمثلًا،
هذا هو الاستثناء الذي يُرمى إذا مرّر المستدعي عددًا سالبًا في معامل يمثّل عدد مرات تكرار إجراء ما. ومن الاستثناءات الأخرى الشائعة إعادة الاستخدام
IllegalStateException. وهو عمومًا الاستثناء الذي يُرمى إذا كان الاستدعاء غير مشروع بسبب حالة الكائن المستقبِل. فمثلًا، هذا هو
الاستثناء الذي يُرمى إذا حاول المستدعي استخدام كائن ما قبل تهيئته بشكل صحيح.
يمكن القول إن كل استدعاء خاطئ لدالة يرجع في النهاية إلى وسيط أو حالة غير مشروعة، لكن هناك استثناءات أخرى تُستخدم قياسيًا لأنواع معينة من الوسائط والحالات غير المشروعة. فإذا
مرّر المستدعي null في معامل لا يُسمح فيه بقيم null، فإن الاصطلاح يقضي برمي NullPointerException بدلًا من IllegalArgumentException.
وبالمثل، إذا مرّر المستدعي قيمة خارج النطاق في معامل يمثّل فهرسًا في متتالية، فيجب رمي IndexOutOfBoundsException بدلًا من IllegalArgumentException.
لا تُعِد استخدام Exception أو RuntimeException أو Throwable أو Error مباشرة. عامل هذه الأصناف كما لو كانت مجردة. ومع ذلك يمكن استخدام RuntimeException إذا أردت
تحويل استثناء مفحوص إلى استثناء وقت تشغيل، وكنت تعرف حقًا ما تفعله.
33. لا تتجاهل الاستثناءات
// Empty catch block ignores exception - Highly suspect!
try {
...
} catch (SomeException e) {
}
كتلة catch الفارغة تُفشل الغاية من الاستثناءات، وهي إجبارك على التعامل مع الحالات الاستثنائية. وتجاهل استثناء أشبه بتجاهل إنذار الحريق – بل وإطفائه حتى لا تتاح لأحد غيرك فرصة التحقق مما إذا كان هناك حريق فعلي.