Overflow و Underflow در جاوا — راهنمای کاربردی

۶۹۰ بازدید
آخرین به‌روزرسانی: ۲۱ خرداد ۱۴۰۳
زمان مطالعه: ۷ دقیقه
Overflow و Underflow در جاوا — راهنمای کاربردی

در این راهنما به بررسی Overflow و Underflow در جاوا و انواع داده عددی آن می‌پردازیم. در این مقاله قصد نداریم وارد ابعاد دقیق تئوریک شویم و صرفاً روی مواردی که این حالت‌ها رخ می‌دهند تمرکز خواهیم کرد. ابتدا انواع عددی صحیح را بررسی می‌کنیم و سپس انواع داده اعشاری را مورد بررسی قرار می‌دهیم. در هر دو مورد به بررسی شیوه تعیین این که Overflow یا Underflow رخ داده است نیز می‌پردازیم.

997696

Overflow و Underflow در جاوا

به بیان ساده Overflow (سرریز) و Underflow (پاریز) در زمان‌هایی رخ می‌دهند که یک مقداری خارج از بازه اعلان‌شده برای نوع داده مورد نظر به متغیر انتساب دهیم. اگر مقدار (مطلق) بسیار بزرگ باشد، آن را Overflow می‌نامیم، اما اگر مقدار مورد نظر کوچک باشد، حالتی که پیش می‌آید را Underflow می‌نامیم.

در ادامه مثالی را بررسی می‌کنیم و در آن تلاش می‌کنیم مقدار 10100010^{1000} یعنی عدد یک با 1000 صفر را به متغیری از نوع int یا double انتساب دهیم. این مقدار برای یک نوع داده int یا double در جاوا بسیار بزرگ است و بدیهی است که حالت Overflow رخ می‌دهد.

در مثال دوم فرض کنید تلاش می‌کنیم عدد 101000-10^{1000} که عددی بسیار کوچک و نزدیک به صفر است را به متغیری از نوع داده double انتساب دهیم. این مقدار برای متغیر double در جاوا بسیار کوچک است و پدیده Underflow رخ می‌دهد. در بخش بعدی به بررسی دقیق‌تر اتفاقی که در این حالت‌ها رخ می‌دهد خواهیم پرداخت.

انواع داده صحیح

انواع داده صحیح (Integer) در جاوا شامل Byte (8 بیت)، Short (16 بیت)، int (32 بیت) و Long (64 بیت) می‌شود. در این بخش روی نوع داده int تمرکز می‌کنیم. همین رفتار در مورد انواع داده دیگر نیز صدق می‌کند، به جز این که مقادیر بیشینه و کمینه آن‌ها متفاوت است.

نوع داده صحیح به صورت int می‌تواند مقادیر مثبت یا منفی داشته باشد که معنی آن این است که با توجه به مقدار 32 بیتی آن، عددی بین 231-2^{31} معادل 2147483648- تا 23112^{31} -1 معادل 2147483647 خواهد بود.

کلاس پوشششی Integer دو ثابت تعریف می‌کند که این مقادیر را نگهداری می‌کند. این مقادیر Integer.MIN_VALUE و Integer.MAX_VALUE نام دارند.

مثال

شاید از خود بپرسید اگر متغیری به نام m از نوع int تعریف کنیم و تلاش کنیم یک مقدار خیلی بزرگ مانند 21474836478 یعنی MAX_VALUE +1 به آن انتساب دهیم چه اتفاقی رخ می‌دهد؟

یکی از خروجی‌های احتمالی این انتساب آن است که مقدار m تعریف‌ نشده باشد و یا این که خطایی ایجاد شود. هر دو این خروجی‌ها ممکن هستند. با این حال در جاوا مقدار m برابر با 2147483648- یعنی کمینه ممکن برای آن نوع داده خواهد شد. از سوی دیگر اگر تلاش کنیم مقدار 2147483649 یعنی MIN_VALUE -1 را به m انتساب دهیم، مقدار آن به صورت 2147483647 (مقدار بیشینه) تغییر می‌یابد. این رفتار به صورت integer-wraparound نامیده می‌شود.

در قطعه کد زیر این رفتار بهتر نمایش یافته است:

1int value = Integer.MAX_VALUE-1;
2for(int i = 0; i < 4; i++, value++) {
3    System.out.println(value);
4}

بدین ترتیب خروجی زیر به دست می‌آید که نشانگر وقوع سرریز است:

2147483646

2147483647

-2147483648

-2147483647

مدیریت Underflow و Overflow برای انواع داده صحیح

زبان جاوا در موارد وقوع Overflow هیچ استثنایی صادر نمی‌کند و از این رو یافتن خطاهایی که منجر به بروز سرریز می‌شوند، کار دشواری است. همچنین امکان دسترسی مستقیم به فلگ Overflow که در اغلب CPU-ها فراهم شده است، وجود ندارد. با این حال روش‌های متفاوتی برای مدیریت موارد احتمالی Overflow وجود دارند. در ادامه برخی از این روش‌های ممکن را بررسی می‌کنیم.

استفاده از نوع داده متفاوت

اگر بخواهیم استفاده از مقادیر بزرگ‌تر از 2147483647 یا کوچک‌تر از 2147483648- نیز ممکن باشد، می‌توانیم این مقادیر را در نوع داده long یا BigInteger قرار دهیم. با این که متغیرهای از نوع long نیز می‌توانند سرریز شوند، اما مقادیر  کمینه و بیشینه آن‌ها بسیار بزرگ‌تر از مقادیر مورد نیاز برای اغلب محاسبات است. بازه مقادیر BigInteger محدودیتی به جز حافظه ارائه شده برای JVM ندارد. در ادامه روش بازنویسی مثال قبلی را با نوع BigInteger می‌بینید:

1BigInteger largeValue = new BigInteger(Integer.MAX_VALUE + "");
2for(int i = 0; i < 4; i++) {
3    System.out.println(largeValue);
4    largeValue = largeValue.add(BigInteger.ONE);
5}

بدین ترتیب خروجی زیر به دست می‌آید:

2147483647
2147483648
2147483649
2147483650

چنان که در خروجی می‌بینید، هیچ سرریزی رخ نداده است.

صدور یک استثنا

موقعیت‌هایی وجود دارند که نمی‌خواهیم از مقادیر بزرگ‌تر استفاده کنیم و همچنین نمی‌خواهیم یک سرریز رخ دهد؛ بلکه می‌خواهیم به جای آن یک استثنا صادر شود. از نسخه 8 جاوا به بعد می‌توانیم از متدهایی برای عملیات حسابی دقیق (Exact) استفاده کنیم. ابتدا به مثال زیر توجه کنید:

1int value = Integer.MAX_VALUE-1;
2for(int i = 0; i < 4; i++) {
3    System.out.println(value);
4    value = Math.addExact(value, 1);
5}

متد استاتیک ()addExact یک عمل جمع معمولی اجرا می‌کند، اما در صورتی که نتیجه عملیات یک سرریز یا پاریز باشد، استثنایی صادر خواهد کرد:

12147483646
22147483647
3Exception in thread "main" java.lang.ArithmeticException: integer overflow
4    at java.lang.Math.addExact(Math.java:790)
5    at baeldung.underoverflow.OverUnderflow.main(OverUnderflow.java:115)

علاوه بر ()addExact، پکیج Math در جاوا 8 متدهای دقیق متناظر با همه عملیات حسابی دیگر را نیز عرضه کرده است برای دیدن لیست همه این متدها به این صفحه (+) مراجعه کنید.

به علاوه متدهای تبدیل دقیق وجود دارند که در صورت وجود یک overflow در طی تبدیل به نوع داده دیگر، استثنایی صادر می‌کنند.

برای نمونه هنگام تبدیل از نوع long به int:

1public static int toIntExact(long a)

و برای تبدیل از BigInteger به int یا long می‌توان از روش زیر استفاده کرد:

1BigInteger largeValue = BigInteger.TEN;
2long longValue = largeValue.longValueExact();
3int intValue = largeValue.intValueExact();

پیش از جاوا 8

متدهای حسابی دقیق به جاوا 8 اضافه شده‌اند. اگر از نسخه‌های قبلی جاوا استفاده می‌کنید، می‌توانید این متدها را خودتان ایجاد کنید. یک گزینه برای انجام این کار، پیاده‌سازی همان متد جاوا 8 است:

1public static int addExact(int x, int y) {
2    int r = x + y;
3    if (((x ^ r) & (y ^ r)) < 0) {
4        throw new ArithmeticException("int overflow");
5    }
6    return r;
7}

انواع داده غیر صحیح

انواع داده غیر صحیح float و double در زمان اجرای عملیات حسابی به روش یکسانی مانند نوع داده صحیح عمل نمی‌کنند. یک تفاوت آن است که انواع عملیات حسابی روی اعداد اعشاری می‌تواند موجب بروز NaN شود. به علاوه متد دقیق حسابی مانند addExact یا multiplyExact که برای اعداد صحیح در پکیج Math ارائه شده است برای انواع اعشاری وجود ندارد.

جاوا از استاندارد IEEE (شماره 754) برای عملیات حسابی اعشاری در انواع داده float و double پیروی می‌کند. این استاندارد پایه‌ای برای روش مدیریت سرریز و پاریز در اعداد اعشاری نیز محسوب می‌شود. در بخش‌های بعدی بر روی سرریز و پاریز نوع داده double و شیوه مدیریت مواردی که این حالت رخ می‌دهد تمرکز خواهیم کرد.

Overflow

در مورد انواع داده صحیح می‌توانیم انتظار داشته باشیم که حالت زیر رخ بدهد:

1assertTrue(Double.MAX_VALUE + 1 == Double.MIN_VALUE);

با این حال در مورد متغیرهای اعشاری این حالت رخ نمی‌دهد و حالت زیر مصداق دارد:

1assertTrue(Double.MAX_VALUE + 1 == Double.MAX_VALUE);

دلیل این حالت آن است که مقدار double تنها شامل تعداد محدودی بیت‌های معنی‌دار است. اگر مقدار یک متغیر double را تنها یک واحد افزایش دهیم، هیچ کدام از بیت‌های معنی‌دار تغییر نمی‌یابند. از این رو مقدار بدون تغییر می‌ماند.

اگر مقدار یک متغیر را طوری افزایش دهیم که یکی از بیت‌های معنی‌دار متغیر افزایش یابند، آن متغیر مقدار بی‌نهایت (INFINITY) پیدا می‌کند:

1assertTrue(Double.MAX_VALUE * 2 == Double.POSITIVE_INFINITY);

در مورد اعداد منفی نیز مقدار منفی بی‌نهایت (NEGATIVE_INFINITY) به دست می‌آید:

1assertTrue(Double.MAX_VALUE * -2 == Double.NEGATIVE_INFINITY);

چنان که می‌بینیم، برخلاف اعداد صحیح، هیچ کلاس پوششی وجود ندارد، اما دو خروجی متفاوت برای overflow محتمل است که یکی بدون تغییر ماندن مقدار و دیگری رسیدن به یکی از مقادیر خاص POSITIVE_INFINITY یا NEGATIVE_INFINITY است.

Underflow

دو ثابت برای مقدار کمینه یک مقدار double تعریف شده است که شامل MIN_VALUE (با مقدار 4.9e-324) و MIN_NORMAL (با مقدار 2.2250738585072014E-308) است. استاندارد شماره 754 IEEE در مورد محاسبات اعشاری (+) جزییات تفاوت بین این موارد را به دقت توضیح داده است. در این بخش صرفاً روی این نکته تمرکز می‌کنیم که اصولاً چرا به یک مقدار کمینه برای اعداد اعشاری نیاز داریم.

مقدار double نمی‌تواند تا هر مقدار دلخواه کوچک شود چون تعداد بیت‌هایی که برای نمایش آن داریم محدود است. کمترین توان برای نمایش انواع اعشاری double به صورت 1074- است. این بدان معنی است که کمترین مقدار مثبتی که یک نوع داده double می‌تواند داشته باشد برابر با Math.pow(2, -1074) است که معادل 4.9e-324 است. در نتیجه دقت نوع double در جاوا از مقادیر بین 0 و 4.9e-324 یا بین 4.9e-324- و 0 برای مقادیر منفی پشتیبانی نمی‌کند.

در صورتی که تلاش کنیم مقدار بسیار کوچکی را به یک متغیر از نوع double انتساب دهیم، حالتی مانند زیر پیش می‌آید:

1for(int i = 1073; i <= 1076; i++) {
2    System.out.println("2^" + i + " = " + Math.pow(2, -i));
3}

خروجی آن چنین است:

2^1073 = 1.0E-323
2^1074 = 4.9E-324
2^1075 = 0.0
2^1076 = 0.0

چنان که می‌بینید اگر مقدار خیلی کوچکی انتساب دهیم با underflow مواجه می‌شویم و مقدار حاصل 0.0 (صفر مثبت) است. به طور مشابه در مورد اعداد منفی یک underflow رخ می‌دهد که نتیجه آن مقدار 0.0- (صفر منفی) است.

تشخیص Underflow و Overflow برای انواع داده اعشاری

از آنجا که در مورد اعداد اعشاری بروز Overflow موجب خروجی مثبت یا منفی بی‌نهایت و پدیده Underflow موجب ارائه خروجی مثبت یا منفی صفر می‌شود، به متدهای حسابی دقیق، مانند آنچه در مورد انواع داده صحیح دیدیم، نیاز نداریم. به جای آن می‌توانیم ثابت‌های خاص مورد اشاره را برای حالت‌های Underflow و Overflow مورد بررسی قرار دهیم.

 

اگر بخواهیم در این حالت‌ها یک استثنا صادر شود، می‌توانیم یک متد کمکی پیاده‌سازی کنیم. به مثال زیر توجه کنید:

1public static double powExact(double base, double exponent) {
2    if(base == 0.0) {
3        return 0.0;
4    }
5     
6    double result = Math.pow(base, exponent);
7     
8    if(result == Double.POSITIVE_INFINITY ) {
9        throw new ArithmeticException("Double overflow resulting in POSITIVE_INFINITY");
10    } else if(result == Double.NEGATIVE_INFINITY) {
11        throw new ArithmeticException("Double overflow resulting in NEGATIVE_INFINITY");
12    } else if(Double.compare(-0.0f, result) == 0) {
13        throw new ArithmeticException("Double overflow resulting in negative zero");
14    } else if(Double.compare(+0.0f, result) == 0) {
15        throw new ArithmeticException("Double overflow resulting in positive zero");
16    }
17 
18    return result;
19}

در این متد باید از متد ()Double.compare استفاده کنیم. عملگرهای مقایسه معمول (<and>) بین صفر مثبت و منفی تمایز قائل نمی‌شوند.

صفر مثبت و منفی

در نهایت مثالی را بررسی می‌کنیم که نشان می‌دهد چرا باید در زمان کار با صفر و بی‌نهایت مثبت و منفی هوشیار باشیم. در این بخش ابتدا چند متغیر را تعریف می‌کنیم:

1double a = +0f;
2double b = -0f;

از آنجا که 0 مثبت و منفی برابر در نظر گرفته می‌شوند:

1assertTrue(a == b);

در حالی که بی‌نهایت مثبت و منفی متفاوت تصور می‌شوند:

1assertTrue(1/a == Double.POSITIVE_INFINITY);
2assertTrue(1/b == Double.NEGATIVE_INFINITY);

با این حال assertion زیر صحیح است:

1assertTrue(1/a!= 1/b);

این وضعیت با assertion نخست ما تناقض دارد.

سخن پایانی

در این مقاله با مفهوم Underflow و Overflow آشنا شدیم، رخداد آن در جاوا را بررسی کردیم و تفاوت بین انواع داده صحیح و اعشاری را در این زمینه مشاهده کردیم. همچنین شیوه تشخیص Underflow و Overflow در طی اجرای برنامه را مورد بررسی قرار دادیم. سورس کد کامل این مقاله را می‌توانید در این ریپوی گیت‌هاب (+) ملاحظه کنید.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
baeldung
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *