ArrayList و HashMap در جاوا؛ آموزش Collections با مثال
آرایه در جاوا اندازه ثابت دارد، اما دادههای برنامه واقعی مدام بزرگ و کوچک میشوند. Collections Framework با ArrayList، HashMap، …
Exception یا استثنا در جاوا رویدادی است که اجرای عادی برنامه را به هم میزند؛ مثل ورودی غیرعددی، فایلی که وجود ندارد یا دسترسی بیرون از آرایه. در این آموزش با مثالهایی که با JDK 25 اجرا کردهایم، try-catch و finally، تفاوت checked و unchecked، throw و throws، استثنای سفارشی، try-with-resources و بهترین روشهای مدیریت خطا را یاد میگیرید.
آخرین بهروزرسانی: ۲۰ آبان ۱۴۰۵ — منابع بررسیشده در این روز: آموزش رسمی استثناها در Oracle و dev.java، فصلهای ۱۱ و ۱۴ مشخصات زبان جاوا ۲۵، مستندات API کلاسهای Throwable، Exception، RuntimeException، AutoCloseable و Objects در JDK 25، JEP 358 و صفحه پروژه JDK.
⚡ پاسخ کوتاه
در جاوا کدی را که ممکن است خطا بدهد در try مینویسید، خطای مشخص را در catch میگیرید و کارهای پایانی را در finally یا بهتر از آن با try-with-resources انجام میدهید. استثناهای checked مثل IOException باید گرفته یا با throws اعلام شوند، اما RuntimeException و زیرکلاسهایش unchecked هستند و معمولاً نشانه باگاند. catch خالی ننویسید و هنگام پرتاب دوباره، خطای اصلی را بهعنوان cause نگه دارید.
📑 فهرست مطالب
Exception یا استثنا رویدادی است که در زمان اجرای برنامه، روند عادی آن را به هم میزند. مثلاً کاربر بهجای سن، کلمه «بیست» را وارد میکند، فایلی که باید خوانده شود وجود ندارد یا برنامه میخواهد به خانهای بیرون از آرایه دسترسی پیدا کند. در این لحظه جاوا یک شیء استثنا میسازد که نوع خطا، پیام آن و مسیر رسیدن به خطا را در خود دارد و آن را «پرتاب» میکند. اگر هیچ بخشی از برنامه این استثنا را نگیرد، برنامه متوقف میشود و پیامی مثل این چاپ میشود:
public class Ex1 {
public static void main(String[] args) {
int[] scores = {18, 15, 20};
System.out.println("start");
System.out.println(scores[3]);
System.out.println("end");
}
}
// output:
// start
// Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
// at Ex1.main(Ex1.java:5)
خط end هرگز چاپ نشد. پیام خطا سه چیز مهم به ما میگوید: نوع استثنا یعنی ArrayIndexOutOfBoundsException، توضیح آن یعنی خانه ۳ در آرایهای با طول ۳ وجود ندارد، و محل خطا یعنی خط ۵ فایل Ex1.java. به این فهرست مسیر، Stack Trace میگویند. یاد گرفتن خواندن همین چند خط، نصف مهارت رفع خطا در جاواست. اگر با آرایهها آشنا نیستید، آرایهها در جاوا را ببینید. مدیریت خطا یعنی برنامه را طوری بنویسیم که برای این اتفاقها آماده باشد: جایی که میشود خطا را درست کرد، آن را بگیریم و رفتار مناسب نشان دهیم، و جایی که نمیشود، خطا را با اطلاعات کافی به لایه بالاتر بفرستیم. همه مثالهای این مقاله را با JDK 25، نسخه پشتیبانی بلندمدت (Temurin 25.0.5)، اجرا کردهایم و خروجیها واقعیاند. برای مقدمات نصب و اجرا، مقدمهای بر جاوا را بخوانید.
همه خطاها در جاوا از کلاس Throwable میآیند. مستندات API جاوا توضیح میدهد که فقط شیءهایی که از Throwable یا زیرکلاسهایش ساخته شدهاند، میتوانند پرتاب و گرفته شوند. زیر Throwable دو شاخه اصلی هست: Error برای مشکلات جدی سطح ماشین مجازی است، مثل تمام شدن حافظه با OutOfMemoryError یا StackOverflowError در بازگشت بیپایان. برنامه معمولی نباید اینها را بگیرد، چون معمولاً کاری برای درست کردنشان از دستش برنمیآید. اگر میخواهید بدانید حافظه JVM چطور سازماندهی شده، نحوه کار JVM را ببینید. Exception برای شرایطی است که برنامه میتواند آنها را پیشبینی و مدیریت کند. خود این شاخه هم دو گروه دارد که در ادامه مفصل میبینیم: استثناهای بررسیشده و RuntimeException با زیرکلاسهایش.

ساختار پایه مدیریت خطا try-catch است. کدی را که ممکن است خطا بدهد داخل try مینویسیم و در catch مشخص میکنیم اگر استثنای خاصی رخ داد، چه کنیم. بخش اختیاری finally هم همیشه اجرا میشود؛ چه خطا رخ بدهد و چه ندهد.
public class Ex2 {
static int parseAge(String text) {
try {
int age = Integer.parseInt(text.trim());
System.out.println("parsed: " + age);
return age;
} catch (NumberFormatException e) {
System.out.println("not a number: " + e.getMessage());
return -1;
} finally {
System.out.println("finally runs for \"" + text + "\"");
}
}
public static void main(String[] args) {
parseAge("27");
parseAge("twenty");
}
}
// output:
// parsed: 27
// finally runs for "27"
// not a number: For input string: "twenty"
// finally runs for "twenty"
در فراخوانی دوم، Integer.parseInt نتوانست متن را به عدد تبدیل کند و NumberFormatException پرتاب کرد. اجرا بلافاصله از try بیرون پرید، خط parsed چاپ نشد و catch مقدار -1 را برگرداند. در هر دو حالت finally اجرا شد. توجه کنید که finally حتی وقتی try با return تمام میشود هم اجرا میشود؛ ما این را هم جداگانه آزمودیم و دیدیم پیام finally قبل از برگشت مقدار چاپ شد.
یک try میتواند چند catch داشته باشد. جاوا آنها را از بالا به پایین بررسی میکند و اولین catch که با نوع خطا بخواند، اجرا میشود. به همین دلیل catch نوع خاصتر باید بالاتر از نوع عمومیتر باشد. اگر catch (Exception e) را بالای catch (NumberFormatException e) بگذارید، کامپایلر خطا میدهد، چون catch دوم هرگز اجرا نمیشود. از جاوا ۷ میشود چند نوع نامرتبط را با علامت | در یک catch گرفت؛ وقتی رفتار شما برای هر دو یکی است، این کار کد تکراری را حذف میکند. نمونهاش را در بخش استثنای سفارشی میبینید.
finally جای کارهای «جمعوجور کردن» است؛ مثل بستن فایل، برگرداندن اتصال پایگاه داده یا آزاد کردن قفل. یک قانون مهم: در finally هرگز return ننویسید و استثنای تازه پرتاب نکنید. اگر این کار را بکنید، خطای اصلی که در try رخ داده بود، بیصدا گم میشود و پیدا کردن علت مشکل خیلی سخت میشود. برای بستن منابع هم بهتر است از try-with-resources استفاده کنید که کمی پایینتر میبینیم.
جاوا استثناها را به دو گروه تقسیم میکند و این تقسیم روی شکل کد شما اثر مستقیم دارد. طبق فصل ۱۱ مشخصات زبان جاوا، RuntimeException و زیرکلاسهایش و همچنین Error و زیرکلاسهایش «بررسینشده» یا unchecked هستند. همه استثناهای دیگر «بررسیشده» یا checked هستند. فرق عملی این است: اگر متدی ممکن است استثنای checked پرتاب کند، کامپایلر مجبورتان میکند یا آن را با try-catch بگیرید یا در امضای متد با throws اعلام کنید. برای unchecked چنین اجباری نیست. ببینید اگر این قاعده را رعایت نکنیم چه میشود:
import java.nio.file.Files;
import java.nio.file.Path;
public class Bad {
public static void main(String[] args) {
var lines = Files.readAllLines(Path.of("prices.txt"));
System.out.println(lines.size());
}
}
// javac output:
// Bad.java:6: error: unreported exception IOException; must be caught or declared to be thrown
این کد اصلاً کامپایل نمیشود. خواندن فایل ممکن است به هزار دلیل شکست بخورد که دست برنامهنویس نیست؛ فایل پاک شده، دسترسی نداریم یا دیسک مشکل دارد. جاوا با checked بودن IOException مطمئن میشود که شما به این حالت فکر کردهاید.
| ویژگی | Checked | Unchecked |
|---|---|---|
| از کدام کلاس | Exception (بهجز RuntimeException) | RuntimeException و Error |
| اجبار کامپایلر | باید گرفته یا با throws اعلام شود | اجباری ندارد |
| معمولاً نشانه | شرایط بیرونی مثل نبودن فایل یا قطع شبکه | باگ در کد |
| نمونه | IOException، NoSuchFileException | NullPointerException، ArrayIndexOutOfBoundsException |
| واکنش درست | مدیریت یا اعلام به لایه بالاتر | درست کردن کد |
یک قاعده ساده برای به خاطر سپردن: checked برای مشکلاتی است که حتی در برنامه درست هم ممکن است رخ دهند و برنامه میتواند برایشان کاری بکند، مثل نبودن فایل یا قطع شبکه. unchecked معمولاً نشانه یک باگ است؛ مثل دسترسی به خانه بیرون از آرایه، تقسیم بر صفر یا صدا زدن متد روی null. راه درست برای unchecked معمولاً درست کردن کد است، نه گرفتن خطا.
این دو کلمه برای مبتدیها گیجکنندهاند، اما کارشان کاملاً جداست. throw یک دستور است و استثنا را همین الان پرتاب میکند. throws بخشی از امضای متد است و اعلام میکند این متد ممکن است فلان استثنا را بیرون بدهد.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class Ex3 {
static List<String> readLines(String name) throws IOException {
return Files.readAllLines(Path.of(name));
}
public static void main(String[] args) {
try {
List<String> lines = readLines("prices.txt");
System.out.println(lines.size() + " lines");
} catch (IOException e) {
System.out.println("cannot read file: " + e);
}
}
}
// output:
// cannot read file: java.nio.file.NoSuchFileException: prices.txt
متد readLines خودش خطا را مدیریت نمیکند و با throws IOException مسئولیت را به صداکننده میسپارد. main آن را میگیرد و پیام مناسب نشان میدهد. نکته جالب خروجی این است که خطای واقعی NoSuchFileException بود، اما catch (IOException e) آن را گرفت؛ چون NoSuchFileException زیرکلاس IOException است. همین ویژگی است که اجازه میدهد یک catch عمومیتر، چند خطای خاصتر را پوشش دهد. چه زمانی خطا را بگیریم و چه زمانی با throws بالا بفرستیم؟ جایی بگیرید که واقعاً بدانید با آن چه کنید. متدی که فقط فایل را میخواند نمیداند باید به کاربر پیام بدهد، فایل پیشفرض بخواند یا برنامه را ببندد. اما لایه بالاتر که با کاربر در ارتباط است، این را میداند.
استثناهای آماده جاوا برای خطاهای عمومی کافیاند، اما گاهی میخواهید خطا به زبان مسئله شما حرف بزند. مثلاً در برنامه بانکی، «موجودی کافی نیست» یک وضعیت قابل پیشبینی کسبوکار است و بهتر است نوع خودش را داشته باشد تا لایههای بالاتر بتوانند آن را جداگانه بگیرند و حتی مبلغ کسری را بخوانند:
public class Ex4 {
static class InsufficientBalanceException extends Exception {
private final long shortage;
InsufficientBalanceException(long shortage) {
super("balance is short by " + shortage);
this.shortage = shortage;
}
long getShortage() {
return shortage;
}
}
static class Account {
private long balance;
Account(long balance) { this.balance = balance; }
void withdraw(long amount) throws InsufficientBalanceException {
if (amount <= 0) {
throw new IllegalArgumentException("amount must be positive: " + amount);
}
if (amount > balance) {
throw new InsufficientBalanceException(amount - balance);
}
balance -= amount;
}
long getBalance() { return balance; }
}
public static void main(String[] args) {
Account acc = new Account(500);
try {
acc.withdraw(200);
acc.withdraw(450);
} catch (InsufficientBalanceException e) {
System.out.println(e.getMessage() + " (shortage=" + e.getShortage() + ")");
}
System.out.println("balance: " + acc.getBalance());
try {
acc.withdraw(-1);
} catch (IllegalArgumentException | InsufficientBalanceException e) {
System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
}
// output:
// balance is short by 150 (shortage=150)
// balance: 300
// IllegalArgumentException: amount must be positive: -1
در این مثال دو نوع خطا داریم و انتخاب نوعشان عمدی است. مبلغ منفی یعنی کد صداکننده اشتباه کرده؛ پس IllegalArgumentException که unchecked است مناسب است. اما کم بودن موجودی یک وضعیت عادی در کار بانک است که صداکننده باید برایش برنامه داشته باشد؛ پس آن را checked تعریف کردهایم تا کامپایلر یادآوری کند. اگر میخواستید استثنای سفارشی unchecked باشد، باید آن را از RuntimeException ارثبری میکردید. کلاس Account و کپسولهسازی آن را در کلاس و شیء در جاوا مفصل ساختهایم. چند قاعده برای استثنای سفارشی: نام آن با Exception تمام شود، پیام روشنی به سازنده والد بدهد، اطلاعات مفید مثل مبلغ کسری را در فیلد نگه دارد و فقط وقتی ساخته شود که واقعاً لایهای قرار است آن را جدا از بقیه بگیرد. اگر قصد ارثبری و طراحی خانوادهای از استثناها را دارید، تفاوت Interface و Abstract Class هم به فهم طراحی سلسلهمراتب کمک میکند.

فایل، اتصال پایگاه داده و سوکت شبکه منابعی هستند که بعد از استفاده باید بسته شوند. نوشتن close در finally کار را شلوغ میکند و اگر خود close هم خطا بدهد، ماجرا پیچیدهتر میشود. از جاوا ۷، try-with-resources این کار را خودکار انجام میدهد. هر شیئی که رابط AutoCloseable را پیاده کرده باشد، میتواند در پرانتز try ساخته شود و جاوا در پایان، حتی اگر خطا رخ دهد، آن را میبندد:
public class Ex5 {
static class Resource implements AutoCloseable {
private final String name;
Resource(String name) { this.name = name; System.out.println("open " + name); }
void use(boolean fail) {
System.out.println("use " + name);
if (fail) throw new IllegalStateException("failed while using " + name);
}
@Override
public void close() { System.out.println("close " + name); }
}
public static void main(String[] args) {
try (Resource db = new Resource("db"); Resource file = new Resource("file")) {
db.use(false);
file.use(true);
} catch (IllegalStateException e) {
System.out.println("caught: " + e.getMessage());
}
}
}
// output:
// open db
// open file
// use db
// use file
// close file
// close db
// caught: failed while using file
به ترتیب خروجی دقت کنید. منابع به ترتیب ساخته شدن باز شدند و به ترتیب برعکس بسته شدند؛ اول file و بعد db. مهمتر اینکه هر دو قبل از اجرای catch بسته شدند. پس وقتی به catch میرسید، مطمئن هستید هیچ منبعی باز نمانده است. اگر close هم خطا بدهد، جاوا خطای اصلی را نگه میدارد و خطای close را بهعنوان استثنای «سرکوبشده» به آن ضمیمه میکند که با متد getSuppressed قابل خواندن است.
گاهی در یک لایه خطای فنی را میگیرید و خطایی با معنای بالاتر پرتاب میکنید. مثلاً لایه گزارشگیری نمیخواهد جزئیات فایل را بیرون بدهد، اما نباید علت اصلی را هم دور بریزد. سازندههای Throwable پارامتری به نام cause دارند که خطای اصلی را نگه میدارد:
import java.io.IOException;
public class Ex6 {
static class ReportException extends Exception {
ReportException(String message, Throwable cause) { super(message, cause); }
}
static String loadConfig() throws IOException {
throw new IOException("config.yml not found");
}
static String buildReport() throws ReportException {
try {
return loadConfig();
} catch (IOException e) {
throw new ReportException("monthly report failed", e);
}
}
public static void main(String[] args) {
try {
buildReport();
} catch (ReportException e) {
System.out.println(e.getMessage());
System.out.println("cause: " + e.getCause().getMessage());
}
}
}
// output:
// monthly report failed
// cause: config.yml not found
اگر Stack Trace این خطا را چاپ کنید، بخشی با عنوان Caused by میبینید که مسیر خطای اصلی را نشان میدهد. اشتباه رایج این است که در catch بنویسید throw new ReportException("failed") و e را پاس ندهید. در آن صورت دقیقاً همان اطلاعاتی که برای پیدا کردن مشکل لازم دارید، از بین میرود.
NullPointerException شاید پرتکرارترین خطای جاوا باشد: وقتی متغیری که null است را مثل شیء واقعی به کار میبرید. از JDK 14 به بعد، طبق JEP 358، پیام این خطا دقیقاً میگوید کدام بخش null بوده است. ما این کد را آزمایش کردیم:
String name = null;
System.out.println(name.length());
// with javac -g:
// Cannot invoke "String.length()" because "name" is null
یک نکته ظریف از همین آزمایش: وقتی کلاس را با javac -g کامپایل کردیم، پیام نام متغیر یعنی name را نشان داد. اما وقتی همان فایل را مستقیم با دستور java Ex6.java اجرا کردیم، پیام بهجای نام متغیر عبارت local1 را نوشت؛ چون اطلاعات دیباگ متغیرهای محلی در آن حالت در دسترس نبود. JEP 358 هم همین را توضیح میدهد: نام متغیرهای محلی فقط وقتی نشان داده میشود که کد با اطلاعات دیباگ کامپایل شده باشد. راه درست مقابله با NullPointerException معمولاً catch کردن آن نیست. بهتر است ورودی را زود بررسی کنید، مثلاً با Objects.requireNonNull در ابتدای متد یا سازنده، تا خطا دقیقاً در همان جایی رخ دهد که مقدار نادرست وارد شده است. این همان رویکرد «زود شکست بخور» است که پیدا کردن باگ را خیلی سادهتر میکند.

| استثنا | نوع | علت رایج | پیشگیری |
|---|---|---|---|
| NullPointerException | unchecked | استفاده از متغیری که null است | Objects.requireNonNull و بررسی ورودی |
| ArrayIndexOutOfBoundsException | unchecked | اندیس منفی یا بزرگتر از طول آرایه | بررسی طول و شرط درست حلقه |
| NumberFormatException | unchecked | تبدیل متن غیرعددی به عدد | گرفتن خطا هنگام تبدیل ورودی کاربر |
| ArithmeticException | unchecked | تقسیم عدد صحیح بر صفر (/ by zero) | بررسی مقسومعلیه |
| ClassCastException | unchecked | تبدیل شیء به نوع نادرست | instanceof پیش از تبدیل |
| IOException | checked | مشکل خواندن یا نوشتن فایل و شبکه | try-with-resources و پیام مناسب |
| ConcurrentModificationException | unchecked | تغییر مجموعه در حین پیمایش | removeIf یا Iterator.remove |
بیشتر این خطاها با بررسی ساده قبل از عمل قابل پیشگیریاند. پیش از دسترسی به آرایه طول آن را بسنجید؛ در حلقهها شرط پایان را دقیق بنویسید که در حلقهها در جاوا توضیح دادهایم؛ و برای تصمیمگیری بر اساس ورودی، از if و switch استفاده کنید که در تصمیمگیری در جاوا آمده است. استثنا برای «اتفاقهای استثنایی» است، نه جایگزین شرط معمولی.
اول، catch خالی ننویسید. بلوک catch که هیچ کاری نمیکند، خطا را مثل این است که زیر فرش پنهان کنید؛ برنامه ادامه پیدا میکند اما در وضعیتی نادرست. اگر واقعاً قرار است خطایی نادیده گرفته شود، دستکم یک خط توضیح بنویسید که چرا. دوم، Exception یا Throwable را بیدلیل نگیرید. گرفتن نوع خیلی عمومی، باگهای واقعی مثل NullPointerException را هم پنهان میکند. نوع دقیقی را بگیرید که برایش برنامه دارید. سوم، پیام خطا را مفید بنویسید. «error» به هیچکس کمک نمیکند؛ «amount must be positive: -1» دقیقاً میگوید چه مقداری مشکل داشت. چهارم، استثنا را فقط یک بار ثبت کنید. اگر هر لایه خطا را هم لاگ کند و هم دوباره پرتاب کند، یک خطا ده بار در لاگ تکرار میشود. یا ثبت کنید و مدیریت کنید، یا با علت به بالا بفرستید. پنجم، برای کنترل جریان عادی برنامه از استثنا استفاده نکنید. ساختن استثنا و Stack Trace هزینه دارد و کدی که با استثنا تصمیم میگیرد، سخت خوانده میشود. ششم، منابع را با try-with-resources ببندید و از return در finally پرهیز کنید.
یک مثال کاربردی رایج، خواندن عدد از آرگومانهای خط فرمان است. کاربر ممکن است آرگومان ندهد یا متن غیرعددی بدهد. در این حالت باید دو خطای متفاوت را جدا کنید: نبودن آرگومان را با بررسی طول آرایه args پیش از دسترسی تشخیص دهید و تبدیل نشدن متن را با گرفتن NumberFormatException. نحوه دادن آرگومان به برنامه را در آرگومانهای خط فرمان در جاوا توضیح دادهایم. وقتی با مجموعهها کار میکنید هم استثناهای خاص خودشان را دارید؛ مثلاً حذف عضو از لیست وسط پیمایش با for-each به ConcurrentModificationException میرسد و افزودن به لیستی که با List.of ساخته شده، UnsupportedOperationException میدهد. این دو را با مثال در آموزش ArrayList و HashMap در جاوا نشان دادهایم. اگر میخواهید جاوا را برای پروژه واقعی یا نرمافزار سازمانی به کار ببرید، خدمات طراحی و توسعه نرمافزار رادینو را هم ببینید.
تمرین اول: متدی بنویسید که یک رشته بگیرد و آن را به عدد صحیح تبدیل کند. اگر رشته null یا خالی بود، IllegalArgumentException با پیام مناسب بدهد و اگر عدد نبود، NumberFormatException را بگیرد و مقدار پیشفرضی که به متد داده شده برگرداند. تمرین دوم: برای کلاس Account متد transfer بنویسید که از یک حساب به حساب دیگر پول منتقل کند. اگر موجودی کافی نبود، InsufficientBalanceException پرتاب کند و هیچکدام از دو حساب تغییر نکند. تمرین سوم: کلاسی بسازید که AutoCloseable را پیاده کند و در close پیام چاپ کند. آن را در try-with-resources استفاده کنید و هم در try و هم در close استثنا پرتاب کنید. بعد با getSuppressed استثنای سرکوبشده را چاپ کنید.
هر دو از Throwable میآیند. Error نشانه مشکل جدی ماشین مجازی است، مثل کمبود حافظه، و معمولاً نباید گرفته شود. Exception شرایطی است که برنامه میتواند پیشبینی و مدیریت کند.
برای checked، کامپایلر مجبورتان میکند آن را با try-catch بگیرید یا با throws اعلام کنید؛ مثل IOException. RuntimeException و زیرکلاسهایش unchecked هستند و چنین اجباری ندارند؛ مثل NullPointerException.
throw دستوری است که یک استثنا را همان لحظه پرتاب میکند. throws در امضای متد میآید و اعلام میکند متد ممکن است آن استثنا را بیرون بدهد.
در حالت عادی بله؛ چه خطا رخ دهد، چه ندهد و حتی وقتی try با return تمام شود. فقط در موارد خاص مثل توقف کامل ماشین مجازی با System.exit یا قطع ناگهانی فرایند اجرا نمیشود.
وقتی خطا معنای مشخصی در مسئله شما دارد و لایه دیگری قرار است آن را جداگانه بگیرد یا اطلاعات اضافهای از آن بخواند؛ مثل کمبود موجودی در برنامه بانکی. برای خطاهای عمومی از استثناهای آماده جاوا استفاده کنید.
نظرات 0
برای ارسال نظر وارد حساب کاربری شوید.
هنوز نظری ثبت نشده؛ اولین نفر باشید.