一、IO簡介
數據流是一組有序,有起點和終點的字節的數據序列。包括輸入流和輸出流。
流序列中的數據既可以是未經加工的原始二進制數據,也可以是經一定編碼處理後符合某種格式規定的特定數據。因此Java中的流分爲兩種:
- 字節流:數據流中最小的數據單元是字節
- 字符流:數據流中最小的數據單元是字符
Java中的字符是Unicode編碼,一個字符佔用兩個字節。
Java.io包中最重要的就是5個類和一個接口。5個類指的是File
、OutputStream
、InputStream
、Writer
、Reader
;一個接口指的是Serializable
。掌握了這些就掌握了Java I/O的精髓了。
Java I/O主要包括如下3層次:
- 流式部分——最主要的部分。如:OutputStream、InputStream、Writer、Reader等
- 非流式部分——如:File類、RandomAccessFile類和FileDescriptor等類
- 其他——文件讀取部分的與安全相關的類,如:SerializablePermission類,以及與本地操作系統相關的文件系統的類,如:FileSystem類和Win32FileSystem類和WinNTFileSystem類。
二、IO詳細介紹
在Android 平臺,從應用的角度出發,我們最需要關注和研究的就是 字節流(Stream)字符流(Reader/Writer)和 File/ RandomAccessFile。當我們需要的時候再深入研究也未嘗不是一件好事。關於字符和字節,例如文本文件,XML這些都是用字符流來讀取和寫入。而如RAR,EXE文件,圖片等非文本,則用字節流來讀取和寫入。面對如此複雜的類關係,有一個點是我們必須要首先掌握的,那就是設計模式中的修飾模式,學會並理解修飾模式是搞懂流必備的前提條件哦。
在具體的學習流之前,我們必須要學的一個設計模式是裝飾模式。因爲從流的整個發展歷史,出現的各種類之間的關係看,都是沿用了修飾模式,都是一個類的功能可以用來修飾其他類,然後組合成爲一個比較複雜的流。比如說:
new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(new File(file))));
從上面的代碼塊中大家不難看出這些類的關係:爲了向文件中寫入數據,首先需要創建一個FileOutputStream,然後爲了提升訪問的效率,所以將它發送給具備緩存功能的BufferedOutput-Stream,而爲了實現與機器類型無關的java基本類型數據的輸出,所以,我們將緩存的流傳遞給了DataOutputStream。從上面的關係,我們可以看到,其根本目的都是爲outputSteam添加額外的功能。而這種額外功能的添加就是採用了裝飾模式來構建的代碼。因此,學習流,必須要學好裝飾模式。
三、字節流
1、字節流的學習過程
爲什麼要按照一個學習路線來呢?原因是他們的功能決定的。OutputStream -> FileOutputStream/FilterOutputStream ->DataOutputStream->bufferedOutputStream
相應的學習InputStream方法就好了。
2、FilterOutputStream
從學習的角度來,我們應該先掌握FilterOutputStream, 以及FileOutputStream,這兩個類是基本的類,從繼承關係可以不難發現他們都是對 abstract 類 OutputStream的拓展,是它的子類。然而,伴隨着 對 Stream流的功能的拓展,所以就出現了 DataOutputStream,(將java中的基礎數據類型寫入數據字節輸出流中、保存在存儲介質中、然後可以用DataOutputStream從存儲介質中讀取到程序中還原成java基礎類型)。這裏多提一句、DataOutputStream、FilterOutputStream三個類的關係的這種設計既使用了裝飾器模式 避免了類的爆炸式增長。
3、BufferedOutputStream
爲了提升Stream的執行效率,所以出現了bufferedOutputStream。bufferedOutputStream就是將本地添加了一個緩存的數組。在使用bufferedOutputStream之前每次從磁盤讀入數據的時候都是需要訪問多少byte數據就向磁盤中讀多少個byte的數據,而出現bufferedOutputSteam之後,策略就改了,會先讀取整個緩存空間相應大小的數據,這樣就是從磁盤讀取了一塊比較大的數據,然後緩存起來,從而減少了對磁盤的訪問的次數以達到提升性能的目的。
另外一方面,我們知道了outputStream(輸出流)的發展歷史後,我們便可以知道如何使用outpuSteam了,同樣的方法,我們可以運用到inputStream中來,這樣對稱的解釋就出現到了inputStream相關的中來了,於是,我們對整個字節流就有了全方位的理解,所以這樣子我們就不會感覺到流的複雜了。這個時候對於其他的一些字節流的使用(byteArrayOutputStream/PipeOutputStream/ObjectOutputStream)的學習就自需要在使用的時候看看API即可。
public class DataStreamTest {
public static void main(String[] args) throws IOException {
testDataOutPutStream();
testDataInputStreamI();
}
private static void testDataOutPutStream() {
try {
DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(
new File("src/testtxt/tataStreamTest.txt"))));
out.writeBoolean(true);
out.writeByte((byte)0x41);
out.writeChar((char)0x4243);
out.writeShort((short)0x4445);
out.writeInt(0x12345678);
out.writeLong(0x987654321L);
out.writeUTF("abcdefghijklmnopqrstuvwxyz嚴12");
out.writeLong(0x023433L);
out.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
private static void testDataInputStreamI() {
try {
File file = new File("src/testtxt/tataStreamTest.txt");
DataInputStream in = new DataInputStream(
new BufferedInputStream(
new FileInputStream(file)));
System.out.println(Long.toHexString(in.readLong()));
System.out.println(in.readBoolean());
System.out.println(byteToHexString(in.readByte()));
System.out.println(charToHexString(in.readChar()));
System.out.println(shortToHexString(in.readShort()));
System.out.println(Integer.toHexString(in.readInt()));
System.out.println(Long.toHexString(in.readLong()));
System.out.println(in.readUTF());
System.out.println(Long.toHexString(in.readLong()));
in.close();
} catch (Exception e) {
// TODO: handle exception
}
}
// 打印byte對應的16進制的字符串
private static String byteToHexString(byte val) {
return Integer.toHexString(val & 0xff);
}
// 打印char對應的16進制的字符串
private static String charToHexString(char val) {
return Integer.toHexString(val);
}
// 打印short對應的16進制的字符串
private static String shortToHexString(short val) {
return Integer.toHexString(val & 0xffff);
}
}
四、字符流
字符流的學習和字節流的學習是一樣的,它和字節流有着同樣的發展過程,只是,字節流面向的是我們未知或者即使知道了他們的編碼格式也意義不大的文件(png,exe, zip)的時候是採用字節,而面對一些我們知道文件構造我們就能夠搞懂它的意義的文件(json,xml)等文件的時候我們還是需要以字符的形式來讀取,所以就出現了字符流。reader 和 Stream最大的區別我認爲是它包含了一個readline()接口,這個接口標明瞭,一行數據的意義,這也是可以理解的,因爲自有字符才具備行的概念,相反字節流中的行也就是一個字節符號。
1、字符流的學習歷程:
Writer- >FilterWriter->BufferedWriter->OutputStreamWriter->FileWriter->其
2、FilterWriter/FilterReader
字符過濾輸出流、與FilterOutputStream功能一樣、只是簡單重寫了父類的方法、目的是爲所有裝飾類提供標準和基本的方法、要求子類必須實現核心方法、和擁有自己的特色。這裏FilterWriter沒有子類、可能其意義只是提供一個接口、留着以後的擴展。。。本身是一個抽象類。
3、BufferedWriter/BufferedReader
BufferedWriter是 Writer類的一個子類。他的功能是爲傳入的底層字符輸出流提供緩存功能、同樣當使用底層字符輸出流向目的地中寫入字符或者字符數組時、每寫入一次就要打開一次到目的地的連接、這樣頻繁的訪問不斷效率底下、也有可能會對存儲介質造成一定的破壞、比如當我們向磁盤中不斷的寫入字節時、誇張一點、將一個非常大單位是G的字節數據寫入到磁盤的指定文件中的、沒寫入一個字節就要打開一次到這個磁盤的通道、這個結果無疑是恐怖的、而當我們使用BufferedWriter將底層字符輸出流、比如FileReader包裝一下之後、我們可以在程序中先將要寫入到文件中的字符寫入到BufferedWriter的內置緩存空間中、然後當達到一定數時、一次性寫入FileReader流中、此時、FileReader就可以打開一次通道、將這個數據塊寫入到文件中、這樣做雖然不可能達到一次訪問就將所有數據寫入磁盤中的效果、但也大大提高了效率和減少了磁盤的訪問量!
private static void testWriterAndStream(){
try {
BufferedWriter bufferedWriter = new BufferedWriter(
// new FileWriter("src/testtxt/writerAndStream.txt"));
new OutputStreamWriter(
new FileOutputStream(
new File("src/testtxt/writerAndStream.txt")),"GBK"));
bufferedWriter.write("我 愛你中國,親愛的母親");
bufferedWriter.flush();
bufferedWriter.close();
System.out.println("end");
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
4、OutputStreamWriter/InputStreamReader
輸入字符轉換流、是輸入字節流轉向輸入字符流的橋樑、用於將輸入字節流轉換成輸入字符流、通過指定的或者默認的編碼將從底層讀取的字節轉換成字符返回到程序中、與OutputStreamWriter一樣、本質也是使用其內部的一個類來完成所有工作:StreamDecoder、使用默認或者指定的編碼將字節轉換成字符;OutputStreamWriter/InputStreamReader只是對StreamDecoder進行了封裝、isr內部所有方法核心都是調用StreamDecoder來完成的、InputStreamReader只是對StreamDecoder進行了封裝、使得我們可以直接使用讀取方法、而不用關心內部實現。
public class InputStreamReaderTest {
public static void testISRDefaultEncoder(InputStream is){
try{
// InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(
new InputStreamReader(is));
String string;
while ((string = br.readLine()) != null) {
// System.out.println("code: " + isr.getEncoding());
System.out.println(string);
}
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void testISRGBK(InputStream is){
try {
InputStreamReader inputStreamReader = new InputStreamReader(is,"GBK");
BufferedReader gbkBr = new BufferedReader(inputStreamReader);
String string;
while ((string = gbkBr.readLine()) != null) {
System.out.println("code: " + inputStreamReader.getEncoding());
System.out.println(string);
}
gbkBr.close();
} catch (IOException e) {
// TODO: handle exception
}
}
public static void testISRUTF8(InputStream is){
try {
InputStreamReader inputStreamReader = new InputStreamReader(is,"UTF-8");
BufferedReader gbkBr = new BufferedReader(inputStreamReader);
String string;
while ((string = gbkBr.readLine()) != null) {
System.out.println("code: " + inputStreamReader.getEncoding());
System.out.println(string);
}
gbkBr.close();
} catch (IOException e) {
// TODO: handle exception
}
}
public static void main(String[] args) throws IOException {
testISRDefaultEncoder(
new FileInputStream(
new File("src/testtxt/OutputStreamWriter.txt")));
testISRGBK(
new FileInputStream(
new File("src/testtxt/OutputStreamWriter.txt")));
testISRUTF8(
new FileInputStream(
new File("src/testtxt/OutputStreamWriter.txt")));
}
}
5、FileReader/FileWriter
FileReader和FileWriter 繼承於InputStreamReader/OutputStreamWriter。
從源碼可以發現FileWriter 文件字符輸出流、主要用於將字符寫入到指定的打開的文件中、其本質是通過傳入的文件名、文件、或者文件描述符來創建FileOutputStream、然後使用OutputStreamWriter使用默認編碼將FileOutputStream轉換成Writer(這個Writer就是FileWriter)。如果使用這個類的話、最好使用BufferedWriter包裝一下、高端大氣上檔次、低調奢華有內涵!
FileReader 文件字符輸入流、用於將文件內容以字符形式讀取出來、一般用於讀取字符形式的文件內容、也可以讀取字節形式、但是因爲FileReader內部也是通過傳入的參數構造InputStreamReader、並且只能使用默認編碼、所以我們無法控制編碼問題、這樣的話就很容易造成亂碼。所以讀取字節形式的文件還是使用字節流來操作的好、同樣在使用此流的時候用BufferedReader包裝一下、就算衝着BufferedReader的readLine()方法去的也要使用這個包裝類、不說他還能提高效率、保護存儲介質。
public static void main(String[] args) throws IOException {
File srcfile = new File("src/testtxt/BufferedReader.txt");
File dstFile = new File("src/testtxt/BufferedWrite.txt");
BufferedWriter bw = new BufferedWriter(new FileWriter(dstFile));
BufferedReader br = new BufferedReader(new FileReader(srcfile));
char[] string = new char[1024]; // 請注意此處不是byte,而是char
while ((br.read(string))!= -1) {
bw.write(string);
}
br.close();
bw.flush();
bw.close();
}
五、字符流最常見用法
字節與字符輸出流字節的關係
字節輸入流和字符輸入流之間的關係
六、RandomAccessFile
1、構造方法
RandomAccessFile raf = newRandomAccessFile(File file, String mode);
其中參數 mode 的值可選 "r":可讀,"w" :可寫,"rw":可讀性;
2、成員方法
-
seek(int index)
:可以將指針移動到某個位置開始讀寫; -
setLength(long len)
:給寫入文件預留空間:
3、RandomAccessFile 特點和優勢
既可以讀也可以寫
RandomAccessFile不屬於InputStream和OutputStream類系的它是一個完全獨立的類,所有方法(絕大多數都只屬於它自己)都是自己從頭開始規定的,這裏麪包含讀寫兩種操作。可以指定位置讀寫
RandomAccessFile能在文件裏面前後移動,在文件裏移動用的seek( ),所以它的行爲與其它的I/O類有些根本性的不同。總而言之,它是一個直接繼承Object的,獨立的類。只有RandomAccessFile纔有seek搜尋方法,而這個方法也只適用於文件。
public class RandomAccessFileTests {
private static final File file = new File("src\\testtxt\\raf.txt");
/**
* 向文件中寫入內容
*/
public static void testRandomAccessFileWriter() throws IOException{
//要先將已有文件刪除、避免干擾。
if(file.exists()){
file.delete();
}
RandomAccessFile rsfWriter = new RandomAccessFile(file, "rw");
//不會改變文件大小、但是他會將下一個字符的寫入位置標識爲10000、
//也就是說此後只要寫入內容、就是從10001開始存、
rsfWriter.seek(10000);
printFileLength(rsfWriter); //result: 0
//會改變文件大小、只是把文件的size改變、
//並沒有改變下一個要寫入的內容的位置、
//這裏註釋掉是爲了驗證上面的seek方法的說明內容
rsfWriter.setLength(10000);
System.out.println("oo");
printFileLength(rsfWriter); //result: 0
System.out.println("xx");
//每個漢子佔3個字節、寫入字符串的時候會有一個記錄寫入字符串長度的兩個字節
rsfWriter.writeUTF("哈哈啊哈哈");
printFileLength(rsfWriter); //result: 10014
//每個字符佔兩個字節
rsfWriter.writeChar('a');
rsfWriter.writeChars("abcde");
printFileLength(rsfWriter); //result: 10026
//再從“文件指針”爲5000的地方插一個長度爲100、內容全是'a'的字符數組
//這裏file長依然是10026、因爲他是從“文件指針”爲5000的地方覆蓋後面
//的200個字節、下標並沒有超過文件長度
rsfWriter.seek(5000);
char[] cbuf = new char[100];
for(int i=0; i<cbuf.length; i++){
cbuf[i] = 'a';
rsfWriter.writeChar(cbuf[i]);
}
printFileLength(rsfWriter); //result: 10026
//再從“文件指針”爲1000的地方插入一個長度爲100、內容全是a的字節數組
//這裏file長依然是10026、因爲他是從“文件指針”爲5000的地方覆蓋後面
//的200個字節、下標並沒有超過文件長度
byte[] bbuf = new byte[100];
for (int i = 0; i < bbuf.length; i++) {
bbuf[i] = 1;
}
rsfWriter.seek(1000);
rsfWriter.writeBytes(new String(bbuf));
printFileLength(rsfWriter);
}
/**
* 從文件中讀取內容
* 這裏我們要清楚現在文件中有什麼內容、而且還要清楚這些內容起始字節下標、長度
*
* @throws IOException
*/
public static void testRandomAccessFileRead() throws IOException{
RandomAccessFile rsfReader = new RandomAccessFile(file, "r");
//可按照自己想讀取的東西所在的位置、長度來讀取
//讀取"哈哈啊哈哈"
rsfReader.seek(10000);
System.out.println(rsfReader.readUTF());
//讀取100個字符'a'
rsfReader.seek(5000);
byte[] bbuf = new byte[200];
rsfReader.read(bbuf);
System.out.println(new String(bbuf));
//讀取100個1
byte[] bbuf2 = new byte[100];
rsfReader.seek(1000);
rsfReader.read(bbuf2, 0, 100);
for(byte b : bbuf2){
System.out.print(b);
}
//讀取字符'aabcde'
byte[] bbuf3 = new byte[12];
rsfReader.seek(10014);
rsfReader.read(bbuf3);
System.out.println(new String(bbuf3));
}
/**
* 打印文件長度
* @param rsfWriter 指向文件的隨機文件流
* @throws IOException
*/
private static void printFileLength(RandomAccessFile rsfWriter)
throws IOException {
System.out.println("file length: " + rsfWriter.length() + " file pointer: " + rsfWriter.getFilePointer());
}
public static void main(String[] args) throws IOException {
testRandomAccessFileWriter();
testRandomAccessFileRead();
}
}
七、NIO—FileChannel
Channel(管道)是對I/O操作的封裝。
FileChannel配合着ByteBuffer,將讀寫的數據緩存到內存中,然後以批量/緩
存的方式read/write,省去了非批量操作時的重複中間操作,操縱大文件時可
以顯著提高效率(和Stream以byte數組方式有什麼區別?經過測試,效率上幾
乎無區別)。
Channel 讀寫文件
private static void copyFileByFileChannel(File sourceFile,File targetFile){
long start = System.currentTimeMillis();
RandomAccessFile randomAccessSourceFile;
RandomAccessFile randomAccessTargetFile;
try {
randomAccessSourceFile = new RandomAccessFile(sourceFile, "r");
randomAccessTargetFile = new RandomAccessFile(targetFile, "rw");
} catch (Exception e) {
e.printStackTrace();
return;
}
FileChannel sourceFileChannel = randomAccessSourceFile.getChannel();
FileChannel targetFileChannel = randomAccessTargetFile.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024*1024);
try {
while(sourceFileChannel.read(byteBuffer) != -1) {
byteBuffer.flip();
targetFileChannel.write(byteBuffer);
byteBuffer.clear();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
sourceFileChannel.close();
} catch (Exception e2) {
e2.printStackTrace();
}
try {
targetFileChannel.close();
}catch (Exception e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
用字節流讀取文件
private static void copyFileByStream(File sourceFile,File targetFile) {
long start = System.currentTimeMillis();
FileInputStream fis;
FileOutputStream fos;
try {
fis = new FileInputStream(sourceFile);
fos = new FileOutputStream(targetFile);
} catch (FileNotFoundException e) {
e.printStackTrace();
return;
}
byte[] readed = new byte[1024*1024];
try {
while (fis.read(readed) != -1) {
fos.write(readed);
}
} catch( IOException e){
e.printStackTrace();
} finally {
try{
fos.close();
}catch (Exception e) {
e.printStackTrace();
}
try {
fis.close();
} catch (Exception e2) {
e2.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
測試
public static void main(String[] args) {
File sourceFile = new File("D://alvin//IOtest//file1.mp4");
File targetFile = new File("D://file1-1.mp4");
targetFile.deleteOnExit();
try {
targetFile.createNewFile();
} catch (Exception e) {
e.printStackTrace();
}
copyFileByStream(sourceFile, targetFile);
copyFileByFileChannel(sourceFile, targetFile);
}
經測試 Channel 讀寫比字節流要快。
總結
1、如果只用FileOutputStream fileOutputStream = new FileOutputStream("d:/text.txt");不是也能輸出到"d:/text.txt"嗎?爲什麼要用其它兩個呢?能起到什麼作用呢。
-
FileOutputStream
:是字節流,它一個字節一個字節的向外邊送數據 -
OutputStreamWriter
:是字符流,它一個字符一個字符的向外邊送數據
2、它們有什麼區別麼
因爲計算機是洋鬼子發明的,它們的英文字符佔一個字節,而我們的中文是一個字符,至少佔倆字節。如果用stream,你讀出來的英語再倒也罷了,讀出來的中文可就是亂碼或者一個個“????”。如果你用WRITER,就不會有亂碼了
3、BufferedWriter Buffer是一個緩衝區,爲什麼要用BUFFER呢?
如果你直接用stream或者writer,你的硬盤可能就是讀一個字符或者一個字節 就去讀寫硬盤一次,IO負擔巨大。可是你用了Buffer,你的硬盤就是讀了一堆數據之後,讀寫一下硬盤。這樣對你硬盤有好處。
4、字節流與字符流的區別
字節流在操作的時候本身是不會用到緩衝區(內存)的,是與文件本身直接操作的,而字符流在操作的時候是使用到緩衝區的字節流在操作文件時,即使不關閉資源(close方法),文件也能輸出,但是如果字符流不使用close方法的話,則不會輸出任何內容,說明字符流用的是緩衝區,並且可以使用flush方法強制進行刷新緩衝區,這時才能在不close的情況下輸出內容。
5、那開發中究竟用字節流好還是用字符流好呢
在所有的硬盤上保存文件或進行傳輸的時候都是以字節的方法進行的,包括圖片也是按字節完成,而字符是隻有在內存中才會形成的,所以使用字節的操作是最多的。
如果要java程序實現一個拷貝功能,應該選用字節流進行操作(可能拷貝的是圖片),並且採用邊讀邊寫的方式(節省內存)。