1. 程式人生 > >Java IO操作詳解

Java IO操作詳解

在Java程式設計中,IO(輸入輸出)是重要的組成部分,Java應用常常需要從外界輸入資料或者把資料輸出到外界。

Java IO的核心用一句話概括:抽象類或介面之中的抽象方法會根據例項化子類的不同,會完成不同的功能。所有的IO操作都在java.io包之中進行定義,而且整個java.io包實際上就是五個類和一個介面:
(1)五個類:File、InputStream、OutputStream、Reader、Wirter;
(2)一個介面:Serializable。

一、檔案操作類File

在整個java.io包之中,File類是唯一的一個與檔案本身操作有關的類,所謂的檔案本身指的是:檔案的建立、刪除、重新命名、取得檔案大小、修改日期。

1、檔案類物件例項化

如果要想使用File類操作檔案的話,那麼肯定要通過構造方法例項化File類物件,而例項化File類物件的過程之中主要使用以下兩種構造方法:

(1)public File(String pathname);//主要在Java EE的開發之中

(2)public File(File parent, String child);//主要在Android開發之中

2、檔案的基本操作

檔案的基本操作,主要有兩種功能:建立和刪除
(1)建立檔案:

public boolean createNewFile() throws IOException;

(2)刪除檔案:

public
boolean delete();

範例:

import java.io.File;

public class TestDemo {
    public static void main(String[] args) throws Exception {
        File file = new File("D:\\demo.txt"); // 檔案的路徑

        if (file.exists()) { // 檔案存在
            file.delete(); // 刪除檔案
        } else { // 檔案不存在
            file.createNewFile(); // 建立新檔案
} } }

本程式操作就表示檔案如果存在則刪除,如果不存在,則建立一個新的檔案,此時基本功能是實現了,不過這個程式此時卻存在問題:

(1)關於路徑分隔符

在windows作業系統之中,使用“\”作為路徑分隔符,而在linux系統下使用“/”作為路徑的分隔符,而從實際的開發而言,大部分情況下都會在windows中做開發,而後將專案部署到linux下,那麼此時,路徑的分隔符都需要進行修改,這樣實在是過於麻煩,為此在File類之中提供了一個常量:

public static final String separator

(按照Java的命名規範來講,對於全域性常量應該使用大寫字母的方式定義,而此處使用的是小寫,是由Java的發展歷史所帶來的問題)。

所以以上程式例項化File類物件修改為:

 File file = new File("D:" + File.separator + "demo.txt"); // 檔案的路徑

(2)之前進行檔案建立的時候都是在根路徑下建立完成的,如果說現在要建立的檔案有目錄呢?
例如,現在要建立一個d:\hellodemo\my\test\demo.txt檔案,而此時在執行程式的時候hellodemo目錄不存在,這個時候執行的話就會出現錯誤提示:

Exception in thread "main" java.io.IOException: 系統找不到指定的路徑。

因為現在目錄不存在,所以不能建立,那麼這個時候必須要首先判斷要建立檔案的父路徑是否存在,如果不存在應該建立一個目錄,之後再進行檔案的建立,而要想完成這樣的操作,需要以下幾個方法的支援:
①找到一個指定檔案的父路徑:

public File getParentFile();

②建立目錄:

public boolean mkdirs();

範例:

import java.io.File;
public class TestDemo {
    public static void main(String[] args) throws Exception {
        File file = new File("D:" + File.separator + "hellodemo"
                + File.separator + "my" + File.separator + "test"
                + File.separator + "demo.txt"); // 檔案的路徑
        if (!file.getParentFile().exists()) { // 父路徑不存在
            file.getParentFile().mkdirs(); // 建立目錄
        }
        if (file.exists()) { // 檔案存在
            file.delete(); // 刪除檔案
        } else { // 檔案不存在
            file.createNewFile(); // 建立新檔案
        }
    }
}

以後在任何的java.io.File類開發的過程之中,都一定要考慮檔案目錄的問題。

3、獲取檔案的基本資訊

除了以上的常用的方法之外,在File類之中還可以通過以下的方法取得一些檔案的基本資訊:

(1)取得檔案的名稱:

public String getName();

(2)給定的路徑是否是資料夾:

public boolean isDirectory();

(3)給定的路徑是否是檔案:

public boolean isFile();

(4)是否是隱藏檔案:

public boolean isHidden();

(5)檔案的最後一次修改日期:

public long lastModified();

(6)取得檔案大小:

public long length();

是以位元組為單位返回的。

二、位元組流與字元流

使用File類執行的所有操作都是針對於檔案本身,但是卻沒有針對於檔案的內容,而要進行檔案內容操作就需要通過Java之中提供的兩組類完成:

(1)位元組操作流:OutputStream、InputStream;
(2)字元操作流:Writer、Reader。

但是不管是位元組流還是字元流的操作,本身都表示資源操作,而執行所有的資源操作都會按照如下的幾個步驟進行,下面以檔案操作為例(對檔案進行讀、寫操作):

(1)如果要操作的是檔案,那麼首先要通過File類物件找到一個要操作的檔案路徑(路徑有可能存在,有可能不存在,如果不存在,則要建立路徑);
(2)通過位元組流或字元流的子類為位元組流或字元流的物件例項化(向上轉型);
(3)執行讀 / 寫操作;
(4) 最後一定要關閉操作的資源(資料流屬於資源操作,資源操作必須關閉)

1、位元組流

OutputStream和InputStream是位元組流的兩個頂層父類。讓他們提供了輸出流類和輸入流類通用API,位元組流一般用於讀寫二進位制資料,如影象和聲音資料。

[1]輸出流:OutputStream

java.io.OutputStream主要的功能是進行位元組資料的輸出的,這個類的定義如下:

public abstract class OutputStream
extends Object
implements Closeable, Flushable

發現OutputStream類定義的時候實現了兩個介面:Closeable、Flushable,這兩個介面的定義如下:

(1)Closeable (JDK 1.5推出):

public interface Closeable extends AutoCloseable {
    public void close() throws IOException;
}

(2)Flushable(JDK 1.5推出):

public interface Flushable {
    public void flush() throws IOException;
}

對於OutputStream類而言發現其本身定義的是一個抽象類(abstract class),按照抽象類的使用原則來講,需要定義抽象類的子類,而現在如果要執行的是檔案操作,則可以使用FileOutputStream子類完成,如果按照面向物件的開發原則,子類要為抽象類進行物件的例項化,而後呼叫的方法以父類中定義的方法為主,而具體的實現找例項化這個父類的子類完成,也就是說在整個的操作之中,使用者最關心的只有子類的構造方法:

(1)例項化FileOutputStream(新建資料):

public FileOutputStream(File file) throws FileNotFoundException;

(2)例項化FileOutputStream(追加資料):

public FileOutputStream(File file, boolean append)
throws FileNotFoundException

當取得了OutputStream類的例項化物件之後,下面肯定要進行輸出操作,在OutputStream類之中定義了三個方法:

(1)輸出單個位元組資料:

public abstract void write(int b) throws IOException;

(2)輸出一組位元組資料:

public void write(byte[] b) throws IOException;

(3)輸出部分位元組資料:

public void write(byte[] b, int off, int len) throws IOException;

範例:使用OutputStream向檔案之中輸出資料,輸出路徑:d:\demo\test.txt

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;

public class TestDemo {
    public static void main(String[] args) throws Exception {
        //第一步:定義檔案路徑
        File file = new File("D:"+File.separator + "demo"+ File.separator + "test.txt");

        if(!file.getParentFile().exists()){
            file.getParentFile().mkdirs();
        }

        //第二步:例項化輸出流
        OutputStream output = new FileOutputStream(file);

        String data = "hello world !\r\nhello world !\r\nhello world !\r\nhello world !";

        // 第三步:輸出資料,要將資料變為位元組陣列輸出
        output.write(data.getBytes());

        //第四步:關閉資源
        output.close();
    }
}

執行結果:

1

在整個的檔案輸出過程之中可以發現,如果現在要輸出的檔案不存在,那麼會出現自動建立檔案的情況,並且如果重複執行以上的程式碼,會出現新的內容覆蓋掉舊內容的操作,所以下面可以使用FileOutputStream類的另外一個構造方法進行資料的追加:

OutputStream output = new FileOutputStream(file, true);

如果說現在不想全部內容輸出,也可以使用另外一個write()方法部分內容輸出:

output.write(data.getBytes(), 0, 5);

執行結果:

w

在OutputStream類之中所有的資料都是以位元組資料為主的。

[2]輸入流:InputStream

如果現在要從指定的資料來源之中讀取資料,使用InputStream,這個類的定義如下:

public abstract class InputStream
extends Object
implements Closeable

既然InputStream為抽象類,那麼這個抽象類要使用就必須有子類,現在是通過檔案讀取內容,肯定使用FileInputStream子類進行操作,與OutputStream類的使用一樣,對於FileInputStream也只關心構造方法:

public FileInputStream(File file) throws FileNotFoundException;

在InputStream類之中,定義了三個讀取資料的操作方法:
(1)讀取單個位元組:

public abstract int read() throws IOException;

注意:每次執行read()方法都會讀取一個數據源的指定資料,如果現在發現已經讀取到了結尾返回-1;

(2)將讀取的資料儲存到位元組陣列中:

public int read(byte[] b) throws IOException;

注意:如果現在要讀取的資料小於byte的資料,這個時候read()方法的返回值int返回的是資料個數,如果資料已經讀完了,則這個時候的int返回的是-1

(3)將讀取的資料儲存在部分位元組陣列中:

public int read(byte[] b, int off, int len) throws IOException;

範例:一次性全部讀取資料進來

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

public class TestDemo {
    public static void main(String[] args) throws Exception {

        //第一步:定義檔案路徑
        File file = new File("D:" + File.separator + "demo" + File.separator + "test.txt"); // 定義檔案路徑
        if (file.exists()) { // 檔案存在則可以讀取

            //第二步:例項化輸入流
            InputStream input = new FileInputStream(file);

            //第三步:讀取資料到位元組陣列
            byte data[] = new byte[1024]; // 假設要讀的長度是1024
            int len = input.read(data); // 讀取資料,返回讀取個數

            //第四步:關閉資源
            input.close(); 
            System.out.println("讀取的資料是:【" + new String(data, 0, len) + "】");
        }
    }
}

範例:單個位元組讀取

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

public class TestDemo {
    public static void main(String[] args) throws Exception {

        File file = new File("D:" + File.separator + "demo" + File.separator + "test.txt"); // 定義檔案路徑
        if (file.exists()) { // 檔案存在則可以讀取

            InputStream input = new FileInputStream(file);

            byte data[] = new byte[1024]; // 假設要讀的長度是1024
            int foot = 0; // 操作data陣列的腳標
            int temp = 0;

            // 第一步:temp = input.read(),讀取一個單個位元組,並且將內容給temp變數
            // 第二步:temp != -1,將接收到的temp的數值判斷是否為-1,如果為-1則表示退出迴圈,如果不是,則儲存資料
            while ((temp = input.read()) != -1) {
                data[foot++] = (byte) temp; // 儲存讀取進來的單個位元組
            }

            input.close(); // 關閉

            System.out.println("讀取的資料是:【" + new String(data, 0, foot) + "】");
        }
    }
}

在今後開發中,都會使用以上的while迴圈方式進行資料的讀取。

2、字元流

[1]字元輸出流:Writer

Writer類也是一個專門用於資料輸出的操作類,這個類定義:

public abstract class Writer
extends Object
implements Appendable, Closeable, Flushable

在Wirter類之中比OutputStream類最為方便的一點就是其可以直接使用String型資料輸出,並且不再需要將其變為位元組陣列了。而Writer類本身也是一個抽象類,那麼如果要使用依然要依靠它的子類,尤其是現在操作的是檔案,使用FileWriter子類。
在Wirter類之中定義的write()方法都是以字元資料為主,但是在這些方法之中,只關心一個:輸出一個字串:

public void write(String str) throws IOException;

範例:使用Wirter類進行內容的輸出

import java.io.File;
import java.io.FileWriter;
import java.io.Writer;

public class TestDemo {
    public static void main(String[] args) throws Exception {
        File file = new File("D:" + File.separator + "demo" + File.separator + "test.txt"); // 定義檔案路徑
        if (!file.getParentFile().exists()) {
            file.getParentFile().mkdirs();// 建立父目錄
        }
        Writer out = new FileWriter(file);
        String data = "Hello World .";
        out.write(data); // 直接輸出字串
        out.close(); // 關閉輸出
    }
}

執行結果:

w

從輸出來講,Wirter類的輸出要比OutputStream類更加的方便。

[2]字元輸入流:Reader

Reader是進行字元資料讀取的操作類,其定義:

public abstract class Reader
extends Object
implements Readable, Closeable

在Writer類之中存在了直接輸出一個字串資料的方法,可是在Reader類之中並沒有定義這樣的方法,只是定義了三個按照字元讀取的方法,為什麼會這樣?
因為在使用OutputStream輸出資料的時候,其程式可以輸出的大小一定是程式可以承受的資料大小,那麼如果說使用InputStream讀取的時候,可能被讀取的資料非常的大,那麼如果一次性全讀進來了,就會出現問題,所以只能一個一個的進行讀取。

在Reader類裡也提供了一系列的read方法,其中一個:
讀取內容到字元陣列:

public int read (char[] cbuf) throws IOException;

注意:返回值表示讀取的資料長度,如果已經讀取到結尾。返回-1。

Reader依然是抽象類,那麼如果從檔案讀取,依然使用FileReader類。

import java.io.File;
import java.io.FileReader;
import java.io.Reader;

public class TestDemo {
    public static void main(String[] args) throws Exception {
        File file = new File("D:" + File.separator + "demo" + File.separator + "test.txt"); // 定義檔案路徑
        if (file.exists()) {
            Reader in = new FileReader(file); // 字元輸入流
            char data[] = new char[1024]; // 開闢陣列
            int len = in.read(data); // 讀取資料
            in.close();
            System.out.println("讀取資料內容:【" + new String(data, 0, len) + "】");

        }
    }
}

三、位元組流與字元流的區別

通過以上的程式碼演示,現在可以發現,對於位元組流和字元流可以完成類似的功能,那麼在開發之中使用那一種呢?兩者的區別:

位元組流在進行IO操作的時候,直接針對的是操作的資料終端(例如:檔案),而字元流操作的時候不是直接針對於終端,而是針對於快取區(理解為記憶體)操作,而後由快取取操作終端(例如:檔案),屬於間接操作,按照這樣的方式,如果說在使用位元組流的時候不關閉最後的輸出流操作,也可以將所有的內容進行輸出,而字元輸出流的時候如果不關閉,則意味著緩衝區之中的內容不會被輸出,當然,這個時候可以由使用者自己去呼叫flush()方法進行強制性的手工清空:

import java.io.File;
import java.io.FileWriter;
import java.io.Writer;
public class TestDemo {
    public static void main(String[] args) throws Exception {
        File file = new File("D:" + File.separator + "demo"
                + File.separator + "test.txt"); // 定義檔案路徑
        if (!file.getParentFile().exists()) {
            file.getParentFile().mkdirs();// 建立父目錄
        }
        Writer out = new FileWriter(file);
        String data = "Hello World .";
        out.write(data) ;   // 直接輸出字串
        out.flush() ;   // 清空緩衝區
    }
}

對於電腦磁碟或者是網路資料傳輸上,使用最多的資料型別都是位元組資料,包括圖片、音樂、各種可執行程式也都是位元組資料,很明顯,位元組資料要比字元資料更加的廣泛,但是在進行中文處理的過程之中,字元流又要比位元組流方便許多,所以如果要使用的話,首先考慮的是位元組流(還有一個原因是因為位元組流和字元流的程式碼形式類似),如果有中文處理的問題,才會考慮使用字元流。

四、綜合例項:檔案拷貝

1、實現思路:
如果要想實現檔案的複製那麼肯定要使用的是位元組流,因為檔案有可能是圖片,而對於這樣的操作有兩種實現方式:
(1)方式一:將要複製的檔案內容全部讀取到記憶體之中,而後將所有內容一次輸出到目標檔案之中;
(2)方式二:採用邊讀邊寫的方式一塊一塊的進行檔案的拷貝。
很明顯,使用方式二會更加的方便,因為如果要複製的檔案很大,那麼方式一是不可能在記憶體之中裝如此只大的資料量,所以全部讀取是不太可能的。

需要拷貝的檔案如下,約47M:

這裡寫圖片描述

2、拷貝程式:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

public class TestDemo {
    public static void main(String[] args) throws Exception {
        File inFile = new File("D:" + File.separator + "demo"
                + File.separator + "test.zip"); // 定義檔案路徑
        File outFile = new File("D:" + File.separator + "demo"
                + File.separator + "test2.zip"); // 定義檔案路徑
        long start = System.currentTimeMillis();

        if (!inFile.exists()) { // 原始檔不存在
            System.out.println("原始檔不存在!");
            System.exit(1); // 程式退出
        }
        if(!outFile.getParentFile().exists()){
            outFile.getParentFile().mkdirs();
        }

        InputStream input = new FileInputStream(inFile);
        OutputStream output = new FileOutputStream(outFile);

        int temp = 0;//儲存每次讀取的個數
        byte data[] = new byte[4096]; // 每次讀取4096位元組

        while ((temp = input.read(data)) != -1) { // 將每次讀取進來的資料儲存在位元組數組裡,並返回讀取的個數
            output.write(data, 0, temp); // 輸出陣列
        }

        long end = System.currentTimeMillis();
        System.out.println("拷貝完成,所花費的時間:" + (end - start) + "毫秒");

        input.close();
        output.close();
    }
}

執行結果:

拷貝完成,所花費的時間:125毫秒

檢視檔案:

3