بهترین روشها از «Effective Java (3rd Edition)»
برگرفته از «Effective Java (3rd Edition) » نوشتهی Joshua Bloch
۱. مجموعه یا آرایهی خالی برگردانید، نه null
اگر متد یک Set برمیگرداند، برای نتیجهی خالی return null; ننویسید – به جایش return Set.of();.
۲. به جای سازندهها، متدهای کارخانهای استاتیک را در نظر بگیرید
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
همیشه بهتر نیستند، اما در برخی موارد استفاده مزایای فراوانی دارند:
- یکی از مزایای متدهای کارخانهای استاتیک این است که برخلاف سازندهها، نام دارند.
- لازم نیست هر بار که فراخوانی میشوند، شیء جدیدی بسازند.
- میتوانند شیئی از هر زیرنوعِ نوع بازگشتیشان برگردانند.
- کلاس شیء برگشتی میتواند از فراخوانی به فراخوانی، بسته به پارامترهای ورودی، متفاوت باشد.
- لازم نیست کلاس شیء برگشتی در زمان نوشتن کلاسِ دربردارندهی متد وجود داشته باشد.
معایب:
- محدودیت اصلیِ ارائهی صرفاً متدهای کارخانهای استاتیک این است که از کلاسهای بدون سازندهی public یا protected نمیتوان زیرکلاس ساخت.
- کاستی دوم متدهای کارخانهای استاتیک این است که برنامهنویسان بهسختی پیدایشان میکنند.
۳. وقتی با پارامترهای زیادی در سازنده روبهرو هستید، Builder را در نظر بگیرید
کارخانههای استاتیک و سازندهها یک محدودیت مشترک دارند: با تعداد زیادی پارامتر اختیاری خوب مقیاس نمیپذیرند. این مشکل به برخی پروژههایی که رویشان کار کردهام ضربهی سختی زد.
۴. ویژگی Singleton را با سازندهی private یا نوع enum اعمال کنید
هر چیزی که به چند نمونه نیاز ندارد باید فقط یکبار نمونهسازی شود – چه با @ApplicationScoped و چه با الگوی Singleton دیگری.
۵. با سازندهی private از نمونهسازی جلوگیری کنید
گاهی میخواهید کلاسی بنویسید که فقط مجموعهای از متدها و فیلدهای استاتیک است. چنین کلاسهایی بدنام شدهاند، چون برخی از آنها سوءاستفاده میکنند تا از فکر کردن در قالب اشیا طفره بروند، اما کاربردهای معتبری هم دارند.
private MyStaticClass() {
// private constructor to prevent instantiation
}
۶. از ساختن اشیای غیرضروری پرهیز کنید
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، یعنی برنامه حدود ۲^۳۱ نمونهی غیرضروری Long میسازد (تقریباً یکی برای هر بار که long i به Long sum افزوده میشود).
۷. از Finalizerها و Cleanerها پرهیز کنید
Finalizerها پیشبینیناپذیر، اغلب خطرناک و عموماً غیرضروریاند. استفاده از آنها میتواند به رفتار نامنظم، کارایی ضعیف و مشکلات قابلیت حمل منجر شود.
۸. 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();
}
۹. پیادهسازی Comparable را در نظر بگیرید
با پیادهسازی Comparable، به کلاستان اجازه میدهید با همهی الگوریتمهای عمومی و پیادهسازیهای Collection که به این اینترفیس وابستهاند، همکاری کند.
۱۰. دسترسیپذیری کلاسها و اعضا را به حداقل برسانید
مهمترین عاملی که یک مؤلفهی خوشطراحی را از یک مؤلفهی بدطراحی متمایز میکند، میزانی است که مؤلفه دادههای داخلی و دیگر جزئیات پیادهسازیاش را از مؤلفههای دیگر پنهان میکند. یک مؤلفهی خوشطراحی همهی جزئیات پیادهسازیاش را پنهان میکند و API خود را بهطور تمیز از پیادهسازیاش جدا میکند. در این صورت مؤلفهها فقط از طریق APIهایشان با هم ارتباط برقرار میکنند و از سازوکار درونی یکدیگر بیخبرند. این مفهوم، که با نام پنهانسازی اطلاعات یا کپسولهسازی شناخته میشود، اصلی بنیادین در طراحی نرمافزار است.
۱۱. در کلاسهای public از متدهای دسترسی استفاده کنید، نه فیلدهای public
گاهی ممکن است وسوسه شوید کلاسهای تباهشدهای بنویسید که هیچ هدفی جز گروهبندی فیلدهای نمونه ندارند:
// Degenerate classes like this should not be public!
class Point {
public double x;
public double y;
}
چون فیلدهای دادهی چنین کلاسهایی مستقیماً در دسترساند، این کلاسها مزایای کپسولهسازی را ارائه نمیدهند. نمیتوانید بازنمایی را بدون تغییر API عوض کنید، نمیتوانید ناورداها (invariants) را اعمال کنید، و نمیتوانید هنگام دسترسی به یک فیلد اقدام کمکی انجام دهید. برنامهنویسان شیءگرای سرسخت معتقدند چنین کلاسهایی ناپسندند و همیشه باید با کلاسهایی دارای فیلدهای private و متدهای دسترسی public (getterها) و، برای کلاسهای تغییرپذیر، تغییردهندهها (setterها) جایگزین شوند:
// 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; }
}
بیشک سرسختها در مورد کلاسهای public حق دارند: اگر کلاسی بیرون از پکیجش در دسترس است، متدهای دسترسی فراهم کنید تا انعطاف تغییر بازنمایی داخلی کلاس حفظ شود. اگر یک کلاس public فیلدهای دادهاش را در معرض بگذارد، هر امیدی به تغییر بازنماییاش از دست میرود، چون کد کلاینتها ممکن است همهجا پخش شده باشد.
اما اگر کلاسی package-private یا یک کلاس تودرتوی private باشد، ذاتاً هیچ ایرادی ندارد که فیلدهای دادهاش را در معرض بگذارد – به شرطی که آن فیلدها انتزاعِ ارائهشده توسط کلاس را بهخوبی توصیف کنند.
۱۲. تغییرپذیری را به حداقل برسانید
کلاس تغییرناپذیر (immutable) بهسادگی کلاسی است که نمونههایش قابل تغییر نیستند. همهی اطلاعات موجود در هر نمونه در طول عمر شیء ثابت است، پس هیچ تغییری هرگز دیده نمیشود. کتابخانههای پلتفرم Java کلاسهای تغییرناپذیر بسیاری دارند، از جمله String، کلاسهای بستهبندی انواع اولیه، و BigInteger و BigDecimal. دلایل خوب زیادی برای این کار هست: کلاسهای تغییرناپذیر طراحی، پیادهسازی و استفادهی آسانتری از کلاسهای تغییرپذیر دارند. کمتر مستعد خطا هستند و امنترند. برای تغییرناپذیر کردن یک کلاس، این پنج قاعده را دنبال کنید:
- متدی فراهم نکنید که وضعیت شیء را تغییر دهد (که به آنها تغییردهنده (mutator) میگویند).
-
مطمئن شوید که کلاس قابل گسترش نیست. این کار از آن جلوگیری میکند که زیرکلاسهای بیدقت یا بدخواه، با رفتاری که گویی وضعیت شیء
تغییر کرده، رفتار تغییرناپذیر کلاس را مخدوش کنند. جلوگیری از زیرکلاسسازی معمولاً مثلاً با
finalکردن کلاس انجام میشود. - همهی فیلدها را final کنید. این کار نیت شما را به شکلی که سیستم اعمالش میکند بهروشنی بیان میکند. همچنین برای تضمین رفتار درست لازم است، اگر ارجاعی به نمونهای تازه ساختهشده بدون همگامسازی از یک نخ به نخ دیگر پاس داده شود، همانطور که در مدل حافظه آمده است.
- همهی فیلدها را private کنید. این کار مانع میشود که کلاینتها به اشیای تغییرپذیری که فیلدها به آنها اشاره میکنند دسترسی پیدا کنند و مستقیماً تغییرشان دهند. اگرچه از نظر فنی مجاز است که کلاسهای تغییرناپذیر فیلدهای public final حاوی مقادیر اولیه یا ارجاع به اشیای تغییرناپذیر داشته باشند، این کار توصیه نمیشود، چون تغییر بازنمایی داخلی در نسخههای بعدی را ناممکن میکند.
-
دسترسی انحصاری به هر مؤلفهی تغییرپذیر را تضمین کنید. اگر کلاستان فیلدهایی دارد که به اشیای تغییرپذیر اشاره میکنند، مطمئن شوید کلاینتهای کلاس نمیتوانند به
این اشیا ارجاع بگیرند. هرگز چنین فیلدی را با ارجاع شیئی که کلاینت داده مقداردهی نکنید و آن فیلد را از یک متد دسترسی برنگردانید. در سازندهها، متدهای دسترسی و متدهای
readObjectکپیهای تدافعی بسازید.
۱۳. ترکیب را به وراثت ترجیح دهید
وراثت روشی قدرتمند برای استفادهی مجدد از کد است، اما همیشه بهترین ابزار برای این کار نیست. اگر نابهجا استفاده شود، به نرمافزار شکننده میانجامد. استفاده از وراثت درون یک پکیج امن است، جایی که پیادهسازی زیرکلاس و ابرکلاس زیر نظر همان برنامهنویسان است. همچنین استفاده از وراثت هنگام گسترش کلاسهایی که مشخصاً برای گسترش طراحی و مستند شدهاند امن است. اما ارثبری از کلاسهای عینی معمولی از مرزهای پکیج خطرناک است.
برخلاف فراخوانی متد، وراثت کپسولهسازی را نقض میکند. شاید فکر کنید اگر فقط متدهای جدید اضافه کنید و از بازنویسی متدهای موجود خودداری کنید، گسترش یک کلاس امن است. اگرچه این نوع گسترش بسیار امنتر است، بیخطر نیست. اگر ابرکلاس در نسخهی بعدی متد جدیدی بگیرد و بدشانسی بیاورید که به زیرکلاس متدی با همان امضا و نوع بازگشتی متفاوت داده باشید، زیرکلاستان دیگر کامپایل نمیشود. اگر به زیرکلاس متدی با همان امضا و نوع بازگشتیِ متد جدید ابرکلاس داده باشید، حالا آن را بازنویسی میکنید و در معرض همان مشکلاتی هستید که پیشتر گفته شد. افزون بر این، تردید جدی هست که متد شما قرارداد متد جدید ابرکلاس را برآورده کند، چون آن قرارداد هنگامی که متد زیرکلاس را مینوشتید هنوز نوشته نشده بود.
خوشبختانه راهی برای پرهیز از همهی مشکلات بالا هست. به جای گسترش یک کلاس موجود، به کلاس جدیدتان یک فیلد private بدهید که به نمونهای از کلاس موجود ارجاع دارد. این طراحی را ترکیب (composition) مینامند، چون کلاس موجود به یکی از مؤلفههای کلاس جدید تبدیل میشود. هر متد نمونه در کلاس جدید، متد متناظر را روی نمونهی دربرگرفتهشدهی کلاس موجود فراخوانی میکند و نتایج را برمیگرداند. به این کار ارسال (forwarding) میگویند و متدهای کلاس جدید را متدهای ارسال مینامند. کلاس حاصل مثل سنگ محکم خواهد بود و هیچ وابستگیای به جزئیات پیادهسازی کلاس موجود نخواهد داشت. حتی افزودن متدهای جدید به کلاس موجود هم هیچ اثری روی کلاس جدید نخواهد داشت.
۱۴. اینترفیسها را به کلاسهای انتزاعی ترجیح دهید
Java دو سازوکار برای تعریف نوعی دارد که چند پیادهسازی را مجاز میکند: اینترفیسها و کلاسهای انتزاعی. از زمان معرفی متدهای پیشفرض (default methods) برای اینترفیسها در Java 8، هر دو سازوکار به شما امکان میدهند برای برخی متدهای نمونه پیادهسازی فراهم کنید.
یک تفاوت مهم این است که برای پیادهسازی نوعی که یک کلاس انتزاعی تعریف کرده، کلاس باید زیرکلاس آن کلاس انتزاعی باشد. از آنجا که Java فقط وراثت یگانه را مجاز میداند، این محدودیتِ کلاسهای انتزاعی استفاده از آنها را بهعنوان تعریف نوع بهشدت محدود میکند. اما هر کلاسی که همهی متدهای لازم را تعریف کند و از قرارداد کلی پیروی کند، مجاز است یک اینترفیس را پیادهسازی کند، صرفنظر از اینکه آن کلاس در کجای سلسلهمراتب کلاسها قرار دارد. کلاسهای موجود را میتوان بهراحتی طوری بازسازی کرد که اینترفیس جدیدی را پیادهسازی کنند.
- اینترفیسها برای تعریف Mixinها ایدهآلاند.
- اینترفیسها امکان ساخت چارچوبهای نوعیِ غیرسلسلهمراتبی را فراهم میکنند.
- اینترفیسها بهبودهای کارکردی امن و قدرتمند را ممکن میکنند، از طریق الگوی کلاس پوشاننده (wrapper class). اگر برای تعریف نوعها از کلاسهای انتزاعی استفاده کنید، برای برنامهنویسی که میخواهد کارکردی اضافه کند، راهی جز وراثت باقی نمیگذارید.
۱۵. Generics: از نوعهای خام (raw types) استفاده نکنید
خطا را تا زمان اجرا کشف نمیکنید، مدتها پس از وقوعش، و در کدی که ممکن است از کد حاوی خطا دور باشد. وقتی ClassCastException را میبینید، باید کل کدبیس را بگردید تا فراخوانی متدی را پیدا کنید که سکه را در کلکسیون تمبر گذاشته است. کامپایلر نمیتواند کمکتان کند.
۱۶. Generics: هشدارهای unchecked را از بین ببرید
وقتی با Generics برنامه مینویسید، هشدارهای کامپایلر زیادی خواهید دید:
- هشدارهای unchecked cast
- هشدارهای فراخوانی متد unchecked
- هشدارهای نوع vararg پارامتری unchecked
- هشدارهای تبدیل unchecked.
هرچه تجربهی بیشتری با Generics کسب کنید، هشدارهای کمتری میگیرید، اما انتظار نداشته باشید کدی که تازه نوشتهاید بیهشدار کامپایل شود. بسیاری از هشدارهای unchecked بهراحتی برطرف میشوند. برای مثال، فرض کنید بهاشتباه این اعلان را نوشتهاید:
Set<Lark> exaltation = new HashSet();
آن را به این تغییر دهید:
Set<Lark> exaltation = new HashSet<>();
برطرف کردن برخی هشدارها بسیار دشوارتر خواهد بود. وقتی هشدارهایی میگیرید که نیاز به فکر دارند، پافشاری کنید! هر هشدار unchecked را که میتوانید، از بین ببرید.
اگر همهی هشدارها را از بین ببرید، مطمئن میشوید که کدتان از نظر نوع ایمن (typesafe) است، و این چیز بسیار خوبی است. یعنی در زمان اجرا ClassCastException نخواهید گرفت، و اطمینانتان
بیشتر میشود که برنامه همانطور که میخواستید رفتار میکند. اگر نمیتوانید هشداری را از بین ببرید، اما میتوانید ثابت کنید کدی که آن را برانگیخته از نظر نوع ایمن است، آنگاه (و فقط آنگاه)
هشدار را با حاشیهنویسی @SuppressWarnings("unchecked") سرکوب کنید. اگر هشدارها را بدون اثبات ایمنی نوع کد سرکوب کنید، به خودتان حس کاذب
امنیت میدهید.
کد ممکن است بدون هیچ هشداری کامپایل شود، اما باز هم در زمان اجرا ClassCastException پرتاب کند.
هر بار که از حاشیهنویسی @SuppressWarnings("unchecked") استفاده میکنید، کامنتی اضافه کنید که بگوید چرا این کار امن است.
۱۷. Generics: وقتی منطقی است
آخ – کلاسی که بهشدت به Generics نیاز دارد:
چنین چیزی باید حداکثر در Merge Request گرفته شود!
۱۸. Generics: چطور نباید انجامش داد
ببخشید، نیازی به توضیح نیست. لطفاً این را هرگز merge نکنید:
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> {
// ...
}
فهمیدن یک اعلان سادهی کلاس هرگز نباید ۵ دقیقه طول بکشد.
۱۹. فهرستها را به آرایهها ترجیح دهید
آرایهها از دو جنبهی مهم با نوعهای جنریک فرق دارند. نخست، آرایهها هموردا (covariant) هستند. این واژهی ترسناک صرفاً یعنی اگر Sub زیرنوعی از Super باشد،
نوع آرایهی Sub[] زیرنوعی از نوع آرایهی Super[] است. در مقابل، Generics ناوردا (invariant) هستند: برای هر دو نوع متمایز Type1 و Type2،
List<Type1> نه زیرنوع List<Type2> است و نه ابرنوع آن.
۲۰. 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()));
۲۱. دامنهی متغیرهای محلی را به حداقل برسانید
قویترین تکنیک برای به حداقل رساندن دامنهی یک متغیر محلی این است که آن را همان جایی اعلان کنید که نخستین بار استفاده میشود...
۲۲. حلقههای for-each را به حلقههای for سنتی ترجیح دهید
// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
... // Do something with e
}
۲۳. کتابخانهها را بشناسید و از آنها استفاده کنید
فرض کنید میخواهید اعداد صحیح تصادفی بین صفر و یک کران بالا تولید کنید. بسیاری از برنامهنویسان در برابر این کار رایج، متد کوچکی مینویسند که چیزی شبیه این است:
// 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() برمیگرداند، با فراخوانی Math.abs به یک int نامنفی نگاشت کند. اگر nextInt()
مقدار Integer.MIN_VALUE را برگرداند، Math.abs هم Integer.MIN_VALUE را برمیگرداند، و عملگر باقیمانده (%) عددی منفی برمیگرداند، با این فرض
که n توانی از دو نباشد. این تقریباً بهطور قطع باعث شکست برنامهتان میشود، و بازتولید این شکست ممکن است دشوار باشد.
۲۴. اگر جواب دقیق لازم است، از float و double پرهیز کنید
برای مبالغ پولی از BigDecimal استفاده کنید. float و double خطای گرد کردن تولید میکنند.
برای مثال، فرض کنید ۱٫۰۳ دلار در جیب دارید و ۴۲ سنت خرج میکنید. چقدر پول برایتان مانده است؟ این یک قطعه برنامهی سادهلوحانه است که میکوشد به این پرسش پاسخ دهد:
System.out.println(1.03 - 0.42);
متأسفانه خروجیاش 0.6100000000000001 است. این یک مورد استثنایی نیست. فرض کنید یک دلار در جیب دارید و نُه واشر به قیمت هر کدام ده سنت میخرید. چقدر
باقیمانده پس میگیرید؟
System.out.println(1.00 - 9 * 0.10);
طبق این قطعه برنامه، $0.09999999999999998 پس میگیرید.
شاید فکر کنید مشکل را میتوان صرفاً با گرد کردن نتایج پیش از چاپ حل کرد، اما متأسفانه این همیشه جواب نمیدهد. برای مثال، فرض کنید یک دلار در جیب دارید و قفسهای میبینید با ردیفی از آبنباتهای خوشمزه به قیمت ۱۰ سنت، ۲۰ سنت، ۳۰ سنت و همینطور تا یک دلار. از هر آبنبات یکی میخرید، از آنکه ۱۰ سنت قیمت دارد شروع میکنید، تا جایی که دیگر پول خرید آبنبات بعدی قفسه را نداشته باشید.
۲۵. نوعهای اولیه را به نوعهای اولیهی بستهبندیشده ترجیح دهید
// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
۲۶. به اشیا از طریق اینترفیسهایشان ارجاع دهید
مثال خوب:
Set<Son> sonSet = new LinkedHashSet<>();
مثال بد:
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
۲۷. اینترفیسها را به Reflection ترجیح دهید
در غیر این صورت:
- همهی مزایای بررسی نوع در زمان کامپایل را از دست میدهید، از جمله بررسی استثناها. اگر برنامهای بکوشد با Reflection متدی ناموجود یا غیرقابلدسترس را فراخوانی کند، در زمان اجرا شکست میخورد، مگر اینکه تدابیر ویژهای اندیشیده باشید.
- کد لازم برای دسترسی بازتابی ناشیانه و پرطولوتفصیل است. نوشتنش ملالآور و خواندنش دشوار است.
- کارایی آسیب میبیند. فراخوانی متد با Reflection بسیار کندتر از فراخوانی عادی متد است.
۲۸. به قراردادهای نامگذاری پذیرفتهشده پایبند باشید
پلتفرم Java مجموعهای جاافتاده از قراردادهای نامگذاری دارد که بسیاری از آنها در The Java Language Specification [JLS, 6.1] آمدهاند.
۲۹. از استثناها فقط برای شرایط استثنایی استفاده کنید
// 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();
}
استثناها، همانطور که از نامشان پیداست، فقط برای شرایط استثنایی هستند؛ هرگز نباید از آنها برای جریان کنترل عادی استفاده کرد.
۳۰. برای شرایط قابل بازیابی از استثناهای checked و برای خطاهای برنامهنویسی از استثناهای زمان اجرا استفاده کنید
از استثناهای checked برای شرایطی استفاده کنید که بهطور معقول میتوان انتظار داشت فراخوان از آنها بازیابی شود. طراح API با روبهرو کردن کاربر با یک استثنای checked، او را ملزم به بازیابی از آن شرایط میکند. کاربر میتواند با گرفتن و نادیده گرفتن استثنا این الزام را نادیده بگیرد، اما این معمولاً فکر بدی است.
از استثناهای زمان اجرا برای نشان دادن خطاهای برنامهنویسی استفاده کنید. اکثریت قریب به اتفاق استثناهای زمان اجرا نشانهی نقض پیششرطها هستند. نقض پیششرط صرفاً یعنی کلاینت
یک API به قراردادی که مشخصات API تعیین کرده پایبند نبوده است. برای مثال، قرارداد دسترسی به آرایه تعیین میکند که اندیس آرایه باید بین صفر و طول آرایه
منهای یک، با احتساب هر دو، باشد. ArrayIndexOutOfBoundsException نشان میدهد که این پیششرط نقض شده است.
۳۱. از استفادهی غیرضروری از استثناهای checked پرهیز کنید
بهطور خلاصه، استثناهای checked اگر با خست به کار روند، میتوانند قابلیت اطمینان برنامهها را بالا ببرند؛ اگر زیادهروی شود، استفاده از APIها را دردناک میکنند.
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
۳۲. استفاده از استثناهای استاندارد را ترجیح دهید
یکی از ویژگیهایی که برنامهنویسان خبره را از کمتجربهترها متمایز میکند این است که خبرهها برای رسیدن به درجهی بالایی از استفادهی مجدد از کد تلاش میکنند و معمولاً به آن میرسند.
استثناها از این قاعده که استفادهی مجدد از کد چیز خوبی است مستثنا نیستند. کتابخانههای Java مجموعهای از استثناها فراهم میکنند که بیشتر نیازهای پرتاب استثنا در بیشتر APIها را پوشش میدهد. استفادهی مجدد از استثناهای استاندارد چند مزیت دارد. مهمترینش این است که API شما را آسانتر قابل یادگیری و استفاده میکند، چون با قراردادهای جاافتادهای که برنامهنویسان از پیش با آنها آشنا هستند مطابقت دارد. مزیت بعدی، با فاصلهای اندک، این است که برنامههایی که از API شما استفاده میکنند خواناترند، چون با استثناهای ناآشنا شلوغ نشدهاند. و در آخر (و کماهمیتتر از همه)، کلاسهای استثنای کمتر یعنی ردپای حافظهی کوچکتر و زمان کمتر برای بارگذاری کلاسها.
پرکاربردترین نوع استثنایی که دوباره استفاده میشود IllegalArgumentException است. این معمولاً همان استثنایی است که وقتی فراخوان آرگومانی با مقدار نامناسب میفرستد، پرتاب میشود. برای
مثال، اگر فراخوان در پارامتری که تعداد دفعات تکرار یک عمل را نشان میدهد عددی منفی بفرستد، این همان استثنایی است که باید پرتاب شود. استثنای دیگری که زیاد دوباره
استفاده میشود IllegalStateException است. این معمولاً همان استثنایی است که وقتی فراخوانی به دلیل وضعیت شیء دریافتکننده غیرمجاز است، پرتاب میشود. برای مثال، این همان
استثنایی است که باید پرتاب شود اگر فراخوان بکوشد از شیئی پیش از مقداردهی اولیهی درستش استفاده کند.
میتوان گفت هر فراخوانی نادرست متد در نهایت به یک آرگومان یا وضعیت غیرمجاز برمیگردد، اما استثناهای دیگری هم هستند که بهطور استاندارد برای انواع خاصی از آرگومانها و وضعیتهای غیرمجاز به کار میروند. اگر
فراخوان در پارامتری که مقدار null در آن ممنوع است، null بفرستد، طبق قرارداد باید NullPointerException پرتاب شود نه IllegalArgumentException.
به همین ترتیب، اگر فراخوان در پارامتری که اندیسی در یک دنباله را نشان میدهد مقداری بیرون از بازه بفرستد، باید IndexOutOfBoundsException پرتاب شود نه IllegalArgumentException.
از Exception، RuntimeException، Throwable یا Error مستقیماً دوباره استفاده نکنید. با این کلاسها طوری رفتار کنید که گویی انتزاعیاند. البته اگر بخواهید
یک استثنای checked را به استثنای زمان اجرا تبدیل کنید و واقعاً بدانید چه میکنید، میتوانید از RuntimeException استفاده کنید.
۳۳. استثناها را نادیده نگیرید
// Empty catch block ignores exception - Highly suspect!
try {
...
} catch (SomeException e) {
}
یک بلوک catch خالی هدف استثناها را بیاثر میکند، که همانا وادار کردن شما به رسیدگی به شرایط استثنایی است. نادیده گرفتن یک استثنا مثل نادیده گرفتن آژیر آتشسوزی است – و خاموش کردنش تا هیچکس دیگر فرصت نکند ببیند آیا واقعاً آتشی در کار است یا نه.