1. 程式人生 > >Java核心技術梳理-IO

Java核心技術梳理-IO

一、引言

IO(輸入/輸出),輸入是指允許程式讀取外部資料(包括來自磁碟、光碟等儲存裝置的資料)、使用者輸入資料。輸出是指允許程式記錄執行狀態,將程式資料輸出到磁碟、光碟等儲存裝置中。

IO的主要內容包括輸入、輸出兩種IO流,這兩種流中又分為位元組流和字元流,位元組流是以位元組為單位來處理輸入、輸出流,而字元流是以字元為單位來處理輸入、輸出流。

二、File 類

File 類是用來操作檔案和目錄的,File能建立、刪除、重新命名檔案和目錄,File不能訪問檔案內容本身,File 類可以通過檔案路徑字串來建立物件,建立完物件之後有很多方法來操作檔案和目錄:

2.1 構造方法

  • File(String pathname):根據一個路徑得到File物件

  • File(String parent, String child):根據一個目錄和一個子檔案/目錄得到File物件

  • File(File parent, String child):根據一個父File物件和一個子檔案/目錄得到File對

2.2 建立方法

//在當前路徑來建立一個File物件
File file = new File("1.txt");
//建立檔案
System.out.println(file.createNewFile());
File file2 = new File("temp");
 //建立物件對應的目錄
System.out.println(file2.mkdir());

2.3 重新命名和刪除功能

//把檔案重新命名為指定的檔案路徑
file2.renameTo(new File("temp2"));
//刪除檔案或者資料夾
file2.delete();

注:重新命名中如果路徑名相同,就是改名,如果路徑名不同,就是改名並剪下。刪除不走回收站,要刪除一個資料夾,請注意該資料夾內不能包含檔案或者資料夾。

2.4 判斷功能

//判斷檔案或目錄是否存在
System.out.println(file.exists());
//判斷是否是檔案
System.out.println(file.isFile());
//判斷是否是目錄
System.out.println(file.isDirectory());
//是否為絕對路徑
System.out.println(file.isAbsolute());
//檔案或目錄是否可讀
System.out.println(file.canRead());
//檔案或目錄是否可寫
System.out.println(file.canWrite());

2.5 獲取功能

//返回檔案內容長度
System.out.println(file.length());
//獲取檔案或目錄名
System.out.println(file.getName());
//獲取檔案或目錄相對路徑
System.out.println(file.getPath());
//獲取檔案或目錄絕對路徑
System.out.println(file.getAbsolutePath());
//獲取上一級路徑
System.out.println(file.getAbsoluteFile().getParent());
//返回當前目錄的子目錄或檔案的名稱
String[] list = file1.list();
for (String fileName : list) {
    System.out.println(fileName);
}
//返回當前目錄的子目錄或檔案,返回的是File陣列
File[] files = file1.listFiles();
//返回系統的所有根路徑
File[] listRoots = File.listRoots();
for (File root : listRoots) {
    System.out.println(root);
}

三、IO 流

實現輸入/輸出的基礎是IO流,Java把不同的源之間的資料互動抽象表達為流,通過流的方式允許Java程式使用相同的方式來訪問不同的資料來源。用於操作流的類都在IO包中。

3.1 流的分類

按照不同的分類方式,流也可以分為不同型別

  1. 輸入流和輸出流:根據流向來分,可以分為輸入流與輸出流

    • 輸入流:從中讀取資料,而不能向其寫入資料

    • 輸出流:向其寫入資料,而不能讀取資料

  2. 位元組流和字元流:這兩種流用法幾乎完全一樣,區別在於所操作的資料單元不一樣,位元組流操作的資料單元是8位的位元組,而字元流是16位的字元。

3.2 InputStream與Reader

InputStream和Reader是所有輸入流的抽象基類,這是輸入流的模板,InputStream中有三個方法

  • int read() :從輸入流讀取單個位元組,返回所讀取的位元組資料。

  • int read(byte b[]):從輸入流中最多讀取b.length個位元組的資料,並將其儲存在陣列b中。

  • int read(byte b[], int off, int len):從輸入流中最多讀取len個位元組的資料,並將其儲存在陣列b中,放入的位置是從off中開始。

Reader中也有三個方法

  • int read() :從輸入流讀取單個位元組,返回所讀取的位元組資料。

  • int read(char cbuf[]):從輸入流中最多讀取cbuf.length個字元的資料,並將其儲存在陣列cbuf中。

  • int read(byte cbuf[], int off, int len):從輸入流中最多讀取len個位元組的資料,並將其儲存在陣列cbuf中,放入的位置是從off中開始。

    兩個類的方法基本相同,用法相同,只是操作單位不一樣

InputStream inputStream = new FileInputStream("StreamTest.java");
byte[] bytes = new byte[1024];
int hasRead = 0;
while ((hasRead = inputStream.read(bytes)) > 0) {
System.out.println(new String(bytes, 0, hasRead));
}

inputStream.close();

3.3 OutputStream與Writer

OutputStream與Writer是所有輸出流的抽象基類,是輸出流模板,OutputStream有三個方法:

  • void write(int b):指定位元組輸出到流中

  • void write(byte b[]):將指定位元組陣列輸出到流中

  • void write(byte b[], int off, int len):將指定位元組陣列從off位置到len長度輸出到流中

Writer中也有三個方法:

  • void write(int b):指定字元輸出到流中

  • void write(char buf[]):將指定位元組陣列輸出到流中

  • void write(char cubf[], int off, int len):將指定位元組陣列從off位置到len長度輸出到流中

由於Writer是以字元為單位進行操作,那可以使用String 來代替,於是有另外的方法

  • void write(String str):將str字串輸出到流中

  • void write(String str, int off, int len):將str從off位置開始長度為len輸出到流中

FileWriter fileWriter = new FileWriter("test.txt");
fileWriter.write("日照香爐生紫煙\r\n");
fileWriter.write("遙看瀑布掛前川\r\n");
fileWriter.write("飛流直下三千尺\r\n");
fileWriter.write("遙看瀑布掛前川\r\n");
fileWriter.close();

注:操作流時一定要記得關閉流,因為開啟的IO資源不屬於記憶體資源,垃圾回收無法回收。

四、輸入/輸出流體系

Java的輸入輸出流提供了40多個類,要全部都記住很困難也沒有必要,我們可以按照功能進行下分類,其實是非常有規律的

分類位元組輸入流位元組輸出流字元輸入流字元輸出流
抽象基類 InputStream OutputStream Reader Writer
訪問檔案 FileInputStream FileOutputStream FileReader FileWriter
訪問陣列 ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter
訪問管道 PipedInputStream PipedOutputStream PipedReader PipedWriter
訪問字串     StringReader StringWriter
緩衝流 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter
轉換流     InputStreamReader OutputStreamWriter
物件流 ObjectInputStream ObjectOutputStream    
過濾流 FilterInputStream FilterOutputStream FilterReader FilterWriter
列印流   PrintStream   PrintWriter
退回輸入流 PushbackInputStream   PushbackReader  
特殊流 DataInputStream DataOutputStream    

一般如果輸入/輸出的內容是文字內容,應該考慮使用字元流,如果輸入/輸出內容是二進位制內容,則應該考慮使用位元組流。

4.1 轉換流

體系中提供了兩個轉換流,實現將位元組流轉換成字元流,InputStreamReader將位元組輸入流轉換成字元輸入流,OutputStreamWriter將位元組輸出流轉換成字元輸出流,System.in代表標準輸入,這個標準輸入是位元組輸入流,但是鍵盤輸入的都是文字內容,這個時候我們可以InputStreamReader轉換成字元輸入流,普通的Reader讀取內容不方便,我們可以使用BufferedReader一次讀取一行資料,如:

//先將System.in轉換成Reader 物件
InputStreamReader inputStreamReader = new InputStreamReader(System.in);
//再將Reader包裝成BufferedReader
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String line = null;
while ((line = bufferedReader.readLine()) != null) {
    if (line.equals("exit")) {
        System.exit(1);
    }
    System.out.println("輸入的內容是:" + line);
}

BufferedReader具有緩衝功能,在沒有讀到換行符則阻塞,讀到換行符再繼續。

4.2 推回輸入流

推回輸入流PushbackInputStream和PushbackReader中都提供瞭如下方法:

  • void unread(int b) :將一個位元組/字元推回到推回緩衝區,從而允許重複讀取剛剛讀取的內容。

  • void unread(byte[] b/char[] b, int off, int len) :將一個位元組/字元數組裡從off開始,長度為len位元組/字元的內容推回到推回緩衝區,從而允許重複讀取剛剛讀取的內容。

  • void unread(byte[] b/char[]):將一個位元組/字元陣列內容推回到推回緩衝區,從而允許重複讀取剛剛讀取的內容。

這兩個推回流都帶有一個推回緩衝區,當呼叫unread()方法時,系統將會把指定的內容推回到該緩衝區,而當每次呼叫read方法時會優先從推回緩衝區讀取,只有完全讀取了推回緩衝區的內容後,但還沒有read()所需的陣列時才會從原輸入流中讀取。

 //建立PushbackReader物件,指定推回緩衝區的長度為64
PushbackReader pushbackReader = new PushbackReader(new FileReader("StreamTest.java"), 64);
char[] buf = new char[32];
//用以儲存上次讀取的字串內容
String lastContent = "";
int hasRead = 0;
//迴圈讀取檔案內容
while ((hasRead = pushbackReader.read(buf)) > 0) {
    //將讀取的內容轉換成字串
    String content = new String(buf, 0, hasRead);
    int targetIndex = 0;
    if ((targetIndex = (lastContent + content).indexOf("new PushbackReader")) > 0) {
        //將本次內容和上次的內容一起推回緩衝區
        pushbackReader.unread((lastContent + content).toCharArray());
        //重新定義一個長度為targetIndex的char陣列
        if (targetIndex > 32) {
            buf = new char[targetIndex];
        }
        //再次讀取指定長度的內容
        pushbackReader.read(buf, 0, targetIndex);
        //列印讀取的內容
        System.out.print(new String(buf, 0, targetIndex));
        System.exit(0);
    } else {
        //列印上次讀取的內容
        System.out.print(lastContent);
        //將本次內容設為上次讀取的內容
        lastContent = content;
    }
}

五、RandomAccessFile

RandomAccessFile是Java輸入/輸出流體系中最豐富的檔案內容訪問類,提供了眾多的方法來訪問檔案內容,既可讀取檔案內容,也可以向檔案輸出資料,RandomAccessFile可以自由訪問檔案的任意位置。

RandomAccessFile包含一個記錄指標,用以標識當前讀和寫的位置,當建立新物件時,指標位置在0處,而當讀/寫了N個位元組後,指標就會向後移動N個位元組,並且RandomAccessFile可以自動的移動該指標位置,當然我們也可以直接的獲取指標的位置。

  • getFilePointer():獲取檔案記錄指標的當前位置。

  • seek(long pos):將檔案記錄指標定位到pos位置。

RandomAccessFile有兩個建構函式:

  • RandomAccessFile(File file, String mode):使用File檔案,指定檔案本身 RandomAccessFile(String name, String mode):使用檔名稱,指定檔案

其中還有一個引數mode(訪問模式),訪問模式有4個值:

  • r:以只讀方式開啟檔案

  • rw:以讀、寫方式開啟檔案,如果檔案不存在,則建立

  • rws:以讀、寫方式開啟檔案,並要求對檔案的內容或者元資料的每個更新都同步寫入到底層儲存裝置

  • rwd:以讀、寫方式開啟檔案,並要求對檔案的內容的每個更新都同步寫入到底層儲存裝置

RandomAccessFile raf = new RandomAccessFile("StreamTest.java", "r");
System.out.println("檔案指標的初始位置:" + raf.getFilePointer());
//移動指標位置
raf.seek(300);
byte[] buf = new byte[1024];
int hasRead = 0;
while ((hasRead = raf.read(buf)) > 0) {
    //讀取資料
    System.out.println(new String(buf, 0, hasRead));
}
//追加內容
RandomAccessFile randomAccessFile=new RandomAccessFile("out.txt","rw");
randomAccessFile.setLength(randomAccessFile.length());
randomAccessFile.write("追加的內容!\r\n".getBytes());

六、物件序列化

物件序列化機制是允許把記憶體中的java物件轉換成平臺無關的二進位制流,這樣我們可以將這二進位制流儲存在磁碟上或者通過網路將起傳輸到另一個網路節點,其他程式獲取到此二進位制流後,可以將其恢復成原來的java物件。

要使一個物件是可序列化的,只需要繼承Serializable或者Externalizable介面,無需實現任何方法。所有可能在網路上傳輸的物件的類都應該是可序列化的,如我們JavaWeb中的輸入引數及返回結果。

6.1 使用物件流實現序列化

我們使用一個物件流來實現序列化物件

先建一個物件類:

@Data
public class Person implements Serializable {

    private int age;

    private String name;

    public Person(String name, int age) {
        System.out.println("有引數的構造器");
        this.age = age;
        this.name = name;
    }
}

序列化物件與反序列化物件

//建立輸出流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("object.txt"));
Person person = new Person("張三", 10);
//將person寫入檔案中
objectOutputStream.writeObject(person);
//建立輸入流
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("object.txt"));
try {
    //讀出資料
    Person p = (Person) objectInputStream.readObject();
    System.out.println(p);
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

反序列化讀取的僅僅是Java物件的資料,而不java類,因此反序列化時必須提供物件所屬類的class檔案,在反序列化物件時沒有呼叫有引數的構造器,說明反序列化時不需要通過構造器來初始化Java物件。

如果一個類中包含了引用型別,那麼引用型別也必須是可序列化的,否則該類也是不可序列化的。

如果我們不希望某個變數被序列化,比如敏感資訊,那需要使用transient來修飾此變數即可。

七、NIO

上面學習的IO都是阻塞式的,而且是底層都是通過位元組的移動來處理的,這樣明顯效率不高,於是後面新增了NIO來進行改進,這些類都放在java.nio包中。

新IO 是將檔案或檔案的一段區域對映到記憶體中,這樣就可以像訪問記憶體一樣來訪問檔案中的內容,相當於虛擬記憶體概念,這種方式比傳統的IO快很多。

新IO的兩大核心物件是Channel(通道)與Buffer(緩衝),Channel與傳統的InputStream、OutputStream最大的區別在於提供了一個map()方法,這個方法是將一塊資料對映到記憶體中,這樣新IO就是面向塊進行處理;Buffer本質是一個數組,可以看做一個容器,傳送到Channel中的所有物件都必須首先放在Buffer中,讀取資料也是從Buffer中讀取。

7.1 Buffer

Buffer是一個抽象類,最常用的子類是ByteChannel和CharBuffer,Buffer類都沒有提供構造器,都是通過XXXBuffer allocate(int capacity) 來得到物件,如

CharBuffer allocate = CharBuffer.allocate(8);

Buffer有三個重要概念:

  • 容量(capacity):緩衝區的容量,表示該buffer的最大資料容量,即最多可儲存多少資料,建立後不可改變。

  • 界限(limit):位於limit後的資料既不可以讀,也不可以寫。

  • 位置(position):用於指明下一個可以被讀出或寫入的緩衝區位置索引,類似IO中的指標。

Buffer的主要作用是裝入資料,然後輸出,當建立buffer時,position在0位置,limit在capacity,當新增資料時,position向後移動。

當Buffer裝好資料時,呼叫flip()方法,這個方法將limit設定為position,position設定為0,也就是說不能繼續輸入,這就給輸出資料做好準備了,而當輸出資料結束後,呼叫clear()方法,這是將position設定為0,limit設定為capacity,這樣就為裝入資料做好了準備。

除了上面的幾個概念,Buffer還有兩個重要方法,即put()與get()方法,就是儲存與讀取資料方法,在儲存和讀取資料時,分為相對和絕對兩種:

  • 相對:從Buffer的position位置開始讀取或者寫入資料,這時候會改變position的數值。

  • 絕對:根據索引讀取或寫入資料,這個時候不會影響position的數值。

//建立buffer
CharBuffer buffer = CharBuffer.allocate(10);
System.out.println("capacity: " + buffer.capacity());
System.out.println("limit:" + buffer.limit());
System.out.println("position:" + buffer.position());
//加入資料
buffer.put('a');
buffer.put('b');
buffer.put('c');
System.out.println("加入元素後,position:" + buffer.position());
buffer.flip();
System.out.println("執行flip後,limit:" + buffer.limit());
System.out.println("position:" + buffer.position());
System.out.println("取出一個數據," + buffer.get());
System.out.println("取出資料後,position:" + buffer.position());
buffer.clear();
System.out.println("執行clear後,limit:" + buffer.limit());
System.out.println(",position:" + buffer.position());
System.out.println("執行clear後緩衝區未被清空:" + buffer.get(2));
System.out.println("絕對讀取後,position不會改變:" + buffer.position());

7.2 Channel

Channel類似傳統流物件,主要區別在於Channel可以將指定檔案的部分或者全部直接對映成Buffer,程式不能直接對Channel中的資料進行讀寫,只能通過Channel來進行資料讀寫。我們用FileChannel來看看如何使用:

File file = new File("StreamTest.java");
//輸入流建立FileChannel
FileChannel inChannel = new FileInputStream(file).getChannel();
//以檔案輸出流建立FileChannel,控制輸出
FileChannel outChannel = new FileOutputStream("a.txt").getChannel();
//將FileChannel對映成ByteBuffer,
MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
Charset charset = Charset.forName("GBK");
//輸出資料
outChannel.write(buffer);
buffer.clear();
CharsetDecoder charsetDecoder = charset.newDecoder();
//轉換成CharBuffer進行輸出
CharBuffer charBuffer = charsetDecoder.decode(buffer);
System.out.println(charBuffer);

7.3 字符集與Charset

我們知道,在計算機底層檔案都是二進位制檔案,都是位元組碼,那為什麼我們還能看到字元,這裡面涉及編碼和解碼兩個概念,簡單講,將字元轉換成二進位制為編碼,而將二進位制轉成字元為解碼。

Java預設使用Unicode字符集(字符集是指二進位制序列與字元之間的對應關係),但很多作業系統不使用Unicode字符集,這樣就會出錯,我們要根據實際情況來使用對應的字符集。

Charset包含了建立解碼器和編碼器的方法,還提供了獲取Charset所支援字符集的方法,我們可以通過Charset的forName()獲取物件,通過物件獲取到CharsetEncoder和CharsetDecoder物件,再通過此物件進行字元序列與位元組序列的轉換。

SortedMap<String, Charset> stringCharsetSortedMap = Charset.availableCharsets();
for(String name:stringCharsetSortedMap.keySet()){
    System.out.println(name);
}
//建立簡體中文對應的Charset
Charset cn = Charset.forName("GBK");
//建立對應的編碼器及解碼器
CharsetEncoder cnEncoder = cn.newEncoder();
CharsetDecoder cnDecoder = cn.newDecoder();
CharBuffer buff = CharBuffer.allocate(8);
buff.put('李');
buff.put('白');
buff.flip();
//將buff的字元轉成位元組序列
ByteBuffer bbuff = cnEncoder.encode(buff);
for (int i = 0; i <bbuff.capacity() ; i++) {
    System.out.print(bbuff.get(i)+ " ");
}
//將bbuff的資料解碼成字元
System.out.println("\n"+cnDecoder.decode(bbuff));

7.4 Path、Paths、Files

早期的Java只提供了File類來訪問檔案系統,功能比較有限且效能不高,後面又提供了Path介面,Path代表一個平臺無關路徑,並提供了Paths與Files兩個工具類,提供了大量的方法來操作檔案。

Path path = Paths.get(".");
System.out.println("path包含的檔案數量:" + path.getNameCount());
System.out.println("path的根路徑:" + path.getRoot());
Path path1 = path.toAbsolutePath();
System.out.println("path的絕對路徑:" + path1);
//多個String構建路徑
Path path2 = Paths.get("G:", "test", "codes");
System.out.println("path2的路徑:" + path2);

System.out.println("StreamTest.java是否為隱藏檔案:" + Files.isHidden(Paths.get("StreamTest.java")));
//一次性讀取所有行
List<String> allLines = Files.readAllLines(Paths.get("StreamTest.java"), Charset.forName("gbk"));
System.out.println(allLines);
//讀取大小
System.out.println("StreamTest.java檔案大小:" + Files.size(Paths.get("StreamTest.java")));
List<String> poem = new ArrayList<>();
poem.add("問君能有幾多愁");
poem.add("恰似一江春水向東流");
//一次性寫入資料
Files.write(Paths.get("poem.txt"), poem, Charset.forName("gbk"));

可以看到Paths與Files非常的強大,提供了很多方法供我們使用,在之前這些方法我們自己寫的話比較麻煩,更多的方法可以自己去看API。

7.5 檔案屬性

java.nio.file.attribute包下提供了大量的屬性工具類,提供了很方便的方法去獲取檔案的屬性:

BasicFileAttributeView baseView = Files.getFileAttributeView(Paths.get("poem.txt"), BasicFileAttributeView.class);
BasicFileAttributes basicFileAttributes = baseView.readAttributes();
System.out.println("建立時間:" + basicFileAttributes.creationTime().toMillis());
System.out.println("最後更新時間:" + basicFileAttributes.lastModifiedTime().toMillis());

&n