更好的阅读体验:点这里 ( www.doubibiji.com
)
12 文件与IO
12.1 File
如果我们想使用 Java 代码来操作文件,就需要使用到 java.io.File
类,它是文件和文件目录的抽象表示形式。
在创建 File 类对象的时候,需要指定路径,这样 File 对象就可以与硬盘上的文件或目录建立映射,通过操作 File 类对象来实现对文件或目录的操作。当然指定路径的文件或目录可能不存在,可以通过 File 对象的方法进行判断或创建文件。
使用 File 对象能新建、删除、重命名文件和目录,但不能访问文件内容本身,如果需要访问文件内容本身,则需要使用后面的输入/输出流。
下面演示一下 File 类的使用。
1 创建 File 对象
可以通过文件的绝对路径来创建文件对象,下面在 C 盘 Document 文件夹下创建一个 text.txt 的 File 对象:
File file = new File("C:\\Document\\test.txt");
也可以使用相对路径:
File file = new File("test.txt");
上面表示在当前工作目录下创建 text.txt 的 File 对象,在当前项目中,工作目录就是项目的根目录。
也可以通过指定父路径和当前文件文件的名称来创建:
File file = new File("C:\\Document", "test.txt");
关于绝对路径和相对路径
绝对路径是指从文件系统的根目录(在Windows中通常是某个驱动器,如C:\
;在Unix或Linux中通常是/
)开始,到目标文件或目录的完整路径。它不会依赖于当前工作目录的位置。
示例:
- 在Windows中:
C:\Users\UserName\Documents\file.txt
- 在Unix或Linux中:
/home/username/documents/file.txt
相对路径是指从当前工作目录开始,到目标文件或目录的路径。它会依赖于当前工作目录的位置。
示例:
- 如果当前工作目录是
C:\Users\UserName
,那么相对路径Documents\file.txt
指向C:\Users\UserName\Documents\file.txt
。 - 如果当前工作目录是
/home/username
,那么相对路径documents/file.txt
指向/home/username/documents/file.txt
。
2 常用操作
有了 File 对象,就可以进行下面的一些列操作了。
package com.doubibiji;
import java.io.File;
import java.io.IOException;
public class FileTest {
public static void main(String[] args) throws IOException {
// 在项目根目录下创建 test.txt 对象
File file = new File("test.txt");
// 创建文件
file.createNewFile();
// 判断文件是否存在
System.out.println(file.exists());
// 判断文件是否是目录
System.out.println(file.isDirectory());
// 判断是否是文件
System.out.println(file.isFile());
// 判断文件是否可读
System.out.println(file.canRead());
// 判断文件是否可写
System.out.println(file.canWrite());
// 判断是否是隐藏文件
System.out.println(file.isHidden());
// 获取文件的绝对路径
System.out.println(file.getAbsolutePath());
// 删除文件,这里屏蔽是因为上面创建了文件,这里又删除了,好像什么都没有发生,屏蔽是为了看到这个文件
// file.delete();
}
}
3 创建目录
在实际的开发中,我们会经常遇到这样的需求,判断某个路径是否存在,如果不存在就创建这个目录 。
下面使用代码实现:
public static void main(String[] args) {
// 指定要创建的目录
String filePath = "C:\\docs\\images";
File file = new File(filePath);
// 判断目录是否存在
if (!file.exists()) {
// 创建目录,会按照结构一层一层创建
boolean result = file.mkdirs();
if (result) {
System.out.println("目录创建成功: " + filePath);
} else {
System.out.println("目录创建失败: " + filePath);
}
} else {
System.out.println("目录已存在: " + filePath);
}
}
4 列出目录内容
对于目录,可以使用 listFiles()
方法列出目录下所有的文件。
public static void main(String[] args) {
File file = new File("C:\\docs\\images");
if (file.isDirectory()) {
// 获取目录下所有的文件
File[] files = file.listFiles();
if (files != null) {
// 遍历文件,获取文件名称
for (File f : files) {
System.out.println(f.getName());
}
}
} else {
System.out.println("不是目录");
}
}
5 文件过滤器
上面通过 listFiles()
方法列出目录下所有的文件,我们也可以在列出文件的时候,通过过滤器,列出想要的文件。
例如,下面列出目录下所有的 .jpg
文件
public static void main(String[] args) {
File file = new File("C:\\docs\\images");
if (file.isDirectory()) {
// 获取目录下所有的文件
File[] files = file.listFiles(new FileFilter() {
@Override
public boolean accept(File subFile) {
return subFile.isFile() && subFile.getName().endsWith("jpg");
}
});
// 获取到所有的jpg文件,就可以针对这些文件进行操作,例如下面删除这些文件
for (File jpgFile : files) {
System.out.println(jpgFile.delete());
}
} else {
System.out.println("不是目录");
}
}
6 文件名过滤器
除了文件过滤器,还有文件名过滤器,可以使用文件名称来过滤。
public static void main(String[] args) {
File file = new File("C:\\docs\\images");
String[] arr = file.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith("jpg");
}
});
for (String fileName : arr) {
System.out.println(fileName);
}
}
7 删除目录下的所有文件
如果一个目录下有文件,是无法通过 file.delete()
来删除的。如果要删除一个目录,需要将这个目录下所有的文件和目录都删掉,然后才能删除该目录。这里就涉及到递归删除了,因为该文件夹下还可能有文件夹。
下面写了一个方法 deleteFile()
,可以用来删除文件或目录:
public class FileTest {
public static void main(String[] args) {
File directoryToDelete = new File("docs/images");
// 删除目录
deleteFile(directoryToDelete);
}
/**
* 删除文件或目录
*/
public static void deleteFile(File file) {
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files != null) {
for (File subFile : files) {
deleteFile(subFile); // 递归删除子文件和子目录
}
}
}
// 在此点,目录应该是空的(或本身就不存在),可以安全删除
boolean success = file.delete();
if (success) {
System.out.println("文件已删除: " + file.getAbsolutePath());
} else {
System.out.println("删除文件失败: " + file.getAbsolutePath());
}
}
}
在上面的 deleteFile
方法中,使用了递归删除。
8 File.separator
File.separator 是 Java 中表示系统默认文件目录分隔符的静态变量。
在 Windows 系统中,文件目录分隔符为 \
,而在 Linux 和 UNIX 系统中,文件目录分隔符为 /
。使用 File.separator
可以确保在任何系统下都能正确表示文件路径,从而避免了因为系统差异导致的路径错误问题。
例如,要在 temp 目录下建立一个名为 test.txt 的文件,可以这样做:
File myFile = new File("C:" + File.separator + "temp" + File.separator + "test.txt");
这样,无论是在Windows系统还是Linux系统,都能正确地创建文件。在编程时,应尽量使用这些与系统相关的字段,以确保代码的跨平台兼容性。
9 常用方法
下面列出 File 对象常用的一些方法:
方法 | 含义 |
---|---|
file.createNewFile(); | 创建文件,创建成功返回true,如果存在就不创建了,返回false |
file.mkdir(); | 创建文件夹,创建成功返回true,如果存在就不创建了,返回false |
file.mkdirs(“cc/dd”); | 创建多级目录,创建成功返回true,如果存在就不创建了,返回false |
file.renameTo(); | 如果路径相同,就是改名,如果路径不同,就是改名并剪切 |
file.delete(); | 删除,Java中删除不走回收站,要删除一个文件夹,请注意该文件夹内不能包含文件或文件夹 |
file.setReadable(false); | 设置文件不可读 |
file.canRead(); | windows系统认为所有的文件都是可读的,可读表示是否可以使用IO流读取 |
file.getAbsolutePath(); | 获取绝对路径 |
file.getPath(); | 获取构造方法中传入的路径,如果传给构造方法的是绝对路径就返回的是绝对路径,如果传递给的是相对路径就返回的是相对路径 |
file.list(); | 获取文件夹下的所有文件或文件夹的名称 |
file.listFiles(); | 获取文件夹下的所有文件或文件夹的File对象 |
12.2 IO流
上面在使用 File 对象来进行操作的时候,是无法对文件的内容进行操作的,如果要对文件进行读写,就需要用到 IO 流。
Java对数据的操作是通过流的方式,用于操作流的类都在IO包中;IO流用来处理设备之间的数据传输的,为 数据源
和 目的地
建立一个输送通道,也就是从哪里读取数据和将数据写到哪里;
流按照流向分为两种:
输入流
程序从输入流读取数据源数据。数据源包括外界(键盘、文件、网络…),即是将数据源的数据读入到程序的通信通道。
输出流
程序向输出流写入数据。将程序中的数据输出到外界(显示器、打印机、文件、网络…)的通信通道;
流按照类型分为两种:
字节流
数据流中最小的数据单元是字节,字节流可以操作任何数据,因为在计算机中任何数据都是以字节的形式存储的;
字符流
数据流中最小的数据单元是字符,字符流只能操作纯字符数据, Java中的字符是Unicode编码,一个字符占用两个字节。
字节流可以操作任何数据了,为什么还需要字符流?
为了方便,可以使用不同的编码进行读取和写出;因为数据编码的不同,而有了对字符进行高效操作的流对象。本质其实就是基于字节流读取时,去查了指定的码表。
1 IO操作主要的类
1 File(文件特征与管理)
用于文件或者目录的描述信息,例如生成新目录,修改文件名,删除文件,判断文件所在路径等。
2 InputStream(二进制格式操作)
抽象类,基于字节的输入操作,是所有输入流的父类。定义了所有输入流都具有的共同特征。
3 OutputStream(二进制格式操作)
抽象类。基于字节的输出操作。是所有输出流的父类。定义了所有输出流都具有的共同特征。
InputStream和OutputStream是字节流的最顶层的抽象父类,需要使用其子类来实现功能。
4 Reader(文本格式操作)
抽象类,基于字符的输入操作。
5 Writer(文本格式操作)
抽象类,基于字符的输出操作。
Reader和Writer是字符流的最顶层的抽象父类,需要使用其子类来实现功能。
通过名称就可以分辨出是字节流还是字符流,以Stream还是Reader/Writer结尾。
6 RandomAccessFile(随机文件操作)
一个独立的类,直接继承至Object,它的功能丰富,可以从文件的任意位置进行存取(输入输出)操作。
12.3 字节流
1 FileInputStream
FileInputStream
用于读取文件数据。
1 读取一个字节
有一个 xxx.txt
文件,其中的内容为:abc
FileInputStream fis = new FileInputStream("xxx.txt");
int x = fis.read();
System.out.println(x);
fis.close();
fis.read()
方法会读取文件的一个字节,复制给 int x
,最终打印的结果是 97
。
数据存储在硬盘上是以二进制保存的,字母 a
对应的二进制数据以十进制表示就是 97
,可以查看ASCII码表查看。
fis.read()
每读取一次,指针会向后移动,读取下一个字节。当读到文件的末尾,没有数据时,会返回 -1
,表示文件结束。
2 一个一个读取所有字节
使用一个循环,使用 read()
方法一直读取,直到读取到的内容是 -1
。
FileInputStream fis = new FileInputStream("xxx.txt");
int b;
while((b = fis.read()) != -1) {
System.out.println(b);
}
fis.close();
read()
方法读取的是一个字节,为什么返回是 int
,而不是 byte
?
因为字节输入流可以操作任意文件,比如图片音频等,这些文件底层都是以二进制形式存储的,如果每次都读取返回 byte
,有时候可能在中间的时候遇到 11111111
,那么这个 11111111
是byte类型的 -1
,程序是遇到 -1
就会停止不读了,后面的数据就读取不到了,所以在读取的时候用int类型接收,如果 11111111
会在前面补上 24 个 0
凑足int的4个字节,那么 byte
类型的 -1
就变成int类型的 255
了,这样就可以保证整个数据读完,而结束标记的 -1
就是int类型。
2 FileOutputStream
FileOutputStream
用于写入数据到文件。FileInputStream,FileOutputStream只是读和写的区别。
1 写入一个字节
FileOutputStream fos = new FileOutputStream("yyy.txt"); //创建字节输出流对象,如果没有就自动创建文件
fos.write(97);
fos.write(98);
fos.write(99);
fos.close();
write()
方法会写出一个字节,虽然写出的是 int
数,但是到文件上是一个字节,会自动去除 int
前三个 8位。所以最终 yyy.txt
文件的内容为:abc
。
上面在写入的时候,是会覆盖原来文件中的内容,如果想在原来的内容后面追加内容,可以在创建 FileOutputStream
指定第二个参数:
new FileOutputStream("yyy.txt", true); //第二个参数表示追加
3 IO流功能的核心代码
下面使用 FileInputStream
和 FileOutputStream
来完成文件的复制。
文件的复制也就是读取一个文件的内容,写入到另一个文件。
复制文件功能的样例:
//创建输入流对象
FileInputStream fis = new FileInputStream("aaa.jpg");
//创建输出流对象
FileOutputStream fos = new FileOutputStream("bbb.jpg");
int b;
//不断的读取每一个字节
while ((b = fis.read()) != -1) {
//将每一个字节写出
fos.write(b);
}
//关闭流释放资源
fis.close();
fos.close();
4 缓存数组
上面的代码都是一个字节一个字节的读取和写出,效率非常低,如何才能提高效率呢?可以使用一个字节数组,一次读取多个字节,一次写出多个字节。如下:
xxx.txt
的内容为:abc
// 创建输入流对象
FileInputStream fis = new FileInputStream("xxx.txt");
// 设置一个长度为2的缓存数组
byte[] arr = new byte[2];
// 读取两个字节
int a = fis.read(arr);
System.out.println("a:" + a);
// 查看一下读取的内容
for (byte b : arr) {
System.out.println(b);
}
System.out.println("-----------");
// 再次读取两个字节
int c = fis.read(arr);
System.out.println("c:" + c);
// 查看一下读取的内容
for (byte b : arr) {
System.out.println(b);
}
//关闭流释放资源
fis.close();
打印结果为:
a:2
97
98
c:1
99
98
read(byte[])
方法会读取 byte[]
长度的字节到该字节数组中,同时返回读取的长度。
xxx.txt
的内容为 abc
,为何第二次读取却读取到了99和98?因为第一次读取到两个字节为97和98,第二次读取的时候,将第三个字节读取覆盖数组的第一个位置,后面没有读取的内容了,但数组后面的内容是之前读取到的内容,并没有被修改。所以在写出数据的时候一定要注意,通过使用 read(byte[])
方法的返回值,即读取的长度来控制写入。
5 完整的输入输出流
通过使用一个数组来读取,可以提高读取的效率,下面循环来读取就可以了。
完整的文件输入输出流代码如下:
//创建输入流对象
FileInputStream fis = new FileInputStream("xxx.txt");
//创建输出流对象
FileOutputStream fos = new FileOutputStream("yyy.txt");
byte[] arr = new byte[2];
int len;
while ((len = fis.read(arr)) != -1) {
//控制写入长度
fos.write(arr, 0, len);
}
//关闭流释放资源
fis.close();
fos.close();
fis.read(arr)
返回读取长度,如果等于 -1
表示读取结束,写出的时候,将数组从第0个写出到len个长度。
数组的尺寸一般定义为1024的整数倍1024 * n。
6 BufferedInputStream和BufferedOutputStream
FileInputStream
的 read()
方法读取一个文件,每读取一个字节就要访问一次硬盘,这种读取的方式效率是很低的,尤其是在处理大文件时。所以后面使用 read(byte[])
方法来提高读取效率。
为了提高字节输入流的工作效率,Java还提供了BufferedInputStream类。在创建BufferedInputStream时,会创建一个内部缓冲区数组,缓冲区大小默认是8192字节。当从 BufferedInputStream 中读取一个字节时,BufferedInputStream 会一次性从文件中读取8192个字节,存在缓冲区中,然后返回一个字节给读取的程序,当读取的程序再次读取时,BufferedInputStream 就不用读取文件了,直接从缓冲区中获取数据,返回给读取的程序,直到 BufferedInputStream 缓冲区中所有的字节都被使用过,BufferedInputStream 才重新从文件再读取8192个字节。
BufferedOutputStream也内置了一个缓冲区(就是一个数组),程序向流中写出字节时,不会直接写到文件,先写到缓冲区,直到缓冲区写满,BufferedOutputStream才会把缓冲区的数据一次性写到文件中。
虽然 FileInputStream
的 read(byte[])
方法通过使用缓冲区提高了性能,但 BufferedInputStream
通过其内部的自动缓冲区管理和可能的额外性能优化,通常能够提供更高效和更便捷的I/O操作。因此,在大多数情况下,推荐使用 BufferedInputStream
来读取数据,特别是当处理大文件或需要频繁进行I/O操作时。
1 使用Buffered读取写入
使用方法如下:
使用 BufferedInputStream 对 FileInputStream 进行一层包装即可。
//创建输入流对象,关联文件
FileInputStream fis = new FileInputStream("xxx.txt");
//创建输出流对象,指定输入对象
FileOutputStream fos = new FileOutputStream("yyy.txt");
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);
//定义的数组的意思是每次读取1024个字节到缓冲区
byte[] arr = new byte[1024];
int len;
while ((len = bis.read(arr)) != -1) {
bos.write(arr, 0, len);
}
//只需要关闭包装后的流对象
bis.close();
bos.close();
在关闭资源的时候,只需要关闭包装后的流对象即可,因为可以这样创建BufferedInputStream:
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("xxx.txt"));
下面是每次读取/写入一个字节到Buffered缓冲区:
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
下面是每次读取/写入1024个字节到Buffered缓冲区:
byte[] arr = new byte[1024];
int len;
while ((len = bis.read(arr)) != -1) {
bos.write(arr, 0, len);
}
7 flush方法
在写入文件的时候,会将文件的内容写入到缓冲区,如果想将缓冲区的内容立刻写入到文件,可以调用输出流的 flush()
方法。
flush 和 close 方法的区别:
close()
方法具备刷新的功能,在关闭流之前,会先刷新一次缓冲区,将缓冲区的字节全都刷新到文件上,然后在关闭。
flush()
方法具备刷新缓冲区,将缓冲区数据写到文件上的功能,刷新完成之后可以继续写。如果想将数据写出,就需要 flush
缓冲区。
8 流的标准异常处理
我们使用 IO 流的时候,会有一些异常,在整个处理的过程中,需要考虑资源的关闭,如果不及时关闭资源,会造成资源泄露,进而引发性能问题或内存溢出等错误。
标准的流的异常处理如下:
public static void main(String[] args) throws IOException {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream("xxx.txt");
fos = new FileOutputStream("yyy.txt");
byte[] arr = new byte[1024];
int len;
while ((len = fis.read(arr)) != -1) {
fos.write(arr, 0, len);
}
} catch (IOException e) {
// 处理读写文件时抛出的异常
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
// 处理fis.close()抛出的异常
e.printStackTrace();
}
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
// 处理fos.close()抛出的异常
e.printStackTrace();
}
}
}
try finally嵌套,能关闭一个资源就关闭一个资源,上面是1.6版本的写法。
Java1.7版本对输入输出流进行了优化,提供了自动关闭的功能
try (
FileInputStream fis = new FileInputStream("xxx.txt");
FileOutputStream fos = new FileOutputStream("yyy.txt");
)
{
byte[] arr = new byte[1024];
int len;
while ((len = fis.read(arr)) != -1) {
fos.write(arr, 0, len);
}
}
代码在执行完大括号中的内容后,会自动关闭小括号中的对象。
因为FileInputStream和FileOutputStream实现了AutoCloseable接口,所以在小括号中是无法创建我们自己编写的对象的,除非我们自己编写的对象也实现了AutoCloseable接口:
static class MyClass implements AutoCloseable {
@Override
public void close() {
System.out.println("我关闭了");
}
}
try (
FileInputStream fis = new FileInputStream("xxx.txt");
FileOutputStream fos = new FileOutputStream("yyy.txt");
MyClass myClass = new MyClass();
)
{
...
}
12.4 字符流
字符流是可以直接读写字符的IO流。字符流读取字符,就要先读取到字节数据,然后转为字符,如果要写出字符,需要把字符转换为字节再写出,通过使用不同的字符编码进行转换。
1 FileReader&FileWriter
FileReader的使用和FileInputStream的使用类似,但是操作的是字符,如下:
FileReader fr = new FileReader("xxx.txt");
int c;
while ((c = fr.read()) != -1) {
System.out.print((char)c);
}
fr.close();
使用 fr.read()
方法,一次读取一个字符,不是一个字节。系统根据编码规则,读取一个字符,汉字也是一个字符。
// FileWriter的使用和FileOutputStream的使用类似,但是操作的是字符,如下:
FileWriter fw = new FileWriter("yyy.txt"); //没有文件会自动创建
//直接写入字符串
fw.write("你好");
//直接写入字符
fw.write(97);
//直接写如字符数组
fw.write(new char[]{'你','好','呀'});
fw.close();
使用 FileReader 和 FileWriter 进行复制,举个栗子:
public static void main(String[] args) throws IOException {
FileReader fr = new FileReader("xxx.txt");
FileWriter fw = new FileWriter("yyy.txt");
int c;
while ((c = fr.read()) != -1) {
fw.write(c);
}
fr.close();
fw.close();;
}
分析:FileReader和FileWriter是自带缓冲区的,根据底层源码分析,缓冲区是2KB。
如果不关闭写入流fw,则最后的字符流数据会在缓冲区中,不能写入到文件中,所以不能忘记关闭 fw.close();
注意1:什么情况下使用字符流?
字符流也可以纯拷贝文本文件,但不推荐使用,因为读取时会把字节流转为字符,写出时还要把字符转换为字节,多了两个转换的操作。
程序需要读取一段文本,或是需要写出一段文本的时候,可以使用字符流。
注意2:字符流不可以拷贝非纯文本的文件
因为在读取的时候会将字节转换为字符,在转换的过程中,根据字符编码将字节转换为字符,有可能读取的字节在对应的字符编码表中找不到对应的字符,就用 ?
代替,写出的时候再将字符转换为字节写出去,如果是 ?
则直接写出,这样写出之后的文件就乱了,看不了了。
使用字符数组读取和写入字符流,举个栗子:
public static void main(String[] args) throws IOException {
FileReader fr = new FileReader("xxx.txt");
FileWriter fw = new FileWriter("yyy.txt");
char[] arr = new char[1024];
int len;
while ((len = fr.read(arr)) != -1) {
fw.write(arr, 0, len);
}
fr.close();
fw.close();;
}
2 BufferedReader&BufferedWriter
BufferedReader和BufferedWriter的使用和BufferedInputStream和BufferedOutputStream的使用方法类似,BufferedReader和BufferedWriter是使用缓冲区的输入输出字符流。
以下是copy的demo:
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new FileReader("xxx.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("yyy.txt"));
int c;
while ((c = br.read()) != -1) {
bw.write(c);
}
br.close();
bw.close();
}
BufferedReader的read()方法读取字符时会先将字符读取若干字符到缓冲区,然后逐个返回给程序,降低读取硬盘文件的次数,提高效率。BufferedWriter的writer()方法写出字符时会先写到缓冲区,缓冲区写满时才会写到文件。
readLine()
方法和 newLine()
方法:
- readLine():BufferedReader中的方法,可以读取一行文本,当遇到
\r
或\n
时为一行的终止。方法返回一个字符串,如果已到达流末尾,即读取文件结束,则返回null。 - newLine():BufferedWriter中的方法,可以写入一个行分隔符。
下面演示一个样例:
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new FileReader("xxx.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("yyy.txt"));
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine();
//bw.write("\r\n"); // 和这个效果一样
}
br.close();
bw.close();
}
分析:上面的程序按照行依次读取和写入字符流,在写入的时候,需要使用 bw.newLine();
或 bw.write("\r\n");
进行换行,否则所有的字符将写在一行中。
newLine()
和 write("\r\n")
什么区别呢?newLine()
是跨平台的,write("\r\n")
只支持的是windows系统,在Linux系统中 \n
是换行。
3 FileReader和BufferedReader的区别
BufferedReader 是对于 FileReader 添加了一个缓冲区,那么 FileReader 不是本来就存在缓冲区吗?
对比源码我们发现,BufferedReader继承了Reader的read(char[] buf)方法,而改写了read()和read(char[] buf, int off, int len)方法,使用read(char[] buf)方法的效果一样,都是读取到数组中,然后从数组中读取数据,但是read()方法,一次只读一个字符,BufferedReader覆盖了Reader的read方法,BufferedReader会将字符数据读取到一个数组中,相当于调用了bufr.read(buf)方法,然后再从这个缓存区中一个一个的读取字符数据。
这是BufferedReader的read方法的源码,我们发现,即使是对于一个字符数据的读取,BufferedReader依旧是先读取一个数组的数据到缓冲区中,然后从缓冲区中一个个的取,对于read方法而言,BufferedReader是比FileReader进行了优化,减少了io操作,但是对于read(char[] buf)的操作而言,两个类都继承与Reader,所以并没有差别。
12.5 其他流操作
1 Input/OutputStreamWriter
InputStreamReader 可以将字节流转换为字符流输入;
OutputStreamWriter 可以将字符流转换为字节流输出;
还具有的作用是在输入输出的时候指定字符编码,demo如下:
public static void main(String[] args) throws IOException {
InputStreamReader isr = new InputStreamReader(new FileInputStream("xxx.txt"), "utf-8");
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("yyy.txt"), "gbk");
int c;
while ((c = isr.read()) != -1) {
osw.write(c);
}
isr.close();
osw.close();
}
可以指定在输入和输出的时候指定不同的字符,为了适应不同文本文件存在不同编码而导致读写乱码的问题。
当然,我们在读写的时候可以使用BufferedReader和BufferedWriter进行装饰,提高效率,demo如下:
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("xxx.txt"), "utf-8"));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("yyy.txt"), "gbk"));
int c;
while ((c = br.read()) != -1) {
bw.write(c);
}
br.close();
bw.close();
}
2 序列流SequenceInputStream
如果想要将两个文件的内容写到一个文件中,则需要定义两个输入流,一个输出流,然后依次将输入流写到输出流中,代码如下:
public static void main(String[] args) throws IOException {
FileInputStream fis1 = new FileInputStream("xxx.txt");
FileOutputStream fos = new FileOutputStream("zzz.txt");
int b;
while ((b = fis1.read()) != -1) {
fos.write(b);
}
fis1.close();
int c;
FileInputStream fis2 = new FileInputStream("yyy.txt");
while ((c = fis2.read()) != -1) {
fos.write(c);
}
fis2.close();
fos.close();
}
此时可以使用SequenceInputStream,将两个输入流依次传递给SequenceInputStream,整合成一个流,然后通过输出流写出,代码如下:
public static void main(String[] args) throws IOException {
FileInputStream fis1 = new FileInputStream("xxx.txt");
FileInputStream fis2 = new FileInputStream("yyy.txt");
SequenceInputStream sis = new SequenceInputStream(fis1, fis2);
FileOutputStream fos = new FileOutputStream("zzz.txt");
int b;
while ((b = sis.read()) != -1) {
fos.write(b);
}
//sis在关闭的时候,会将构造方法中传入的流对象都关闭
sis.close();
fos.close();
}
什么是序列流?
序列流可以把多个字节输入流整合成一个,从序列流中读取数据时,将从被整合的第一个流开始读,读完一个之后会继续读取第二个,以此类推。
如果有多个输入流,使用SequenceInputStream该怎么整合呢?
使用集合的方式进行整合。
public static void main(String[] args) throws IOException {
FileInputStream fis1 = new FileInputStream("aaa.txt");
FileInputStream fis2 = new FileInputStream("bbb.txt");
FileInputStream fis3 = new FileInputStream("ccc.txt");
Vector<FileInputStream> v = new Vector<>();
v.add(fis1);
v.add(fis2);
v.add(fis3);
SequenceInputStream sis = new SequenceInputStream(v.elements());
FileOutputStream fos = new FileOutputStream("zzz.txt");
int b;
while ((b = sis.read()) != -1) {
fos.write(b);
}
//sis在关闭的时候,会将构造方法中传入的流对象都关闭
sis.close();
fos.close();
}
3 内存输出流
什么是内存输出流?
该输出流可以向内存中写数据,把内存当做一个缓冲区,写出之后可以一次性获取出所有数据。
此处用到的是ByteArrayOutputStream,此类实现了一个输出流,其中的数据被写入一个byte数组缓冲区,缓冲区会随着数据的不断写入而自动增长。可使用toByteArray()和toString()获取数据。
注意,因为ByteArrayOutputStream不是和文件关联的,而是在内存中,所以关闭ByteArrayOutputStream无效,此类中的方法在在调用close()方法关闭流后仍可被调用,不会产生IO异常。
在将一个文本文件中的内容读取去来打印到控制台的时候,为了不出现乱码,我们可以使用字符流来读取,因为使用字节流不知道一次读取多少个字节算一个字符。
现在还可以使用ByteArrayOutputStream来读取,将所有的内容读取到内存中,然后再转换为字符串,代码如下:
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("xxx.txt");
//在内存中创建了一个可以自动增长的内存数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b;
while ((b = fis.read()) != -1) {
//将读取到的数据依次写入到内存中
baos.write(b);
}
//将缓冲区的数据全部取出来,并复制给arr
byte[] arr = baos.toByteArray();
System.out.println(new String(arr));
fis.close();
//baos不需要关闭
}
ByteArrayOutputStream中的toString()方法可以将缓冲区的内容直接转换为字符串,用的是平台的默认编码,如果需要指定字符编码,可以使用.toByteArray()方法,然后再讲字节数组通过指定字符编码转换为字符串。
4 随机访问流
RandomAccessFile
RandomAccessFile类不属于流,是Object类的子类。但它融合了InputStream和OutputStream的功能。支持对随机访问文件的读取和写入。
RandomAccessFile作用可以支持多线程下载,不同的线程从文件不同的位置去写入。
下面给出一个demo,在文件的不同位置写入和更改数据。
public static void main(String[] args) throws IOException {
RandomAccessFile raf = new RandomAccessFile("xxx.txt", "rw");
//将指针移动到index为3的位置
raf.seek(3);
int b = 97;
//将index为3的位置的数据替换为字节97
raf.write(b);
raf.close();
}
r:以只读方式打开。调用结果对象的任何write方法都将导致抛出IOException。
rw:打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。
rws:打开以便读取和写入,对于rw而言,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
rwd:打开以便读取和写入,对于rw而言,还需要对文件内容的每个更新都同步写入到底层存储设备。
5 对象操作流
什么是对象操作流?
该流可以将一个对象写出,或者读取一个对象到程序中,也就是执行了序列化和反序列化的操作。
序列化:将对象写到文件上;
反序列化:将文件上的数据转换为对象。
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("xxx.txt"));
Dog dog1 = new Dog("大黄", 3);
Dog dog2 = new Dog("小黑", 4);
//将对象序列化保存到文件中
oos.writeObject(dog1);
oos.writeObject(dog2);
oos.close();
//从文件中,将序列化的数据反序列化成对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("xxx.txt"));
Dog dog3 = (Dog)ois.readObject();
Dog dog4 = (Dog)ois.readObject();
System.out.println(dog3);
System.out.println(dog4);
ois.close();
}
上面的代码将两个对象序列化保存到文件中,然后反序列化获取两个对象。
如果获取的时候继续获取第三个对象,则或报出EOFException,即EndOfFileException,到达文件末尾的异常,如何优化这个问题呢?
可以将多个对象加入到一个集合中,然后将集合序列化保存到文件中,然后在从文件中反序列化得到集合的对象,然后在遍历集合获取到对象。
在需要序列化的类需要实现Serializable接口,其中需要定义一个serialVersionUID常量,如果不定义系统会自动生成一个serialVersionUID,但是如果对象后来添加了属性,再直接去读取文件进行反序列化会报错。所以需要为类指定一个serialVersionUID,这样对类改动不会报错。
6 数据输入输出流
什么是数据输入输出流?
DataInputStream,DataOutputStream可以按照基本数据类型大小读写数据。
例如按Long大小写出一个数字,写出是该数据占8个字节,读取的时候也按照Long类型读取,一次读取8个字节。
在开发中字节流和字符流用的较多,其他流用的比较少,以下是数据输入输出流Demo演示:
public static void main(String[] args) throws IOException, ClassNotFoundException {
//定义一个数据输出流,往文件中写出数据
DataOutputStream dos = new DataOutputStream(new FileOutputStream("xxx.txt"));
dos.writeInt(997);
dos.writeInt(998);
dos.writeInt(999);
dos.close();
//用数据输入流从文件中读入数据
DataInputStream dis = new DataInputStream(new FileInputStream("xxx.txt"));
int x = dis.readInt();
int y = dis.readInt();
int z = dis.readInt();
System.out.println(x);
System.out.println(y);
System.out.println(z);
dis.close();
}
7 打印流
什么是打印流?
该流可以很方便的将对象的toString()结果输出,并且自动加上换行,而且可以使用自动刷出的模式。
System.out就是一个PrintStream,其默认向控制台输出信息。
1 字节打印流
PrintStream ps = System.out;
ps.println(97); //其实底层用的是Integer.toString(x),将x转换为数字字符打印
ps.println("xxx");
ps.println(new Dog("DaHuang"));
Dog dog = null;
ps.println(dog); //如果是null,就打印null,如果不是null,就调用对象的toString();
注意,如果输入输出流不和文件关联,可以不关闭
2 字符打印流
PrintWriter pw = new PrintWriter(new FileOutputStream("aaa.txt"), true);
pw.write(97);
pw.print("大家好");
pw.println("你好"); //自动刷出,只针对的是println()方法,直接将缓冲区内容写入到文件
pw.close();
8 标准输入输出流
什么是标准输入输出流?
System.in是InputStream,标准输入流,默认可以从键盘读取输入字节数据;
System.out是PrintStream,标准输出流,默认可以向Console中输出字符和字节数据。
修改标准输入输出流
修改输入流:System.setIn(InputStream)
修改输出流:System.setOut(PrintStream)
样例:
下面的功能相当于拷贝文件,只是演示,开发时千万不要使用。
public static void main(String[] args) throws IOException {
System.setIn(new FileInputStream("aaa.txt")); //改变标准输入流,从文件读取
System.setOut(new PrintStream("bbb.txt")); //改变标准输出流,写入到文件
InputStream is = System.in; //获取标准的键盘输入流,默认指向键盘,改变后指向文件
PrintStream ps = System.out; //获取标准输出流,默认指向的是控制台,改变后指向文件
int b;
while ((b = is.read()) != -1) {
ps.write(b);
}
//System.out.println(); //也是一个输出流,不用关,因为没有和硬盘上的文件建立管道
is.close();
ps.close();
}
可以使用BufferedReader对标准输入输出流进行包装,按行读取。
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line = br.readLine();
System.out.println(line);
br.close();
等作用于:
Scanner sc = new Scanner(System.in);
String line = sc.nextLIne();
System.out.println(line);
Scanner不仅仅是可以以System.in作为参数,还可以是文件,不只是针对键盘输入。
9 Properties
Properties类表示一个持久的属性集合;
Properties可保存在流中或从流中加载;
属性列表中每个键及其对应值都是一个字符串。
Properties继承自Hashtable,所以可以作为Map来使用:
Properties prop = new Properties();
prop.put("abc", 123);
System.out.println(prop.get("abc"));
1 Properties的操作
public Object setProperty(String key, String value); //添加元素
public String getProperty(String key); //根据键获取
public Enumeration<?> propertyNames(); //获取所有健
举个栗子:
Properties prop = new Properties();
prop.setProperty("name", "zhangsan");
prop.setProperty("tel", "13712345678");
//遍历
Enumeration<String> en = (Enumeration<String>) prop.propertyNames();
while (en.hasMoreElements()) {
String key = en.nextElement();
String value = prop.getProperty(key);
System.out.println(key + " = " + value);
}
2 IO流
Properties一般配合配置文件使用,配置文件是在硬盘上,Properties是在内存中,所以牵扯到IO流。
Properties 的 load() 和 store() 方法:
load() :从文件读取配置;
store() :将配置保存到文件中。
样例:
public static void main(String[] args) throws IOException {
Properties prop = new Properties();
System.out.println("读取前:" + prop);
//从文件加载配置
prop.load(new FileInputStream("config.properties"));
prop.setProperty("tel", "13711111111");
//将配置写在文件
//第二个参数是用来描述文件列表的,如果不描述,可以传递null。
prop.store(new FileOutputStream("config.properties"), null);
System.out.println("读取后:" + prop);
}
一些项目库中常用于读取数据库配置,如下:
InputStream in = JdbcPool.class.getClassLoader().getResourceAsStream("db.properties");
Properties prop = new Properties();
try {
prop.load(in);
String driver = prop.getProperty("driver");
String url = prop.getProperty("url");
String username = prop.getProperty("username");
String password = prop.getProperty("password");
} catch (Exception e) {
e.printStackTrace();
}