coding……
但行好事 莫问前程

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.方法说明
1public int read() throws IOException从字符输入流Reader中读取一个字符,返回值为读取字符对应的int值,返回-1表示字符流中的内容已经读取结束
2public int read(char cbuf[]) throws IOException从字符输入流Reader中读取多个字节,放入字符数组cbuf中,返回值为实际读入的字符个数
3abstract public int read(char cbuf[], int off, int len) throws IOException从字符输入流Reader中读取len个字节放入字符数组cbuf index off开始的位置, 返回值为实际读入的字符个数
4public boolean ready() throws IOException返回字符输入流Reader下一次read()操作是否需要阻塞,如果需要阻塞返回true,否则返回false。跟InputStream中的available方法类似。
5public long skip(long n) throws IOException跳过字符输入流Reader n个字符,返回值为实际跳过的字符个数
6public boolean markSupported()判断当前字符输入流是否支持mark/reset操作
7public void mark(int readAheadLimit) throws IOException标记能够从字符输入流中往后读取的字符个数readAheadLimit
8public void reset() throws IOException重新从标记位置读取字符输入流
9abstract public void close() throws IOException关闭字符输入流,释放资源

方法大体上跟InputStream很类似,不同的是,InputStream读取的单位是字节,Reader读取的单位是char。

1.1 Writer

S.N.方法说明
1public void write(int c) throws IOException向字符输出流中写入一个字符
2public void write(char cbuf[]) throws IOException将字符数组cbuf中所有的字符写入字符输出流
3abstract public void write(char cbuf[], int off, int len) throws IOException将字符数组cbuf中从index off开始,长度为len的字符写入字符输出流
4public void write(String str) throws IOException将字符串str中所有的字符写入到字符输出流
5public void write(String str, int off, int len) throws IOException将字符串str中index 从off开始长度为len的所有字符写入到字符输出流
6public Writer append(char c) throws IOException操作等同于write(int c),向字符输出流中写入一个字符
7public Writer append(CharSequence csq) throws IOException操作等同于write(String str),将CharSequence中所有的字符写入到字符输出流
8public Writer append(CharSequence csq, int start, int end) throws IOException将CharSequence中index再[start, end)区间内所有的字符写入到字符输出流
9abstract public void flush() throws IOException将缓冲而未实际写的数据进行实际写入
10abstract 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.方法说明
1public PrintWriter (Writer out)装饰模式,将普通字符输出流包装为PrintWriter
2public PrintWriter(Writer out, boolean autoFlush)装饰模式,将普通字符输出流包装为PrintWriter,autoFlush表示同步缓冲区的时机,如果为true,则在调用print、printf、format方法时会同步缓冲区,否则需要根据情况调用flush方法
3public PrintWriter(OutputStream out)适配器模式,将字节输出流转化为PrintWriter
4public PrintWriter(OutputStream out, boolean autoFlush)适配器模式,将字节输出流转化为PrintWriter,autoFlush参数含义同上
5public PrintWriter(String fileName) throws FileNotFoundException构造输出目标时文件的PrintWriter,参数fileName表示文件路径
6public PrintWriter(String fileName, String csn)构造输出目标时文件的PrintWriter,参数fileName表示文件路径,参数csn表示字符集,用于指定输出到文件时的编码,上面没指定字符集时,表示使用系统默认字符集
7public PrintWriter(File file) throws FileNotFoundException构造输出目标时文件的PrintWriter,参数file为文件对象
8public 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.
方法说明
1public PrintWriter append(char c)向writer中追加一个char
2public PrintWriter append(CharSequence csq)向writer中追加一个CharSequence
3public PrintWriter append(CharSequence csq, int start, int end)将CharSequence c从index start到index end的子串追加到writer中
4public PrintWriter format(Locale l, String format, Object … args)使用指定的格式字符串和参数将格式化的字符串写入此writer。 如果启用了自动刷新,则调用此方法将刷新输出缓冲区。l表示格式化过程中使用的语言环境
5public PrintWriter format(String format, Object … args)同上format方法,只是没指定语言环境,而使用默认语言环境
6public void print(boolean b)向writer中写入一个boolean基本类型变量,如果为true,写入字符串”true”,否则写入”false”
7public void print(char c)向writer中写入一个char基本类型变量
8public void print(char s[])向writer中写入一个char数组
9public void print(double d)向writer中写入一个double基本类型变量,写入的实际是调用String.valueOf(d)返回的字符串
10public void print(float f)向writer中写入一个float基本类型变量,写入的实际是调用String.valueOf(f)返回的字符串
11public void print(int i)向writer中写入一个int基本类型变量,写入的实际是调用String.valueOf(i)返回的字符串
12public void print(long l)向writer中写入一个long基本类型变量,写入的实际是调用String.valueOf(l)返回的字符串
13public void print(Object obj)向writer中写入一个Object对象,写入的实际是调用String.valueOf(obj)返回的字符串
14public void print(String s)向writer中写入一个String对象,如果s为null,写入”null”
15public PrintWriter printf(Locale l, String format, Object … args)使用指定的格式字符串和参数将格式化的字符串写入此writer,同4
16public PrintWriter printf(String format, Object … args)使用指定的格式字符串和参数将格式化的字符串写入此writer,同5
17public void println()通过向Writer写入行分隔符字符串来实现换行
18public void println(boolean x)向writer中写入一个boolean基本类型变量并换行
19public void println(char x)向writer中写入一个char基本类型变量并换行
20public void println(char x[])向writer中写入一个char数组并换行
21public void println(double x)向writer中写入一个double基本类型变量并换行
22public void println(float x)向writer中写入一个float基本类型变量并换行
23public void println(int x)向writer中写入一个int基本类型变量并换行
24public void println(long x) 向writer中写入一个long基本类型变量并换行
25public void println(Object x)向writer中写入一个Object对象并换行
26public 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编程的逻辑》

赞(3) 打赏
Zhuoli's Blog » Java编程拾遗『字符流』
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址