# Java 輸入輸出流
[TOC]
## 導學
Java的輸入輸出流在我們的日常的使用中,無處不在。只要涉及到數據的傳輸,比如復制粘貼文件,微信,QQ上傳頭像,下載游戲安裝包等都是在利用輸入輸出流。再比如我們之前所學習過的`System.out.println()`,它的作用就是向控制臺輸出一條信息,也是運用了流的概念。
**那么什么是流呢?**
>[info]**流就是指一連串流動的字符,以先進先出的的方式發送信息的通道**

在程序中所有的數據都是以流的方式進行傳輸或保存的,程序需要數據的時候要使用輸入流讀取數據,而當程序需要將一些數據保存起來的時候,就要使用輸出流完成。
**程序中的輸入輸出都是以流的形式保存的,流中保存的實際上全都是字節文件**。
**什么是輸入輸出流**
* 流是個抽象的概念,是對輸入輸出設備的抽象,輸入流可以看作一個輸入通道,輸出流可以看作一個輸出通道。
* 輸入流是相對程序而言的,外部傳入數據給程序需要借助輸入流。
* 輸出流是相對程序而言的,程序把數據傳輸到外部需要借助輸出流。
在java.io包中操作文件內容的主要有兩大類:字節流(二進制數據)、字符流(char和String等類型的文字數據),兩類都分為輸入和輸出操作。在字節流中輸出數據主要是使用OutputStream完成,輸入使的是InputStream,在字符流中輸出主要是使用Writer類完成,輸入流主要使用Reader類完成。
## File類應用
### 文件簡介
首先,什么是文件?**文件可認為是相關記錄或放在一起的數據的集合。**
在實際的存儲數據中,如果對數據的讀寫速度要求不高,而且存儲的數據量也不是很大,此時,可以選擇使用文件這種持久化的存儲方式。
所謂**持久化**,就是當程序退出,或者計算機關機以后,數據還是存在的。但是在程序內存中的數據會在程序關閉或計算機退出時丟失。
文件的組成:路徑+文件的全名(文件名和文件后綴)。
關于文件后綴:只是定義了文件的打開方式不一樣,如果更改后綴不會對文件的內部數據產生變化。
在不同的操作系統中,文件的路徑表示形式是不一樣的。
比如:
`windows c:\windows\system\driver.txt`
`Linux /user/my/tomcat/startup.txt`
>[success] 如果程序需要在不同的操作系統中運行,那么如果出現文件路徑相關的設置時,必須要進行操作系統的判斷,特別是windows和Linux關于斜杠的區別。
>[warning] 針對于不同操作系統的斜杠我們可以使用File類的路徑分隔符常量`File.separator`
### File類應用
`java.io.File`類是一個與文件本身操作有關的類,此類可以實現文件創建,刪除,重命名,取得文件大小,修改日期等常見的系統文件操作
**`File`類常用方法:**
| 方法 | 描述 |
| --- | --- |
| canRead() | 文件是否可讀 |
| canWrite() | 文件是否可寫 |
| exists() | 文件或目錄是否存在 |
| getName() | 獲取文件或路徑的名稱 |
| isDirectory() | 是否是目錄 |
| isFile() | 是否是文件 |
| isHidden() | 是否是隱藏文件 |
|mkdir()|是否創建單級目錄|
|mkdirs()|是否創建多級目錄|
**示例:**
~~~
public class FileDemo {
public static void main(String[] args) {
//創建File對象
//File file1=new File("c:\\dodoke\\io\\score.txt");
//File file1=new File("c:\\dodoke","io\\score.txt");//文件和目錄分成兩個字符串
File file=new File("c:\\dodoke");//File.separator
File file1=new File(file,"io\\score.txt");
//判斷是文件還是目錄
System.out.println("是否是目錄:"+file1.isDirectory());
System.out.println("是否是文件:"+file1.isFile());
//創建目錄
File file2=new File("c:\\dodoke\\set\\HashSet");
if(!file2.exists()) {
file2.mkdirs();
}
//創建文件
if(!file1.exists()) {
try {
file1.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
~~~
### 絕對路徑與相對路徑
#### 絕對路徑
**絕對路徑:是指文件在硬盤上真正存在的路徑。(指對站點的根目錄而言某文件的位置)————以web站點為根目錄為參考基礎的目錄路徑,之所以成為絕對,意指當所有網頁引用同一文件時,所引用的路徑都是一樣的。**
##### 引用本地文件
~~~
Windows系統中的文件絕對路徑
E:\companyWorkSpace\braun\bin\src\main\resources\js\dicList.js
當我們想要引入這樣本地的一個js文件的時候。
寫法:
<script src="file:///E:/companyWorkSpace/braun/bin/src/main/resources/js/dicList.js"></script>
~~~
`file:///`:本地超文本傳輸協議
注意點:需要將路徑中的反斜杠\\改為斜桿/
##### 引用網絡文件
~~~
寫法:
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/4.0.0/css/bootstrap.min.css" />
~~~
`https://`:網絡安全超文本協議
#### 相對路徑
**相對路徑:就是相對于自己的目標文件的位置。(指以當前文件所處目錄而言文件的位置)————以引用文件之間網頁所在位置為參考基礎,而建立出的目錄路徑。因此當保存于不同目錄的網頁引用同一個文件時,所使用的路徑將不相同,故稱之為相對。**
##### 相對路徑的點與斜杠概念
**/、./、../、../../**
* `/`這個斜杠代表的是根目錄的意思,什么是根目錄呢?
**先看例子:**
~~~
F盤中有個文件夾vue_bamboos和一張圖片 test-me.png
vue_bamboos下有一個文件夾 a , a文件夾中有一個文件夾b;
b文件夾下有一個index.html文件;
F-------------------------------------------
vue_bamboos-------------------------
a--------------------------
b-----------------
index.html-
test-me.png-------------------------
index.html:顯示一張圖片test-me.png, 這里我們使用就是根目錄,也就是我們項目目錄的上一級,也就是 F 盤是我們的根目錄;
注意,我們的項目目錄是vue_bamboos,但是vue_bamboos不是根目錄,它的上一級才是!!!
<body>
<img src="/test-me.png" alt="測試根目錄">
</body>
~~~
* `./`這個代表的是當前目錄,也就是和我們的index.html 在同一級上
**先看例子**
~~~
假設我們的項目目錄:
F---------------------------------
vue_bamboos---------------
index.html------
test-me.png-----
<body>
<img src="./test-me.png" alt="測試當前目錄">
<img src="test-me.png" alt="測試當前目錄">
</body>
也就是說我們可以這樣寫 ./test-me.png 或者省略 ./ 也是可以的, 直接寫 test-me.png
~~~
* `../`?這個代表的意思是返回到上一級目錄;?
**先看例子**
~~~
假設我們的項目目錄:
F-------------------------------------
vue_bamboos-------------------
index.html----------
b------------------------------
test-me.png---
<body>
<img src="../b/test-me.png" alt="測試父目錄">
</body>
也就是說我們先找到index.html所在的vue_bamoos這個文件夾,再在vue_bamoos文件夾的上級目錄F盤中,找到b文件夾,最后找到test-me.png
~~~
> 第四個?`../../`? ? ? ? ?這個代表的是返回到上一級,再向上返回一級,返回了兩級;
> 第五個?`../../../`? ? ? 這個比上面的多了一級,那么就是向上返回三級了;
##### eclipse中的相對路徑
在eclipse中路徑比較特殊,clipse中java程序的當前工作目錄都是在項目根目錄下的。
## 字節流
字節流常用于讀取圖像,音頻,視頻之類的二進制字節數據。
### 字節輸入流`InputStream`
`InputStream`是字節輸入流的父類,以下是`Inputstream`的子類結構圖

> [info]常用的輸入流有`FileInputStream`,`ObjectInputStream`,`BufferedInputStream`
#### 文件輸入流`FileInputStream`
該類用于從文件系統中的某個文件中獲得輸入字節,用于讀取諸如圖像數據之類的原始字節流。
**常用方法:**
| 方法名 | 描述 |
| --- | --- |
| public int read() | 從輸入流中讀取一個數據字節 |
| public int read(byte\[\] b) | 從輸入流中將最多b.length個字節的數據讀入到一個byte數組中 |
| public int read(byte\[\] b,int off,int len) | 從此輸入流中將最多`len`個字節的數據讀入一個 byte 數組中。 |
| public void close() | 關閉此文件輸入流并釋放與此流有關的所有系統資源。 |
>[warningi] 除了第一個方法可以得到讀取到的下一個數據字節,這三個read()方法,如果返回值為-1,則表示已經到達文件末尾。可以用來作為文件是否讀完的一個標志。
~~~java
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class FileInputDemo {
public static void main(String[] args) {
//創建一個FileInputStream對象
try {
FileInputStream fis = new FileInputStream("d:"+File.separator + "fileTest" + File.separator + "abcNew.txt");
//********************無參read()方法使用**********************
/*int n = fis.read();
System.out.println((char)n);//得到H
*/
/*int n = fis.read();
while(n != -1) {
System.out.println((char)n);
n = fis.read();
}*/
//為了提升代碼的簡潔性
/*int n = 0;
while((n = fis.read()) != -1) {
System.out.println((char)n);
}*/
//*******************read(byte[] b)和read(byte[] b,int off,int len)方法的使用*****************
/*byte[] b = new byte[100];
fis.read(b);
System.out.println(new String(b));*/
byte[] b = new byte[100];
fis.read(b,0,5);
System.out.println(new String(b));
fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
~~~
### 字節輸出流`OutputStream`
`OutputStream`是字節輸出流的父類,以下是`Outputstream`的子類結構圖

>[success] 常用的輸入流有`FileOutputStream`,`ObjectOutputStream`,`BufferedOutputStream`
#### 文件輸出流`FileOutputStream`
該類用來講數據寫入到文件系統的文件中
**常用方法:**
| 方法名 | 描述 |
| --- | --- |
| public void write(int b) | 將指定字節寫入此文件輸出流。 |
| public void write(byte\[\] b) | 將`b.length`個字節從指定 byte 數組寫入此文件輸出流中。 |
| public void write(byte\[\] b,int off,int len) | 將指定 byte 數組中從偏移量`off`開始的`len`個字節寫入此文件輸出流。 |
| public void close() | 關閉此文件輸出流并釋放與此流有關的所有系統資源。 |
~~~java
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputDemo {
public static void main(String[] args) {
String path = "d:"+File.separator + "fileTest" + File.separator + "abcNew.txt";
FileOutputStream fos;
FileInputStream fis;
try {
fos = new FileOutputStream(path,true);
fis = new FileInputStream(path);
/*fos.write(50);
fos.write('e');
//編碼問題導致
System.out.println(fis.read());
System.out.println((char)fis.read());*/
String content = "\nsay Hello World again!";
fos.write(content.getBytes());
fos.close();
fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
~~~
**文件拷貝**
~~~java
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputDemo {
public static void main(String[] args) {
String path = "d:"+File.separator + "picture" + File.separator + "20170315inHome.jpg";
String copyPath = "d:"+File.separator + "fileTest" + File.separator + "flower.jpg";
FileInputStream fis;
FileOutputStream fos;
try {
//創建輸入流
fis = new FileInputStream(path);
File file = new File(copyPath);
//判斷文件的父路徑是否存在,如果不存在則新建目錄
if(!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
//創建新文件
file.createNewFile();
//創建輸出流
fos = new FileOutputStream(copyPath);
int n = 0;
byte[] b = new byte[1024];
while((n = fis.read(b)) != -1) {
//一邊讀取輸入流,一邊寫入到新建的文件中去
//fos.write(b);
//為了保證最后復制文件和源文件同樣大小
fos.write(b, 0, n);
}
fos.close();
fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
~~~
### 緩沖流
* 緩沖輸入流`BufferedInputStream`
* 緩沖輸出流`BufferedOutputStream`
相較于之前直接從硬盤中讀取數據,利用緩沖輸入輸出流,可以從內存中讀取數據,可以極大的提高讀寫速度。
以緩沖輸入流為例,它是不能直接讀取文件系統中的文件的,需要和文件輸入流結合。讀取原理是,文件輸入流首先從文件系統中讀取數據,讀取到數據后不是直接到程序中,而是給緩沖流讀取到字節數組中。輸出流也是一樣。
>[warning] 針對于緩沖輸出流,如果緩沖區已滿,則執行文件寫入操作,如果緩沖區不滿需要先執行`flush()`方法刷新緩沖區。
~~~java
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class BufferedIODemo {
public final static String PATH = "d:"+File.separator + "fileTest" + File.separator + "abcNew.txt";
public static void main(String[] args) {
setBufferedIO();
}
public static void setBufferedIO() {
try {
//首先利用文件輸出流,執行文件的寫操作
FileOutputStream fos = new FileOutputStream(PATH);
BufferedOutputStream bos = new BufferedOutputStream(fos);
//讀操作
FileInputStream fis = new FileInputStream(PATH);
BufferedInputStream bis = new BufferedInputStream(fis);
//利用緩沖輸出流進行寫操作,需要注意的是此時數據是寫在緩沖區的,如果緩沖未滿,且沒有調用flush()方法,則不會寫入文件
bos.write(50);
bos.write('a');
//一般會要求同時寫flush和close
bos.flush();
System.out.println(bis.read());
System.out.println((char)bis.read());
fos.close();
//close()方法會釋放有關bos的一切資源,所以調用close()方法,也會執行寫入操作
bos.close();
fis.close();
bis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
~~~
## 字符流
Java提供了字符流便于針對字符串的輸出,尤其是針對中文的數據處理,都會采用字符流的形式。
### 字符輸入流`Reader`
`Reader`是字符輸入流的父類,以下是字符輸入流的結構圖

>[info] 常用的字符輸入流有`BufferedReader`,`InputStreamReader`,`FileReader`
#### 字節字符轉換輸入流
轉換流,可以將一個字節流轉換為字符流,也可以將一個字符流轉換為字節流。
`InputStreamReader`:**將輸入的字節流轉換為字符流輸入形式。**
~~~java
public class ReadDemo {
public final static String PATH = "d:"+File.separator + "fileTest" + File.separator + "abcNew.txt";
public static void main(String[] args) {
FileInputStream fis;
InputStreamReader isr;
try {
fis = new FileInputStream(PATH);
//這里其實是一個流的連接
isr = new InputStreamReader(fis);
int n = 0;
/*while((n = isr.read()) != -1) {
System.out.println((char)n);
}*/
char[] charArr = new char[15];
while((n = isr.read(charArr)) != -1) {
//System.out.println(new String(charArr));
//根據操作系統和字符編碼的不同,可能最后的讀取的效果不經如人意
System.out.println(new String(charArr,0,n));
}
fis.close();
isr.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
~~~
### 字符輸出流`Writer`
Writer是字符輸出流的父類,以下是字符輸出流的結構圖

> 常用的字符輸出流有`BufferedWriter`,`InputStreamWriter`,`FileWriter`
#### 字節字符轉換輸出流
轉換流,可以將一個字節流轉換為字符流,也可以將一個字符流轉換為字節流。
`OutputStreamWriter`:**可以將輸出的字符流轉換為字節流的輸出形式。**
~~~java
public class WriterDemo {
public final static String PATH = "d:"+File.separator + "fileTest" + File.separator + "abcNew.txt";
public final static String PATH2 = "d:"+File.separator + "fileTest" + File.separator + "abcNew2.txt";
public static void main(String[] args) {
FileInputStream fis;
InputStreamReader isr;
FileOutputStream fos;
OutputStreamWriter ost;
try {
fis = new FileInputStream(PATH);
isr = new InputStreamReader(fis,"UTF-8");
//其實這個文件并沒有但是文件輸出流會幫我們自動創建一個
fos = new FileOutputStream(PATH2);
ost = new OutputStreamWriter(fos,"UTF-8");
int n = 0;
char[] charArr = new char[13];
while((n = isr.read(charArr)) != -1) {
ost.write(charArr, 0, n);
ost.flush();
}
fis.close();
isr.close();
fos.close();
ost.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
~~~
>[success] 讀寫的時候需要保持編碼的一致
### 字節字符轉換緩沖流
`BufferedReader`和`BufferedWriter`
~~~java
public class BufferedRWDemo {
public final static String PATH = "d:"+File.separator + "fileTest" + File.separator + "abcNew.txt";
public final static String PATH2 = "d:"+File.separator + "fileTest" + File.separator + "abcNew2.txt";
public static void main(String[] args) {
FileInputStream fis;
InputStreamReader isr;
FileOutputStream fos;
OutputStreamWriter ost;
try {
fis = new FileInputStream(PATH);
isr = new InputStreamReader(fis,"UTF-8");
//其實與之前的緩沖輸入輸出流相比,這里出現的是一個三層的連接
BufferedReader br = new BufferedReader(isr);
//其實這個文件并沒有但是文件輸出流會幫我們自動創建一個
fos = new FileOutputStream(PATH2);
ost = new OutputStreamWriter(fos,"UTF-8");
BufferedWriter bw = new BufferedWriter(ost);
int n = 0;
char[] charArr = new char[13];
while((n = br.read(charArr)) != -1) {
bw.write(charArr, 0, n);
bw.flush();
}
fis.close();
isr.close();
fos.close();
ost.close();
br.close();
bw.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
~~~
### 總結
字符流與字節流中的類多種多樣,不可能全部講完,但是各個類的使用大多是大同小異。
在字符流中使用字節流去讀寫文件,其實只是為了模擬網絡文件的讀寫操作。字符流有著自己的讀寫文件的類`FileReader`和`FileWriter`
## 對象序列化與反序列化
針對于對象序列化,我們可以設想這樣一個場景。比如,我們在同別人聊天的過程中,僅僅是把一條信息發送過去了嗎?并不是這樣的,發送的信息中可能包含發送方的ip,接收方的ip,以及端口號,聊天內容,昵稱等等,那么這些信息如果一條一條的發送過去是沒有辦法接收的。
所以,我們可以把這些信息封裝到一個類中,然后把包含這些信息的對象發送過去,然后接收方就可以根據這些信息作出反應。那么在這個場景中如何去發送對象內容,以及如何去解析對象的內容,就可以通過**對象序列化**技術解決。
**對象序列化的實現步驟:**
1. 創建一個類,實現Serializable接口(只有當類實現了Serializable接口才能序列化與反序列化)
2. 創建該類的對象
3. 將對象寫入文件(不一定要寫入文件,也可以寫入網絡中)
4. 從文件讀取對象信息
>[info]對象序列化需要借助兩個類,`ObjectInputStream()`和`ObjectOutputStream()`
>[success]序列化:把Java對象轉換為字節序列的過程。反序列化:把字節序列恢復為Java對象的過程。
~~~
import java.io.Serializable;
public class Goods implements Serializable{
private String goodsId;
private String goodsName;
private double price;
public Goods(String goodsId,String goodsName,double price){
this.goodsId=goodsId;
this.goodsName=goodsName;
this.price=price;
}
public String getGoodsId() {
return goodsId;
}
public void setGoodsId(String goodsId) {
this.goodsId = goodsId;
}
public String getGoodsName() {
return goodsName;
}
public void setGoodsName(String goodsName) {
this.goodsName = goodsName;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
@Override
public String toString() {
return "商品信息 [編號:" + goodsId + ", 名稱:" + goodsName
+ ", 價格:" + price + "]";
}
}
~~~
~~~
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class GoodsTest {
public static void main(String[] args) {
// 定義Goods類的對象
Goods goods1 = new Goods("gd001", "電腦", 3000);
try {
FileOutputStream fos = new FileOutputStream("dodoke.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
FileInputStream fis = new FileInputStream("dodoke.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
// 將Goods對象信息寫入文件
oos.writeObject(goods1);
oos.writeBoolean(true);
oos.flush();
// 讀對象信息
try {
Goods goods = (Goods) ois.readObject();
System.out.println(goods);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(ois.readBoolean());//讀取的順序要和寫入的順序一致
fos.close();
oos.close();
fis.close();
ois.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
~~~