第十章 Java基本I/O

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是串流的二個節點.

stream-2

家裏要用水就要接水管, 要用瓦斯就要接瓦斯管.
在串流內流動的資料分二種, 一種為byte, 一種為char.
傳輸byte時的管子叫InputStream/OutputStream
傳輸char時的管子叫Reader/Writer

byte streamio_byte

char streamio_char

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

將水管, 水塔, 瀘水器等連接起來, 生活用水才會便利, 對吧. 如果家裏沒裝水塔, 那停水了不就不用洗澡了嗎

為了高效能及彈性, 資料流也必需串連起來.

stream

上面第一個圖, 先使用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

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *