《疯狂java讲义》学习(37):文件文本和字符流

版权声明:本文为博主原创文章,如若转载请注明出处 https://blog.csdn.net/tonydz0523/article/details/86769843

文本文件和字符流

字节流没有编码的概念,不能按行处理,使用不太方便,更适合的是使用字符流。
文本文件的基本概念、与二进制文件的区别、编码,以及字符流和字节流的区别,然后介绍Java中的主要字符流,它们有:

  1. Reader/Writer:字符流的基类,它们是抽象类;
  2. InputStreamReader/OutputStreamWriter:适配器类,将字节流转换为字符流;
  3. FileReader/FileWriter:输入源和输出目标是文件的字符流;
  4. CharArrayReader/CharArrayWriter:输入源和输出目标是char数组的字符流;
  5. StringReader/StringWriter:输入源和输出目标是String的字符流;
  6. BufferedReader/BufferedWriter:装饰类,对输入/输出流提供缓冲,以及按行读写功能;
  7. PrintWriter:装饰类,可将基本类型和对象转换为其字符串形式输出的类。

除了这些类,Java中还有一个类Scanner,类似于一个Reader,但不是Reader的子类,可以读取基本类型的字符串形式,类似于PrintWriter的逆操作。理解了字节流和字符流后,我们介绍Java中的标准输入输出和错误流。最后,我们总结一些简单的实用方法。

Reader/Writer

Reader与字节流的InputStream类似,也是抽象类,部分主要方法有:

public int read() throws IOException
public int read(char cbuf[]) throws IOException
abstract public void close() throws IOException
public long skip(long n) throws IOException
public boolean ready() throws IOException

方法的名称和含义与InputStream中的对应方法基本类似,但Reader中处理的单位是char,比如read读取的是一个char,取值范围为0~65535。Reader没有available方法,对应的方法时ready()。
Writer与字节流的OutputStream类似,也是抽象类,部分主要方法有:

public void write(int c)
public void write(char cbuf[])
public void write(String str) throws IOException
abstract public void close() throws IOException;
abstract public void flush() throws IOException;

含义与OutputStream的对应方法基本类似,但Writer处理的单位是char,Writer还接受String类型,我们知道,String的内部就是char数组,处理时,会调用String的getChar方法先获取char数组。

InputStreamReader/OutputStreamWriter

InputStreamReader和OutputStreamWriter是适配器类,能将InputStream/OutputStream转换为Reader/Writer。

1.OutputStreamWriter

OutputStreamWriter的主要构造方法为:

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

一个重要的参数是编码类型,可以通过名字charsetName或Charset对象传入,如果没有传入,则为系统默认编码,默认编码可以通过Charset.defaultCharset()得到。OutputStreamWriter内部有一个类型为StreamEncoder的编码器,能将char转化为对应编码的字节。

Writer writer = new OutputStreamWriter(
        new FileOutputStream("hello.txt"), "GB2312");
try{
    String str = "ffzs";
    writer.write(str);
}finally{
    writer.close();
}

创建一个FileOutputStream,然后将其包在一个OutputStreamWriter中,就可以直接以字符串写入了。

2.InputStreamReader

InputStreamReader的主要构造方法为:

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

与OutputStreamWriter一样,一个重要的参数是编码类型。InputStreamReader内部有一个类型为StreamDecoder的解码器,能将字节根据编码转换为char。
我们看一段简单的代码,将上面写入的文件读进来:

Reader reader = new InputStreamReader(
        new FileInputStream("hello.txt"), "GB2312");
try{
    char[] cbuf = new char[1024];
    int charsRead = reader.read(cbuf);
    System.out.println(new String(cbuf, 0, charsRead));
}finally{
    reader.close();
}

这段代码假定一次read调用就读到了所有内容,且假定长度不超过1024。为了确保读到所有内容,可以借助CharArrayWriter或StringWriter。

FileReader/FileWriter

FileReader/FileWriter的输入和目的是文件。FileReader是InputStreamReader的子类,它的主要构造方法有:

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

FileWriter是OutputStreamWriter的子类,它的主要构造方法有:

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

append参数指定是追加还是覆盖,如果没传,则为覆盖。
需要注意的是,FileReader/FileWriter不能指定编码类型,只能使用默认编码,如果需要指定编码类型,可以使用InputStreamReader/OutputStreamWriter。

CharArrayReader/CharArrayWriter

CharArrayWriter与ByteArrayOutputStream类似,它的输出目标是char数组,这个数组的长度可以根据数据内容动态扩展。
CharArrayWriter有如下方法,可以方便地将数据转换为char数组或字符串:

public char[] toCharArray()
public String toString()

使用CharArrayWriter,我们可以改进上面的读文件代码,确保将所有文件内容读入:

Reader reader = new InputStreamReader(
        new FileInputStream("hello.txt"), "GB2312");
try{
    CharArrayWriter writer = new CharArrayWriter();
    char[] cbuf = new char[1024];
    int charsRead = 0;
    while((charsRead=reader.read(cbuf))! =-1){
        writer.write(cbuf, 0, charsRead);
    }
    System.out.println(writer.toString());
}finally{
    reader.close();
}

读入的数据先写入CharArrayWriter中,读完后,再调用其toString()方法获取完整数据。
CharArrayReader与上节介绍的ByteArrayInputStream类似,它将char数组包装为一个Reader,是一种适配器模式,它的构造方法有:

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

StringReader/StringWriter

StringReader/StringWriter与CharArrayReader/CharArrayWriter类似,只是输入源为String,输出目标为StringBuffer,而且,String/StringBuffer内部是由char数组组成的,所以它们本质上是一样的,具体我们就不赘述了。之所以要将char数组和String与Reader/Writer进行转换,也是为了能够方便地参与Reader/Writer构成的协作体系,复用代码。

BufferedReader/BufferedWriter

BufferedReader/BufferedWriter是装饰类,提供缓冲,以及按行读写功能。Buffered-Writer的构造方法有:

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

参数sz是缓冲大小,如果没有提供,默认为8192。它有如下方法,可以输出平台特定的换行符:

public void newLine() throws IOException

BufferedReader的构造方法有:

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

参数sz是缓冲大小,如果没有提供,默认8192。它有如下方法,可以读入一行:

public String readLine() throws IOException

字符’\r’或’\n’或’\r\n’被视为换行符,readLine返回一行内容,但不会包含换行符,当读到流结尾时,返回null。
FileReader/FileWriter是没有缓冲的,也不能按行读写,所以,一般应该在它们的外面包上对应的缓冲类。我们来看个例子,还是学生列表,这次我们使用可读的文本进行保存,一行保存一条学生信息,学生字段之间用逗号分隔,保存的代码为:

public static void writeStudents(List<Student> students) throws IOException{
    BufferedWriter writer = null;
    try{
        writer = new BufferedWriter(new FileWriter("students.txt"));
        for(Student s : students){
            writer.write(s.getName()+", "+s.getAge()+", "+s.getScore());
            writer.newLine();
        }
    }finally{
        if(writer! =null){
            writer.close();
        }
    }
}

从文件中读取的代码为:

public static List<Student> readStudents() throws IOException{
    BufferedReader reader = null;
    try{
        reader = new BufferedReader(
                new FileReader("students.txt"));
        List<Student> students = new ArrayList<>();
        String line = reader.readLine();
        while(line! =null){
            String[] fields = line.split(", ");
            Student s = new Student();
            s.setName(fields[0]);
            s.setAge(Integer.parseInt(fields[1]));
            s.setScore(Double.parseDouble(fields[2]));
            students.add(s);
            line = reader.readLine();
        }
        return students;
    }finally{
        if(reader! =null){
            reader.close();
        }
    }
}

使用readLine读入每一行,然后使用String的方法分隔字段,再调用Integer和Double的方法将字符串转换为int和double。这种对每一行的解析可以使用类Scanner进行简化。

PrintWriter

PrintWriter有很多重载的print方法,如:

public void print(int i)
public void print(Object obj)

它会将这些参数转换为其字符串形式,即调用String.valueOf(),然后再调用write。它也有很多重载形式的println方法,println除了调用对应的print,还会输出一个换行符。除此之外,PrintWriter还有格式化输出方法,如:

public PrintWriter printf(String format, Object ... args)

format表示格式化形式,比如,保留小数点后两位,格式可以为:

PrintWriter writer = …
writer.format("%.2f", 123.456f);

输出为:

123.45

更多格式化的内容可以参看API文档,本节就不赘述了。
PrintWriter的方便之处在于,它有很多构造方法,可以接受文件路径名、文件对象、OutputStream、Writer等,对于文件路径名和File对象,还可以接受编码类型作为参数,比如:

public PrintWriter(File file) throws FileNotFoundException
public PrintWriter(String fileName, String csn)
public PrintWriter(OutputStream out, boolean autoFlush)
public PrintWriter(Writer out)

参数csn表示编码类型,对于以文件对象和文件名为参数的构造方法,PrintWriter内部会构造一个BufferedWriter,比如:

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

对于以OutputSream为参数的构造方法,PrintWriter也会构造一个BufferedWriter,比如:

public PrintWriter(OutputStream out, boolean autoFlush) {
    this(new BufferedWriter(new OutputStreamWriter(out)), autoFlush);}

对于以Writer为参数的构造方法,PrintWriter就不会包装BufferedWriter了。
构造方法中的autoFlush参数表示同步缓冲区的时机,如果为true,则在调用println、printf或format方法的时候,同步缓冲区,如果没有传,则不会自动同步,需要根据情况调用flush方法。
可以看出,PrintWriter是一个非常方便的类,可以直接指定文件名作为参数,可以指定编码类型,可以自动缓冲,可以自动将多种类型转换为字符串,在输出到文件时,可以优先选择该类。
上面的保存学生列表代码,使用PrintWriter,可以写为:

public static void writeStudents(List<Student> students) throws IOException{
    PrintWriter writer = new PrintWriter("students.txt");
    try{
        for(Student s : students){
            writer.println(s.getName()+", "+s.getAge()+", "+s.getScore());
        }
    }finally{
        writer.close();
    }
}

PrintWriter有一个非常相似的类PrintStream,除了不能接受Writer作为构造方法外, PrintStream的其他构造方法与PrintWriter一样。PrintStream也有几乎一样的重载的print和println方法,只是自动同步缓冲区的时机略有不同,在PrintStream中,只要碰到一个换行字符’\n’,就会自动同步缓冲区。PrintStream与PrintWriter的另一个区别是,虽然它们都有如下方法:

public void write(int b)

但含义是不一样的,PrintStream只使用最低的8位,输出一个字节,而PrintWriter是使用最低的两位,输出一个char。

Scanner

Scanner是一个单独的类,它是一个简单的文本扫描器,能够分析基本类型和字符串,它需要一个分隔符来将不同数据区分开来,默认是使用空白符,可以通过useDelimiter()方法进行指定。Scanner有很多形式的next()方法,可以读取下一个基本类型或行,如:

public float nextFloat()
public int nextInt()
public String nextLine()

Scanner也有很多构造方法,可以接受File对象、InputStream、Reader作为参数,它也可以将字符串作为参数,这时,它会创建一个StringReader。比如,以前面的解析学生记录为例,使用Scanner,代码可以改为:

public static List<Student> readStudents() throws IOException{
    BufferedReader reader = new BufferedReader(
            new FileReader("students.txt"));
    try{
        List<Student> students = new ArrayList<Student>();
        String line = reader.readLine();
        while(line! =null){
            Student s = new Student();
            Scanner scanner = new Scanner(line).useDelimiter(", ");
            s.setName(scanner.next());
            s.setAge(scanner.nextInt());
            s.setScore(scanner.nextDouble());
            students.add(s);
            line = reader.readLine();
        }
        return students;
    }finally{
        reader.close();
    }
}

标准流

我们之前一直在使用System.out向屏幕上输出,它是一个PrintStream对象,输出目标就是所谓的“标准”输出,经常是屏幕。除了System.out, Java中还有两个标准流:System. in和System.err。
System.in表示标准输入,它是一个InputStream对象,输入源经常是键盘。比如,从键盘接受一个整数并输出,代码可以为:

Scanner in = new Scanner(System.in);
int num = in.nextInt();
System.out.println(num);

System.err表示标准错误流,一般异常和错误信息输出到这个流,它也是一个Print-Stream对象,输出目标默认与System.out一样,一般也是屏幕。
标准流的一个重要特点是,它们可以重定向,比如可以重定向到文件,从文件中接受输入,输出也写到文件中。在Java中,可以使用System类的setIn、setOut、setErr进行重定向,比如:

System.setIn(new ByteArrayInputStream("hello".getBytes("UTF-8")));
System.setOut(new PrintStream("out.txt"));
System.setErr(new PrintStream("err.txt"));
try{
    Scanner in = new Scanner(System.in);
    System.out.println(in.nextLine());
    System.out.println(in.nextLine());
}catch(Exception e){
    System.err.println(e.getMessage());
}

标准输入重定向到了一个ByteArrayInputStream,标准输出和错误重定向到了文件,所以第一次调用in.nextLine就会读取到"hello",输出文件out.txt中也包含该字符串,第二次调用in.nextLine会触发异常,异常消息会写到错误流中,即文件err.txt中会包含异常消息,为"No line found"。
在实际开发中,经常需要重定向标准流。比如,在一些自动化程序中,经常需要重定向标准输入流,以从文件中接受参数,自动执行,避免人手工输入。在后台运行的程序中,一般都需要重定向标准输出和错误流到日志文件,以记录和分析运行的状态和问题。
在Linux系统中,标准输入输出流也是一种重要的协作机制。很多命令都很小,只完成单一功能,实际完成一项工作经常需要组合使用多条命令,它们协作的模式就是通过标准输入输出流,每个命令都可以从标准输入接受参数,处理结果写到标准输出,这个标准输出可以连接到下一个命令作为标准输入,构成管道式的处理链条。比如,查找一个日志文件access.log中127.0.0.1出现的行数,可以使用命令:

cat access.log | grep 127.0.0.1 | wc -l

有三个程序cat、grep、wc, |是管道符号,它将cat的标准输出重定向为了grep的标准输入,而grep的标准输出又成了wc的标准输入。

实用方法

可以看出,字符流也包含了很多的类,虽然很灵活,但对于一些简单的需求,却需要写很多代码,实际开发中,经常需要将一些常用功能进行封装,提供更为简单的接口。下面我们提供一些实用方法,以供参考,代码比较简单,就不解释了。
复制Reader到Writer,代码为:

public static void copy(final Reader input,
        final Writer output) throws IOException {
    char[] buf = new char[4096];
    int charsRead = 0;
    while((charsRead = input.read(buf)) ! = -1) {
        output.write(buf, 0, charsRead);
    }
}

将文件全部内容读入到一个字符串,参数为文件名和编码类型,代码为:

public static String readFileToString(final String fileName,
        final String encoding) throws IOException{
    BufferedReader reader = null;
    try{
        reader = new BufferedReader(new InputStreamReader(
                new FileInputStream(fileName), encoding));
        StringWriter writer = new StringWriter();
        copy(reader, writer);
        return writer.toString();
    }finally{
        if(reader! =null){
            reader.close();
        }
    }
}

这个方法利用了StringWriter,并调用了上面的复制方法。
将字符串写到文件,参数为文件名、字符串内容和编码类型,代码为:

public static void writeStringToFile(final String fileName,
        final String data, final String encoding) throws IOException {
    Writer writer = null;
    try{
        writer = new OutputStreamWriter(
                    new FileOutputStream(fileName), encoding);
        writer.write(data);
    }finally{
        if(writer! =null){
            writer.close();
        }
    }
}

按行将多行数据写到文件,参数为文件名、编码类型、行的集合,代码为:

public static void writeLines(final String fileName, final String encoding,
    final Collection<? > lines) throws IOException {
    PrintWriter writer = null;
    try{
        writer = new PrintWriter(fileName, encoding);
        for(Object line : lines){
            writer.println(line);
        }
    }finally{
        if(writer! =null){
            writer.close();
        }
    }
}

按行将文件内容读到一个列表中,参数为文件名、编码类型,代码为:

public static List<String> readLines(final String fileName,
        final String encoding) throws IOException{
    BufferedReader reader = null;
    try{
        reader = new BufferedReader(new InputStreamReader(
                new FileInputStream(fileName), encoding));
        List<String> list = new ArrayList<>();
        String line = reader.readLine();
        while(line! =null){
            list.add(line);
            line = reader.readLine();
        }
        return list;
    }finally{
        if(reader! =null){
            reader.close();
        }
    }
}

写文件时,可以优先考虑PrintWriter,因为它使用方便,支持自动缓冲、指定编码类型、类型转换等。读文件时,如果需要指定编码类型,需要使用InputStreamReader;如果不需要指定编码类型,可使用FileReader,但都应该考虑在外面包上缓冲类Buffered-Reader。

#Java实例练习

以字符为单位读取文件内容

本实例我们将为大家演示如何以字符为单位读取文件内容。以字符为单位读取文件,常用于读取文本、数字等类型的文件。要得到标准输入的字节流,然后转换成字符流,用经过缓冲得到标准的输入流。

1.

新建项目CharsReadFile,并在其中创建一个CharsReadFile.java文件。在该类的主方法中使用FileReader()方法去读取指定文件的内容并输出:

public class CharsReadFile{                             // 以字符为单位读取文件
    public static void readFileByChars(String fileName){
        File file = new File(fileName);                 // 创建文件
        Reader read = null;
        try {
            System.out.println("以字符为单位读取文件内容,一次读一个字节:");
            // 一次读一个字符
            read = new FileReader (new FileReader (file));
            int tempchar;
            while ((tempchar = read.read()) != -1) {
                  /* 对于windows下,rn这两个字符在一起时,表示一个换行。
                  但如果这两个字符分开显示时,会换两次行。
                  因此,屏蔽掉r,或者屏蔽n。否则,将会多出很多空行。*/
                  if (((char) tempchar) != 'r') {            // 只要是不换行就读取
                      System.out.print((char) tempchar);
                  }
            }
            read.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            System.out.println("\n以字符为单位读取文件内容,一次读多个字节:");
            char[] tempchars = new char[30];
            int charread = 0;
            read = new FileReader(new FileReader(fileName));    // 创建文件读入流
            while ((charread = read.read(tempchars)) != -1) {   // 一次读多个字符
                  // 同样屏蔽掉r不显示
                  if ((charread == tempchars.length)
                          && (tempchars[tempchars.length - 1] != 'r'))
                      System.out.print(tempchars);
                  } else {
                      for (int i = 0; i < charread; i++) {
                          if (tempchars[i] == 'r') {
                              continue;    // 停止执行当前的迭代,然后退回循环开始处
                          } else {
                              System.out.print(tempchars[i]);
                          }
                      }
                  }
            }
        } catch (Exception e1) {                        // 捕获异常
            e1.printStackTrace();
        } finally {                                     // 内容总执行
            if (read != null) {
                  try {
                      read.close();                      // 确保关闭
                  } catch (IOException e1) {
                  }
            }
        }
    }
    public static void main(String[] args) {
        String fileName = "D:/text.txt";
        System.out.println("按字符为单位读取文件:");
        readFileByChars(fileName);
    }
}

FileReader和FileReader类似,所不同的是它是针对字符进行操作,而不是字节。它的间接父类是字符流Reader。FileWriter是用于写入字符文件的便捷类。在FileReader类中未自定义方法,而是继承了其父类及间接父类中的方法。

以行为单位读取文件内容

本实例我们将为大家演示如何以行为单位读取文件内容。以行为单位读取文件,常用于读面向行的格式化文件。

1.

新建项目LinesReadFile,并在其中创建一个LinesReadFile.java文件。在该类的主方法中使用BufferedReader()方法去读取指定文件的内容并输出:

public class LinesReadFile {                            // 以行为单位读取文件
    public static void readFileByLines(String fileName){
        File file = new File(fileName);
        BufferedReader reader = null;                   // 创建缓存读取
        try {
            System.out.println("以行为单位读取文件内容,一次读一整行:");
            reader = new BufferedReader(new FileReader(file));
                                                      // 将文件放在缓存读取中
            String tempString = null;
            int line = 1;
            // 一次读入一行,直到读入null为文件结束
            while ((tempString = reader.readLine()) != null){   // 显示行号
                  System.out.println("line " + line + ": " + tempString);
                  line++;
            }
            reader.close();
        } catch (IOException e) {                       // 捕获异常
            e.printStackTrace();
        } finally {                                     // 内容总执行
            if (reader != null) {
                  try {
                      reader.close();                    // 关闭缓存读取
                  } catch (IOException e1) {
                  }
            }
        }
    }
    public static void main(String[] args) {
        String fileName = "D:/text.txt";
        System.out.println("按行为单位读取文件:");
        readFileByLines(fileName);
    }
}

BufferedReader类从字符输入流中读取文本,缓冲各个字符,从而提供字符、数组和行的高效读取。Java中,控制台输入由从System.in读取数据来完成。BufferedReader支持缓冲输入流。

猜你喜欢

转载自blog.csdn.net/tonydz0523/article/details/86769843