اکسپشن (exception) ها در جاوا و کاتلین

اکسپشن (exception) ها در جاوا و کاتلین

آشنایی با اکسپشن

به خطاهای برنامه اکسپشن میگیم؛ اکسپشن (exception) ها در جاوا و کاتلین برای اعلام باگ های برنامه مورد استفاده قرار میگیرن.

اکسپشن ها انواع مختلفی دارن که در ادامه با عملکرد و نحوه ی استفاده ازشون آشنا میشیم.

توجه

این مطلب برای کسایی که با کاتلین سروکار دارند نیز قابل مطالعه است.

در زیر یک آرایه به طول ۶ تعریف کردیم؛ برنامه از کاربر میخواد شماره ی یکی از خونه های ارایه رو وارد کنه تا برنامه مقدار اون خونه رو نمایش بده. اگه شماره خارج از تعداد خونه های ارایه باشه برنامه دچار IndexOutOfBoundsException شده و متوقف میشه.

public static void main(String[] args){
    Scanner input = new Scanner(System.in);

    int[] randomNumbers = new int[6];
    for(int i=0; i<randomNumbers.length; i++)
        randomNumbers[i] = (int) (Math.random() * 100);

            System.out.print("Enter corresponding index: ")
            int index = input.nextInt();
            System.out.println("The value of corresponding index is: "+ randomNumbers[index]);

    }
}
                    
fun main() {
    val randomNumbers = Array<Int>(6){
        (0 until 100).random()
    }

    val input = Scanner(System.`in`)

    println("Enter corresponding index: ")
    val index = input.nextInt()

    println("The value of corresponding index is ${randomNumbers[index]}")
}
        

فرض کنید میخوایم یک عدد رو به عدد دیگه تقسیم کنیم:

public static void main(String[] args){
    Scanner input = new Scanner(System.in);

    System.out.println("Enter two numbers ")

    int number0 = input.nextInt();
    int number1 = input.nextInt();
    int result = quotient(number0, number1);
    System.out.println("number0 / number1 = " + result);
}

public static int quotient(number0, number1){
    return number0 / number1;
}
        
fun main() {
    val input = Scanner(System.`in`)

    println("Enter two numbers ")

    val number0 = input.nextInt()
    val number1 = input.nextInt()
    val result = quotient(number0, number1)

    println("$number0 / $number1 = $result")
}

fun quotient(number0: Int, number1: Int): Int{
    return number0 / number1
}
        

اگه در مثال بالا هنگام اجرای برنامه برای number1 عدد 0 رو وارد کنیم متد quotient دچار ArithmeticException شده و پیغامیش در کنسول نمایش داده میشه.

اگه عمل بالا رو داخل حلقه بزاریم و هر زمان به number1 مقدار 0 بدیم باز هم دچار ArithmeticException شده و باز هم برنامه متوقف میشه.

public static void main(String[] args){
Scanner input = new Scanner(System.in);

    do{
        System.out.println("Enter two numbers ")
`       int number0 = input.nextInt();
        int number1 = input.nextInt();
        int result = quotient(number0, number1);
        System.out.println("number0 /number1 = " + result);
    }while(true);
}

public static int quotient(number0, number1){
    return number0 / number1;
}
        
fun main() {

    val input = Scanner(System.`in`)

    do{
        println("Enter two numbers ")

        val number0 = input.nextInt()
        val number1 = input.nextInt()
        val result = quotient(number0, number1)

        println("$number0 / $number1 = $result")

    }while(true)
}

fun quotient(number0: Int, number1: Int): Int{
    return number0 / number1
}
        

برای حل مشکل بالا یکی از راه حل ها اضافه کردن شرط بررسی صفر نبودن number1 است؛ این شرط از وقوع اکسپشن جلوگیری میکنه.

public static void main(String[] args){
    Scanner input = new Scanner(System.in);
    boolean continueLoop = true;

    do{
        int number0 = input.nextInt();
        int number1 = input.nextInt();
        int result = number0 / number1;
        System.out.println("number0 / number1 = " + result);

    }while(true);
}

public static int quotient(number0, number1){
    //شرط صفر نبودن مخرج
    if(number1 != 0) return number0 / number1;
    return Integer.MIN_VALUE;
}
        
fun main() {

    val input = Scanner(System.`in`)

    do{
        println("Enter two numbers ")

        val number0 = input.nextInt()
        val number1 = input.nextInt()
        val result = quotient(number0, number1)

        println("$number0 / $number1 = $result")

    }while(true)
}

fun quotient(number0: Int, number1: Int): Int{
    //شرط صفر نبودن مخرج
    if(number1 != 0) return number0 / number1;
    return Int.MIN_VALUE
}
        

حالا اگه خواسته یا ناخواسته اکسپشن رخ بده، باید چکار کنیم تا برنامه متوقف نشه؟

کنترل کردن اکسپشن

با استفاده از بلوک try-catch میتونیم یک اکسپشن رو کنترل و از متوقف شدن برنامه جلو گیری کنیم.

اگه بدونیم جایی در کد های برنامه اکسپشن قراره رخ بده، میتونیم اونو داخل بلوک try-catch قرار بدیم؛ با این کار اکسپشن رو میگیریم و برنامه ادامه پیدا میکنه.

فرم کلی


try{

    ...

}catch(Exception ex){

    ...

}
                        

بلوک catch شبیه متد (تابع) عمل می کنه؛ کلاس های اکسپشن داخل پرانتز catch درواقع پارامتر های catch هستند، که هنگام رخ دادن اکسپشن در try بلوک catch صدا زده میشه و اکسپشن رخ داده در try توسط catch به عنوان مقدار پارامتر گرفته میشه.

اگه داخل بلوک try توسط متد یا کدی اکسپشن رخ بده، بلوک catch صدا زده میشه، اکسپشن رو میگیره و داخل بلوک میتونیم اکسپشن رو کنترل کنیم و بعد از اجرا شدن کد های داخل بلوک catch کد های بعد از اجرا شده و برنامه به کار خودش ادامه میده.

در مثال قبل میخوایم برنامه رو بعد از بیرون افتادن اکسپشن در catch کنترل کنیم.

public static void main(String[] args){
    Scanner input = new Scanner(System.in);
    boolean continueLoop = true;

    do{
        int number0 = input.nextInt();
        int number1 = input.nextInt();

        try{

            int result = number0 / number1;
            System.out.println("number0 / number1 = " + result);

        }catch(ArithmeticException ex){
            System.out.println(ex.getMessage())
        }

    }while(continueLoop);
}

public static int quotient(number0, number1){
        //شرط صفر نبودن مخرج
        //if(number1 != 0) return number0 / number1;
        return number0 / number1;
}
                        
fun main() {

    val input = Scanner(System.`in`)

    do{
        println("Enter two numbers ")

        val number0 = input.nextInt()
        val number1 = input.nextInt()
        try{
            val result = quotient(number0, number1)

            println("$number0 / $number1 = $result")
        }catch(e: ArithmeticException){
            println(e.message);
        }
    }while(true)
}

fun quotient(number0: Int, number1: Int): Int{
    //شرط صفر نبودن مخرج
    if(number1 != 0) return number0 / number1;
    return Int.MIN_VALUE
}
                        

بعد از بلوک catch یک بلوک دیگه داریم به اسم finally؛ استفاده از این بلوک اختیاریه؛ این بلوک ضمانت می کنه کد های داخلش بعد از try-catch اجرا بشه.

try{

    ...

}catch(Exception e){

    ...

}finally{

    //اجرای کدهای داخل finally بعد از try-catch

}
    //اجرای بقیه ی کد های برنامه...
                        
try{

...

}catch(e: Exception){

...

}finally{

//اجرای کدهای داخل finally بعد از try-catch

}

//اجرای بقیه ی کد های برنامه...
                        

ایجاد اکسپشن با کلیدواژه ی throw

اکسپشن ها یه کلاسن؛ هنگامی که یک آبجکت از این کلاس ها با کلیدواژه ی throw ایجاد میشه برنامه رو از اجرای عادی خارج کرده و اصطلاحا میگیم برنامه دچار اکسپشن شده.

با استفاده از کلیدواژه ی throw در جاوا می تونیم یک اکسپشن داخل متد بندازیم.

public void myMethod(){
    throw new ArithmeticException();
}
                        
fun myMethod(){
    throw ArithmeticException()
}
                        

در مثال quotient (خارج قسمت) میخوایم در صورت برقرار بودن شرط، یک اکسپشن داخل متد quotient ایجاد کنیم.

public static void main(String[] args){
    Scanner input = new Scanner(System.in);
     boolean continueLoop = true;

     do{
        int number0 = input.nextInt();
        int number1 = input.nextInt();

        try{

            int result = number0 / number1;
            System.out.println("number0 / number1 = " + result);

        }catch(ArithmeticException ex){
            System.out.println(ex.getMessage())
        }

    }while(continueLoop);
}

public static int quotient(number0, number1){

    if(number1 == 0) throw new ArithmeticException("Can not divide by Zero");

    return number0 / number1;
}
                        
fun main() {

val input = Scanner(System.`in`)

    do{
        println("Enter two numbers ")

        val number0 = input.nextInt()
        val number1 = input.nextInt()
        try{
            val result = quotient(number0, number1)
            println("$number0 / $number1 = $result")
        }catch(e: ArithmeticException){
            println(e.message);
        }
    }while(true)
}

fun quotient(number0: Int, number1: Int): Int{
    //شرط صفر نبودن مخرج
    if(number1 == 0) throw ArithmeticException("Can not divide by Zero")
    return Int.MIN_VALUE
}
                        

در بالا پارامتر کنسراکتور کلاس ArithmeticException رو با "Can not divide by Zero" مقدار دهی کردیم.

راهنمایی

در صورت امکان بهتره به‌جای کنترل کردن اکسپشن، با استفاده از if جلوی انداختنشو بگیریم؛ این کار، برنامه رو بهینه تر میکنه چون هر اکسپشن یک کلاس است و با هربار انداختنش، یک آبجکت جدید ایجاد میشه.

کلاس های اکسپشن

شکل زیر مراتب ارث بری کلاس های اکسپشن رو نشون میده این کلاس ها در جاوا تعریف شده اما برای کاتلین هم قابل استفاده هستند:

سلسله مراتب ارث بری کلاس های اکسپشن در جاوا
سلسله مراتب ارث بری کلاس های اکسپشن در جاوا

همانطور که در شکل مشخصه تمام کلاس های اکسپشن ساب کلاس Exception هستند و کلاس Exception نیز ساب کلاس Throwable است.

با توجه به تصویر، خطاهای یک برنامه در جاوا به ارور ها و اکسپشن ها تقسیم میشن.

ارور ها

کلاس Error سوپرکلاس تمام این کلاس هاست، این خطاها مربوط به JVM هستند و در پروژه کمتر باهاشون مواجه میشیم.

اکسپشن ها

این کلاس ها به طور کلی به ارور های برنامه، تعامل برنامه با ماشین (مثل خوندن فایل ها، کامپایل کردن کلاس های جاوا، عملیات ریاضی و...) مربوط میشن.

تمام اکسپشن ها حداقل دوتا کانستراکتور دارن یکی کانستراکتور بدون پارامتر و دیگری کانستراکتور با پارامتر از نوع String که هنگام ایجاد نمونه ی جدید پیغام خطا رو نشون میده.

ساب کلاس های Exception از نظر نوع اکسپشن به دو دسته ی checked و unchecked تقسیم میشن.

اکسپشن های checked

اگه نوع اکسپشن checked-exception باشه باید قبل از کامپایل؛ متد رو داخل بلوک try-catch قرار بدیم.

این نوع اکسپشن ها قبل از اجرای برنامه توسط کامپایلر چک میشن.

توجه

در کاتلین چیزی به اسم checked-exception نداریم و تمام اکسپشن ها unchecked هستند.

اکسپشن های unchecked

کلاس های unchecked-exception توسط کامپایلر قابل شناسایی نیستند و زمان اجرای برنامه در صورت بروز خطا اجرا میشن.

بر خلاف دسته ی اول، این کلاس ها به صورت آشکار نیازی به بلوک try-catch ندارن مگه اینکه خودمون از قبل بدونیم خطایی در کار است.

ساب کلاس های RuntimeException در این دسته قرار دارن.

تعریف کلاس اختصاصی اکسپشن

میتونیم مانند سایر کلاس ها برای برنامه ی خود یک کلاس اکسپشن اختصاصی تعریف کنیم.

بزارید با یک مثال به کارمون در این قسمت خاتمه بدیم

میخوایم یک کلاس اکسپشن برای دایره تعریف کنیم، در صورت منفی بودن شعاع دایره برنامه دچار خطا بشه و پیغام خطا بده.

public class NegativeRadiusException extends IllegalArgumentException {
    private final double radius;

    public NegativeRadiusException(double radius){
        super("Radius is " + radius + " and it can not be negative");
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }
}
                        
class NegativeRadiusException(var radius: Double): IllegalArgumentException("Radius is $radius and it can not be Negative")
                        

انداختن اکسپشن تعریف شده داخل متد و کنستراکتور های کلاس دایره:

public class Circle {
    private double radius;

    public Circle(double radius) {
        if (radius < 0) throw new NegativeRadiusException(radius);
        this.radius = radius;
    }

    public Circle() {
        this(1.0);
    }

    public void setRadius(double radius) {
        if (radius < 0) throw new NegativeRadiusException(radius);
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    public double getArea() {
         return radius * radius * Math.PI;
    }

    public double getPerimeter() {
        return 2 * radius * Math.PI;
    }

}
                        
class Circle {
    var radius: Double = 0.0
        set(value) {
            if (value < 0) throw NegativeRadiusException(value)
            field = value
        }

    constructor(radius: Double) {
        if (radius < 0) throw NegativeRadiusException(radius)
    }

    constructor() : this(radius = 1.0)

    fun getArea(): Double {
        return PI * radius * radius
    }

    fun getPerimeter(): Double {
        return 2 * PI * radius
    }
}
                        

استفاده از کلاس دایره در متد main:

Circle c0  = new Circle(23);
System.out.println("Radius of circle c0: " + c0.getRadius());
System.out.println("Area of circle c0: " + c0.getArea());
System.out.println("Perimeter of circle c0: " + c0.getPerimeter());

System.out.println();

//اختصاص دادن عمدی مقدار منفی به کانستراکتور کلاس دایره برای بیرون انداختن اکسپشن
Circle c1 = new Circle(-2);
System.out.println("Radius of circle c1: " + c1.getRadius());
System.out.println("Area of circle c1: " + c1.getArea());
System.out.println("Perimeter of circle c1: " + c1.getPerimeter());
                        
val c0  = Circle(23);
println("Radius of circle c1: ${c0.getRadius()}");
println("Area of circle c1: ${c0.getArea()}" + );
println("Perimeter of circle c0: ${c0.getPerimeter()}");

println();

//اختصاص دادن عمدی مقدار منفی به کانستراکتور کلاس دایره برای بیرون انداختن اکسپشن
val c1 = new Circle(-2);
println("Radius of circle c1: ${c1.getRadius()}");
println("Area of circle c1: ${c1.getArea()}" + );
println("Perimeter of circle c1: ${c1.getPerimeter()}");
        

استفاده از کلیدواژه ی throws

از کلیدواژه ی throws معمولا زمانی که نوع اکسپشن checked است استفاده میشه.

گفتیم هنگامی که نوع اکسپشن checked است مجبوریم قبل از کامپایل اکسپشن رو با try-catch بگیریم.

گاهی به هر دلیلی نمیخوایم از try-catch استفاده کنیم مثلا کد های برنامه زیاد و گیج کنندس یا متد یک متد نهایی برای اجرا نیست.

در این مواقع به‌جای استفاده از try-catch میتونیم با استفاده از کلید واژه ی throws اکسپشن رو بندازیم جایی که قراره متد رو صدا بزنیم.

public void firstMethod() throws IOException {
    FileInputStream fis = new FileInputStream("PATH");
        ...
}

public void secondMethod(){
    try{
        firstMethod();
    }catch(IOException ioe){
        System.out.println(ioe.getMessage());
    }
}
                        

میتونیم چندتا اکسپشن رو با throws بندازیم به متد بعدی

public void readData() throws Exception0, Exception1, ... , ExceptionN{
    ...
}
                        

توجه:

در ارث بری؛ اگه متد سوپر کلاس از throws استفاده نکرده باشه نمی تونیم در ساب کلاس برای استفاده از throws متد رو بازنویسی کنیم.

در کاتلین checked-exception نداریم بنابراین این کلیدواژه وجود نداره و نیازی بهش نداریم اما Throws در کاتلین برای پر کردن جای خالیش وجود داره.

@Throws(IOException::class)
fun readData(){
    val fis = FileInputException("PathToFile")
    ...
}
                        

خلاصه

  • تو جاوا به باگ های برنامه اکسپشن میگیم.
  • کلاس های اکسپشن، برنامه رو از اجرای عادی خارج میکنن.
  • با استفاده از کلیدواژه ی throw میتونیم یک اکسپشن رو داخل یک متد ایجاد کنیم.
  • به طور کلی در جاوا دو نوع خطا داریم؛ خطاهایی که ساب کلاس Error هستند و خطاهایی که ساب کلاس Exception هستند.
  • ساب کلاس های کلاس Exception نیز به دو دسته ی checked-exception و unchecked-exception تقسیم میشن.
  • تمام ساب کلاس های RuntimeException از نوع unchecked-exception هستند.
  • اگه داخل متد checked-exception انداخته باشیم میتونیم با استفاده از کلیدواژه ی throws اکسپشن رو بندازیم جایی که متد صدا زده میشه.

برای اطلاع از جدیدترین مطالب یا پرسش و پاسخ عضو کانال و گروه تلگرامی ما شوید.

arrow_drop_up
کپی شد!