IO流(二)

io思維導向圖

IO里邊的東西確實特別多,所以再另開一篇,把IO里面剩下的東西記錄一下,這篇里面主要的內容有打印流、對象的序列化和反序列化,隨機讀取流等等,還是相當實用的。

一、打印流

打印流在IO第一篇文章里面是有接觸過的,當時用的轉換流打印從鍵盤接受到的數據的時候用到了System.out這么個流對象,其實它返回的就是打印流對象PrintStream,當時實用接口直接調用了,也就是多態,并沒有關注其到底是哪個子類。說道這里,也就清楚了,最常用的System.out.println()中println()方法就是打印流里面的方法。

打印流分兩種:

  1. 字節輸出流:PrintStream
  2. 字符輸出流:PrintWriter,注意PrintWriter實現了PrintStream中所有的打印方法

兩者的區別:

  • 共性:都是負責打印數據,不會拋出異常,不操作數據源,有很多種打印方法。
  • 區別:還是之前的說話,一個是字節流,一個是字符流,所以兩個流的構造方法不一樣,字符流(PrintWriter)流構造方法接收File對象、字節輸出流、字符串形式的文件名、字符輸出流四種類型的參數;而字節流(PrintStream)接收File文件對象,字符形式的文件名和字節輸出流這三種類型的參數。

作用:

打印流到底有啥作用?其實說白了就是將數據打印到指定的目標上,目標可以是文件,控制臺,別的電腦等等,所以打印流和之前的比如說FileWriter這些輸出流的功能是有重疊的,更甚至于打印流還可以干轉換流的活,代碼如下:

PrintWriter pw = new PrintWriter(System.out,true); //true為開啟自動刷新

所以以后要是需要輸出數據,優先選擇打印流其實是比較方便和實用的。實際開發中,web開發,服務器其實都用的是打印流,將數據打印到客戶瀏覽器端。

打印流還可以開啟自動刷新,具體做法就是在構造方法上加上true開啟即可。

注意:這里是有限制的。要開啟自動刷新,必輸是流對象,且在調用printlnprintfformat這三個方法時才會啟用。

demo:

package io;
/*
 * 打印流demo
 */
import java.io.*;

public class PrintWriterDemo {
    public static void main(String[] args) throws Exception{
        method();
    }
    
    /*
     * 使用打印流,將數據打印到各個目的地
     */
    public static void method() throws Exception{
        
        //常規打印方式,和以前的字符輸出流區別不大
        PrintWriter pw = new PrintWriter("e:\\PrintDemo.txt");
        pw.print("this is a test!");
        pw.flush();
        pw.close();
        
        
        //將字符打印到字符輸出流中
        PrintWriter pw1 = new PrintWriter(new FileWriter("e:\\PrintDemo1.txt"),true); //開啟自動刷新
        pw1.println("this is a test!");//換行打印效果,相當于緩沖流的write+newLine
        pw1.print("this is a test!");
        pw1.close();
        
        //實現轉換流的效果,只有字節流才有轉換
        //開啟自動刷新,重新編碼
        PrintStream pw2 = new PrintStream(new FileOutputStream("e:\\PrintDemo2.txt"),true,"UTF-8");
        pw2.print("測試");//換行打印效果,相當于緩沖流的write+newLine
        pw2.flush();
        pw2.close();
        
        //從控制臺到文件
        BufferedReader bfr = new BufferedReader(new InputStreamReader(System.in));//控制臺接收的緩沖流
        PrintWriter pw3 = new PrintWriter(new FileOutputStream("e:\\PrintDemo3.txt"),true);
        String s = null ;
        while((s = bfr.readLine()) != null){
            if(s.endsWith("over"))
                break;
            pw3.println(s);
        }
        pw3.close();
    }
}

二、對象序列化

將對象的數據寫到文件中就是對象序列化,相反把文件中的數據讀取并轉換為對象就是反序列化。涉及到的兩個流是:

  • ObjectOutputStream,這個是寫對象的流
  • ObjectInputStream,這個是讀對象的流

1. 序列化

寫對象數據的流 是ObjectOutputStream類,構造方法和寫的方法:

ObjectOutputStream(OutputStream out);//一般傳遞子類對象 FileOutputStream
//寫入方法支持基本數據類型和對象
writeObject(Object o); //寫入對象
writeInt(int i);//寫入基本類型,都是write+基本類型名,這個基本沒用我覺得

通過將對象和數據寫入文件中 ,可是實現對數據的永久性保存。通過讀取和重構就可以恢復數據,但是讀取數據時類型和順序應該和寫入的順序相同。

注意:序列化的類必須繼承Serializable接口,此接口中沒有方法,作用就是做一個序列化的標記。

2. 反序列化

其實io學到這里,基本的規律基本上已經出來了,有輸出就對應著有輸入,有寫就有讀,方法都是對應起來的。

ObjectInputStream(new FileInputStream("e:\\ObjectStream.txt"));//構造方法的一個例子
Object readObject();// 讀的方法

讀這里需要注意一點,返回的是Object對象,也就是說類型被提升,多態調用時沒有什么問題,但是需要調用子類特有方法時,需要做類型轉化,加之如果文件里寫了很多種不同的類對象,在順序沒有保證的情況下,最后在做類型轉換之前進行判斷。

序列化和反序列化的demo:

package io;
/*
 * 對象序列化demo
 */
import java.io.*;
import SetDemo.Person;//person類的代碼在set那篇文章里有寫

public class ObjectStreamDemo {
    public static void main(String[] args) throws Exception{
        method();
        method_1();
    }
    
    /*
     * 序列化
     */
    public static void method() throws Exception{
        //其實輸出的是二進制文件
      //Person類已實現Serializable接口,否則會報異常
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("e:\\ObjectStream.txt"));
        
        Person p1 = new Person("楊比軒", 20);//新建兩個對象用于序列化
        Person p2 = new Person("比軒", 21);
        
        oos.writeObject(p1);
        oos.writeObject(p2);
        
        oos.flush();
        oos.close();
    }
    
    /*
     * 反序列化
     */
    public static void method_1() throws Exception{
        //讀比較簡單,保證對象class文件可以加載,并且按順序走就可以了
        //如果沒有person類, 會報出 找不到此類  的異常
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("e:\\ObjectStream.txt"));
        
        System.out.println(ois.readObject());
        System.out.println(ois.readObject());
        
        ois.close();
    }
}

3. 阻止變量的序列化

有兩種方法:

  • 一種是將對象內的成員變量變為靜態的,此時默認不會將其序列化,反之讀取的時候就獲取不到此數據。
  • 另一種是加關鍵字transient即可,這個關鍵字也就這一個作用:阻止變量序列化。

4.Serializable接口的意義

上面說了,沒有實現Serializable接口的對象時沒有辦法序列化的,但是Serializable接口中有沒有方法,那這個到底是干嘛用的呢?

存在意義,稱為標記型接口,就是在你的類上標記一下,JVM看到這個標記,可以進行序列化。就像現實生活中給文件蓋個章一樣,有戳JVM就認,沒戳就不認,這么個道理。

5. 序列化ID

首先先解釋一下,什么是序列號。其實java文件在編譯生成class文件的時候,會計算出一個序列號保存在二進制文件當中。所以我們當我們反序列化時,如果此時被序列化的對象的class文件在被序列化后改動過,其序列號就會發生變化,一做反序列化,就會報出序列號不一致的異常(InvalidClassException)。

那如果實現就算了改了對象的源碼,也重新編譯了class文件,此時反序列化也不會報錯呢,這就需要顯式的申明一個序列號的long型變量:

static final long serialVersionUID = XXXX L;//變量的修飾符和變量名都是java固定的,只能修改具體的數值。

三、關于接口和Object

接口是不繼承任何類的,但是下面這個代碼卻可以編譯通過:

interface A{    
}

class B implements A{
}

public class demo{
  public static void main(String[] args){
    A temp = new B();
    A.hashCode();
  }
}

我們都知道,編譯看左邊,所以編譯的時候其實和子類B是沒有什么關系的,此時接口A既沒有繼承Object類,也沒有任何方法,但是卻可以調用Object類下的任一方法,這個是不是有點想不通。

原因呢Sun公司其實有說過:就是在借樓中,隱含定義了Object中的所有方法,這些方法都是抽象的。

四、Properties和IO的結合使用

Properties和IO的結合使用可以用來加載配置文件等等,使用起來還是比較方便的。而且,之前也說話,Properties是一個線程安全的類,存儲鍵值對的集合,不能存儲null和重復對象,特有方法為setProperty和getProperty。

load方法的形式為public synchronized void(Reader/InputStream),傳遞字節流對象或者字符流對象,沒有返回值。只需要將io對象傳遞進去即可,全自動,比較方便。

同時,如果需要將內存中的Properties對象中的數據寫回文件,可以使用public void store(Writer writer, String comments)方法, comments為注釋。

demo:

package map;
/*
 * IO和Properties的配合使用
 */
import java.io.*;
import java.util.*;
public class PropertiesDemo1 {
    public static void main(String[] args) throws Exception {
      method();
    }
    
    /*
     * 從文本中讀取鍵值對存儲到Properties中
     * 修改使用setProperty
     * 
     * 保存使用store
     * 注意先load后再新建文件輸出流,否則會清空掉文件內的數據
     * 注釋不能寫中文,因為寫進去的是utf的十六進制編碼,不是文本
     * 
     * list方法,傳遞打印流,將鍵值對打印到配置文件中
     */
    public static void method() throws Exception{
        Properties p = new Properties();
        
        FileReader fr = new FileReader("f:\\config.ini");
        
        p.load(fr);
        
        //p.setProperty("id", "789");
        
        FileWriter fw = new FileWriter("f:\\config.ini");
        p.store(fw,null);
        fr.close();
        fw.close();
        System.out.println(p);
        
        p.setProperty("address", "home");
        p.setProperty("address1", "school");
        p.setProperty("address2", "company");
        p.setProperty("id", "123");
        
        p.list(new PrintStream(new File("f:\\config.ini")));
        
    }
    
    //測試結論:使用完load方法后,Reader指向了文件末尾,但是沒有被關閉
    public static void test() throws Exception{
        
        Properties p = new Properties();
        
        FileReader fr = new FileReader("f:\\config.ini");
        
        p.load(fr);
        
        char[] ch = new char[100];
        
        fr.read(ch);
        System.out.println(ch);
    }
}

附上一個練習,自己實現load方法:

package map;
/*
 * 自己實現load方法
 * 方法申明和原方法基本一致
 * 有兩個重載形式字節流和字符流
 * 因為是自定義方法,所以沒有this對象,直接把Properties當做參數傳遞機那里
 */

import java.io.*;
import java.util.*;

public class LoadDemo {
    public static void main(String[] args) throws IOException {

        Properties p = new Properties();
        FileReader fr = new FileReader("f:\\config.ini");
        
        load(fr, p);
        System.out.println(p);
    }

    /*
     * 字符流load
     * 目標,讀取配置文件里的數據,轉換為Properties的元素
     * 按行讀取,獲取行數據,然后按等號切割,返回String[],前K后V
     * 
     * 疑問:
     * 按照io的規范,其實BufferedReader在這里是要close掉的
     * 但是此處要是關閉了BufferedReader,傳遞進來的Reader也會被關掉
     * 也就是說,用戶傳遞進來的參數一進被關閉掉了,這回使的用戶后續使用出現問題
     * 但是,思考了下,也不知道怎么解決這個問題,希望有大神可以回答
     * java的load是沒有關閉傳遞進去的對象的,但是看不大懂內部的具體實現
     */
    public static void load(Reader reader, Properties p) throws IOException {

        String str = null;
        BufferedReader br = new BufferedReader(reader); //使用Buffered可以使用readLine
        while ((str = br.readLine()) != null) {
            //先去掉空格
            str = str.trim();
            // 忽略掉注釋的內容...
            if (!(str.startsWith("#") || str.startsWith("-"))) {
                
                String[] kv = str.split("=");

                // 去掉不符合規定的內容
                if (kv.length == 2) {
                    p.setProperty(kv[0], kv[1]);
                }
            }
        }
    }
    
    //重載字節流的形式
    public static void load(InputStream is, Properties p) throws IOException{
        
        //使用轉換流,將字節流數據處理為字符流,之后和字符流處理方式相同
        InputStreamReader br = new InputStreamReader(new FileInputStream("F:\\config.ini"));
        load(br,p);//其余都是一樣的,調參數為reader類型的方法
    }
}

這個練習也引出了一個疑問,在代碼中有提到:

按照io的規范,其實BufferedReader在這里是要close掉的。但是此處要是關閉了BufferedReader,傳遞進來的Reader也會被關掉。也就是說,用戶傳遞進來的參數已經被關閉掉了,這會使得用戶后續使用出現問題。思考了下,也不知道怎么解決這個問題,希望有大神可以回答。試了一下,java的load是沒有關閉傳遞進去的對象的,但是看不大懂內部的具體實現

五、IO讀寫基本類型數據

可以使用IO流DataOutputStream,DataInputStream來讀寫基本數據。

  • DataOutputStream繼承OutputStream,字節輸出流。構造方法和輸出方法為:
DataOutputStream(OutputStream o);//實際操作都傳遞FileOutputStream
writeInt(int) //方法名為:write + 基本數據類型名稱
  • DataInputStream繼承InputStream,字節輸入流。構造方法和讀取方法為:
DataInputStream(InputStream i);//實際操作都傳遞FileInputStream
int readInt();//方法名為:read + 基本數據類型名

DataInputStream讀到末尾時會拋出EOFException(end of file exception),所以需要循環讀取時一般都利用異常賴退出循環。

直接提供demo:

package io;
/*
 * 基本數據類型的讀寫demo
 * DataOutputStream 基本數據類型的輸出
 * 只有一種構造方法
 * DataOutputStream(OutputStream out) 
 * 
 * DataInputStream 基本數據類型的讀取,其余一樣    
 * 
 */
import java.io.*;
public class DataStreamDemo {
    public static void main(String[] args) throws Exception{
        
//      method();
//      method2();
        wirteAndReadUTF();
    }
    
    /*
     * 基本數據類型的輸出
     * 寫入的是實際的數據內容,而且是連續性的寫入
     * 并不會記錄寫的數據類型及其數量信息
     * 所以,讀取的時候,不知道寫的具體類型是什么,
     * 讀到文件末尾了,就報出EOF異常
     */
    public static void method() throws Exception{
        DataOutputStream dos =  new DataOutputStream(new FileOutputStream("F:\\data.txt"));
        
        dos.writeInt(654);
        dos.writeByte(45);
        dos.writeByte(45);
        dos.writeByte(45);
        dos.writeByte(45);
        dos.writeDouble(68516951.684162);
        dos.close();
    }
    
    /*
     * 基本數據類型的讀取
     * while永真循環讀取,抓住異常時break
     * 
     */
    public static void method2() throws Exception{
        DataInputStream dis =  new DataInputStream(new FileInputStream("F:\\data.txt"));
        double a = 0;
        while(true){
            try{
                a = dis.readDouble();
                System.out.println(a);
            }catch(EOFException e){
                break;
            }
        }
        dis.close();
    }
    
    /*
     * 使用writeUTF的形式進行寫入和讀取
     */
    public static void wirteAndReadUTF() throws Exception{
        
        DataOutputStream dos = new DataOutputStream(new FileOutputStream("F:\\utf.txt"));
        dos.writeUTF("寫什么都行0\r\n");
        
        dos.writeUTF("寫什么都行1");
        dos.writeUTF("寫什么都行2");
        dos.writeUTF("寫什么都行3");
        dos.close();
        
        //先嘗試用普通的方式去讀取
        //結論,內容可以正確讀取,但是前面有不能解碼的數據
        BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("f:\\utf.txt"),"utf-8"));
        System.out.println("UTF編碼的轉換流進去讀取:" + br.readLine());
        br.close();
        
        //嘗試使用DataInputStram 進行讀取,可以正確的讀取,沒有任何的異常出現
        DataInputStream dis = new DataInputStream(new FileInputStream("F:\\utf.txt"));
        System.out.println("writeUTF讀取:" + dis.readUTF());
        dis.close();
    }
}

六、操作內存中的字節數組

ByteArrayInputStream,此流對象用來讀取字節數組,而數組是存在內存中的,不占用底層系統的資源,所以此流關閉無效,強行調用close方法之后也繼續可以使用,同時不會產生IO異常,構造方法中傳遞一個字節數組,用來存儲讀取的數據。

ByteArrayOutputStream,用來寫字節數組,實際的數據是寫進的內存中,同時也是關閉無效,空參構造。

輸出流常用的有以下方法:

Byte[] toByteArray(); //返回流中的數組
String toString(); //將流中的數據,轉成字符串
String toString(String字符集名字); //將流中的數據,按照傳入的編碼表轉成字符串

demo

import java.io.*;
/*
 * ByteArrayStreamDemo
 */

public class ByteArrayStreamDemo {
    public static void main(String[] args) throws Exception{
        method1();
    }
    
    /*
     * 讀取文件到內存,然后使用ByteArrayOutputStream輸出到新文件
     */
    public static void method1() throws Exception{
        
        //指定文件,替換為自己的測試文件即可
        FileInputStream fis = new FileInputStream
          (new File("F:\\迅雷下載\\372.90-notebook-win10-64bit-international-whql.exe"));
        ByteArrayOutputStream dos = new ByteArrayOutputStream();
        
        byte[] data = new byte[2014];
        int len = 0;
        
        //這樣做會把整個文件在裝到內存中,文件過大會出內存溢出的異常
        //我裝的文件大概在330MB,此程序結束后,內存會被釋放掉
        //可以調用垃圾回收提前釋放內存
        while((len = fis.read(data)) != -1){
            dos.write(data,0,len);
        }
        fis.close();
        
        //睡十秒,便于觀察內存的占用的情況
        Thread.sleep(10000);
        
        //直接全部輸出到新的文件中去
        FileOutputStream fos = new FileOutputStream(new File("F:\\nivdia.exe"));
        fos.write(dos.toByteArray());
        fos.flush();
        fos.close();
    }
    
    /*
     * 普通讀寫操作
     */
    public static void method() throws UnsupportedEncodingException{
        
        ByteArrayInputStream bats = new ByteArrayInputStream("嘻嘻哈哈".getBytes());
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        
        int len = 0;
        while((len = bats.read()) != -1){
            System.out.println(len);
            baos.write(len);
        }
        
        String s = baos.toString("gbk");
        System.out.println(s);
        
    }
}

七、隨機讀寫流

RandomAccessFile這個隨機讀寫流有點類似于C++中的文件操作對象,能夠指定讀寫的位置,同時同一流對象既能夠讀取數據也能夠寫入數據,在已經有數據存在的位置繼續寫數據會覆蓋掉之前的數據。

構造方法:

RandomAccessFile(File file, String mode); //mode為文件打開的方式
RandomAccessFile(String name, String mode); 

mode為文件打開的方式,具體有以下四種方式:

mode 含義
r 以只讀方式打開。調用結果對象的任何 write 方法都將導致拋出 IOException
rw 打開以便讀取和寫入。如果該文件尚不存在,則嘗試創建該文件。
rwd 打開以便讀取和寫入,對于 "rw",還要求對文件的內容或元數據的每個更新都同步寫入到底層存儲設備。
rws 打開以便讀取和寫入,對于 "rw",還要求對文件內容的每個更新都同步寫入到底層存儲設備。

rws和rwd的具體區別:

"rwd" 模式可用于減少執行的 I/O 操作數量。使用 "rwd" 僅要求更新要寫入存儲的文件的內容;使用 "rws" 要求更新要寫入的文件內容及其元數據,這通常要求至少一個以上的低級別 I/O 操作。

常用的方法:

void close(); //關閉流對象
long getFilePointer(); //獲取當前文件指針的位置
long length(); //獲取文件的長度,按字節計算
int read(); //讀取一個字節數據
int read(byte[] b); //讀取一個字節數組長度的數據
int read(byte[] b, int off, int len); //讀取指定長度的數據
int readInt(); //讀取int長度的數據
String readLine(); //讀行
void seek(long pos); //設置文件指針的位置
void setLength(long newLength); //設置文件大小,可以用于創建空白文件
void write(byte[] b); //寫數據

demo:

package io;

import java.io.*;

/*
 * 隨機讀寫流RandomAccessFile
 * 此流特點,能讀能寫,能指定文件指針位置,四種打開模式
 * 寫:可以寫基本數據類型和字節數據
 */
public class RandomAccessFileDemo {
    public static void main(String[] args) throws Exception {
        method();
    }

    /*
     * 實現文件的指定位置讀寫 
     * 1. 獲取一個文件,包括文件名和大小信息
     * 2. 在新目錄下創建一個相同大小的新文件,模擬下載文件 
     * 3.然后先復制文件的后半段,再復制文件的前半段信息 
     * 4. 完成復制,嘗試打開復制的exe文件,看是否成功
     */
    public static void method() throws Exception {

        // 初始化源文件
        //一個330MB的exe程序
        File file = new File("F:\\nivdia.exe");
        long fileLength = file.length();

        // 源文件名字的處理
        String fileName = file.getName();
        System.out.println(fileName);
        String[] nameAndType = fileName.split("\\.");
        System.out.println(nameAndType.length);
        fileName = nameAndType[0] + "2." + nameAndType[1];

        // 創建隨機讀取對象,指定模式為rw模式
        RandomAccessFile raf = new RandomAccessFile
          (new File(file.getParentFile(), fileName), "rw");
        // 設置文件大小,如果文件存在,文件大小會被改變
        raf.setLength(fileLength);
        
        //先寫后半部分的數據
        // 因為要同時操作兩個文件,都還要指定位置讀寫,所有還需要創建一個隨機讀寫流
        // 因為原文件只需要讀取數據,所以為只讀模式
        RandomAccessFile rf = new RandomAccessFile(file, "r");
        int len = 0;
        byte[] data = new byte[1024];
        long backHalf = fileLength / 2;
        rf.seek(backHalf);
        raf.seek(backHalf);
        
        //因為文件不規律,所以需要處理好尾部
        //有規律的話,用抓文件末尾異常也可以結束掉循環
        byte[] endsData = new byte[(int) (backHalf % 1024)];
        
        while (true) {
            if((rf.getFilePointer() - backHalf + 1024) > backHalf){
                rf.read(endsData);
                raf.write(endsData);
                break;
            }else{
                rf.read(data);
                raf.write(data);
            }
        }
        
        //接著寫前半部分的數據
        rf.seek(0);
        raf.seek(0);
        //此處由于沒有到文件末尾,所以不能使用EOF異常來進行判斷
        //前后兩次肯定會有交叉的地方,由于會覆蓋讀寫,所以不用考慮,
        //只需要確定超過一半就可以
        while(true){
            len = rf.read(data);
            raf.write(data, 0, len);
            if(rf.getFilePointer() > backHalf)
                break;
        }
        rf.close();
        raf.close();
    }
}

模擬下載后文件信息對比(下篇關于線程的文章里會提供多線程通信發包的方式來模擬下載):

信息對比

以上就是IO流的所以內容。

附:
java--IO流(一)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容