تمام فایل هایی که در حافظه ذخیره میشن از نوع باینری هستند.
توجه
مطالب این قسمت زبان کاتلین را نیز پوشش می دهند و مثال هایی که زده شده با هردو زبان جاوا و کاتلین است.
برای کامپیوتر فرقی نداره یک فایل از چه نوعی (متنی، تصویری یا...) باشه؛ تمام فایل ها در کامپیوتر به صورت باینری ذخیره میشن؛ تنها چیزی که یک فایل متنی رو با باینری در جاوا متمایز میکنه رمزنگاری و رمزگشایی کاراکترهای (حروف) فایل متنی است.
هنگامی که بخوایم یک کاراکتر رو داخل فایل ذخیره کنیم ابتدا کاراکتر به کد متناظر خودش تبدیل میشه و سپس این کد که یک عدد است توسط کلاس مربوطه در فایل نوشته میشه. به این فرایند رمزنگاری (encoding) کاراکتر میگیم.
با هربار نوشتن کاراکتر در فایل توسط کلاس هایی مانند PrintWriter در جاوا مشابه عملیات زیر اتفاق میوفته.
char ch = 'A';
//encoding A
int code = (int) ch;
System.out.println("The code for character A is " + code);
هنگامی که بخوایم یک کاراکتر رو از فایل متنی بخونیم ابتدا کد متناظر با کاراکتر در فایل متنی توسط برنامه خونده شده و سپس این کد توسط jvm به کاراکتر (حرف) مورد نظر تبدیل شده و نمایش داده میشه. به این فرایند رمزگشایی (decoding) کاراکتر میگیم.
با خوندن هر کاراکتر (حرف) از فایل توسط کلاس هایی مانند Scanner در جاوا مشابه عملیات زیر اتفاق میوفته.
int code = 66;
char ch = (char) code;
System.out.println("The character for code " + code + " is " + ch);
کلاس های binary-io سریع تر از کلاس های text-io هستند چون هنگام خوندن و نوشتن درگیر فرایند رمزگشایی و رمزنگاری نمیشن.
در جاوا کلاس های مختلفی برای عملیات io تعریف شده در ادامه ی این مطلب به چندتا از این کلاس ها می پردازیم.
کلاس های Binary-IO
کلاس های InputStream و OutputStream سوپرکلاس تمام کلاس های Stream در پکیج io در جاوا هستند.
در نمودار زیر رابطه ی بین کلاس هایی که قراره در این مطلب بهشون پرداخته بشه رو نشون دادیم.
سلسله مراتب ارث بری کلاس های Binary-IO در جاوا
همونطور که مشاهده کردید تمام کلاس ها در تصویر از کلاس های InputStream و OutputStream ارث بری میکنن، این دو کلاس ابسترکت هستند و ازشون نمیتونیم شی سازی کنیم و فقط برای ارث بری سایر کلاس ها ساخته شده اند.
کلاس InputStream سوپر کلاس تمام کلاس های Input و کلاس OutputStream سوپر کلاس تمام کلاسهای Output در پکیج io است.
دو نمودار UML زیر متد (تابع) های تعریف شده در این دو کلاس رو نشون میدن.
java.io.InputStream
read(): int
یک بایت از فایل رو میخونه و اگه به انتهای فایل برسه -1 رو بر میگردونه.
read(b: byte[]): int
به اندازه ی طول آرایه ی b از فایل بایت میخونه و تعداد بایت هایی که خونده رو به صورت integer برمیگردونه و اگه به اخر فایل برسه -1 رو بر میگردونه.
available(): int
میگه چندتا بایت باقی مونده و اگه به انتهای فایل رسیده باشیم مقدار 0 رو بر میگردونه.
skip(n: long): long
تعداد n بایت از فایل رو نادیده میگیره و تعداد نادیده گرفته شده رو بر میگردونه.
+close()
استریم فعلی رو میبنده و منابع درگیر رو آزاد میکنه استفاده از close در پایان کار ضروریه.
java.io.OutputStream
write(b: int): void
مقدار یک بایت رو در فایل مینویسه.
write(b: byte[]): void
بایت های داخل آرایه ی b رو در فایل می نویسه.
flush(): void
پس از پایان کار صدا زدن این متد توصیه میشه چون مقادیر باقی مونده ای که در buffer وجود دارند و ممکنه هنوز نوشته نشده باشن رو در فایل مینویسه.
+close()
استریم فعلی رو میبنده و منابع درگیر رو آزاد میکنه استفاده از close در پایان کار ضروریه.
کلاس FileInputStream و FileOutputStream
دو کلاس FileInputStream و FileOutputStream به ترتیب از کلاس های InputStream و OutputStream ارث بری کردن در ادامه کانستراکتور های تعریف شده در این دو کلاس رو بررسی میکنیم.
نمودار UML مربوط به FileInputStream در جاوا
java.io.FileInputStream
+FileInputStream(path: String)
یک نمونه از FileInputStream با پاس دادن String به کانستراکتور ایجاد میکنه؛ String مسیر فایل مورد نظر است.
+FileInputStream(file: File)
یک نمونه از FileInputStream با پاس دادن File به کانستراکتور ایجاد میکنه.
نمودار UML مربوط به FileOutputStream در جاوا
java.io.FileOutputStream
+FileOutputStream(path: String)
یک نمونه از FileOutputStream با پاس دادن String به کانستراکتور ایجاد میکنه؛ String مسیر فایل مورد نظر است.
+FileOutputStream(path: String, append: boolean)
یک نمونه از FileOutputStream با پاس دادن String و مقدار boolean به کانستراکتور ایجاد میکنه؛ String مسیر فایل مورد نظر است و اگه فایل از قبل وجود داشته باشه و مقدار boolean برابر با true باشه داده ها رو ادامه ی فایل می نویسه و اگه false باشه فایل رو حذف کرده و یک فایل جدید ایجاد میکنه.
+FileOutputStream(file: File)
یک نمونه از FileOutputStream با پاس دادن File به کانستراکتور ایجاد میکنه.
+FileOutputStream(file: File, append: boolean)
یک نمونه از FileOutputStream با پاس دادن File به کانستراکتور ایجاد میکنه. اگه فایل از قبل وجود داشته باشه و مقدار boolean برابر با true باشه داده ها رو ادامه ی فایل می نویسه و اگه false باشه فایل رو حذف کرده و یک فایل جدید ایجاد میکنه.
در مثال زیر میخوایم یک فایل به نام temp.dat با FileOutputStream ایجاد کنیم و سپس مقادیر بایت ها رو در فایل نوشته و در نهایت فایل رو با FileInputStream بخونیم.
نکته
تمام کلاس های io-stream در جاوا حامل checked-exception هستند یعنی باید حتما اکسپشن هاشون رو هندل کنیم اما در کاتلین checked-exception وجود نداره.
public static void main(String[] args) throws IOException{
try(FileOutputStream output = new FileOutputStream("temp.dat")) {
for (int i =0; i<10; i++)
output.write(i);
}
try(FileInputStream input = new FileInputStream("temp.dat")){
int value;
while ((value = input.read()) != -1){
System.out.print(value + " ");
}
}
}
fun main() {
val file = File("temp.dat")
val output = FileOutputStream(file)
for (i in 0 until 10) {
output.write(i)
}
output.close()
val input = FileInputStream(file)
var b: Int
do {
b = input.read()
print("$b ")
}while (b != -1)
output.close()
}
برای جلوگیری از دست رفتن داده ها و اطمینان از نوشته شدن تمام داده ها در فایل ضروریه که در پایان کار متد close رو صدا بزنیم اما در قسمت جاوا در مثال بالا این کار رو نکردیم. چرا؟
چون تمام کلاس های IO Stream در جاوا از AutoClosable ارث بری میکنن AutoCloseable مشخص میکنه که کلاس یک io-stream است پس باید متد close در پایان کار صدا زده بشه. بنابراین میتونیم از کلاس های io-stream در try-catch مانند بالا داخل پرانتز یک نمونه ایجاد کنیم.
به این نوع از try-catch، در جاوا try-with-resources گفته میشه. در این حالت سیستم پس از پایان کار به طور خودکار stream ایجاد شده داخل پرانتز رو می بنده و نیازی به صدا زدن close نداریم.
کلاس DataInputStream و DataOutputStream
وظیفه ی این کلاس ها نوشتن و خوندن مقادیر primitive و string داخل فایل است. در این دو کلاس به ترتیب اینترفیس های DataInput و DataOutput پیادهسازی شده است.
تصویر زیر ارث بری دو کلاس DataInputStream و DataOutputStream رو نشان می دهد.
ارث بری دو کلاس DataInputStream و DataOutputStream در جاوا
برای این کلاس های DataInputStream و DataOutputStream فقط یک کانستراکتور تعریف شده که به ترتیب مقادیری از جنس InputStream و OutputStream رو باید به صورت پارامتر بهشون پاس بدیم.
بررسی متد های اینترفیس DataInput
<<java.io.DataInput>>
+readBoolean(): boolean
مقدار boolean رو از فایل میخونه و برمیگردونه.
+readByte(): byte
مقدار byte رو از فایل میخونه و بر میگردونه.
+readChar(): char
مقدار char رو از فایل میخونه و بر میگردونه.
+readFloat(): float
مقدار float رو از فایل میخونه و بر میگردونه.
+readDouble(): double
مقدار double رو از فایل میخونه و بر میگردونه.
+readInt(): int
مقدار int رو از فایل میخونه و بر میگردونه.
+readLong(): long
مقدار long رو از فایل میخونه و بر میگردونه.
+readShort(): short
مقدار short رو از فایل میخونه و بر میگردونه.
+readLine(): String
مقدار String رو از فایل میخونه و بر میگردونه.
+readString(): String
مقدار String رو به صورت UTF از فایل میخونه و بر میگردونه.
بررسی متد های اینترفیس DataOutput
<<java.io.DataOutput>>
+writeBoolean(b: boolean): void
مقدار boolean رو داخل فایل می نویسه.
+writeChar(c: char): void
مقدار char رو داخل فایل می نویسه.
+writeFloat(f: float): void
مقدار float رو داخل فایل می نویسه.
+writeDouble(d: double): void
مقدار double رو داخل فایل می نویسه.
+writeInt(i: int): void
مقدار int رو داخل فایل می نویسه.
+writeLong(l: long): void
مقدار long رو داخل فایل می نویسه.
+writeShort(sh: short): void
مقدار short رو داخل فایل می نویسه.
+writeByte(b : byte): void
مقدار byte رو داخل فایل می نویسه.
+writeChars(s: String): void
مقدار String رو داخل فایل می نویسه.
+writeUTF(utf: String): void
مقدار String رو داخل به فرمت utf داخل فایل می نویسه.
در زیر میخوایم مقادیری رو با DataOutputStream در فایل بنویسیم و با DataInputStream همان مقادیر رو بخونیم.
fun main() {
val file = File("temp.dat")
val output = FileOutputStream(file)
val dataOutput = DataOutputStream(output)
dataOutput.writeUTF("John")
dataOutput.writeDouble(45.5)
dataOutput.writeUTF("Emily")
dataOutput.writeDouble(50.2)
dataOutput.writeUTF("Joseph")
dataOutput.writeDouble(82.0)
dataOutput.close()
val input = FileInputStream(file)
val dataInput = DataInputStream(input)
do {
println("name: ${dataInput.readUTF()} score: ${dataInput.readDouble()}")
}while (dataInput.available() > 0)
output.close()
}
کلاس ObjectInputStream و ObjectOutputStream
برای نوشتن آبجکت در فایل باید کلاس مربوط به آبجکت اینترفیس Serializable رو پیادهسازی کرده باشه.
این دو کلاس به ترتیب اینترفیس های ObjectInput و ObjectOutput رو پیادهسازی میکنن.
تصویر زیر نحوه ی ارث بری این دو کلاس رو نشون میده.
ارث بری دو کلاس ObjectInputStream و ObjectOutputStream در جاوا
همانطور که در تصویر مشاهده میکنید برای هردو کلاس یک کانستراکتور تعریف شده که هنگام نمونه سازی از ObjectInputStream و ObjectOutputStream میتونیم به ترتیب مقادیری از جنس InputStream و OutputStream رو به کانستراکتور هاشون پاس بدیم.
اینترفیس های ObjectInput و ObjectOutput به ترتیب از اینترفیس های DataInput و DataOutput ارث بری میکنن و تمام متد های این دو اینترفیس رو به ارث می برن.
در اینترفیس ObjectInput علاوه بر متد های اینترفیس DataInput که به ارث می بره یک متد به اسم readObject تعریف شده است.
<<java.io.ObjectInput>>
readObject(): Object
یک آبجکت رو از فایل میخونه.
در اینترفیس ObjectOutput علاوه بر متد های اینترفیس DataOutput که به ارث می بره یک متد به اسم writeObject تعریف شده است.
<<java.io.ObjectOutput>>
writeObject(object: Object): void
یک آبجکت رو داخل فایل می نویسه.
توجه
آبجکتی که میخوایم داخل فایل بنویسیم باید Serializable باشه در غیر این صورت دچار اکسپشن میشیم.
اینترفیس Serializable متدی برای پیادهسازی نداره و صرفا یک برچسبه که به جاوا میگیم آبجکت های کلاس مورد نظر قابل نوشتن در فایل هستند.
کلاس java.util.Date اینترفیس Serializable رو پیادهسازی کرده است. در مثال زیر آبجکت های کلاس Date رو داخل فایل می نویسیم و سپس با ObjectInputStream از فایل میخونیمشون.
fun main() {
val file = File("temp.dat")
//نوشتن داده ها
val output = FileOutputStream(file)
val objectOutput = ObjectOutputStream(output)
objectOutput.writeUTF("John")
objectOutput.writeDouble(45.5)
objectOutput.writeObject(Date())
objectOutput.writeUTF("Emily")
objectOutput.writeDouble(50.2)
objectOutput.writeObject(Date())
objectOutput.writeUTF("Joseph")
objectOutput.writeDouble(82.0)
objectOutput.writeObject(Date())
objectOutput.close()
//خوندن داده ها
val input = FileInputStream(file)
val objectInput = ObjectInputStream(input)
do {
println("Name: ${objectInput.readUTF()} Score: ${objectInput.readDouble()} Date: ${objectInput.readObject()}")
}while (objectInput.available() > 0)
objectInput.close()
}
کلاس BufferedInputStream و BufferedOutputStream
از BufferedInputStream و BufferedOutputStream برای خوندن یا نوشتن فایل هایی با حجم بیش از 100 مگابایت استفاده میکنیم.
هر بار که عملیات خوندن یا نوشتن انجام میشه، کلاس های IO Stream برای خوندن یا نوشتن، کرنل سیستم عامل رو صدا میزنن و از طریق کرنل و سایر اجزای سیستم عامل عملیات خوندن یا نوشتن رو انجام میدن؛ برای همین این کار سنگین و هزینه بره.
کلاس های BufferedInputStream و BufferedOutputStream دارای حافظه های موقتی به اسم buffer هستند که با هر بار خوندن یا نوشتن بهجای اینکه مقادیر مستقیم توسط اجزای سیستم عامل خونده یا نوشته بشه ابتدا داخل حافظه ی buffer این کلاس ها ذخیره میشه و زمانی که این حافظه پر شد داده ها یه جا توسط سیستم عامل خونده یا نوشته میشن، بنابراین با این کلاس ها میتونیم عملیات خوندن و نوشتن رو بهینه تر کنیم.
حافظه ی پیشفرض buffer در این کلاس ها ۵۱۲ بایت است؛ میتونیم داخل کد این حافظه رو تغییر داده و به مقدار دلخواه خود تنظیم کنیم.
در نمودار uml زیر به بررسی BufferedInputStream پرداخته ایم:
java.io.BufferedInputStream
+BufferedInputStream(in: InputStream)
یک نمونه از BufferedInputStream با حافظه ی buffer پیش فرض 512 بایت ایجاد میکنه.
یک نمونه از BufferedOutputStream با حافظه ی buffer دلخواه ایجاد میکنه.
در زیر با استفاده از BufferedInputStream یک صد عدد تصادفی ۰ - ۲۵۵ (یک بایت) رو در فایل ذخیره میکنیم و سپس با BufferedInputStream اونا رو میخونیم.
public static void main(String[] args) throws IOException {
try(BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream("temp.dat"))){
for (int i =0; i<100; i++){
output.write((int)(Math.random() * 255));
}
}
int numberOfBytesRead = 0;
try(BufferedInputStream input = new BufferedInputStream(new FileInputStream("temp.dat"))) {
int b;
while ((b = input.read()) != -1){
numberOfBytesRead ++;
System.out.printf("%4d", b);
if (numberOfBytesRead % 10 == 0)
System.out.println();
}
}
}
fun main() {
val file = File("temp.dat")
val output = FileOutputStream(file)
val bufferedOutput = BufferedOutputStream(output)
for (i in 0 until 100){
val data = Random.nextInt(0, 256)
bufferedOutput.write(data)
}
bufferedOutput.close()
val input = FileInputStream(file)
val bufferedInput = BufferedInputStream(input)
var numberOfBytesPerLine = 0
do {
numberOfBytesPerLine++
print("${bufferedInput.read()} ")
if (numberOfBytesPerLine % 10 == 0) {
println()
}
}while (bufferedInput.available() > 0)
bufferedInput.close()
}
مورد مطالعه (مثال)
مثال زیر یک برنامه ی کاربردی است که از طریق کنسول (terminal یا cmd) یک فایل رو کپی میکنه.
public static void main(String[] args) {
if (args.length != 2) {
System.out.println("Usage: java Copy source target");
System.exit(1);
}
File source = new File(args[0]);
if (!source.exists()) {
System.out.println("Source file " + args[0] + " does not exist");
System.exit(2);
}
File target = new File(args[1]);
if (target.exists()) {
System.out.println("Target file " + args[1] + " already exists");
System.exit(3);
}
int b, numberOfBytesCopied = 0;
try(
BufferedInputStream input = new BufferedInputStream(new FileInputStream(source));
BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(target));
) {
while ((b = input.read()) != -1) {
output.write((byte) b);
numberOfBytesCopied++;
}
}catch (IOException e){
e.fillInStackTrace();
}finally {
System.out.println("Copy ok!" + numberOfBytesCopied + " bytes copied");
}
}
fun main(args: Array<String>) {
if (args.size != 2){
println("Usage: java Copy sourceFile target file")
exitProcess(1)
}
val source = File(args[0])
if (!source.exists()){
println("Source file " + args[0] + " does not exist")
exitProcess(2)
}
val target = File(args[1])
if (target.exists()){
println("Target file " + args[1] + " already exists")
exitProcess(3)
}
val input = BufferedInputStream(FileInputStream(args[0]))
val output = BufferedOutputStream(FileOutputStream(args[1]))
var numberOfBytesCopied: Int = 0
var b: Int
do {
b = input.read()
if (b != -1) {
output.write(b)
numberOfBytesCopied++
}
}while (b != -1)
input.close()
output.close()
println("Copy ok!$numberOfBytesCopied bytes copied")
}
خلاصه
- تمام فایل ها به صورت باینری در کامپیوتر ذخیره میشن.
- فایل های متنی کدگذاری شده هستند که هر کد مختص به یک حرف است.
- به فرایند تبدیل کاراکتر (حرف) به کد متناظر خودش رمزنگاری (encoding) میگیم
- به فرایند تبدیل کد به کاراکتر متناظر خودش رمزگشایی (decoding) میگیم.
- خوندن و نوشتن به صورت باینری سریع تر از متنی است چون عملیات مستقیم و بدون رمزنگار یا رمزگشایی انجام میشه.
- تمام کلاس های استریم پیکیج io در جاوا از دو کلاس InputStream و OutputStream ارث بری میکنن.
- کلاس FileInputStream برای خوندن بایت های یک فایل و کلاس FileOutputStream برای نوشتن بایت ها در فایل مورد استفاده قرار میگیرن.
- از دو کلاس DataInputStream و DataOutputStream برای خوندن و نوشتن مقادیر primitive استفاده میکنیم.
- دو کلاس ObjectInputStream و ObjectOutputStream علاوه بر مقادیر primitive آبجکت هایی که Serializable هستند رو نیز برای خوندن و نوشتن پشتیبانی میکنن.
- دو کلاس BufferedInputStream و BufferedOutputStream برای فایل هایی با حجم بالا مورد استفاده قرار میگیرند.