05 Java IO

IO部分的知識比較零散,而且一般是配合其它程序來共同完成功能的。爲了清楚起見,筆者還是按照知識點逐一列舉代碼,並寫一些自己的評論。

1. 文件類File

文件類File是用來表示文件或者目錄路徑名的類,在新建、刪除、重命名文件,新建、刪除、查找目錄,以及建立與文件或者目錄有關的流時都會用到。

1.1 創建新文件

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        File f = new File("F:" + File.separator + "test.txt");
        f.createNewFile();
    }
}
這裏的文件路徑名沒有像視頻中使用\\分隔符,而是用File.separator這個File類中的一個靜態屬性字符串來作爲分隔符。這樣可以避免分隔符不兼容問題,而且路徑分層更清晰一些。createNewFile()方法要特別注意,只有調用它的File類對象的路徑名指向的文件不存在時,纔會創建新文件。已有的文件不會被替換。

1.2 刪除文件/目錄

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        //File f = new File("F:" + File.separator + "dir_to_be_deleted");
        File f = new File("F:" + File.separator + "test.txt");
        if(f.exists())
            f.delete();
        else
            System.out.println("File not found.");
    }
}
delete()方法不僅可以刪除文件,也可以刪除目錄,不過這個目錄必須是空的。這樣做可以減少目錄被誤刪除的可能性,但是也像後面例子裏表示的,使得刪除目錄需要先遞歸刪除文件,使得操作複雜化。

1.3 創建目錄

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException 
    {
        File f = new File("F:" + File.separator + "dir_to_be_built");
        f.mkdir();
    }
}

1.4 列出目錄中的下級文件和目錄

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        File f = new File("F:" + File.separator);
        File[] str = f.listFiles();
        for (int i = 0; i < str.length; i++) 
        {
            System.out.println(str[i]);
        }
    }
}
listFiles()返回一個File類型的數組,數組元素就是要查找的下級文件和目錄名。如果調用這個方法的文件類對象f使用的是絕對路徑,則返回的也是絕對路徑,否則是相對路徑。

1.5 列出目錄全部內容

這裏需要用到遞歸,程序遇到目錄時需要遞歸地查找其下級文件和目錄。
import java.io.*;
class Test
{
    public static void main(String[] args) 
    {
        File f = new File("F:" + File.separator);
    }
    //遞歸開始
    public static void print(File f)
    {
        if(f != null)
        {
            if(f.isDirectory())     //判斷爲目錄則遞歸查找所有下級文件和目錄
            {
                File[] fileArr = f.listFiles();
                if(fileArr != null)
                {
                    for (int i = 0; i < fileArr.length; i++) 
                    {
                        //遞歸調用
                        print(fileArr[i]);
                    }
                }
            }
            else
            {
                System.out.println(f);
            }
        }
    }
}
遞歸時要特別注意排除引用爲空的情況。當發生IO錯誤時listFiles()方法會返回null值。這裏不能像前面程序在main方法上拋出異常,只能選擇性地忽略null值繼續查找直到所有文件和目錄都查找完畢。

2. 文件字符流類 FileReader/FileWriter

2.1 向文件寫入數據

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException 
    {
        //第二個參數默認爲false,覆蓋原文件。若爲true,追加在文件後。   
        File f = new File("F:" + File.separator + "test.txt");
        Writer out = new FileWriter(f);  //向下轉型
        String str = "hello";           
        out.write(str); //接收字符,字符數組或字符串作爲參數
        out.close();    //關閉流,將緩衝區內容輸出到文件
    }
}
如果使用FileWriter類的另一個構造方法,可以更簡便寫作:Writer out = new FileWriter("F:" + File.separator + "test.txt");

2.2 從文件中讀取數據,無緩衝區

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        Reader r = new FileReader("F:" + File.separator + "test" + File.separator + "test2.java");
        int c;
        //read方法返回當前讀到的單個字符的ASCII碼,用int型表示
        while((c = r.read()) !=  - 1)       //讀到 - 1表示結尾
        {
            System.out.print((char) c);     //顯示字符需要將ASCII碼轉換回字符類型
        }
        r.close();
    }
}

2.3 從文件中讀取數據,有緩衝區

class Test
{
    public static void main(String[] args) throws IOException
    {
        Reader r = new FileReader("F:" + File.separator + "test" + File.separator + "test2.java");
        char[] buf = new char[100]; //讀緩衝區
        int len = 0;
        //read方法在有數據寫入緩衝區時返回寫入數據的大小,沒有數據寫入緩衝區時(說明已到末尾)返回 -1
        while((len = r.read(buf)) !=  - 1)  
        {
            System.out.print(new String(buf, 0, len));
            System.out.println(len);
        }
        r.close();
    }
}
2.2和2.3兩個例子涉及了兩個重載的read方法。無參read方法返回的是當前讀到的字符的ASCII碼,-1爲結尾。有參read方法接收緩衝區引用作爲參數,返回的是當次讀取所佔緩衝區的大小,也是 -1爲結尾。爲提高效率,應儘量用緩衝區。

3. 文件字節流類FileInputStream/FileOutputStream

3.1 向文件寫入數據

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        OutputStream o = new FileOutputStream("F:" + File.separator + "test.txt");  //加上第二個參數true則會追加,否則覆蓋
        String str = "text";
        byte[] arr = str.getBytes();
        o.write(arr[0]);
        o.write(str.getBytes());    //接收字節或字節數組作爲參數
        o.flush();                  //將寫緩衝區內容輸出到文件
        o.close();
    }
}

3.2 從文件中讀取數據,無緩衝區

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        InputStream in = new FileInputStream("F:" + File.separator + "test.txt");
        int Byte = 0;
        while((Byte = in.read()) !=  - 1)   //無參read方法返回int型數據來表示當前讀到的字節
        {
            System.out.print((char)Byte);   //如果是文本,字節數據是字符的ASCII碼
        }
        in.close();
    }
}

3.3 從文件中讀取數據,有緩衝區

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        InputStream in = new FileInputStream("F:" + File.separator + "test.txt");
        byte[] buf = new byte[256];         //字節數組作緩衝區
        int len = 0;                    
        while((len = in.read(buf)) !=  - 1) //有參read方法返回讀完當前緩衝區後讀取的長度
        {
            System.out.print(new String(buf, 0, len));
        }
        in.close();
    }
}

3. 字符流與字節流的比較

字節流一次讀入或輸出的是一個字節的數據,8位,而字符流是兩個字節16位。字符流的輸出需要用到緩衝區,所以在close()方法前要用到flush()方法,否則輸出不完全。而字節流的輸出不需要緩衝區,因而使用字節流輸出字節數據是即時的。字節流可以用來讀取硬盤中所有類型的文件,而字符流只能用於文本文件。所以在讀寫文件時要根據文件類型來決定使用哪一種流。

4. 轉換流InputStreamReader/OutputStreamWriter

上面總結的文件字符流/字節流都是程序與文件直接交互傳輸時用到的流,成爲節點流。另一類流並不直接與文件接觸,而是像包裝一樣包住文件流,對其進行處理並利用自身與程序交互。包裝流有很多類,包括轉換流,緩衝流,數據操作流,合併流,Object流等。
轉換流是溝通字符流和字節流的橋樑,使得我們可以將從硬盤/內存讀入的字節流轉換成易於處理的字符流,處理完畢後轉回字節流寫回硬盤/內存。

4.1 輸出字符流轉換爲字節流

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        //轉換流作爲文件字節輸出流的包裝被賦值給字符輸出流
        Writer w = new OutputStreamWriter(new FileOutputStream("F:" + File.separator + "test.txt"));
        //後面的操作與字符輸出流相同
        String str = "text";
        w.write(str);
        w.flush();                
        w.close();
    }
}
這裏也用到了向下轉型。OutputStreamWriter是Writer類的子類。

4.2 輸入字節流轉換爲字符流

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        //轉換流作爲文件字節輸入流的包裝被賦值給字符輸入流
        Reader in = new InputStreamReader(new FileInputStream("F:" + File.separator + "test.txt"));
        char[] buf = new char[256];         //字符數組作緩衝區
        int len = 0;                    
        while((len = in.read(buf)) !=  - 1) //有參read方法返回讀完當前緩衝區後讀取的長度
        {
            System.out.print(new String(buf, 0, len));
        }
        in.close();
    }
}
InputStreamReader是Reader類的子類。

5. 緩衝流類BufferedReader, BufferedWriter, BufferedInputStream, BufferedOutputStream

爲了提高傳輸效率,可以使用四種緩衝流。緩衝流也是一種包裝流,用於連接程序與節點流。如果單純使用節點流,每次讀寫請求都會使得CPU中斷以便程序訪問硬盤,這樣效率是很低的。而使用緩衝流,可以利用緩衝流中的緩衝區進行批量讀寫,提高效率。

5.1 字符緩衝流

BufferedReader類用於字符輸入緩衝,繼承於Reader類。BufferedWriter類用於字符輸出緩衝,繼承於Writer類。所以可以用字符節點流的方法來操作字符緩衝流。
import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        //標準輸入流是字節流,需要先轉換成字符流再連接字符緩衝流
        BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
        //標準輸出流是字節流,需要先轉換成字符流再連接字符緩衝流
        BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(System.out));
        String str = null;
        while((str = bufr.readLine()) != null)  //一次讀取一行
        {
            if(str.equals("end"))   //從屏幕輸入時要自定義結束標誌
                break;
            bufw.write(str);        
            bufw.write("\n");       //或者用bufw.newLine()來輸入行分隔符
            bufw.flush();           
        }
        bufr.close();
        bufw.close();
    }
}
兩種字符緩衝流有8192字符的緩衝區。BufferedReader在讀取文本文件時,會先儘量從文件中讀入字符並置入緩衝區,之後調用read()方法,會先從緩衝區中讀取。如果緩衝區數據不足,纔會從文件中讀取。BufferedWriter在寫入文件時,會將數據先寫入緩衝區,緩衝區滿後再寫入到文件。

5.2 字節緩衝流

BufferedInputStream用於字節輸入緩衝,繼承於FilterInputStream類,後者繼承於InputStream類。BufferedOutputStream用於字節輸出緩衝,繼承於FilterOutputStream類,後者繼承於OutputStream類。所以可以用字節節點流的方法操作字節緩衝流。可以用節點流和緩衝流實現文件複製,比如複製一張圖片:
import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        //原文件
        File src = new File("E:" + File.separator + "src.jpg");
        //複製到
        File dest = new File("F:" + File.separator + "dest.jpg");
        //用字節輸入緩衝流包裝字節輸入節點流
        BufferedInputStream bufi = new BufferedInputStream(new FileInputStream(src));
        //用字節輸出緩衝流包裝字節輸出節點流
        BufferedOutputStream bufo = new BufferedOutputStream(new FileOutputStream(dest));
        //暫存讀到的字節
        int tmp = 0;
        //循環讀寫直到遇到文件結束標誌
        while((tmp = bufi.read()) !=  - 1)
        {
            bufo.write(tmp);
        }
        bufi.close();
        bufo.close();
    }
}

6.其它包裝流

6.1 數據操作流DataOutputStream, DataInputStream類

數據操作流可以以機器無關的方式讀寫基本類型的數據, 包括byte類型和byte數組類型。這兩個類直接繼承於Object類,故不可用字節流的方法操作,而是由專門的方法。
import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        //數據輸出流包裝了文件輸出流以便向其寫入基本類型數據
        DataOutputStream out = new DataOutputStream(new FileOutputStream("F:" + File.separator + "test.txt"));

        //用一個字節寫入boolean值
        boolean b = true;
        out.writeBoolean(b);
        //用一個字節寫入byte值
        byte B = 2;
        out.writeByte(B);
        //用兩個字節寫入char值
        char c = 'A';
        out.writeChar(c);
        //用四個字節寫入int值
        int i = 0;
        out.writeInt(i);
        //用四個字節寫入float值
        float f = 2.0f;
        out.writeFloat(f);
        //用八個字節寫入double值
        double d = 2.0;
        out.writeDouble(d);

        out.close();
    }
}
用DataInputStream包裝字節輸入節點流可以讀出這些基本類型數據:
import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        //數據輸入流包裝了文件輸入流以便從其讀入基本類型數據
        DataInputStream in = new DataInputStream(new FileInputStream("F:" + File.separator + "test.txt"));

        //用一個字節讀入boolean值
        boolean b = in.readBoolean();
        System.out.println(b);
        //用一個字節讀入byte值
        byte B = in.readByte();
        System.out.println(B);
        //用兩個字節讀入char值
        char c = in.readChar();
        System.out.println(c);
        //用四個字節讀入int值
        int i = in.readInt();
        System.out.println(i);
        //用四個字節讀入float值
        float f = in.readFloat();
        System.out.println(f);
        //用八個字節讀入double值
        double d = in.readDouble();
        System.out.println(d);

        in.close();
    }
}
輸出:
true
2
A
0
2.0
2.0

6.2 邏輯串聯流SequenceInputStream

該包裝流可以將有序集合中的多個節點輸入流按照順序合併組成新的輸入流。有續集合中如果只有不超過兩個節點輸入流,可以將它們在SequenceInputStream的構造方法中作爲參數指定,否則需要建立枚舉類去包含一個含有這些節點流的框架類,將枚舉類對象作爲參數輸入。
SequenceInputStream繼承於InputStream類,故可以使用read()等方法用串聯流讀取串聯後的數據:
import java.io.*;
import java.util.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        //創建串聯流
        SequenceInputStream sis = null;
        //創建輸出流
        BufferedOutputStream bufo = null;
        //創建Vector框架, 將要合併的節點輸入流加入框架中
        Vector<InputStream> v = new Vector<InputStream>();
        //新的流依次添加到Vector的末尾
        v.addElement(new FileInputStream("F:" + File.separator + "sub1.txt"));
        v.addElement(new FileInputStream("F:" + File.separator + "sub2.txt"));
        v.addElement(new FileInputStream("F:" + File.separator + "sub3.txt"));
        //返回Vector的枚舉給枚舉接口用於生成枚舉對象
        Enumeration<InputStream> e = v.elements();

        //串聯流來自於枚舉對象
        sis = new SequenceInputStream(e);
        //輸出流輸出到指定文件
        bufo = new BufferedOutputStream(new FileOutputStream("F:" + File.separator + "sum.txt"));

        //字節緩衝區
        byte[] buf = new byte[1024];
        //當前一次讀取的數據量
        int len = 0;
        //讀取合併後的流並寫入目標文件
        while((len = sis.read(buf)) !=  - 1)
        {
            bufo.write(buf, 0, len);
            bufo.flush();       //輸出緩衝區的內容
        }
    }
}

6.3 對象序列化流ObjectInputStream, ObjectOutputStream

對象序列化流可以實現對象和二進制數據流的互相轉化。當然這要求對象所屬類實現Serializable接口以便具有序列化的能力。注意Serializable接口沒有任何字段或方法,單純用作可序列化的標誌。
import java.io.*;
public class Test
{
    public static void main(String[] args) throws Exception
    {
        Student stu1 = new Student("Jack", 21);
        Student stu2 = new Student("Tom", 22);
        //序列化流包裝文件輸出流以便將序列化的Student類對象輸出到文件
        ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream("F:" + File.separator + "object.txt"));
        //輸出對象
        oo.writeObject(stu1);
        oo.writeObject(stu2);
        //關閉流
        oo.close();
        //序列化流包裝文件輸入流以便將序列化的Student類對象信息從文件讀入到對應引用
        ObjectInputStream oi = new ObjectInputStream(new FileInputStream("F:" + File.separator + "object.txt"));
        //讀入對象, readObject()方法返回Object類型,必須強制轉換
        Student r1 = (Student) oi.readObject();
        Student r2 = (Student) oi.readObject();
        //顯示讀入的內容
        System.out.println(r1);
        System.out.println(r2);
        //關閉流
        oi.close();
    }
}

class Student implements Serializable   //實現序列化接口,具有序列化能力
{
    String name = null;
    int age = 0;

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

    @Override
    public String toString()
    {
        return "name : " + name + ", age : " + age;
    }
}
輸出:
name : Jack, age : 21
name : Tom, age : 22

6.4 打印流PrintStream

PrintStream繼承於FilterOutputStream類,後者繼承於OutputStream類。我們經常用到的System.out.println()語句其實就是在取得out這個System類中的指向PrintStream對象的引用後,調用PrintStream類的println方法來實現的。默認情況下這個out引用指向的PrintStream對象會將流打印在屏幕上。我們可以修改out的指向,讓它指向一個我們自定義的PrintStream對象以便重定向標準輸出:
import java.io.*;
public class Test
{
    public static void main(String[] args) throws IOException
    {
        //此時輸出到屏幕
        System.out.println("screen");
        //out是個static類型的引用,只需修改一次, 將重定向目標作爲構造參數傳給PrintStream構造方法
        System.setOut(new PrintStream(new FileOutputStream("F:" + File.separator + "redirect.txt")));
        System.out.println("redirected to file");
    }
}

7. 總結

IO這部分的知識還是較好整理的,關鍵要掌握各個類之間的父子關係,也可以根據流的名字來推斷。另外就是要多比較易混淆的重名的方法,它們往往有重載關係,比如有參和無參的read()方法。另一個要注意的地方是異常處理,一定要拋出或者捕獲IOException。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章