I/O流
基本概念
什麼是 I/O?
通過 I/O 可以完成硬碟檔案的讀和寫。
概念圖:輸入、輸出是相對於記憶體而言的,以記憶體為參照物。
1、I/O 流的分類方式:
-
按照流的方向進行分類:
以記憶體為參照物,往記憶體中去稱為輸入(Input)或者讀(Read)。
-
按照讀取資料方式不同進行分類:
有的流是按照位元組的方式讀取資料,一次讀取 1 個位元組(byte)= 8 bit。這種流是萬能的,什麼型別檔案都可以讀取(文字檔案、音訊檔案、圖片檔案等等)。
-
按照字元的方式讀取資料,一次讀取一個字元,這種流是為了方便讀取普通文字檔案(能用記事本編輯的檔案,比如java檔案、txt 檔案等)而存在的,不能讀取其他型別的檔案,甚至 word 檔案也無法讀取( word 檔案不是普通文字)。
示例:a中國bc張三fe
a
等字元在 windows 佔用一個位元組(但是在Java中佔用兩個位元組),中
等漢字在 windows 佔用兩個位元組。
按位元組讀:
第一次:一個位元組,讀到a
;
第二次:一個位元組,讀到中
字元的一半;
……
按字元讀:
第一次:一個字元,讀到a
;
第二次:一個字元,讀到中
;
……
2、I/O 四大流:(都是抽象類)
java.io.InputStream
:位元組輸入流
java.io.OutputStream
:位元組輸出流
java.io.Reader
:字元輸入流
java.io.Writer
:字元輸出流
所有的流都實現了java.io.Closeable
介面,都有close()
所有的輸出流都實現了java.io.Flushable
介面,都是可重新整理的,都有flush()
方法表示將通道當中剩餘未輸出的資料強行輸出完(清空管道),輸出流在最終輸出之後一定要記得重新整理一下清空管道。(如果沒有flush()
可能會導致丟失資料)
注意:在 java 中只要類名以stream
結尾的都是位元組流;以 Reader/Writer 結尾的都是字元流。
檔案專屬:
1、java.io.FileInputStream
(掌握)
public static void main(String[] args){ //建立檔案位元組流輸入物件 FileInputStream files = null; try{ /* 方式一: files = new FileInputStream("E:/java課堂筆記/file"); * */ //方式二:檔案絕對路徑是“E:\java課堂筆記\file”,"\\"其中一個是轉義字元表示“\” files=new FileInputStream("E:\\java課堂筆記\\file"); //儲存內容:abcdef /* 讀資料 int readeData=files.read(); //返回值:讀取到的位元組本身,讀到檔案末尾沒有資料就返回-1 System.out.println(readeData);//結果:97 (字元a的ASCII碼)*/ /*while(true){//迴圈讀出 int readeData=files.read(); if(readeData == -1){ break; } System.out.println(readeData); }*/ //改造迴圈 int readeData; while((readeData=files.read()) != -1){ System.out.println(readeData); } } catch(FileNotFoundException e){ e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { //確保一定關閉流 if(files != null){ //如果files為空則不用關閉流 try { files.close(); } catch (IOException e) { e.printStackTrace(); } } } }
注意:如果是空格則返回 32 。
缺點:一次讀取一個位元組,硬碟和記憶體的資料互動太頻繁效率較低,開發時使用較少。
改進:使用int read(byte[] b)
一次最多讀取b.length
個位元組,減少硬碟和記憶體的互動,提高程式的執行效率,往byte[]
陣列當中讀。
相對路徑是從當前所在的位置作為起點。
IDEA預設的當前路徑是工程Project的根。
例如:上圖中的根就是ideaProjects
,和train
並列的檔案路徑為
files = new FileInputStream("file");
//在train資料夾中的檔案
files = new FileInputStream("train/file");
讀取資料:
public static void main(String[] args){
//建立檔案位元組流輸入物件
FileInputStream files = null;
try{
/*
方式一:
files = new FileInputStream("E:/java課堂筆記/file");
* */
//方式二:檔案絕對路徑是“E:\java課堂筆記\file”,"\\"其中一個是轉義字元表示“\”
files=new FileInputStream("E:\\java課堂筆記\\file"); //儲存內容:abcdef
//準備一個4個長度的byte陣列,一次最多讀取4個位元組
byte[] bytes = new byte[4];
//返回值:讀取到的位元組數量
int readCount = files.read(bytes);
System.out.println(readCount);//第一次讀到4個位元組
readCount = files.read(bytes);//第二次只能讀到2個位元組
System.out.println(readCount);//2
readCount = files.read(bytes);//第三次一個位元組都沒,返回-1
System.out.println(readCount);//-1
} catch(FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//確保一定關閉流
if(files != null){
//如果files為空則不用關閉流
try {
files.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
將讀出的位元組陣列轉換成字串輸出:
//準備一個4個長度的byte陣列,一次最多讀取4個位元組
byte[] bytes = new byte[4];
//返回值:讀取到的位元組數量
int readCount = files.read(bytes);
System.out.println(readCount);//第一次讀到4個位元組
System.out.println(new String(bytes));//返回:abcd
readCount = files.read(bytes);//第二次只能讀到2個位元組
System.out.println(readCount);//2
System.out.println(new String(bytes));//返回:efcd
readCount = files.read(bytes);//第三次一個位元組都沒,返回-1
System.out.println(readCount);//-1
由以上程式碼看出第二次讀出的資料是efcd
,與我們的期望不符合,我們應該讀取多少個位元組就轉換多少個。
System.out.println(new String(bytes,0,readCount));
//表示從陣列bytes的第0個下標開始,讀取長度為readCount的位元組陣列
讀取的最終版本:
public static void main(String[] args){
//建立檔案位元組流輸入物件
FileInputStream files = null;
try{
/*
方式一:
files = new FileInputStream("E:/java課堂筆記/file");
* */
//方式二:檔案絕對路徑是“E:\java課堂筆記\file”,"\\"其中一個是轉義字元表示“\”
files=new FileInputStream("E:\\java課堂筆記\\file"); //儲存內容:abcdef
//準備一個4個長度的byte陣列,一次最多讀取4個位元組
byte[] bytes = new byte[4];
/* while(true){
//返回值:讀取到的位元組數量
int readCount = files.read(bytes);
if(readCount = -1){
break;
}
System.out.println(new String(bytes,0,readCount));
}*/
//改進
int readCount = 0;
while((readCount = files.read(bytes)) != -1){
System.out.println(new String(bytes,0,readCount));
}
} catch(FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//確保一定關閉流
if(files != null){
//如果files為空則不用關閉流
try {
files.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
FileInputStream
類的常用方法:
1、int available()
返回流當中剩餘的沒有讀到的位元組數量。
應用:獲取檔案的總位元組數量,然後通過read()
方法一次性讀完,但是因為陣列不能太大所以不適合太大的檔案。
public static void main(String[] args){
//建立檔案位元組流輸入物件
FileInputStream files = null;
try{
//方式二:檔案絕對路徑是“E:\java課堂筆記\file”,"\\"其中一個是轉義字元表示“\”
files=new FileInputStream("E:\\java課堂筆記\\file"); //儲存內容:abcdef
System.out.println("總位元組數量:"+ files.available());
//準備一個4個長度的byte陣列,一次最多讀取4個位元組
byte[] bytes = new byte[files.available()];
//知道了總位元組數不需要迴圈了,一次性讀取完
int readCount = files.read(bytes);
System.out.println(new String(bytes));
} catch(FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//確保一定關閉流
if(files != null){
//如果files為空則不用關閉流
try {
files.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2、long skip(long n)
跳過幾個位元組不讀。
public static void main(String[] args){
//建立檔案位元組流輸入物件
FileInputStream files = null;
try{
//方式二:檔案絕對路徑是“E:\java課堂筆記\file”,"\\"其中一個是轉義字元表示“\”
files=new FileInputStream("E:\\java課堂筆記\\file"); //儲存內容:abcdef
System.out.println("總位元組數量:"+ files.available());
//skip跳過幾個位元組不讀取
files.skip(3);//表示從 d 開始讀
System.out.println(files.read());//返回:100 (即d的ASCII值)
} catch(FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//確保一定關閉流
if(files != null){
//如果files為空則不用關閉流
try {
files.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2、java.io.FileOutputStream
(掌握):將資料從記憶體中寫到硬碟中。
1)第一種:每次都是先將檔案清空,然後重新寫入資料。
public static void main(String[] args){
//建立檔案位元組流輸入物件
FileOutputStream fos = null;
try{
//方式二:檔案絕對路徑是“E:\java課堂筆記\file”,"\\"其中一個是轉義字元表示“\”
fos = new FileOutputStream("E:\\java課堂筆記\\myfile"); //將資料寫入“E:\java課堂筆記\myfile”,檔案不存在時會自動建立
//開始寫
byte[] bytes = {97,98,99,100,101};//寫入abcde
//將bytes陣列全部寫出
fos.write(bytes);
//將bytes陣列的一部分寫出
fos.write(bytes,0,2);//再寫出ab,即abcdeab
//寫完之後,最後一定要重新整理
fos.flush();
} catch(FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//確保一定關閉流
if(fos != null){
//如果files為空則不用關閉流
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2)FileOutputStream(String name,boolean append)
寫入的內容寫在檔案已有的資料後面
public static void main(String[] args){
//建立檔案位元組流輸入物件
FileOutputStream fos = null;
try{
//方式二:檔案絕對路徑是“E:\java課堂筆記\file”,"\\"其中一個是轉義字元表示“\”
//此處要處理異常
fos = new FileOutputStream("E:\\java課堂筆記\\myfile",true); //將資料寫入“E:\java課堂筆記\myfile”,檔案不存在時會自動建立
//開始寫
byte[] bytes = {97,98,99,100,101};//寫入abcde
//將bytes陣列全部寫出
fos.write(bytes);
//將bytes陣列的一部分寫出
fos.write(bytes,0,2);//再寫出ab,即abcdeab
//寫出字串
String s = "我是誰";
//將字串轉換成byte陣列
byte[] b = s.getBytes();
//寫出
fos.write(b);
//寫完之後,最後一定要重新整理
fos.flush();//此處要處理異常
} catch(FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//確保一定關閉流
if(fos != null){
//如果files為空則不用關閉流
try {
fos.close();//此處要處理異常
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
每次執行後都是在原有資料的基礎上追加資料。
3)檔案的拷貝(FileInputStream、FileOutputStream)
拷貝過程:檔案一邊輸入記憶體,一邊從記憶體中輸出。(一邊讀一邊寫)使用以上的位元組流拷貝檔案的時候,檔案型別隨意,什麼檔案(資料夾不能,只是單個檔案)都能拷貝。
public static void main(String[] args){
FileInputStream fis = null;
FileOutputStream fos = null;
try{
//建立一個輸入流物件
fis = new FileInputStream("D://javase");
//建立一個輸出流物件
fos = new FileOutputStream("E://javase");
//核心:一邊讀一邊寫
byte[] bytes = new byte[1024 * 1024];//一次最多讀取1MB
int readCount = 0;
while((readCount = fis.read(bytes)) != -1){
fos.write(bytes,0,readCount);
}
//輸出流最後要重新整理
fos.flush();
}catch(FileNotFoundException e){
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
}finally{
//這裡輸入輸出流的關閉的異常要分開處理;如果一起處理有一個出現異常,可能會影響到另一個流的關閉
if(fos != null){
try{
fos.close();
}catch(IOException e){
e.printStackTrace();
}
}
if(fis != null){
try{
fis.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
}
3、java.io.FileReader
檔案字元輸入流(讀,記憶體讀取硬碟資訊),只能讀普通文字(能用記事本編輯的檔案,比如text檔案等)。
public static void main(String[] args){
FileReader reader = null;
try{
reader = new FileReader("E:\\java課堂筆記\\myfile");
/*
顯示讀出的資訊
//準備一個char陣列
char[] chars = new char[4];
//往char陣列中讀
reader.read(chars);//按字元方式讀取,第一次a,第二次b,…(一個漢字也是一個字元)
//遍歷
for(char c : chars){
System.out.println(c);
}
*/
//一次讀取4個字元
char[] chars = new char[4];
int readCount = 0;
while((readCount = reader.read(chars)) != -1){
System.out.print(new String(chars,0,readCount));
}
}catch(FileNotFoundException e){
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
}finally{
if(reader != null){
try{
reader.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
}
4、java.io.FileWriter
檔案字元輸出流(寫,記憶體寫入硬碟)只能輸出普通文字。
public static void main(String[] args){
FileWriter out = null;
try{
//建立檔案字元輸出流物件,沒有該檔案時會自動建立
out = new FileWriter("file");
/* //寫入時緊接著檔案內容的後面寫入,不會清空檔案內容重新寫入
out = new FileWriter("file",true);
*/
//開始寫
char[] chars = {'躬','行'};
//會將檔案內容清空然後重新寫入
out.write(chars);
//接著寫入陣列的一部分,從下標為0開始長度為1
out.write(chars,0,1);
//寫入一個字串
out.write("我是一名程式設計師");
//重新整理
out.flush();
}catch(IOException e){
e.printStackTrace();
}finally{
if(out != null){
try{
out.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
}
複製普通文字檔案的過程和複製位元組流檔案相同。
緩衝流專屬:
1、java.io.BufferedReader
帶有緩衝區的字元輸入流。使用這個流的時候不需要自定義 char 陣列,或者說不需要自定義 byte 陣列,自帶緩衝。
public static void main(String[] args) throws Exception{//此處將異常統一處理,不需要try-catch
FileReader reader = new FileReader("copy.java");
//當一個流的構造方法中需要一個流的時候,這個被傳進來的流叫做:節點流。
//外部負責包裝的這個流叫做:包裝流(也叫處理流)
//FileReader就是一個節點流,BufferedReader就是包裝流(處理流)
BufferedReader br = new BufferedReader(reader);
/*
一行一行的讀:
String firstLine = br.readLine();
System.out.println(firstLine);
String secondLine = br.readLine();
System.out.println(secondLine);
*/
//當readLine()返回值為null時讀操作結束
String s = null;
while((s = br.readLine()) != null){//br.readLine()方法讀取一個文字行,但不會換行,文字會輸出在同一行
System.out.println(s);//輸出一行文字後換行
}
//關閉流,對於包裝流來說,只需要關閉包裝流就行了,因為包裝流的close()方法中呼叫了節點流的close()方法
br.close();
}
觀察原始碼發現:關閉流時,對於包裝流來說,只需要關閉包裝流就行了,因為包裝流的close()方法中呼叫了節點流的close()方法。
轉換流:(將位元組流轉換成字元流)
1、java.io.InputStreamReader
public static void main(String[] args) throws Exception{//此處將異常統一處理,不需要try-catch
//位元組流
FileInputStream in = new FileInputStream("copy.java");
//通過轉換流轉換(InputStreamReader將位元組流轉換成字元流)
InputStreamReader reader = new InputStreamReader(in);//in是節點流,reader是包裝流
//這個構造方法只能傳一個字元流,不能傳位元組流
BufferedReader br = new BufferedReader(reader);//reader是節點流,br是包裝流
//合併寫法
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("copy.java")));
//輸出
String line = null;
while((line = br.readLine()) != null){
System.out.println(line);
}
}
2、java.io.OutputStreamWriter
帶有緩衝區的字元輸出流。
public static void main(String[] args) throws Exception{//讓寫入內容追加在原內容之後
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("file",true)));
//開始寫
out.write("hello world");
out.write("\n"); //換行符
out.write("world");
//重新整理
out.flush();
//關閉最外層
out.close();
}
資料流專屬:
1、java.io.DataOutputStream
這個流可以將資料連同資料型別一併寫入檔案,這個檔案不是普通文字文件(用記事本打不開。)
public static viod main(String[] args) throws Exception{
//建立資料專屬的位元組流
DataOutputStream dos = new DataOutputStream(new FileOutputStream("data"));
//寫資料
byte b = 100;
long l = 400L;
double d = 3.14;
char c = 'a';
//寫,把資料及資料型別一併寫入檔案中
dos.writeByte(b);
dos.writeLong(l);
dos.writeDouble(d);
dos.writeChar(c);
//重新整理
dos.flush();
//關閉最外層
dos.close();
}
OutputStream
是抽象類不能例項化,可以例項化它的子類FileOutputStream
。
使用DataOutputStream
寫入的資料無法用記事本開啟(開啟是亂碼),我們必須使用DataInputStream
還要按照寫入時的順序讀出來。
2、java.io.DataInputStream
資料位元組輸入流,讀DataOutputStream
寫的檔案,讀的時候需要提前知道寫入的順序,讀的順序和寫的資料順序一致才能正常讀取出資料。
public static void main(String[] args) throws Exception{
DataInputStream dis = new DataInputStream(new FileInputStream("data"));
//讀取資料
byte b = dis.readByte();
long l = dis.readLong();
double d = dis.readDouble();
char c = dis.readChar();
System.out.println(b);
System.out.println(l);
System.out.println(d);
System.out.println(c);
dis.close();
}
標準輸出流:
java.io.PrintStream
(掌握)
標準的位元組輸出流,預設輸出到控制檯。
public static void main(String[] args){
PrintStream ps = System.out;
ps.println("hello world");
//合併寫法
System.out.println("hello world");
//標準輸出流不需要手動關閉
//標準輸出流不再指向控制檯,指向“log”檔案
PrintStream printStream = new PrintStream(new FileOutputStream("log"));
//修改輸出方向為“log”檔案
System.setOut(printStream);
//輸出
System.out.println("hello world");
}
setOut()
的引數為PrintStream
型別的,而PrintStream
的引數是OutputStream
抽象類不能例項化,可以用其子類FileOutputStream
代替。
記錄日誌檔案的方法:
public class Logger{
public static void log(String msg){
try{
//指向一個日誌檔案
PrintStream out = new PrintStream(new FileOutputStream("log.txt",true));
//改變輸出方向
System.setOut(printStream);
//日期當前時間
Date nowTime = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String strTime = sdf.format(nowTime);
System.out.println(strTime+":"+msg);
}catch(FileNotFoundException e){
e.printStackTrace();
}
}
}
//測試日誌工具類
public class LogTest{
public static void main(String[] args){
Logger.log("呼叫了。。");
}
}
結果:在“log.text”檔案中列印了2020-07-18 ……:呼叫了。。