封面来源:碧蓝航线 北境序曲 活动CG
本文参考:尚硅谷 宋红康 Java 零基础教程 P577-P619
1. File 类的使用
1.1 路径分隔符
路径中的每级目录之间用一个 路径分隔符 隔开。
路径分隔符与系统有关:
- windows 和 DOS 系统默认使用 “\” 来表示
- UNIX 和 URL 使用 “/” 来表示
Java 程序支持跨平台运行,因此路径分隔符要 慎用 !
为了解决这个隐患,File 类提供了一个常量:
1
| public static final String separator
|
使用这个常量可以根据操作系统,动态的提供分隔符。比如:
1 2 3
| File file1 = new File("D:\\mofan.txt");
File file2 = new File("D:" + File.separator + "mofan.txt");
File file3 = new File("d:/mofan");
|
这里出现了路径的一种写法:绝对路径,既然有绝对路径,那么也有相对路径。
- 相对路径:相较于某个路径,指明的路径
- 绝对路径:包含盘符在内的文件或文件目录的路径
相对路径的注意要点
IDEA 中:
如果开发使用 JUnit 中的单元测试方法进行测试,相对路径即为当前 Module 下。
如果使用 main() 进行测试,相对路径即为当前 Project 下。
1 2 3 4 5 6 7 8 9 10
| public static void main(String[] args) {
// 相当于当前工程
File file = new File("hello.txt");
// D:\Code\IdeaCode\JavaSE\hello.txt
System.out.println(file.getAbsolutePath());
File file1 = new File("IO-Stream\\hello.txt");
// D:\Code\IdeaCode\JavaSE\IO-Stream\hello.txt
System.out.println(file1.getAbsolutePath());
}
|
Eclipse 中:
不管使用单元测试方法还是使用 main() 方法,相对路径都是当前 Project 下。
1.2 常用构造器
第一种
1
| public File(String pathname)
|
以 pathname 为路径创建 File 对象,可以是 绝对路径或相对路径 ,如果 pathname 是相对路径,则默认的当前路径在系统属性 user.dir 中存储。
- 绝对路径:是一个固定的路径,从盘符开始
- 相对路径:相对于某个位置开始
第二种
1
| public File(String parent, String child)
|
以 parent 为父路径,child 为子路径创建 File 对象
第三种
1
| public File(File parent, String child)
|
根据一个父 File 对象和子文件路径创建 File 对象
1.3 常用方法
File 类的获取功能
public String getAbsolutePath():获取绝对路径
public String getPath():获取路径
public String getName() :获取名称
public String getParent():获取上层文件目录路径。若无,返回null
public long length() :获取文件长度 (即:字节数) ,但不能获取目录的长度。
public long lastModified() :获取最后一次的修改时间,毫秒值
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Test
public void test2() {
File file1 = new File("hello.txt");
File file2 = new File("D:\\io\\hi.txt");
System.out.println(file1.getAbsoluteFile());
System.out.println(file1.getPath());
System.out.println(file1.getName());
System.out.println(file1.getParent());
System.out.println(file1.length());
// 返回毫秒级别的时间戳
System.out.println(new Date(file1.lastModified()));
System.out.println();
System.out.println(file2.getAbsoluteFile());
System.out.println(file2.getPath());
System.out.println(file2.getName());
System.out.println(file2.getParent());
System.out.println(file2.length());
System.out.println(file2.lastModified());
}
|
如下两个方法适用于文件目录:
public String[] list() :获取指定目录下的所有文件或者文件目录的名称数组
public File[] listFiles() :获取指定目录下的所有文件或者文件目录的 File 数组
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Test
public void test3() {
File file = new File("D:\\io");
String[] list = file.list();
for (String s : list) {
// 打印出文件名
System.out.println(s);
}
File[] files = file.listFiles();
for (File f : files) {
// 打印出绝对路径
System.out.println(f);
}
}
|
File 类的重命名功能
public boolean renameTo(File dest):把文件重命名为指定的文件路径。
针对 file1.renameTo(file2) 来说,需要 file1 存在,但是 file2 不存在,将 file1 移动到 file2 并改名为 file2 (相当于剪切与重命名)。
测试代码:
1 2 3 4 5 6 7 8 9 10 11
| @Test
public void test4() {
// hello.txt 存在
File file1 = new File("hello.txt");
// hi.txt 并不存在
File file2 = new File("D:\\io\\hi.txt");
boolean renameTo = file1.renameTo(file2);
// 要想保证返回 true,file1 要存在,file2 要不存在
System.out.println(renameTo);
}
|
File 类的判断功能
public boolean isDirectory():判断是否是文件目录
public boolean isFile():判断是否是文件
public boolean exists():判断是否存在
public boolean canRead():判断是否可读
public boolean canWrite():判断是否可写
public boolean isHidden():判断是否隐藏
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Test
public void test5() {
File file1 = new File("hello.txt");
System.out.println(file1.isDirectory());
System.out.println(file1.isFile());
System.out.println(file1.exists());
System.out.println(file1.canRead());
System.out.println(file1.canWrite());
System.out.println(file1.isHidden());
System.out.println("======================");
File file2 = new File("D:\\io");
System.out.println(file2.isDirectory());
System.out.println(file2.isFile());
System.out.println(file2.exists());
System.out.println(file2.canRead());
System.out.println(file2.canWrite());
System.out.println(file2.isHidden());
}
|
FIle 类的创建功能
public boolean createNewFile():创建文件。若文件存在,则不创建并返回 false 。
public boolean mkdir() :创建文件目录。如果此文件目录存在,就不创建。如果此文件目录的上层目录不存在,也不创建。
public boolean mkdirs():创建文件目录。如果上层文件目录不存在, 一并创建。
注意:如果你创建文件或者文件目录没有写盘符路径,那么默认在项目路径下。
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| // 文件目录的创建
@Test
public void test7() {
// io1 并不存在
File file1 = new File("D:\\io\\io1\\io3");
boolean mkdir1 = file1.mkdir();
if (mkdir1) {
System.out.println("创建成功1"); // 不会打印
}
// io1 并不存在
File file2 = new File("D:\\io\\io1\\io4");
boolean mkdir2 = file1.mkdirs();
if (mkdir2) {
System.out.println("创建成功2"); // 最终打印
}
}
|
当硬盘中真有一个真实的文件或目录存在,创建 File 对象的时候,各个属性会被显式赋值。
当硬盘中 没有 真实的文件或目录存在时,那么创建 File 对象时,除了指定的目录和路径之外,其他的属性都是取成员变量的默认值 。
File 类的删除功能
public boolean delete():删除文件或者文件夹
删除注意事项:
Java 中的删除不走回收站 。要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录。
1.4 总结
使用 File 类时,我们需要明白:
1、File 类的一个对象,代表一个文件或一个文件目录
2、File 类声明在 java.io 包下
3、File 类中设计到关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法,但是并未设计到写入或读取文件内容的操作。如果需要读取或写入文件内容,就必须使用 IO 流来完成。
4、后续 File 类的对象常会作为参数传递到流的构造器中,指明读取或写入的“终点”。
2. IO 流概述
2.1 Java IO 原理
I/O 是 Input / Output 的缩写,I/O 技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。😮
Java 程序中,对于数据的输入 / 输出操作以“流(stream)”的方式进行。
java.io 包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。
输入 input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中
输出 output:将程序(内存)数据输出到磁盘、光盘等存储设备中。
输入与输出针对不同的对象,会得到不同的结果。比如站在文件的角度来看,将文件中的数据写入到内存中,那这就是输出,但从内存的角度来看,这就是输入。因为我们是编写代码的人,因此我们需要 站在内存(程序)的角度 来看待 IO。
2.2 流的分类
按操作数据单位不同可分为:字节流(8 bit)、字符流(16 bit)
按数据流的流向不同分为:输入流、输出流
按流的角色不同分为:节点流、处理流
| 抽象基类 |
字节流 |
字符流 |
| 输入流 |
InputStream |
Reader |
| 输出流 |
OutputStream |
Writer |
Java 的 IO 流共涉及到 40 多个类,实际上非常规则,都是从如下 4 个抽象基类派生的。
由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。
当我们操作文本数据时,里面的数据都是一个一个的字符,所以用字符流;当我们操作视频、音频或图片时,就需要用字节流。
针对数据流的流向,我们是一直站在程序(内存)的角度出发的,写入内存叫输入流,从内存中输出到外部存储就叫输出流。
如果流直接作用在文件上,那么这叫节点流;如果当前流作用在已有的流的基础之上,那么当前流叫做处理流(可以理解成:水流等价于节点流,管道等价于处理流)。😵
关于节点流和处理流还可以这样理解:自己就可以操作的叫节点流,依赖于别的流操作的叫处理流。😰
IO 流体系图
Java IO 流体系图参考:Java IO 流体系图
| 分类 |
字节输入流 |
字节输出流 |
字符输入流 |
字符输出流 |
| 抽象基类 |
InputStream |
OutputStream |
Reader |
Writer |
| 访问文件 |
FileInputStream |
FileOutputStream |
FileReader |
FileWriter |
| 访问数组 |
ByteArrayInputStream |
ByteArrayOutputStream |
CharArrayReader |
CharArrayWriter |
| 访问管理 |
PipedInputStream |
PipedOutputStream |
PipedReader |
PipedWriter |
| 访问字符串 |
|
|
StringReader |
StringWriter |
| 缓冲流 |
BufferedInputStream |
BufferedOuputStream |
BufferedReader |
BufferedWriter |
| 转换流 |
|
|
InputStreamReader |
OutputStreamWriter |
| 对象流 |
ObjectInputStream |
ObjectOutputStream |
|
|
|
FilterInputStream |
FilterOutputStream |
FilterReader |
FilterWriter |
| 打印流 |
|
PrintStream |
|
PrintWriter |
| 推回输入流 |
PushbackInputStream |
|
PushbackReader |
|
| 特殊流 |
DataInputStream |
DataOutputStream |
|
|
重点放在:抽象基类、访问文件、缓冲流、转换流和对象流上。
3. 字符流
3.1 数据读入
使用 FileReader 读取文件中的数据,测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| /*
* 将 IO-Stream 下的 hello.txt 文件内容输出到控制台
* 1. 异常的处理:为了保证资源一定可以执行关闭操作,不建议使用 throws ,采用 try-catch 更好
* 2. 读入的文件一定要存在,否则会抛出 FileNotFoundException 异常
* */
@Test
public void testFileReader() {
FileReader fileReader = null;
try {
// 1. 实例化 File,指明要操作的文件
File file = new File("hello.txt"); // 当前 Module 下存在 hello.txt 文件
// 2. 提供具体的流
fileReader = new FileReader(file);
// 3. 数据的流入
// read(): 返回读入的一个字符(ASCII码)。如果达到文件末位,返回 -1
/*int data = fileReader.read();
while (data != -1) {
System.out.print(((char) data));
data = fileReader.read();
}*/
// 优化一下
int data;
while ((data = fileReader.read()) != -1) {
System.out.print(((char) data));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fileReader != null) {
// 4. 关闭流
fileReader.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
|
在上述代码中,FileReader 的构造方法、read() 和 close() 都会抛出异常,为了保证资源一定可以执行关闭操作,不建议使用 throws,采用 try-catch 更好。
被读取文件一定要存在,否则会抛出 FileNotFoundException 异常。
在上述代码中,读取文件中的内容使用了 read() 方法,这个方法可以读取单个字符 ,如果纯文本文件中有内容的话,就会返回 ASCII 码表中对应的数字值,如果 ASCII 表中不存在,就会去查找 Unicode 表。将字符读取完毕后,会将读取目标指向下一个字符,因此常与循环一起使用。
如果纯文本文件没有内容 ,或者前面的内容已经被读取完了,read() 返回的就是 -1,由于我们不知道文本内容有多少,所以我们需要使用一个循环。
read() 方法只能读取一个字符,如果读取的文本很长,那效率也太低了。😳
因此,我们想可不可以一次性多读取一点,然后放到数组或字符串中,那样多奈斯!😏
JDK 提供了一个 read() 的重载方法,可以一次性多读取一点,并将其放到 char[] 中。测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| // 对 read() 的操作升级,使用 read() 的重载方法
@Test
public void testFileReader1(){
FileReader fileReader = null;
try {
// 1. File 类的实例化
File file = new File("hello.txt");
// 2. FileReader流的实例化
fileReader = new FileReader(file);
// 3. 读入的操作
// read(char[] cBuf):返回每次读入 cBuf 数组中的字符的个数。如果达到文件末尾,返回 -1
char[] cBuf = new char[5];
int len;
while ((len = fileReader.read(cBuf)) != -1) {
// 方式一
// 错误的写法
// for (int i = 0; i < cBuf.length; i++) {
// System.out.print(cBuf[i]);
// }
// 正确的写法
// for (int i = 0; i < len; i++) {
// System.out.print(cBuf[i]);
// }
// 方式二
// 错误的写法,对应方式一错误的写法
// String str = new String(cBuf);
// System.out.print(str);
// 正确的写法
String str = new String(cBuf, 0, len);
System.out.print(str);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4. 资源关闭
try {
if (fileReader != null) {
fileReader.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
|
read(char[] cBuf):返回每次读入 cBuf 数组中的字符的个数。如果达到文件末尾,返回 -1 。
使用 read(char[] cBuf) 方法时,将读取字符数组。假设字符数组的长度是 2,那么一口气读的是 2 个字符,但由于文本内容只有 3 个字符,那么第二次读取的时候,可读取的有效长度就是 1 ,那么存放在字符数组的第 1 位就会被覆盖掉,但是第 2 位没有被覆盖,就会保留。
如果我们直接读取字符串,而不指定有效长度的话,会按字符数组的长度读取,并将没覆盖的字符读取出来,而不是只读有效长度。
所以我们在创建一个 String 对象的时候,要如此使用。
1
| String str = new String(a, 0, len);
|
a 代表的是字符数组,0 代表起始位置,len 代表读取的长度。
参考链接:FileReader用法和问答
3.2 数据写出
有了读入,当然还有写出,字符流写出将用到 FileWriter ,测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| // 从内存中写出数据到硬盘文件中
@Test
public void testFileWriter() {
FileWriter fileWriter = null;
try {
// 1. 提供 File 类的对象,指明写出到的文件
File file = new File("hello1.txt");
// 2. 提供 FileWriter 的对象,用于数据的写出
fileWriter = new FileWriter(file);
// 3. 写出的操作
fileWriter.write("I have a dream!\n");
fileWriter.write("You need to have a dream!\n");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fileWriter != null) {
// 4. 流资源的关闭
fileWriter.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
|
使用字符流写出数据时,有几点需要进行说明:
1、使用字符流进行写出操作时,内容写入的目标文件可以不存在,且不会报异常。
2、File 对应的硬盘中的文件如果不存在,在输出的过程中,会自动创建文件。
3、File 对应的硬盘中的文件如果存在:
-
如果流使用构造器是 FileWriter(file, false) 或 FileWriter(file) 将会对原有文件进行覆盖
-
如果流使用构造器是 FileWriter(file, true) 不会对原有文件进行覆盖,而是原有文件基础上追加内容
3.3 读入与写出
前面列举了数据读入和写出,但是读入是将文本文件内容输出到控制台上,写入是使用代码的方式直接写入到文本文件中,那能否将两者结合一下?
先读入 A 文件的内容,然后将该内容写入到 B 文件中呢?(这就是传说中的文本复制! 😂)
其实也很简单,直接上测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| @Test
public void testFileReaderFileWriter() {
FileReader fileReader = null;
FileWriter fileWriter = null;
try {
// 1. 创建 File 类的对象,指明读入和写出的文件
File srcFile = new File("hello.txt"); // 该文件存在当前 Module 中
File destFile = new File("hello2.txt"); // 该文件并不存在当前 Module 中
// 2. 创建输出流和输入流的对象
fileReader = new FileReader(srcFile);
fileWriter = new FileWriter(destFile);
// 3. 数据的读入和写出操作
char[] cBuf = new char[5];
int len; // 记录每次写入到 cBuf 数组中的字符的个数
while ((len = fileReader.read(cBuf)) != -1) {
// 每次写出 len 个字符
fileWriter.write(cBuf, 0 ,len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4. 关闭流资源
try {
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
fileReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
|
3.4 拓展
我们前面编写的代码都是处理文本文件的,那么那些代码可以处理图片吗?
是否只需要将 File 构造器内的文件名修改一下就可以了呢?比如:
1 2 3
| // 1. 创建 File 类的对象,指明读入和写出的文件
File srcFile = new File("pic.jpg");
File desFile = new File("pic2.jpg");
|
其实测试一下就知道,压根是不行的,虽然 pic2.jpg 会出现在我们的 Module 下,但并不能打开该文件。
原因也很简单,FileReader 和 FileWriter 都是处理字符流的,或者说都是处理文本文件的,而图片并不是文本文件,属于二进制文件,要想处理图片,就需要用到字节流! 👇
4. 字节流
4.1 字节输入流初体验
上文说到,对于文本文件(.txt, .java, .c, .cpp 等)使用字符流处理,对于非文本文件(.jpg, .mp3, .mp4, .pdf 等)使用字节流处理。我们也验证了使用字符流 FileReader 和 FileWriter 处理非文本文件会出错,那么使用字节流 FileInputStream 和 FileOutputStream 来处理文本文件会怎么样呢?
不多 BB,直接上代码,步骤和格式与前面的操作差不多:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| // 使用字节流 FileInputStream 处理文本文件可能出现乱码
@Test
public void testFileInputStream() {
FileInputStream fileInputStream = null;
try {
// 操作文件
File file = new File("hello.txt"); // 该文件存在当前 Module 中
// 造流
fileInputStream = new FileInputStream(file);
// 读数据
byte[] bytes = new byte[5];
int len; // 记录每次读取的字节的个数
while ((len = fileInputStream.read(bytes)) != -1) {
String s = new String(bytes, 0, len);
System.out.print(s);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
// 关闭资源
if (fileInputStream != null) {
fileInputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
|
相信聪明的你一定看到贴出代码的首行注释了:使用字节流 FileInputStream 处理文本文件可能出现乱码。于是你一通 cv 把代码拷贝到自己的项目中,然后发现并没有出现乱码,这是怎么回事?
可以看一下 hello.txt 文件的编码,一般来说都是 UTF-8 ,在 UTF-8 中,英文字符和数字也都是一个字节,使用字节流传输是没有什么问题的,但是如果你加几个中文汉字呢?
于是你又一番操作,结果发现还是没乱码?咋回事?(第一次这么想让汉字乱码😂)
原因也很简单。 在 UTF-8 编码中,一个汉字占 3 个字节。 我们代码中设置的字节数组长度为 5,然后你编写的文本中的数据恰好也是 5 字节的整数倍,不妨再加一个汉字,看看会不会乱码?
我相信,一定会乱码的!
因此结论也得出来了:使用字节流 FileInputStream 处理文本文件可能出现乱码。(这可能二字用得就很妙!)
总的来说,操作文本文件还是使用字符流好,毕竟在字符流中,无论英文、数字,抑或汉字,都算一个字符,如此下来,使用字符流操作文本文件是不会出现乱码的情况的。
扯了这么多,接下来就该进入主题了,使用字节流操作非文本文件。
4.2 操作非文本文件
在本节我们使用字节流来操作一下非文本文件,通俗一点:复制图片。😝
操作步骤和前面的复制文本文件一下,方法也基本一样,只是换几个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| // 实现对图片的复制
@Test
public void testFileInputOutputStream() {
FileInputStream fileInputStream = null;
FileOutputStream fileOutputStream = null;
try {
File srcFile = new File("妖梦.jpg"); // 该文件存在当前 Module 中
File destFile = new File("魂魄妖梦.jpg"); // 该文件并不存在当前 Module 中
fileInputStream = new FileInputStream(srcFile);
fileOutputStream = new FileOutputStream(destFile);
byte[] bytes = new byte[10];
int len;
while ((len = fileInputStream.read(bytes)) != -1) {
fileOutputStream.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fileOutputStream != null) {
fileOutputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fileInputStream != null) {
fileInputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
|
运行后,确实实现了图片的复制,复制得到的图片也可以正常打开。
什么?魂魄妖梦是啥?😇 老二刺猿了!
4.3 文件复制
根据 4.2 操作非文本文件 提供的代码,我们可以改写一下代码,编写一个复制文件的方法 copyFile() 。
copyFile() 方法和测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| // 实现指定路径下文件复制
public void copyFile(String srcPath, String destPath) {
FileInputStream fileInputStream = null;
FileOutputStream fileOutputStream = null;
try {
File srcFile = new File(srcPath);
File destFile = new File(destPath);
fileInputStream = new FileInputStream(srcFile);
fileOutputStream = new FileOutputStream(destFile);
byte[] bytes = new byte[1024];
int len;
while ((len = fileInputStream.read(bytes)) != -1) {
fileOutputStream.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fileOutputStream != null) {
fileOutputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fileInputStream != null) {
fileInputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Test
public void testCopyFile() {
long start = System.currentTimeMillis();
String srcPath = "D:\\01-视频.mp4";
String destPath = "D:\\02-视频.mp4";
// String srcPath = "hello.txt";
// String destPath = "hello3.txt";
copyFile(srcPath, destPath);
long end = System.currentTimeMillis();
// 164MB --> 2085ms
System.out.println("复制操作花费时间为:" + (end - start));
}
|
运行上述代码,复制一个大小 164M 的视频文件耗费了 2085ms。
你会发现提供的代码中,我还尝试复制了文本文件,那么真的可以复制成功吗?不会像前面测试那样乱码吗?
不妨取消注释,自己尝试一下,最终会发现能够复制成功。
总结
针对上面的现象进行进行一个小总结:
1、 使用字节流读入文本文件并显示在控制台,可能会出现乱码,但是使用字节流复制文本文件并不会出现乱码,并且能够成功复制。 (直接看就乱码,不看就不会乱码,这是薛定谔的代码?😆)
2、不能够使用字符流来处理非文本文件,就算是复制也不行!当然对于文本文件来说,还是使用字符流处理更好。
5. 缓冲流
5.1 非文本文件的复制
缓冲流的概述
前面讲的字符流(FileReader、FileWriter)和字节流(FileInputStream、FileOutputStream) 属于节点流,而缓冲流属于处理流的一种 ,在节点流的基础上包装了一层。那么,缓冲流有什么用?
缓冲流的作用就是为了提高文件的读写效率。因此在设计开发过程中,一般不会使用前面列述的几个类,而是考虑使用缓冲流,因为它们是比较基本的几个流,效率不是很高。
有关缓冲流,存在这四个类:BufferedInputStream 、 BufferedOuputStream 、 BufferedReader 和 BufferedWriter 。根据前面的说明,我们可以很简单地明白前两个是针对字节流的,后面两个是针对字符流的。
还有一点,缓冲流是不能直接作用在源文件上的,需要作用在节点流之上。
缓冲流就是“套接”在已有流的基础上。
测试代码
编写代码,实现图片的复制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| // 实现非文本文件的复制
@Test
public void BufferedStreamTest(){
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
// 1. 指定原文件与目标文件
File srcFile = new File("妖梦.jpg");
File destFile = new File("Youmu.jpg");
// 2. 造流
// 2.1 造节点流
FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(destFile);
// 2.2 造缓冲流
bis = new BufferedInputStream(fis);
bos = new BufferedOutputStream(fos);
// 3. 复制的细节:读取和写入
byte[] bytes = new byte[10];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4. 资源的关闭
// 要求:先关闭外层的流,再关闭内层的
try {
if (bos != null) {
bos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (bis != null) {
bis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
// 说明:关闭外层的流时,内层流也会自动关闭。因此,内层流的关闭可以省略。
// fos.close();
// fis.close();
}
}
|
运行代码后,可以前往当前 Module 中查看复制得到的图片。
上述代码中,涉及到 4 个类,两种节点流,两种作用于节点流的处理流(缓冲流),那么最后关闭的时候要关闭 4 种?
当然不用。关闭外层的流时,内层流也会自动关闭,因此,内层流的关闭可以省略。
使用缓冲流可以实现非文本文件的复制,但是怎么体现缓冲流的作用(提高了读写效率)呢?
5.2 读写速度对比
和前面一样,提取一个 copyFileWithBuffered() 方法用于复制一个视频,然后使用与前面相同的视频进行测试,比较使用缓冲流所耗费的时间与未使用缓冲流所耗费的时间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| public void copyFileWithBuffered(String srcPath, String destPath) {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
// 1. 指定原文件与目标文件
File srcFile = new File(srcPath);
File destFile = new File(destPath);
// 2. 造流
// 2.1 造节点流
FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(destFile);
// 2.2 造缓冲流
bis = new BufferedInputStream(fis);
bos = new BufferedOutputStream(fos);
// 3. 复制的细节:读取和写入
byte[] bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bos != null) {
bos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (bis != null) {
bis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Test
public void testCopyFile() {
long start = System.currentTimeMillis();
String srcPath = "D:\\01-视频.mp4";
String destPath = "D:\\02-视频.mp4";
copyFileWithBuffered(srcPath, destPath);
long end = System.currentTimeMillis();
// 未使用缓冲流: 164MB --> 2085ms
// 使用缓冲流后: 164MB --> 450ms
System.out.println("复制操作花费时间为:" + (end - start));
}
|
运行代码后得出使用缓冲流复制相同视频所用时间为 450ms,而未使用缓冲流时,所消耗时间为 2085ms,效率得到了明显的提升(注意测试时将字节数组的大小设置成一样,控制变量嘛)。
那么为什么使用缓冲流后读写效率会得到提升呢?
主要是因为内部提供了一个 8kb 的缓冲区,数据会先写入缓冲区,当缓冲区数据满(达到某一条件)后,刷新缓冲区,将数据进行复写到目标文件中。
缓冲流内部还有一个 flush() 方法,这个方法用于刷新缓冲区。如果每次使用缓冲流写入后,手动调用这个方法(这个时候缓冲区还没达到刷新条件),会发现耗时又增加了(我的环境下耗时为 1431ms)。
5.3 文本文件的复制
对于字符流也有对应的缓冲流,我们可以测试一下(被复制文本最好是个大文本文件):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| // 使用 BufferedReader 和 BufferedWriter 实现文本文件的复制
@Test
public void testBufferedReaderBufferedWriter() {
BufferedReader br = null;
BufferedWriter bw = null;
try {
// 原 Module 中已经存在
br = new BufferedReader(new FileReader(new File("training-1000.txt")));
// 原 Module 中并不存在
bw = new BufferedWriter(new FileWriter(new File("result.txt")));
// 读写操作
// 方式一 使用 char[] 数组
/*char[] chars = new char[1024];
int len;
while ((len = br.read(chars)) != -1) {
bw.write(chars, 0, len);
}*/
// 方式二 使用 String
String data;
while ((data = br.readLine()) != null) {
// 方法一
// bw.write(data + "\n"); // data 中不包含换行符
// 方法二
bw.write(data);
bw.newLine(); // 提供换行操作
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
try {
if (bw != null) {
bw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (br != null) {
br.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
|
在上述代码的读写操作中,使用了两种方法:一种使用 char[] 数组,一种使用了 String 类。
使用 char[] 数组时,与使用 byte[] 一样,就不赘述了。
使用 String 类时需要注意实例化的字符串中并 不包含换行符 ,如果直接写入,将无法写入换行符,导致所有内容都在一行。解决方法也有两种:
- 在字符串末尾拼接
\n 再写入
- 依旧按照原方式写入,但是后续调用
readLine() 方法
5.4 技巧练习
图片加密
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| // 图片的加密
@Test
public void test1() {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream("妖梦.jpg");
fos = new FileOutputStream("妖梦-secret.jpg");
byte[] bytes = new byte[20];
int len;
while ((len = fis.read(bytes)) != -1) {
// 直接数组进行修改
for (int i = 0; i < len; i++) {
// 异或的异或就是原数据
bytes[i] = (byte) (bytes[i] ^ 5);
}
fos.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
|
使用字节流传输文件时,可以将获取的字节与某个特定的数进行异或,这样就可以实现图片的简单加密。
我们知道一个数异或的异或就是原数据,因此解密时,只需要再进行异或就可以了。
字的统计
给定一个文本,统计该文本中每次字出现的次数。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| @Test
public void testWordCount() {
FileReader fr = null;
BufferedWriter bw = null;
HashMap<Character, Integer> map = new HashMap<>();
try {
// 被统计文本
fr = new FileReader("training-1000.txt");
int c = 0;
while ((c = fr.read()) != -1) {
// int 还原 char
char ch = (char) c;
// 判断 char 是否在 map 中第一次出现
if (map.get(ch) == null) {
map.put(ch, 1);
} else {
map.put(ch, map.get(ch) + 1);
}
}
// 输出文本
bw = new BufferedWriter(new FileWriter("wordCount.txt"));
Set<Map.Entry<Character, Integer>> entrySet = map.entrySet();
for (Map.Entry<Character, Integer> entry : entrySet) {
switch (entry.getKey()) {
case ' ':
bw.write("空格= " + entry.getValue());
break;
case '\t':
bw.write("tab键= " + entry.getValue());
break;
case '\r':
bw.write("回车= " + entry.getValue());
break;
case '\n':
bw.write("换行= " + entry.getValue());
break;
default:
bw.write(entry.getKey() + "=" + entry.getValue());
break;
}
bw.newLine();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fr != null) {
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bw != null) {
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
|
小技巧
使用 FileInputStream 、 FileOutputStream 、 FileReader 和 FileWriter 的构造方法时,可以直接传入路径,不用再书写 new File() 获取 File 对象。
6. 转换流
6.1 转换流的概述
转换流也是处理流的一种,转换流提供了在字节流和字符流之间的转换。
Java API 提供了两个转换流:
InputStreamReader :将 InputStream 转换为 Reader ,将一个字节的输入流转换成字符的输入流
OutputStreamWriter :将 Writer 转换为 OutputStream ,将一个字符的输出流转换成字节的输出流
字节流中的数据都是字符时,转换成字符流操作更加高效。
很多时候我们使用转换流来处理文件乱码问题,实现编码和解码的功能。
解码与编码
解码:字节、字节数组 ------> 字符数组、字符串 使用 InputStreamReader
编码:字符数组、字符串 ------> 字节、字节数组 使用 OutputStreamWriter
使用字节流读取一个文本文件,然后将字节流转换成字符流,最后将文本内容输出到控制台:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| // 处理异常仍应该使用 try-catch-finally,下述代码仅仅是为了简便(偷懒)
@Test
public void test1() throws IOException {
FileInputStream fis = new FileInputStream("hello.txt");
// InputStreamReader isr = new InputStreamReader(fis); // 使用默认的字符集
// 这种方式指明了字符集,具体使用哪个字符集,取决于文件保存时使用的字符集
// 字节转字符
InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
char[] chars = new char[5];
int len;
while ((len = isr.read(chars)) != -1) {
String s = new String(chars, 0, len);
System.out.println(s);
}
isr.close();
fis.close();
}
|
上述代码中的异常处理仍应该使用 try-catch-finally 来处理,下述代码仅仅是为了简便,或者说偷懒。
6.3 综合使用
综合使用 InputStreamReader 和 OutputStreamWriter 实现文本文件的解码与编码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| // 处理异常仍应该使用 try-catch-finally,下述代码仅仅是为了简便(偷懒)
// 综合使用 InputStreamReader 和 OutputStreamWriter
@Test
public void test2() throws IOException{
File file1 = new File("hello.txt");
File file2 = new File("hello-gbk.txt");
FileInputStream fis = new FileInputStream(file1);
FileOutputStream fos = new FileOutputStream(file2);
InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
OutputStreamWriter osw = new OutputStreamWriter(fos, "gbk");
char[] chars = new char[5];
int len;
while ((len = isr.read(chars)) != -1){
osw.write(chars, 0, len);
}
isr.close();
osw.close();
}
|
运行上述代码后,可以生成 helle-gbk.txt 文本文件,这个文本文件的编码是 gbk 的,如果在 IDEA 中直接打开,将会出现乱码,因为默认使用的 UTF-8 的编码打开的,所以会出现乱码。
当然如果使用 Nodepad3 、Edit Plus 打开就不会,因为这些软件可以自动匹配字符集。
6.4 字符集说明
编码表的由来
计算机只能识别二进制数据,早期由来是电信号。为了方便应用计算机,让它可以识别各个国家的文字。就将各个国家的文字用数字来表示,并一一对应,形成一张表。而这就是编码表。
常见的编码表
ASCII:美国标准信息交换码。用一个字节的 7 位可以表示。
IS08859-1:拉丁码表、欧洲码表。用一个字节的 8 位表示。
GB2312:中国的中文编码表。最多两个字节编码所有字符。
GBK:中国的中文编码表升级,融合了更多的中文文字符号,最多两个字节编码。中文是两个字节,但是英文、数字是一个字节的。
Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码, 所有的文字都用两个字节来表示。
UTF-8:变长的编码方式,可用 1 - 4 个字节来表示一个字符。
编码的问题
Unicode 虽然融合了人类使用的所有字,但是它不完美,这里就有三个问题,一个是我们已经知道,英文字母只用一个字节表示就够了,第二个问题是如何才能区别 Unicode 和 ASCII ?计算机怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?第三个,如果和 GBK 等双字节编码方式一样,用最高位是 1 或 0 表示两个字节和一个字节,就少了很多值无法用于表示字符,不够表示所有字符。Unicode 在很长一段时间内无法推广,直到互联网的出现。
面向传输的众多UTF (UCS Transfer Format)标准出现了,顾名思义,UTF-8 就是每次 8 个位传输数据,而 UTF-16 就是每次 16 个位。这是为传输而设计的编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。
Unicode 只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯一确定的编号,具体存储成什么样的字节流,取决于字符编码方案。推荐的 Unicode 编码是 UTF-8 和 UTF-16 。|
补充
ANSI 编码,通常指的是平台的默认编码,例如英文操作系统中是 ISO-8859-1,中文系统是 GBK。
Unicode 字符集只是定义了字符的集合和唯一编号,Unicode 编码这是对 UTF-8、UCS-2 / UTF-16 等具体编码方案的统称而已,并不是具体的编码方案。
参考视频:多种字符编码集的说明
7. 标准的输入、输出流
7.1 概述
System.in 和 System.out 分别代表了系统标准的输入和输出设备
默认输入设备是:键盘,输出设备是:显示器
System.in 的类型是 InputStream,标准的输入流,默认从键盘输入
System.out 的类型是 PrintStream,它是 OutputStream 的子类、FilterOutputStream 的子类,标准的输出流,默认从控制台输出
重定向:通过 System 类的 setln, setOut方法对默认设备进行改变。
7.2 具体使用
从键盘上输入字符串,要求将读取到的整行字符串转成大写输出,然后继续进行输入操作,直至输入 e 或 exit 时,退出程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| // System.in 是字节流,BufferedReader 的 readLine() 是字符流,需要使用转换流
public static void main(String[] args) {
BufferedReader br = null;
try {
InputStreamReader isr = new InputStreamReader(System.in);
br = new BufferedReader(isr);
// 也可以直接使用 Scanner, Scanner s = new Scanner(System.in)
while (true) {
System.out.println("请输入字符串:");
String data = br.readLine();
if ("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)) {
System.out.println("程序结束!");
break;
}
String upperCase = data.toUpperCase();
System.out.println(upperCase);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
|
8. 打印流
打印流概要
实现将基本数据类型的数据格式转化为字符串 输出 。打印流,打印流,肯定是用来打印输出的,因此没有输入。
打印流:PrintStream 和 PrintWriter
提供了一系列重载的 print() 和 println() 方法, 用于多种数据类型的输出
PrintStream 和 PrintWriter 的输出不会抛出 IOException 异常
PrintStream 和 PrintWriter 有自动 flush 功能
PrintStream 打印的所有字符都使用平台的默认字符编码转换为字节。在需要写入字符而不是写入字节的情况下,应该使用 PrintWriter 类。
System.out 返回的是 PrintStream 的实例
具体使用
实现使用打印流将 ASCII 码对应的字符打印到指定文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| // 打印流 PrintStream 和 PrintWriter
@Test
public void test1() {
PrintStream ps = null;
try {
FileOutputStream fos = new FileOutputStream(new File("D:\\io\\text.txt"));
// 创建打印输出流,设置为自动刷新模式(写入换行符或字节 '\n' 时都会刷新输出缓冲区)
ps = new PrintStream(fos, true);
if (ps != null) { // 把标准输出流(控制台输出)改成文件
System.setOut(ps);
}
for (int i = 0; i < 255; i++) { // 输出 ASCII 字符
System.out.print(((char) i));
if (i % 50 == 0) { // 每 50 个数据换一行
System.out.println();
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (ps != null) {
ps.close();
}
}
}
|
9. 数据流
为了方便地操作 Java 语言的基本数据类型和 String 的数据, 可以使用数据流。
数据流有两个类:(用于读取和写出基本数据类型、String 类的数据)
DataInputStream 中的方法
boolean readBoolean()
byte readByte()
char readChar()
float readFloat()
double readDouble()
short readShort()
long readLong()
int readInt()
String readUTF()
void readFully(byte[] b)
DataOutputStream 中的方法
将上述的方法的 read 改为相应的 write 即可。
具体使用
将内存中的基本数据类型和字符串写入文件:
1 2 3 4 5 6 7 8 9 10 11 12 13
| // 将内存中的基本数据变量和字符串写入文件
@Test
public void test2() throws IOException {
DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.txt"));
dos.writeUTF("默烦");
dos.flush(); // 刷新操作,将内存中的数据写入文件中
dos.writeInt(20);
dos.flush();
dos.writeBoolean(true);
dos.flush();
dos.close();
}
|
将文件中存储的基本数据类型和字符串读取到内存中,保存到变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| // 将文件中存储的基本数据类型和字符串读取到内存中,保存到变量
// 读取不同类型的数据的顺序要与当初写入文件时,保存的数据的顺序一致
@Test
public void test3() throws IOException{
DataInputStream dis = new DataInputStream(new FileInputStream("data.txt"));
String name = dis.readUTF();
int age = dis.readInt();
boolean isMale = dis.readBoolean();
System.out.println("name = " + name);
System.out.println("age = " + age);
System.out.println("isMale = " + isMale);
dis.close();
}
|
10. 对象流
10.1 对象流的概述
对象流使用到的类是: ObjectInputStream 和 OjbectOutputSteam
对象流是用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可以把 Java 中的对象写入到数据源中,也能把对象从数据源中还原回来。这样就涉及到序列化和反序列化的概念:
序列化:用 ObjectOutputStream 类 保存 基本类型数据或对象的机制
反序列化:用 ObjectInputStream 类 读取 基木类型数据或对象的机制
注意: ObjectOutputStream 和 ObjectInputStream 不能序列化 static 和 transient 修饰的成员变量。
对象的序列化
对象序列化机制允许把内存中的 Java 对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的 Java 对象。
序列化的好处在于可将任何实现了 Serializable 接口的对象转化为字节数据 ,使其在保存和传输时可被还原.
序列化是RMI(Remote Method Invoke - 远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是 JavaEE 平台的基础。
如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下 两个接口之一 ,否则,会抛出 NotSerializableException 异常:
-
Serializable (常用)
-
Externalizable
10.2 字符串的序列化
将某个字符串序列化写入文件 object.dat ,然后使用反序列化将文件 object.dat 中的数据输出到控制台:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| // 序列化:将内存中的 Java 对象保存到磁盘中或通过网络传输出去
@Test
public void testObjectOutputStream() {
ObjectOutputStream oos = null;
try {
// 当前 Module 下并没有 object.dat 文件
oos = new ObjectOutputStream(new FileOutputStream("object.dat"));
oos.writeObject(new String("默烦"));
oos.flush(); // 数据刷新
} catch (IOException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
// 反序列化
@Test
public void testObjectInputStream() {
ObjectInputStream ois = null;
try {
// 当前 Module 下存在上次序列化的 object.dat 文件
ois = new ObjectInputStream(new FileInputStream("object.dat"));
Object obj = ois.readObject();
String str = (String) obj;
System.out.println(str);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
|
运行上述代码后,当前 Module 下生成 object.dat 文件,控制台输出字符串 “默烦” 。
10.3 自定义类的序列化
使用对象流序列化对象
若某个类实现了 Serializable 接口,那么该类的对象就是可序列化的。序列化过程如下:
1、创建一个 ObjectOutputStream 对象
2、调用 ObjectOutputStream 对象的 writeObject(对象) 方法输出可序列化对象
3、注意写出一次,就要操作 flush() 一次
那么反序列化:
1、创建一个 ObjectInputStream 对象
2、调用 readObject() 方法读取流中的对象
强调: 如果某个类的属性不是基本数据类型或 String 类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的 Field 的类也不能序列化。
序列化要点
1、如果需要让某个对象支持序列化机制,那么这个对象所属的类必须实现 Serializable 接口,在这个接口中没有任何抽象方法,我们一般称这样的接口为 标识接口。
2、需要被序列化的类必须提供一个全局常量: serialVersionUID 。
3、除了保证需要被序列化的类需要实现 Serializable 接口之外,还必须保证其内部所有属性都必须是可序列化的。(默认情况下,基本数据类型都可序列化)
自定义类序列化示例
创建一个实体类 Person ,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| /**
* @author 默烦 2020/10/12
*/
public class Person implements Serializable {
private static final long serialVersionUID = 4216546544L;
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person() { }
// 省略 Getter、Setter 和 toString 方法
}
|
在字符串序列化的代码基础上进行修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| // 序列化:将内存中的 Java 对象保存到磁盘中或通过网络传输出去
@Test
public void testObjectOutputStream() {
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("object.dat"));
oos.writeObject(new String("默烦"));
oos.flush(); // 数据刷新
oos.writeObject(new Person("默烦", 20));
oos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
// 反序列化
@Test
public void testObjectInputStream() {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("object.dat"));
// 注意反序列化顺序
Object obj = ois.readObject();
String str = (String) obj;
Person person = (Person) ois.readObject();
System.out.println(str);
System.out.println(person);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
|
运行上述代码后,控制台输出:

需要注意的有:
1、每次序列化一个对象后,记得执行 flush() 操作
2、多个对象进行反序列化时,反序列化的顺序和序列化的顺序 一致 (注意是一致),否则会报错
10.4 serialVersionUID
凡是实现 Serializable 接口的类都有一个表示序列化版木标识符的静态变量:
1
| private static final long serialVersionUID;
|
serialVersionUID 用来表明类的不同版本间的兼容性。简言之,其目的是以序列化对象进行版本控制,有关各版本反序列化时是否兼容。
如果类没有显式定义这个静态变量,它的值是 Java 运行时环境根据类的内部细节自动生成的。 若类的实例变量做了修改,serialVersionUID 可能发生变化。 故建议,显式声明。
简单来说,Java 的序列化机制是通过在运行时判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常 InvalidCastException。
10.5 序列化要点
1、如果需要让某个对象支持序列化机制,那么这个对象所属的类必须实现 Serializable 接口,在这个接口中没有任何抽象方法,我们一般称这样的接口为 标识接口。
2、需要被序列化的类必须提供一个全局常量: serialVersionUID 。
3、除了保证需要被序列化的类需要实现 Serializable 接口之外,还必须保证其内部所有属性都必须是可序列化的(默认情况下,基本数据类型都可序列化)。
4、 ObjectOutputStream 和 ObjectInputStream 不能序列化 static 和 transient 修饰的成员变量。
5、每次序列化一个对象后,记得执行 flush() 操作
6、多个对象进行反序列化时,反序列化的顺序和序列化的顺序 一致 (注意是一致),否则会报错
序列化更多相关内容可以查阅:Lambda 与序列化
11. 随机存取文件流
11.1 RandomAccessFile 类
RandomAccessFile 类的概述
RandomAccessFile 声明在 java.io 包下, 但直接继承于 java.lang.Object 类,并
且它实现了 Datalnput、DataOutput 这两 个接口,也就意味着这个类既可以读也可以写。
RandomAccessFile 类支持“随机访问”的方式,程序可以直接跳到文件的任意地方来读、写文件,比如:
-
支持只访问文件的部分内容
-
可以向已存在的文件后追加内容
RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置。RandomAccessFile 类对象可以自由移动记录指针:
RandomAccessFile 构造器
1 2 3
| public RandomAccessFile(File file, String mode)
public RandomAccessFile(String name, String mode)
|
创建 RandomAccessFile 类实例需要指定一个 mode 参数,该参数指定 RandomAccessFile 的访问模式:
如果模式为只读 r,则不会创建文件,而是会去读取一个已经存在的文件,如果读取的文件不存在则会出现异常。如果 模式为 rw 读写,且文件不存在则会去创建文件,反之则不会创建。
RandomAccessFile 实现图片的复制示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| @Test
public void test1() {
RandomAccessFile raf1 = null;
RandomAccessFile raf2 = null;
try {
// 当前 Module 下存在此文件
raf1 = new RandomAccessFile(new File("妖梦.jpg"), "r");
// 当前 Module 下并无此文件,需要实现图片的复制
raf2 = new RandomAccessFile(new File("魂魄妖梦.jpg"), "rw");
byte[] bytes = new byte[1024];
int len;
while ((len = raf1.read(bytes)) != -1) {
raf2.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (raf1 != null) {
try {
raf1.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (raf2 != null) {
try {
raf2.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
|
RandomAccessFile 作为输出流示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| /*
* RandomAccessFile 作为输出流时,写出到的文件如果并不存在,在执行过程中会自动创建
* 如果写出到的文件存在,则会对原有文件进行覆盖(默认情况下,从头开始覆盖)
* */
@Test
// 异常处理还是使用 try-catch-finally 处理更好,这里为了简便(偷懒)
public void test2() throws IOException {
// 当前 Module 下存在 hello.txt 文件
RandomAccessFile raf1 = new RandomAccessFile("hello.txt", "rw");
raf1.write("mofan".getBytes());
raf1.close();
}
|
我们需要知道:
1、RandomAccessFile 作为输出流时,写出到的文件如果并不存在,在执行过程中会自动创建
2、如果写出到的文件存在,则会对原有文件进行覆盖(默认情况下,从头开始覆盖)
在当个方法中调用 write() 方法后会记录角标的位置,如果再次调用该方法,将会从记录的角标位置后进行写入。
11.2 实现数据的插入
实现指定位置的复写
在前文代码的基础上指定角标的位置:
1 2 3 4 5 6 7 8 9
| @Test
public void test2() throws IOException {
RandomAccessFile raf1 = new RandomAccessFile("hello.txt", "rw");
raf1.seek(3); // 指针调到角标为 3 的位置
// 从角标为 4 的位置开始插入(角标从 1 开始计算)
raf1.write("xyz".getBytes());
raf1.close();
}
|
实现指定位置的插入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| // 实现数据的插入效果
@Test
public void test3() throws IOException {
RandomAccessFile raf1 = new RandomAccessFile("hello.txt", "rw");
raf1.seek(3); // 指针调到角标为 3 的位置
// 保存指针 3 后面的所有数据到 StringBuffer 中
StringBuffer builder = new StringBuffer((int) new File("hello.txt").length());
byte[] bytes = new byte[20];
int len;
while ((len = raf1.read(bytes)) != -1) {
builder.append(new String(bytes, 0, len));
}
raf1.seek(3);
raf1.write("xyz".getBytes());
// 将 StringBuffer 中的数据写入到文件里
raf1.write(builder.toString().getBytes());
raf1.close();
}
|
RandomAccessFile 并没有提供插入方法,我们实现的插入其实就是:复制指定位置后的所有数据,在指定位置后写入数据,最后再将复制的数据写入文件。
那 RandomAccessFile 有什么用呢?
我们可以用 RandomAccessFile 这个类,来实现一个 多线程断点下载 的功能,用过下载工具的朋友们都知道,下 载前都会建立 两个临时文件 ,一个是与被下载文件大小相同的空文件,另一个是记录文件指针的位置文件,每次暂停的时候,都会保存上一次的指针,然后断点下载的时候,会继续从上一次的地方下载,从而实现断点下载或上传的功能,有兴趣的朋友们可以自己实现下。
12. NIO 的简单介绍
12.1 NIO.2
NIO 的简单介绍
Java NIO(New IO,Non-Blocking I0)是从 Java 1.4 版本开始引入的一套新的 IO API,可以替代标准的 Java IO API。NIO 与原来的 IO 有同样的作用和目的,但是使用的方式完全不同,NIO 支持面向缓冲区的(IO 是面向流的)、基于通道的 IO 操作。 NIO 将以更加高效的方式进行文件的读写操作。
Java API 中提供了两套 NIO,一套是针对标准输入输出 NIO,另一套就是网络编程NIO。
1 2 3 4 5
| ➢|-----java.nio.channels.Channel
|-----FileChannel : 处理本地文件
|-----SocketChannel : TCP 网络编程的客户端的Channel
|-----ServerSocketChannel : TCP 网絡编程的服务器端的 Channel
|-----DatagramChannel : UDP 网络编程中发送端和接收端的 Channel
|
但是 JDK 1.4 中 NIO 的使用体验并不好,随着 JDK 7 的发布,Java 对 NIO 进行了极大的扩展,增强了对文件处理和文件系统特性的支持,以至于我们称它们为 NIO.2 。因为 NIO 提供的一-些功能,NIO 已经成为文件处理中越来越重要的部分。
NIO.2 中 Path、Paths 的使用
早期的 Java 只提供了一个 File 类来访问文件系统,但 File 类的功能比较有限,所提供的方法性能也不高。而且大多数方法在出错时仅返回失败,并不会提供异常信息。
NIO. 2 为了弥补这种不足,引入了 Path 接口,代表一个平台无关的平台路径,描述了目录结构中文件的位置。Path 可以看成是 File 类的升级版本,实际引用的资源也可以不存在。
在以前IO操作都是这样写的:
1 2
| import java.io.File;
File file = new File("index.html");
|
但在Java7中,我们可以这样写:
1 2 3
| import java.nio.file.Path;
import java.nio.file.Paths;
Path path = Paths.get("index.html");
|
同时,NIO.2 在 java.nio.file 包下 还提供了 Files、Paths 工具类,Files 包含了大量静态的工具方法来操作文件,Paths 则包含 了两个返回 Path的静态工厂方法。
Paths 类提供的静态 get() 方法用来获取 Path 对象:
1 2 3
| static Path get(String first, String ... more): 用于将多个字符串串连成路径
static Path get(URI uri): 返回指定 uri 对应的 Path 路径
|
NIO.2 中 Files 类的使用
java.nio.file.Files 用于操作文件或目录的上具类。
Files 常用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| Path copy(Path src, Path dest, CopyOption ... how): 文件的复制
Path createDirectory(Path path, FileAttribute<?> ... attr): 创建一一个目录
Path createFile(Path path, FileAttribute<?> ... arr): 创建一一个文件
void delete(Path path): 删除一个文件/目录,如果不存在,执行报错
void deletelfExists(Path path): Path对应的文件/目录如果存在,执行删除
Path move(Path src, Path dest, CopyOption... how): 将src移动到dest位置
long size(Path path): 返回path指定文件的大小
Files常用方法: 用于判断
boolean exists(Path path, LinkOption ... opts): 判断文件是否存在
boolean isDirectory(Path path, LinkOption ... opts): 判断是否是目录
boolean isRegularFile(Path path, LinkOption ... opts): 判断是否是文件
boolean isHidden(Path path): 判断是否是隐藏文件
boolean isReadable(Path path): 判断文件是否可读
boolean isWritable(Path path): 判断文件是否可写
boolean notExists(Path path, LinkOption ... opts): 判断文件是否不存在
Files常用方法: 用于操作内容
SeekableByteChannel newByteChannel(Path path, OpenOption..how): 获取与指定文件的连接,how 指定打开方式。
DirectoryStream<Path> newDirectoryStream(Path path): 打开path指定的目录
InputStream newInputStream(Path path, OpenOption... how): 获取InputStream对象
OutputStream newOutputStream(Path path, OpenOpition... how) : 获取OutputStream对象
|
到此,Java IO 流基础就基本说完了,在实际开发过程中可能会书写这些 IO 流代码,但是大概率是不会使用的,经常使用的是已经封装好的第三方库,比如 Apache 下的 commons-io。
Java IO 流基础完