Java I/O Fundamentals
目的
Java基本輸入輸出
自主控台讀取及寫入資料
stream讀取及寫入檔案
serialization讀取寫入物件
Java基本I/O
Java提供龐大的函數庫執行I/O功能. 將I/O channel定義為串流(stream). 串流可以代表不同的來源及目的, 包含磁碟檔案, 裝置, 其他程式, 記憶体陣列. 串流也支援不同種類的資料, 如bytes, 原生資料型態, 字元和物件.
File
File是收集檔案相關資訊的類別, 如建立檔名, 修改檔名, 設定屬性等.
public class FileTest { public static void main(String[] args) { File file1=new File("d:/testdir"); file1.mkdirs(); File file2=new File(file1, "test.txt"); try { file2.createNewFile(); System.out.println(file2+" 是否存在 : "+file2.exists()); } catch (IOException ex) { Logger.getLogger(FileTest.class.getName()).log(Level.SEVERE, null, ex); } File file3=new File("d:/mp3"); String[] files=file3.list(); for (String f:files){ if (new File(file3, f).isFile()) System.out.println(file1+"\\"+f); } }
mkdir():建立目錄, 但父目錄不存在, 則不會建立
mkdirs() : 建立目錄, 父目錄不存在則一並建立
createNameFile() : 建立檔案
exists() : 檢查檔案是否存在
isDirectory() : 檢查是否為目錄
isFile() : 檢查是否為檔案
list(): 傳回目錄下的檔案及目錄
在Windows系統下, 路徑的分隔可用 “\\” 或 “/”
在Linux系統下, 路徑分隔只能用 “/”
FilenameFilter
FilenameFilter為一介面, 需實作accept()方法, 再將產生的物件置於list()方法內
String[] files=file3.list(new FilenameFilter(){ @Override public boolean accept(File dir, String name) { return name.endsWith("mp3"); } });
I/O串流
程式使用輸入串流從來源地讀取資料. 使用輸出串流將資料寫出. 不需理會串流裏面是如何運作的, 只需注意簡單的使用方法. 串流取得的資料是有順序性.
資料來源串流(Data source stream)初始化資料流, 稱Input Stream 或Reader, 資料終端串流(Data sink stream)終止資料流, 稱為Output Stream或Writer. Sources及sink是串流的二個節點.
家裏要用水就要接水管, 要用瓦斯就要接瓦斯管.
在串流內流動的資料分二種, 一種為byte, 一種為char.
傳輸byte時的管子叫InputStream/OutputStream
傳輸char時的管子叫Reader/Writer
byte stream
char stream
InputStream
InputStream以read()讀取單一byte資料或一連串byte
int read() : 傳回讀取到的byte資料, 或 -1代表結束
read(byte[] b) : 讀取資料到b中, 並傳回讀取的byte 數
read(byte[] b, int off, int len) : 指定要放入buffer的位置及長度
特別注意一下, Java SE7使用AutoCloseable實作InputStream, 也就是說, 在try-catch內使用InputStream, 會自動關閉串流
其他方法包含
void close()
int available() : 回報可讀取的長度(byte)
long skip(long n) : 放棄讀取 n byte的串流資料
boolean markSupported() : 若串流有支援mark()及reset(), 則返回true
void mark(int readlimit) : 標註可重讀的地方, 再用reset()回標註點重讀
void reset() : 若沒有mark(), 則reset()無作用
OutputStream
OutputStream以write()寫入一byte資料或一連串byte
write(int b)
write(byte[] b)
write(byte[] b, int off, int len)
其他方法
void close()
void flush()
Reader
int read() : 返回讀到的Unicode 字元, 或 -1代表結束
read(char[] buff)
read(char[] buff, int off, int len)
其他方法
void close()
boolean ready()
long skip(long n)
boolean markSupported()
void mark(int readAheadLimit)
void reset()
Writer
write(int c)
write(char buff)
write(String str)
write(char[] buff, int off, int len)
write(String str, int off, int len)
其他方法
void close()
void flush()
FileInputStream/FileOutputStream
繼承InputStream/OutputStream, 傳輸byte資料. 開啟檔案若不存在, 會自動建立檔案
FileOutputStream(File file, boolean append) , 若append為true, 表示要將資料寫入原始檔案中即有資料之後. 若為false, 則會清除原始的檔案, 重新寫入
public class ByteStreamCopyTest { public static void main(String[] args) { byte[] buffer=new byte[128]; try(FileInputStream fis=new FileInputStream("d:\\test1.srt"); FileOutputStream fos=new FileOutputStream("d:\\test2.txt")){ int count=0;int read=0; System.out.printf("可讀取byte : %d\n", fis.available()); while((read=fis.read(buffer)) != -1){ if(read<buffer.length)fos.write(buffer, 0, read); else fos.write(buffer); count+=read; } System.out.printf("己寫入byte : %d\n", count); } catch (IOException ex) { Logger.getLogger(ByteStreamCopyTest.class.getName()).log(Level.SEVERE, null, ex); } } }
BufferedInputStream/BufferedOutputStream
public static void main(String[] args) { byte[]buffer=new byte[128]; try(BufferedInputStream bis=new BufferedInputStream(new FileInputStream("c:/thomas/a.txt")); BufferedOutputStream bos=new BufferedOutputStream(new FileOutputStream("c:/thomas/b.txt"))){ System.out.printf("可讀取 : %d\n", bis.available()); int count=0, read=0; while((read=bis.read(buffer))!=-1){ for (int i=0;i<read;i++){ System.out.printf("%02x ", buffer[i]); } } } catch(IOException e){}
FileReader/FileWriter
FileReader/FileWriter分別是InputStreamReader/OutputStreamWriter的子類別, 而這二個又是Reader/Writer的子類別.
可用於傳輸char資料
public class CharStreamCopyTest { public static void main(String[] args) { char []cbuf=new char[128]; try(FileReader fr=new FileReader("d:\\test1.srt"); FileWriter fw=new FileWriter("d:\\test2.txt")){ int count=0, read=0; while((read=fr.read(cbuf))!=-1){ if(read<cbuf.length)fw.write(cbuf, 0, read); else fw.write(cbuf); count +=read; } System.out.printf("寫入字數 : %d\n", count); } catch (IOException ex) { Logger.getLogger(CharStreamCopyTest.class.getName()).log(Level.SEVERE, null, ex); } } }
以上只能讀取utf-8之中文檔, 若是unicode, 則會出現亂碼
Processing Streams
自來水跟瓦斯好比是資料流的byte, char. 家裏不是只有水管(inputStream)而以, 還有水塔(BufferedInputStream), 化糞池(BufferedOutputStream), 瀘水器(FilterInputStream), 分歧器(三向接頭, DataInputStream), 臉盆(PrintStream), 瓦斯爐PrintWriter)
程式中暫存或資料處理的Stream物件.
Functionality | Character Streams | Byte Streams |
Buffering(Strings) | BufferedReader BufferedWriter |
BufferedInputStream BufferedOutputStream |
Filtering | FilterReader FilterWriter |
FilterInputStream FilterOutputStream |
Conversion(byte to character) | InputStreamReader OutputStreamWriter |
|
Object serialization | ObjectInputStream ObjectOutputStream |
|
Data conversion | DataInputStream DataOutputStream |
|
Counting | LineNumberReader | LineNumberInputStream |
Peeking ahead | PushbackReader | PushbackInputStream |
Printing | PrintWriter | printStream |
在上面的表中, Filtering比較特殊, 其建構子都是protected的. 也就是說在其他的package中, 除非繼承Filtering, 否則不能new 出物件.
為什麼要這麼作呢, 因為Filtering是給Java SE系統開發其他類別用的.
InputStream<–FilterInputStream<–DataInputStream/BufferedInputStream
OutputStream<–FilterOutputStream<–DataOutputStream/BufferedInputStream/PrintStream
Reader<–FilterReader<–PushbackReader
Writer<–FilterWriter
DataInput/DataOut介面
DataInputStream繼承了FilterInputStream, 也實作了DataInput介面可以讀取不同的資料, 比如readByte(), readChar(), readDouble(), readFloat(), readInt(), readLong(), readShort()
DataOutputStream同上
資料串流鏈結-I/O Stream Chaining
將水管, 水塔, 瀘水器等連接起來, 生活用水才會便利, 對吧. 如果家裏沒裝水塔, 那停水了不就不用洗澡了嗎
為了高效能及彈性, 資料流也必需串連起來.
上面第一個圖, 先使用FileStream開啟檔案, 再傳入buffer Stream增快效能, 再用DataInputStream轉成資料
BufferReader/BufferWriter
要取水, 直跟跟水塔要比較快也比較方便. 所以針對BufferedReader/BufferedWriter下執讀寫, 是最有效率的.
當然, 也有BufferedInputStream/BufferedOutputStream
另外, 關閉檔案, 也請以Buffered的close 為主. 使用FileReader的close, 會造成錯誤
public class BufferedStreamCopyTest { public static void main(String[] args) { try(BufferedReader bufInput=new BufferedReader(new FileReader("d:\\test1.srt")); BufferedWriter bufOutput=new BufferedWriter(new FileWriter("d:\\test2.txt"))){ String line=""; while((line=bufInput.readLine())!=null){ bufOutput.write(line); bufOutput.newLine(); } } catch (IOException ex) { Logger.getLogger(BufferedStreamCopyTest.class.getName()).log(Level.SEVERE, null, ex); } } }
上述使用BufferedReader連結FileReader, 再由BufferedReader使用readLine()傳入String物件中
InputStreamReader/OutputStreamWriter
這二個是作資料的轉換
InputStreamReader : byte 轉char
OutputStreamWriter : char 轉 byte
建構子為
new InputStreamReader(InputStream, “ISO8859_1”);
new OutputStreamWriter(OutputStream, “ISO8859_1”);
FileReader/FileWriter即為這二個的子類別
Console I/O
java.lang package中的System 類別, 有三個static fields : out, in, err.
System.in : 指向InputStream物件, 用於讀取標準輸入, 通常指向鍵盤
System.out : 指向PrintStream物件, 用於寫入標準輸出, 通常指向螢幕
System.err : 指向PrintStream物件, 用於寫入標準錯誤, 通常指向螢幕, 用於輸出錯誤訊息
通常使用InputStreamReader(System.in)建立一條通道, 再將此通道放入BufferedReader()之中即可
public class InputStremTest {
public static void main(String[] args) throws IOException {
InputStreamReader isr=new InputStreamReader(System.in);
BufferedReader br=new BufferedReader(isr);
PrintWriter pw=new PrintWriter(System.out, true);
String s="";
while(!s.equals("quit")){
pw.print("請輸入字串 : ");
pw.flush();
s=br.readLine();
pw.println("輸入的結果是"+s);
}
}
}
上述使用InputStreamReader將System.in的byte轉成char, 再使用PrintWriter印出
java.io.Console
System類別也可以存取java.io.Console物件, 如下例
public class ConsoleTest { public static void main(String[] args) { Console cons=System.console(); if(cons!=null){ String userName, pwd; while(true){ userName=cons.readLine("%s", "User name : "); if(userName.equals("exit"))break; pwd=new String(cons.readPassword("%s", "Password : ")); System.out.printf("User Name : %s, Password : %s\n", userName, pwd); } } } }
因為NetBeans沒有Console, 所以上述程式無法在NetBeans內執行, 需使用命令列才能執行
Channel I/O
FileChannel是將整個檔案一次讀進跟寫出的機制, 在Java 1.4就己出現. 使用方法為
利用FileInputStream().getChannel()方法, 產生FileChannel的物件.
整備一大塊的buffer : ByteBuffer buff=ByteBuffer.allocate(size)
然後再用FileChannel的物件方法read(buff), 讀入buff裏
public class ByteChannelCopyTest { public static void main(String[] args) { try(FileChannel fcIn=new FileInputStream("d:\\test1.srt").getChannel(); FileChannel fcOut=new FileOutputStream("d:\\test2.txt").getChannel()){ ByteBuffer buff=ByteBuffer.allocate((int)fcIn.size()); fcIn.read(buff); buff.position(0); fcOut.write(buff); } catch (IOException ex) { Logger.getLogger(ByteChannelCopyTest.class.getName()).log(Level.SEVERE, null, ex); } } }
以上可以讀取UniCode的檔案, 不過檔案如果太大, 可能就會讀到掛掉吧
持久化(Persistence)
把資料存在永久的裝置叫Persistence, 如硬碟. 物件是可以使用 persistence的, 非persistence物件只存在runtime時期. 序列化(serialization是Java的標準機制, 可以有順序的使用 byte在儲存, 並於之後重建複制物件. 要達此目的, 類別需implement java.io.Serializable介面
序列化及物件(Serialization and Object Graphs)
當物件序列化, 只保留fields, 若fields參考一個物件, 則此物件也必需是序列化
Transient Fields and Objects
當類別implement Serializable介面後,裏面的Fields都是可以被ObjectOutputStream存在硬碟的檔案裏的. 若此時某個Field不想被儲存時(如password), 就可以在此Field前加上transient
public class TransientTest implements Serializable{ public transient FileInputStream inputFile; public static int BASE=100; //static field是不能被序列化的 private transient int totalValue=10; protected Stock[] stocks;//Stock也必需被序列化 }
Fields的存取修飾子不影響序列化
static fields的變數不能序列化
區域變數不能被序列化
方法也不能被序列化
Serial Version UID
序列化時, 可以使用 private static final long serialVersionUID = 8294180014912103005L 來作核對, 當解序列化時, 若Serial Version UID不對, 就會產生InvalidClassException
若沒宣告serial version UID, 則於執行時期, 系統會自動產生, 但建議要自己宣告
public class TransientTest{ public static void main(String[] args) { boolean saveFlag=false; if(saveFlag){ Pikachu p = new Pikachu(); p.setName("皮卡丘99號"); p.setPasswd("123456"); try { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/obj.txt")); oos.writeObject(p); oos.flush(); oos.close(); } catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} System.out.printf("name : %s\n",p.getName()); System.out.printf("password : %s\n", p.getPasswd()); System.out.printf("count : %s\n", Pikachu.count); System.out.println("Saved successful"); } else{ Pikachu p=null; try { ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/obj.txt")); p = (Pikachu) ois.readObject(); ois.close(); } catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();} if(p!=null){ System.out.printf("name : %s\n",p.getName()); System.out.printf("password : %s\n", p.getPasswd()); System.out.printf("count : %s\n", Pikachu.count); } } } } class Pikachu implements Serializable { private static final long serialVersionUID = 8294180014912103005L; private String name; private transient String passwd; public static int count; public String getName() {return name;} public void setName(String s) {this.name = s;} public String getPasswd() {return passwd;} public void setPasswd(String s) {this.passwd = s;} public Pikachu(){ count++; } }
序列化方法(Serialization Methods)
當序列化物件要寫入時, writeObject()方法會被執行, 若沒有此方法, 則會執行defaultWriteObject().
當物件被解序列化時(就是被讀入時), readObject()方法就會被執行, 沒此方法就會執行defaultReadObject()
這些方法, 都是private