آموزش قدم به قدم جاوا – قسمت بیست و پنجم

ورودی و خروجی (Input/Output)

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

استریم (Stream)

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

دو نوع کلی استریم در جاوا وجود دارد: Byte Stream و Character Stream

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

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

نکته: این تقسیم‌بندی باعث شده تا کلاس‌های زیادی برای کار با IO وجود داشته باشد اما تمام این کلاس‌ها از یک سری قواعد کلی پیروی می‌کنند که با یادگیری آن‌ها می‌توان از تمام این کلاس‌ها به راحتی استفاده کرد. ما فقط از چند کلاس مهم این مجموعه در این آموزش استفاده می‌کنیم و در انتهای این مطلب مرجعی را برای مطالعه بیشتر کلاس‌های IO معرفی خواهیم کرد.

کار با Byte Streamها

کلاس‌های مربوط به کار با Byte Stream در سلسله مراتب وراثت در نهایت به یکی از دو کلاس انتزاعی InputStream و یا OutputStream می‌رسند. کلاس InputStream مشخصه‌های کلی و ملزومات وارد کردن بایت‌ها از منبع خارجی به برنامه را تعریف می‌کند و کلاس OutputStream نیز قواعد ارسال بایت‌ها از برنامه به منبع خارجی را مشخص می‌کند.

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

مثال اول: می‌خواهیم با استفاده از کلاس FileInputStream محتویات یک فایل را بخوانیم:

حال به تحلیل این قطعه کد می‌پردازیم:

۱ – ابتدا متغیری به نام filePath تعریف کردیم که شامل آدرس کامل یک فایل به نام test با فرمت txt است (این فایل قبلا ایجاد شده است). تنها نکته قابل توجه در این خط استفاده از دو \ به جای یکی است. بعضی کاراکترهای خاص در زبان جاوا برای کار با رشته‌ها وجود دارند که با یک بک اسلش و یک حرف مشخص می‌شوند بنابراین برای اینکه جاوا کاراکتر \ که برای مشخص کردن مسیر فایل به کار رفته را با یک کاراکتر خاص اشتباه نگیرد باید از دو بک اسلش استفاده کنیم.

 

۲ – متغیری به نام fileInput از نوع کلاس FileInputStream ایجاد کردیم و مقدار اولیه آن را null قرار دادیم. null به معنی پوچ و بدون مقدار است و متغیرهایی که از نوع Reference Type ها هستند می‌توانند null باشند.

توجه داشته باشید که کلاس FileInputStream و کلاس‌های دیگری که در این آموزش استفاده می‌کنیم در پکیج java.io قرار دارند.

 

۳ – سپس شیئی از کلاس FileInputStream ایجاد کردیم و به سازنده آن، آدرس فایل خود را دادیم. از این کلاس برای خواندن بایت‌های یک فایل استفاده می‌شود. کد سازنده این کلاس به این صورت است:

استثنای FileNotFound که وقتی فایل مورد نظر وجود نداشته باشد رخ می‌دهد یک Checked Exception است و همانطور که در قسمت استثناها خواندید این نوع استثناها هنگام کامپایل چک می‌شوند و حتما باید آن‌ها را در کد مدیریت کرد بنابراین کدی که یک شی از کلاس FileInputStream می‌سازد چون سازنده این کلاس را فراخوانی می‌کند باید درون یک بلاک try و catch قرار بگیرد و استثنای FileNotFoundException را مدیریت کند همانگونه که در کد مشخص است.

 

۴ – در خط بعد متغیری به نام currentByte تعریف کردیم و مقدار بازگشتی حاصل از اجرای متد read از شی fileInput را به آن نسبت دادیم. متد read یک بایت از محتویات فایل را می‌خواند و مقدار آن را برمی‌گرداند و هربار فراخوانی این متد باعث خواندن بایت بعدی می‌شود. کدی که متد read را فراخوانی می‌کند باید استثنای IOException را مدیریت کند و به همین دلیل یک بلاک catch دیگر برای مدیریت این استثنا اضافه کردیم.

 

۵ – سپس حلقه‌ای تعریف کردیم که شرط آن مساوی نبودن مقدار currentByte با عدد 1- است. وقتی متد read بخواهد بایت بعدی فایل را بخواند اما به انتهای فایل رسیده باشد عدد منفی یک را بر می‌گرداند بنابراین این حلقه تا وقتی که متد read به انتهای فایل نرسد اجرا خواهد شد.

 

۶ – داخل حلقه مقدار currentByte را به char تبدیل و آن را چاپ می‌کنیم. همانطور که در قسمت‌ نوع داده‌ها گفتیم هر کاراکتر یک کد دارد که وقتی عدد یک بایتی را به کاراکتر تبدیل کنیم آنگاه کاراکتر مربوط به آن نمایش داده می‌شود. در خط بعد نیز بایت بعدی را خوانده و دوباره در متغیر currentByte قرار می‌دهیم.

 

۷ – با بلاک finally هم آشنایی دارید و می‌دانید که این بلاک در هر صورت اجرا می‌شود چه استثنایی رخ دهد و چه رخ ندهد. نکته مهمی که هنگام کار با فایل‌ها باید در نظر داشته باشید این است که حتما بعد از اتمام کار با یک Stream که با یک فایل کار می‌کند باید آن را ببندید تا برنامه ما فایل را رها کند و نبستن آن ممکن است باعث به وجود آمدن مشکلاتی در برنامه شود.

بهترین مکان برای بستن فایل همان بلاک finally است چون اگر مثلا کد بستن فایل را در انتهای بلاک try قرار دهید در صورت بروز استثنا قبل از رسیدن به کد بستن فایل، برنامه کدهای دیگر را اجرا نخواهد کرد و در نتیجه Stream باز می‌ماند.

قبل از بستن Stream ابتدا null بودن متغیر fileInput را چک کردیم. دلیل این کار این است که اگر هنگام ایجاد شی از کلاس FileInputStream یک استثنای FileNotFound رخ دهد آنگاه مقدار fileInput همان null باقی می‌ماند و وقتی بلاک finally اجرا شود و بخواهیم متد close را از یک شی null فراخوانی کنیم با خطا مواجه خواهیم شد.

ضمنا متد close را نیز باید درون یک بلاک try نوشت چون ممکن است یک IOException تولید کند.

دلیل اینکه متغیر fileInput را قبل از شروع بلاک try تعریف کردیم برای این بود که در بلاک finally بتوانیم به آن دسترسی داشته باشیم.

 

مثال دوم: کلاس FileOutputStream برای نوشتن اطلاعات باینری در فایل طراحی شده است. می‌خواهیم با این کلاس یک رشته را داخل یک فایل بنویسیم:

۱ – پس از مشخص‌کردن مسیر فایلی که می‌خواهیم یک رشته را در آن بنویسیم، شیئی از کلاس FileOutputStream ایجاد کردیم و مسیر فایل مورد نظر را به آن دادیم. اگر فایلی که آدرس آن را دادیم وجود نداشته باشد آنگاه این کلاس یک فایل به همان نام می‌سازد اما اگر مسیر فایلی که به آن دادیم اشتباه باشد (مثلا مسیر یک فولدر باشد نه یک فایل) یا به هر دلیلی امکان ساخت فایل وجود نداشته باشد (مثلا ممکن است اجازه ساخت فایل در بعضی از فولدرها وجود نداشته باشد یا کاربر فعلی اجازه ساخت فایل نداشته باشد) آنگاه استثنای FileNotFound رخ می‌دهد.

 

۲ – سپس متغیری به نام content تعریف کردیم که محتوای مورد نظر ما در آن قرار دارد. در خط بعد متد getBytes از کلاس String را فراخوانی کردیم و مقدار بازگشتی آن را به آرایه‌ای از نوع byte نسبت دادیم. متد getBytes آرایه‌ای نوع byte برمی‌گرداند که هر خانه از این آرایه حاوی مقدار یکی از کاراکترهای رشته بر حسب بایت می‌باشد. چون Byte Stream ها فقط می‌توانند با بایت‌ها کار کنند بنابراین باید رشته مورد نظر خود را به بایت تبدیل می‌کردیم.

 

۳ – سپس متد write از شی fileInput را فراخوانی کرده و آرایه بایتی که ایجاد کردیم را به آن می‌دهیم. این متد محتویات آرایه را به ترتیب در فایل می‌نویسد.

بستن فایل به صورت خودکار

تا قبل از نسخه 7 جاوا بستن فایل‌ها به همان شکلی انجام می‌شد که در مثال‌های قبل مشاهده کردید و هنوز هم بسیاری از برنامه نویسان از این روش استفاده می‌کنند و البته این روش کاملا صحیح است اما از نسخه 7 جاوا به بعد قابلیت جدیدی اضافه شد که به صورت خودکار این کار را انجام می‌دهد. این قابلیت شکل جدیدی از بلاک try ارائه کرده است.

مثال: می‌خواهیم مثال اول را با استفاده از این ویژگی دوباره بنویسیم:

همانطور که دیدید شی fileInput را در پرانتزی روبروی بلاک try تعریف کردیم و دو بلاک catch لازم را نیز اضافه کردیم. وقتی که کار بلاک try تمام شود استریم fileInput نیز به صورت خودکار بسته می‌شود.

سوالی که پیش می‌آید این است که به جز FileInputStream چه کلاس‌های دیگری می‌توانند از این قابلیت بهره بگیرند؟

اینترفیسی به نام AutoCloseable در پکیج java.lang قرار دارد که خود از اینترفیس دیگری به نام Closeable در پکیج java.io ارث بری دارد. هر کلاسی که اینترفیس AutoCloseable را پیاده‌سازی کند (تمام کلاس‌های استریم‌ها این اینترفیس را پیاده سازی کرده‌اند) می‌تواند از قابلیت بسته‌شدن خودکار بهره ببرد.

کار با Character Stream ها

حال به سراغ گروه دوم از استریم‌ها در جاوا می‌رویم. این استریم‌ها مخصوصا برای کار با کاراکترها و حروف طراحی شده‌اند نه برای کار با بایت‌های خام. دو کلاس انتزاعی Writer و Reader برای این استریم‌ها وجود دارند که کلاس‌های مختلفی از این دو کلاس ارث بری دارند. کاراکتر استریم‌ها کاربرد کمتری نسبت به بایت استریم‌ها دارند.

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

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

بافرینگ در عملیات ورودی و خروجی (IO Buffering)

در مثال اول که یک فایل را می‌خواندیم برنامه برای خواندن هر بایت از فایل به هارد دیسک مراجعه می‌کرد تا یک بایت از فایل مورد نظر را خوانده و به برنامه بدهد. یعنی اگر مثلا یک فایل 100 بایتی داشتیم برنامه باید 100 بار به دیسک مراجعه می‌کرد و یک بایت را می‌خواند که این عملیات بسیار سنگین است و در فایل‌های بزرگ باعث کندی برنامه می‌شود.

راه حل جاوا برای حل این مشکل استفاده از قابلیتی به نام بافرینگ است. تعریف ساده بافرینگ در عملیات ورودی و خروجی این است که با تعریف کردن یک حافظه واسطه به نام بافر، تعداد درخواست‌های برنامه به سیستم عامل و دیسک را برای انجام عملیات ورودی یا خروجی کم کنیم.

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

دو کلاس BufferedInputStream و BufferedOutputStream برای بافرینگ در Byte Stream ها و دو کلاس BufferedReader و BufferedWriter برای بافرینگ در Character Stream ها طراحی شده‌اند.

مثال: می‌خواهیم محتوای فایلی که در مثال قبل در آن یک متن را نوشتیم را با BufferedReader بخوانیم:

ابتدا شیئی از کلاس BufferedReader ایجاد کردیم به سازنده آن یک FileReader دادیم تا این بافر یک FileReader را در بر بگیرد. سپس با متد readLine که در کلاس BufferedReader وجود دارد یک خط از محتوای فایل را خواندیم و آن را چاپ کردیم.

طرز کار دیگر کلاس‌های بافرینگ دیگر هم به همین شکل است که باید به سازنده آن‌ها یک شی از استریمی که قصد بافر کردن آن را دارید بدهید.

سخن آخر

در این قسمت شما با کلاس‌های زیر آشنا شدید:

InputStream

OutputStream

FileInputStream

FileOutputStream

Writer

Reader

FileWriter

همانطور که در ابتدا گفتیم پکیج java.io بزرگ و شامل کلاس‌های زیادی است و مجال توضیح دادن تک تک کلاس‌های آن وجود ندارد. حتی در جامع‌ترین منابع آموزشی جاوا نیز به آموزش تعداد محدودی از کلاس‌های IO بسنده کرده‌اند.

از طرفی شما به عنوان برنامه نویس جاوا باید بتوانید به مراجع و مستندات رسمی API جاوا مراجعه کنید و کلاس‌های مورد نیاز خود را مطالعه کنید و با طرز کار آن‌ها آشنا شوید. مطمئن باشید هیچ برنامه‌نویس جاوایی وجود ندارد که تمام API جاوا را حفظ باشد! یکی از مهارت‌های لازم برای هر برنامه‌نویس توانایی یافتن مطلب درباره موضوع مورد نیاز خود و یافتن پاسخ برای مشکلاتی که حین برنامه‌نویسی ممکن است پیش بیاید است.

برای مشاهده API نسخه 8 جاوا به این لینک مراجعه کنید. البته به دلیل تحریم ایران توسط شرکت اوراکل باید از روش های خاص برای دسترسی به این سایت استفاده کنید!

تصویر زیر به درک بهتر طرز کار مستندات جاوا کمک می‌کند:

api

مصطفی نصیری

دانشجوی نرم افزار هستم و علاقه شدیدی به برنامه نویسی مخصوصا با زبان جاوا دارم! در حال حاضر تمرکزم روی اندرویده. دوست دارم چیزایی که یاد میگیرم رو با بقیه به اشتراک بگذارم :)

همچنین ممکن است دوست داشته باشید ...

۳ واکنش

  1. جواد گفت:

    سلام دوست عزیز
    ممنون از آموزشهای خوبتون.
    اگر برنامه نویسی آندروید رو هم بزارید ممنون میشم.ضمناً اگه ممکنه ایمیل و یا ID تلگرامتون رو جهت مکاتبه در اختیار ما قرار بدین . با تشکر از تمام زحماتی که برای ما میکشید.

  2. در صورت امکان روش پورت کردن برنامه های iOS به ویندوز هم اموزش بدین
    ممنون

پاسخ دهید

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