Java IO詳解
學習Java的同學註意了!!!
學習過程中遇到什麽問題或者想獲取學習資源的話,歡迎加入Java學習交流群,群號碼:618528494 我們一起學Java!
初學Java,一直搞不懂Java裏面的io關系,在網上找了很多大多都是給個結構圖草草描述也看的不是很懂。而且沒有結合到java7 的最新技術,所以自己來整理一下,有錯的話請指正,也希望大家提出寶貴意見。
首先看個圖:(如果你也是初學者,我相信你看了真個人都不好了,想想java設計者真是煞費苦心啊!)
這是Java io 比較基本的一些處理流,除此之外我們還會提到一些比較深入的基於io的處理類,比如console類,SteamTokenzier,Externalizable接口,Serializable接口等等一些高級用法極其原理。
一、Java io的開始:文件
1. 我們主要講的是流,流的本質也是對文件的處理,我們循序漸進一步一步從文件將到流去。
2. java 處理文件的類 File,java提供了十分詳細的文件處理方法,舉了其中幾個例子,其余的可以去
Java代碼
- package com.hxw.io;
- import java.io.*;
- public class FileExample{
- public static void main(String[] args) {
- createFile();
- }
- /**
- * 文件處理示例
- */
- public static void createFile() {
- File f=new File("E:/電腦桌面/jar/files/create.txt");
- try{
- f.createNewFile(); //當且僅當不存在具有此抽象路徑名指定名稱的文件時,不可分地創建一個新的空文件。
- System.out.println("該分區大小"+f.getTotalSpace()/(1024*1024*1024)+"G"); //返回由此抽象路徑名表示的文件或目錄的名稱。
- f.mkdirs(); //創建此抽象路徑名指定的目錄,包括所有必需但不存在的父目錄。
- // f.delete(); // 刪除此抽象路徑名表示的文件或目錄
- System.out.println("文件名 "+f.getName()); // 返回由此抽象路徑名表示的文件或目錄的名稱。
- System.out.println("文件父目錄字符串 "+f.getParent());// 返回此抽象路徑名父目錄的路徑名字符串;如果此路徑名沒有指定父目錄,則返回 null。
- }catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
二、字節流:
1.字節流有輸入和輸出流,我們首先看輸入流InputStream,我們首先解析一個例子(FileInputStream)。
Java代碼
- package com.hxw.io;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.IOException;
- import java.io.InputStream;
- public class FileCount {
- /**
- * 我們寫一個檢測文件長度的小程序,別看這個程序挺長的,你忽略try catch塊後發現也就那麽幾行而已。
- */
- publicstatic void main(String[] args) {
- //TODO 自動生成的方法存根
- int count=0; //統計文件字節長度
- InputStreamstreamReader = null; //文件輸入流
- try{
- streamReader=newFileInputStream(new File("D:/David/Java/java 高級進階/files/tiger.jpg"));
- /*1.new File()裏面的文件地址也可以寫成D:\\David\\Java\\java 高級進階\\files\\tiger.jpg,前一個\是用來對後一個
- * 進行轉換的,FileInputStream是有緩沖區的,所以用完之後必須關閉,否則可能導致內存占滿,數據丟失。
- */
- while(streamReader.read()!=-1) { //讀取文件字節,並遞增指針到下一個字節
- count++;
- }
- System.out.println("---長度是: "+count+" 字節");
- }catch (final IOException e) {
- //TODO 自動生成的 catch 塊
- e.printStackTrace();
- }finally{
- try{
- streamReader.close();
- }catch (IOException e) {
- //TODO 自動生成的 catch 塊
- e.printStackTrace();
- }
- }
- }
- }
我們一步一步來,首先,上面的程序存在問題是,每讀取一個自己我都要去用到FileInputStream,我輸出的結果是“---長度是: 64982 字節”,那麽進行了64982次操作!可能想象如果文件十分龐大,這樣的操作肯定會出大問題,所以引出了緩沖區的概念。可以將streamReader.read()改成streamReader.read(byte[]b)此方法讀取的字節數目等於字節數組的長度,讀取的數據被存儲在字節數組中,返回讀取的字節數,InputStream還有其他方法mark,reset,markSupported方法,例如:
markSupported 判斷該輸入流能支持mark
和 reset
方法。
mark用於標記當前位置;在讀取一定數量的數據(小於readlimit的數據)後使用reset可以回到mark標記的位置。
FileInputStream不支持mark/reset操作;BufferedInputStream支持此操作;
mark(readlimit)的含義是在當前位置作一個標記,制定可以重新讀取的最大字節數,也就是說你如果標記後讀取的字節數大於readlimit,你就再也回不到回來的位置了。
通常InputStream的read()返回-1後,說明到達文件尾,不能再讀取。除非使用了mark/reset。
2.FileOutputStream 循序漸進版, InputStream是所有字節輸出流的父類,子類有ByteArrayOutputStream,FileOutputStream,ObjectOutputStreanm,這些我們在後面都會一一說到。先說FileOutputStream
我以一個文件復制程序來說,順便演示一下緩存區的使用。(Java I/O默認是不緩沖流的,所謂“緩沖”就是先把從流中得到的一塊字節序列暫存在一個被稱為buffer的內部字節數組裏,然後你可以一下子取到這一整塊的字節數據,沒有緩沖的流只能一個字節一個字節讀,效率孰高孰低一目了然。有兩個特殊的輸入流實現了緩沖功能,一個是我們常用的BufferedInputStream.)
Java代碼
- package com.hxw.io;
- import java.io.*;
- public class FileCopy {
- public static void main(String[] args) {
- // TODO自動生成的方法存根
- byte[] buffer=new byte[512]; //一次取出的字節數大小,緩沖區大小
- int numberRead=0;
- FileInputStream input=null;
- FileOutputStream out =null;
- try {
- input=new FileInputStream("D:/David/Java/java 高級進階/files/tiger.jpg");
- out=new FileOutputStream("D:/David/Java/java 高級進階/files/tiger2.jpg"); //如果文件不存在會自動創建
- while ((numberRead=input.read(buffer))!=-1) { //numberRead的目的在於防止最後一次讀取的字節小於buffer長度,
- out.write(buffer, 0, numberRead); //否則會自動被填充0
- }
- } catch (final IOException e) {
- // TODO自動生成的 catch 塊
- e.printStackTrace();
- }finally{
- try {
- input.close();
- out.close();
- } catch (IOException e) {
- // TODO自動生成的 catch 塊
- e.printStackTrace();
- }
- }
- }
- }
3.讀寫對象:ObjectInputStream 和ObjectOutputStream ,該流允許讀取或寫入用戶自定義的類,但是要實現這種功能,被讀取和寫入的類必須實現Serializable接口,其實該接口並沒有什麽方法,可能相當於一個標記而已,但是確實不合缺少的。實例代碼如下:
Java代碼
- package com.hxw.io;
- import java.io.*;
- public class ObjetStream {
- /**
- * @param args
- */
- public static void main(String[] args) {
- // TODO自動生成的方法存根
- ObjectOutputStream objectwriter=null;
- ObjectInputStream objectreader=null;
- try {
- objectwriter=new ObjectOutputStream(new FileOutputStream("D:/David/Java/java 高級進階/files/student.txt"));
- objectwriter.writeObject(new Student("gg", 22));
- objectwriter.writeObject(new Student("tt", 18));
- objectwriter.writeObject(new Student("rr", 17));
- objectreader=new ObjectInputStream(new FileInputStream("D:/David/Java/java 高級進階/files/student.txt"));
- for (int i = 0; i < 3; i++) {
- System.out.println(objectreader.readObject());
- }
- } catch (IOException | ClassNotFoundException e) {
- // TODO自動生成的 catch 塊
- e.printStackTrace();
- }finally{
- try {
- objectreader.close();
- objectwriter.close();
- } catch (IOException e) {
- // TODO自動生成的 catch 塊
- e.printStackTrace();
- }
- }
- }
- }
- class Student implements Serializable{
- private String name;
- private int age;
- public Student(String name, int age) {
- super();
- this.name = name;
- this.age = age;
- }
- @Override
- public String toString() {
- return "Student [name=" + name + ", age=" + age + "]";
- }
- }
運行後系統輸出:
Student [name=gg, age=22]
Student [name=tt, age=18]
Student [name=rr, age=17]
4.有時沒有必要存儲整個對象的信息,而只是要存儲一個對象的成員數據,成員數據的類型假設都是Java的基本數據類型,這樣的需求不必使用到與Object輸入、輸出相關的流對象,可以使用DataInputStream、DataOutputStream來寫入或讀出數據。下面是一個例子:(DataInputStream的好處在於在從文件讀出數據時,不用費心地自行判斷讀入字符串時或讀入int類型時何時將停止,使用對應的readUTF()和readInt()方法就可以正確地讀入完整的類型數據。)
Java代碼
- package com.hxw;
- public class Member {
- private String name;
- private int age;
- public Member() {
- }
- public Member(String name, int age) {
- this.name = name;
- this.age = age;
- }
- public void setName(String name){
- this.name = name;
- }
- public void setAge(int age) {
- this.age = age;
- }
- public String getName() {
- return name;
- }
- public int getAge() {
- return age;
- }
- }
打算將Member類實例的成員數據寫入文件中,並打算在讀入文件數據後,將這些數據還原為Member對象。下面的代碼簡單示範了如何實現這個需求。
Java代碼
- package com.hxw;
- import java.io.*;
- public class DataStreamDemo
- {
- public static void main(String[]args)
- {
- Member[] members = {newMember("Justin",90),
- newMember("momor",95),
- newMember("Bush",88)};
- try
- {
- DataOutputStreamdataOutputStream = new DataOutputStream(new FileOutputStream(args[0]));
- for(Member member:members)
- {
- //寫入UTF字符串
- dataOutputStream.writeUTF(member.getName());
- //寫入int數據
- dataOutputStream.writeInt(member.getAge());
- }
- //所有數據至目的地
- dataOutputStream.flush();
- //關閉流
- dataOutputStream.close();
- DataInputStreamdataInputStream = new DataInputStream(new FileInputStream(args[0]));
- //讀出數據並還原為對象
- for(inti=0;i<members.length;i++)
- {
- //讀出UTF字符串
- String name =dataInputStream.readUTF();
- //讀出int數據
- int score =dataInputStream.readInt();
- members[i] = newMember(name,score);
- }
- //關閉流
- dataInputStream.close();
- //顯示還原後的數據
- for(Member member : members)
- {
- System.out.printf("%s\t%d%n",member.getName(),member.getAge());
- }
- }
- catch(IOException e)
- {
- e.printStackTrace();
- }
- }
- }
5.PushbackInputStream類繼承了FilterInputStream類是iputStream類的修飾者。提供可以將數據插入到輸入流前端的能力(當然也可以做其他操作)。簡而言之PushbackInputStream類的作用就是能夠在讀取緩沖區的時候提前知道下一個字節是什麽,其實質是讀取到下一個字符後回退的做法,這之間可以進行很多操作,這有點向你把讀取緩沖區的過程當成一個數組的遍歷,遍歷到某個字符的時候可以進行的操作,當然,如果要插入,能夠插入的最大字節數是與推回緩沖區的大小相關的,插入字符肯定不能大於緩沖區吧!下面是一個示例。
Java代碼
- package com.hxw.io;
- import java.io.ByteArrayInputStream; //導入ByteArrayInputStream的包
- import java.io.IOException;
- import java.io.PushbackInputStream;
- /**
- * 回退流操作
- * */
- public class PushBackInputStreamDemo {
- public static void main(String[] args) throws IOException {
- String str = "hello,rollenholt";
- PushbackInputStream push = null; // 聲明回退流對象
- ByteArrayInputStream bat = null; // 聲明字節數組流對象
- bat = new ByteArrayInputStream(str.getBytes());
- push = new PushbackInputStream(bat); // 創建回退流對象,將拆解的字節數組流傳入
- int temp = 0;
- while ((temp = push.read()) != -1) { // push.read()逐字節讀取存放在temp中,如果讀取完成返回-1
- if (temp == ‘,‘) { // 判斷讀取的是否是逗號
- push.unread(temp); //回到temp的位置
- temp = push.read(); //接著讀取字節
- System.out.print("(回退" + (char) temp + ") "); // 輸出回退的字符
- } else {
- System.out.print((char) temp); // 否則輸出字符
- }
- }
- }
- }
6.SequenceInputStream:有些情況下,當我們需要從多個輸入流中向程序讀入數據。此時,可以使用合並流,將多個輸入流合並成一個SequenceInputStream流對象。SequenceInputStream會將與之相連接的流集組合成一個輸入流並從第一個輸入流開始讀取,直到到達文件末尾,接著從第二個輸入流讀取,依次類推,直到到達包含的最後一個輸入流的文件末尾為止。 合並流的作用是將多個源合並合一個源。其可接收枚舉類所封閉的多個字節流對象。
Java代碼
- package com.hxw.io;
- import java.io.*;
- import java.util.Enumeration;
- import java.util.Vector;
- public class SequenceInputStreamTest {
- /**
- * @param args
- * SequenceInputStream合並流,將與之相連接的流集組合成一個輸入流並從第一個輸入流開始讀取,
- * 直到到達文件末尾,接著從第二個輸入流讀取,依次類推,直到到達包含的最後一個輸入流的文件末尾為止。
- * 合並流的作用是將多個源合並合一個源。可接收枚舉類所封閉的多個字節流對象。
- */
- public static void main(String[] args) {
- doSequence();
- }
- private static void doSequence() {
- // 創建一個合並流的對象
- SequenceInputStream sis = null;
- // 創建輸出流。
- BufferedOutputStream bos = null;
- try {
- // 構建流集合。
- Vector<InputStream> vector = new Vector<InputStream>();
- vector.addElement(new FileInputStream("D:\text1.txt"));
- vector.addElement(new FileInputStream("D:\text2.txt"));
- vector.addElement(new FileInputStream("D:\text3.txt"));
- Enumeration<InputStream> e = vector.elements();
- sis = new SequenceInputStream(e);
- bos = new BufferedOutputStream(new FileOutputStream("D:\text4.txt"));
- // 讀寫數據
- byte[] buf = new byte[1024];
- int len = 0;
- while ((len = sis.read(buf)) != -1) {
- bos.write(buf, 0, len);
- bos.flush();
- }
- } catch (FileNotFoundException e1) {
- e1.printStackTrace();
- } catch (IOException e1) {
- e1.printStackTrace();
- } finally {
- try {
- if (sis != null)
- sis.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- try {
- if (bos != null)
- bos.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
7.PrintStream 說這個名字可能初學者不熟悉,如果說System.out.print()你肯定熟悉,System.out這個對象就是PrintStream,這個我們不做過多示例
三、字符流(顧名思義,就是操作字符文件的流)
1.java 使用Unicode存儲字符串,在寫入字符流時我們都可以指定寫入的字符串的編碼。前面介紹了不用拋異常的處理字節型數據的流ByteArrayOutputStream,與之對應的操作字符類的類就是CharArrayReader,CharArrayWriter類,這裏也會用到緩沖區,不過是字符緩沖區,一般講字符串放入到操作字符的io流一般方法是
CharArrayReaderreader=mew CharArrayReader(str.toCharArray()); 一旦會去到CharArrayReader實例就可以使用CharArrayReader訪問字符串的各個元素以執行進一步讀取操作。不做例子
2.我們用FileReader ,PrintWriter來做示範
Java代碼
- package com.hxw.io;
- import java.io.FileNotFoundException;
- import java.io.FileReader;
- import java.io.IOException;
- import java.io.PrintWriter;
- import java.nio.CharBuffer;
- public class Print {
- /**
- * @param args
- */
- public static void main(String[] args) {
- // TODO自動生成的方法存根
- char[] buffer=new char[512]; //一次取出的字節數大小,緩沖區大小
- int numberRead=0;
- FileReader reader=null; //讀取字符文件的流
- PrintWriter writer=null; //寫字符到控制臺的流
- try {
- reader=new FileReader("D:/David/Java/java 高級進階/files/copy1.txt");
- writer=new PrintWriter(System.out); //PrintWriter可以輸出字符到文件,也可以輸出到控制臺
- while ((numberRead=reader.read(buffer))!=-1) {
- writer.write(buffer, 0, numberRead);
- }
- } catch (IOException e) {
- // TODO自動生成的 catch 塊
- e.printStackTrace();
- }finally{
- try {
- reader.close();
- } catch (IOException e) {
- // TODO自動生成的 catch 塊
- e.printStackTrace();
- }
- writer.close(); //這個不用拋異常
- }
- }
- }
3.相對我們前面的例子是直接用FileReader打開的文件,我們這次使用鏈接流,一般比較常用的都用鏈接流,所謂鏈接流就是就多次對流的封裝,這樣能更好的操作個管理數據,(比如我們利用DataInputStream(BufferedInputStream(FileInputStream))將字節流層層包裝後,我們可以讀取readByte(),readChar()這樣更加具體的操作,註意,該流屬於字節流對字符進行操作,)字符流用CharArrayReader就可以了。下面的示例我們將用到j2se 5中的一個可變參數進行一個小度擴展。使用BufferedWriter 和BufferedReader用文件級聯的方式進行寫入,即將多個文件寫入到同一文件中(自帶緩沖區的輸出輸出流BufferedReader和BufferedWriter,該流最常用的屬readLine()方法了,讀取一行數據,並返回String)。
Java代碼
- package com.hxw.io;
- import java.io.BufferedReader;
- import java.io.BufferedWriter;
- import java.io.FileReader;
- import java.io.FileWriter;
- import java.io.IOException;
- import java.util.Iterator;
- public class FileConcatenate {
- /**
- * 包裝類進行文件級聯操作
- */
- public static void main(String[] args) {
- // TODO自動生成的方法存根
- try {
- concennateFile(args);
- } catch (IOException e) {
- // TODO自動生成的 catch 塊
- e.printStackTrace();
- }
- }
- public static voidconcennateFile(String...fileName) throws IOException{
- String str;
- //構建對該文件您的輸入流
- BufferedWriter writer=new BufferedWriter(new FileWriter("D:/David/Java/java 高級進階/files/copy2.txt"));
- for(String name: fileName){
- BufferedReader reader=new BufferedReader(new FileReader(name));
- while ((str=reader.readLine())!=null) {
- writer.write(str);
- writer.newLine();
- }
- }
- }
- }
4.Console類,該類提供了用於讀取密碼的方法,可以禁止控制臺回顯並返回char數組,對兩個特性對保證安全有作用,平時用的不多,了解就行。
5.StreamTokenizer 類,這個類非常有用,它可以把輸入流解析為標記(token), StreamTokenizer 並非派生自InputStream或者OutputStream,而是歸類於io庫中,因為StreamTokenizer只處理InputStream對象。
首先給出我的文本文件內容:
‘水上漂‘
青青草
"i love wyhss"
{3211}
23223 3523
i love wyh ,。
. ,
下面是代碼:
Java代碼
- package com.hxw.io;
- import java.io.BufferedReader;
- import java.io.FileReader;
- import java.io.IOException;
- import java.io.StreamTokenizer;
- /**
- * 使用StreamTokenizer來統計文件中的字符數
- * StreamTokenizer 類獲取輸入流並將其分析為“標記”,允許一次讀取一個標記。
- * 分析過程由一個表和許多可以設置為各種狀態的標誌控制。
- * 該流的標記生成器可以識別標識符、數字、引用的字符串和各種註釋樣式。
- *
- * 默認情況下,StreamTokenizer認為下列內容是Token: 字母、數字、除C和C++註釋符號以外的其他符號。
- * 如符號"/"不是Token,註釋後的內容也不是,而"\"是Token。單引號和雙引號以及其中的內容,只能算是一個Token。
- * 統計文章字符數的程序,不是簡單的統計Token數就萬事大吉,因為字符數不等於Token。按照Token的規定,
- * 引號中的內容就算是10頁也算一個Token。如果希望引號和引號中的內容都算作Token,應該調用下面的代碼:
- * st.ordinaryChar(‘\‘‘);
- * st.ordinaryChar(‘\"‘);
- */
- public class StreamTokenizerExample {
- /**
- * 統計字符數
- * @param fileName 文件名
- * @return 字符數
- */
- public static void main(String[] args) {
- String fileName = "D:/David/Java/java 高級進階/files/copy1.txt";
- StreamTokenizerExample.statis(fileName);
- }
- public static long statis(String fileName) {
- FileReader fileReader = null;
- try {
- fileReader = new FileReader(fileName);
- //創建分析給定字符流的標記生成器
- StreamTokenizer st = new StreamTokenizer(new BufferedReader(
- fileReader));
- //ordinaryChar方法指定字符參數在此標記生成器中是“普通”字符。
- //下面指定單引號、雙引號和註釋符號是普通字符
- st.ordinaryChar(‘\‘‘);
- st.ordinaryChar(‘\"‘);
- st.ordinaryChar(‘/‘);
- String s;
- int numberSum = 0;
- int wordSum = 0;
- int symbolSum = 0;
- int total = 0;
- //nextToken方法讀取下一個Token.
- //TT_EOF指示已讀到流末尾的常量。
- while (st.nextToken() !=StreamTokenizer.TT_EOF) {
- //在調用 nextToken 方法之後,ttype字段將包含剛讀取的標記的類型
- switch (st.ttype) {
- //TT_EOL指示已讀到行末尾的常量。
- case StreamTokenizer.TT_EOL:
- break;
- //TT_NUMBER指示已讀到一個數字標記的常量
- case StreamTokenizer.TT_NUMBER:
- //如果當前標記是一個數字,nval字段將包含該數字的值
- s = String.valueOf((st.nval));
- System.out.println("數字有:"+s);
- numberSum ++;
- break;
- //TT_WORD指示已讀到一個文字標記的常量
- case StreamTokenizer.TT_WORD:
- //如果當前標記是一個文字標記,sval字段包含一個給出該文字標記的字符的字符串
- s = st.sval;
- System.out.println("單詞有: "+s);
- wordSum ++;
- break;
- default:
- //如果以上3中類型都不是,則為英文的標點符號
- s = String.valueOf((char) st.ttype);
- System.out.println("標點有: "+s);
- symbolSum ++;
- }
- }
- System.out.println("數字有 " + numberSum+"個");
- System.out.println("單詞有 " + wordSum+"個");
- System.out.println("標點符號有: " + symbolSum+"個");
- total = symbolSum + numberSum +wordSum;
- System.out.println("Total = " + total);
- return total;
- } catch (Exception e) {
- e.printStackTrace();
- return -1;
- } finally {
- if (fileReader != null) {
- try {
- fileReader.close();
- } catch (IOException e1) {
- }
- }
- }
- }
- }
運行結果為:
標點有: ‘
單詞有: 水上漂
標點有: ‘
單詞有: 青青草
標點有: "
單詞有: i
單詞有: love
單詞有: wyh
單詞有: ss
標點有: "
標點有: {
數字有:3211.0
標點有: }
數字有:23223.0
數字有:35.23
單詞有: i
單詞有: love
單詞有: wyh
單詞有: ,。
數字有:0.0
標點有: ,
數字有 4個
單詞有 10個
標點符號有: 7個
Total= 21
我們從其中可以看到很多東西:
1.一個單獨的小數點“.”是被當做一個數字來對待的,數字的值為0.0;
2.一串漢字只要中間沒有符號(空格回車 分號等等)都是被當做一個單詞的。中文的標點跟中文的漢字一樣處理
3.如果不對引號化成普通字符,一個引號內的內容不論多少都被當做是一個標記。
4.該類能夠識別英文標點
5. java io裏面還有其他接口類似Serializable接口的子接口Externalizable接口,比Serializable復雜一些,這裏不再介紹。還有關於java對象版本化的東西感興趣的可以百度。java nio的東西這裏沒有涉及,後續會結合到線程再發一篇文章專門解析這個東西。
學習Java的同學註意了!!!
學習過程中遇到什麽問題或者想獲取學習資源的話,歡迎加入Java學習交流群,群號碼:618528494 我們一起學Java!
Java IO詳解