مقایسه ی کلاس های Text-IO با Binary-IO
تمام فایل هایی که در حافظه ذخیره میشن از نوع باینری هستند.
توجه
مطالب این قسمت زبان کاتلین را نیز پوشش می دهند و مثال هایی که زده شده با هردو زبان جاوا و کاتلین است.
برای کامپیوتر فرقی نداره یک فایل از چه نوعی (متنی، تصویری یا...) باشه؛ تمام فایل ها در کامپیوتر به صورت باینری ذخیره میشن؛ تنها چیزی که یک فایل متنی رو با باینری در جاوا متمایز میکنه رمزنگاری و رمزگشایی کاراکترهای (حروف) فایل متنی است.
هنگامی که بخوایم یک کاراکتر رو داخل فایل ذخیره کنیم ابتدا کاراکتر به کد متناظر خودش تبدیل میشه و سپس این کد که یک عدد است توسط کلاس مربوطه در فایل نوشته میشه. به این فرایند رمزنگاری (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 در جاوا هستند.
در نمودار زیر رابطه ی بین کلاس هایی که قراره در این مطلب بهشون پرداخته بشه رو نشون دادیم.
همونطور که مشاهده کردید تمام کلاس ها در تصویر از کلاس های InputStream و OutputStream ارث بری میکنن، این دو کلاس ابسترکت هستند و ازشون نمیتونیم شی سازی کنیم و فقط برای ارث بری سایر کلاس ها ساخته شده اند.
کلاس InputStream سوپر کلاس تمام کلاس های Input و کلاس OutputStream سوپر کلاس تمام کلاسهای Output در پکیج io است.
دو نمودار UML زیر متد (تابع) های تعریف شده در این دو کلاس رو نشون میدن.
کلاس FileInputStream و FileOutputStream
دو کلاس FileInputStream و FileOutputStream به ترتیب از کلاس های InputStream و OutputStream ارث بری کردن در ادامه کانستراکتور های تعریف شده در این دو کلاس رو بررسی میکنیم.
نمودار UML مربوط به FileInputStream در جاوا
نمودار UML مربوط به FileOutputStream در جاوا
در مثال زیر میخوایم یک فایل به نام 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 فقط یک کانستراکتور تعریف شده که به ترتیب مقادیری از جنس InputStream و OutputStream رو باید به صورت پارامتر بهشون پاس بدیم.
بررسی متد های اینترفیس DataInput
بررسی متد های اینترفیس DataOutput
در زیر میخوایم مقادیری رو با DataOutputStream در فایل بنویسیم و با DataInputStream همان مقادیر رو بخونیم.
public static void main(String[] args) throws IOException{
try(DataOutputStream output = new DataOutputStream(new FileOutputStream("temp.dat"))){
output.writeUTF("John");
output.writeDouble(45.5);
output.writeUTF("Emily");
output.writeDouble(50.2);
output.writeUTF("Joseph");
output.writeDouble(82.0);
}
try(DataInputStream input = new DataInputStream(new FileInputStream("temp.dat"))){
while (input.available() > 0) {
System.out.println("Name: " + input.readUTF() + " Score: " + input.readDouble());
}
}
}
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 میتونیم به ترتیب مقادیری از جنس InputStream و OutputStream رو به کانستراکتور هاشون پاس بدیم.
اینترفیس های ObjectInput و ObjectOutput به ترتیب از اینترفیس های DataInput و DataOutput ارث بری میکنن و تمام متد های این دو اینترفیس رو به ارث می برن.
در اینترفیس ObjectInput علاوه بر متد های اینترفیس DataInput که به ارث می بره یک متد به اسم readObject تعریف شده است.
در اینترفیس ObjectOutput علاوه بر متد های اینترفیس DataOutput که به ارث می بره یک متد به اسم writeObject تعریف شده است.
توجه
آبجکتی که میخوایم داخل فایل بنویسیم باید Serializable باشه در غیر این صورت دچار اکسپشن میشیم.
اینترفیس Serializable متدی برای پیادهسازی نداره و صرفا یک برچسبه که به جاوا میگیم آبجکت های کلاس مورد نظر قابل نوشتن در فایل هستند.
کلاس java.util.Date اینترفیس Serializable رو پیادهسازی کرده است. در مثال زیر آبجکت های کلاس Date رو داخل فایل می نویسیم و سپس با ObjectInputStream از فایل میخونیمشون.
public static void main(String[] args) throws IOException, ClassNotFoundException{
//نوشتن داده ها
try(ObjectOutput output = new ObjectOutputStream(new FileOutputStream("temp.dat"))){
output.writeUTF("John");
output.writeDouble(45.5);
output.writeObject(new Date());
output.writeUTF("Emily");
output.writeDouble(50.2);
output.writeObject(new Date());
output.writeUTF("Joseph");
output.writeDouble(82.0);
output.writeObject(new Date());
}
//خوندن داده ها
try(ObjectInputStream input = new ObjectInputStream(new FileInputStream("temp.dat"))){
while (input.available() > 0) {
System.out.println("Name: " + input.readUTF() + " Score: " + input.readDouble() + " Date: " + input.readObject());
}
}
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 پرداخته ایم:
در نمودار uml زیر به بررسی BufferedOutputStream پرداخته ایم:
در زیر با استفاده از 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 برای فایل هایی با حجم بالا مورد استفاده قرار میگیرند.