文章目录
1、创作初衷
使用Java语言做了几年的开发,虽然IO这块不是经常用到,但是偶尔也会使用。在我每一次使用的时候,总是会被IO包下很多的方法给搞晕,总是遇到不知道该用谁、不知道谁该包裹谁、不知道谁的效率好、不知道谁对中文支持好等等这些情况…
说到头来,还是自己的基础不扎实。所以打算去把它们的源码都看看,然后进行总结并纪录在此。
2、FileOutputStream 使用流程
用FileOutputStream 将内容写入文件流程如下:
- 打开文件:用File对象打开文件
- 打开输出流:实例化FileOutputStream对象
- 写入数据:往流中写入数据
- 关闭流:关闭输出流
3、FileOutputStream 源码分析
3.1、FileOutputStream的构造方法
FileOutputStream提供了4个常用构造方法,用于实例化FileOutputStream对象,这4个不同的构造方法,对应着不同的使用场景。
3.1.1、构造函数1
public FileOutputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null, false);
}
该构造方法允许直接传入文件路径,参数 name 表示文件路径。
如果 name 参数指定的文件不存在,则会抛出 fileNotFoundException 异常。
查看该构造方法的代码,可以看见其内部使用了 name 参数创建了File对象打开文件。
后面这个 false参数表示的是:是否在原来内容的基础上追加写入内容,默认为false,不追加,也就是会覆盖原有的内容。
这一点需要注意,请小心使用。
3.1.1.1、使用示例
public static void main(String[] args) {
try {
FileOutputStream fos = new FileOutputStream("F:\\FilesExample\\A.txt");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
3.1.2、构造函数2
public FileOutputStream(String name, boolean append) throws FileNotFoundException{
this(name != null ? new File(name) : null, append);
}
该构造函数和第一个构造函数极为相似,唯一的区别是多了一个参数表明是否追加内容,也就是说现在是否追加内容可以通过这个构造函数来手动控制。
append参数为true时,数据从文件尾部写入,append参数为false时,数据覆盖原文件。
3.1.2.1、使用示例
public static void main(String[] args) {
try {
FileOutputStream fos = new FileOutputStream("F:\\FilesExample\\A.txt", true);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
3.1.3、构造函数3
public FileOutputStream(File file) throws FileNotFoundException {
this(file, false);
}
该构造函数传入一个File对象参数,使用File对象打开本地文件,从文件读取数据。
与构造函数1、2相比,需要自己创建好File对象后,传入FileOutputStream构造器。构造方法实现中的第二个参数也是表示是否追加内容的意思。
3.1.3.1、使用示例
public static void main(String[] args) {
try {
File myFile = new File("F:\\FilesExample\\A.txt");
FileOutputStream fos = new FileOutputStream(myFile);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
3.1.4、构造函数4
public FileOutputStream(File file, boolean append)
throws FileNotFoundException
{
// 先对 file 参数进行 null 处理
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkWrite(name);
}
// 判断是否为 null
if (name == null) {
throw new NullPointerException();
}
// 判断file对象是否合法
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
this.fd = new FileDescriptor();
this.append = append;
this.path = name;
fd.incrementAndGetUseCount();
// 打开具有指定名称的文件进行覆盖或追加。
open(name, append);
}
其实上面三个构造器最终都会调用这个构造函数。
3.1.4.1、使用示例
public static void main(String[] args) {
try {
File myFile = new File("F:\\FilesExample\\A.txt");
FileOutputStream fos = new FileOutputStream(myFile, true);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
4、FileOutputStream的写方法
FileOutputStream 类提供了三个文件写入方法(wirte),可以单独写一个字节到文件,也可以写一个byte数组到文件,也可以取byte数组的部分数据写入到文件。
wirte方法是被重载了三次。
4.1、wirte方法一
public void write(int b) throws IOException {
Object traceContext = IoTrace.fileWriteBegin(path);
int bytesWritten = 0;
try {
write(b, append);
bytesWritten = 1;
} finally {
IoTrace.fileWriteEnd(traceContext, bytesWritten);
}
}
将指定的字节写入此文件输出流,int类型的参数 b 就是指定的字节,可以看到该方法其实还去调用了一个write(b, append)方法,这是一个 native 方法,append参数默认为false,也就是不进行追加内容,即覆盖。
一般简单的源码分析到native这一步就可以了,不必追根溯源。
4.1.1、使用示例
public static void main(String[] args) {
FileOper fileOper = new FileOper();
String filePath = "F:\\FilesExample\\A.txt";
// 创建文件
fileOper.createFile(filePath);
// 创建File对象
File myFile = new File(filePath);
// 要输出的内容
String str = "This is FileOutputStreamTest Content";
// 使用 try-with-resource 打开资源
try (FileOutputStream fos = new FileOutputStream(myFile)){
// 遍历输出内容
for (int i = 0; i < str.length(); i++) {
int b = str.charAt(i);
fos.write(b);
fos.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
FileOper文件操作类,是我写的一个文件操作公共类,具体内容会在文章最后贴出来。在这里不用在意它。
我们需要在意的是,先是创建了一个File对象,然后利用这个File对象创建了一个 FileOutputStream 字节输出流。然后再使用write(b)这个方法输出。
4.2、write方法2
public void write(byte b[]) throws IOException {
Object traceContext = IoTrace.fileWriteBegin(path);
int bytesWritten = 0;
try {
writeBytes(b, 0, b.length, append);
bytesWritten = b.length;
} finally {
IoTrace.fileWriteEnd(traceContext, bytesWritten);
}
}
将指定的字节数组写入到这个文件输出流,byte数组类型的参数 b 就是这个指定的数组。
这个方法它调用了一个 native 的writeBytes(b, 0, b.length, append)方法,这几个参数什么意思呢?
- 参数 b:就是要写入到这个文件输出流的字节数组。
- 参数 0:偏移量。到底什么是偏移量呢?下面解释。
- 参数 b.length:要写入的字节长度,也就是要写入多少个字节。
- 参数 append:和上面方法含义一样,是否追加内容,true为追加,false为不追加,默认为false。
4.2.1、偏移量
Java IO流中的偏移量是指接收数据的数组(或叫缓冲区)的偏移量,并不是数据流的偏移量。
详细解释:详解Java IO流中偏移量
这是它调用的 native 方法
private native void writeBytes(byte b[], int off, int len, boolean append)
throws IOException;
这三个参数联合起来用一句话来说就是:
将指定字节数组从偏移量off 开始的 len 个字节写入这个输出流。
4.2.2、使用示例
public static void main(String[] args) {
FileOper fileOper = new FileOper();
String filePath = "F:\\FilesExample\\A.txt";
// 创建文件
fileOper.createFile(filePath);
// 创建File对象
File myFile = new File(filePath);
// 要输出的内容
String str = "This is FileOutputStreamTest Content";
// 使用 try-with-resource 打开资源
try (FileOutputStream fos = new FileOutputStream(myFile)){
// 输出内容
byte[] bytes = str.getBytes();
fos.write(bytes);
fos.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
try块外面的内容和上面完全一致,唯一的区别就是在使用wirte方法输出的时候,这里传入的是一个byte数组。
4.3、wirte方法3
public void write(byte b[], int off, int len) throws IOException {
Object traceContext = IoTrace.fileWriteBegin(path);
int bytesWritten = 0;
try {
writeBytes(b, off, len, append);
bytesWritten = len;
} finally {
IoTrace.fileWriteEnd(traceContext, bytesWritten);
}
}
这个方法和上面的两个方法都是大同小异。
可以说是方法1最简单,方法3最灵活,至于里面的参数呢,是和上面方法的参数一样的。已经详细解释过了,就不重复解释了。
4.3.1、使用示例
public static void main(String[] args) {
FileOper fileOper = new FileOper();
String filePath = "F:\\FilesExample\\A.txt";
// 创建文件
fileOper.createFile(filePath);
// 创建File对象
File myFile = new File(filePath);
// 要输出的内容
String str = "This is FileOutputStreamTest Content";
// 使用 try-with-resource 打开资源
try (FileOutputStream fos = new FileOutputStream(myFile)){
// 输出内容
byte[] bytes = str.getBytes();
fos.write(bytes, 0, bytes.length);
fos.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
唯一的区别也就是 wirte方法处。
5、总结
使用FileOutputStream流可以输出字节数据到目标文件,FileOutputStream提供了单字节写入和byte数组写入两种方式。建议使用byte数组写入,将待写入的数据存储到一个byte数组中,然后再写入文件。当写入的文件已经存在时,需要指明写入方式是覆盖还是追加。
6、最后附上FileOper的内容
/**
* 文件操作类
* Created by 6092002538 on 2019/7/15.
*/
public class FileOper {
/**
* 创建文件夹
* @param dirPath 文件夹路径,也就是这个文件夹会创建在什么地方
*/
public void createDir(String dirPath){
File myDir = new File(dirPath);
if (!myDir.exists()){
myDir.mkdirs();
}
}
/**
* 在指定位置创建文件
* @param filePath 指定的文件路径,也就是这个文件会被创建在什么地方
*/
public void createFile(String filePath){
// 创建File对象
File myFile = new File(filePath);
// 创建文件夹
createDir(myFile.getParent());
try {
// 如果文件不存在则创建
if (!myFile.exists()){
myFile.createNewFile();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
技 术 无 他, 唯 有 熟 尔。
知 其 然, 也 知 其 所 以 然。
踏 实 一 些, 不 要 着 急, 你 想 要 的 岁 月 都 会 给 你。