Java編程拾遺『字符流』

之前一篇文章講述了Java中字節流的分類及簡單用法,最後我們可以發現,Java中的字節流既可以用來處理二進制文件,也可以用來處理文本文件。但是字節流對於文本文件的處理是不太方便的,比如字節流的媒介是字節,但是文本文件的內容都是可顯式地字符,很明顯通過字符作爲媒介更合理(需要手動進行字節和字符通過某種特定編碼格式的轉化)。另外,在之前那篇文章Java編程拾遺『搞定編碼』中,我們瞭解到,字符可以有很多種編碼方式,但是在使用字節流處理文本文件,編碼這一塊就不是很方便處理,因爲字節流是沒有編碼地概念的。但是文本或者講字符串,是Java中比較常見地一種形式,能方便地進行處理還是很重要的,所以字符流就出現了。可以講,字符流就是用來方便處理可顯示字符的。字符流的繼承體系如下:

本文重點介紹如下幾種字符流:

  • Reader/Writer:抽象類,也是字符流的基類
  • InputStreamReader/OutputStreamWriter:適配器類,將字節流轉換爲字符流
  • FileReader/FileWriter:輸入源和輸出目標是文件的字符流
  • CharArrayReader/CharArrayWriter: 輸入源和輸出目標是char數組的字符流
  • StringReader/StringWriter:輸入源和輸出目標是String的字符流
  • BufferedReader/BufferedWriter:裝飾類,對輸入輸出流提供緩衝,以及按行讀寫功能
  • PrintWriter:裝飾類,可將基本類型和對象轉換爲其字符串形式輸出的類

1. Reader/Writer

Reader和Writer是抽象類,是所有字符流的基類。

1.1 Reader

Reader中的方法跟InputStream中的方法類似,區別在於InputStream讀取的單位是字節,Reader讀取的單位char。

S.N. 方法 說明
1 public int read() throws IOException 從字符輸入流Reader中讀取一個字符,返回值爲讀取字符對應的int值,返回-1表示字符流中的內容已經讀取結束
2 public int read(char cbuf[]) throws IOException 從字符輸入流Reader中讀取多個字節,放入字符數組cbuf中,返回值爲實際讀入的字符個數
3 abstract public int read(char cbuf[], int off, int len) throws IOException 從字符輸入流Reader中讀取len個字節放入字符數組cbuf index off開始的位置, 返回值爲實際讀入的字符個數
4 public boolean ready() throws IOException 返回字符輸入流Reader下一次read()操作是否需要阻塞,如果需要阻塞返回true,否則返回false。跟InputStream中的available方法類似。
5 public long skip(long n) throws IOException 跳過字符輸入流Reader n個字符,返回值爲實際跳過的字符個數
6 public boolean markSupported() 判斷當前字符輸入流是否支持mark/reset操作
7 public void mark(int readAheadLimit) throws IOException 標記能夠從字符輸入流中往後讀取的字符個數readAheadLimit
8 public void reset() throws IOException 重新從標記位置讀取字符輸入流
9 abstract public void close() throws IOException 關閉字符輸入流,釋放資源

方法大體上跟InputStream很類似,不同的是,InputStream讀取的單位是字節,Reader讀取的單位是char。

1.1 Writer

S.N. 方法 說明
1 public void write(int c) throws IOException 向字符輸出流中寫入一個字符
2 public void write(char cbuf[]) throws IOException 將字符數組cbuf中所有的字符寫入字符輸出流
3 abstract public void write(char cbuf[], int off, int len) throws IOException 將字符數組cbuf中從index off開始,長度爲len的字符寫入字符輸出流
4 public void write(String str) throws IOException 將字符串str中所有的字符寫入到字符輸出流
5 public void write(String str, int off, int len) throws IOException 將字符串str中index 從off開始長度爲len的所有字符寫入到字符輸出流
6 public Writer append(char c) throws IOException 操作等同於write(int c),向字符輸出流中寫入一個字符
7 public Writer append(CharSequence csq) throws IOException 操作等同於write(String str),將CharSequence中所有的字符寫入到字符輸出流
8 public Writer append(CharSequence csq, int start, int end) throws IOException 將CharSequence中index再[start, end)區間內所有的字符寫入到字符輸出流
9 abstract public void flush() throws IOException 將緩衝而未實際寫的數據進行實際寫入
10 abstract public void close() throws IOException 關字符輸出流,釋放資源

2. InputStreamReader/OutputStreamWriter

InputStreamReader和OutputStreamWriter是適配器類,負責InputStream/OutputStream和Reader/Writer之間的轉換。

2.1 OutputStreamWriter

OutputStreamWriter可以將字節輸出流OutputStream轉換writer。舉個例子,如果要將字符串寫入一個文本文件中,直接通過FileOutputStream是很不方便的(需要先將字符串通過某種編碼轉化爲字節數組,然後將字節數組通過FileOutputStream最終寫入到文件中),但是通過OutputStreamWriter將FileOutputStream轉化爲Writer後,就可以很方便地直接將字符串寫入文件。OutputStreamWriter可以通過如下方式構建:

public OutputStreamWriter(OutputStream out)
public OutputStreamWriter(OutputStream out, Charset cs)
public OutputStreamWriter(OutputStream out, String charsetName)

參數out爲待適配的字節輸出流OutputStream對象,第二個參數cs/charSetName爲用來將字符編碼的字符集,在將字符轉化爲字節數組時使用。如果沒有傳,則使用系統默認編碼。

2.2 InputStreamReader

InputStreamReader可以將字節輸入流InputStream轉化爲Reader,可以方便地通過字符/字符串讀取字節輸入流InputStream中的內容。可以通過如下方式構建:

public InputStreamReader(InputStream in)
public InputStreamReader(InputStream in, Charset cs)
public InputStreamReader(InputStream in, String charsetName)

跟OutPutStreamWriter構造函數類似,參數in爲待適配的InputStream對象,第二個參數cs/charSetName爲用來將字符編碼的字符集,在將字節轉化爲字符時使用。 如果沒有傳,則使用系統默認編碼。

2.3 示例

@SneakyThrows
public static void main(String[] args) {
    try (OutputStream fileOutputStream = new FileOutputStream("d:/hello.txt");
         Writer outPutStreamWriter = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8)) {
        String text = "hello 卓立";
        outPutStreamWriter.write(text);
    }

    try (InputStream fileInputStream = new FileInputStream("d:/hello.txt");
         Reader inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8)) {
        char[] cbuf = new char[1024];
        int charsRead = inputStreamReader.read(cbuf);
        System.out.println(new String(cbuf, 0, charsRead));
    }
}

運行結果:

hello 卓立

可以看到,通過OutputStreamWriter,將FileOutputStream對象轉化爲Writer實例,然後直接調用Writer的write方法,直接將字符串通過UTF-8編碼寫入到文件中。然後通過InputStreamReader,將FileInputStream對象轉化爲Reader實例,然後直接調用Reader的read方法,直接將文件中的二進制內容通過UTF-8編碼,讀取到字符數組cbuf中,並轉化爲String打印輸出。

3. FileReader/FileWriter

FileReader/FileWriter是輸入源和輸出目標是文件的字符流,FileReader繼承了InputStreamReader,FileWriter繼承了OutputStreamWriter

2.1 FileWriter

FileWriter可以方便地將字符/字符串寫入文件,從這個角度上講,FileWriter和OutputStreamWriter非常類似。但是不同的是,FileWriter不能指定編碼類型,只能使用系統默認編碼,而OutputStreamWriter可以指定編碼。可以通過如下方式構造FileWriter:

public FileWriter(File file) throws IOException
public FileWriter(File file, boolean append) throws IOException
public FileWriter(String fileName) throws IOException
public FileWriter(String fileName, boolean append) throws IOException 

參數file/fileName,表示要輸出的文件。第二個參數append,表示是追加文件還是覆蓋文件。除了構造函數外,其他方法都試繼承自OutputStreamWriter類。

2.2 FileReader

FileReader可以方便地將文件內容讀取爲字符/字符串,跟InputSreamReader的功能非常類似。同時,FileReader也不能指定編碼類型,只能使用系統默認編碼,而InputStreamReader可以指定編碼。可以通過如下方式構造FileReader:

public FileReader(File file) throws FileNotFoundException
public FileReader(String fileName) throws FileNotFoundException

跟FileWriter類似,FileReader類中也只定義了構造函數,其他方法都繼承自InputStreamReader。

2.3 示例

@SneakyThrows
public static void main(String[] args) {
    try (Writer fileWriter = new FileWriter("d:/hello.txt")) {
        String text = "hello 卓立";
        fileWriter.write(text);
    }

    try (Reader fileReader = new FileReader("d:/hello.txt")) {
        char[] cbuf = new char[1024];
        int charsRead = fileReader.read(cbuf);
        System.out.println(new String(cbuf, 0, charsRead));
    }
}

運行結果:

hello 卓立

4. CharArrayReader/CharArrayWriter

CharArrayReader/CharArrayWriter是源和目的地是char數組的字符流,跟字節流中的ByteArrayInputStream/ByteArrayOutputStream的功能類似。

4.1 CharArrayWriter

任何一個Reader的實現類都可以實現將輸入流中的內容讀取爲字符數組,但是有這樣一個問題Reader是無法解決的,那就是用來存儲輸入流中內容的字符數組大小需要預先定義,如果不夠大,也不方便擴展。這時候就可以藉助CharArrayWriter類,先通過Reader將輸入流中的內容讀取到一個固定大小的字符數組中,如果超過固定大小的字符數組,可分多次讀取,然後將每次讀取的內容依次寫入到CharArrayWriter對象中,最後調用CharArrayWriter的toCharArray方法獲取一個完整的數組。CharArrayWriter可以通過如下方式構建:

public CharArrayWriter()
public CharArrayWriter(int initialSize)

initialSize表示CharArrayWriter內部用來存儲寫入內容的char數組的大小,第一個構造函數初始化的char數組默認大小爲32,如果寫入時char數組容量不夠,會進行動態擴容,每次擴容爲之前的兩倍。除了構造方法之外,CharArrayWriter對Writer接口的方法進行了重寫。其餘方法參考上面Writer類中的方法說明,這裏不再講了。

4.2 CharArrayReader

CharArrayReader與上篇文章講的的ByteArrayInputStream類似,它可以將char數組包裝爲Reader,是一種適配器模式,可以通過如下方式構建:

public CharArrayReader(char buf[])
public CharArrayReader(char buf[], int offset, int length) 

第一個構造函數是將char數組buf中的全部內容包裝到CharArrayReader(效果是buf數組中的全部char都體現在CharArrayReader中)。第二個構造函數是將char數組buf中index從offset開始長度爲length的所有char包裝到CharArrayReader(效果是buf數組中offset到offset + len -1的char體現在CharArrayReader中,read操作也只能讀取到offset及之後的char)。CharArrayReader的所有數據都在內存,支持mark/reset重複讀取。

4.3 示例

@SneakyThrows
public static void main(String[] args) {

    try (Reader inputStreamReader = new InputStreamReader(new FileInputStream("d:/hello.txt"), StandardCharsets.UTF_8)) {
        CharArrayWriter charArrayWriter = new CharArrayWriter();
        char[] cbuf = new char[1024];
        int charsRead;
        //分多次將Reader地內容讀出,並寫到writer中
        while ((charsRead = inputStreamReader.read(cbuf)) != -1) {
            charArrayWriter.write(cbuf, 0, charsRead);
        }
        System.out.println(charArrayWriter.toString());
    }

    char[] chars = "卓立 test".toCharArray();

    //調用CharArrayReader()構造函數初始化CharArrayReader
    try (CharArrayReader charArrayReader = new CharArrayReader(chars);
         CharArrayWriter charArrayWriter = new CharArrayWriter()) {
        writeChars(charArrayReader, charArrayWriter);
    }

    //調用CharArrayReader(char buf[], int offset, int length)構造函數初始化CharArrayReader
    try (CharArrayReader charArrayReader = new CharArrayReader(chars, 1, 3);
         CharArrayWriter charArrayWriter = new CharArrayWriter()) {
        writeChars(charArrayReader, charArrayWriter);
    }
}

@SneakyThrows
private static void writeChars(CharArrayReader charArrayReader, CharArrayWriter charArrayWriter) {
    char[] buf = new char[16];
    int charsRead;
    while ((charsRead = charArrayReader.read(buf)) != -1) {
        charArrayWriter.write(buf, 0, charsRead);
    }
    String data = charArrayWriter.toString();
    System.out.println(data);
}

運行結果:

hello 卓立
卓立 test
立 t

5. StringReader/StringWriter

StringReader/StringWriter是輸入源和輸出目標是String的字符流,這樣描述其實也不完全正確,因爲StringWriter類中實際是使用StringBuffer存儲write方法寫入的內容的

5.1 StringWriter

將數據通過StringWriter的write操作寫入,其實就是將數據寫入到StringWriter內部的StringBuffer中了。實現了數據的“寫出”,只不過跟CharArrayWriter類似,寫出的數據還在內存中。StringWriter可以通過如下方式構造:

public StringWriter()
public StringWriter(int initialSize)

第二個構造函數的參數initialSize,表示StringWriter中用來存儲寫入內容的StringBuffer初始化時的容量。第一個構造函數會調用StringBuffer的無參構造函數初始化一個StringBuffer實例對象。通過之前講的StringBuffer的內容可知,StringBuffer的無參構造函數,其其實內部char數組聲明的大小爲16,而如果指定大小,其內部char數組的大小即爲指定的大小。StringWriter內部使用了StringBuffer存儲寫入的內容,最終其實也等價於使用char數組,並且也可以動態擴容,本質上跟CharArrayWriter沒什麼區別。

5.2 StringReader

StringReader跟CharArrayReader一樣,也是一種適配器模式,可以將字符串包裝爲字符流。可以通過如下方式構建:

public StringReader(String s)

5.3 示例

@SneakyThrows
public static void main(String[] args) {

    try (Reader inputStreamReader = new InputStreamReader(new FileInputStream("d:/hello.txt"), StandardCharsets.UTF_8)) {
        StringWriter stringWriter = new StringWriter();
        char[] cbuf = new char[1024];
        int charsRead;
        //分多次將Reader地內容讀出,並寫到writer中
        while ((charsRead = inputStreamReader.read(cbuf)) != -1) {
            stringWriter.write(cbuf, 0, charsRead);
        }
        System.out.println(stringWriter.toString());
    }

    String str = "卓立 test";
    
    try (StringReader stringReader = new StringReader(str);
         StringWriter stringWriter = new StringWriter()) {
        writeString(stringReader, stringWriter);
    }
}

@SneakyThrows
private static void writeString(StringReader stringReader, StringWriter stringWriter) {
    char[] buf = new char[16];
    int charsRead;
    while ((charsRead = stringReader.read(buf)) != -1) {
        stringWriter.write(buf, 0, charsRead);
    }
    String data = stringWriter.toString();
    System.out.println(data);
}

運行結果:

hello 卓立
卓立 test

6. BufferedReader/BufferedWriter

BufferedReader/BufferedWriter直接繼承自Reader/Writer,是裝飾類,可爲普通普通字符流提供緩衝按行讀寫功能

6.1 BufferedWriter

BufferedWriter提供基礎字符輸出流緩衝寫和按行寫的能力,是一種裝飾器模式。可以通過如下方式構建:

public BufferedWriter(Writer out)
public BufferedWriter(Writer out, int sz)

out表示待包裝的基礎字符輸出流,sz表示緩衝大小,第一個構造函數中,沒有指定緩衝大小,默認爲8192。BufferedWriter繼承自Writer,對於繼承自Writer類的方法不再單獨講述了,參考上面的Writer方法列表,這裏列一下BufferedWriter中新增的按行寫的方法:

public void newLine() throws IOException

該方法可以輸出當前環境下特定的換行符

6.2 BufferedReader

BufferedReader提供基礎字符輸入流緩衝讀和按行讀的能力,是一種裝飾器模式。可以通過如下方式構建:

public BufferedReader(Reader in)
public BufferedReader(Reader in, int sz)

in表示待包裝的字符輸入流,sz表示緩衝的大小,第一個構造函數中,沒有指定緩衝大小,默認爲8192。BufferedReader繼承自Reader,對於繼承自Reader類的方法不再單獨講述了,參考上面的Reader方法列表,這裏列一下BufferedReader中新增的按行讀取的方法:

public String readLine() throws IOException

該方法返回一行內容,當讀到流結尾時,返回null

6.3 示例

FileReader/FileWriter讀寫時是沒有緩衝的,也不能按行讀寫,這裏通過裝飾類BufferedReader/BufferedWriter提供緩衝和按行讀寫功能:

@SneakyThrows
public static void main(String[] args) {
    try (FileWriter fileWriter = new FileWriter("d:/hello.txt");
         BufferedWriter bufferedWriter = new BufferedWriter(fileWriter)) {
        bufferedWriter.write("first line");
        bufferedWriter.newLine();
        bufferedWriter.write("second line");
        bufferedWriter.newLine();
        bufferedWriter.write("third line");
    }

    try (FileReader fileReader = new FileReader("d:/hello.txt");
         BufferedReader bufferedReader = new BufferedReader(fileReader)) {
        String line;
        while ((line = bufferedReader.readLine()) != null ) {
            System.out.println(line);
        }
    }
}

運行結果:

first line
second line
third line

7. PrintWriter

PrinterWriter直接繼承了Writer類,它有很多構造函數,可以接受文件路徑名、文件對象、OutputStream、Writer等。同時提供了很多自定義方法,可以方便的將各種基本類型數據、對象轉化爲字符串輸出,也可以實現按行輸出,格式化輸出。在輸出到文件時,可以優先選擇該類。PrinterWriter可以通過如下方式構建:

S.N. 方法 說明
1 public PrintWriter (Writer out) 裝飾模式,將普通字符輸出流包裝爲PrintWriter
2 public PrintWriter(Writer out, boolean autoFlush) 裝飾模式,將普通字符輸出流包裝爲PrintWriter,autoFlush表示同步緩衝區的時機,如果爲true,則在調用print、printf、format方法時會同步緩衝區,否則需要根據情況調用flush方法
3 public PrintWriter(OutputStream out) 適配器模式,將字節輸出流轉化爲PrintWriter
4 public PrintWriter(OutputStream out, boolean autoFlush) 適配器模式,將字節輸出流轉化爲PrintWriter,autoFlush參數含義同上
5 public PrintWriter(String fileName) throws FileNotFoundException 構造輸出目標時文件的PrintWriter,參數fileName表示文件路徑
6 public PrintWriter(String fileName, String csn) 構造輸出目標時文件的PrintWriter,參數fileName表示文件路徑,參數csn表示字符集,用於指定輸出到文件時的編碼,上面沒指定字符集時,表示使用系統默認字符集
7 public PrintWriter(File file) throws FileNotFoundException 構造輸出目標時文件的PrintWriter,參數file爲文件對象
8 public PrintWriter(File file, String csn) 構造輸出目標時文件的PrintWriter,參數file表示文件對象,參數csn表示字符集,用於指定輸出到文件時的編碼,上面沒指定字符集時,表示使用系統默認字符集
public PrintWriter (Writer out) {
    this(out, false);
}

public PrintWriter(Writer out, boolean autoFlush) {
    super(out);
    this.out = out;
    this.autoFlush = autoFlush;
    lineSeparator = java.security.AccessController.doPrivileged(
        new sun.security.action.GetPropertyAction("line.separator"));
}

public PrintWriter(OutputStream out) {
    this(out, false);
}

public PrintWriter(OutputStream out, boolean autoFlush) {
    this(new BufferedWriter(new OutputStreamWriter(out)), autoFlush);
    // save print stream for error propagation
    if (out instanceof java.io.PrintStream) {
        psOut = (PrintStream) out;
    }
}

public PrintWriter(String fileName) throws FileNotFoundException {
    this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName))),
         false);
}

public PrintWriter(String fileName, String csn) throws FileNotFoundException, UnsupportedEncodingException {
    this(toCharset(csn), new File(fileName));
}

public PrintWriter(File file) throws FileNotFoundException {
    this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file))), false);
}

public PrintWriter(File file, String csn) throws FileNotFoundException, UnsupportedEncodingException {
    this(toCharset(csn), file);
}

private PrintWriter(Charset charset, File file) throws FileNotFoundException {
    this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), charset)), false);
}
  • 對於參數類型時文件路徑、文件對象和OutputStream的構造函數,PrintWriter內部會構造一個BufferedWriter用於緩衝。但是對於Writer爲參數的構造函數,PrinterWriter內部就不包裝BufferedWriter了。
  • 參數csn表示字符集,可以控制寫入文件時的編碼格式,是通過構造OutputStreamWriter時生效的,具體可以參見上述最後一個私有的構造函數。
  • 構造方法中的參數autoFlush表示同步緩衝區的時機,如果爲true,則在調用println、printf或format方法的時候,同步緩衝區,如果沒有傳,則不會自動同步,需要根據情況調用flush方法。

下面來看一下PrintWriter類中除了繼承字Writer類之外的其它方法:

S.N. 方法 說明
1 public PrintWriter append(char c) 向writer中追加一個char
2 public PrintWriter append(CharSequence csq) 向writer中追加一個CharSequence
3 public PrintWriter append(CharSequence csq, int start, int end) 將CharSequence c從index start到index end的子串追加到writer中
4 public PrintWriter format(Locale l, String format, Object … args) 使用指定的格式字符串和參數將格式化的字符串寫入此writer。 如果啓用了自動刷新,則調用此方法將刷新輸出緩衝區。l表示格式化過程中使用的語言環境
5 public PrintWriter format(String format, Object … args) 同上format方法,只是沒指定語言環境,而使用默認語言環境
6 public void print(boolean b) 向writer中寫入一個boolean基本類型變量,如果爲true,寫入字符串”true”,否則寫入”false”
7 public void print(char c) 向writer中寫入一個char基本類型變量
8 public void print(char s[]) 向writer中寫入一個char數組
9 public void print(double d) 向writer中寫入一個double基本類型變量,寫入的實際是調用String.valueOf(d)返回的字符串
10 public void print(float f) 向writer中寫入一個float基本類型變量,寫入的實際是調用String.valueOf(f)返回的字符串
11 public void print(int i) 向writer中寫入一個int基本類型變量,寫入的實際是調用String.valueOf(i)返回的字符串
12 public void print(long l) 向writer中寫入一個long基本類型變量,寫入的實際是調用String.valueOf(l)返回的字符串
13 public void print(Object obj) 向writer中寫入一個Object對象,寫入的實際是調用String.valueOf(obj)返回的字符串
14 public void print(String s) 向writer中寫入一個String對象,如果s爲null,寫入”null”
15 public PrintWriter printf(Locale l, String format, Object … args) 使用指定的格式字符串和參數將格式化的字符串寫入此writer,同4
16 public PrintWriter printf(String format, Object … args) 使用指定的格式字符串和參數將格式化的字符串寫入此writer,同5
17 public void println() 通過向Writer寫入行分隔符字符串來實現換行
18 public void println(boolean x) 向writer中寫入一個boolean基本類型變量並換行
19 public void println(char x) 向writer中寫入一個char基本類型變量並換行
20 public void println(char x[]) 向writer中寫入一個char數組並換行
21 public void println(double x) 向writer中寫入一個double基本類型變量並換行
22 public void println(float x) 向writer中寫入一個float基本類型變量並換行
23 public void println(int x) 向writer中寫入一個int基本類型變量並換行
24 public void println(long x) 向writer中寫入一個long基本類型變量並換行
25 public void println(Object x) 向writer中寫入一個Object對象並換行
26 public void println(String x) 向writer中寫入一個String對象並換行

示例

@SneakyThrows
public static void main(String[] args) {
    List<Student> students = Lists.newArrayList();
    students.add(new Student("zhuoli", 18, 99.0f));
    students.add(new Student("Michael", 19, 90.0f));
    students.add(new Student("Jane", 20, 60.0f));
    try (PrintWriter printWriter = new PrintWriter("d:/hello.txt")) {
        for (Student student : students) {
            printWriter.println(student);
        }
    }

    try (FileReader fileReader = new FileReader("d:/hello.txt");
         BufferedReader bufferedReader = new BufferedReader(fileReader)) {
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            System.out.println(line);
        }
    }
}

運行結果:

Student(name=zhuoli, age=18, grade=99.0)
Student(name=Michael, age=19, grade=90.0)
Student(name=Jane, age=20, grade=60.0)

參考鏈接:

1. Java API

2. 《Java編程的邏輯》

發佈了117 篇原創文章 · 獲贊 33 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章