چگونه از اکسپشنهای جاوا بهتر استفاده کنیم؟
استثناها یا اکسپشنها در بسیاری از زبانهای برنامهنویسی برای مدیریت خطاها و مشکلات احتمالی وجود دارند. لازمهٔ برنامهنویس خبره شدن آشنایی با استثناها و ساخت کلاسهای استثنای جدید و دریافت (catch) کردن آنها در جای مناسب است. بسیاری از اوقات ما تنها به یادگیری سینتکس throw و catch و finally بسنده میکنیم اما چیزی که در نهایت اهمیت دارد، استفاده درست و «بِهروش»هاست. در این مطلب میخواهیم تعدادی از کارهای اشتباهی که بین برنامهنویسان جاوا رایج است را به همراه راه اصلاح آنها با هم مرور کنیم.
مرور کلی روی استثناها در جاوا
برای شروع، سلسه مراتب throwableها را اینجا آوردهایم.
کلاس اصلی Throwable است که هر شی قابل throw را شامل میشود. شامل ۲ زیرکلاس اصلی Error و Exception است. تفاوت ارور و استثنا در این است که Error زمانی اتفاق میافتد که خطای مهلکی پیش آمده مثلا ارور پر شدن حافظه یا سرریز پشته، از آنجا که در قبال این ارورها کاری نمیتوانیم بکنیم آنها را دریافت نمیکنیم و خود جاوا هم آنها را چکنشده باقی گذاشته است.
در مقابل، Exception را داریم که به خطاهایی اشاره دارد که در برنامه رخ میدهند و حالات خاصی مثل پیدا نشدن فایل یا تقسیم بر صفر هستند. در این موارد اگرچه متدی که در آن خطا رخ میدهد نمیداند با آن چطور رفتار کند ولی به لطف وجود چارچوبِ Exception Handling میتواند یک استثنا را پرتاب کند تا یکی از متدهای موجود در پشته که به صورت مستقیم یا غیرمستقیم متدِ اشکالدار را فراخوانی کرده و میداند خطا را چگونه مدیریت کند آن را دریافت و مدیریت کند.
در خود کلاس Exception نیز، همه استثناها چکشده نیستند و زیرکلاسِ RuntimeException مربوط به استثناهای چکنشدهاست. یعنی استثناهایی که لازم نیست برنامهنویس تصریح کند متدش این استثناها را پرتاب میکند و حتما هم لازم نیست از try/catch استفاده شود.
بعد از try/catch، گاهی قسمتِ finally را داریم که برنامهنویس میتواند از آن استفاده کند و تمیزکاریهای احتمالی را انجام دهد، مثلا منابع را close کند.
چه کارهایی باید انجام دهیم و چه کارهایی نباید؟
1- در بلوک catch، هیچوقت null برنگردانید
catch (NoSuchMethodException e) { return null; }
شاید به نظر بیاید همینکه یک استثنا را دریافت کردیم و نمیتوانیم برای هندل کردن آن کار خاصی انجام دهیم، بهتر است به جای خروجی متدِ خود، null برگردانیم و با این روش متدی که متدِ ما را صدا زده احتمالا خودش متوجه خطا میشود.
اما این شیوه نادرستی است. ما به جای اینکه خطا را از منبع شناسایی کنیم، علت آن را پنهان کردهایم و تنها باعث یک استثنای NullPointer در جای دیگری از کد شدهایم. این کار پیدا کردن و برطرف کردن مشکل را بسیار پیچیده و زمانبر میکند.
2- استثناهای چکشده را دقیقا مشخص کنید
اگر متدِ ما استثنای چکشده پرتاب میکند، ممکن است تصمیم بگیریم که در امضای متد بنویسیم throws Exception و نوع استثناها را مشخص نکنیم. اینکار در کوتاه مدت به نظر سادهتر میآید ولی از نظر طراحی ایراد دارد، چرا که اگر همکار ما قرار باشد چند ماه بعد روی این کد کار کند و از این متد استفاده کند، دقیق نمیداند چه استثناهایی ممکن است پرتاب شود و مجبور است برای انواع حالتها و استثناها آمادگی داشته باشد. اما در صورتی که استثنا را دقیقا مشخص کنیم همکارمان (یا استفادهکننده از کدِ ما) میداند باید چه استثناهایی را دریافت کند.
public void doNotDoThis() throws Exception { ... } public void doThis() throws NumberFormatException { ... }
3- هیچگاه Throwable را دریافت نکنید
همانطور که گفته شد، Throwable کلاس مادرِ همه استثناها و ارورهاست. اگرچه به لحاظ قواعد زبان جاوا، میتوانیم همه ارورها و استثناها را با هم دریافت کنیم ولی باید از این کار اجتناب کنیم. ارور زمانی اتفاق میافتد که خود JVM قادر به رفع مشکل نیست. یعنی مثلا ارور سرریز پشته یا پر شدن حافظه رخ داده است و این حالتها چیزی نیست که خود برنامه بخواهد مدیریت کند و بعد از آن هم به کارش ادامه دهد. باید اجازه دهیم این ارورها برنامه را مختل کنند و برنامه بسته شود چون اصلا چاره دیگری نداریم. یادمان نرود که زمانی که ارور رخ داده یعنی به یک حالت غیرقابلبازگشت رسیدهایم.
public void doNotCatchThrowable() { try { // do something } catch (Throwable t) { // don't do this! } }
4- با wrap کردنِ استثنا، جلوی از دست رفتن stack trace را بگیرید
catch (NoSuchMethodException e) { throw new MyServiceException("Some information: " + e.getMessage()); //Incorrect way }
گاهی برنامهنویسان، برای تغییر یک استثنا و معنیدار کردن آن، یک یا انواعی از استثناها را دریافت میکنند و به جای همه آنها، یک استثنای شخصیسازیشده (CusomException) پرتاب میکنند. این کار بسیار خوبی است ولی مشکل از جایی شروع میشود که مثل کد بالا استثنای جدید را تنها از روی پیغام استثنای قبلی میسازیم. این روش، اطلاعات مربوط به stack trace استثنای قبلی را از بین میبرد و کار غلطی است. کار بهتر این است که به شیوه زیر، استثنا را داخل استثنای جدید wrap کنیم. دقت کنید که در این روش، خودِ استثنای قبلی را به سازنده استثنای جدید پاس میدهیم.
catch (NoSuchMethodException e) { throw new MyServiceException("Some information: " , e); //Correct way }
زمانی که از این شیوه استفاده میکنیم، استثنای جدید، استثنای قبلی را هم در خودش دارد. همچنین میتوانیم با صدا زدن getCause روی آن، به استثنای قبلی هم دسترسی پیدا کنیم.
در مورد chained exception بیشتر بخوانید.
5- یا استثنا را لاگ بزنید و یا آن را مجدد پرتاب کنید، نه هر دو
catch (NoSuchMethodException e) { LOGGER.error("Some information", e); throw e; }
در کد بالا، هم لاگ زدن و هم پرتاب مجدد استثنا، موجب میشود از یک خطای واحد، چندین لاگ داشته باشیم و کار را برای کسی که کد ما را اشکالزدایی میکند سخت میکند.
6- هرگز اجازه ندهید از بلوک finally، استثنا پرتاب شود
try { someMethod(); //Throws exceptionOne } finally { cleanUp(); //If finally also threw any exception the exceptionOne will be lost forever }
این کد به شرطی که متد cleanUp هیچ استثنایی پرتاب نکند درست است. اگر کد اصلی (someMethod) استثنا پرتاب کرده باشد و خود cleanUp هم استثنا پرتاب کند، استثنای دوم (که در finally پرتاب شده) از متد خارج میشود و استثنای اصلی (دلیل اصلی بروز خطا) از دست میرود.
برای همین اگر کدی که داخل finally نوشتیم ممکن است استثنایی پرتاب کند، حتما آن را همان جا دریافت کنیم و اجازه ندهیم از بلوک finaly خارج شود.
همچنین دقت کنید که return کردن در بلوک finally نیز، هم مقدارِ برگشتی و هم استثنای پرتابشده از قسمت try catch را بیاثر میکند، پس هرگز از بلو ک finally چیزی return نکنیم.
7- استثناها را نادیده نگیرید
هرگز هیچ استثنایی را نادیده نگیرید حتی استثنایی که الان به نظر میرسد هیچگاه اتفاق نخواهد افتاد ولی کد، ثابت نخواهد ماند و در طول زمان تغییر خواهد کرد. ممکن است در آینده کد به گونهای عوض شود که این اتفاقی که به نظر میآید هرگز رخ نخواهد داد، واقعا رخ دهد.
public void doNotIgnoreExceptions() { try { // do something } catch (NumberFormatException e) { // this will never happen } }
حداقل کاری که الان میتوانیم انجام دهیم این است که خطا را لاگ بزنیم.
public void logAnException() { try { // do something } catch (NumberFormatException e) { log.error("This should never happen: " + e); } }
8- تنها استثناهایی را دریافت کنید که واقعا میتوانید هندل کنید
نباید هدفِ استفاده از چارچوب مدیریت استثناها را فراموش کنیم. ما استثنا را پرتاب میکنیم چون نمیتوانیم در محلِ آن، خطا را برطرف کنیم و جایی دریافت میکنیم که بتوانیم مشکل را مدیریت کنیم. این که استثنا را جایی دریافت کنیم که نمیتوانیم مدیریتش کنیم، کار اشتباهی است. باید فقظ استثناهایی را دریافت کنیم که در همین مرحله میتوانیم مدیریت کنیم.
catch (NoSuchMethodException e) { throw e; //Avoid this as it doesn't help anything }
9- اگر استثنا را هندل نمیکنید، فقط از بلوک finally استفاده کنید
بعد از بلوک try، بلوک catch یا finally یا هردو میآیند. ترکیب try/catch را زیاد دیدهایم ولی try/finally هم کاربردهای خودش را دارد. گاهی ما واقعا هنوز نمیدانیم با استثنا چه کار کنیم، صرفا لازم است یک تمیزکاری انجام دهیم، پس اصلا قسمت دریافت استثنا را نمینویسیم و به بلوک finally بسنده می کنیم.
try { someMethod(); //Method 2 } finally { cleanUp(); //do cleanup here }
10- زود پرتاب کنید، دیر دریافت کنید
این احتمالا مهمترین و معروفترین بِهروش برای مدیریت استثناهاست. این قاعده میگوید که در زودترین زمانی که میتوان متوجه خطا شد، استثنا را پرتاب کنید. این اتفاق معمولا در متدهای با سطح انتزائیسازی (abstraction) پایین رخ میدهد.
در مقابل، زمانی استثنا را دریافت میکنیم که در پشته به سطح انتزائیسازی بالایی رسیدهایم و اطلاعات کافی داریم و قادر به هندل کردن استثنا هستیم.
11- پس از هندل کردنِ استثنا، تمیزکاری کنید
اگر از منابعی مثل دیتابیس یا ارتباط شبکه استفاده میکنید، مطمئن شوید که حتی در صورت بروز استثنا هم آنها را close میکنید. در این موارد در صورتی که نخواهیم در محل، استثنا را مدیریت کنیم و هدف، بستن منابع باشد، استفاده از try/finally توصیه میشود.
12- استثنای مرتبط پرتاب کنید
فرض کنیم که متد شما قرار است با فایل کار کند، حالا اگر استثنای NullPointer پرتاب کند، برای دریافتکننده هیچ معنایی ندارد، اما اگر در عوض این NullPointer را به استثنای معنیدار و مرتبطی تبدیل کند (مثلا NoSuchFileFoundException)، معنیدارتر است و کار را برای استفادهکنندگان متد راحت میکند.
13- از استثناها به عنوان GOTO استفاده نکنید
استفاده از استثناها برای عوض کردن روند برنامه مثل استفادهی if/else، کار صحیحی نیست و کد را زشت و ناخوانا و غیرقابل فهم میکند. از استثنا تنها در جایی استفاده میکنیم که واقعا خطایی رخ داده و نمی توانیم همینجا برطرف کنیم.
public void doSomething() { try { // bunch of code throw new MyException(); // second bunch of code } catch (MyException e) { // third bunch of code } }
مطالعه بیشتر در این زمینه در این لینک
14- از Try-With-Resource استفاده کنید
گاهی برنامهنویسان به اشتباه، منابع را در انتهای بلوک try میبندند. این کد در زمانی که استثنایی اتفاق نیفتد، به خوبی کار میکند اما زمانی که خطایی پیش بیاید و بلوک try به آخر نرسد، کار آزادسازی هم انجام نمیشود.
اما اگر برنامهنویس بخواهد در بلوک finally کار بستن منابع را انجام دهد، کد مشابه و تکراری زیر در سراسر پروژه خواهد بود:
finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { LOGGER.error(e.getMessage()); } } }
با استفاده از Try-With-Resource که در جاوا ۷ اضافه شده، کافیست منابعی که احتیاج به بستهشدن دارند را در پرانتز جلوی try بنویسیم تا به صورت خودکار پس از اتمام کار try (چه موفق و چه با خطا) بسته شوند.
File file = new File("./tmp.txt"); try (FileInputStream inputStream = new FileInputStream(file);) { // use the inputStream to read a file } catch (FileNotFoundException e) { LOGGER.error(e.getMessage()); } catch (IOException e) { LOGGER.error(e.getMessage()); }
در استفاده از این روش، دیگر نیازی به بلوک finally نیست و کد خواناتر و کوتاهتر است.
منابع:
.
.
.
.
با ما همراه باشید
آدرس کانال تلگرام: JavaCupIR@
آدرس اکانت توییتر: JavaCupIR@
آدرس صفحه اینستاگرام: javacup.ir
آدرس گروه لینکدین: Iranian Java Developers