本部分總結一下JAVA IO的相關知識。
全部章節傳送門:
JAVA IO概要
JAVA I/O主要包括以下幾個部分:
- 流式部分--IO的主體部分;
- 非流式部分--主要包含一些輔助流式部分的類,如File類;
- 其它類--文件讀取部分與安全/操作系統等相關的類。
層次如下圖:
其中最核心的是5個類和1個接口。5個類指File、OutputStream、InputStream、Writer、Reader;1個接口指Serializable。
常用類的介紹見下表:
類 | 說明 | 描述 |
---|---|---|
File | 文件類 | 用于文件或者目錄的描述信息,例如生成新目錄,修改文件名,判斷文件路徑 |
RandomAccessFile | 隨機存取文件類 | 一個獨立的類,直接繼承Object,可以從文件的任意位置進行存取操作 |
InputStream | 字節輸入流 | 抽象類,基于字節的輸入操作,是所有輸入流的父類 |
OutputStream | 字節輸出流 | 抽象類,基于字節的輸入操作,是所有輸出流的父類 |
Reader | 字符輸入流 | 抽象類,基于字符的輸入操作 |
Writer | 字符輸出流 | 抽象類,基于字符的輸出操作 |
流的概念
《Thinking in Java》中介紹,流代表任何有能力產出數據的數據源對象或者是有能力接受數據的接收端對象。
Java中的將輸入輸出抽象成為流,將兩個容器連接起來。流是一組有順序的,有起點和終點的字節集合,是對數據傳輸的總稱或抽象,即數據在兩設備間的傳輸成為流。
Java的IO模型使用裝飾者(Decorator)模式,按功能劃分Stream,可以動態裝配這些Stream,以便獲得需要的功能。例如:當你需要一個具有緩沖的文件輸入流,可以組合使用FileInputStream和BufferedInputStream。
流的分類
根據處理數據類型不同分為:字符流和字節流。
根據數據流向不同分為:輸入流和輸出流。
根據數據來源分類:
- File: FileInputStream, FileOutputStream, FileReader, FileWriter
- byte[]: ByteArrayInputStream, ByteArrayOutputStream
- Char[] CharArrayReader, CharArrayWriter
- String: StringBufferInputStream, StringReader, StringWriter
- 網絡數據流: InputStream, OutputStream, Reader, Writer
字符流和字節流
字符流的由來: 因為數據編碼的不同,而有了對字符進行高效操作的流對象。本質其實就是基于字節流讀取時,去查了指定的碼表。Java中字符采用Unicode標準,一個字符2個字節。
字節流和字符流的區別:
讀寫單位不同:字節流以字節(8bit)為單位,字符流以字符為單位,根據碼表映射字符,一次可能讀多個字節。
處理對象不同:字節流能處理所有類型的數據(如圖片、avi等),而字符流只能處理字符類型的數據。
字節流在操作的時候本身是不會用到緩沖區的,是文件本身的直接操作的;而字符流在操作的時候下后是會用到緩沖區的,是通過緩沖區來操作文件,我們將在下面驗證這一點。
結論:優先選用字節流。首先因為硬盤上的所有文件都是以字節的形式進行傳輸或者保存的,包括圖片等內容。但是字符只是在內存中才會形成的,所以在開發中,字節流使用廣泛。
輸入流和輸出流
對輸入流只能進行讀操作,對輸出流只能進行寫操作,程序中需要根據待傳輸數據的不同特性而使用不同的流。
字節流
InputStream和OutputStream為各種輸入輸出字節流的基類,所有字節流都繼承這兩個基類。
常見的字節流(以輸入流為例,輸出一樣)包括:
- ByteArrayInputStream 它包含一個內部緩沖區,該緩沖區包含從流中讀取的字節數組。
-
StringBufferInputStream將String轉換為InputStream,已經廢棄,不再使用。 - FileInputStream 文件字節輸入流,對文件數據以字節的形式進行讀取操作如讀取圖片視頻等。
- PipedInputStream 管道輸出流,讓多線程可以通過管道進行線程間的通訊。在使用管道通信時,必須將PipedOutputStream和PipedInputStream配套使用。
- SequenceInputStream 將2個或者多個InputStream對象轉換成單一InputStream。
- FilterInputStream 抽象類,作為裝飾器接口為其它的InputStream提供有用功能。
- DataIputStream 用來裝飾其它輸入流,它允許應用程序以與機器無關方式從底層輸入流中讀取基本 Java 數據類型。
- BufferedInputStream 代表使用緩沖區。
-
LineNumberInputStream提供了額外的功能來保留當前行號的記錄,已經廢棄. - PushbackInputStream 可以把讀取進來的數據回退到輸入流的緩沖區之中。
- PrintStream 用來產生格式化輸出,有2個重要的方法print()和println()。
FileInputStream和FileOutputStream
這兩個類對文件流進行操作,是最常見的IO操作流。
/**
* 復制文件內容
* @param inFile 源文件
* @param outFile 目標文件
*/
public static void readFileByBytes(String inFile, String outFile) {
InputStream in = null;
OutputStream out = null;
try {
byte[] tempBytes = new byte[1024];
int byteRead = 0;
in = new FileInputStream(inFile);
out = new FileOutputStream(outFile);
while((byteRead = in.read(tempBytes)) != -1) {
out.write(tempBytes, 0, byteRead);
}
} catch (Exception e1) {
e1.printStackTrace();
} finally {
if(in != null) {
try {
in.close();
} catch (IOException e1) {
e1.printStackTrace();
}
try {
out.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
ByteArrayInputStream和ByteArrayOutputStream
ByteArrayOutputStream類是在創建它的實例時,程序內部創建一個byte型別數組的緩沖區,然后利用ByteArrayOutputStream和ByteArrayInputStream的實例向數組中寫入或讀出byte型數據。在網絡傳輸中我們往往要傳輸很多變量,我們可以利用ByteArrayOutputStream把所有的變量收集到一起,然后一次性把數據發送出去。
package medium.io;
import java.io.ByteArrayInputStream;
public class ByteArrayStreamTest {
private static final int LEN = 5;
// 對應abcddefghijklmnopqrsttuvwxyz的ASCII碼
private static final byte[] arrayLetters = {
0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A
};
public static void main(String[] args) {
testByteArrayInputStream();
}
private static void testByteArrayInputStream() {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(arrayLetters);
//讀取5個字節
for(int i = 0; i < LEN; i++) {
//如果還可以繼續讀取
if(byteArrayInputStream.available() >= 0) {
//讀取下一個字節并打印
int temp = byteArrayInputStream.read();
System.out.printf("%d : 0x%s\n", i, Integer.toHexString(temp));
}
}
//如果不支持mark功能
if(!byteArrayInputStream.markSupported()) {
System.out.println("Make not supported!");
}
//標記當前位置,一旦調用reset()方法,會重置到標記位置
//mark方法的參數0其實沒有實際意義
byteArrayInputStream.mark(0);
//跳過5個字節
byteArrayInputStream.skip(5);
//讀取5個字節
byte[] buf = new byte[LEN];
byteArrayInputStream.read(buf, 0, LEN);
String str1 = new String(buf);
System.out.printf("str1=%s\n", str1);
//重置回標記位置
byteArrayInputStream.reset();
byteArrayInputStream.read(buf, 0, LEN);
String str2 = new String(buf);
System.out.printf("str2=%s\n", str2);
}
}
PipedInputStream和PipedOutputStream
PipedOutputStream和PipedInputStream分別是管道輸出流和管道輸入流。
它們的作用是讓多線程可以通過管道進行線程間的通訊。在使用管道通信時,必須將PipedOutputStream和PipedInputStream配套使用。
使用管道通信時,大致的流程是:我們在線程A中向PipedOutputStream中寫入數據,這些數據會自動的發送到與PipedOutputStream對應的PipedInputStream中,進而存儲在PipedInputStream的緩沖中;此時,線程B通過讀取PipedInputStream中的數據。就可以實現,線程A和線程B的通信。
package medium.io;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
public class PipeStreamTest {
public static void main(String[] args) throws IOException {
final PipedOutputStream output = new PipedOutputStream();
final PipedInputStream input = new PipedInputStream(output);
//創建2個線程
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
output.write("Hello Pipe".getBytes());
output.close();
} catch(IOException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
int data = input.read();
while(data != -1) {
System.out.print((char)data);
data = input.read();
}
input.close();
System.out.println();
} catch(IOException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
}
}
SequenceInputStream
SequenceInputStream 表示其他輸入流的邏輯串聯。它從輸入流的有序集合開始,并從第一個輸入流開始讀取,直到到達文件末尾,接著從第二個輸入流讀取,依次類推,直到到達包含的最后一個輸入流的文件末尾為止。
package medium.io;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.SequenceInputStream;
public class SequenceStreamTest {
public static void main(String[] args) throws IOException{
FileInputStream f1 = new FileInputStream("123.txt");
FileInputStream f2 = new FileInputStream("234.txt");
SequenceInputStream s1 = new SequenceInputStream(f1, f2);
BufferedInputStream b1 = new BufferedInputStream(s1);
byte[] b = new byte[1024];
int n = 0;
while((n = b1.read(b)) != -1) {
String s = new String(b, 0, n);
System.out.println(s);
}
}
}
FilterInputStream和FilterOutputStream
FilterInputStream 的作用是用來“封裝其它的輸入流,并為它們提供額外的功能”。
DataInputStream和DataOutputStream
DataInputStream 是數據輸入流,它繼承于FilterInputStream。DataOutputStream 是數據輸出流,它繼承于FilterOutputStream。二者配合使用,“允許應用程序以與機器無關方式從底層輸入流中讀寫基本 Java 數據類型”。
private static void testDataOutputStream() {
DataOutputStream out = null;
try {
File file = new File("file.txt");
out = new DataOutputStream(new FileOutputStream(file));
out.writeBoolean(true);
out.writeByte((byte)0x41);
out.writeChar((char)0x4243);
out.writeShort((short)0x4445);
out.writeInt(0x12345678);
out.writeLong(0x0FEDCBA987654321L);
out.writeUTF("abcdefg");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
out.close();
} catch (IOException e) {
}
}
}
private static void testDataInputStream() {
DataInputStream in = null;
try {
File file = new File("file.txt");
in = new DataInputStream(new FileInputStream(file));
System.out.printf("readBoolean(): %s\n", in.readBoolean());
System.out.printf("readByte(): %s\n", in.readByte());
System.out.printf("readChar(): %s\n", in.readChar());
System.out.printf("readShort(): %s\n", in.readShort());
System.out.printf("readInt(): %s\n", in.readInt());
System.out.printf("readLong(): %s\n", in.readLong());
System.out.printf("readUTF(): %s\n", in.readUTF());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch(IOException e) {
}
}
}
BufferedInputStream和BufferedOutputStream
BufferedInputStream是帶緩沖區的輸入流,它繼承于FilterInputStream。默認緩沖區大小是8M,能夠減少訪問磁盤的次數,提高文件讀取性能。
BufferedOutputStream是帶緩沖區的輸出流,它繼承于FilterOutputStream,能夠提高文件的寫入效率。
它們提供的“緩沖功能”本質上是通過一個內部緩沖區數組實現的。例如,在新建某輸入流對應的BufferedInputStream后,當我們通過read()讀取輸入流的數據時,BufferedInputStream會將該輸入流的數據分批的填入到緩沖區中。每當緩沖區中的數據被讀完之后,輸入流會再次填充數據緩沖區;如此反復,直到我們讀完輸入流數據。
public static void readAndWrite(String src, String des) {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
bis = new BufferedInputStream(new FileInputStream(src));
bos = new BufferedOutputStream(new FileOutputStream(des));
byte[] b = new byte[1024];
int len = 0;
while((len = bis.read(b, 0, b.length)) != -1) {
bos.write(b, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(bos != null) {
try {
bos.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if(bis != null) {
try {
bis.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
字符流
既然字節流提供了能夠處理任何類型的輸入/輸出操作的功能,那為什么還要存在字符流呢?字節流不能直接操作Unicode字符,因為一個字符有兩個字節,字節流一次只能操作一個字節。 在平時使用更多的還是字節流。
大部分字節流都可以找到對應的字符流。
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("qqq.txt");
//字節流轉字符流
OutputStreamWriter osw = new OutputStreamWriter(fos);
BufferedWriter bw = new BufferedWriter(osw);
String s1 = "愛你一生一世";
String s2 = "愛你一生一世";
bw.write(s1);
bw.write("\r\n");
bw.write(s2);
bw.close();
FileInputStream fis = new FileInputStream("qqq.txt");
//字節流轉字符流并指定編碼
InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
BufferedReader br = new BufferedReader(isr);
String s = null;
while(null != (s = br.readLine())) {
System.out.println(s);
}
br.close();
}
在實際使用中,感覺文件中有中文的時候使用字符流很容易出現亂碼,一般還是使用字節流。
RandomAccessFile
RandomAccessFile是Java輸入/輸出流體系中功能最豐富的文件內容訪問類,既可以讀取文件內容,也可以向文件輸出數據。與普通的輸入/輸出流不同的是,RandomAccessFile支持跳到文件任意位置讀寫數據,RandomAccessFile對象包含一個記錄指針,用以標識當前讀寫處的位置,當程序創建一個新的RandomAccessFile對象時,該對象的文件記錄指針對于文件頭(也就是0處),當讀寫n個字節后,文件記錄指針將會向后移動n個字節。除此之外,RandomAccessFile可以自由移動該記錄指針
RandomAccessFile包含兩個方法來操作文件記錄指針:
- long getFilePointer():返回文件記錄指針的當前位置。
- void seek(long pos):將文件記錄指針定位到pos位置。
RandomAccessFile類在創建對象時,除了指定文件本身,還需要指定一個mode參數,該參數指定RandomAccessFile的訪問模式,該參數有如下四個值:
- r:以只讀方式打開指定文件。如果試圖對該RandomAccessFile指定的文件執行寫入方法則會拋出IOException。
- rw:以讀取、寫入方式打開指定文件。如果該文件不存在,則嘗試創建文件。
- rws:以讀取、寫入方式打開指定文件。相對于rw模式,還要求對文件的內容或元數據的每個更新都同步寫入到底層存儲設備,默認情形下(rw模式下),是使用buffer的,只有cache滿的或者使用RandomAccessFile.close()關閉流的時候兒才真正的寫到文件。
- rwd:與rws類似,只是僅對文件的內容同步更新到磁盤,而不修改文件的元數據。
下面進行一個項目實戰例子。
package medium.io;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
public class RandomAccessFileTest {
public static void main(String[] args) throws IOException{
String content = "測試文字啊";
//創建臨時文件
File tempFile = File.createTempFile("temp", null);
//虛擬機終止時,刪除此文件
tempFile.deleteOnExit();
FileOutputStream fos = new FileOutputStream(tempFile);
RandomAccessFile raf = new RandomAccessFile("123.txt", "rw");
raf.seek(5);
byte[] buffer = new byte[1024];
int num = 0;
int len = 0;
while((len = raf.read(buffer)) != -1) {
fos.write(buffer, num, len);
num += len;
}
raf.seek(5);
raf.write(content.getBytes());
FileInputStream fis = new FileInputStream(tempFile);
num = 0;
while((len = fis.read(buffer)) != -1) {
raf.write(buffer, num, len);
}
}
}
需要注意的是,由于是按字節偏移,中文有時候會出現亂碼。
標準IO
標準IO指“程序所使用的單一信息流”,程序的所有輸入都可以來自于標準輸入,所有輸出也都可以發送到標準輸出,所有的錯誤信息都可以發送到標準輸入。
標準IO的意義在于:我們很容易把程序串聯在一起,一個程序的標準輸出可以稱為另一個程序的標準輸入。
讀取標準輸入
Java提供了System.in、System.out和System.err。其中System.out和System.err已被包裝成了PrintStream對象,可以直接使用,System.in卻必須包裝才可以使用。
public class Echo {
public static void main(String[] args) throws IOException {
BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
String s;
while((s = stdin.readLine()) != null && s.length() != 0) {
System.out.println(s);
}
}
}
IO重定向
可以使用方法對標準IO進行重定向:
- setIn(PrintStream)
- setOut(PrintStream)
- setErr(PrintStream)
在程序大量輸出難以直接閱讀的時候,重定向輸出很有用,在重復輸入某個序列的時候,重定向輸入很有用。
public class Redirecting {
public static void main(String[] args) throws IOException{
PrintStream console = System.out;
BufferedInputStream in = new BufferedInputStream(new FileInputStream("123.txt"));
PrintStream out = new PrintStream(new BufferedOutputStream(new FileOutputStream("234.txt")));
//設置輸入流和輸出流
System.setIn(in);
System.setOut(out);
System.setErr(out);
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String s;
while((s = br.readLine()) != null) {
System.out.println(s);
}
out.close();
//恢復輸出流
System.setOut(console);
}
}