基本概念
FileInputStream 从文件系统中的某个文件中获得输入字节
FileOutputStream 文件输出流是用于将数据写入 File 或 FileDescriptor 的输出流。文件是否可用或能否可以被创建取决于基础平台。特别是某些平台一次只允许一个 FileOutputStream(或其他文件写入对象)打开文件进行写入。在这种情况下,如果所涉及的文件已经打开,则此类中的构造方法将失败。
继承结构
实例探究
写入文件的字节都会被底层按照系统默认编码转换成字符存到文件中
当使用文件字节输出来的时候会被反转成字节
public class Test {
private static final String TEMPFILE = "E:" + File.separator + "Test.txt";
private static final String DESFILE = "E:" + File.separator + "Test2.txt";
//英文字母 a ~ h
private static final byte[] byteArray = { 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69 };
public static void main(String[] args) throws IOException {
write(TEMPFILE);
read(TEMPFILE);
copyFile(TEMPFILE, DESFILE);
}
public static void write(String path) {
// 初始化,不然关闭流时编译不通过
FileOutputStream fos = null;
try {
// 创建流,这里探究追加模式。假设文本内容现在为 1,2,3
fos = new FileOutputStream(new File(path), true);
// 写入 a
fos.write(byteArray[0]);
// 写入 b,c,d,e,f
fos.write(byteArray, 1, 5);
fos.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void read(String path) {
FileInputStream fis = null;
try {
fis = new FileInputStream(new File(path));
char temp = (char) fis.read();
// 输出 1
System.out.print(temp);
byte[] buffer = new byte[1024];
int count = 0;
while ((count = fis.read(buffer)) != -1) {
// 输出 234abcdefabcdefabcdef
System.out.println(new String(buffer, 0, count));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 文件复制
public static void copyFile(String srcPath, String desPath) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(new File(srcPath));
fos = new FileOutputStream(new File(desPath));
// 为了效率,一般采用按字节数组读取
byte[] buffer = new byte[1024];
int count = 0;
while ((count = fis.read(buffer)) != -1) {
fos.write(buffer, 0, count);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
源码分析
1.FileInputStream
类结构图如下:
首先来看类中的静态代码块,它的作用是设置类中(也就是FileInputStream)的属性的内存地址偏移量,便于在必要时操作内存给它赋值。
static {
initIDs();
}
private static native void initIDs();
成员变量
// 文件描述符类,表示用来打开文件的句柄
private FileDescriptor fd;
// 文件通道,NIO部分
private FileChannel channel = null;
private Object closeLock = new Object();
private volatile boolean closed = false;
private static final ThreadLocal<Boolean> runningFinalize = new ThreadLocal<Boolean>();
该类总共定义了 3 个构造函数,通过代码可以发现可以 ①③ 都是通过获取实际的文件连接,在通过 open 方法来创建流。而 ② 是直接通过文件描述符创建流。
// ①构造函数,通过文件路径创建
public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);
}
// ②构造函数,通过文件描述符创建
public FileInputStream(FileDescriptor fdObj) {
SecurityManager security = System.getSecurityManager();
if (fdObj == null) {
throw new NullPointerException();
}
if (security != null) {
security.checkRead(fdObj);
}
fd = fdObj;
fd.incrementAndGetUseCount();
}
// ③构造函数,通过文件连接创建
public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
// 操作文件之前,检查是否具有 read 权限
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
// 判断路径的合法性
if (name == null) {
throw new NullPointerException();
}
// 打开文件
fd = new FileDescriptor();
fd.incrementAndGetUseCount();
open(name);
}
// 关键-->打开系统文件,native 方法
private native void open(String name) throws FileNotFoundException;
接下来看它 的 read 方法,观察下面的代码,发现在类中定义了常见的 3 种读取方式,如①②③。① 本身就是个native 方法,而 ②③则是通过 readBytes 这个 native 方法用来实现文件的读取。
//① 从此输入流中读取一个数据字节
public native int read() throws IOException;
//关键-->实际操作由它完成
private native int readBytes(byte b[], int off, int len) throws IOException;
//②从此输入流中将最多 len 个字节的数据读入一个 byte 数组中
public int read(byte b[]) throws IOException {
return readBytes(b, 0, b.length);
}
//③从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中
public int read(byte b[], int off, int len) throws IOException {
return readBytes(b, off, len);
}
接着来看其他流中没有的而 FileInputStream 中独有的几个方法
//返回文件描述符,表示该文件正在被 FileInputStream 使用
public final FileDescriptor getFD() throws IOException {
if (fd != null) {
return fd;
}
throw new IOException();
}
//返回文件文件通过,这里只允许单线程访问
public FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, true, false, this);
fd.incrementAndGetUseCount();
}
return channel;
}
}
//重写了 Object 的方法,确保该文件输入流的close方法被调用的时候它不用有引用
protected void finalize() throws IOException {
if ((fd != null) && (fd != FileDescriptor.in)) {
// 当前其他流操作该对象时,调用该方法无法释放资源。但是可以调用 colse 方法强行释放。
runningFinalize.set(Boolean.TRUE);
try {
close();
} finally {
runningFinalize.set(Boolean.FALSE);
}
}
}
最后再来看看剩下的几个方法
public native long skip(long n) throws IOException;
public native int available() throws IOException;
public void close() throws IOException {
synchronized (closeLock) {
if (closed) {
return;
}
closed = true;
}
if (channel != null) {
// 减少与该 FD 相关联的计算器(当每获得一个新的通道时,该计算器增加)
fd.decrementAndGetUseCount();
channel.close();
}
int useCount = fd.decrementAndGetUseCount();
if ((useCount <= 0) || !isRunningFinalize()) {
close0();
}
}
private native void close0() throws IOException;
2.FileOutputStream
类结构图
首先来看静态代码块,作用同上。
static {
initIDs();
}
private static native void initIDs();
成员变量
private FileDescriptor fd;
private FileChannel channel = null;
private boolean append = false;
private Object closeLock = new Object();
private volatile boolean closed = false;
private static final ThreadLocal<Boolean> runningFinalize = new ThreadLocal<Boolean>();
在分析构造函数之前先来看两个 native 方法
//替换原文件的内容
private native void open(String name) throws FileNotFoundException;
//文件原有内容的末尾追加新内容
private native void openAppend(String name) throws FileNotFoundException;
该类定义了 5 个构造函数,其中 ① ~ ④ 都是通过 file 类来创建流,具体的实现都在 ④ 里面。默认未指定 append 时为false,表示替换原文件的内容,当 append 为 true 时,表示在文件原有内容的末尾追加新内容。观察 ④,发现该方法分别调用了 open ,openAppend 这两个 native 方法来实现操作。而 ⑤ 则是通过文件描述符完成创建
//①构造函数,根据文件路径创建,默认不追加内容
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);
}
//③构造函数,根据文件连接创建,默认不追加内容
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();
}
fd = new FileDescriptor();
fd.incrementAndGetUseCount();
this.append = append;
//判断是否追加内容
if (append) {
openAppend(name);
} else {
open(name);
}
}
//⑤构造函数,根据文件描述符创建
public FileOutputStream(FileDescriptor fdObj) {
SecurityManager security = System.getSecurityManager();
if (fdObj == null) {
throw new NullPointerException();
}
if (security != null) {
security.checkWrite(fdObj);
}
fd = fdObj;
fd.incrementAndGetUseCount();
}
接着来看 writer 方法,与 文件输入流的 read 方法类型,不再分析。
public native void write(int b) throws IOException;
private native void writeBytes(byte b[], int off, int len) throws IOException;
public void write(byte b[]) throws IOException {
writeBytes(b, 0, b.length);
}
public void write(byte b[], int off, int len) throws IOException {
writeBytes(b, off, len);
}
再来看看普通输出流没有,而 FileOutputStream 独有的方法。
public final FileDescriptor getFD() throws IOException {
if (fd != null) {
return fd;
}
throw new IOException();
}
public FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, false, true, this, append);
fd.incrementAndGetUseCount();
}
return channel;
}
}
protected void finalize() throws IOException {
if (fd != null) {
if (fd == FileDescriptor.out || fd == FileDescriptor.err) {
flush();
} else {
runningFinalize.set(Boolean.TRUE);
try {
close();
} finally {
runningFinalize.set(Boolean.FALSE);
}
}
}
}
最后再来看看 close 方法
public void close() throws IOException {
synchronized (closeLock) {
if (closed) {
return;
}
closed = true;
}
if (channel != null) {
fd.decrementAndGetUseCount();
channel.close();
}
int useCount = fd.decrementAndGetUseCount();
if ((useCount <= 0) || !isRunningFinalize()) {
close0();
}
}
private static boolean isRunningFinalize() {
Boolean val;
if ((val = runningFinalize.get()) != null)
return val.booleanValue();
return false;
}
private native void close0() throws IOException;