1. 程式人生 > 其它 >Java學習之路——檔案操作

Java學習之路——檔案操作

技術標籤:Java

Java學習之路——檔案操作

概述

在計算機系統中,檔案是非常重要的儲存方式。在著名的作業系統 Linux 則更是“一切皆檔案”,因此會操作檔案我們需要掌握的基礎本領之一。

一、java.io.File

Java的標準庫java.io提供了File物件來操作檔案和目錄。

java.io.File類用於描述檔案系統中的一個檔案或目錄 該類可以:

  • 1、訪問檔案或目錄的屬性資訊
  • 2、訪問一個目錄中的所有子項
  • 3、操作檔案或目錄(建立、刪除)

但是,File 類不能訪問檔案的具體內容!例如讀檔案、寫檔案等操作就不能使用該類來完成。

構造檔案物件

File 類有以下幾種構造器:

  • 通過給定的父抽象路徑名和子路徑名字串構造檔案物件;
File(File parent, String child);
  • 通過將給定路徑名字串轉換成抽象路徑名來構造檔案物件;
File(String pathname) 
  • 根據 parent 路徑名字串和 child 路徑名字串構造檔案物件;
File(String parent, String child) 
  • 通過將給定的 file: URI 轉換成一個抽象路徑名來構造檔案物件。
File(URI uri) 

示例

import java.io.File;

public class Demo {
    public static void
main(String[] args) { String dirname = "/java"; File f1 = new File(dirname); if (f1.isDirectory()) { System.out.println("Directory of " + dirname); String s[] = f1.list(); for (int i = 0; i < s.length; i++) { File f =
new File(dirname + "/" + s[i]); if (f.isDirectory()) { System.out.println(s[i] + " is a directory"); } else { System.out.println(s[i] + " is a file"); } } } else { System.out.println(dirname + " is not a directory"); } } }

常用方法

方法功能
getName()返回由此抽象路徑名錶示的檔案或目錄的名稱;
getParent()返回此抽象路徑名的父路徑名的路徑名字串,如果此路徑名沒有指定父目錄,則返回 null
getParentFile()返回此抽象路徑名的父路徑名的抽象路徑名,如果此路徑名沒有指定父目錄,則返回 null
getPath()將此抽象路徑名轉換為一個路徑名字串;
isAbsolute()測試此抽象路徑名是否為絕對路徑名;
getAbsolutePath()返回抽象路徑名的絕對路徑名字串;
canRead()測試應用程式是否可以讀取此抽象路徑名錶示的檔案;
canWrite()測試應用程式是否可以修改此抽象路徑名錶示的檔案;
exists()測試此抽象路徑名錶示的檔案或目錄是否存在;
isDirectory()測試此抽象路徑名錶示的檔案是否是一個目錄;
isFile()測試此抽象路徑名錶示的檔案是否是一個標準檔案;
lastModified()返回此抽象路徑名錶示的檔案最後一次被修改的時間;
length()返回由此抽象路徑名錶示的檔案的長度;
createNewFile()當且僅當不存在具有此抽象路徑名指定的名稱的檔案時,原子地建立由此抽象路徑名指定的一個新的空檔案;
delete()刪除此抽象路徑名錶示的檔案或目錄;
deleteOnExit()在虛擬機器終止時,請求刪除此抽象路徑名錶示的檔案或目錄;
list()返回由此抽象路徑名所表示的目錄中的檔案和目錄的名稱所組成字串陣列;
list(FilenameFilter filter)返回由包含在目錄中的檔案和目錄的名稱所組成的字串陣列,這一目錄是通過滿足指定過濾器的抽象路徑名來表示的;
listFiles()返回一個抽象路徑名陣列,這些路徑名錶示此抽象路徑名所表示目錄中的檔案;
listFiles(FileFilter filter)返回表示此抽象路徑名所表示目錄中的檔案和目錄的抽象路徑名陣列,這些路徑名滿足特定過濾器;
mkdir()建立此抽象路徑名指定的目錄;
mkdirs()建立此抽象路徑名指定的目錄,包括建立必需但不存在的父目錄;
renameTo(File dest)重新命名此抽象路徑名錶示的檔案;
setLastModified(long time)設定由此抽象路徑名所指定的檔案或目錄的最後一次修改時間;
setReadOnly()標記此抽象路徑名指定的檔案或目錄,以便只可對其進行讀操作;
createTempFile(String prefix, String suffix, File directory)在指定目錄中建立一個新的空檔案,使用給定的字首和字尾字串生成其名稱;
createTempFile(String prefix, String suffix)在預設臨時檔案目錄中建立一個空檔案,使用給定字首和字尾生成其名稱;
compareTo(File pathname)按字母順序比較兩個抽象路徑名;
compareTo(Object o)按字母順序比較抽象路徑名與給定物件;
equals(Object obj)測試此抽象路徑名與給定物件是否相等;
toString()返回此抽象路徑名的路徑名字串。

二、java.io.RandomAccessFile

java.io.RandomAccessFile類用於讀寫檔案資料。其基於指標對檔案進行讀寫。

由於是基於指標,以位元組為單位的讀寫,其效率較低。

建立 RandomAccessFile有兩種方式:

  • 1:r:只讀模式,僅僅對檔案資料進行讀取;
  • 2:rw:讀寫模式,對檔案資料可以編輯。

在計算機中,類似檔案、網路埠這些資源,都是由作業系統統一管理的。

應用程式在執行的過程中,如果打開了一個檔案進行讀寫,完成後要及時地關閉,以便讓作業系統把資源釋放掉,否則,應用程式佔用的資源會越來越多,不但白白佔用記憶體,還會影響其他應用程式的執行。

因此,我們需要用try ... finally來保證InputStream在無論是否發生IO錯誤的時候都能夠正確地關閉。

示例

import java.io.IOException;
import java.io.RandomAccessFile;

public class Demo {
    public static void main(String[] args) throws IOException {
        RandomAccessFile raf = new RandomAccessFile("demo.txt" , "rw");
        try {
            /*
             * void write(int d)
             * 寫入1個位元組,寫出的是該整數d對應的2進制中的低八位(一個位元組8個位)
             * 00000001
             */
            int a = 1;
            raf.write(a);   //硬碟中存的是二進位制   如果檔案已經存在,在首次執行時覆蓋原有內容,後面的不覆蓋
            a = 98;
            raf.write(a);
            System.out.println("寫入硬碟完畢");
        } finally {
            // 讀寫完畢後,關閉;防止記憶體洩漏
            raf.close();
        }
    }
}

try ... finally來編寫上述程式碼會感覺比較複雜,更好的寫法是利用Java 7引入的新的try(resource)的語法,只需要編寫try語句,讓編譯器自動為我們關閉資源。

示例

import java.io.IOException;
import java.io.RandomAccessFile;

public class Demo {
    public static void main(String[] args) throws IOException {
        try (RandomAccessFile raf = new RandomAccessFile("demo.txt" , "rw")) {
            int a = 1;
            raf.write(a);   
            a = 98;
            raf.write(a);
            System.out.println("寫入硬碟完畢");
        }
    }
}

常用方法

方法功能
close()關閉此隨機訪問檔案流並釋放與該流關聯的所有系統資源
getChannel()返回與此檔案關聯的唯一FileChannel物件,NIO用到
getFilePointer()返回此檔案中的當前偏移量
length()返回此檔案的長度
read()從此檔案中讀取一個數據位元組
read(byte[] b)將最多 b.length 個數據位元組從此檔案讀入byte陣列,返回讀入的總位元組數,如果由於已經達到檔案末尾而不再有資料,則返回-1。在至少一個輸入位元組可用前,此方法一直阻塞
read(byte[] b, int off, int length)將最多 length 個數據位元組從此檔案的指定初始偏移量off讀入byte陣列
readBoolean()從此檔案讀取一個boolean,與 readByte()、readChar()、readDouble()等類似
readLine()從此檔案讀取文字的下一行
seek(long pos)重要,設定到此檔案開頭測量到的檔案指標偏移量,在該位置發生下一個讀取或寫入操作
skipBytes(int n)重要,嘗試跳過輸入的n個位元組以丟棄跳過的位元組,返回跳過的位元組數
write(byte[] b)將 b.length 個位元組從指定byte陣列寫入到此檔案中
write(byte[] b, int off, int length)將 length 個位元組從指定byte陣列寫入到此檔案,並從偏移量off處開始
write(int b)向此檔案寫入指定的位元組
writeBoolean(boolean v)按單位元組值將boolean寫入該檔案,與 writeByte(int v)、writeBytes(String s)、writeChar(int v)等方法類似

三、java.io.FileInputStreamjava.io.FileOutputStream

輸入流都有一個抽象父類:java.io.InputStream,他是所有位元組輸入流的父類。FileInputStreamInputStream的子類,是檔案位元組輸入流,是一個低階流(節點流),其使用方式和RandomAccessFile一致。InputStream及其子類只負責讀檔案,不負責寫檔案

輸出流都有一個抽象父類:java.io.OutputStream,他是所有位元組輸出流的父類。FileOutputStreamOutputStream的子類,是檔案位元組輸出流,是一個低階流(節點流),其使用方式和RandomAccessFile一致。OutputStream及其子類只負責寫檔案,不負責讀檔案

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

public class Demo {
    public static void main(String[] args) throws IOException {
        try (FileInputStream fis = new FileInputStream("demo.txt");  // 輸入檔案
             FileOutputStream fos = new FileOutputStream("demo-copy.txt" , true)) {  // 輸出檔案
            byte[] data = new byte[100];
            int len = fis.read(data);  // 讀取輸入檔案
            String inputData = new String(data , 0 , len);
            System.out.println(inputData);
            String outData = inputData + "-copy";
            fos.write(outData.getBytes());  // 輸出至檔案
        }
    }
}

常用方法

序號方法及描述所屬物件
close()關閉此檔案輸入流並釋放與此流有關的所有系統資源。丟擲IOException異常。通用
finalize()這個方法清除與該檔案的連線。確保在不再引用檔案輸入流時呼叫其 close 方法。丟擲IOException異常。通用
read(int r)這個方法從 InputStream 物件讀取指定位元組的資料。返回為整數值。返回下一位元組資料,如果已經到結尾則返回 -1。FileInputStream
read(byte[] r)這個方法從輸入流讀取r.length長度的位元組。返回讀取的位元組數。如果是檔案結尾則返回-1。FileInputStream
available()返回下一次對此輸入流呼叫的方法可以不受阻塞地從此輸入流讀取的位元組數。返回一個整數值。FileInputStream
write(int w)這個方法把指定的位元組寫到輸出流中。FileOutputStream
write(byte[] w)把指定陣列中 w.length 長度的位元組寫到OutputStream中。FileOutputStream

四、java.io.BufferedInputStreamjava.io.BufferedOutputStream

BufferedInputStreamBufferedOutputStream是一對緩衝流,屬於高階流(處理流),用於處理低階流(節點流)的資料,使用它們可以提高讀寫的效率(先將資料寫入緩衝區,在寫入硬碟,減少了讀寫次數)。緩衝流單獨存在沒意義,必須和低階流一起使用。

檔案複製

import java.io.*;

public class Demo {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("demo.txt");       //低階輸入流
        BufferedInputStream bis = new BufferedInputStream(fis);     //將低階輸入流接到高階輸入流上

        FileOutputStream fos = new FileOutputStream("demo-copy.txt");   //低階輸出流
        BufferedOutputStream bos = new BufferedOutputStream(fos);    //將低階輸出流接到高階輸出流上​
        try {
            int len = -1;
            /*
             * 緩衝流內部維護了一個緩衝區,當我們呼叫下面的read()方法讀取一個位元組時,
             * 實際上緩衝流會讓FileInputStream讀取一組位元組並存入到緩衝流自身內部的位元組陣列中,然後將第一個位元組返回。
             * 當我們再次呼叫read()方法讀取一個位元組時,緩衝流會直接將陣列中的第二個位元組返回,以此類推,直到該陣列中所有位元組都被讀取
             * 過後才會再次讀取一組位元組。所以實際上還是通過提高每次讀取資料的數量減少讀取的次數來提高讀取效率。
             */
            while((len = bis.read()) != -1){
                /*
                 * 緩衝輸出流也是類似的
                 */
                bos.write(len);
            }
            System.out.println("複製完成");
        } finally {
            bis.close();  //只需要關高階流(內部會先關閉低階流)
            bos.close();
        }
        
    }
}

手動將快取中的資料刷到磁碟

import java.io.*;

public class Demo {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("demo.txt");
        try (BufferedOutputStream bos = new BufferedOutputStream(fos)) {
            /*
             * 通過緩衝輸出流寫出的位元組不會立即被寫入硬碟,會先存在記憶體的位元組陣列,直到該陣列滿了,才會一次性寫出所有的資料。
             * 這樣做等同於提高了一次性的寫入資料量,減少了寫的次數,提高效率
             */
            bos.write("手動將快取中的資料刷到磁碟".getBytes());   //不會及時寫入硬碟,在記憶體中。如果不加colse(),最終被GC幹掉,不會寫入檔案
            /*
             * flush方法強制將緩衝區的資料一次性輸出。提高了及時性,但是頻繁操作會降低操作效率。
             */
            bos.flush();//強制及時寫入記憶體,不會等到緩衝區滿。 執行次數越多,效率越低
            System.out.println("完成");
        }
    }
}

五、java.io.ObjectInputStreamjava.io.ObjectOutputStream

ObjectInputStreamObjectOutputStream是一對物件流,屬於高階流,ObjectInputStream可以讀取一組位元組轉化為 Java 物件,而ObjectOutputStream的作用是可以直接將 Java 中的一個物件轉化為一組位元組後輸出。這其實就是 Java 物件的序列化和反序列化,因此Java 物件必須要實現序列化介面。

1. 定義一個物件

class Person implements Serializable{
    /**
     * 1.實現Serializable介面。該介面中沒有方法,不需要重寫方法,這樣的介面也叫簽名介面。
     *
     * 2.需要有serialVersionUID,序列化的版本號(影響反序列化能否成功)。
     * 當一個類實現了Serializable介面後,該類會有一個常量表示這個類的版本號,版本號影響這個物件反序列化的結果。
     * 不顯式定義serialVersionUID,會預設隨機產生一個(根據類的結構由演算法產生的,類只要改過,隨機產生的就會變化)
     * 建議自行維護版本號(自定義該常量並給定值)。若不指定,編譯器會根據當前類的結構產生一個版本號,類的結構不變則版本號不變,但是結構變了(屬性型別、名字變化等),都會導致版本號改變。
     * 序列化時,這個版本號會存入到檔案中。
     * 反序列化物件時,會檢查該物件的版本號與當前類現在的版本號是否一致,一致則可以還原,不一致則反序列化失敗。
     * 版本號一致時,就算反序列化的物件與當前類的結構有出入,也會採取相容模式,即:任然有的屬性就進行還原,沒有的屬性則被忽略。
     */
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    /*
     * 3.transient關鍵字的作用是修飾一個屬性。那麼當這個類的例項進行序列化時,該屬性不會被包含在序列化後的位元組中,從而達到了“瘦身”的目的
     * 反序列化後是該型別的預設值。引用型別預設是null,其他型別預設是0。如果是靜態變數,則對映為記憶體中的該變數的值。
     */
    private transient List<String> otherInfo;

    public void setName(String 諸葛小猿) {
    }

    public void setAge(int i) {
    }

    public void setOtherInfo(List<String> otherInfo) {
    }
}

2. 物件序列化

import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class Demo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person p = new Person();
        p.setName("ObjectOutputStream");
        p.setAge(30);
        List<String> otherInfo = new ArrayList<String>();
        otherInfo.add("物件序列化");
        p.setOtherInfo(otherInfo);
        System.out.println(p);
        FileOutputStream fos = new FileOutputStream("demo.obj");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        /*
         * ObjectOutputStream的writeObject方法的作用:將給定的java物件轉換為一組位元組後寫到硬碟上,
         * 這裡由於ObjectOutputStream是裝在FileOutputStream上的,所以轉換的這組位元組最終通過FOS寫入到檔案中。
         *
         * 若希望該物件可以被寫出,那麼前提是該物件所屬的類必須實現Serializable介面
         * 實際資料寫入檔案的資訊比物件本身資訊多,因為儲存了物件的結構資訊
         *
         * 該方法涉及到兩個操作:
         * 1:將物件轉換為一組位元組(稱為:物件序列化(編碼))
         * 2:將該位元組寫入到檔案中(硬碟上)(稱為:資料持久化)
         */
        oos.writeObject(p);

        System.out.println("成功");
        oos.close();
    }
}

3. 物件反序列化

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

public class Demo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        FileInputStream fis = new FileInputStream("demo.obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        /*
         * 將一組位元組還原為物件的過程稱為:物件的反序列化
         *
         * 反序列化時,demo.obj檔案中的serialVersionUID要和Person類中的serialVersionUID一致才能成功
         *
         * readObject() 返回的是Object
         */
        Person p = (Person) ois.readObject();
        System.out.println(p);
        ois.close();
    }
}

六、java.io.InputStreamReaderjava.io.OutputStreamWriter

Java 根據讀寫資料單位不同,將流分為:位元組流與字元流。

位元組流的最小讀寫單位是一個位元組,前面介紹的InputStreamOutputStream都屬於這一類。

字元流的最小讀寫單位是一個字元,字元流雖然是以字元為單位讀寫資料,其底層實際上還是要以位元組的形式讀寫,所以字元流天生就具備將位元組轉換為字元或字元轉換成位元組的能力,所以所有的字元流都是高階流,方便我們讀寫字元資料,無需再關心字元與位元組的相互轉換。字元流的父類java.io.Readerjava.io.Writer,他們是以char為單位讀寫,轉換為Unicode,他們規定了字元流的基本方法。這裡介紹兩個常用的字元流java.io.InputStreamReaderjava.io.OutputStreamWriter的使用。字元是高階流,也需要和低階流聯合使用。

除了InputStreamReaderOutputStreamWriter之外的字元流,大部分都只處理其他字元流。但是低階流都是位元組流,這時若希望用一個字元流來處理一個位元組流就會產生衝突。所以可以通過建立InputStreamReaderOutputStreamWriter來處理位元組流,而InputStreamReaderOutputStreamWriter本身是字元流,所以可以使得其他字元流得以處理該流。這樣,InputStreamReaderOutputStreamWriter相當於聯絡位元組流和字元流的紐帶,類似轉化器的效果,因此這兩個流也叫轉換流。

1. OutputStreamWriter寫檔案

import java.io.*;
import java.nio.charset.StandardCharsets;

public class Demo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        FileOutputStream fos = new FileOutputStream("demo.txt");
        try (OutputStreamWriter osw = new OutputStreamWriter(fos , StandardCharsets.UTF_8)) {
            /*
             * OutputStreamWriter的常用構造方法:
             * 1.OutputStreamWriter(OutputStream out)
             *
             * 2.OutputStreamWriter(outputStream out, String csn)
             * 將給定的位元組輸出流轉換為位元組流的同時,指定通過當前字元流寫出的字元資料以何種字符集轉換為位元組。
             */
 
            osw.write("OutputStreamWriter");
            osw.write("寫檔案");
 
            char[] buf = "Hello World!".toCharArray();
            osw.write(buf , 0 , buf.length);
 
            System.out.println("成功");
        }
    }
}

2. InputStreamReader讀檔案

import java.io.*;
import java.nio.charset.StandardCharsets;

public class Demo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        FileInputStream fis = new FileInputStream("demo.txt");
        /*
         * 也可以使用一個引數的構造,不加字符集
         */
        InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
        int len = -1;

        /*
         * int read();
         * 一次讀取一個字元,返回一個該字元編碼的int值,若返回值為-1則表示讀到末尾
         */
        while((len = isr.read()) != -1){
            // 強轉,取低16位
            char d = (char) len;
            System.out.print(d);
        }
    }
}

七、java.io.BufferedReaderjava.io.BufferedWriter

BufferedWriterBufferedRead是緩衝位元組流,屬於高階流,按行讀取字串。由於這兩個字元流不能直接處理位元組流,所以需要InputStreamReaderOutputStreamWriter這兩個轉換流做紐帶,將低階位元組流和BufferedReaderBufferedWriter關聯起來。

雖然這兩個流讀寫的速度快,但是沒有太多的方法可以使用,所有使用的較少。下面只測試一下java.io.BUfferedReader

import java.io.*;
import java.nio.charset.StandardCharsets;

public class Demo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        FileInputStream fis = new FileInputStream("demo.txt");

        //轉換流InputStreamReader可以指定字符集
        BufferedReader br = new BufferedReader(new InputStreamReader(fis, StandardCharsets.UTF_8));

        /*
         * String readLine()
         * 連續讀取若干字元,直到遇到換行符為止,將換行符之前的所有字元以一個字串返回(不包括換行符)
         * 若該方法返回值為null,則表示讀取到了末尾(如果一行為空,返回空串“”)
         * 注意,返回的字串不包含最後的換行符
         */
        String line = null;
        while((line = br.readLine()) != null){
            //line中不包含\n
            System.out.println(line);
        }
        br.close();
    }
}

八、java.io.PrintWriter

​ 緩衝位元組流 java.io.PrintWriter,是一種比較常用的輸出流。其內部維護了一個緩衝區(位元組陣列),按行寫字串,寫字元效率高。內部自動處理BufferedWriter來完成緩衝操作, 並且PrintWriter具有自動行重新整理功能。

1. PrintWriter寫檔案

import java.io.*;
import java.nio.charset.StandardCharsets;

public class Demo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        /**
         * PrintWriter提供了豐富的構造方法
         * 其中提供了可以針對檔案寫出操作的構造方法:
         * PrintWriter(String path)
         * PrintWriter(File file)
         * 是個高階流,不用對接低階流,原始碼已經使用了低階流,可以不加字符集
         */
        try (PrintWriter pw = new PrintWriter("demo.txt" , "utf-8")) {
            pw.println("`PrintWriter`寫檔案");

            System.out.println("finished!");
        }
        // 最終的檔案大小大於所有的字元之和,因為有換行符
    }
}

2. PrintWriter處理其他流

import java.io.*;
import java.nio.charset.StandardCharsets;

public class Demo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        FileOutputStream fos = new FileOutputStream("demo.txt");

        /*
         * PrintWriter構造傳入位元組流,不能指定字符集: PrintWriter pw = new PrintWriter(fos);
         * 若希望指定字符集,需要在中間使用轉換流OutputStreamWriter
         */
        OutputStreamWriter osw =  new OutputStreamWriter(fos, StandardCharsets.UTF_8);
        try (PrintWriter pw = new PrintWriter(osw)) {
            pw.println("PrintWriter處理其他流");
            System.out.println("成功");
        }
    }
}

3. PrintWriter自動行重新整理

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

public class Demo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //true是自動重新整理,可以只用一個引數
        try (PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream("demo.txt")) , true)) {
            System.out.println("請輸入檔案內容:");
            Scanner sc = new Scanner(System.in);

            while (true) {
                String str = sc.nextLine();
                if ("exit".equals(str)) {
                    System.out.println("結束");
                    break;
                }

                //具有自動行重新整理的pw在使用println方法是會自動flush
                pw.println(str);
            }
        }
    }
}