死磕 IO 流?你都磕對地方了么
前言
我們?nèi)粘i_發(fā)過程中,有許多方面都涉及到 IO 流,比如上傳下載、傳輸、設(shè)計模式等等。而所有的一切都是基于 IO 流來進行,所以今天就來看看 Java 中 IO 流的相關(guān)知識點。
本文主要內(nèi)容安排如下:
文件對象 流簡介 字節(jié)流 字符流
文件對象
文件路徑
Java 標準庫 java.io 提供了 File 對象用于操作文件和目錄,也就是說我們的文件和目錄都是可以通過 File 封裝成對象的。構(gòu)造 File 對象時,需要傳入我們的文件或目錄的路徑名,常用的構(gòu)造方法如下:
| 方法 | 描述 |
|---|---|
File(String pathName) | 通過將給定路徑名字符串轉(zhuǎn)換為抽象路徑名來創(chuàng)建新實例 |
File(String parent, String child) | 從父路徑名字符串和子路徑名字符串創(chuàng)建新實例 |
File(File parent, String child) | 從父抽象路徑名和子路徑名字符串創(chuàng)建新實例 |
import java.io.File;
/**
* @author : cunyu
* @version : 1.0
* @className : FileObject
* @date : 2021/4/20 9:20
* @description : 創(chuàng)建 File 對象的三個構(gòu)造方法
*/
public class FileObject {
public static void main(String[] args) {
File file1 = new File("D:/PersonalFiles/github/githubCodes/IDEA/TheWay2Java/IOStream/data/1.txt");
System.out.println(file1);
File file2 = new File("D:/PersonalFiles/github/githubCodes/IDEA/TheWay2Java/IOStream/data", "1.txt");
System.out.println(file2);
File file3 = new File("D:/PersonalFiles/github/githubCodes/IDEA/TheWay2Java/IOStream/data");
File file4 = new File(file3, "1.txt");
System.out.println(file4);
}
}

對于我們傳入文件的路徑,既可以使用絕對路徑,也可以使用相對路徑。
相對路徑:以當前文件所在位置為參考,然后建立出另一個文件所在位置路徑。我們最常用的有 .和..,前者表示當前目錄,而后者則表示當前目錄的上一級目錄。假設(shè)我們當前目錄為/home/cunyu1943/data,則.仍然表示該目錄,而..則表示/home/cunyu1943目錄。絕對路徑:又可以分為 本地絕對路徑 和 網(wǎng)絡(luò)絕對路徑。本地絕對路徑以根目錄為參考,指文件在硬盤中真實存在的路徑,比如在 Windows 系統(tǒng),我們的一個絕對路徑是 D:\\Softwares\\Typora\\Typora.exe,而在類Unix系統(tǒng)中則為/home/cunyu1943/IO.md,此時需要注意平臺間的分隔符是不一樣的,但為了同一,推薦同一寫成/,這樣程序在不同系統(tǒng)中遷移時也不會出現(xiàn)問題。而網(wǎng)絡(luò)絕對位置則指帶有網(wǎng)址的路徑,比如https://cunyu1943.site/index.html。
import java.io.File;
import java.io.IOException;
/**
* @author : cunyu
* @version : 1.0
* @className : FilePath
* @date : 2021/4/20 9:55
* @description : 文件路徑
*/
public class FilePath {
public static void main(String[] args) throws IOException {
File file = new File("../data/1.txt");
System.out.println(file.getPath());
System.out.println(file.getAbsolutePath());
System.out.println(file.getCanonicalPath());
}
}

文件和目錄操作
創(chuàng)建與刪除
既然拿到了 File 對象,接下來就是通過操作該對象來進行創(chuàng)建和刪除文件或目錄了,以下是一些 File 類常用的創(chuàng)建和刪除方法。
| 返回值 | 方法 | 描述 |
|---|---|---|
boolean | createNewFile() | 當具有該名稱的文件不存在時,創(chuàng)建一個由該抽象路徑名命名的新空文件;存在時,則創(chuàng)建失敗 |
boolean | mkdir() | 創(chuàng)建由此抽象路徑名命名的目錄 |
boolean | mkdirs() | 創(chuàng)建由此抽象路徑名命名的多級目錄,包括任何必需但不存在的父目錄 |
boolean | delete() | 刪除由此抽象路徑名命名的文件或目錄,刪除目錄的前提是該目錄必須為空 |
import java.io.File;
import java.io.IOException;
/**
* @author : cunyu
* @version : 1.0
* @className : CreateAndDelete
* @date : 2021/4/20 10:40
* @description : 創(chuàng)建&刪除
*/
public class CreateAndDelete {
public static void main(String[] args) throws IOException {
File file1 = new File("D:/PersonalFiles/github/githubCodes/IDEA/TheWay2Java/IOStream/data/2.txt");
if (file1.createNewFile()) {
System.out.println("創(chuàng)建文件成功");
} else {
System.out.println("創(chuàng)建文件失敗");
}
if (file1.delete()) {
System.out.println("刪除文件成功");
} else {
System.out.println("刪除文件失敗");
}
File file2 = new File("D:/PersonalFiles/github/githubCodes/IDEA/TheWay2Java/IOStream/data/demo");
if (file2.mkdir()) {
System.out.println("創(chuàng)建文件夾成功");
} else {
System.out.println("創(chuàng)建文件夾失敗");
}
File file3 = new File("D:/PersonalFiles/github/githubCodes/IDEA/TheWay2Java/IOStream/data/JavaSE/demo");
if (file3.mkdirs()) {
System.out.println("創(chuàng)建多級目錄成功");
} else {
System.out.println("創(chuàng)建多級目錄失敗");
}
}
}

注意:
創(chuàng)建文件時,調(diào)用的是
createNewFile()方法,而創(chuàng)建目錄時調(diào)用的是mkdir()或者mkdirs()方法。我們在調(diào)用時要注意區(qū)分,否則就算你的路徑是文件,當調(diào)用了創(chuàng)建目錄的方法后它也會創(chuàng)建成目錄而非文件。對應(yīng)的,就算你給定的路徑是目錄,當調(diào)用創(chuàng)建文件的方法后它也會創(chuàng)建成文件而非目錄。刪除目錄時,若目錄中有內(nèi)容(目錄、文件),則 不能直接刪除,而是應(yīng)該先刪除目錄中的內(nèi)容,然后才能刪除目錄;
相關(guān)屬性
獲取到 File 對象后,我們可以對其相關(guān)屬性進行判斷,常用方法如下:
| 返回值 | 方法 | 描述 |
|---|---|---|
long | length() | 該抽象路徑名表示的文件的所占字節(jié)大小 |
boolean | canRead() | 該抽象路徑名表示的文件是否可讀 |
boolean | canWrite() | 該抽象路徑名表示的文件是否可寫 |
boolean | canExecute() | 該抽象路徑名表示的文件是否可執(zhí)行 |
import java.io.File;
/**
* @author : cunyu
* @version : 1.0
* @className : Main
* @date : 2021/4/20 11:04
* @description : 相關(guān)屬性
*/
public class Main {
public static void main(String[] args) {
File file = new File("D:/PersonalFiles/github/githubCodes/IDEA/TheWay2Java/IOStream/data/new.txt");
if (file.canExecute()) {
System.out.println("該對象可執(zhí)行");
} else {
System.out.println("該對象不可執(zhí)行");
}
if (file.canRead()) {
System.out.println("該對象可讀");
} else {
System.out.println("該對象不可讀");
}
if (file.canWrite()) {
System.out.println("該對象可寫");
} else {
System.out.println("該對象不可寫");
}
System.out.println("文件大小:" + file.length() + " Byte");
}
}

判斷和獲取
獲取到 File 對象后,我們既可以用它來表示文件,也可以用來表示目錄。而對于文件和目錄的判斷和獲取功能,可以使用如下常用的方法:
| 返回值 | 方法 | 描述 |
|---|---|---|
boolean | isFile() | 測試此抽象路徑名表示的文件是否為普通文件 |
boolean | isDirectory() | 測試此抽象路徑名表示的文件是否為目錄 |
boolean | exists() | 測試此抽象路徑名表示的文件或目錄是否存在 |
String | getPath() | 將抽象路徑轉(zhuǎn)換為路徑字符串 |
String | getAbsolutePath() | 返回此抽象路徑名的絕對路徑名字符串 |
String | getName() | 返回由此抽象路徑名表示的文件或目錄的名稱 |
String[] | list() | 返回字符串數(shù)組,表示該抽象路徑名表示目錄下的文件和目錄 |
File[] | listFiles() | 返回抽象路徑名數(shù)組,表示該抽象路徑名表示目錄下的文件 |
import java.io.File;
/**
* @author : cunyu
* @version : 1.0
* @className : Main
* @date : 2021/4/20 11:15
* @description : 判斷和獲取
*/
public class Main {
public static void main(String[] args) {
File file = new File("D:/PersonalFiles/github/githubCodes/IDEA/TheWay2Java/IOStream/data");
System.out.println(file.isDirectory());
System.out.println(file.isFile());
System.out.println(file.exists());
System.out.println("-------------------------");
System.out.println(file.getPath());
System.out.println(file.getAbsolutePath());
System.out.println(file.getName());
System.out.println("-------------------------");
System.out.println("目錄下的文件和目錄列表:(文件或目錄名)");
for (String path : file.list()) {
System.out.println(path);
}
System.out.println("-------------------------");
System.out.println("目錄下的文件和目錄列表:(完整絕對路徑)");
for (File path : file.listFiles()) {
System.out.println(path);
}
}
}

練習
假設(shè)我們要遍歷 Windows 下 C 盤的 Windows 目錄,并且列出其中文件名和文件大小,而不用列出目錄名,我們可以利用如下代碼來實現(xiàn):
import java.io.File;
/**
* @author : cunyu
* @version : 1.0
* @className : Test
* @date : 2021/4/20 11:40
* @description : 遍歷 C 盤 Windows 目錄下的文件,并打印文件名和大小
*/
public class Test {
public static void main(String[] args) {
File file = new File("C:/windows");
for (File item : file.listFiles()) {
if (item.isFile()) {
System.out.println("文件名:" + item.getName() + "\t文件大小占:" + item.length() + " 字節(jié)");
}
}
}
}

流
什么是流
所謂流,就是一系列數(shù)據(jù)的組合。當我們需要進行數(shù)據(jù)交互的時候,比如在服務(wù)器和客戶端之間進行數(shù)據(jù)交互時,我們此時就可以使用 Java 中的流來實現(xiàn)。Java 中,數(shù)據(jù)的輸入和輸出都是以流的形式來進行的。根據(jù)數(shù)據(jù)流方向的不同,我們可以將其分為:輸入流、輸出流。而根據(jù)處理的數(shù)據(jù)單位不同,可分為:字節(jié)流、字符流。兩者的關(guān)系可以描述為下表:
| 字節(jié)流 | 字符流 | |
|---|---|---|
| 輸入流 | InputStream | Reader |
| 輸出流 | OutputStream | Writer |
而對于字節(jié)流和字符流的選用原則,我們建議遵循如下規(guī)則:如果數(shù)據(jù)能夠通過 Windows 自帶筆記本軟件打開并且能夠讀懂其中的內(nèi)容,則選用字符流,否則選擇字節(jié)流。而如果我們也不知道應(yīng)該使用何種類型的流,則默認使用字節(jié)流。
下圖描述了字節(jié)流和字符流的類層次圖,注意:無論是字節(jié)流還是字符流,其子類名都是以其父類名作為子類名的后綴的。

InputStream

注意,InputStream 并非是并不是一個接口,而是所有字節(jié)輸入流所有類的父類。下面我們主要以 FileInputStream 來舉例,所謂 FileInputStream,就是從文件流中讀取數(shù)據(jù),然后將數(shù)據(jù)從文件中讀取到內(nèi)存,常用方法如下:
| 返回值 | 方法 | 描述 |
|---|---|---|
int | available() | 返回該輸入流中可以讀取的字節(jié)數(shù)的估計值 |
void | close() | 關(guān)閉輸入流并釋放相關(guān)資源 |
int | read(bytep[] b) | 從輸入流讀取一些字節(jié)數(shù),并將其存儲到緩沖區(qū) b |
下面是一個從文件中讀取數(shù)據(jù)到內(nèi)存中的實例,文件內(nèi)容如下:

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* @author : cunyu
* @version : 1.0
* @className : TestInputStream
* @date : 2021/4/20 15:29
* @description : InputStream 實例
*/
public class TestInputStream {
public static void main(String[] args) {
String result = null;
File file = new File("D:/PersonalFiles/github/githubCodes/IDEA/TheWay2Java/IOStream/data/1.txt");
try (InputStream inputStream = new FileInputStream(file)) {
// 讀取輸入流中可以被讀的 bytes 估計值
int size = inputStream.available();
// 根據(jù) bytes 數(shù)創(chuàng)建數(shù)組
byte[] array = new byte[size];
// 數(shù)據(jù)讀取到數(shù)組
inputStream.read(array);
// 數(shù)組轉(zhuǎn)化為字符串
result = new String(array);
} catch (IOException e) {
e.printStackTrace();
}
// 打印字符串
System.out.println(result);
}
}

OutputStream

OutputStream 并非是并不是一個接口,而是所有輸出字節(jié)流的所有類的父類。下面我們主要以 FileOutputStream 來舉例,所謂 FileOutputStream,就是從內(nèi)存中讀取數(shù)據(jù),然后將數(shù)據(jù)從內(nèi)存存放到文件中,常用方法如下:
| 返回值 | 方法 | 描述 |
|---|---|---|
void | write(byte[] b) | 將 b.length 個字節(jié)從指定字節(jié)數(shù)組寫入此文件輸出流 |
void | close() | 關(guān)閉文件輸出流并釋放相關(guān)資源 |
import java.io.*;
/**
* @author : cunyu
* @version : 1.0
* @className : TestOutputStream
* @date : 2021/4/20 15:58
* @description : OutputStream 實例
*/
public class TestOutputStream {
public static void main(String[] args) {
File file = new File("D:/PersonalFiles/github/githubCodes/IDEA/TheWay2Java/IOStream/data/2.txt");
String content = "這是一個 OutputStream 實例!";
try (OutputStream outputStream = new FileOutputStream(file)) {
// 字符串轉(zhuǎn)換為 byte 數(shù)組
byte[] array = content.getBytes();
// 寫入數(shù)據(jù)
outputStream.write(array);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("寫入成功");
}
}


需要注意的點:
字節(jié)流寫入數(shù)據(jù)時如何實現(xiàn)換行?
寫入換行的轉(zhuǎn)義字符的字節(jié)數(shù)組即可,但是需要注意,不同系統(tǒng)下?lián)Q行的轉(zhuǎn)義字符不同,Windows 下為 \r\n,macOS 下為 \r,而 Linux 下為 \m。
字節(jié)流寫入數(shù)據(jù)時如何實現(xiàn)追加?
調(diào)用 public FileOutputStream(String name, boolean append) 這個構(gòu)造方法即可,當 append 為 true 時,表示追加,默認情況下是 false,表示不追加。
字符串中的編解碼問題
編碼
byte[] getBytes():使用平臺默認字符集將該字符串編碼成一系列字節(jié),然后將結(jié)果存儲到新的字節(jié)數(shù)組中;byte[] getBytes(String charsetName):使用指定字符集將該字符串編碼為一系列字節(jié),然后將結(jié)果存儲到新的字節(jié)數(shù)組中;
解碼
String(byte[] bytes):使用平臺默認字符集解碼指定的字節(jié)數(shù)來構(gòu)造新的字符串;String(byte[] bytes, String charsetName):通過指定的字符集解碼指定的字節(jié)數(shù)組來構(gòu)造新的字符串;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
/**
* @author : cunyu
* @version : 1.0
* @className : EncodeAndDecode
* @date : 2021/4/21 9:37
* @description : 編碼和解碼
*/
public class EncodeAndDecode {
public static void main(String[] args) throws UnsupportedEncodingException {
// 編碼
String str = "村雨遙";
byte[] bytes1 = str.getBytes();
byte[] bytes2 = str.getBytes("UTF-8");
byte[] bytes3 = str.getBytes("GBK");
System.out.println(Arrays.toString(bytes1));
System.out.println(Arrays.toString(bytes2));
System.out.println(Arrays.toString(bytes3));
// 解碼
String res1 = new String(bytes1);
String res2 = new String(bytes1, "UTF-8");
String res3 = new String(bytes1, "GBK");
System.out.println(res1);
System.out.println(res2);
System.out.println(res3);
}
}

Writer

當我們要寫入基于字符的數(shù)據(jù)到數(shù)據(jù)源中時,需要使用寫入器 Writer. 以其中的 FileWriter 具體展開,其常用方法如下:
| 返回值 | 方法 | 描述 |
|---|---|---|
void | close() | 先刷新再關(guān)閉流,不能再寫數(shù)據(jù) |
void | flush() | 刷新流,可以繼續(xù)寫數(shù)據(jù) |
void | newLine() | 寫入行分隔符 |
void | write() | 寫入字符或字符串 |
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
/**
* @author : cunyu
* @version : 1.0
* @className : TestWriter
* @date : 2021/4/20 18:35
* @description : Writer 實例
*/
public class TestWriter {
public static void main(String[] args) {
File file = new File("D:/PersonalFiles/github/githubCodes/IDEA/TheWay2Java/IOStream/data/2.txt");
try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(file))) {
bufferedWriter.write("公眾號:村雨遙");
bufferedWriter.newLine();
bufferedWriter.write("Blog:https://cunyu1943.site");
bufferedWriter.newLine();
bufferedWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("寫入成功");
}
}

Reader

當我們要從數(shù)據(jù)源讀取基于字符的數(shù)據(jù)時,需要使用讀取器 Reader. 我們以 FileReader 實踐,其常用的方法有:
| 返回值 | 方法 | 描述 |
|---|---|---|
void | close() | 關(guān)閉流并釋放相關(guān)資源 |
int | read() | 讀取一個字符 |
String | readLine() | 讀一行文字 |
boolean | ready() | 獲取該流是否準備好被讀取 |
我們以從文件中讀取內(nèi)容為例:
import java.io.*;
/**
* @author : cunyu
* @version : 1.0
* @className : TestReader
* @date : 2021/4/20 18:40
* @description : Reader 實例
*/
public class TestReader {
public static void main(String[] args) {
String content = null;
File file = new File("D:/PersonalFiles/github/githubCodes/IDEA/TheWay2Java/IOStream/data/2.txt");
System.out.println("內(nèi)容如下:");
try (BufferedReader bufferedReader = new BufferedReader(new FileReader(file))) {
while ((content = bufferedReader.readLine()) != null) {
System.out.println(content);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

總結(jié)
好了,關(guān)于 IO 流的知識點到此就結(jié)束了,趕緊學起來!如果你覺得本文對你有所幫助,那就點贊關(guān)注一波吧!
對于文中遺漏或者錯誤的知識點,歡迎大家評論留言,咱們評論區(qū)見!



