目录
说明:
上一篇总结了Java I/O流的分类,其中 InputStream和 OutputStream 是所有字节流的基类、Reader 和 Writer 是所有字符流的基类。当然了,不同用途的流则对应有不同的子类,这篇从访问文件的角度,整理下文件流和转换流。文件流有:字节文件输入流 FileInputStream、字节文件输出流 FileOutputStream、字符文件输入流 FileReader、字符文件输出流 FileWriter;转换流则有:InputStreamReader 和 OutputStreamWriter,它们在字节转字符操作中起到桥梁的作用。
一、文件字节流
1、字节输入流 FileInputStream
用于应用程序读取外部设备(这里指的是文件)的字节数据,源码分析如下:
/**
* 源码注释:JDK8
* FileInputStream是从文件系统中的文件中获取输入字节,用于读取原始字节流,比如图像数据。
**/
public class FileInputStream extends InputStream{
private final FileDescriptor fd; //文件描述符
private final String path; //引用文件路径
private FileChannel channel = null; //文件通道
private final Object closeLock = new Object(); //对象监视器
private volatile boolean closed = false; //流关闭的标识
-------------------------------构造方法---------------------------------
// 传入文件路径
public FileInputStream(String name) throws FileNotFoundException {
// this指的是参数为File的FileInputStream构造器
this(name != null ? new File(name) : null);
}
// 传入文件对象File
public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
if (name == null) {
throw new NullPointerException();
}
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
fd = new FileDescriptor();
fd.attach(this);
path = name;
open(name); // 打开文件,本质上调用一个本地方法open0(),获取文件中的输入字节
}
private void open(String name) throws FileNotFoundException {open0(name);}
private native void open0(String name) throws FileNotFoundException;
// 传入文件描述符
public FileInputStream(FileDescriptor fdObj) {
SecurityManager security = System.getSecurityManager();
if (fdObj == null) {
throw new NullPointerException();
}
if (security != null) {
security.checkRead(fdObj);
}
fd = fdObj;
path = null;
fd.attach(this);
}
-------------------------------从输入流中读取字节数据---------------------------------
// 直接读取一个字节的数据
public int read() throws IOException {return read0();}
// 从输入流中读取字节数据到整个字节数组中(默认偏移量和大小)
public int read(byte b[]) throws IOException {return readBytes(b, 0, b.length);}
// 从输入流中读取指定偏移量和大小的字节数据到字节数组中
public int read(byte b[], int off, int len) throws IOException {return readBytes(b, off, len);}
// 读取的时候,跳过指定数量的字节
public long skip(long n) throws IOException {return skip0(n);}
// 预估输入流中可用字节的数量
public int available() throws IOException {return available0();}
// 私有本地方法
private native int read0() throws IOException;
private native int readBytes(byte b[], int off, int len) throws IOException;
private native long skip0(long n) throws IOException;
private native int available0() throws IOException;
-------------------------------关闭输入流---------------------------------
// finalize():输入流的终结方法。
// 如果FileDescriptor对象的引用是共享的,要确保在安全操作下调用终结器,
// 当引用全都不可达时,才能调用close()关闭。
protected void finalize() throws IOException {
if ((fd != null) && (fd != FileDescriptor.in)) {
/* if fd is shared, the references in FileDescriptor
* will ensure that finalizer is only called when
* safe to do so. All references using the fd have
* become unreachable. We can call close()
*/
close();
}
}
// 关闭流,释放资源
public void close() throws IOException {
synchronized (closeLock) { //线程同步操作
if (closed) {
return;
}
closed = true;
}
if (channel != null) {
channel.close(); //关闭文件通道
}
fd.closeAll(new Closeable() { //关闭所有文件
public void close() throws IOException {
close0();
}
});
}
private native void close0() throws IOException; // 私有本地方法
}
小结一下:
- 通过构造方法创建一个可以从输入流中读取字节数据的对象,参数可以是文件路径、文件对象File,文件描述符对象FileDescriptor。
- FileInputStream提供了多个读取字节数据的read()方法,及跳过指定数量字节的skip()方法,还有关闭流并释放资源的close()方法。
- finalize()是一个终结方法,当fd对象的引用不再有其他对象关联或使用时,也就是JVM中的不可达分析,才能调用close()方法关闭。
实战操作:(从目标文件读取字节数据)
// FileInputStream方式没有使用缓冲区,效率较低,实际工作中不建议使用
public class Test {
public static void main(String[] args) throws IOException {
File file = new File("D:/targetTest.txt");
System.out.println(read(file));
}
public static String read(File file) throws IOException {
InputStream in = new FileInputStream(file);
// 一次性取多少个字节
byte[] bytes = new byte[1024];
// 用来接收读取的字节数组
StringBuilder sb = new StringBuilder();
// 读取到的字节数组长度,为-1时表示没有数据
int length = 0;
// 循环取数据
while ((length = in.read(bytes)) != -1) {
// 将读取的内容转换成字符串
sb.append(new String(bytes, 0, length));
}
in.close();
return sb.toString();
}
}
2、字节输出流 FileOutputStream
用于应用程序向外部设备(这里指的是文件)写入字节数据,源码分析如下:
/**
* 源码注释:JDK8
* FileOutputStream向文件或文件描述符写入字节数据,用于编写原始字节流,比如图像数据。
**/
public class FileOutputStream extends OutputStream {
private final FileDescriptor fd;
private final boolean append; //追加文件内容的标识
private FileChannel channel;
private final String path;
private final Object closeLock = new Object();
private volatile boolean closed = false;
-------------------------------构造方法---------------------------------
// 传入文件路径,默认写入时不追加内容
public FileOutputStream(String name) throws FileNotFoundException {this(name != null ? new File(name) : null, false);}
public FileOutputStream(String name, boolean append) throws FileNotFoundException{this(name != null ? new File(name) : null, append);}
// 传入文件对象File,默认写入时不追加内容
public FileOutputStream(File file) throws FileNotFoundException {this(file, false);}
public FileOutputStream(File file, boolean append)
throws FileNotFoundException
{
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkWrite(name);
}
if (name == null) {
throw new NullPointerException();
}
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
this.fd = new FileDescriptor();
fd.attach(this);
this.append = append;
this.path = name;
open(name, append); //会调用本地的open0()
}
// 传入FileDescriptor,默认写入时不追加内容
public FileOutputStream(FileDescriptor fdObj) {...}
-------------------------------向输出流中写入字节数据---------------------------------
// 写入指定数量的字节
public void write(int b) throws IOException {write(b, append);}
// 写入整个字符数组的字节(默认偏移量和大小)
public void write(byte b[]) throws IOException {writeBytes(b, 0, b.length, append);}
// 写入字符数组的一部分字节(带有偏移量和指定大小)
public void write(byte b[], int off, int len) throws IOException {writeBytes(b, off, len, append);}
-------------------------------关闭输出流---------------------------------
//finalize():终结方法,参考InputStream说明
protected void finalize() throws IOException {
if (fd != null) {
if (fd == FileDescriptor.out || fd == FileDescriptor.err) {
flush();
} else {
/* if fd is shared, the references in FileDescriptor
* will ensure that finalizer is only called when
* safe to do so. All references using the fd have
* become unreachable. We can call close()
*/
close();
}
}
}
// 关闭流,释放资源
public void close() throws IOException {
synchronized (closeLock) {...} //线程同步操作,关闭标识
if (channel != null) {... } //关闭文件通道
fd.closeAll... //关闭所有文件
}
}
小结一下:
- 通过构造方法创建一个向输出流中写入字节数据的对象,参数可以是文件路径+是否追加文件内容、文件对象File+是否追加文件内容,文件描述符对象FileDescriptor。
- FileOutputStream 提供了多个写入字节数据的write()方法,还有关闭流并释放资源的close()方法。
实战操作:(向目标文件写入字节数据)
// FileOutputStream方式没有使用缓冲区,效率较低,实际工作中不建议使用
public class Test {
public static void main(String[] args) throws IOException {
File file = new File("D:/targetTest.txt");
write(file);
}
public static void write(File file) throws IOException {
OutputStream os = new FileOutputStream(file, true);
// 要写入的字符串
String string = "五一快乐啊!!!";
// 写入文件
os.write(string.getBytes());
// 关闭流
os.close();
}
}
二、文件字符流
FileReader和FileWriter源码也很简单,就是通过super()调用父类的构造器,源码分析如下:
public class FileReader extends InputStreamReader {
// 通过super()调用父类InputStreamReader构造方法
public FileReader(String fileName) throws FileNotFoundException {super(new FileInputStream(fileName));}
public FileReader(File file) throws FileNotFoundException {super(new FileInputStream(file));}
public FileReader(FileDescriptor fd) {super(new FileInputStream(fd));}
}
public class FileWriter extends OutputStreamWriter {
// 通过super()调用父类OutputStreamWriter构造方法
public FileWriter(String fileName) throws IOException {super(new FileOutputStream(fileName));}
public FileWriter(String fileName, boolean append) throws IOException {super(new FileOutputStream(fileName, append));}
public FileWriter(File file) throws IOException {super(new FileOutputStream(file));}
public FileWriter(File file, boolean append) throws IOException {super(new FileOutputStream(file, append));}
public FileWriter(FileDescriptor fd) {super(new FileOutputStream(fd));}
}
小结一下:
- FileReader 和 FileWriter 是面向字符数据操作的流,源码中通过继承各自对应的转换流,让面向字符数据的操作可以适配成面向字节数据的操作,InputStreamReader/OutputStreamWriter 则是将字节操作转换成字符操作的桥梁 ,这是设计模式中的适配器设计模式。
- FileReader 只是通过父类 InputStreamReader 中参数为 InputStream 的一种构造方法实现的,而 InputStreamReader 还提供了读取字符read()方法,关闭流close()方法,也可以使用转换流对字符数据进行操作。FileWriter同理。
- 字符输出流相比于字节输出流,可以通过append()方法进行追加文本内容。
实战操作:(实现文件复制)
// 把D盘中的Stream.txt复制到C盘的根目录下
public static void main(String[] args) {
File filesrc = new File("D:\\iotest2\\Stream.txt");
File filedest = new File("C:\\"+"copy.txt");
try {
Reader in = new FileReader(filesrc);
Writer out = new FileWriter(filedest);
char[]cs = new char[1024];
int len = -1;//实际的位置
while((len = in.read(cs,0,cs.length)) != -1){
out.write(cs,0,len);
}
out.close();
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
三、转换流
转换流指的是InputStreamReader和OutputStreamWriter,在字节转字符操作起到了桥梁的作用。InputStreamReader父类是Reader ,OutputStreamWriter父类是Writer。
1、字符输入流 InputStreamReader
InputStreamReader 源码分析如下:
public class InputStreamReader extends Reader {
private final StreamDecoder sd; //流的解码器,实现字节转字符操作
// 实现类FileReader调用的构造器,默认字符集名称为null
public InputStreamReader(InputStream in) {
super(in); // 调用的是父类Reader的Reader(Object lock)构造方法
try {
sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
} catch (UnsupportedEncodingException e) {
// The default encoding should always be available
throw new Error(e);
}
}
// 含有字符集的构造方法
public InputStreamReader(InputStream in, String charsetName) throws UnsupportedEncodingException{...}
public InputStreamReader(InputStream in, Charset cs) {...}
public InputStreamReader(InputStream in, CharsetDecoder dec) {...}
// 读取单个字符
public int read() throws IOException {return sd.read();}
// 从输入流中读取指定偏移量和大小的字节数据到字节数组中
public int read(char cbuf[], int offset, int length) throws IOException {return sd.read(cbuf, offset, length);}
// 测试输入流是否可读
public boolean ready() throws IOException {return sd.ready();}
// 关闭流,释放资源
public void close() throws IOException {sd.close();}
}
2、字符输出流 OutputStreamWriter
OutputStreamWriter源码分析如下:
public class OutputStreamWriter extends Writer {
private final StreamEncoder se; //流的编码,实现字节转字符操作
// 实现类FileWriter调用的构造器,默认字符集名称为null
public OutputStreamWriter(OutputStream out) {
super(out);
try {
se = StreamEncoder.forOutputStreamWriter(out, this, (String)null);
} catch (UnsupportedEncodingException e) {
throw new Error(e);
}
}
// 含有字符集的构造方法
public OutputStreamWriter(OutputStream out, String charsetName) throws UnsupportedEncodingException{...}
public OutputStreamWriter(OutputStream out, Charset cs) throws UnsupportedEncodingException{...}
public OutputStreamWriter(OutputStream out, CharsetEncoder enc) throws UnsupportedEncodingException{...}
// 写入指定大小的字符
public void write(int c) throws IOException {se.write(c);}
// 写入字符数组的一部分(带有偏移量和指定大小)
public void write(char cbuf[], int off, int len) throws IOException {se.write(cbuf, off, len);}
// 写入字符串的一部分
public void write(String str, int off, int len) throws IOException {se.write(str, off, len);}
// 清空缓冲区数据
public void flush() throws IOException {se.flush();}
// 关闭流,释放连接
public void close() throws IOException {se.close();}
}
实战操作:(IDEA控制台输入一行文字,保存到文件中,这里使用BufferedReader、BuffereWrite加强读写功能)
public static void main(String[] args) throws IOException {
// 标准输入--->内存
InputStream is = System.in;
//System.in已经是描述好的标准输入流对象,转换
InputStreamReader ir = new InputStreamReader(is, "utf-8");
// 加入缓冲流
BufferedReader bfr = new BufferedReader(ir);
System.out.println("请输入一行文字:");
String msg = bfr.readLine();
// 保存到文件中,创建原始流
FileOutputStream fos = new FileOutputStream("E:\\save.txt");
// 转换成字符输出流
OutputStreamWriter ops = new OutputStreamWriter(fos, "utf-8");
// 加入缓冲流
BufferedWriter bfw = new BufferedWriter(ops);
bfw.write(msg);
bfr.close();
bfw.close();
}
总结:
这里从源码的角度,分析了面向文件操作的字节流和字符流,以及将字节转换成字符操作的转换流,并进行了相关的实战操作。从使用角度看,面对数据量较大的读写操作时,不推荐使用文件字节流,而是使用文件字符流或转换流,同时为了加强读写功能,一般还会通过缓冲流进行包装,下一篇将介绍缓冲流。