2007年8月7日 星期二

Java 學習筆記 (8) - I/O

I/O 是個相當大的主題,若要深入探討, 包括各式設備的 I/O,可以寫厚厚的一本書,不過在這邊只是學習入門而已,因此就圍繞在檔案 I/O 部分,以下就慢慢來說明。

檔案

首先必須瞭解在 Java 中,是如何表示檔案的。

File class
在不同的 OS 中,檔案路徑的表示法也大不相同,例如:
  • Windows => D:\Software\Free
  • Unix-Like => /var/log/messages

因此在實作上就必須要額外多注意些了! 以下用個範例簡單說明 File class 的使用方式:
package net.twcic;

import java.io.File;
import java.util.ArrayList;

public class FileDemo {
public static void main(String args[]) {
try {
File file = new File("D:\");
if(file.isFile()) {
System.out.println("D:\ 檔案");
System.out.print((file.canRead() ? "可讀" : "不可讀") + " ");
System.out.print((file.canWrite() ? "可寫" : "不可寫") + " ");
System.out.println(file.length() + "位元組");
}
else {
//列出所有檔案與目錄
File[] files = file.listFiles();
ArrayList<File> fileList = new ArrayList<File>();

for(int i = 0 ; i < files.length ; i++) {
if(files[i].isDirectory()) //若是目錄就直接列出
System.out.println("[" + files[i].getPath() + "]");
else
fileList.add(files[i]); //檔案先存入 ArrayList,等會列出
}

for(File f : fileList)
System.out.println(f.toString());
System.out.println();
}
}
catch(ArrayIndexOutOfBoundsException e) {
System.out.println("using: Java FileDemo pathname");
}
}
}
上述範例僅是檔案、目錄名稱的讀取,而若要實際上處理檔案的 I/O,還是必須搭配其他的 class 來使用才行。

RandomAccessFile class

一般來說,檔案都是以循序的方式進行存取,但有時候會必須要針對檔案中的某個區段進行修改,也就是要能夠進行隨機存取(random access),此時就可以使用 java.io.RandomAccessFile class 中的 seek() method 來處理。

跟 C 相同,seek() 並不會回傳任何資料,而是將 file pointer 指向指定的位置上。而在 C 中,每筆資料可以透過 structure 的方式設定其大小、結構....等等資訊,而在 Java 中則可以透過定義 class 的方式達到設定資料結構的目的。

以下使用範例來說明:

Student.java
package net.twcic;

public class Student {

private String name;
private int score;

public Student() {
setName("noname");
}

public Student(String name, int score) {
setName(name);
this.score = score;
}

public void setName(String name) {
StringBuilder builder = null;
if(name != null)
builder = new StringBuilder(name);
else
builder = new StringBuilder(15);

builder.setLength(15);
this.name = builder.toString();
}

public void setScore(int score) {
this.score = score;
}

public String getName() {
return name;
}

public int getScore() {
return score;
}
public static int size() {
return 34;
}
}
RandomAccessFileDemo.java
package net.twcic;

import java.io.IOException;
import java.io.File;
import java.io.RandomAccessFile;
import java.util.Scanner;

public class RandomAccessFileDemo {
public static void main(String args[]) {
Student[] students = {
new Student("godleon", 95),
new Student("leon", 90),
new Student("bill", 85),
new Student("john", 80)
};

try {
//開啟檔案,並指定讀寫方式
File file = new File("myStudents.list");
RandomAccessFile raf = new RandomAccessFile(file, "rw");

for(int i = 0 ; i < students.length ; i++) {
//使用 RandomAccessFile class 所提供的寫入方式
raf.writeChars(students[i].getName());
raf.writeInt(students[i].getScore());
}

Scanner scanner = new Scanner(System.in);
System.out.print("讀取第幾筆資料? ");
int num = scanner.nextInt();
raf.seek((num-1) * Student.size());

//使用 RandomAccessFile class 所提供的資料讀取方式
Student student = new Student();
student.setName(readName(raf));
student.setScore(raf.readInt());

System.out.println("姓名:" + student.getName());
System.out.println("分數:" + student.getScore());

raf.close(); //關閉檔案
}
catch(ArrayIndexOutOfBoundsException e) {
System.out.println("請指定檔案名稱");
}
catch(IOException e) {
e.printStackTrace();
}
}

private static String readName(RandomAccessFile raf) throws IOException {
char[] name = new char[15]; //姓名長度為15
for(int i = 0 ; i < name.length ; i++)
name[i] = raf.readChar();
return (new String(name)).replace('0', ' ');
}
}
在上述範例中,有以下幾個重點:
  1. 開啟檔案,並指定讀寫的方式
  2. 如何使用 class 中所提供的寫入與讀取方法
  3. 使用完後必須關閉檔案


位元串流

Java 將資料在來源與目的地之間的流動,抽象化為一個 stream(串流),而 stream 所流動的則是以 byte 為單位的資料。

而在 Java SE 中有兩個 class 用來作為 stream 的抽象表示,分別是 java.io.InputStream(例如:System.in)以及 java.io.OutputStream(例如:System.out),以下用簡單的程式碼來示範用 in 物件與 out 物件讀取及輸出 stream 資料:
package net.twcic;

import java.io.IOException;

public class StreamDemo {
public static void main(String args[]) {
try {
System.out.print("輸入字元:");
System.out.println("輸入字元十進位表示:" + System.in.read());
}
catch(IOException e) {
e.printStackTrace();
}
}
}

FileInputStream & FileOuputStream

由名稱即可知道,這兩個 class 繼承了 java.io.InputStream 以及 java.io.OutputStream 是專門處理檔案之用的,當產生 FileInputStream 或是 FileOutputSteam object 時,就表示 stream 已經被開啟,而使用完之後務必記得關閉 stream,以免消耗不必要的系統資源,以下用範例程式說明:
package net.twcic;

import java.io.IOException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class FileStreamDemo {
public static void main(String args[]) {
try {
byte[] buffer = new byte[1024]; //作為緩衝區之用

//定義來源目的檔案
String srcFileName = "srcMyFile";
String dstFileName = "dstMyFile";

FileInputStream input = new FileInputStream(new File(srcFileName));
FileOutputStream output = new FileOutputStream(new File(dstFileName));

System.out.println("複製檔案:" + input.available() + " bytes.");
while(true) {
if(input.available() < 1024) {
int remain = -1;
while((remain = input.read()) != -1) //每次 read() 取得 1 byte 的資料
output.write(remain); //因為是 btye,所以可以用 int 表示
break;
}
else {
//使用 buffer,一次寫入長度為 1024 byets 的 stream
input.read(buffer);
output.write(buffer);
}
}

//關閉 stream
input.close();
output.close();

System.out.println("複製完成");
}
catch(ArrayIndexOutOfBoundsException e) {
System.out.println("using: java FileStreamDemo src dst");
}
catch(IOException e) {
e.printStackTrace();
}
}
}

BufferedInputStream & BufferedOutputStream

在上一段的例子中,使用了一個 byte array 作為 buffer,但由於這是檔案存取,因此利用硬碟作為 buffer 遠比使用實體記憶體作為 buffer 速度要慢的多,因此 J2SE 中提供了 java.io.BufferedInputStream 以及 java.io.BufferedOutputStream 來協助處理需要記憶體作為 buffer 的工作。

需要注意的是,若要使用 BufferedInputStream 的功能,必須指定一個 InputStream 供其使用;同樣的,要使用 BufferedOutputStream,也要指定 OutputStream 供其使用。以下用個範例程式來說明:
package net.twcic;

import java.io.IOException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;

public class BufferedStreamDemo {
public static void main(String args[]) {
try {
byte[] data = new byte[1];

//定義來源目的檔案
File srcFile = new File("srcMyFile");
File dstFile = new File("dstMyFile");

BufferedInputStream bufInput = new BufferedInputStream(new FileInputStream(srcFile));
BufferedOutputStream bufOutput = new BufferedOutputStream(new FileOutputStream(dstFile));

System.out.println("複製檔案:" + srcFile.length() + " bytes");
while(bufInput.read(data) != -1)
bufOutput.write(data);
bufOutput.flush(); //將緩衝區資料全部寫出,確保 buffer 中的資料完全寫入檔案中

//關閉 stream
bufInput.close();
bufOutput.close();

System.out.println("複製完成");
}
catch(ArrayIndexOutOfBoundsException e) {
System.out.println("using: java UseFileStream src dst.");
}
catch(IOException e) {
e.printStackTrace();
}
}
}
DataInputStream & DataOutputStream

這段要介紹的是 Java SE 中提供用來讀寫基本型態(Primitive Type)資料用的 class,分別是 java.io.DataInputStream 以及 java.io.DataOutputStream,其中提供了用來處理各種型態資料的 method,因此不論在不同的平台上,都不需要考慮到型態的大小,以下用範例程式來說明:

Member.java
package net.twcic;

public class Member {
private String name;
private int age;

public Member() {
}

public Member(String name, int age) {
this.name = name;
this.age = age;
}

public void setName(String name) {
this.name = name;
}

public void setAge(int age) {
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}
}
DataStreamDemo.java
package net.twcic;

import java.io.IOException;
import java.io.FileOutputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.DataInputStream;

public class DataStreamDemo {
public static void main(String args[]) {
Member[] members = {
new Member("游錫堃", 30),
new Member("逃喆", 20),
new Member("比爾", 30),
new Member("許功蓋碁", 40)
};

try {
//宣告 DataOutputStream,必須內含 FileOutputStream object
DataOutputStream dataOutput = new DataOutputStream(new FileOutputStream("myMemberList.txt."));
for(Member m : members) {
dataOutput.writeUTF(m.getName());
dataOutput.writeInt(m.getAge());
}
dataOutput.flush(); //將緩衝區資料全部倒出至檔案
dataOutput.close(); //關閉 stream

//宣告 DataInputStream,必須內含 FileInputStream object
DataInputStream dataInput = new DataInputStream(new FileInputStream("myMemberList.txt"));
for(int i = 0 ; i < members.length ; i++) {
String name = dataInput.readUTF();
int age = dataInput.readInt();
members[i] = new Member(name, age);
}
dataInput.close(); //關閉 stream

for(Member m : members)
System.out.println(m.getName() + " : " + m.getAge());
}
catch(IOException e) {
e.printStackTrace();
}
}
}

ObjectInputStream & ObjectOutputStream

在 Java 中,許多資料都是以 object 的方式呈現,而 Java SE 中還提供將 object 直接存入檔案中的功能,而未來需要用到時,也提供從檔案中還原出 object 的功能。而提供這些功能的,則是 java.io.ObjectInputStream 以及 java.io.ObjectOutputStream

而必須注意的是,此兩個 class 所使用的方式是將 object 序列化後,再存入檔案,因此要存入檔案的 object,其 class 就必須實作 Serializable interface,此 interface 並沒有任何 method 需要實作,僅僅是宣告此 class 所產生的 object 是可以序列化的。以下用範例程式來說明:

User.java
package net.twcic;

import java.io.Serializable;

public class User implements Serializable {
//用來維持版本一致之用,以免發生 java.io.InvalidClassException
private static final long serialVersionUID = 1L;

private String name;
private int number;

public User() {
}

public User(String name, int number) {
this.name = name;
this.number = number;
}

public void setName(String name) {
this.name = name;
}

public void setNumber(int number) {
this.number = number;
}

public String getName() {
return name;
}

public int getNumber() {
return number;
}
}
ObjectStreamDemo.java
package net.twcic;

import java.io.IOException;
import java.io.FileNotFoundException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;
import java.util.ArrayList;

public class ObjectStreamDemo {
public static void main(String args[]) {
User[] users = {
new User("godleon", 12),
new User("Leon", 22)
};

String strFileName = "objFile.txt";
writeObjectToFile(users, strFileName);

try {
users = readObjectFromFile(strFileName);
for(User user : users)
System.out.println(user.getName() + "t" + user.getNumber());
System.out.println();

users = new User[2];
users[0] = new User("Bill", 1);
users[1] = new User("Mary", 33);
appendObjectToFile(users, strFileName);
users = readObjectFromFile(strFileName);
for(User user : users)
System.out.println(user.getName() + "t" + user.getNumber());
}
catch(FileNotFoundException e) {
e.printStackTrace();
}
}

//將 object 寫入檔案
public static void writeObjectToFile(Object[] objs, String filename) {
try {
//宣告 ObjectOutputStream,且必須代入 FileOutputStream object
ObjectOutputStream objOutput = new ObjectOutputStream(new FileOutputStream(new File(filename)));
for(Object obj : objs)
objOutput.writeObject(obj); //將 serialize 後的 object 寫入檔案
objOutput.close(); //關閉 stream
}
catch(IOException e) {
e.printStackTrace();
}
}

//將 object 從檔案中讀出
public static User[] readObjectFromFile(String filename) throws FileNotFoundException {
File file = new File(filename);
if(!file.exists())
throw new FileNotFoundException();

//宣告 List 作為 object container
List<User> list = new ArrayList<User>();
try {
FileInputStream fileInput = new FileInputStream(file);
//宣告 ObjectInputStream,且必須代入 FileInputStream object
ObjectInputStream objInput = new ObjectInputStream(fileInput);
while(fileInput.available() > 0) //檢查檔案中的 object 是否讀取完
list.add((User)objInput.readObject());
objInput.close(); //關閉 stream
}
catch(ClassNotFoundException e) {
e.printStackTrace();
}
catch(IOException e) {
e.printStackTrace();
}

//將讀取出來的 List 轉為物件
User[] users = new User[list.size()];
return list.toArray(users);
}

//將 object 附加到檔案中
public static void appendObjectToFile(Object[] objs, String filename) throws FileNotFoundException {
File file = new File(filename);
if(!file.exists())
throw new FileNotFoundException();

try {
//要以附加的方式加入資料到檔案中,必須設定第二個參數為 true
ObjectOutputStream objOutput = new ObjectOutputStream(new FileOutputStream(file, true)) {
//override writeStreamHeader(),避免 header 不一致的情形發生
protected void writeStreamHeader() throws IOException {}
};

for(Object obj : objs)
objOutput.writeObject(obj);
objOutput.close();
}
catch(IOException e) {
e.printStackTrace();
}
}
其中有幾個重點是必須注意的:
  1. 在 class 中宣告 serialVersionUID 的目的是為了維持版本訊息的一致
  2. 讀寫 object 的方法分別是 ObjectInputStream.readObject() 與 ObjectOutputStream.writeObject()
  3. 若檔案被附加(append)多次,就會包含多個 stream header,進行讀取檢查的時候會發生 java.io.StreamCorruptedException,為了解決這個問題,必須重新定義 ObjectOutputStream.writeStreamHeader()

PrintStream

之前介紹的 OuputStream 物件,都是直接將 memory 中的資料寫至目的地,而若是想要顯示純資料的話,就必須使用 PrintStream class,以下用個範例來說明:
package net.twcic;

import java.io.IOException;
import java.io.File;
import java.io.PrintStream;
import java.io.FileOutputStream;

public class StreamTest {
public static void main(String args[]) throws IOException {
//使用 FileOutputStream
FileOutputStream fileOutput = new FileOutputStream(new File("streamTest.txt"));
fileOutput.write(1); //檔案中的資料為記憶體中的資料
fileOutput.close();

//使用 PrintStream
PrintStream print = new PrintStream(new FileOutputStream(new File("printStreamTest.txt")));
print.println(1); //為正常的純文字資料
print.close();
}
}

ByteArrayInputStream & ByteArrayOutputStream

stream 的來源或目的不一定是檔案,也可以是記憶體空間,例如這部分要說明的 java.io.ByteArrayInputStream 以及 java.io.ByteArrayOutputStream 即是使用 byte array 作為 stream 的來源與目的。以下用一個範例來說明:
package net.twcic;

import java.io.*;
import java.util.Scanner;

public class ByteArrayStreamDemo {
public static void main(String args[]) {
try {
File file = new File("ByteArrayStream.txt");
BufferedInputStream bufInput = new BufferedInputStream(new FileInputStream(file));
ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();

byte[] bytes = new byte[1];
while(bufInput.read(bytes) != -1) //使用 BufferedInputStream 讀取檔案資料
byteOutput.write(bytes); //將資料寫入 byteArrayOutputStream
byteOutput.close();
bufInput.close();

//以字元的方式顯示 ByteArray 內容
bytes = byteOutput.toByteArray();
for(int i = 0 ; i < bytes.length ; i++)
System.out.print((char)bytes[i]);
System.out.println();

Scanner scanner = new Scanner(System.in);
System.out.print("輸入修改位置:");
int pos = scanner.nextInt();
System.out.print("輸入修改字元:");
bytes[pos-1] = (byte)scanner.next().charAt(0); //修改陣列中的字元

//將 byte array 中的內容存回檔案
ByteArrayInputStream byteInput = new ByteArrayInputStream(bytes);
BufferedOutputStream bufOutput = new BufferedOutputStream(new FileOutputStream(file));
byte[] tmp = new byte[1];
while(byteInput.read(tmp) != -1) //從 byte stream 中取得資料並存入 tmp 中
bufOutput.write(tmp); //將 tmp 中的資料寫入檔案
byteInput.close();
bufOutput.flush();
bufOutput.close();
}
catch(ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
}
catch(IOException e) {
e.printStackTrace();
}
}
}


字元串流

有別於位元串流,字元串流是以一個字元(2 bytes)的長度為單位進行處理,並進行適當的字元編碼轉換,而 J2SDK 中則提供了 java.io.Reader 以及 java.io.Writer 作為字元串流的處理(即為純文字檔案的處理)。

由於 java.io.Reader 以及 java.io.Writer 皆為 abstract class,因此真正提供處理功能的都是其 sub class,以下就逐一介紹比較常用的幾個 class。

InputStreamReader & OutputStreamWriter

若要使用 InputStream 以及 OutputStream 進行字元處理,可使用 java.io.InputStreamReader 以及 java.io.OutputStreamWriter 來加上字元處理的功能,如此一來就不需要自行判斷字元的編碼(InputStream 以及 OutputStream 一次僅能處理 1 byte 長度的資料)。

以下用個簡單的範例來說明:
package net.twcic;

import java.io.*;

public class StreamReaderWriterDemo {
public static void main(String args[]) {
try {
//設定檔案為 UTF-8 編碼
InputStreamReader streamReader = new InputStreamReader(new FileInputStream("ReaderWriterText.txt"), "UTF-8");
OutputStreamWriter streamWriter = new OutputStreamWriter(new FileOutputStream("ReaderWriterText_bak.txt"), "UTF-8");

int ch = 0;
while((ch = streamReader.read()) != -1) { //讀取字元
System.out.print((char)ch);
streamWriter.write(ch);
}
System.out.println();

streamReader.close();
streamWriter.close();
}
catch(IOException e) {
e.printStackTrace();
}
}
}
在編碼的部分,可以參考官方文件說明

FileReader & FileWriter

若要存取的是純文字檔案,可以直接使用 java.io.FileReader 以及 java.io.FileWriter 兩個 class 進行處理,不過僅能使用系統預設編碼,若要指定其他編碼,則還是必須使用 java.io.InputStreamReader 以及 java.io.OutputStreamWriter,然而使用方法上則是大同小異。

BufferedReader & BufferedWriter

java.io.BufferedReader 與 java.io.BufferedWriter 在讀取文字檔案時,會先將資料讀入 buffer 中,而使用 read() method 時是先從 buffer 將資料取出,除非等到 buffer 資料取光時,才會到檔案中再度取資料進 buffer 中存放。而透過 buffer 的方式,可以減少磁碟 I/O 的動作,因此效率會較好。

以下有一段官方文件說明:
Without buffering, each invocation of read() or readLine() could cause bytes to be read from the file, converted into characters, and then returned, which can be very inefficient.
接著用個範例來說明:
package net.twcic;

import java.io.*;

public class BufferedReaderWriterDemo {
public static void main(String args[]) {
try {
//緩衝 System.in 輸入 stream
BufferedReader bufReader = new BufferedReader(new InputStreamReader(System.in));
//緩衝 FileWriter 字元輸出 stream
BufferedWriter bufWriter = new BufferedWriter(new FileWriter("bufWriter.txt"));

//每讀一行進行一次寫入動作
String input = null;
while(!(input = bufReader.readLine()).equals("quit")) {
bufWriter.write(input);
bufWriter.newLine();
}
bufReader.close();
bufWriter.close();

}
catch(IOException e) {
e.printStackTrace();
}
}
}

PrintWriter

此 class 與之前介紹過的 java.io.PrintStream 功能是類似的,都可將 Java 的 primitive type 的資料直接轉換為系統預設編碼下的對應字元,然後輸出至 OutputStream 中;而 java.io.PrintWriter 則還可以接受 Writer object 作為參數。以下用個範例示範 PrintStreamPrintWriter 的用法:
package net.twcic;

import java.io.*;

public class StreamWriterDemo {
public static void main(String args[]) {
try {
byte[] sim = {
(byte)0xbc, (byte)0xf2,
(byte)0xcc, (byte)0xe5,
(byte)0xd6, (byte)0xd0,
(byte)0xce, (byte)0xc4
};

//資料來源
ByteArrayInputStream byteInput = new ByteArrayInputStream(sim);
InputStreamReader inputReader = new InputStreamReader(byteInput, "GB2312");

//使用 PrintWriter
PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(new FileOutputStream("printWriter.txt"), "GB2312"));
int in = 0;
printWriter.write("PrintWriter:");
while((in = inputReader.read()) != -1)
printWriter.print((char)in);
printWriter.println();
printWriter.close();

//使用 PrintStream
byteInput.reset();
PrintStream printStream = new PrintStream(new FileOutputStream("printWriter.txt", true), true, "GB2312");
printStream.print("PrintStream:");
while((in = inputReader.read()) != -1)
printStream.print((char)in);
printStream.println();
printStream.close();
}
catch(IOException e) {
e.printStackTrace();
}
}
}

CharArrayReader & CharArrayWriter

與之前介紹的 java.io.ByteArrayInputStreamjava.io.ByteArrayOutputStream 類似,只是這邊所介紹的 java.io.CharArrayReaderjava.io.CharArrayWriter 是將字元陣列作為資料輸入來源與輸出目的,用來處理 ASCII 與各種不同非西歐國家語系文字混雜的檔案很合適。以下用個範例來說明:
package net.twcic;

import java.io.*;
import java.util.*;

public class CharArrayReaderWriterDemo {
public static void main(String args[]) {
try {
File file = new File("CharArray.txt");

BufferedReader bufReader = new BufferedReader(new FileReader(file));
CharArrayWriter charWriter = new CharArrayWriter();
char[] array = new char[1];
while(bufReader.read(array) != -1)
charWriter.write(array); //將 array 中的資料寫入 buffer
charWriter.close();
bufReader.close();

array = charWriter.toCharArray();
for(char ch : array)
System.out.print(ch);
System.out.println();
}
catch(IOException e) {
e.printStackTrace();
}
}
}

沒有留言:

張貼留言