文本文件和字符流
字节流没有编码的概念,不能按行处理,使用不太方便,更适合的是使用字符流。
文本文件的基本概念、与二进制文件的区别、编码,以及字符流和字节流的区别,然后介绍Java中的主要字符流,它们有:
- Reader/Writer:字符流的基类,它们是抽象类;
- InputStreamReader/OutputStreamWriter:适配器类,将字节流转换为字符流;
- FileReader/FileWriter:输入源和输出目标是文件的字符流;
- CharArrayReader/CharArrayWriter:输入源和输出目标是char数组的字符流;
- StringReader/StringWriter:输入源和输出目标是String的字符流;
- BufferedReader/BufferedWriter:装饰类,对输入/输出流提供缓冲,以及按行读写功能;
- 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支持缓冲输入流。