آشنایی با اکسپشن ها در جاوا
به خطاهای برنامه در جاوا اکسپشن میگیم؛ اکسپشن (exception) ها در جاوا برای اعلام باگ های برنامه مورد استفاده قرار میگیرن.
اکسپشن ها انواع مختلفی دارن که در ادامه با عملکرد و نحوه ی استفاده ازشون آشنا میشیم.
توجه
این مطلب برای کسایی که با کاتلین سروکار دارند نیز قابل مطالعه است.
در زیر یک آرایه به طول ۶ تعریف کردیم؛ برنامه از کاربر میخواد شماره ی یکی از خونه های ارایه رو وارد کنه تا برنامه مقدار اون خونه رو نمایش بده.
1 | |
2 | public static void main(String[] args){ |
3 | Scanner input = new Scanner(System.in); |
4 | |
5 | int[] randomNumbers = new int[6]; |
6 | for(int i=0; i<randomNumbers.length; i++) |
7 | randomNumbers[i] = (int) (Math.random() * 100); |
8 | |
9 | System.out.print("Enter corresponding index: ") |
10 | int index = input.nextInt(); |
11 | System.out.println("The value of corresponding index is: "+ randomNumbers[index]); |
12 | |
13 | } |
14 | } |
15 |
اگه شماره خارج از تعداد خونه های ارایه باشه برنامه دچار خطای IndexOutOfBoundsException شده و متوقف میشه.
فرض کنید میخوایم یک عدد رو به عدد دیگه تقسیم کنیم:
1 | |
2 | public static void main(String[] args){ |
3 | Scanner input = new Scanner(System.in); |
4 | |
5 | System.out.println("Enter two numbers ") |
6 | |
7 | int number0 = input.nextInt(); |
8 | int number1 = input.nextInt(); |
9 | int result = quotient(number0, number1); |
10 | System.out.println("number0 / number1 = " + result); |
11 | } |
12 | |
13 | public static int quotient(number0, number1){ |
14 | return number0 / number1; |
15 | } |
16 |
اگه در مثال بالا هنگام اجرای برنامه برای number1 عدد 0 رو وارد کنیم متد quotient یه اکسپشن میندازه بیرون، برنامه متوقف شده و پیغام ArithmeticException در کنسول نمایش داده میشه.
اگه عمل بالا رو داخل حلقه بزاریم و هر زمان به number1 مقدار 0 بدیم باز هم یه اکسپشن میوفته بیرون و با پیغام ArithmeticException از برنامه خارج میشیم.
1 | |
2 | public static void main(String[] args){ |
3 | Scanner input = new Scanner(System.in); |
4 | |
5 | do{ |
6 | System.out.println("Enter two numbers ") |
7 | ` int number0 = input.nextInt(); |
8 | int number1 = input.nextInt(); |
9 | int result = quotient(number0, number1); |
10 | System.out.println("number0 /number1 = " + result); |
11 | }while(true); |
12 | } |
13 | |
14 | public static int quotient(number0, number1){ |
15 | return number0 / number1; |
16 | } |
17 |
برای حل مشکل بالا یکی از راه حل ها اضافه کردن شرط بررسی صفر نبودن number1 است؛ این شرط از بیرون افتادن اکسپشن جلوگیری میکنه.
1 | |
2 | public static void main(String[] args){ |
3 | Scanner input = new Scanner(System.in); |
4 | boolean continueLoop = true; |
5 | |
6 | do{ |
7 | int number0 = input.nextInt(); |
8 | int number1 = input.nextInt(); |
9 | int result = number0 / number1; |
10 | System.out.println("number0 / number1 = " + result); |
11 | |
12 | }while(true); |
13 | } |
14 | |
15 | public static int quotient(number0, number1){ |
16 | //شرط صفر نبودن مخرج |
17 | if(number1 != 0) return number0 / number1; |
18 | return Integer.MIN_VALUE; |
19 | } |
20 |
حالا اگه اکسپشن بیوفته بیرون باید چکار کنیم تا برنامه متوقف نشه؟
کنترل کردن اکسپشن
با استفاده از بلوک try-catch میتونیم یک اکسپشن رو کنترل و از متوقف شدن برنامه جلو گیری کنیم.
اگه جایی از کد های برنامه یه اکسپشن بیوفته بیرون؛ میتونیم اونو داخل بلوک try-catch قرار بدیم با این کار اکسپشن رو میگیریم و برنامه ادامه پیدا میکنه.
فرم کلی
1 | |
2 | |
3 | try{ |
4 | |
5 | ... |
6 | |
7 | }catch(Exception ex){ |
8 | |
9 | ... |
10 | |
11 | } |
12 |
بلوک catch شبیه متد (تابع) عمل می کنه؛ کلاس های اکسپشن داخل پرانتز catch همون پارامتر های catch است که هنگام بیرون افتادن اکسپشن در try بلوک catch صدا زده میشه و اکسپشن انداخته شده در try توسط catch به عنوان مقدار پارامتر گرفته میشه.
اگه داخل بلوک try توسط متد یا کدی اکسپشن بیرون انداخته بشه؛ بلوک catch صدا زده میشه، اکسپشن رو میگیره و داخل بلوک میتونیم اکسپشن رو کنترل کنیم و بعد از اجرا شدن کد های داخل بلوک catch کد های بعد از بلوک به اجرا در میاد.
در مثال قبل میخوایم برنامه رو بعد از انداختن اکسپشن کنترل کنیم:
1 | |
2 | public static void main(String[] args){ |
3 | Scanner input = new Scanner(System.in); |
4 | boolean continueLoop = true; |
5 | |
6 | do{ |
7 | int number0 = input.nextInt(); |
8 | int number1 = input.nextInt(); |
9 | |
10 | try{ |
11 | |
12 | int result = number0 / number1; |
13 | System.out.println("number0 / number1 = " + result); |
14 | |
15 | }catch(ArithmeticException ex){ |
16 | System.out.println(ex.getMessage()) |
17 | } |
18 | |
19 | }while(continueLoop); |
20 | } |
21 | |
22 | public static int quotient(number0, number1){ |
23 | //شرط صفر نبودن مخرج |
24 | //if(number1 != 0) return number0 / number1; |
25 | return number0 / number1; |
26 | } |
27 |
بعد از بلوک catch یک بلوک دیگه داریم به اسم finally؛ استفاده از این بلوک اختیاریه؛ این بلوک ضمانت می کنه کد های داخلش بعد از try-catch اجرا بشه.
1 | |
2 | try{ |
3 | |
4 | ... |
5 | |
6 | }catch(Exception e){ |
7 | |
8 | ... |
9 | |
10 | }finally{ |
11 | |
12 | //اجرای کدهای داخل finally بعد از try-catch |
13 | |
14 | } |
15 | //اجرای بقیه ی کد های برنامه... |
16 |
کلیدواژه ی throw
اکسپشن ها یه کلاسن؛ هنگامی که یک آبجکت از این کلاس ها با کلیدواژه ی throw ایجاد میشه برنامه رو از اجرای عادی خارج کرده و اصطلاحا میگیم برنامه دچار اکسپشن شده.
با استفاده از کلیدواژه ی throw در جاوا می تونیم یک اکسپشن داخل متد بندازیم.
1 | |
2 | public void myMethod(){ |
3 | throw new ArithmeticException(); |
4 | } |
5 |
در مثال quotient (خارج قسمت) میخوایم در صورت برقرار بودن شرط، یک اکسپشن داخل متد quotient بندازیم.
1 | |
2 | public static void main(String[] args){ |
3 | Scanner input = new Scanner(System.in); |
4 | boolean continueLoop = true; |
5 | |
6 | do{ |
7 | int number0 = input.nextInt(); |
8 | int number1 = input.nextInt(); |
9 | |
10 | try{ |
11 | |
12 | int result = number0 / number1; |
13 | System.out.println("number0 / number1 = " + result); |
14 | |
15 | }catch(ArithmeticException ex){ |
16 | System.out.println(ex.getMessage()) |
17 | } |
18 | |
19 | }while(continueLoop); |
20 | } |
21 | |
22 | public static int quotient(number0, number1){ |
23 | |
24 | if(number1 == 0) throw new ArithmeticException("Can not divide by Zero"); |
25 | |
26 | return number0 / number1; |
27 | } |
28 |
در بالا پارامتر کنسراکتور کلاس ArithmeticException رو با "Can not divide by Zero" مقدار دهی کردیم.
توجه:
در صورت امکان بهتره بهجای کنترل کردن اکسپشن، با استفاده از if جلوی انداختنشو بگیریم؛ این کار، برنامه رو بهینه تر میکنه چون هر اکسپشن یک کلاس است و با هربار انداختنش، یک آبجکت جدید ایجاد میشه.
بررسی کلاس های اکسپشن
شکل زیر مراتب ارث بری کلاس های اکسپشن رو نشون میده:

همانطور که در شکل مشخصه تمام کلاس های اکسپشن ساب کلاس Exception هستند و کلاس Exception نیز ساب کلاس Throwable است.
تمام اکسپشن ها حداقل دوتا کانستراکتور دارن یکی کانستراکتور بدون پارامتر و دیگری کانستراکتور با پارامتر از نوع String که هنگام ایجاد نمونه ی جدید پیغام خطا رو نشون میده.
به طور کلی دو نوع کلاس اکسپشن داریم:
1- کلاس هایی که ساب کلاس Error هستند:
این اکسپشن ها به خطا های JVM مربوط میشن و کم پیش میاد با این اکسپشن ها مواجه بشیم.
2- کلاس هایی که ساب کلاس Exception هستند:
این کلاس ها به طور کلی به ارور های برنامه، تعامل برنامه با ماشین (مثل خوندن فایل ها، کامپایل کردن کلاس های جاوا، عملیات ریاضی و...) مربوط میشن.
ساب کلاس های کلاس Exception از نظر نوع اکسپشن به دو دسته ی کلی تقسیم میشن:
1-اکسپشن های checked
توجه
در کاتلین چیزی به اسم checked-exception نداریم و تمام اکسپشن ها unchecked هستند.
اگه نوع اکسپشن checked-exception باشه باید قبل از کامپایل؛ متد رو داخل بلوک try-catch قرار بدیم.
این نوع اکسپشن ها قبل از اجرای برنامه توسط کامپایلر چک میشن.
2-اکسپشن های unchecked
کلاس های unchecked-exception توسط کامپایلر قابل شناسایی نیستند و زمان اجرای برنامه در صورت بروز خطا اجرا میشن.
بر خلاف دسته ی اول، این کلاس ها به صورت آشکار نیازی به بلوک try-catch ندارن مگه اینکه خودمون از قبل بدونیم خطایی در کار است.
ساب کلاس های RuntimeException در این دسته قرار دارن.
تعریف کلاس اختصاصی اکسپشن
میتونیم مانند سایر کلاس ها برای برنامه ی خود یک کلاس اکسپشن اختصاصی تعریف کنیم.
بزارید با یک مثال به کارمون در این قسمت خاتمه بدیم
میخوایم یک کلاس اکسپشن برای دایره تعریف کنیم، در صورت منفی بودن شعاع دایره برنامه دچار خطا بشه و پیغام خطا بده
1 | |
2 | public class RadiusException extends IllegalArgumentException { |
3 | private final double radius; |
4 | |
5 | public RadiusException(double radius){ |
6 | super("Radius is " + radius + " and it can not be negative"); |
7 | this.radius = radius; |
8 | } |
9 | |
10 | public double getRadius() { |
11 | return radius; |
12 | } |
13 | } |
14 |
انداختن اکسپشن تعریف شده داخل متد و کنستراکتور های کلاس دایره:
1 | |
2 | public class CircleWithRadiusException { |
3 | private double radius; |
4 | |
5 | |
6 | public CircleWithRadiusException(double radius) { |
7 | if (radius < 0) throw new RadiusException(radius); |
8 | this.radius = radius; |
9 | } |
10 | |
11 | public CircleWithRadiusException() { |
12 | this(1.0); |
13 | } |
14 | |
15 | |
16 | public void setRadius(double radius) { |
17 | if (radius < 0) throw new RadiusException(radius); |
18 | this.radius = radius; |
19 | } |
20 | |
21 | public double getRadius() { |
22 | return radius; |
23 | } |
24 | |
25 | public double getArea() { |
26 | return radius * radius * Math.PI; |
27 | } |
28 | |
29 | public double getPerimeter() { |
30 | return 2 * radius * Math.PI; |
31 | } |
32 | |
33 | } |
34 |
استفاده از کلاس دایره در متد main:
1 | |
2 | CircleWithRadiusException c0 = new CircleWithRadiusException(23); |
3 | System.out.println("Radius of circle c0: " + c0.getRadius()); |
4 | System.out.println("Area of circle c0: " + c0.getArea()); |
5 | System.out.println("Perimeter of circle c0: " + c0.getPerimeter()); |
6 | |
7 | System.out.println(); |
8 | |
9 | //اختصاص دادن عمدی مقدار منفی به کانستراکتور کلاس دایره برای بیرون انداختن اکسپشن |
10 | CircleWithRadiusException c1 = new CircleWithRadiusException(-2); |
11 | System.out.println("Radius of circle c1: " + c1.getRadius()); |
12 | System.out.println("Area of circle c1: " + c1.getArea()); |
13 | System.out.println("Perimeter of circle c1: " + c1.getPerimeter()); |
14 |
استفاده از کلیدواژه ی throws
از کلیدواژه ی throws معمولا زمانی که نوع اکسپشن checked است استفاده میشه.
گفتیم هنگامی که نوع اکسپشن checked است مجبوریم قبل از کامپایل اکسپشن رو با try-catch بگیریم.
گاهی به هر دلیلی نمیخوایم از try-catch استفاده کنیم مثلا کد های برنامه زیاد و گیج کنندس یا متد یک متد نهایی برای اجرا نیست.
در این مواقع بهجای استفاده از try-catch میتونیم با استفاده از کلید واژه ی throws اکسپشن رو بندازیم جایی که قراره متد رو صدا بزنیم.
1 | |
2 | public void firstMethod() throws IOException { |
3 | FileInputStream fis = new FileInputStream("PATH"); |
4 | ... |
5 | } |
6 | |
7 | public void secondMethod(){ |
8 | try{ |
9 | firstMethod(); |
10 | }catch(IOException ioe){ |
11 | System.out.println(ioe.getMessage()); |
12 | } |
13 | } |
14 |
میتونیم چندتا اکسپشن رو با throws بندازیم به متد بعدی
1 | |
2 | public void readData() throws Exception0, Exception1, ... , ExceptionN{ |
3 | ... |
4 | } |
5 |
توجه:
در ارث بری؛ اگه متد سوپر کلاس از throws استفاده نکرده باشه نمی تونیم در ساب کلاس برای استفاده از throws متد رو بازنویسی کنیم.
در کاتلین checked-exception نداریم بنابراین این کلیدواژه وجود نداره و نیازی بهش نداریم اما Throws در کاتلین برای پر کردن جای خالیش وجود داره.
1 | |
2 | @Throws(IOException::class) |
3 | fun readData(){ |
4 | val fis = FileInputException("PathToFile") |
5 | ... |
6 | } |
7 |
خلاصه
- تو جاوا به باگ های برنامه اکسپشن میگیم.
- کلاس های اکسپشن، برنامه رو از اجرای عادی خارج میکنن.
- به اجرای اکسپشن داخل متد، انداختن اکسپشن میگیم.
- با استفاده از کلیدواژه ی throw میتونیم یک اکسپشن رو داخل یک متد بندازیم.
- به طور کلی دو نوع اکسپشن داریم اکسپشن هایی که ساب کلاس Error هستند و اکسپشن هایی که ساب کلاس Exception هستند.
- ساب کلاس های کلاس Exception نیز به دو دسته ی checked-exception و unchecked-exception تقسیم میشن.
- تمام ساب کلاس های RuntimeException از نوع unchecked-exception هستند.
- اگه داخل متد checked-exception انداخته باشیم میتونیم با استفاده از کلیدواژه ی throws اکسپشن رو بندازیم جایی که متد صدا زده میشه.