前言
本文以圖文並茂的形式重點記錄了這一週學習Java中IO操作的心得,並配以大量練習代碼。Java的IO流無外乎就是輸入流和輸出流,所以基礎部分還是比較簡單的。
簡述
IO:用於處理設備上的數據的技術。設備:內存、硬盤、光盤。java中所涉及的功能對象都存儲到java.io包中。
流:系統資源,windows系統本身就可以操作設備。各種語言只是使用了系統平臺上的這個資源。並對外提供了各種語言自己的操作功能,這些功能最終調用的是系統資源。使用完資源一定要記住:釋放。(也就是說IO一定要寫finally!)
IO流也進行分類:
1:輸入流(讀)和輸出流(寫)。
2:因爲處理的數據不同,分爲字節流和字符流。
輸入流和輸出流相對於內存設備而言。
將外設中的數據讀取到內存中:輸入-->讀
將內存的數寫入到外設中:輸出-->寫
注意:流的操作只有兩種:讀和寫。
字節流:處理字節數據的流對象。設備上的數據無論是圖片或者dvd,文字,它們都以二進制存儲的。二進制的最終都是以一個8位爲數據單元進行體現,所以計算機中的最小數據單元就是字節。意味着,字節流可以處理設備上的所有數據,所以字節流一樣可以處理字符數據。
字符流:因爲字符每個國家都不一樣,所以涉及到了字符編碼問題,那麼GBK編碼的中文用unicode編碼解析是有問題的,所以需要獲取中文字節數據的同時+ 指定的編碼表纔可以解析正確數據。爲了方便於文字的解析,所以將字節流和編碼表封裝成對象,這個對象就是字符流。只要操作字符數據,優先考慮使用字符流體系。
流的體系因爲功能不同,但是有共性內容,不斷抽取,形成繼承體系。該體系一共有四個基類,而且都是抽象類。
字節流:InputStream OutputStream
字符流:Reader Writer
在這四個系統中,它們的子類,都有一個共性特點:子類名後綴都是父類名,前綴名都是這個子類的功能名稱。
File類
java.io.File類將文件系統中的文件和文件夾封裝成了對象。提供了更多的屬性和行爲可以對這些文件和文件夾進行操作。這些是流對象辦不到的,因爲流只操作數據。設備上的數據(0、1串)最常見的存儲表現形式是文件file。先學習一下文件的基本操作。
查閱API,描述文件或者文件夾(目錄路徑名)的類是File類。常用方法需要查表。
首先我們來演示一下File構造函數的使用。
- package ustc.lichunchun.file.demo;
-
- import java.io.File;
- /*
- * File對象創建過程、字段信息。
- *
- * 注意:File文件對象只能操作文件或者文件夾的屬性,
- * 例如文件或文件夾的創建、刪除、獲取文件屬性(大小、所在目錄等),
- * 但我們最終建立文件的目的,是往文件裏面存數據,File對象是做不了這個的。
- * 這時,我們就要用到IO流。
- */
- public class FileDemo {
-
- //File類已經提供了相應字段。
- //private static final String FILE_SEPARATOR = System.getProperty("file.separator");
-
- public static void main(String[] args) {
-
- //將某一個文件或者文件夾封裝成了File對象。可以封裝存在的文件或目錄,也可以封裝不存在的文件或目錄。
- //注意,這裏File對象封裝的實際上是1.txt,至於d:\\只是作爲全路徑目錄。
- //再比如,new File("abc\\a\\b\\c");實際上封裝的是c文件夾以及其全路徑目錄。
- File file = new File("d:\\1.txt");
-
- //File(String parent, String child);這樣可以將目錄和文件名分開。
- File file1 = new File("d:\\", "1.txt");
-
- File dir = new File("d:\\");
- File file2 = new File(dir, "1.txt");
-
- //File f = new File("d:"+System.getProperty("file.separator")+"abc"+System.getProperty("file.separator")+"1.txt");
-
- //File f = new File("d:"+FILE_SEPARATOR+"abc"+FILE_SEPARATOR+"1.txt");
-
- File f = new File("d:"+File.separator+"abc"+File.separator+"1.txt");
-
- System.out.println(f);
- }
- }
如果把某個文件視爲一個對象,讓你來描述這個文件,你認爲,它應該具備什麼樣的功能或者方法?
File類常見方法:
1:創建。
boolean createNewFile():在指定目錄下創建文件,如果該文件已存在,則不創建。而對操作文件的輸出流而言,比如FileOutputStream,輸出流對象已建立,就會創建文件,如果文件已存在,會覆蓋。除非續寫。
boolean mkdir():創建此抽象路徑名指定的目錄。
boolean mkdirs():創建多級目錄。
2:刪除。
boolean delete():刪除此抽象路徑名錶示的文件或目錄。
void deleteOnExit():在虛擬機退出時刪除。
注意:在刪除文件夾時,必須保證這個文件夾中沒有任何內容,纔可以將該文件夾用delete刪除。
window的刪除動作,是從裏往外刪。注意:java刪除文件不走回收站。要慎用。
3:獲取。
long length():獲取文件大小。
String getName():返回由此抽象路徑名錶示的文件或目錄的名稱。
String getPath():將此抽象路徑名轉換爲一個路徑名字符串。
String getAbsolutePath():返回此抽象路徑名的絕對路徑名字符串。
String getParent():返回此抽象路徑名父目錄的抽象路徑名,如果此路徑名沒有指定父目錄,則返回 null。
long lastModified():返回此抽象路徑名錶示的文件最後一次被修改的時間。
File.pathSeparator:返回當前系統默認的路徑分隔符,windows默認爲 「;」。
File.Separator:返回當前系統默認的目錄分隔符,windows默認爲 「\」。
4:判斷。
boolean exists():判斷文件或者文件夾是否存在。
boolean isDirectory():測試此抽象路徑名錶示的文件是否是一個目錄。
boolean isFile():測試此抽象路徑名錶示的文件是否是一個標準文件。
boolean isHidden():測試此抽象路徑名指定的文件是否是一個隱藏文件。
boolean isAbsolute():測試此抽象路徑名是否爲絕對路徑名。
5:重命名。
boolean renameTo(File dest):可以實現移動的效果。剪切+重命名。
String[] list():列出指定目錄下的當前的文件和文件夾的名稱。包含隱藏文件。如果調用list方法的File 對象中封裝的是一個文件,那麼list方法返回數組爲null。如果封裝的對象不存在也會返回null。只有封裝的對象存在並且是文件夾時,這個方法纔有效。
注意:list(FilenameFilter filter)和listFiles(FileFilter filter)往往是和過濾器實例參數一起使用。
代碼示例1:
- package ustc.lichunchun.file.demo;
-
- import java.io.File;
-
- public class FileMethodDemo {
-
- public static void main(String[] args) {
-
- /*
- * File類,常見方法。
- * 1.名字。獲取名稱。
- * String getName();
- * 2.大小。獲取大小。
- * long length();
- * 3.類型。獲取類型。
- * 沒有,因爲類型可以自定義。
- * 4.獲取所在目錄。
- * String getParent();
- */
-
- File file = new File("d:\\abc\\1.txt");
- String file_name = file.getName();
- System.out.println(file_name);//1.txt-->File對象封裝的是1.txt所在路徑(而且不是絕對路徑)。
-
- long len = file.length();
- System.out.println(len);//0
-
- System.out.println(file.getParent());//d:\abc
- }
- }
代碼示例2:
代碼示例3:
- package ustc.lichunchun.file.demo;
-
- import java.io.File;
-
- public class FileMethodTest2 {
-
- public static void main(String[] args) {
-
- /*
- * 9.獲取指定文件夾中的所有文件和文件夾的名稱。
- */
-
- File dir = new File("d:\\");
-
- String[] names = dir.list();//列出當前目錄下的所有文件和文件夾名稱,包含隱藏文件。
- //list()侷限:只獲取名稱。
- //如果目錄存在但是沒有內容,會返回一個數組,但是長度爲0。
- if(names != null){
- for (String name : names) {
- System.out.println(name);
- }
- }
- System.out.println("-------------------------");
-
- File[] files = dir.listFiles();//獲取當前目錄下的所有文件和文件夾的File對象,更爲常用。
- for(File f : files){
- System.out.println(f.getName()+"......"+f.length());
- }
- }
- }
過濾器
接下來我要介紹的是文件名過濾器和文件過濾器,它們的用途是爲了獲取指定目錄下的指定類型文件。所採用到的設計模式屬於策略設計模式,目的就是爲了降低容器和過濾條件之間的耦合性。
1.文件名過濾器:FilenameFilter
它的底層源碼如下:
- public String[] list(FilenameFilter filter) {
- String names[] = list();
- if ((names == null) || (filter == null)) {
- return names;
- }
- List<String> v = new ArrayList<>();
- for (int i = 0 ; i < names.length ; i++) {
- if (filter.accept(this, names[i])) {
- v.add(names[i]);
- }
- }
- return v.toArray(new String[v.size()]);
- }
下面實現一個判斷文件是否是以.java格式結尾的過濾器:
- package ustc.lichunchun.filter;
-
- import java.io.File;
- import java.io.FilenameFilter;
- /*
- * 根據文件名稱的後綴名進行過濾的過濾器。
- */
- public class FilterBySuffix implements FilenameFilter {
-
- private String suffix;
-
- public FilterBySuffix(String suffix) {
- super();
- this.suffix = suffix;
- }
-
- /**
- * @param name 被遍歷目錄dir中的文件夾或者文件的名稱。
- */
- @Override
- public boolean accept(File dir, String name) {
-
- return name.endsWith(suffix);
- }
- }
示例1:獲取指定目錄下的.java文件:
- package ustc.lichunchun.file.demo;
-
- import java.io.File;
-
- import ustc.lichunchun.filter.FilterBySuffix;
-
- public class FilenameFilterDemo {
-
- public static void main(String[] args) {
-
- /*
- * 10.能不能只獲取指定目錄下的.java文件呢?
- * 文件名過濾器:list(FilenameFilter filter);
- */
-
- /*
- File dir = new File("d:\\");
- String[] names = dir.list();
- for(String name : names){
- if(name.endsWith(".java"))//-->耦合性太強。
- System.out.println(name);
- }
- */
-
- //文件名過濾器:讓容器和過濾條件分離,降低耦合性。
- //類似於比較器,都屬於策略設計模式。不要面對具體的過濾或者排序動作,我只面對接口。
- File dir = new File("d:\\");
- //傳入一個過濾器。
- String[] names = dir.list(new FilterBySuffix(".java"));
- for(String name : names){
- System.out.println(name);
- }
- }
- }
我又實現了一個文件名包含指定字段的過濾器:
- package ustc.lichunchun.filter;
-
- import java.io.File;
- import java.io.FilenameFilter;
-
- public class FilterByContain implements FilenameFilter {
-
- private String content;
-
- public FilterByContain(String content) {
- super();
- this.content = content;
- }
-
- @Override
- public boolean accept(File dir, String name) {
-
- return name.contains(content);
- }
- }
示例2:獲取指定目錄下,文件名中包含指定字段的文件。
- package ustc.lichunchun.file.demo;
-
- import java.io.File;
- import java.io.FilenameFilter;
-
- import ustc.lichunchun.filter.FilterByContain;
- import ustc.lichunchun.filter.FilterBySuffix;
-
- public class FilenameFilterDemo2 {
-
- public static void main(String[] args) {
-
- //需求:不是獲取指定後綴名的文件,而是獲取文件名中包含指定字段的文件。
- File dir = new File("d:\\");
-
- FilenameFilter filter = new FilterBySuffix(".java");//過濾後綴名的過濾器。
- filter = new FilterByContain("Demo");//過濾內容的過濾器。
-
- String[] names = dir.list(filter);
-
- for(String name : names){
- System.out.println(name);
- }
- }
- }
2.文件過濾器:FileFilter
文件過濾器其實更爲常用。因爲過濾器中pathname.getName().endsWith(".java")可以實現同樣的文件名過濾操作。
我再用文件過濾器的方法,實現上面文件名過濾器所示例的,過濾指定類型文件的過濾器:
- package ustc.lichunchun.filter;
-
- import java.io.File;
- import java.io.FileFilter;
-
- public class FilterBySuffix2 implements FileFilter {
-
- private String suffix;
-
- public FilterBySuffix2(String suffix) {
- super();
- this.suffix = suffix;
- }
-
- @Override
- public boolean accept(File pathname) {
-
- return pathname.getName().endsWith(suffix);
- }
- }
爲了更好地讓讀者理解,下面還實現了當前目錄下,只過濾出文件和只過濾出文件夾的文件過濾器:
- package ustc.lichunchun.filter;
-
- import java.io.File;
- import java.io.FileFilter;
-
- public class FilterByFile implements FileFilter {
-
- @Override
- public boolean accept(File pathname) {
-
- return pathname.isFile();//文件過濾器。只篩選出文件,不要文件夾。
- }
- }
- package ustc.lichunchun.filter;
-
- import java.io.File;
- import java.io.FileFilter;
-
- public class FilterByDirectory implements FileFilter {
-
- @Override
- public boolean accept(File pathname) {
-
- return pathname.isDirectory();
- }
- }
示例3:
- package ustc.lichunchun.file.demo;
-
- import java.io.File;
- import java.io.FileFilter;
-
- import ustc.lichunchun.filter.FilterByDirectory;
- import ustc.lichunchun.filter.FilterByFile;
- import ustc.lichunchun.filter.FilterBySuffix2;
-
- public class FileFilterDemo {
-
- public static void main(String[] args) {
-
- File dir = new File("d:\\");
-
- FileFilter filter = new FilterByFile();//過濾出當前目錄下所有文件
- filter = new FilterByDirectory();//過濾出當前目錄下所有文件夾
- filter = new FilterBySuffix2(".java");//過濾出當前目錄下所有以指定後綴名結尾的文件和文件夾
-
- File[] files = dir.listFiles(filter);
-
- for(File file : files){
- System.out.println(file);
- }
- System.out.println("-------------------------");
- }
- }
練習:編寫一個完整的Java Application程序。主類爲NumberArray,要求NumberArray能夠通過對接口NumberFilter的調用完成從一組整數中過濾出滿足NumberArray使用者篩選條件的部分整數,設計中涉及到的類和接口的具體要求和含義如下:
(1)接口NumberFilter:表示過濾器接口。接口NumberFilter的抽象方法boolean is(int n)供使用者實現以設置篩選條件。
(2)類NumberArray:表示一組整數的對象(可用int型數組作爲其成員來存放一組整數)。類NumberArray的方法print(0輸出滿足過濾器NumberFilter設置的篩選條件的部分整數。類NumberArray的方法setFilter(NumberFilter nf)用於設置過濾器。
(3)類Filter:表示具體的過濾器,實現接口NumberFilter,實現is()方法用於設置篩選條件,這裏要求過濾出偶數。
- package ustc.lichunchun.filter;
- public class Test {
- public static void main(String[] args) {
- int[] array = {1,2,3,4,5,6,7,8,9};
- NumberFilter nf = new Filter();
- NumberArray na = new NumberArray(array, nf);
- na.print();
- }
- }
- class NumberArray {
- private int[] array;
- public NumberArray(int[] array, NumberFilter nf) {
- super();
- this.array = array;
- this.nf = nf;
- }
- private NumberFilter nf;
- public void print(){
- for(int i : array){
- if(nf.is(i)){
- System.out.print(i+ " ");
- }
- }
- System.out.println();
- }
- public void setFilter(NumberFilter nf){
- this.nf = nf;
- }
- }
- class Filter implements NumberFilter {
- @Override
- public boolean is(int n) {
- if(n % 2 == 0)
- return true;
- return false;
- }
- }
- interface NumberFilter {
- boolean is(int n);
- }
遞歸及其IO應用
說完了上面的過濾器以後,我們已經可以實現過濾出指定目錄下的特定類型文件了。但是現在我又有一個需求:遍歷指定目錄下的內容(包含子目錄中的內容)。這該如何處理呢?再解決這個問題之前,我們下來簡單回顧一下算法課中所學的遞歸思想。
遞歸:就是函數自身調用自身。
什麼時候用遞歸呢?當一個功能被重複使用,而每一次使用該功能時的參數不確定,都由上次的功能元素結果來確定。簡單說:功能內部又用到該功能,但是傳遞的參數值不確定。(每次功能參與運算的未知內容不確定)。
遞歸的注意事項:
1:一定要定義遞歸的條件。
2:遞歸的次數不要過多。容易出現 StackOverflowError 棧內存溢出錯誤。
其實遞歸就是在棧內存中不斷的加載同一個函數。
遞歸示例:
- package ustc.lichunchun.recursion;
-
- public class RecursionDemo {
-
- public static void main(String[] args) {
-
- /*
- * 遞歸使用時,一定要定義條件。
- * 注意:遞歸次數過多,會出現棧內存溢出。
- */
- //show();
-
- int sum = getSum(3);
- System.out.println("sum = "+sum);//3+((3-1)+(3-1-1))
-
- int sum1 = getSum(999999);//java.lang.StackOverflowError
- }
-
- public static int getSum(int num){
- if(num == 1)
- return 1;
- return num + getSum(num - 1);
- }
-
- /*這也是遞歸,並且會溢出。
- public static void show(){
- method();
- }
- public static void method(){
- show();
- }
- */
- }
IO遞歸示例1:遍歷指定目錄下的內容,要求包含子目錄的內容。
- package ustc.lichunchun.file.test;
-
- import java.io.File;
-
- public class GetAllFilesTest {
-
- public static void main(String[] args) {
-
- /*
- * 遍歷指定目錄下的內容(包含子目錄中的內容)
- *
- * 遞歸:函數自身調用自身,不斷進棧。函數內部又使用到了該函數功能。
- * 什麼時候使用呢?
- * 功能被重複使用,但是每次該功能使用參與運算的數據不同時,可以考慮遞歸方式解決。
- *
- */
- File dir = new File("d:\\JavaSE_code");
- getAllFiles(dir);
-
- }
- public static void getAllFiles(File dir){
-
- System.out.println("dir: "+dir);
-
- //1.獲取該目錄的文件對象數組
- File[] files = dir.listFiles();
-
- //2.對數組進行遍歷
- if(files != null){//windows一些文件夾是不可以被java訪問到的。
- for(File file : files){
- if(file.isDirectory()){
- getAllFiles(file);
- }else{
- System.out.println("file: "+file);
- }
- }
- }
- }
- }
IO遞歸示例2:刪除一個帶內容的文件夾。
- package ustc.lichunchun.file.test;
-
- import java.io.File;
-
- public class DeleteDirTest {
-
- public static void main(String[] args) {
-
- /*
- * 基於遞歸,做一個練習:刪除一個帶內容的文件夾。必須從裏往外刪。
- */
-
- File dir = new File("d:\\JavaSE_code");
-
- //System.out.println(dir.delete());//false,有內容的文件夾不能直接刪
-
- deleteDir(dir);
- }
- public static void deleteDir(File dir){
-
- //1.列出當前目錄下的文件以及文件夾。
- File[] files = dir.listFiles();
-
- //2.對該數組進行遍歷。
- for(File file : files){
-
- //3/判斷是否有目錄。如果有,繼續使用該功能遍歷,遞歸!如果不是文件夾,直接刪除。
- if(file.isDirectory()){
- deleteDir(file);
- }else{
- System.out.println(file + ":" +file.delete());
- }
- }
- //4.刪除不含文件了的文件夾
- System.out.println(dir + ":" +dir.delete());
- }
- }
File類綜合練習
獲取一個想要的指定文件的集合。獲取JavaSE_code下(包含子目錄)的所有的.java的文件對象。並存儲到集合中。
思路:
1.既然包含子目錄,就需要遞歸。
2.在遞歸的過程中,需要過濾器。
3.凡是滿足條件的,都添加到集合中。
- package ustc.lichunchun.test;
-
- import java.io.File;
- import java.io.FileFilter;
- import java.util.ArrayList;
- import java.util.List;
-
- import ustc.lichunchun.filter.FilterBySuffix2;
-
- public class Test {
-
- public static void main(String[] args) {
-
- /*
- * 需求:獲取一個想要的指定文件的集合。獲取JavaSE_code下(包含子目錄)的所有的.java的文件對象。並存儲到集合中。
- *
- * 思路:
- * 1.既然包含子目錄,就需要遞歸。
- * 2.在遞歸的過程中,需要過濾器。
- * 3.凡是滿足條件的,都添加到集合中。
- */
- File dir = new File("e:\\JavaSE_code");
- List<File> list = fileList(dir, ".java");
- for(File file : list){
- System.out.println(file);
- }
- }
- /**
- * 定義一個獲取指定過濾器條件的文件的集合。
- */
- public static List<File> fileList(File dir, String suffix){
-
- //1.定義集合
- List<File> list = new ArrayList<File>();
-
- //2.定義過濾器。
- FileFilter filter = new FilterBySuffix2(suffix);
- /*匿名內部類也可以,不過不建議這麼做。
- FileFilter filter = new FileFilter(){
-
- @Override
- public boolean accept(File pathname) {
-
- return pathname.getName().endsWith(suffix);
- }
- };*/
- getFileList(dir, list, filter);
- return list;
- }
-
- /**
- * 對指定目錄進行遞歸。
- *
- * 多級目錄下,都要用到相同的集合和過濾器,那麼不要在遞歸方法中定義,而是不斷地進行傳遞。
- *
- * @param dir 需要遍歷的目錄
- * @param list 用於存儲符合條件的File對象
- * @param filter 接收指定的過濾器
- */
- public static void getFileList(File dir, List<File> list, FileFilter filter){
-
- //1.通過ListFiles方法,獲取dir當前下的所有的文件和文件夾對象。
- File[] files = dir.listFiles();
-
- //2.遍歷該數組。
- for(File file : files){
-
- //3.判斷是否是文件夾。如果是,遞歸。如果不是,那就是文件,就需要對文件進行過濾。
- if (file.isDirectory()){
- getFileList(file, list, filter);
- }else{
- //4.通過過濾器對文件進行過濾。
- if(filter.accept(file)){
- list.add(file);
- }
- }
- }
- }
- }
其中所用到的文件過濾器上面有,這裏就不單獨再列出來了。
字節流
File文件對象只能操作文件或者文件夾的屬性,例如文件或文件夾的創建、刪除、獲取文件屬性(大小、所在目錄等),我們最終建立文件的目的,是往文件裏面存數據,File對象是做不了這個的。這時,我們就要用到IO流。
InputStream:是表示字節輸入流的所有類的超類。
|--FileInputStream:從文件系統中的某個文件中獲得輸入字節。哪些文件可用取決於主機環境。
用於讀取諸如圖像數據之類的原始字節流。要讀取字符流,請考慮使用 FileReader。
|--FilterInputStream:包含其他一些輸入流,它將這些流用作其基本數據源,它可以直接傳輸數據或提供一些額外的功能。
|--BufferedInputStream:該類實現緩衝的輸入流。
|--DataInputStreamStream:操作基本數據類型值的流。
|--ObjectInputStream:對象的序列化。
|--PipedInputStream:管道輸出流是管道的發送端。
|--SequenceInputStream:序列流。
|--ByteArrayInputStream:操作內存數組。關閉動作無效。
|--System.in:鍵盤錄入。
OutputStream:此抽象類是表示輸出字節流的所有類的超類。
|--FileoutputStream:文件輸出流是用於將數據寫入File或FileDescriptor的輸出流。
注意處理IO異常。續寫和換行。
|--FilterOutputStream:此類是過濾輸出流的所有類的超類。
|--BufferedOutputStream:該類實現緩衝的輸出流。
|--PrintStream:字節打印流,保證數值的表現形式不變,實現自動刷新和換行。
|--DataOutputStream:操作基本數據類型值的流。
|--ObjectOutputStream:對象的反序列化。
|--PipedOutputStream:管道輸入流應該連接到管道輸出流。
|--ByteArrayOutputStream:操作內存數組。關閉動作無效。
|--System.out:控制檯打印到屏幕上。
FileOutputStream
將數據寫入到文件中,使用字節輸出流:FileOutputStream。
在演示字節輸出流之前,有以下三點需要注意:
1.輸出流所關聯的目的地,如果不存在,會自動創建。如果存在,則替換並覆蓋。(這與File對象,如果存在、創建失敗有所區別)
2.底層流資源使用完以後一定要記得釋放資源。也即IO一定要寫finally。
3.一定要在釋放資源前先判斷輸出流對象是否爲空。因爲try中創建輸出流對象失敗,則fos依然是null,但是空指針沒法調用close()函數釋放資源,這回導致拋出NullPointerException異常。
下面演示一下創建字節輸出流對象、調用輸出流的寫功能的代碼。
- package ustc.lichunchun.bytestream;
-
- import java.io.File;
- import java.io.FileOutputStream;
- import java.io.IOException;
-
- public class FileOutputStreamDemo {
-
- public static void main(String[] args) throws IOException {
-
- /*
- * 將數據寫入到文件中。
- * 使用字節輸出流。
- * FileOutputStream。
- */
- File dir = new File("tempfile");
- if(!dir.exists()){
- dir.mkdir();
- }
-
- //1.創建字節輸出流對象。用於操作文件,在對象初始化時,必須明確數據存儲的目的地。
- //輸出流所關聯的目的地,如果不存在,會自動創建。如果存在,則替換並覆蓋。(這與File對象,如果存在、創建失敗有所區別)
- FileOutputStream fos = new FileOutputStream("tempfile\\fos.txt");
-
- //2.調用輸出流的寫功能。
- //String str = "abcde";
- //byte[] buf = str.getBytes();
- fos.write("abcde".getBytes());
-
- //3.釋放資源。
- fos.close();
- }
- }
接着,我們演示一下帶處理IO異常的規範寫法:
- package ustc.lichunchun.bytestream;
-
- import java.io.File;
- import java.io.FileOutputStream;
- import java.io.IOException;
-
- public class IOExceptionDemo {
- /*
- * IO異常的處理方式:IO一定要寫finally!
- */
- public static void main(String[] args) {
-
- File dir = new File("tempfile");
- if(!dir.exists()){
- dir.mkdir();
- }
-
- FileOutputStream fos = null;//如果try中內容失敗,fos還是null,所以finally要先判斷。
-
- try {
- fos = new FileOutputStream("tempfile\\fos.txt");
-
- fos.write("abcdefg".getBytes());
-
- } catch (IOException e) {
- System.out.println(e.toString() + "---");
-
- } finally {
- if (fos != null) {// 一定要在釋放資源前先判斷!
- try {
- fos.close();
- } catch (IOException e) {
-
- throw new RuntimeException("關閉失敗" + e);//不要把異常拋給main函數、並讓主函數聲明、虛擬機處理!
- }
- }
- }
- }
- }
需求:實現續寫和換行操作。注意:行分隔符可以通過System.getProperty("line.separator");獲取。
- package ustc.lichunchun.bytestream;
-
- import java.io.File;
- import java.io.FileOutputStream;
- import java.io.IOException;
-
- public class NewlineDemo {
-
- private static final String LINE_SEPARATOR = System.getProperty("line.separator");
-
- public static void main(String[] args) {
-
- /*
- * 續寫和換行。
- *
- * Linux換行符是"\n"
- * Windows換行符是"\r\n"
- * System.getProperty("line.separator")
- */
- File dir = new File("tempfile");
- if(!dir.exists()){
- dir.mkdir();
- }
-
- FileOutputStream fos = null;
- try{
- fos = new FileOutputStream("tempfile\\fos.txt",true);//傳入true實現續寫。
- String str = LINE_SEPARATOR + "abc";
- fos.write(str.getBytes());
- }catch(IOException e){
- System.out.println(e.toString()+"--");
- }finally{
- if(fos != null){
- try{
- fos.close();
- }catch(IOException e){
- throw new RuntimeException(""+e);
- }
- }
- }
- }
- }
FileInputStream
那如何將已有文件的數據讀取出來呢?既然是讀,使用InputStream,而且是要操作文件,FileInpuStream。
爲了確保文件一定在讀之前是存在的,可以先將字符串路徑封裝成File對象。
下面演示創建文件字節讀取流對象、逐個讀取並打印文本文件中的字節。
- package ustc.lichunchun.bytestream;
-
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.IOException;
-
- public class FileInputStreamDemo {
-
- public static void main(String[] args) throws IOException {
-
- /*
- * 將已有文件的數據讀取出來。
- * 既然是讀,使用InputStream
- * 而且是要操作文件,FileInpuStream。
- */
-
- //爲了確保文件一定在讀之前是存在的,將字符串路徑封裝成File對象。
- File file = new File("tempfile\\fos.txt");
- if(!file.exists()){
- throw new RuntimeException("要讀取的文件不存在");
- }
-
- //創建文件字節讀取流對象時,必須明確與之關聯的數據源。
- FileInputStream fis = new FileInputStream(file);
-
- //調用讀取流對象的讀取方法。read();
- /*
- int by1 = fis.read();
- System.out.println("by1 = "+by1);//97
- int by2 = fis.read();
- System.out.println("by2 = "+by2);//98
- int by3 = fis.read();
- System.out.println("by3 = "+by3);//99
- int by4 = fis.read();
- System.out.println("by4 = "+by4);//-1
- int by5 = fis.read();
- System.out.println("by5 = "+by5);//-1
- */
- int by = 0;
- while((by = fis.read()) != -1){
- System.out.println(by);
- }
-
- //關閉資源。
- fis.close();
- }
- }
但是你肯定會覺得說,這樣一個一個字節的讀取文件中的字節,那得有多慢啊?
所以我建議使用下面的這第二種字節流讀取方式:創建一個緩衝區字節數組,大小自定義,
然後調用FileInputStream的read(byte[])方法,這樣一來,效率會提升不少。
建議這裏介紹的三種方法中,選擇此種方法。當然,最好是用BufferedInputStream,這我稍後便會闡述。
- package ustc.lichunchun.bytestream;
-
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.IOException;
-
- public class FileInputStreamDemo2 {
-
- private static final int DEFAULT_SIZE = 1024*1024*2;//2MB 緩衝區
-
- public static void main(String[] args) {
-
- //演示第二種讀取方式。read(byte[]); --> 第二種方式較好!
- File file = new File("tempfile\\fos.txt");
- if(!file.exists()){
- throw new RuntimeException("要讀取的文件不存在");
- }
-
- FileInputStream fis = null;
- try{
- fis = new FileInputStream(file);//流與文件關聯
-
- //創建一個緩衝區字節數組。
- byte[] buf = new byte[DEFAULT_SIZE];//緩衝區大小一般設置爲1024的整數倍。
-
- //調用read(byte[])方法
- /*
- int len = fis.read(buf);//len記錄的是往字節數組裏存儲的字節個數
- System.out.println(len + "..." + new String(buf,0,len));//2...ab
- int len1 = fis.read(buf);
- System.out.println(len1 + "..." + new String(buf,0,len1));//1...c
- int len2 = fis.read(buf);
- System.out.println(len2 + "..." + new String(buf));//-1...cb
- */
-
- int len = 0;
- while((len = fis.read(buf)) != -1){
- System.out.println(new String(buf,0,len));
- }
- }catch(IOException e){
- //一般將異常信息寫入到日誌文件中,進行記錄。
- }finally{
- if(fis != null){
- try{
- fis.close();
- }catch(IOException e){
- //一般可以throw new RuntimeException異常。或者將異常信息寫入到日誌文件中,進行記錄。
- }
- }
- }
- }
- }
當然了,其實還有一種比較蠢方式,就是把自定義緩衝區的大小設置爲和文件本身大小一樣大,示例如下。不過我非常不建議用這種方法。
- package ustc.lichunchun.bytestream;
-
- import java.io.FileInputStream;
- import java.io.IOException;
-
- public class FileInputStreamDemo3 {
-
- public static void main(String[] args) throws IOException {
-
- FileInputStream fis = new FileInputStream("tempfile\\fos.txt");
-
- System.out.println(fis.available());//可以獲取與之關聯文件的字節數。可以理解爲file.length();
-
- byte[] buf = new byte[fis.available()];//創建了一個和文件大小一樣的緩衝區,剛剛好。不建議用。
- fis.read(buf);
- String s = new String(buf);
- System.out.println(s);
-
- fis.close();
- }
- }
需求:複製一個文件。(文件類型不限,可以是文本數據,也可以是媒體數據)
思路:
讀取源數據,將數據寫到目的中。用到了流,操作設備上的數據。
讀,用到輸入流;寫,用到輸出流。而且操作的還是文件。需要用到字節流中操作文件的流對象。
- package ustc.lichunchun.copy;
-
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
-
- public class CopyTextTest {
-
- public static void main(String[] args) throws IOException {
-
- /*
- * 需求:複製一個文件。
- * 思路:
- * 讀取源數據,將數據寫到目的中。
- * 用到了流,操作設備上的數據。
- * 讀,用到輸入流;寫,用到輸出流。
- * 而且操作的還是文件。需要用到字節流中操作文件的流對象。
- */
- copyText();
- }
-
- public static void copyText() throws IOException {
-
- //1.創建一個輸入流和源數據相關聯。
- FileInputStream fis = new FileInputStream("複製文本文件圖解.bmp");
-
- //2.創建一個輸出流,並通過輸出流創建一個目的。
- FileOutputStream fos = new FileOutputStream("tempfile\\io_copy.bmp");
-
- //讀一個,寫一個。-->這種方式非常不好,效率太低,千萬別用此方法。
- int by = 0;
- while((by = fis.read()) != -1){
- fos.write(by);
- }
- fos.close();
- fis.close();
- }
- }
很顯然,上面這種逐字節讀取文件的方式效率太低,所以我們考慮使用在FileInputStream中所講的第二種方法,自定義緩衝區。
- package ustc.lichunchun.copy;
-
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
-
- public class CopyTextByBufTest {
-
- public static void main(String[] args) {
- copyTextByBuf();
- }
-
- public static void copyTextByBuf() {
- FileInputStream fis = null;
- FileOutputStream fos = null;
- try{
- fis = new FileInputStream("tempfile\\fos.txt");
- fos = new FileOutputStream("tempfile\\copy_fos.txt");
- //創建緩衝區
- byte[] buf = new byte[1024];//1KB,這就是緩衝區.
- //定義記錄字符個數的變量
- int len = 0;
- //循環讀寫
- while((len = fis.read(buf)) != -1){
- fos.write(buf, 0, len);
- }
- }catch(IOException e){
- //異常日誌。
- }finally{
- if(fos != null){
- try {
- fos.close();
- } catch (IOException e) {
- //異常日誌。
- }
- }
- if(fis != null){
- try {
- fis.close();
- } catch (IOException e) {
- //異常日誌。
- }
- }
- }
- }
- }
BufferedInputStream、BufferedOutputStream
前面在FileInputStream小節中,我已經介紹了利用自定義緩衝區實現高效的字節流讀取。
java也考慮到了這一點,在底層將緩衝區封裝成了對象,實際上就是在一個類中封裝了數組,
對流所操作的數據進行緩存。緩衝區的作用就是爲了提高操作數據的效率。
這樣可以避免頻繁的在硬盤上尋道操作。緩衝區創建時,必須有被緩衝的流對象與之相關聯。
原理圖解:
需求:利用緩衝區完成複製圖片的例子。
- package ustc.lichunchun.bytestream.buffer;
-
- import java.io.BufferedInputStream;
- import java.io.BufferedOutputStream;
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
-
- public class CopyPicByBufferDemo {
-
- public static void main(String[] args) throws IOException {
-
- /*
- * java將緩衝區封裝成了對象,實際上就是在一個類中封裝了一個數組,對流所操作的數據進行緩存。
- * 緩衝區的作用就是爲了提高操作數據的效率。這樣可以避免頻繁的在硬盤上尋道操作。
- * 緩衝區創建時,必須有被緩衝的流對象。
- *
- * 利用緩衝區完成複製圖片的例子。
- */
- copyPicByBuffer();
- }
-
- public static void copyPicByBuffer() throws IOException {
-
- //演示緩衝區。
- //1.創建具體的流對象。
- FileInputStream fis = new FileInputStream("tempfile\\1.jpg");
-
- //2.讓緩衝區與指定流相關聯。
- //對流中的數據進行緩衝。位於內存中的緩衝區的數據讀寫速度遠遠大於硬盤上的讀寫。
- BufferedInputStream bufis = new BufferedInputStream(fis);//緩衝區默認讀8MB字節
-
- FileOutputStream fos = new FileOutputStream("tempfile\\copy_1.jpg");
-
- BufferedOutputStream bufos = new BufferedOutputStream(fos);
-
- byte[] buf = new byte[1024];
-
- int len = 0;
-
- while((len = bufis.read(buf)) != -1){
- bufos.write(buf, 0, len);//使用緩衝區的寫入方法將數據先寫入到緩衝區中。
- bufos.flush();//將緩衝區的數據刷新到底層目的地中。(即使不寫,緩衝區滿了,java也會自動刷新 )
- }
-
- //關閉緩衝區,其實關閉的就是被緩衝的流對象。
- bufos.close();
- bufis.close();
- }
- }
問題探討:用字節流操作中文數據。
在計算機中,無論是文本,還是圖片、mp3、視頻等,所有數據最終都是以字節形式存在。
字節流:能操作以字節爲單位的文件的流對象。所以字節流能操作計算機上的一切數據。
- package ustc.lichunchun.readcn;
-
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
-
- public class ReadCNDemo {
-
- public static void main(String[] args) throws IOException {
-
- /*
- * 字節流操作中文數據。
- *
- * String --> String.getbyte() --> byte[] --> FileOutputStream.write(byte[])
- * 這裏我們使用的是String類的getBytes()方法按平臺默認字符集,編碼字符串爲字節數組,
- * 用以將中文字符串按字節流輸出到硬盤文件中,並以字節形式存儲。
- *
- * byte[] --> new String(byte[]) --> String --> System.out.println()
- * 爲了在控制檯能夠打印出中文字符,而不是逐個字節打印出int值,
- * 我們又使用String類的String(byte[] bytes,int offset,int length)構造函數按平臺默認字符集,解碼字節數組爲字符串。
- *
- * FileInputStream --> read() --> FileOutputStream --> write()
- * 如果只是將中文文本文件拷貝一份,或者複製其他類型的媒體文件諸如圖片音視頻等,我們則不必關心各種類型文件所使用的各種編碼方式,
- * 只需要通過輸入字節流逐個讀取硬盤上文件的字節,然後在通過輸出字節流輸出到相應目的地文件中即可,相應的特定軟件會自行解碼並打開的。
- *
- * BufferedXxxStream --> 爲了高效。
- *
- * 在計算機中,無論是文本,還是圖片、mp3、視頻等,所有數據最終都是以字節形式存在。
- * 字節流:能操作以字節爲單位的文件的流對象。
- * 所以字節流能操作計算機上的一切數據。
- * 而字符流只能操作以字符爲單位的文本數據。
- *
- * 注意:
- * windows簡體中文系統下,默認中文編碼表是GBK,其中編碼中文時,是兩個字節對應一箇中文字符。
- */
- //writeCNText();
- readCNText();
- }
-
- public static void writeCNText() throws IOException {
- FileOutputStream fos = new FileOutputStream("tempfile\\cn.txt");
- fos.write("你A好".getBytes());//按照默認編碼表GBK編碼,將String編碼爲byte[]。(System.getProperty("file.encoding")獲取)
- //getBytes():使用平臺的默認字符集將此 String編碼爲 byte 序列,並將結果存儲到一個新的 byte 數組中。
- fos.close();
- }
-
- public static void readCNText() throws IOException {
- FileInputStream fis = new FileInputStream("tempfile\\cn.txt");
-
- /*
- //這裏對中文文本文件逐個字節讀取,並打印到控制檯,肯定是不行的,編碼方式決定了不可以這樣。
- int by = fis.read();
- System.out.println(by);//196
- int by1 = fis.read();
- System.out.println(by1);//227
- int by2 = fis.read();
- System.out.println(by2);//65
- int by3 = fis.read();
- System.out.println(by3);//186
- int by4 = fis.read();
- System.out.println(by4);//195
- int by5 = fis.read();
- System.out.println(by5);//-1
- */
-
- //讀取中文,按照字節形式。但是一箇中文GBK碼錶中是兩個字節。
- //而字節流的read方法一次只讀一個字節。如何可以在控制檯獲取到一箇中文呢?
- //別讀一個就操作,多讀一些存起來,再操作。存到字節數組中,將字節數組轉成字符串就哦了。
- //因爲String類有一個構造函數可以通過使用平臺的默認字符集解碼指定的 byte數組,構造一個新的 String。
- byte[] buf = new byte[1024];
- int len = 0;
- len = fis.read(buf);
- String s = new String(buf,0,len);//將字節數組轉成字符串,而且是按照默認的編碼表(GBK)進行解碼。
- System.out.println(s);
- /*
- //字節是硬盤文件存儲基本單位,所以複製文件問題中我不需關心媒體或者中文字符到底幾個字節,
- //我就逐個字節往另一個相同類型文件裏存,至於不同編碼表,那是打開特定文件的軟件的問題,與字節流操作無關。
- FileOutputStream fos = new FileOutputStream("tempfile\\copy_cn.txt");
- int by = 0;
- while((by = fis.read())!=-1){
- fos.write(by);
- }
-
- //字節流引發的小問題:
- //用字節流讀取中文太費勁,如果中英文穿插,中文2字節,英文1字節,
- //如果每次只讀一箇中文字符new byte[2],然後我就轉成字符串打印,
- //"你"字符可以打印出來,但是A和"好"字符的前一半連在一起出來,就出錯了。
- //也許你會說,我可以加一個判斷中英文編碼的條件,但是這樣做太麻煩。
- //像上面我們做的那種打印中文文本的程序,需要讀取字節數據,再自己調用String構造函數解碼,也太麻煩,並且如何解碼也不知道。
- //所以我們就考慮用新的流:字符流。
- byte[] buf = new byte[2];
- int len = 0;
- while((len = fis.read(buf)) != -1){
- System.out.println(new String(buf,0,len));
- }
- */
- fis.close();
- }
- }
正如上面這段代碼最後一段的註釋中所說,對於純文本數據的文件,用字節流操作中英文數據時由於編碼問題,不同語言的字符對應的字節數不同,會顯得比較繁瑣。這時我們就該考慮字符流了。但是在這之前,我先把調研到的編碼相關背景知識總結分享一下。
編碼表
在計算機中,無論是文本,還是圖片、mp3、視頻等,所有數據最終都是以字節形式存在。字節流:能操作以字節爲單位的文件的流對象。所以字節流能操作計算機上的一切數據。文本數據、媒體數據等,什麼數據字節流都可以搞定。而字符流只能操作以字符爲單位的文本數據。
主要的編碼表有:ASCII、ISO8859-1、GB2312、GBK、GB18030、Unicode、UTF-8(標識頭信息)等。如果想要獲取到當前系統使用的默認編碼表,可以使用如下代碼獲取系統平臺默認字符集:System.getProperty("file.encoding")--> GBK。
ASCII:美國標準信息交換碼,0打頭,用一個字節的後7位表示,讀一個字節。
ISO8859-1:歐洲碼錶,用一個字節的8位表示,1打頭代表拉丁文,讀一個字節。
GB2312:中文編碼表,兩個字節都是1打頭,讀兩個字節
GBK:擴展版中文編碼表,第一個字節1打頭,讀兩個字節10101011 01010110 打頭都是1,讀到1打頭,就讀兩個字節,再去查GBK表。
Unicode:國際標準碼,重新編排碼錶,統一規定所有文字都是兩個字節,Java語言使用的就是unicode。弊端:能用一個字節表示的,都用兩個字節裝,浪費空間。
UTF-8:對Unicode優化,能用一個字節表示的就不用兩個字節,最多用三個字節來表示一個字符。讀完相應若干字節後,再去查表。01111010 單字節字符,0打頭,讀完單字節就去查表;11001010 10101010 兩個字節字符,110打頭第一個字節,10打頭第二個字節,讀完兩個字節再去查表;11100101 10101001 10010001三個字節字符,1110打頭第一個字節,10打頭第二、三個字節,讀完三個字節再去查表。
字符流爲什麼不能複製圖片?
字符流讀完字節數據後,並沒有直接往目的地裏面寫,而是去查表了。但是讀到的媒體性字節數據,都有自身千變萬化的編碼方式,在碼錶裏沒有對應內容。那麼它就會在表的未知字符區域隨便找一些數據寫到目的地中,這時,目的數據和源數據就不一致,就不能圖片編輯器解析爲圖片。所以,字符流不能用來操作媒體文件。
編解碼詳述
編碼:字符串 --> 字節數組。(默認都是按照windows系統本地的編碼GBK存儲的)
解碼:字節數組 --> 字符串。
代碼示例:
上述代碼示例中的test2()函數,我們有如下的處理過程:
這種原理在實際中,也有應用,比如tomcat服務器端,就是有這樣的轉換:
練習:按照字節數截取一個字符串。"abc你好"如果截取到半個中文,捨棄。比如截取4字節,abc,截取5個字節abc你。定義功能實現。(友情提示:GB2312編碼的一箇中文是兩個字節,而且兩個字節的最高位都是1,也就是說是一個負數。)
思路:
1.提示告訴我中文兩個字節都是負數。
2.判斷截取的最後一個字節是否是負數。如果不是,直接截取;如果是,就往回判斷,前一個是否是負數。並記錄住負數的個數。如果連續的負數有奇數個,捨棄最後一個字節;如果連續的負數是偶數個,不捨棄。
- package ustc.lichunchun.encode;
-
- import java.io.IOException;
-
- public class Test {
-
- public static void main(String[] args) throws IOException {
-
- /*
- * 按照字節數截取一個字符串。"abc你好"如果截取到半個中文,捨棄。比如截取4字節,abc,截取5個字節abc你。
- * 定義功能實現。
- * 友情提示:GB2312編碼的一箇中文是兩個字節,而且兩個字節的最高位都是1,也就是說是一個負數。
- *
- * 思路:
- * 1.提示告訴我中文兩個字節都是負數。
- * 2.判斷截取的最後一個字節是否是負數。
- * 如果不是,直接截取。
- * 如果是,就往回判斷,前一個是否是負數。並記錄住負數的個數。如果連續的負數有奇數個,捨棄最後一個字節。
- * 如果連續的負數是偶數個,不捨棄。
- *
- * 拓展1:GBK擴容後,中文既有負數,又有正數。它只保證第一個字節是負數,第二個字節不保證。此程序依然適用。
- * 拓展2:如果將字符串編碼成utf-8格式,咋辦?這時,一箇中文3個字節。
- */
- //字符串轉成字節數組。
- String str = "a琲bc你好d琲e";
- byte[] buf = str.getBytes("utf-8");
-
- for(int i = 1; i <= buf.length; i++){
- String temp = cutStringByCount2(str,i);
- System.out.println("截取"+i+"個字節是:"+temp);
- }
- }
-
- public static String cutStringByCount1(String str, int len) throws IOException {
-
- //1.將字符串轉成字符數組。因爲要判斷截取的字節是否是負數,所以要先有字節。
- byte[] bytes = str.getBytes("gbk");
- //2.定義計數器,記錄住負數的個數。
- int count = 0;
- //3.對字節數組進行遍歷。應該從截取長度的最後一個字節開始判斷,並往回判斷。
- for(int x = len-1; x >= 0; x--){
- //4.在遍歷過程中,只要滿足負數就進行計數。只要不是負數,直接結束遍歷。
- if(bytes[x]<0){
- count++;
- }else{
- break;
- }
- }
- //5.對遍歷後,計數器的值進行判斷,奇數,就捨棄最後字節並將字節數組轉成字符串。
- //偶數,不捨棄,將字節數組轉成字符串。
- if(count%2 == 0)
- return new String(bytes, 0, len);
- else
- return new String(bytes, 0, len-1);
- }
-
- public static String cutStringByCount2(String str, int len) throws IOException {
- byte[] bytes = str.getBytes("utf-8");
- int count = 0;
- for(int x = len-1; x >= 0; x--){
- if(bytes[x] < 0){
- count++;
- }else{
- break;
- }
- }
- if(count%3 ==0)
- return new String(bytes, 0, len, "utf-8");
- else if(count%3 == 1)
- return new String(bytes, 0, len-1, "utf-8");
- else
- return new String(bytes, 0, len-2, "utf-8");
- }
- }
字符流
字符流只能用來操作字符數據--文本。但是由於字符流封裝了編碼部分,所以操作字符比較方便。字符流=字節流+編碼表。
Reader:用於讀取字符流的抽象類。子類必須實現的方法只有 read(char[],int,int)和 close()。
|--BufferedReader:從字符輸入流中讀取文本,緩衝各個字符,從而實現字符、數組和行的高效讀取。
可以指定緩衝區的大小,或者可使用默認的大小。大多數情況下,默認值就足夠大了。
|--LineNumberReader:跟蹤行號的緩衝字符輸入流。此類定義了方法 setLineNumber(int)和 getLineNumber(),
它們可分別用於設置和獲取當前行號。
|--InputStreamReader:是字節流通向字符流的橋樑:它使用指定的 charset讀取字節並將其解碼爲字符。
它使用的字符集可以由名稱指定或顯式給定,或者可以接受平臺默認的字符集。
|--FileReader:用來讀取字符文件的便捷類。此類的構造方法假定默認字符編碼和默認字節緩衝區大小都是適當的。
要自己指定這些值,可以先在 FileInputStream上構造一個InputStreamReader。
|--CharArrayReader:沒有調用系統流,只是操作內存中的數組。
|--StringReader:沒有調用系統流,只是操作內存中的數組。
Writer:寫入字符流的抽象類。子類必須實現的方法僅有 write(char[],int,int)、flush()和 close()。
|--BufferedWriter:將文本寫入字符輸出流,緩衝各個字符,從而提供單個字符、數組和字符串的高效寫入。
|--OutputStreamWriter:是字符流通向字節流的橋樑:可使用指定的 charset將要寫入流中的字符編碼成字節。
它使用的字符集可以由名稱指定或顯式給定,否則將接受平臺默認的字符集。
|--FileWriter:用來寫入字符文件的便捷類。此類的構造方法假定默認字符編碼和默認字節緩衝區大小都是可接受的。
要自己指定這些值,可以先在 FileOutputStream上構造一個OutputStreamWriter。
|--PrintWriter:字符打印流,保證數值的表現形式不變,可以對字節流、字符流自動刷新並換行。
|--CharArrayWriter:沒有調用系統流,只是操作內存中的數組。
|--StringWriter:沒有調用系統流,只是操作內存中的數組。
注意:只要是輸出字符流,都有查表後的緩衝區。所以它的write()方法後面一定要跟隨flush()刷新操作!(即使沒寫但運行通過,那也是因爲close()封裝了flush方法)
轉換流
通過編碼表的演示,讀者應該很容易發現針對純文本文件,用字符流的方式處理更爲方便。那就很自然的考慮到,我如何才能將字節流轉換成字符流呢?下面我就來介紹一下字節和字符,二者之間轉換的橋樑——轉換流。
轉換流:
字節-->字符:解碼(看不懂的-->看得懂的):InputStreamReader-->字節通向字符的橋樑,將讀到的若干字節進行解碼。
字符-->字節:編碼(看得懂的-->看不懂的):OutputStreamWriter-->字符通向字節的橋樑,將字符編碼成若干字節。
記住:只要是字符輸出流,都有查表後的緩衝區。所以它的write()方法後面一定要跟隨flush()刷新操作!
緩衝的原因:
每次調用 write方法都會導致在給定字符(或字符集)上調用編碼轉換器,在寫入底層輸出流之前,得到的這些字節將在緩衝區中累積。中文最終變成字節才能出去,中文變成什麼字節呢?識別中文的碼錶不止一個,沒有指定,會按默認碼錶GBK。這意味着,會把中文先臨時存儲起來,去查相應的碼錶,再把查碼錶得到的字節存起來緩衝一下,再刷新寫出去。之前學的字節流,是因爲不用進行查碼錶、緩衝轉換,字節該什麼樣就什麼樣,直接讀寫即可。現在需要拿着字符數據去查碼錶,把查到的字節數據存起來,再寫出去。所以,有了緩衝區,就得做刷新。
轉換流代碼示例:
- package ustc.lichunchun.charstream;
-
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
- import java.io.InputStreamReader;
- import java.io.OutputStreamWriter;
-
- public class TransStreamDemo {
-
- public static void main(String[] args) throws IOException {
-
- /*
- * 需求:通過字符流讀取中文數據。
- *
- * 字符流 = 字節流 + 編碼表。
- *
- * Java內置的碼錶是Unicode編碼。見encode包有關編碼解碼的代碼示例。
- *
- * 轉換流:
- * 字節-->字符:解碼(看不懂的-->看得懂的):InputStreamReader
- * 字符-->字節:編碼(看得懂的-->看不懂的):OutputStreamWriter
- */
- //字節通向字符的橋樑,將讀到的若干字節進行解碼。
- //readCnText();
-
- //字符通向字節的橋樑,將字符編碼成若干字節。
- writeCNText();
- }
-
- public static void readCnText() throws IOException {
-
- //1.操作字節流的字符流對象,必須先有字節流。
- FileInputStream fis = new FileInputStream("tempfile\\cn.txt");
-
- //2.建立字節向字符的橋樑。(解碼)
- InputStreamReader isr = new InputStreamReader(fis);
-
- int ch = isr.read();//注意這裏read()讀取的是單個字符,所以要強轉(char)int,纔可以打印在控制檯
- System.out.println((char)ch);//(char)20320 = '你'
- int ch1 = isr.read();
- System.out.println((char)ch1);//(char)65 = 'A'
- int ch2 = isr.read();
- System.out.println((char)ch2);//(char)22909 = '好'
- int ch3 = isr.read();
- System.out.println(ch3);//(char)-1 = '?' , 虛擬機返回-1就代表到達文件末尾 了。
- isr.close();
- }
-
- public static void writeCNText() throws IOException {
-
- //1.創建字節流對象。
- FileOutputStream fos = new FileOutputStream("tempfile\\GBK.txt");
-
- //2.創建字符通向字節的橋樑。
- OutputStreamWriter osw = new OutputStreamWriter(fos);
-
- //3.使用osw的write方法直接寫中文字符串。因爲需要拿着字符數據去查表,所以寫入字節數據前,都會存儲到緩衝區中。
- osw.write("你A好");
-
- //4.需要刷新該字符流的緩衝區。將查表得到的字節數據寫到fos流中,然後通過Windows底層資源寫入到GBK.txt中。
- //osw.flush();
-
- //5.關閉此流前,會先刷新一下。
- osw.close();
-
- /*
- close()底層實現:
- void close(){
- flush();
- close();
- }
- */
- }
- }
flush和close區別:
flush()刷新完,流可以繼續使用;
close()刷新完,流直接關閉,流結束了,無法再用。再用,會報Stream closed異常。
轉換流的好處:可以指定編碼表。
什麼時候使用轉換流呢?
1.操作文本數據。
2.需要指定編碼表。
下面我們使用不同的編碼表來演示轉換流:
- package ustc.lichunchun.charstream;
-
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
- import java.io.InputStreamReader;
- import java.io.OutputStreamWriter;
-
- public class TransStreamDemo2 {
-
- public static void main(String[] args) throws IOException {
-
- /*
- * 使用不同的編碼表演示轉換流。
- *
- * 字節 -->字符: InputStreamReader
- * 字符 -->字節:OutputStreamWriter
- *
- */
- //writeText();
- readText();
- }
-
- public static void writeText() throws IOException {
-
- FileOutputStream fos = new FileOutputStream("tempfile\\u8.txt");
- OutputStreamWriter osw = new OutputStreamWriter(fos);//使用默認的編碼表GBK。4字節。
- osw = new OutputStreamWriter(fos, "UTF-8");//6字節。
- osw.write("你好");//記住會有查表、緩衝區的動作
- osw.close();
- }
-
- public static void readText() throws IOException {
-
- FileInputStream fis = new FileInputStream("tempfile\\u8.txt");
- InputStreamReader isr = new InputStreamReader(fis);//默認碼錶GBK。
- isr = new InputStreamReader(fis, "UTF-8");
- int ch1 = isr.read();
- System.out.println("ch1 = "+(char)ch1);//浣--> 你
- int ch2 = isr.read();
- System.out.println("ch2 = "+(char)ch2);//犲--> 好
- int ch3 = isr.read();
- System.out.println("ch3 = "+ch3);//ソ--> -1
- isr.close();
- }
- }
FileWriter、FileReader
二者是轉換流的子類,專門用於操作文本文件的字符流對象。
FileWriter --> 用來寫入字符文件的便捷類。
FileReader--> 用來讀取字符文件的便捷類。
侷限性:
1.只能操作純文本文件。
2.只能使用默認編碼。
由字符流的體系結構圖我們可以清楚地看到,FileReader類繼承自InputStreamReader轉換流類。理由是:想要操作文本文件,必須要進行編碼轉換,而編碼轉換動作轉換流都完成了,所以操作文件的流對象只要繼承自轉換流就可以讀取一個字符了。但是子類有一個侷限性,就是子類中使用的編碼是固定的,是本機默認的編碼表,對於簡體中文版的系統默認碼錶是GBK。
- FileReader fr = new FileReader("a.txt");
- InputStreamReader isr = new InputStreamReader(new FileInputStream("a.txt"),"gbk");
以上兩句代碼功能一致,如果僅僅使用平臺默認碼錶,就使用FileReader fr = new FileReader("a.txt"); //因爲簡化。
如果需要制定碼錶,必須用轉換流。
轉換流 = 字節流+編碼表。
轉換流的子類File = 字節流 + 默認編碼表。
凡是操作設備上的文本數據,涉及編碼轉換,必須使用轉換流。
- package ustc.lichunchun.charstream;
-
- import java.io.FileReader;
- import java.io.FileWriter;
- import java.io.IOException;
-
- public class SubTransStreamDemo {
-
- public static void main(String[] args) throws IOException {
-
- /*
- * 轉換流的子類,專門用於操作文本文件的字符流對象。
- * FileWriter --> 用來寫入字符文件的便捷類。
- * FileReader --> 用來讀取字符文件的便捷類。
- */
- //writeText();
- readText();
-
- }
-
- public static void writeText() throws IOException {
-
- //1.創建一個用於操作文件的字符輸出流對象。
- FileWriter fw = new FileWriter("tempfile\\fw.txt");//內部使用了默認的碼錶。而且只能操作文件。
- //等效於:
- //FileOutputStream fos = new FileOutputStream("tempfile\\fw.txt");
- //OutputStreamWriter osw = new OutputStreamWriter(fos);
-
- fw.write("你好");//記住會查表、緩衝區.
-
- fw.close();
- }
-
- public static void readText() throws IOException {
- FileReader fr = new FileReader("tempfile\\fw.txt");
- //等效於:
- //FileInputStream fis = new FileInputStream("tempfile\\fw.txt");
- //InputStreamReader isr = new InputStreamReader(fis);
-
- int ch = 0;
- while((ch = fr.read()) != -1){
- System.out.println((char)ch);//發現一次讀取一個字符比較慢,接下來我們考慮使用緩衝區讀取字符數組。
- }
- fr.close();
- }
- }
練習:使用自定義字符流緩衝區,複製文本文件示例。
注意:
1.字節流使用的緩衝區是字節數組,字符流使用的緩衝區是字符數組。
2.僅爲複製文件,建議使用通用類型的字節流;如果是純文本文件,並且有多餘操作,建議使用字符流。
- package ustc.lichunchun.charstream;
-
- import java.io.FileReader;
- import java.io.FileWriter;
- import java.io.IOException;
-
- public class SubTransStreamDemo2 {
-
- public static void main(String[] args) throws IOException {
-
- /*
- * 使用字符流緩衝區,複製文本文件示例。
- *
- * 字節流使用的緩衝區是字節數組,
- * 字符流使用的緩衝區是字符數組。
- *
- * 如果僅僅是爲了複製,建議使用字節流。
- * 但是一旦涉及到中文字符的操作,用字符流較好,
- * 比如在while循環體內對讀到的文本字符數據的"你"字替換爲"我"字,字節流就辦不到了。
- *
- */
- copyText();
- }
-
- public static void copyText() throws IOException {
-
- //1.明確數據源,定義字符讀取流和數據源關聯。
- FileReader fr = new FileReader("IO流_2.txt");
-
- //2.明確數據目的,定義字符輸出流,創建存儲數據的目的。
- FileWriter fw = new FileWriter("tempfile\\copy_2.txt");
-
- //3.創建自定義緩衝區。
- char[] buf = new char[1024];//char佔兩個字節,所以聲明瞭一個2KB的數組。
-
- int len = 0;
- while((len = fr.read(buf)) != -1){
- fw.write(buf, 0, len);
- }
- fw.close();
- fr.close();
- }
- }
上述讀取字符的操作,同樣可以使用java封裝好的高效緩衝區字符流對象。下面我們就來演示字符流的緩衝區。
BufferedWriter、BufferedReader
BufferedXxx緩衝字符流存在的好處是:爲了高效,並且有readLine()、newLine()方法。首先演示一下演示字符流的緩衝區讀寫操作。
- package ustc.lichunchun.charstream.buffer;
-
- import java.io.BufferedReader;
- import java.io.BufferedWriter;
- import java.io.FileReader;
- import java.io.FileWriter;
- import java.io.IOException;
-
- public class CharStreamBufferDemo {
-
- public static void main(String[] args) throws IOException {
-
- /*
- * 演示字符流的緩衝區。
- * BufferedReader
- * BufferedWriter
- *
- * BufferedXxx緩衝字符流存在的好處是:
- * 爲了高效,並且有readLine()、newLine()方法。
- */
- //writeTextByBuffered();