在Java中,输入输出(IO)操作是编程中一项重要的任务。无论是从外部文件中读取数据,还是向文件写入数据,或者是与用户进行交互,都需要用到IO操作。
普通IO
FileOutputStream的使用
FileOutputStream是Java中的一个类,属于java.io包。它用于将数据写入文件。当你需要将数据(通常是字节或字符)写入一个文件时,可以使用FileOutputStream。
以下是FileOutputStream的一些基本用法:
创建一个新的文件并写入数据:
package com.morris.io;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* 创建一个新的文件并写入数据
*/
public class FileOutputStreamDemo {
public static void main(String[] args) throws IOException {
FileOutputStream outputStream = new FileOutputStream("/home/vagrant/testfileio/FileOutputStreamDemo.txt");
outputStream.write("Hello, World!".getBytes());
outputStream.close();
}
}
我们可以使用strace命令追踪上面程序执行过程中产生的系统调用:
openat(AT_FDCWD, "/home/vagrant/testfileio/FileOutputStreamDemo.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 4
fstat(4, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
write(4, "Hello, World!", 13) = 13
close(4)
向现有文件追加数据:
如果你想向一个已经存在的文件追加数据,而不是覆盖它,你可以使用FileOutputStream的另一个构造函数:
package com.morris.io;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* 向现有文件追加数据
*/
public class FileOutputStreamAppendDemo {
public static void main(String[] args) throws IOException {
FileOutputStream outputStream = new FileOutputStream("/home/vagrant/testfileio/FileOutputStreamDemo.txt", true);
outputStream.write("Appended data".getBytes());
outputStream.close();
}
}
产生的系统调用如下:
openat(AT_FDCWD, "/home/vagrant/testfileio/FileOutputStreamDemo.txt", O_WRONLY|O_CREAT|O_APPEND, 0666) = 4
fstat(4, {st_mode=S_IFREG|0664, st_size=13, ...}) = 0
write(4, "Appended data", 13) = 13
close(4)
可以看到在创建新的文件时,openat系统调用传的标志为O_TRUNC
,在追加数据时传的标志为O_APPEND
。
O_TRUNC
和O_APPEND
的区别:
- O_TRUNC:打开一个文件时,如果文件已经存在,它的长度将被截断为 0,然后可以重新写入。换句话说,这个操作会清空文件内容。如果文件不存在,则会创建一个新文件。
- O_APPEND:打开一个文件时,写操作总是发生在文件的末尾。即使写入操作在文件的开始部分进行,写指针也会被放在文件的末尾。如果文件不存在,则会创建一个新文件。
注意:当使用FileOutputStream或其他IO类时,始终建议在完成操作后关闭流。这可以通过使用try-with-resources语句或手动调用close()方法来完成。长时间不关闭流可能会导致资源泄露。
FileWriter的使用
FileWriter是Java中的一个类,它属于java.io包,主要用于将字符数据写入文件。这个类提供了方便的方法来将字符串或者字符数组写入文件。
FileWriter的构造方法有多种,可以根据不同的需求选择:
- FileWriter(String fileName):根据给定的文件名构造一个FileWriter对象。如果文件不存在,它将被创建。如果文件已经存在,它的内容将被覆盖。
- FileWriter(String fileName, boolean append):根据给定的文件名以及指示是否附加写入数据的boolean值来构造FileWriter对象。如果append为true,则数据将被附加到文件末尾而不是覆盖原有内容;如果append为false,则数据将覆盖原有内容。
- FileWriter(File file):根据给定的File对象构造一个FileWriter对象。如果文件不存在,它将被创建。如果文件已经存在,它的内容将被覆盖。
- FileWriter(FileDescriptor fd):构造与文件描述符关联的FileWriter对象。
此外,FileWriter类还提供了flush()和close()方法来刷新缓冲区和关闭流。在完成写入操作后,应始终关闭流以释放系统资源。
需要注意的是,在某些平台上,如果一个文件已经被另一个FileOutputStream(或其他文件写入对象)打开进行写入,那么在这个文件上再使用FileWriter进行写入可能会失败。因此,在使用FileWriter时,应确保在尝试写入之前没有其他对象已经打开该文件进行写入操作。
FileWriter的使用如下:
package com.morris.io;
import java.io.FileWriter;
import java.io.IOException;
/**
* FileWriter的使用,直接写入字符
*/
public class FileWriterDemo {
public static void main(String[] args) throws IOException {
FileWriter fileWriter = new FileWriter("/home/vagrant/testfileio/FileWriterDemo.txt");
fileWriter.write("Hello, World!");
fileWriter.close();
}
}
产生的系统调用如下:
openat(AT_FDCWD, "/home/vagrant/testfileio/FileWriterDemo.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 4
fstat(4, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
write(4, "Hello, World!", 13) = 13
close(4)
可以看到与FileOutputStream产生的系统调用一致,说明FileWriter只是java层面的封装,底层还是按字节来写。
FileOutputStream和FileWriter的区别
FileOutputStream和FileWriter都是Java中用于文件操作的类,但它们之间存在一些重要的区别。
- 工作方式:FileOutputStream用于二进制数据的写入,每次读入或输出的是8位二进制。而FileWriter则是字符流,每次读入或输出的是16位二进制,即两个字节。
- 处理方式:FileOutputStream是字节流,对于图像数据等二进制数据更为合适。而FileWriter是字符流,更适合处理unicode编码的字符数据,如文本文件。
带Buffer的IO
BufferedOutputStream的使用
BufferedOutputStream是一个缓冲输出流,用于将数据写入底层输出流。它通过建立内部缓冲区来存储数据,从而提高写入效率。使用BufferedOutputStream,应用程序可以将多个字节写入缓冲区,而不是为每个字节编写对底层系统的调用。
BufferedOutputStream的构造器可以接受一个OutputStream对象作为参数,用于创建一个新的缓冲输出流,将数据写入指定的底层输出流。
使用BufferedOutputStream时,可以调用write()方法将数据写入缓冲区。当缓冲区满时,数据会自动刷新到底层输出流中。此外,还可以调用flush()方法强制刷新缓冲区并将数据写入底层输出流。
BufferedOutputStream的使用如下:
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* BufferedOutputStream的使用
*/
public class BufferedOutputStreamDemo {
public static void main(String[] args) throws IOException {
FileOutputStream outputStream = new FileOutputStream("/home/vagrant/testfileio/BufferedOutputStreamDemo.txt");
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
bufferedOutputStream.write("Hello, World!".getBytes(StandardCharsets.UTF_8));
bufferedOutputStream.close();
}
}
产生的系统调用如下:
openat(AT_FDCWD, "/home/vagrant/testfileio/BufferedOutputStreamDemo.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 4
write(4, "Hello, World!", 13) = 13
close(4)
可以看到与FileOutputStream产生的系统调用一致,那么BufferedOutputStream与BufferedOutputStream到底有什么区别呢?
FileOutputStream与BufferedOutputStream的区别
先来看一个数据的对比:
FileOutputStream速度测试:
package com.morris.io;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* FileOutputStream速度测试
*/
public class FileOutputStreamTest {
public static void main(String[] args) throws IOException {
FileOutputStream outputStream = new FileOutputStream("/home/vagrant/testfileio/FileOutputStreamTest.txt");
long start = System.currentTimeMillis();
for (int i = 0; i < 1000_0000; i++) {
outputStream.write("abc1234567890".getBytes());
}
outputStream.close();
long end = System.currentTimeMillis();
System.out.println("cost : " + (end - start));
}
}
上面代码运行的结果耗时为10659
。
BufferedOutputStream速度测试:
package com.morris.io;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* BufferedOutputStream速度测试
*/
public class BufferedOutputStreamTest {
public static void main(String[] args) throws IOException {
FileOutputStream outputStream = new FileOutputStream("/home/vagrant/testfileio/BufferedOutputStreamTest.txt");
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
long start = System.currentTimeMillis();
for (int i = 0; i < 1000_0000; i++) {
bufferedOutputStream.write("abc1234567890".getBytes());
}
bufferedOutputStream.close();
long end = System.currentTimeMillis();
System.out.println("cost : " + (end - start));
}
}
上面代码运行的结果耗时为400
。
是什么原因会导致上面两个例子的耗时相差几十倍呢?
我们先来看下这两个例子对应的系统调用:
FileOutputStreamTest:
openat(AT_FDCWD, "/home/vagrant/testfileio/FileOutputStreamTest.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 4
fstat(4, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
write(4, "abc1234567890", 13) = 13
write(4, "abc1234567890", 13) = 13
write(4, "abc1234567890", 13) = 13
write(4, "abc1234567890", 13) = 13
write(4, "abc1234567890", 13) = 13
BufferedOutputStreamTest:
openat(AT_FDCWD, "/home/vagrant/testfileio/BufferedOutputStreamTest.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 4
fstat(4, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
write(4, "abc1234567890abc1234567890abc123"..., 8190) = 8190
write(4, "abc1234567890abc1234567890abc123"..., 8190) = 8190
write(4, "abc1234567890abc1234567890abc123"..., 8190) = 8190
write(4, "abc1234567890abc1234567890abc123"..., 8190) = 8190
write(4, "abc1234567890abc1234567890abc123"..., 8190) = 8190
可以看到BufferedOutputStream内置了一个8kb的缓冲区来存储数据,当缓冲区满时,数据会被刷新到Page Cache中,大大的减少了系统调用的次数。这种机制允许BufferedOutputStream在需要时自动刷新缓冲区,确保数据及时写入底层输出流。
BufferedOutputStream是一个装饰者模式的应用。装饰者模式是一种设计模式,用于在不修改现有类的前提下,动态地给一个对象添加新的功能。
BufferedOutputStream作为一个装饰者,扩展了OutputStream的功能,添加了缓冲区来存储数据。它接受一个OutputStream对象作为参数,并在内部维护了一个缓冲区。通过缓冲区,它可以批量写入数据,而不是为每个字节都进行一次底层调用,从而提高了写入效率。
当应用程序调用BufferedOutputStream的write()方法时,数据被写入缓冲区而不是直接写入底层输出流。当缓冲区满时,数据会被刷新到底层输出流中。这种机制允许BufferedOutputStream在需要时自动刷新缓冲区,确保数据及时写入底层输出流。
BufferedWriter的使用
BufferedWriter用于将文本写入字符输出流,并缓冲字符以便更有效地写入单个字符、数组和字符串。它继承自Writer类,并提供了额外的缓冲功能来提高写入效率。
BufferedWriter的主要特点包括:
- 缓冲机制:BufferedWriter内部维护了一个缓冲区,用于存储写入的数据。通过将数据写入缓冲区,可以提高写入效率,减少对底层输出流的直接写入操作。当缓冲区满时,数据会自动刷新到底层输出流中。
- 数据类型:BufferedWriter的write()方法可以接受字符、字符数组和字符串作为参数,以便写入不同类型的数据。
- 行分隔符:BufferedWriter提供了newLine()方法,用于写入一个行分隔符,以便在输出中自动添加换行符。这使得在输出文本时可以方便地添加换行操作。
- 异常处理:BufferedWriter的方法可能会抛出IOException异常,因此需要在代码中进行适当的异常处理。
- 构造函数参数:BufferedWriter的构造函数可以接受一个Writer对象作为参数,用于创建新的BufferedWriter对象。
- 刷新缓存:BufferedWriter提供了flush()方法,用于强制刷新缓冲区并将数据写入底层输出流。
使用BufferedWriter时,可以将数据逐个写入缓冲区,而不是直接写入底层输出流。当需要将数据刷新到底层输出流时,可以调用flush()方法。此外,通过使用newLine()方法,可以在输出中自动添加换行符,简化换行操作。
BufferedWriter的使用:
package com.morris.io;
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
/**
* BufferedWriter的使用
* 相当于FileWriter+BufferedOutputStream的结合
*/
public class BufferedWriterDemo {
public static void main(String[] args) throws IOException {
FileOutputStream outputStream = new FileOutputStream("/home/vagrant/testfileio/BufferedWriterDemo.txt");
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
bufferedWriter.write("Hello, World!");
bufferedWriter.close();
}
}
PrintWriter的使用
PrintWriter向文本输出流打印对象的格式化表示形式(Prints formatted representations of objects to a text-output stream)。
PrintWriter相对于BufferedWriter的好处在于,如果PrintWriter开启了自动刷新,那么当PrintWriter调用println,prinlf或format方法时,输出流中的数据就会自动刷新出去。PrintWriter不但能接收字符流,也能接收字节流。
PrintWriter的使用:
package com.morris.io;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
/**
* PrintWriter的使用
* 对BufferedWriter的升级
*/
public class PrintWriterDemo {
public static void main(String[] args) throws IOException {
FileOutputStream outputStream = new FileOutputStream("/home/vagrant/testfileio/BufferedWriterDemo.txt");
PrintWriter bufferedWriter = new PrintWriter(new OutputStreamWriter(outputStream));
bufferedWriter.println("Hello, World!");
bufferedWriter.close();
}
}
Socket编程中,尽量用PrintWriter取代BufferedWriter,下面是PrintWriter的优点:
- PrintWriter的print、println方法可以接受任意类型的参数,而BufferedWriter的write方法只能接受字符、字符数组和字符串;
- PrintWriter的println方法自动添加换行,BufferedWriter需要显示调用newLine方法;
- PrintWriter的方法不会抛异常,若关心异常,需要调用checkError方法看是否有异常发生;
- PrintWriter构造方法可指定参数,实现自动刷新缓存(autoflush);
- PrintWriter的构造方法更广。