Java學習總結之Java IO系統(一)

概述

java.io 包幾乎包含了所有操作輸入、輸出需要的類。所有這些流類代表了輸入源和輸出目標。java.io 包中的流支持很多種格式,比如:基本類型、對象、本地化字符集等等。一個流可以理解爲一個數據的序列。輸入流表示從一個源讀取數據,輸出流表示向一個目標寫數據。Java 爲 I/O 提供了強大的而靈活的支持,使其更廣泛地應用到文件傳輸和網絡編程中。

Java 的 I/O 大概可以分成以下幾類:

  • 磁盤操作:File
  • 字節操作:InputStream 和 OutputStream
  • 字符操作:Reader 和 Writer
  • 對象操作:Serializable
  • 網絡操作:Socket
  • 新的輸入/輸出:NIO

File

Java中IO操作有相應步驟,以文件操作爲例,主要操作流程如下:
1.使用File類打開一個文件
2.通過字節流或字符流的子類,指定輸出的位置
3.進行讀/寫操作
4.關閉輸入/輸出

那麼我們先來介紹一下File類
Java文件類在java.io包中,它以抽象的方式代表文件名和目錄路徑名。該類主要用於獲取文件和目錄的屬性,文件和目錄的創建、查找、刪除、重命名等,但不能進行文件的讀寫操作
File對象代表磁盤中實際存在的文件和目錄。通過以下構造方法創建一個File對象。

通過給定的父抽象路徑名子路徑名字符串創建一個新的File實例。
File(File parent, String child)

通過將給定路徑名字符串轉換成抽象路徑名來創建一個新 File 實例。
File(String pathname)

根據 parent 路徑名字符串和 child 路徑名字符串創建一個新 File 實例。
File(String parent, String child)

通過將給定的 file: URI 轉換成一個抽象路徑名來創建一個新的 File 實例。
File(URI uri)

注意: 
1.在各個操作系統中,路徑的分隔符是不一樣的,例如:Windows中使用反斜槓:"\",Linux|Unix中使用正斜槓:"/"。在使用反斜槓時要寫成"\\"的形式,因爲反斜槓要進行轉義。如果要讓Java保持可移植性,應該使用File類的靜態常量File.pathSeparator。
2.構建一個File實例並不會在機器上創建一個文件。不管文件是否存在,都可以創建任意文件名的File實例。可以調用File實例上的exists()方法來判斷這個文件是否存在。通過後續的學習我們會知道,當把一個輸出流綁定到一個不存在的File實例上時,會自動在機器上創建該文件,如果文件已經存在,把輸出流綁定到該文件上則會覆蓋該文件,但這些都不是在創建File實例時進行的。

創建File對象成功後,可以使用以下列表中的方法操作文件。

File1.png

 

File2.png

File3.png

File4.png

下面給出一個使用File類的實例:

import java.io.File;
public class DirList {
   public static void main(String args[]) {
      String dirname = "/java";
      File f1 = new File(dirname);
      if (f1.isDirectory()) {
         System.out.println( "Directory of " + dirname);
         String s[] = f1.list();
         for (int i=0; i < s.length; i++) {
            File f = new File(dirname + "/" + s[i]);
            if (f.isDirectory()) {
               System.out.println(s[i] + " is a directory");
            } else {
               System.out.println(s[i] + " is a file");
            }
         }
      } else {
         System.out.println(dirname + " is not a directory");
    }
  }
}

小貼士:lastModified()方法返回的是從時間戳(1970年1月1日0時0分0秒)到當前的毫秒數,返回值類型是long,可以用Date類對它進行包裝使其更易讀。

Java中的目錄

創建目錄: 
File類中有兩個方法可以用來創建文件夾:

  • mkdir( )方法創建一個文件夾,成功則返回true,失敗則返回false。失敗表明File對象指定的路徑已經存在,或者由於整個路徑還不存在,該文件夾不能被創建。
  • mkdirs()方法創建一個文件夾和它的所有父文件夾。
    下面的例子創建 "/tmp/user/java/bin"文件夾:
import java.io.File;
 
public class CreateDir {
  public static void main(String args[]) {
    String dirname = "/tmp/user/java/bin";
    File d = new File(dirname);
    // 現在創建目錄
    d.mkdirs();
  }
}

mkdirs是遞歸創建文件夾,允許在創建某文件夾時其父文件夾不存在,從而一同創建;mkdir必須滿足路徑上的父文件夾全都存在 
注意: Java 在 UNIX 和 Windows 自動按約定分辨文件路徑分隔符。如果你在 Windows 版本的 Java 中使用分隔符 (/) ,路徑依然能夠被正確解析。
讀取目錄: 
一個目錄其實就是一個 File 對象,它包含其他文件和文件夾。
如果創建一個 File 對象並且它是一個目錄,那麼調用 isDirectory() 方法會返回 true。
可以通過調用該對象上的 list() 方法,來提取它包含的文件和文件夾的列表。
下面展示的例子說明如何使用 list() 方法來檢查一個文件夾中包含的內容:

import java.io.File;
 
public class DirList {
  public static void main(String args[]) {
    String dirname = "/tmp";
    File f1 = new File(dirname);
    if (f1.isDirectory()) {
      System.out.println( "目錄 " + dirname);
      String s[] = f1.list();
      for (int i=0; i < s.length; i++) {
        File f = new File(dirname + "/" + s[i]);
        if (f.isDirectory()) {
          System.out.println(s[i] + " 是一個目錄");
        } else {
          System.out.println(s[i] + " 是一個文件");
        }
      }
    } else {
      System.out.println(dirname + " 不是一個目錄");
    }
  }
}

刪除目錄或文件: 
刪除文件可以使用 java.io.File.delete() 方法。
以下代碼會刪除目錄/tmp/java/,即便目錄不爲空。
測試目錄結構:

/tmp/java/
|-- 1.log
|-- test

deleteFolder是一個遞歸函數,類似於DFS思想

import java.io.File;
 
public class DeleteFileDemo {
  public static void main(String args[]) {
      // 這裏修改爲自己的測試目錄
    File folder = new File("/tmp/java/");
    deleteFolder(folder);
  }
 
  //刪除文件及目錄
  public static void deleteFolder(File folder) {
    File[] files = folder.listFiles();
        if(files!=null) { 
            for(File f: files) {
                if(f.isDirectory()) {
                    deleteFolder(f);
                } else {
                    f.delete();
                }
            }
        }
        folder.delete();
    }
}

RandomAccessFile

RandomAccessFile不同於File,它提供了對文件內容的訪問,可以讀寫文件且支持隨機訪問文件的任意位置
RandomAccessFile讀寫用到文件指針,它的初始位置爲0,可以用getFilePointer()方法獲取文件指針的位置。下面是RandomAccessFile常用的方法。

RandomAccessFile.png

public int read(int x) throws IOException 方法只讀取一個字節,也就是x的低八位。


import java.io.File ;
import java.io.RandomAccessFile ;
public class RandomAccessFileDemo01{
    // 所有的異常直接拋出,程序中不再進行處理
    public static void main(String args[]) throws Exception{
        File f = new File("d:" + File.separator + "test.txt") ; // 指定要操作的文件
        RandomAccessFile rdf = null ;       // 聲明RandomAccessFile類的對象
        rdf = new RandomAccessFile(f,"rw") ;// 讀寫模式,如果文件不存在,會自動創建
        String name = null ;
        int age = 0 ;
        name = "zhangsan" ;         // 字符串長度爲8
        age = 30 ;                  // 數字的長度爲4
        rdf.writeBytes(name) ;      // 將姓名寫入文件之中
        rdf.writeInt(age) ;         // 將年齡寫入文件之中
        name = "lisi    " ;         // 字符串長度爲8
        age = 31 ;                  // 數字的長度爲4
        rdf.writeBytes(name) ;      // 將姓名寫入文件之中
        rdf.writeInt(age) ;         // 將年齡寫入文件之中
        name = "wangwu  " ;         // 字符串長度爲8
        age = 32 ;                  // 數字的長度爲4
        rdf.writeBytes(name) ;      // 將姓名寫入文件之中
        rdf.writeInt(age) ;         // 將年齡寫入文件之中
        rdf.close() ;               // 關閉
    }
};

寫完之後,開始讀取數據。寫的時候可以將一個字符串寫入,讀的時候需要一個個的以字節的形式讀取出來。


import java.io.File ;
import java.io.RandomAccessFile ;
public class RandomAccessFileDemo02{
    // 所有的異常直接拋出,程序中不再進行處理
    public static void main(String args[]) throws Exception{
        File f = new File("d:" + File.separator + "test.txt") ; // 指定要操作的文件
        RandomAccessFile rdf = null ;       // 聲明RandomAccessFile類的對象
        rdf = new RandomAccessFile(f,"r") ;// 以只讀的方式打開文件
        String name = null ;
        int age = 0 ;
        byte b[] = new byte[8] ;    // 開闢byte數組
        // 讀取第二個人的信息,意味着要空出第一個人的信息
        rdf.skipBytes(12) ;     // 跳過第一個人的信息
        for(int i=0;i<b.length;i++){
            b[i] = rdf.readByte() ; // 讀取一個字節
        }
        name = new String(b) ;  // 將讀取出來的byte數組變爲字符串
        age = rdf.readInt() ;   // 讀取數字
        System.out.println("第二個人的信息 --> 姓名:" + name + ";年齡:" + age) ;
        // 讀取第一個人的信息
        rdf.seek(0) ;   // 指針回到文件的開頭
        for(int i=0;i<b.length;i++){
            b[i] = rdf.readByte() ; // 讀取一個字節
        }
        name = new String(b) ;  // 將讀取出來的byte數組變爲字符串
        age = rdf.readInt() ;   // 讀取數字
        System.out.println("第一個人的信息 --> 姓名:" + name + ";年齡:" + age) ;
        rdf.skipBytes(12) ; // 跳過第二個人的信息
        for(int i=0;i<b.length;i++){
            b[i] = rdf.readByte() ; // 讀取一個字節
        }
        name = new String(b) ;  // 將讀取出來的byte數組變爲字符串
        age = rdf.readInt() ;   // 讀取數字
        System.out.println("第三個人的信息 --> 姓名:" + name + ";年齡:" + age) ;
        rdf.close() ;               // 關閉
    }
};

結果如下:

result.png

 

在Java程序中所有的數據都是以的方式進行傳輸或保存的,程序需要數據的時候要使用輸入流讀取數據,而當程序需要將一些數據保存起來的時候,就要使用輸出流完成。程序中的輸入輸出都是以流的形式保存的,流中保存的實際上全都是字節文件。流涉及的領域很廣:標準輸入輸出,文件的操作,網絡上的數據流,字符串流,對象流,zip文件流等等。

Stream.png

 

流具有方向性,至於是輸入流還是輸出流則是一個相對的概念,一般以程序爲參考,如果數據的流向是程序至設備,我們成爲輸出流,反之我們稱爲輸入流。
可以將流想象成一個“水流管道”,水流就在這管道中形成了,自然就出現了方向的概念。

Information.jpg

 

先上一個Java IO流類層次圖,如前所述,一個流被定義爲一個數據序列。輸入流用於從源讀取數據,輸出流用於向目標寫數據:

JavaIO流類層次圖.png

 

是不是被嚇到了?沒關係,我們將通過一個個例子來學習這些功能。

IO流分類

1.按操作數據類型分:字符流和字節流

編碼與解碼:編碼就是把字符轉換爲字節,而解碼是把字節重新組合成字符。
如果編碼和解碼過程使用不同的編碼方式那麼就出現了亂碼。

  • GBK 編碼中,中文字符佔 2 個字節,英文字符佔 1 個字節;
  • UTF-8 編碼中,中文字符佔 3 個字節,英文字符佔 1 個字節;
  • UTF-16be 編碼中,中文字符和英文字符都佔 2 個字節。

UTF-16be 中的 be 指的是 Big Endian,也就是大端。相應地也有 UTF-16le,le 指的是 Little Endian,也就是小端。
Java 使用雙字節編碼 UTF-16be,這不是指 Java 只支持這一種編碼方式,而是說 char 這種類型使用 UTF-16be 進行編碼。char 類型佔 16 位,也就是兩個字節,Java 使用這種雙字節編碼是爲了讓一箇中文或者一個英文都能使用一個 char 來存儲。

字符流:Java中的字符流處理的最基本的單元是2字節的Unicode碼元(char),它通常用來處理文本數據,如字符、字符數組或字符串等。所謂Unicode碼元,也就是一個Unicode代碼單元,範圍是0x0000~0xFFFF。在以上範圍內的每個數字都與一個字符相對應,Java中的String類型默認就把字符以Unicode規則編碼而後存儲在內存中。然而與存儲在內存中不同,存儲在磁盤上的數據通常有着各種各樣的編碼方式。使用不同的編碼方式,相同的字符會有不同的二進制表示。實際上字符流是這樣工作的:

  • 輸出字符流:把要寫入文件的字符序列(實際上是Unicode碼元序列)轉爲指定編碼方式下的字節序列,然後再寫入到文件中。
  • 輸入字符流:把要讀取的字節序列按指定編碼方式解碼爲相應字符序列(實際上是Unicode碼元序列從)從而可以存在內存中。

也就是說,所有的文件在硬盤或在傳輸時都是以字節的方式進行的,包括圖片等都是按字節的方式存儲的,而字符是只有在內存中才會形成。

字節流:Java中的字節流處理的最基本單位爲單個字節(byte),它通常用來處理二進制數據,如果要得到字節對應的字符需要強制類型轉換。

兩者比較: 
1.字符流是由Java虛擬機將字節轉化爲2個字節的Unicode字符爲單位的字符而成的,所以它對多國語言支持性較好,如果要操作中文數據等,用字符流。
2.字符流只用來處理文本數據,字節流還可以用來處理媒體數據,如視頻、音頻、圖片等。
3.字符流的兩個抽象基類爲Reader和Writer,字節流的兩個抽象基類爲InputStream和OutputStream。它們的具體子類名以基類名爲後綴進行擴展。
4.字節流在操作的時候不會用到緩衝區(內存),是直接對文件本身操作的,而字符流在操作的時候使用緩衝區。

Compare.jpg

 

以向一個文件輸出"Hello world!"爲例,我們分別使用字節流和字符流進行輸出,且在使用完之後都不關閉流。

使用字節流不關閉執行:

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;


public class IOPractice {

    public static void main(String[] args) throws IOException {
        // 第1步:使用File類找到一個文件    
             File f = new File("/home/xiejunyu/"+
             "桌面/text.txt");   
        // 第2步:通過子類實例化父類對象     
             OutputStream out = new FileOutputStream(f); 
        // 通過對象多態性進行實例化    
        // 第3步:進行寫操作    
             String str = "Hello World!";      
        // 準備一個字符串    
             byte b[] = str.getBytes();          
        // 字符串轉byte數組    
             out.write(b);                      
        // 將內容輸出    
         // 第4步:關閉輸出流    
            // out.close();                  
        // 此時沒有關閉    
                }    
}

1.png

此時沒有關閉字節流操作,但是文件中也依然存在了輸出的內容,證明字節流是直接操作文件本身的。

使用字符流不關閉執行:

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;


public class IOPractice {

    public static void main(String[] args) throws IOException {
         // 第1步:使用File類找到一個文件    
        File f = new File("/home/xiejunyu/桌面/test.txt");
        // 第2步:通過子類實例化父類對象            
        Writer  out = new FileWriter(f);            
        // 第3步:進行寫操作    
        String str = "Hello World!";      
        // 準備一個字符串    
        out.write(str);                    
        // 將內容輸出    
        // 第4步:關閉輸出流    
        // out.close();
        // 此時沒有關閉    
    }    
}

2.png

程序運行後會發現文件中沒有任何內容,這是因爲字符流操作時使用了緩衝區,而在關閉字符流時會強制性地將緩衝區中的內容進行輸出,但是如果程序沒有關閉字符流,緩衝區中的內容是無法輸出的,所以得出結論:字符流使用了緩衝區,而字節流沒有使用緩衝區。如果想讓緩衝區中的內容輸出,要麼關閉流強制刷新緩衝區,要麼調用flush方法沖刷緩衝區。可以簡單地把緩衝區理解爲一段特殊的內存。某些情況下,如果一個程序頻繁地操作一個資源(如文件或數據庫),則性能會很低,此時爲了提升性能,就可以將一部分數據暫時讀入到內存的一塊區域之中,以後直接從此區域中讀取數據即可,因爲讀取內存速度會比較快,這樣可以提升程序的性能。
在字符流的操作中,所有的字符都是在內存中形成的,在輸出前會將所有的內容暫時保存在內存之中,所以使用了緩衝區暫存數據。

建議: 
1.雖然不關閉字節流不影響數據的輸出,且後續JVM會自動回收這部分內存,但還是建議在使用完任何流對象之後關閉流。
2.使用流對象都要聲明或拋出IOException
3.在創建一個文件時,如果目錄下有同名文件將被覆蓋
4.在寫文件時,如果文件不存在,會在創建輸出流對象並綁定文件時自動創建文件,不必使用File的exists方法提前檢測
4.在讀取文件時,必須使用File的exists方法提前檢測來保證該文件已存在,否則拋出FileNotFoundException

2.按流向分:輸入流和輸出流

輸入流:程序從輸入流讀取數據源。數據源包括外界(鍵盤、文件、網絡等),即是將數據源讀入到程序的通信通道。輸入流主要包括兩個抽象基類:InputStream(字節輸入流)和Reader(字符輸入流)及其擴展的具體子類。
輸出流:程序向輸出流寫入數據。將程序中的數據輸出到外界(顯示器、打印機、文件、網絡等)的通信通道。輸出流主要包括兩個抽象基類:OutputStream(字節輸出流)和Writer(字符輸出流)及其擴展的具體子類。

3.按功能分:節點流和處理流

按照流是否直接與特定的地方(如磁盤、內存、設備等)相連,分爲節點流和處理流兩類。
節點流:程序用於直接操作目標設備所對應的類叫節點流。(低級流)
處理流:程序通過一個間接流類去調用節點流類,以達到更加靈活方便地讀寫各種類型的數據,這個間接流類就是處理流。處理流可以看成是對已存在的流進行連接和封裝的流。(高級流)

注意:在使用到處理流對流進行連接和封裝時,讀寫完畢只需關閉處理流,不用關閉節點流。處理流關閉的時候,會調用其處理的節點流的關閉方法。如果將節點流關閉以後再關閉處理流,會拋出IO異常。

(1) 節點流

節點流.png

  • File 文件流。對文件進行讀、寫操作:FileReader、FileWriter、FileInputStream、FileOutputStream。
  • Memory 流。
    向內存數組讀寫數據: CharArrayReader與 CharArrayWriter、ByteArrayInputStream與ByteArrayOutputStream。
    向內存字符串讀寫數據:StringReader、StringWriter、StringBufferInputStream。
  • Pipe管道流:實現管道的輸入和輸出(進程間通信): PipedReader與PipedWriter、PipedInputStream與PipedOutputStream。

節點流示意圖.png

(1) 處理流

處理流.png

  • Buffering緩衝流:在讀入或寫出時,對數據進行緩存,以減少I/O的次數:BufferedReader與BufferedWriter、BufferedInputStream與BufferedOutputStream。
  • Filtering 濾流:在數據進行讀或寫時進行過濾:FilterReader與FilterWriter、FilterInputStream與FilterOutputStream。
  • Converting between Bytes and Characters 轉換流:按照一定的編碼/解碼標準將字節流轉換爲字符流,或進行反向轉換(Stream到Reader):InputStreamReader、OutputStreamWriter。
  • Object Serialization 對象流 :ObjectInputStream、ObjectOutputStream。
  • DataConversion數據流:按基本數據類型讀、寫(處理的數據是Java的基本類型):DataInputStream、DataOutputStream 。
  • Counting計數流:在讀入數據時對行記數 :LineNumberReader、LineNumberInputStream。
  • Peeking Ahead預讀流: 通過緩存機制,進行預讀 :PushbackReader、PushbackInputStream。
  • Printing打印流: 包含方便的打印方法 :PrintWriter、PrintStream。

處理流示意圖.png

讀取控制檯輸入

在Java中,從控制檯輸入有三種方法:

1.使用標準輸入流對象System.in

System.in是System中內置的InputStream類對象,它的read方法一次只讀入一個字節數據,返回0 ~ 255的一個byte值,一般用來讀取一個字符,需要強制類型轉換爲char類型,而我們通常要取得一個字符串或一組數字,故這種方法不常用。下面給出這種方法的一個例子:

public class CharTest{
public static void main(String[] args) {
         try{   
         System.out.print("Enter a Char:");   
         char i = (char)System.in.read();   
         System.out.println("Yout Enter Char is:" + i);           }   
         catch(IOException e){   
            e.printStackTrace();   
         }   
    }
}

使用這種方法必須提供try-catch塊或者在main方法首部聲明IOException異常,因爲System.in是一個流對象

2.使用Scanner類

Scanner類功能十分強大,可以讀入字符串、整數、浮點數、布爾類型值等等。下面是例子:

public class ScannerTest{
public static void main(String[] args){
    Scanner sc = new Scanner(System.in);   
    System.out.println("ScannerTest, Please Enter Name:");   
    String name = sc.nextLine();  //讀取字符串型輸入   
    System.out.println("ScannerTest, Please Enter Age:");   
    int age = sc.nextInt();    //讀取整型輸入   
    System.out.println("ScannerTest, Please Enter Salary:");   
    float salary = sc.nextFloat(); //讀取float型輸入   
    System.out.println("Your Information is as below:");   
    System.out.println("Name:" + name +"\n" + "Age:"+age 
    + "\n"+"Salary:"+salary);   
    }
 }   

注意: 
1.用nextXXX()讀入XXX類型的數據,XXX可以是除了char外的所有基本數據類型,還可以是BigInteger或BigDecimal,其中凡是整型類型的數據還可以指定radix(進制),可以用next()和nextLine()讀取一個字符串或一行字符
2.next()和nextLine()的區別:
next()

  • 一定要讀取到有效字符後纔可以結束輸入。
  • 對輸入有效字符之前遇到的空白,next() 方法會自動將其去掉。
  • 只有輸入有效字符後纔將其後面輸入的空白作爲分隔符或者結束符。
  • next() 不能得到帶有空格的字符串,除非用useDelimeter方法修改分隔符。

nextLine()

  • 以Enter爲結束符,也就是說 nextLine()方法返回的是輸入回車之前的所有字符。
  • 可以獲得空白。

3.可以用循環配合hasNextXXX方法判斷輸入是否繼續
4.Scanner類沒有直接提供讀取一個字符的方法,如果要讀取一個字符,有三種方法,一是讀入一個字符串後取字符串的第一個字符,二是使用System.in的read方法,三是使用字符流讀入

更多Scanner的用法之前已經在Java學習總結之Java基本程序設計結構中總結過了,不再贅述。

3.使用BufferedReader對象

可以把 System.in 包裝在一個 BufferedReader 對象中來創建一個字符流
下面是創建 BufferedReader 的基本語法:

BufferedReader br = new BufferedReader(new 
                      InputStreamReader(System.in));

其中,System.in是一個InputStream對象(字節流),使用InputStreamReader作爲橋樑,將字節流轉換爲字符流,然後再使用BufferedReader進行進一步包裝。
BufferedReader 對象創建後,我們便可以使用 read() 方法從控制檯讀取一個字符(讀入一個用0~65535之間的整數表示的字符,需要強制類型轉換爲char類型,如果已到達流末尾,則返回 -1),或者用 readLine() 方法讀取一個字符串。下面是例子:

public static void main(String[] args){
//必須要處理java.io.IOException異常
  BufferedReader br = new BufferedReader(new InputStreamReader
  (System.in ));
  //java.io.InputStreamReader繼承了Reader類
  String read = null;
  System.out.print("輸入數據:");
  try {
   read = br.readLine();
  } catch (IOException e) {
   e.printStackTrace();
  }
  System.out.println("輸入數據:"+read);
 }

下面的程序示範了用 read() 方法從控制檯不斷讀取字符直到用戶輸入 "q"。

// 使用 BufferedReader 在控制檯讀取字符
 
import java.io.*;
 
public class BRRead {
  public static void main(String args[]) throws IOException
  {
    char c;
    // 使用 System.in 創建 BufferedReader 
    BufferedReader br = new BufferedReader(new 
                       InputStreamReader(System.in));
    System.out.println("輸入字符, 按下 'q' 鍵退出。");
    // 讀取字符
    do {
       c = (char) br.read();
       System.out.println(c);
    } while(c != 'q');
  }
}

下面的程序讀取和顯示字符行直到你輸入了單詞"end"。

// 使用 BufferedReader 在控制檯讀取字符
import java.io.*;
public class BRReadLines {
  public static void main(String args[]) throws IOException
  {
    // 使用 System.in 創建 BufferedReader 
    BufferedReader br = new BufferedReader(new
                            InputStreamReader(System.in));
    String str;
    System.out.println("Enter lines of text.");
    System.out.println("Enter 'end' to quit.");
    do {
       str = br.readLine();
       System.out.println(str);
    } while(!str.equals("end"));
  }
}

在ACM等算法競賽中,我們常常也會使用Java,在輸入數據時有以下幾點注意: 
1.hasXXX等價於C++中讀到文件末尾(EOF)
2.使用BufferedReader輸入會比Scanner輸入快十倍左右!

控制檯輸出

控制檯的輸出由 print() 和 println() 完成。這些方法都由類 PrintStream 定義,System.out 是該類的一個對象。
PrintStream 繼承了 OutputStream類,並且實現了方法 write()。這樣,write() 也可以用來往控制檯寫操作。
PrintStream 定義 write() 的最簡單格式如下所示:
void write(int byteval)該方法將 byteval 的低八位字節寫到流中,即System.out的write方法一次只能寫一個字節(類比System.in的read方法一次只能讀取一個字節)。
下面的例子用 write() 把字符 "A" 和緊跟着的換行符輸出到屏幕:

import java.io.*;
 
// 演示 System.out.write().
public class WriteDemo {
   public static void main(String args[]) {
      int b; 
      b = 'A';//向上類型轉換
      System.out.write(b);
      System.out.write('\n');
   }
}

注意:write() 方法不經常使用,因爲 print() 和 println() 方法用起來更爲方便。

字節流(OutputStream、InputStream)

字節流主要是操作byte類型的數據,以byte數組爲準,主要操作類是OutputStream、InputStream。
由於文件讀寫最爲常見,我們先討論兩個重要的字節流 FileInputStream(文件輸入流) 和 FileOutputStream(文件輸出流),分別是抽象類InputStream和OutputStream的具體子類:

FileInputStream 
該流用於從文件讀取數據,它的對象可以用關鍵字 new 來創建。
有多種構造方法可用來創建對象。
可以使用字符串類型的文件名來創建一個輸入流對象來讀取文件:

InputStream f = new FileInputStream("C:/java/hello");

也可以使用一個文件對象來創建一個輸入流對象來讀取文件。我們首先得使用 File() 方法來創建一個文件對象:

File f = new File("C:/java/hello");
InputStream in = new FileInputStream(f);

創建了InputStream對象,就可以使用下面的方法來讀取流或者進行其他的流操作。

 

InputStream.png

下面是一個例子:

public static void main(String[] args) throws IOException{
    InputStream f  = new FileInputStream
    ("/home/xiejunyu/桌面/test.txt");
    int c = 0;
    while((c =  f.read()) != -1) 
    //這裏也可以先用available方法得到可讀的字節數
    System.out.println((char)c);
}

當我們需要創建一個byte[]來保存讀取的字節時,如果數組太小,無法完整讀入數據,如果太大又會造成內存浪費。可以使用File類的length方法得到文件的數據字節數,從而有效確定byte數組的大小。

public static void main(String[] args) {
        // 創建一個FileInputStream對象
        try {
            FileInputStream fis = new FileInputStream("/home/xiejunyu/桌面/test.txt");
            byte[] b=new byte[100];
            fis.read(b,0,5); 
            /*把字節從文件讀入b數組,從b數組的0位置開始存放,
            讀取5個字節*/
            System.out.println(new String(b));
            fis.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

注意: 每調用一次read方法,當前讀取在文件中的位置就會向後移動一個字節或者移動byte[]的長度(read的兩個重載方法),已經到文件末尾會返回-1,可以通過read方法返回-1判斷是否讀到文件末尾,也可以使用available方法返回下一次可以不受阻塞讀取的字節數來讀取。FileInputStream不支持mark和reset方法進行重複讀取。BufferedInputStream支持此操作。

FileOutputStream 
該類用來創建一個文件並向文件中寫數據。
如果該流在打開文件進行輸出前,目標文件不存在,那麼該流會創建該文件。 
有兩個構造方法可以用來創建 FileOutputStream 對象。
使用字符串類型的文件名來創建一個輸出流對象:

OutputStream f = new FileOutputStream("C:/java/hello")

也可以使用一個文件對象來創建一個輸出流來寫文件。我們首先得使用File()方法來創建一個文件對象:

File f = new File("C:/java/hello");
OutputStream f = new FileOutputStream(f);

之前的所有操作中,如果重新執行程序,則肯定會覆蓋文件中的已有內容,那麼此時就可以通過FileOutputStream向文件中追加內容,FileOutputStream的另外一個構造方法:

public FileOutputStream(File file,boolean append) 

在構造方法中,如果將append的值設置爲true,則表示在文件的末尾追加內容。程序代碼如下:

File f = new File("C:/java/hello");
OutputStream f = new FileOutputStream(f,true);

創建OutputStream 對象完成後,就可以使用下面的方法來寫入流或者進行其他的流操作。

FileOutputStream.png

 

當有一個字符串時,可以用getBytes方法轉爲byte數組用於字節流的輸出。

下面是一個演示 InputStream 和 OutputStream 用法的例子:

import java.io.*;
 
public class FileStreamTest{
  public static void main(String args[]){
    try{
      byte bWrite[] = "ABC".getBytes();
      OutputStream os = new FileOutputStream("/home/xiejunyu/桌面/test.txt");
      for(int x=0; x < bWrite.length ; x++){
      os.write(bWrite[x] ); // writes the bytes
    }
    os.close();
 
    InputStream is = new FileInputStream("/home/xiejunyu/桌面/test.txt");
    int size = is.available();
 
    for(int i=0; i< size; i++){
      System.out.print((char)is.read() + "  ");
    }
      is.close();
    }catch(IOException e){
      System.out.print("Exception");
    }  
  }
}

上面的程序首先創建文件test.txt,並把給定的數字以二進制形式寫進該文件,同時輸出到控制檯上。
以上代碼由於是二進制寫入,可能存在亂碼,你可以使用以下代碼實例來解決亂碼問題:

import java.io.*;
 
public class fileStreamTest2{
  public static void main(String[] args) throws IOException {
    
    File f = new File("a.txt");
    FileOutputStream fop = new FileOutputStream(f);
    // 構建FileOutputStream對象,文件不存在會自動新建;如果存在會覆蓋原文件
    
    OutputStreamWriter writer = new OutputStreamWriter(fop, "UTF-8");
    // 構建OutputStreamWriter對象,參數可以指定編碼,默認爲操作系統默認編碼,windows上是gbk
    
    writer.append("中文輸入");
    // 寫入到緩衝區
    
    writer.append("\r\n");
    //換行
    
    writer.append("English");
    // 刷新緩衝區,寫入到文件,如果下面已經沒有寫入的內容了,直接close也會寫入
    
    writer.close();
    //關閉寫入流,同時會把緩衝區內容寫入文件,所以上面的註釋掉
    
    fop.close();
    // 關閉輸出流,釋放系統資源
 
    FileInputStream fip = new FileInputStream(f);
    // 構建FileInputStream對象
    
    InputStreamReader reader = new InputStreamReader(fip, "UTF-8");
    // 構建InputStreamReader對象,編碼與寫入相同
 
    StringBuffer sb = new StringBuffer();
    while (reader.ready()) {
      sb.append((char) reader.read());
      // 轉成char加到StringBuffer對象中
    }
    System.out.println(sb.toString());
    reader.close();
    // 關閉讀取流
    
    fip.close();
    // 關閉輸入流,釋放系統資源
 
  }
}

以上例子證明:在對多國語言的支持上,字符流表現更優,此時應使用字符流而不是字節流。

還可以用InputStream和OutputStream配合進行文件的複製,即讀取原件數據,寫入副本文件。
複製有兩種實現方式:
實現一:將源文件中的內容全部讀取進來,之後一次性的寫入到目標文件
實現二:邊讀邊寫

在實際開發中建議使用邊讀邊寫的方式,代碼如下:

public static void main(String[] args) {
        // 文件拷貝
        try {
            FileInputStream fis=new FileInputStream("happy.gif");
            FileOutputStream fos=new FileOutputStream("happycopy.gif");
            int n=0;
            byte[] b=new byte[1024];
            while((n=fis.read(b))!=-1){ 
            /*循環讀取,每次1024個字節,最後一次可能不滿1024。
            後面的字節覆蓋前面的字節,不必擔心數組溢出。*/
                fos.write(b,0,n); //n是實際讀取到的字節數,如果寫fos.write(b),會造成最後一次數組未滿的情況也寫1024個字節,從而造成副本比原件略大
            }
            fis.close();
            fos.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }catch(IOException e){
            e.printStackTrace();
        }
    }

實際上邊讀邊寫也分爲三種方式:
1.批量拷貝(循環讀取,每次讀入一個byte數組)
2.緩衝拷貝(使用緩衝流)
3.批量+緩衝拷貝(循環批量讀取到字節數組中,然後使用緩衝輸出流寫入到文件)

第三種方式是最快的。

注意:InputStream的int read()方法讀取一個字節,並用這個字節填充整型的低八位並返回,OutputStream的void write(int x)寫入x的低八位,如果要寫入一個int,需要移位並寫4次。讀寫基本數據類型建議使用DataInputStream和DataOutputStream。小編推薦一個學Java的學習社區【 戳我立即加入 】,無論你是大牛還是小白,是想轉行還是想入行都可以來了解一起進步一起學習!裙內有開發工具,很多幹貨和技術資料分享!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章