天天看點

JavaSE進階開發之I/O流(二)

1. 記憶體操作流

除了檔案之外,

I/O

的操作也可以發生在記憶體之中,這種流被稱為記憶體操作流。檔案流不管最後這個檔案資料是否會被保留都會産生一個檔案資料,而記憶體流是跟記憶體有關的,不需要關聯檔案。如果有個需求是要進行I/O處理,但是又不希望産生臨時檔案,這種情況下就可以使用記憶體流。

1.1 基本使用

記憶體流的分類:
1. 位元組記憶體流:ByteArrayInputStream、 ByteArrayOutputStream
2. 字元記憶體流:CharArrayReader、 CharArrayWriter
           
  • 通過記憶體流實作小寫轉大寫:
package com.file;

import java.io.*;

//将字元串中的字元轉換成大寫,要求使用記憶體流
public class TestMemoryStream {

    public static void main(String[] args) {

        String message = "hello world";
        byte[] messageBytes = message.getBytes();

        //記憶體操作,不需要捕獲異常
        //in out 都是記憶體流,資料都存在記憶體中
        ByteArrayInputStream in = new ByteArrayInputStream(messageBytes);
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        try {
            int c = -1;
            while((c = in.read()) != -1) {
                //轉大寫
                c = c-32;
                out.write(c);
            }
            out.flush();

            //輸出流裡面沒有把輸出流變成位元組數組的方法,是ByteArray裡面有的
            //byte[] newMessage = ((ByteArrayOutputStream) out).toByteArray();
            byte[] newMessage = out.toByteArray();
            System.out.println(new String(newMessage));
            
        } catch (IOException e) {

        }
    }
}
           
JavaSE進階開發之I/O流(二)

這個時候發生了

I/O

操作,但是沒有檔案産生,可以了解為一個臨時檔案處理。

1.2 檔案合并

package com.file;

import java.io.*;

public class MemoryStreamMerge {

    public static void main(String[] args) {

        //data-1.txt + data-2.txt = data.txt

        /*步驟:
         * 1. data-1.txt 複制到記憶體的輸出流
         * 2. data-2.txt 複制到記憶體的輸出流
         * 3. 記憶體的輸出流 -> byte[]位元組流
         * 4. byte[] -> 輸出到檔案的輸出流
         */

        File part1 = new File("D:" + File.separator + "test" + File.separator + "data-1.txt");
        File part2 = new File("D:" + File.separator + "test" + File.separator + "data-2.txt");
        File part = new File("D:" + File.separator + "test" + File.separator + "data.txt");

        try(FileInputStream in1 = new FileInputStream(part1);
            FileInputStream in2 = new FileInputStream(part2);
            ByteArrayOutputStream out1 = new ByteArrayOutputStream();
            FileOutputStream out2 = new FileOutputStream(part)
        ) {

            byte[] buff = new byte[1024];
            int len = -1;
            while((len = in1.read(buff)) != -1) {
                out1.write(buff, 0, len);
            }
            while((len = in2.read(buff)) != -1) {
                out1.write(buff, 0, len);
            }
            out1.flush();

            byte[] data = out1.toByteArray();
            out2.write(data);
            out2.flush();

        } catch (IOException e) {
			e.printStackTrace();
        }

    }
}
           
  • 既然是在記憶體中讀寫,記憶體流速度雖然快,但是受制于記憶體大小,适合于處理批量的少量資料。
/**源碼:
 * Closing a <tt>ByteArrayOutputStream</tt> has no effect. The methods in
 * this class can be called after the stream has been closed without
 * generating an <tt>IOException</tt>.
 */
public void close() throws IOException {
}
           

上面是記憶體流的源碼,看出來記憶體流可以關閉也可以不關閉,因為

close()

裡面沒有任何代碼。

2. 列印流

PrintStream

可以稱之為

OutputStream

的加強版。

OutputStream

隻能放位元組或者位元組數組,如果操作的不是二進制資料,就需要

getBytes

,是以不是很友善,總結下來其缺點有兩個:

① 所有的資料必須轉換為位元組數組。

② 如果要輸出的是

int

或者

double

等類型就不友善了。

2.1 基本使用

列印流設計的主要目的是為了解決

OutputStream

的設計問題,其本質不會脫離

OutputStream

package com.file;

import java.io.*;

/**
 * 目的:為了OutputStream的輸出更加簡單
 * 類似代理模式,但是不完全遵循
 * 代碼複用性強
 */
public class PrintUtil {

    private final OutputStream out;

    public PrintUtil(OutputStream out) {
        this.out = out;
    }

    public void print(String value) {
        try {
            this.out.write(value.getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void println(String value) {
        this.print(value);
        try {
            this.out.write((int)'\n');
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void print(int value) {
        this.println(String.valueOf(value));
    }

    public static void main(String[] args) {
        try(FileOutputStream out = new FileOutputStream("D:" + File.separator + "test"
                + File.separator + "printutil.txt")
        ) {

            PrintUtil printUtil = new PrintUtil(out);
            printUtil.print("hello");
            printUtil.print(" world");
            printUtil.print(22);
            printUtil.print(10);
            /*
             * hello world
             * 22
             * 10
             */

        }  catch (IOException e) {
            e.printStackTrace();
        }
    }
}
           

經過簡單處理之後,讓

OutputStream

的功能變的更加強大了,其實本質就隻是對

OutputStream

的功能做了一個封裝而已。

  • 但是上面僅僅是我們自己定義的列印流,還不能滿足全部的需求。

2.2 系統提供的列印流

1. 位元組列印流:PrintStream
2. 字元列印流:PrintWriter
           

列印流的設計屬于裝飾設計模式:核心依然是某個類的功能,但是為了得到更好的操作效果,讓其支援的功能更多一些。

  • 不光支援數字、字元、字元串和字元數組等,還可以支援格式化輸出。

    格式化輸出:

    printf(String format, Object ... args)

package com.file;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;

public class TestPrintStream {

    public static void main(String[] args) {

        String file = "D:" + File.separator + "test"
                + File.separator + "printStream.txt";

        //字元的列印流
        try(PrintWriter writer = new PrintWriter(file)) {

            writer.write("姓名");
            writer.write("張三");
            writer.write("\n");
            writer.write(50);
            writer.write("\n");
            writer.write(new char[]{'A', 'B', 'C'});

            //鍊式調用
            writer.append("A").append("B").append("C");

            //格式化輸出,String裡面也有個格式化輸出的方法, System.out.printf也是
            writer.printf("姓名:%s 年齡:%d 身高:%.2fcm \n", "張三", 22, 180.25F);
            String str = String.format("姓名:%s 年齡:%d 身高:%.2fcm", "張三", 22, 180.25F);
            writer.println(str);

            writer.flush();
        } catch(IOException e) {

        }

    }
}
           

3. System對I/O的支援

實際上我們一直在使用的系統輸出

System.out.println

就是利用了

I/O

流的模式完成。

/*
System:類
out:靜态屬性,PrintStream類型
println:out屬性的一個對象的方法
 */
System.out.println("hello world");
           
  • System

    類中定義了三個操作的常量:

    ① 标準輸出(顯示器) :

    public final static PrintStream out

    ② 錯誤輸出:

    public final static PrintStream err

    ③ 标準輸入(鍵盤):

    public final static InputStream in

3.1 系統輸出

系統輸出一共有兩個常量:

out

err

,并且這兩個常量表示的都是

PrintStream

類的對象:

out

輸出的是希望使用者能看到的内容

err

輸出的是不希望使用者看到的内容

package com.file;

public class SystemIO {

    public static void main(String[] args) {

        try {
            //String -> Integer
            Integer.parseInt("abc");
        } catch(NumberFormatException e) {
            System.out.println(e.getMessage());
            System.err.println(e.getMessage());
        }
	}
}
           
JavaSE進階開發之I/O流(二)

但是這兩種輸出在實際的開發之中都沒用了,取而代之的是 “ 日志 ” 。

  • System.err

    隻是作為一個保留的屬性而存在,現在幾乎用不到。唯一可能用得到的就是

    System.out

  • 由于

    System.out

    PrintStream

    的執行個體化對象,而

    PrintStream

    又是

    OutputStream

    的子類,是以可以直接使用

    System.out

    直接為

    OutputStream

    執行個體化對象,也就是說如果要得到輸出到控制台或螢幕的對象,選擇

    System.out

  • 使用

    System.out

    OutputStream

    執行個體化:
package com.file;

import java.io.*;

public class SystemIO {

    public static void main(String[] args) {
    
        //輸出
        PrintStream printStream = System.out;
        //PrintStream是OutputStream的子類
        OutputStream out = System.out;
        try {
            out.write("hello".getBytes());
        } catch(IOException e) {
            e.printStackTrace();
        }
	}
}
           
JavaSE進階開發之I/O流(二)

3.2 系統輸入

System.in

對應的類型是

InputStream

,而這種輸入流指的是由使用者通過鍵盤進行輸入(使用者輸入)。java本身并沒有直接的使用者輸入處理,如果要想實作這種操作,必須使用

java.io

的模式來完成。

  • 使用

    System.in

    實作資料輸入:
package com.file;

import java.io.*;

public class SystemIO {

    public static void main(String[] args) {
    	
    	//輸入
        InputStream in = System.in;
        try {
            byte[] buff = new byte[5];
            int len = in.read(buff);
            System.out.println("讀取了"+len+"位元組, 内容是:"+new String(buff, 0, len));
        } catch (IOException e) {
            e.printStackTrace();
        }
	
	}
}
           
JavaSE進階開發之I/O流(二)
  • 以上的程式本身有一個緻命的問題,核心點在于:開辟的位元組數組長度固定,如果現在輸入的長度超過了位元組數組長度,那麼隻能夠接收部分資料。這個時候是由于一次讀取不完所造成的問題,是以此時最好的做法是引入記憶體操作流來進行控制,這些資料先儲存在記憶體流中而後一次取出。
package com.file;

import java.io.*;

public class SystemIO {

    public static void main(String[] args) {
    	
    	InputStream in = System.in;
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        try {
            byte[] buff = new byte[5];
            int len = -1;
            while((len = in.read(buff)) != -1) {
                out.write(buff, 0, len);
                if(len < buff.length) {
                    //已經讀到最後一批
                    break;
                }
            }
            byte[] data = out.toByteArray();
            System.out.println("讀取了"+data.length+"位元組, 内容是:"+new String(data));
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}
           
JavaSE進階開發之I/O流(二)
  • 至于為什麼命名輸入的隻有8個位元組,卻顯示讀取了9個位元組的問題,是因為輸入完成之後在鍵盤上敲下回車,所有的鍵盤操作都是會被記錄下來的。

現在雖然實作了鍵盤輸入資料的功能,但是整體的實作邏輯過于混亂了,即java提供的

System.in

并不好用,還要結合記憶體流來完成,導緻複雜度很高。是以也就有了下面兩種輸入流的産生。

4. 兩種輸入流

4.1 BufferedReader類

BufferedReader

類屬于一個緩沖的輸入流,而且是一個字元流的操作對象。

1. 位元組緩沖流:BufferedInputStream
2. 字元緩沖流:BufferedReader
           

之是以選擇

BufferedReader

類操作是因為在此類中提供有如下方法:

按行讀取,回車換行:

String readLine() throws IOException

  • 利用

    BufferedReader

    實作鍵盤輸入:
package com.file;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Buffer {

    public static void main(String[] args) {

        //位元組輸入流
        InputStream inputStream = System.in;

        //字元輸入流(位元組流轉字元流)
        InputStreamReader inputStreamReader = new InputStreamReader(inputStream);

        //緩沖的字元輸入流
        BufferedReader reader = new BufferedReader(inputStreamReader);

        //互動式的反複輸入
        while(true) {
            System.out.println("請輸入名字:");
            try {
                String line = reader.readLine();
                System.out.println(line);
                if(line.equals("quit")) {
                    break;
                }
            } catch(IOException e) {
                e.printStackTrace();
            }
        }

    }
}
           
JavaSE進階開發之I/O流(二)

以上操作形式是java十多年前輸入的标準格式,但是時過境遷,這個類也淹沒在曆史的潮流之中,被

JDK1.5

提供的

java.util.Scanner

類所取代。

4.2 java.util.Scanner類

Scanner

是一個專門進行輸入流處理的程式類,利用這個類可以友善處理各種資料類型,同時也可以結合正規表達式進行各項處理,在這個類中主要關注以下方法:

① 判斷是否有指定類型資料:

public boolean hasNextXxx()

② 取得指定類型的資料:

public 資料類型 nextXxx()

③ 定義分隔符:

public Scanner useDelimiter(Pattern pattern)

  • 使用

    Scanner

    實作資料輸入:
package com.file;

import java.util.Scanner;

public class TestScanner {

    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);

        System.out.println("請輸入資料:");
        if(scanner.hasNext()) {
            System.out.println("輸入的内容:"+scanner.next());
        }

	}
}
           
JavaSE進階開發之I/O流(二)
  • 接收其他類型資料:
  • 使用

    Scanner

    還可以接收各種資料類型,并且幫助使用者減少轉型處理。
package com.file;

import java.util.Scanner;

public class TestScanner {

    public static void main(String[] args) {
		
		Scanner scanner = new Scanner(System.in);
		
		System.out.println("請輸入年齡:");
        if(scanner.hasNextInt()) {
            System.out.println("輸入的内容是int");
        } else {
            System.out.println("輸入的内容不是int");
        }

	}
}
           
JavaSE進階開發之I/O流(二)
  • 對接收的資料類型使用正規表達式判斷:
package com.file;

import java.util.Scanner;

public class TestScanner {

    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);
		
		//正規表達式
        System.out.println("請輸入生日:");
        if(scanner.hasNext("\\d{4}-\\d{2}-\\d{2}")) {
            System.out.println("輸入的内容:"+scanner.next());
        } else {
            System.out.println("輸入的内容不是生日格式");
        }

	}
}
           
JavaSE進階開發之I/O流(二)
  • Scanner

    讀取檔案操作:
  • 使用

    Scanner

    本身能夠接收的是一個

    InputStream

    對象,那麼也就意味着可以接收任意輸入流,例如:檔案輸入流,

    Scanner

    完美的替代了

    BufferedReader

    ,而且更好的實作了

    InputStream

    的操作。
package com.file;

import java.io.IOException;
import java.nio.file.Paths;
import java.util.Scanner;

public class TestScanner {

    public static void main(String[] args) {

		//讀取檔案(針對字元)
        try(Scanner scanner = new Scanner(Paths.get("D:", "test", "data.txt"))) {
            //比File.separater更友善

            scanner.useDelimiter("\n");
            while(scanner.hasNext()) {
                System.out.println(scanner.next());
            }

        } catch (IOException e) {
            e.printStackTrace();
        }


    }
}
           
JavaSE進階開發之I/O流(二)
  • 總結:

    PrintStream 解決的是 OutputStream 類的缺陷,BufferedReader 解決的是 InputStream 類的缺陷。而 Scanner 則解決的是 BufferedReader 類的缺陷(替換了BufferedReader類)。

5. 序列化與反序列化

- 序列化(Java Object -> byte[]):ObjectOutputStream
 - 反序列化(byte[] -> Java Object):ObjectInputStream
           

5.1 序列化技術

概念:把Java對象變成

byte

數組(二進制流),主要是用于網絡傳輸。

  • 一個類的執行個體化對象要能夠進行序列化必須實作序列化(

    Serializable

    )接口!
  • ObjectOutputStream

    裡面的方法有:

    ObjectOutputStream(OutputStream out)

    writeObject(Object obj)

    ,寫對象
package com.serializable;

import java.io.*;

class Person implements Serializable {

    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class TestSerializable {

    public static void main(String[] args) {

        Person person1 = new Person();
        person1.setName("張三");
        person1.setAge(22);

        //序列化
        //Java Object  ->  byte[]
        //可以寫到file或者byte[]裡面
        try(ByteArrayOutputStream stream = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(stream)
        ) {

            out.writeObject(person1);
            out.flush();
            //data對象變成的二進制流
            byte[] data = stream.toByteArray();
            System.out.println(new String(data));

        } catch (IOException e) {
            e.printStackTrace();
        }
        
    }

}
           
JavaSE進階開發之I/O流(二)

5.1 反序列化技術

概念:把

byte

數組轉換成對象。

  • ObjectInputStream

    裡面的方法有:

    ObjectInputStream(InputStream in)

    readObject(Object obj)

    ,讀對象
package com.serializable;

import java.io.*;

class Person implements Serializable {

    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class TestSerializable {

    public static void main(String[] args) {

        Person person1 = new Person();
        person1.setName("張三");
        person1.setAge(22);
        System.out.println("person1:"+person1);

        //序列化
        //Java Object  ->  byte[]
        //可以寫到file或者byte[]裡面
        try(ByteArrayOutputStream stream = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(stream)
        ) {

            out.writeObject(person1);
            out.flush();
            //data對象變成的二進制流
            byte[] data = stream.toByteArray();

            //反序列化
            //byte[] -> java Object
            try(ByteArrayInputStream instream = new ByteArrayInputStream(data);
                    ObjectInputStream in = new ObjectInputStream(instream)
            ) {

                Object returnValue = in.readObject();
                System.out.println(returnValue.getClass());

                Person person2 = (Person) returnValue;
                System.out.println("Person2:" + person2);

                //前者Person1是通過執行個體化new出來的,在堆上配置設定出來的
                //後者person2是通過二進制流變成的Java對象
                System.out.println(person1 == person2);

            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }

        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}
           
JavaSE進階開發之I/O流(二)

5.3 通過網絡傳輸

package com.serializable;

import java.io.*;

class Person implements Serializable {

    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class TestSerializable {

    public static void main(String[] args) {

        Person person1 = new Person();
        person1.setName("張三");
        person1.setAge(22);

        try(FileOutputStream stream = new FileOutputStream("D:" + File.separator + "test" + File.separator + "person.obj");
            ObjectOutputStream out = new ObjectOutputStream(stream)
        ) {

            out.writeObject(person1);
            out.flush();

        } catch (IOException e) {
            e.printStackTrace();
        }
        
    }
}
           

假設我把

person.obj

複制粘貼到了

D:\\test

目錄下,相當于通過網絡傳輸給了别人,那麼别人如何打開這個檔案并擷取到其中的内容呢?

package com.serializable;

import java.io.*;

class Person implements Serializable {

    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class TestSerializable {

    public static void main(String[] args) {

        try(FileInputStream stream = new FileInputStream("D:" + File.separator + "person.obj");
            ObjectInputStream in = new ObjectInputStream(stream)
        ) {

            Object returnValue = in.readObject();
            Person person = (Person) returnValue;
            System.out.println(person);

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}
           
JavaSE進階開發之I/O流(二)

5.4 transient關鍵字

Serializable

預設會将對象中所有屬性進行序列化儲存,如果現在某些屬性(比如密碼)不希望被儲存了,那麼就可以使用

transient

關鍵字,即被transient修飾過的屬性不再參與序列化。

package com.serializable;

import com.sun.corba.se.impl.orb.PropertyOnlyDataCollector;

import java.io.*;

class Person implements Serializable {

    private String name;
    private Integer age;
    private transient String password;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", password='" + password + '\'' +
                '}';
    }
}

public class TestSerializable {
    public static void main(String[] args) {

        Person person1 = new Person();
        person1.setName("李四");
        person1.setAge(20);
        person1.setPassword("abc123");

		//序列化
        try(FileOutputStream stream = new FileOutputStream("D:" + File.separator + "person.obj");
            ObjectOutputStream out = new ObjectOutputStream(stream1)
        ) {

            out.writeObject(person1);
            out.flush();

			//反序列化
            try(FileInputStream inStream = new FileInputStream("D:" + File.separator + "person.obj");
            ObjectInputStream in = new ObjectInputStream(inStream)
            ) {

                Object returnValue = in.readObject();
                Person person2 = (Person) returnValue;
                System.out.println(person2);


            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }


        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}
           
JavaSE進階開發之I/O流(二)