尽管可以使用File进行文件的操作,但是如果要进行文件内容的操作,在Java之中就必须通过两类流操作完成。 Java的流操作分为字节流和字符流两种。
字符流处理的对象单元是Unicode字符,每个Unicode字符占据2个字节,而字节流输入输出的数据是以单个字节(Byte)为读写单位。这种流操作方式给操作一些双字节字符带来了困难。字符流是由Java虚拟机将单个字节转化为2个字节的Unicode字符,所以它对多国语言支持较好。
若将一段二进制数据(如音频、视频和图像)写入某个设备,或从某个设备读出,使用字节流操作进行读写更加方便。
若是读写一段文本,则使用字节流进行操作,读取时将文本以字节流的方式读入,如果要将字节显示为字符,就需要使用字节和字符之间的转换。需要一个直接用于操作文本数据的I/O类——字符流。字符流将字节流进行包装,接受字符串输入,并在底层将字符转换为字节。Java 的流式输入/输出建立在4个抽象类的基础上:InputStream(字节输入流)、OutputStream(字节输出流)、Reader(字符输入流)和Writer(字符输出流)。
通常来说,处理字符或字符串时应使用字符流类,处理字节或二进制对象时应使用字节流类。
一般在操作文件流时,不管是字节流还是字符流,都可以按照如下的流程进行:
(1)使用File类找到一个要操作的文件路径;
(2)通过File类的对象去实例化字节流或字符流的子类;
(3)进行字节(字符)的读/写操作;
(4)IO流属于资源操作,操作的最后必须关闭。
一、字节流
字节流类为处理字节式输入/输出提供了丰富的环境。一个字节流可以与其他任何类型的对象并用,包括二进制数据。 这样的多功能性使得字节流对很多类型的程序都很重要。
字节流包含两个顶层抽象类:InputStream和OutputStream。所有的读操作都继承自一个公共超类java.io.InputStream类。所有的写操作都继承自一个公共超类java.io.OutputStream类。
这两个抽象类都由不同的子类来具体实现某项“个性化”的功能,完成不同类型设备的输入和输出。下表列出常用的字节流名称及对应功能的简单介绍。
1、字节输出流——OutputStream
如果要通过程序输出内容到文件中,则必须使用OutputStream类完成, 它是一个抽象类,它定义了流式字节输出模式,该类的所有方法返回一个void 值,并且在出错的情况下,会抛出一个IOException异常。
这个类的定义如下:
public abstract class OutputStream extends Object implements Closeable, Flushable
可以发现OutputStream类之中实现了两个接口,这两个接口定义如下:
public interface Closeable extends AutoCloseable {
public void close() throws IOException;
}
public interface Flushable {
public void flush() throws IOException;
}
一般而言,很少去关心Closeable和Flushable两个接口,因为OutputStream类是在JDK 1.0的时候就定义的,而上面的两个接口是在JDK 1.5的时候才定义的,人们所关心的不是这两个接口,而是直接观察OutputStream类中定义的方法,下表中显示了OutputStream的方法:
方法 | 功能 |
void close() | 关闭输出流,关闭后的写操作会产生IOException异常 |
void flush() | 刷新此输出流并强制写出所有缓冲的输出字节 |
abstract void write(int b) throws IOException | 向输出流写入单个字节。注意参数是一个整型数,它允许设计者不必把参数转换成字节型就可以调用write()方法 |
abstract void write(byte[ ] b) throws IOException | 向一个输出流写一个完整的字节数组 |
void write(byte[ ] b, int off, int len) throws IOException | 写数组b以b[off]为起点的len个字节区域内的内容 |
上表中的多数方法由OutputStream的子类来实现。下面以其子类FileOutputStream为例来讨论这些方法的使用和不使用的情况:
对于OutputStream类而言,其本身是一个抽象类,按照面向对象的概念来解释的话,对于抽象类要想实例化必须通过子类完成,如果说现在要操作的是文件的输出,则可以使用子类FileOutputStream类完成。
FileOutputStream 创建了一个可以向文件写入字节的类OutputStream,它常用的构造方法如下所示:
FileOutputStream(String filePath) //创建新的文件输出
FileOutputStream(File fileObj) //创建新的文件输出
FileOutputStream(String filePath, booleanappend) //在原有文件基础上追加数据
如果发生打开文件失败等意外,它们都可以引发IOException或SecurityException异常。
在这里, filePath是文件的绝对路径,fileObj是描述该文件的File对象。如果参数append为true,文件则是以设置搜索路径模式打开,在原有文件基础上追加数据。FileOutputStream的创建不依赖于文件是否存在。在创建对象时,FileOutputStream会在打开输出文件之前就创建它。在这种情况下如果试图打开一个只读文件,则会引发一个IOException异常。
2、字节输入流——InputStream
InputStream 是一个定义了Java流式字节输入模式的抽象类,该类的所有方法在出错时都会引发一个IOException 异常。
下表中显示了InputStream的方法:
FileInputStream 类创建一个能从文件读取字节的InputStream 类,它的两个常用的构造方法如下:
FileInputStream(String filepath) //构造方法1
FileInputStream(File fileObj) //造方法2
这两个构造方法都能引发FileNotFoundException异常。在这里filepath 是文件的绝对路径,fileObj是描述该文件的File对象。
下面的例子创建了两个使用同样磁盘文件且各含一个上面所描述的构造方法的FileInputStream类:
InputStream f0 = new FileInputStream("d:\\test.txt") ;
File f = new File("d:\\test.txt");
InputStream f1 = new FileInputStream(f);
尽管第1个构造方法可能更常用到,但第2个构造方法可允许在把文件赋给输入流之前用File方法更进一步检查文件。当一个FileInputStream被创建时,它可被公开读取。
在下面的综合例子中,首先用FileOutputStream类向文件中写入一个字符串,然后用FileInputStream读出写入的内容。
下面以InputStream 的子类FileInputStream(文件输入流)为例说明上述部分方法的使用:
package com.xy.io;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class StreamDemo {
public static void main(String[] args) {
File f = new File("C:\\Users\\XY\\Desktop\\temp.txt");
OutputStream out = null;
try {
out = new FileOutputStream(f);
}
catch(FileNotFoundException e) {
e.printStackTrace();
}
// 将字符串转成字节数组
byte[] b = "Hello World!".getBytes();
try {
// 将byte数组写入到文件中
out.write(b);
}
catch(IOException e) {
e.printStackTrace();
}
try {
out.close();
}
catch(IOException e) {
e.printStackTrace();
}
// 以下为读文件
InputStream in = null;
try {
in = new FileInputStream(f);
}
catch(FileNotFoundException e) {
e.printStackTrace();
}
// 开辟一个空间用于接收文件读进来的数据
byte[] b1 = new byte[1024];
int i = 0;
try {
// 将b1的引用传递到read()方法之中,此方法返回读入数据的个数
i = in.read(b1);
}
catch(IOException e) {
e.printStackTrace();
}
try {
in.close();
}
catch(IOException e) {
e.printStackTrace();
}
// 将byte数组转换为字符串输出
System.out.println(new String(b1, 0, i));
}
}
【结果】
二、字符流
尽管字节流提供了处理任何类型输入/输出操作的足够的功能,但它们不能直接操作Unicode字符。
既然Java的一个主要目标是支持“一次编写,处处运行”,那么支持多国语言字符的直接输入/输出是必要的。在这个方面上,Java中的Writer类有着重要的支撑作用。下面将从Writer抽象类开始,介绍字符输出流及其相关子类的一些方法。
1、字符输出流——Writer
Writer 是定义流式字符输出的抽象类,所有该类的方法都返回一个void 值并在出错的条件下引发IOException 异常。
表中给出了Writer类中方法:
下面来说明Writer抽象类的子类FileWriter的一些特性。
FileWriter 创建一个可以写文件的Writer 类。它最常用的3个构造方法如下所示:
FileWriter(String fileName)
FileWriter(File file)
FileWriter(String fileName, boolean append)
它们可以引发IOException或SecurityException异常。在这里fileName是包括文件名的绝对路径,file是描述该文件的File类的对象。如果布尔类型的append为true,则输出的内容附加到文件尾的。FileWriter类的创建不依赖于文件存在与否。在创建文件之前,FileWriter将在创建对象时打开它来作为输出。如果试图打开一个只读文件,将引发一个IOException异常。
2、字符输入流——Reader
Reader是定义Java的流式字符输入模式的抽象类。
Reader是专门进行输入数据的字符操作流,这个类的定义如下:
public abstract class Reader extends Object implements Readable, Closeable
在Reader类之中也定义了若干个读取数据的方法,该类的所有方法在出错的情况下都将引发IOException 异常。下表中给出了Reader类中的主要方法:
由于Reader类是抽象类,所以要通过文件读取时,肯定使用的是FileReader子类,FileReader子类创建了一个可读取文件内容的Reader类。它最常用的构造方法如下:
FileReader(String filePath) throws FileNotFoundException;
FileReader(File fileObj) throws FileNotFoundException;
每一个构造方法在无法找到打开的文件时,都会引发一个FileNotFoundException异常。在这里filePath是一个文件的完整路径,fileObj是描述该文件的File对象。*
【示例】
package com.xy.io;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
public class CharDemo {
public static void main(String[] args) throws IOException {
File f = new File("C:\\Users\\XY\\Desktop\\temp.txt");
Writer out = null;
try {
out = new FileWriter(f); // 向上转型
}
catch(FileNotFoundException e) {
e.printStackTrace();
}
// 声明一个String对象
String str = "Hello World!";
try {
// 将byte数组写入到文件中
out.write(str);
}
catch(IOException e) {
e.printStackTrace();
}
try {
out.close();
}
catch(IOException e) {
e.printStackTrace();
}
// 以下为读文件
Reader in = null;
try {
in = new FileReader(f);
}
catch(FileNotFoundException e) {
e.printStackTrace();
}
// 开辟一个空间用于接收文件读进来的数据
char[] c = new char[1024];
int i = 0;
try {
// 将b1的引用传递到read()方法之中,此方法返回读入数据的个数
i = in.read(c); // 将文件内容读到char数组中,并返回个数
}
catch(IOException e) {
e.printStackTrace();
}
try {
in.close();
}
catch(IOException e) {
e.printStackTrace();
}
// 将byte数组转换为字符串输出
System.out.println(new String(c, 0, i));
}
}
【结果】
【注意】
读者可以将示例中的第30~35行注释掉,也就是说在向文件写入内容之后不关闭文件,然后直接打开文件,可以发现文件中没有任何内容,这是为什么?从JDK文档之中查找FileWriter类,如下图所示:
由上图可以看到,FileWriter类并不是直接继承自Writer类,而是继承了Writer的子类(OutputStreamWriter),此类为字节流和字符流的转换类。也就是说真正从文件中读取进来的数据还是字节,只是在内存中将字节转换成了字符。
第31行的“out.close()”的“关闭字符流”功能,可以完成将内存缓冲区的转换好的字符流,刷新输出至(外存储器的)文件中。
字符流的操作多了一个中间环节—用到了缓冲区,而字节流没有用到缓冲区,直接对文件“实时”操作。 另外,也可以用Writer类中的flush()方法强制清空缓冲区,也就是说,将第31行换成“out.flush();”,也可以保证D盘的“temp.txt”有输出的数据。
三、字节流与字符流的转换
在字节流和字符流之间转换,有两个类:
(1)字节输入流变为字符输入流:InputStreamReader;
(2)字节输出流变为字符输出流:OutputStreamWriter。InputStreamReader用于将一个字节流中的字节解码成字符,OutputStreamWriter用于将写入的字符编码成字节后写入一个字节流。
InputStreamReader和OutputStreamWriter的主要构造方法:
// 用默认字符集创建一个InputStreamReader对象
InputStreamReader(InputStream in)
// 接收已指定字符集名的字符串,并用该字符集创建对象
InputStreamReader(InputStream in,String CharsetName)
// 用默认字符集创建一个OutputStreamWriter对象
OutputStreamWriter(OutputStream in)
// 接收已指定字符集名的字符串,并用该字符集创建OutputStreamWriter对象
OutputStreamWriter(OutputStream in,String CharsetNarme)
为了达到较高的转换效率,避免频繁地进行字符与字节间的相互转换,建议最好不要直接使用这两个类来进行读写,而应尽量使用BufferedWriter类包装OutputStreamWriter类,用BufferedReader类包装InputStreamReader类。
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
然后,从一个实际的应用中来了解InputStreamReader的作用。怎样用一种简单的方式一下子就读取到键盘上输入的一整行字符呢?只要用下面的两行程序代码就可以解决这个问题:
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String strLine = in.readLine();
可见,构建BufferedReader对象时,必须传递一个Reader类型的对象作为参数,而键盘对应的System.in是一个InputStream类型的对象,所以这里需要用到一个InputStreamReader的转换类,将System.in转换成字符流之后,放入到字符流缓冲区之中,之后从缓冲区中每次读入一行数据。
【示例】
package com.xy.io;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class BufferReaderDemo {
public static void main(String[] args) {
BufferedReader buf = null;
buf = new BufferedReader(new InputStreamReader(System.in));
String str = null;
while(true) {
System.out.println("请输入数字:");
try {
str = buf.readLine();
}
catch(IOException e) {
e.printStackTrace();
}
int i = -1;
try {
i = Integer.parseInt(str);
i++;
System.out.println("输入的数字修改后为:" + i);
break;
}
catch(Exception e) {
System.out.println("输入的内容不正确,请重新输入!");
}
}
}
}
【结果】