Java I/O流讲解
每博一文案
谁让你读了这么多书,又知道了双水村以外还有一个大世界,如果从小你就在这个天地里,日出而作,日落而息。 那你现在就会和众乡亲抱同一理想:经过几年的辛劳,像大哥一样娶个满意的媳妇,生个胖儿子,加上你的体魄, 会成为一名出色的庄稼人。不幸的是,你知道的太多了,思考的太多了,因此才有了,这种不能为周围人所理解的苦恼。 —————— 《平凡的世界》 人生是这样的不可预测,没有永恒的痛苦,也没有永恒的幸福,生活就像流水一般, 有时是那么平展,有时又是那么曲折。 世界上有些人因为忙而感到生活的沉重,也有些人因为闲而活得压抑,人啊,都有自己一本难念的经; 可是不同处境的人又很难理解别人的苦处。 细想过来,每个人的生活也同样是一个世界,即使是最平方的人,也要为他那个世界的存在而战斗。 —————— 《平凡的世界》
@
1. File 类
java.io.File
类:文件和文件目录路径的抽象表示形式,与平台无关。
File 能新建,删除,重命名文件和目录,但File 不能访问文件内容本身。如果需要访问文件内容本身,则需要使用 输入/输出 流。
想要在Java 程序中表示一个真实存在的文件或目录,那么必须有一个 File 对象,但是 Java 程序中的一个 File 对象,可能没有一个真实存在的文件或目录。
File 对象可以作为参数传递给流的构造器。
1.1 File 类中:构造器
public File(String pathname); // 过将给定路径名字符串转换为抽象路径名来创建一个新 File 实例。如果给定字符串是空字符串,那么结果是空抽象路径名 public File(String parent,String child); // 根据 parent 路径名字符串和 child 路径名字符串创建一个新 File 实例。以parent为父路径,child为子路径创建File对象 public File(File parent, String child); // 根据 parent 抽象路径名和 child 路径名字符串创建一个新 File 实例。根据一个父File对象和子文件路径创建File对象 // 路径可以是绝对路径,也可以是相对路径
- 绝对路径: 是一个固定的路径,从盘符开始。
- 相对路径: 是相对于某个位置开始。IDEA中默认相对路径是从
**Project**
项目(路径)下和同级的**src**
的路径开始的,注意不是模块开始的**Module**
的 。如下图所示:src 和 Module 模块是同级的。
1.2 File 类中:路径分隔符
路径中的每级目录之间用一个路径分隔符隔开。
路径分隔符和系统有关:
- windows和DOS系统默认使用
“”
来表示,需要注意的是在 java 中**""**
具有转义的意思,所以想要表示真正的 “” 需要两个**"\"**
来转义回来表示一个斜杆。 - UNIX和URL使用
“/”
来表示。
Java程序支持跨平台运行,因此路径分隔符要慎用。
为了解决这个隐患,File类提供了一个常量:
public static final String separator。// 根据操作系统,动态的提供分隔符。
File file1 = new File("E:\Test\info.txt"); File file2 = new File("E:" + File.separator + "Test" + File.separator + "info.txt"); File file3 = new File("E:/Test"); // 这三者表示的路径是一样的。只是表示方式不同而已。
举例:
package blogs.blog9; import java.io.File; public class FileTest { /** * File 构造器的使用 */ public static void main(String[] args) { // 构造器一: // 绝对路径: 带盘符 File file = new File("E:\Java\JavaRebuilt\src\blogs\blog9"); // 双右斜杆表示一个 (转义) // 相对路径: IDEA默认是Project的根目录,不是模块Module的根目录, // 也可以使用:左斜杆表示路径分隔符 System.out.println(file); File file2 = new File("src/blogs/blog9"); // 这里是在src的包下(src 和 Module 模块是同级的) System.out.println(file2); // 构造器二: // 第一个参数是第二个参数的父路径,第二个参数是子路径 File file3 = new File("E:\Java\JavaRebuilt\src\blogs","blog9"); System.out.println(file3); // 构造器三: // 第一个参数是 File 类对象(这里是第二个参数的父路径的File对象),第二个参数是子路径 File file4 = new File(file3,"blog9"); System.out.println(file4); } }
1.3 File 类中:常用方法
获取文件属性的信息的方法:
- getAbsoluteFile() : 返回此File对象中路径的绝对路径。返回的是 File 对象
public File getAbsoluteFile(); // 返回此抽象路径名的绝对路径名形式。
- getAbsolutePath() : 返回此抽象路径名的绝对路径名字符串
public String getAbsolutePath(); // 返回此抽象路径名的绝对路径名字符串
- getPath() : 返回此抽象路径名以字符串的形式。
public String getPath(); // 返回此抽象路径名
- getName() : 获取名称
public String getName(); // 返回由此抽象路径名表示的文件或目录的名称。该名称是路径名名称序列中的最后一个名称。如果路径名名称序列为空,则返回空字符串
- getParent() : 获取上层文件目录路径(也就是父路径)。若无,返回null
public String getParent(); // 返回此抽象路径名父目录的路径名字符串;如果此路径名没有指定父目录,则返回 null。
- length() : 返回文件长度即(字节数)
public long length(); // 获取文件长度(即:字节数)。不能获取目录的长度
- lastModified() : 获取最后一次的修改时间,毫秒值。
public long lastModified(); // 获取最后一次的修改时间,毫秒值
- list() : 获取指定目录下的所有文件或者文件目录的名称数组
public String[] list(); // 返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中的文件和目录。
- listFiles() : 获取指定目录下的所有文件或者文件目录的 File 数组
public File[] listFiles(); // 返回一个抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件。
举例:
import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; public class FileTest { public static void main(String[] args) { File file = new File("src\blog9\hello.txt"); // 注意转义以及文件后缀 File file2 = new File("src/blog9/hello3.txt"); // 左斜杆也是可以的 String absolutePath = file.getAbsolutePath(); // 返回绝对路径,以String的形式返回 System.out.println(absolutePath); File absoluteFile = file.getAbsoluteFile(); // 返回绝对路径,以File 对象的形式返回 System.out.println(); System.out.println(file.getPath()); // 返回此路径名/目录名 System.out.println(file.getName()); // 返回该文件名/目录名 System.out.println(file.getParent()); // 返回该上层文件/目录名称 System.out.println(file.length()); // 返回文件长度即文件的大小(字节) long l = file.lastModified(); // 返回该文件的最后一次修改的时间值(毫秒值)时间戳 // 将时间戳转换为Date,再转换为 指定格式的字符串 Date date = new Date(l); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:ss:mm SSS"); String format = simpleDateFormat.format(date); System.out.println(format); } }
举例:
import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; public class FileTest { /** * File 文件目录 */ public static void main(String[] args) { File file = new File("src/blogs/blog9"); // 也可以使用 左斜杆 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类的重命名功能
- renameTo(File dest) : 把文件重命名为指定的文件路径。换句话说:就是剪切附加对文件的重命名 的意思。
public boolean renameTo(File dest); // 重新命名此抽象路径名表示的文件。
注意: 这里的剪切效果,有一定的要求:就是比如:file.renameTo(dest)
想要将 file 剪切到 dest 位置路径上。要保证 file 剪切的文件实际在硬盘中存在,并且 dest 不能在硬盘文件中存在(仅仅当一个路径)。如果不满足会失败,返回 false
举例:
import java.io.File; public class FileTest { public static void main(String[] args) { File file = new File("src\blogs\blog9\hello.txt"); File dest = new File("E:\临时文件\temp\test.txt"); boolean b = file.renameTo(dest); // 将file文件剪切到 dest 中并重命名 System.out.println(b); } }
失败:原因是:dest 中的 test.txt 是在硬盘中实际存在的。
将test.txt去了就没事了 ,再重新剪切,就可以了。
File 类的判断功能
- isDirectory() : 判断是否是文件目录
public boolean isDirectory(); // 测试此抽象路径名表示的文件是否是一个目录。
- isFile() :判断是否是文件
public boolean isFile(); // 当且仅当此抽象路径名表示的文件存在且 是一个标准文件时,返回 true;否则返回 false
- exists() : 判断该文件/目录是否存在
public boolean exists(); // 测试此抽象路径名表示的文件或目录是否存在
- canRead() : 判断该文件是否可读的
public boolean canRead(); // 当且仅当此抽象路径名指定的文件存在且 可被应用程序读取时,返回 true;否则返回 false
- canWrite() : 判断该文件是否可写的
public boolean canWrite(); // 当且仅当文件系统实际包含此抽象路径名表示的文件且 允许应用程序对该文件进行写入时,返回 true;否则返回 false.
- isHidden() : 判断该文件是否隐藏的
public boolean isHidden(); // 当且仅当此抽象路径名表示的文件根据底层平台约定是隐藏文件时,返回 true
举例:
import java.io.File; public class FileTest { public static void main(String[] args) { File file = new File("src\blogs\blog9\hello.txt"); // 注意转义以及文件后缀(该文件实际存在的) File file2 = new File("src/blogs/blog9/hello3.txt"); // 左斜杆也是可以的 (该文件不存在的) System.out.println(file.isDirectory()); // 判断是否是文件目录 System.out.println(file.isFile()); // 判断是否为文件 System.out.println(file.exists()); // 判断该文件/目录是否实际存在 System.out.println(file.canRead()); // 判断该我呢见是否可读的 System.out.println(file.canWrite()); // 判断该文件是否是可写的 System.out.println(file.isHidden()); // 判断该文件是否隐藏的 System.out.println("*************************** file2 *************************"); 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 类的创建功能
- **createNewFile() ** : 创建文件。若文件存在,则不创建,返回false
public boolean createNewFile() throws IOException // 如果指定的文件不存在并成功地创建,则返回 true;如果指定的文件已经存在,则返回 false
- mkdirs() : 创建文件目录。创建文件目录。如果上层文件目录不存在,一并创建
public boolean mkdirs(); // 创建此抽象路径名指定的目录,包括所有必需但不存在的父目录。注意,此操作失败时也可能已经成功地创建了一部分必需的父目录。
- mkdir() : 创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。
public boolean mkdir(); // 创建此抽象路径名指定的目录。
注意事项:如果你创建文件或者文件目录没有写盘符路径,那么,默认在项目路径下。
举例:
import java.io.File; import java.io.IOException; public class FileTest { public static void main(String[] args) { File file = new File("src/blogs/blog9/hello.txt"); boolean b = false; try { b = file.createNewFile(); // 文件存在不创建,不存在文件创建 } catch (IOException e) { e.printStackTrace(); } System.out.println(b); } }
创建目录: 使用 mkdir
import java.io.File; public class FileTest { public static void main(String[] args) { File file = new File("src/blogs/blog9/test/test2/"); boolean b = file.mkdir(); // 如果对应的 test2目录的上级目录test不存在,则目录都不创建 System.out.println(b); } }
使用 mkdirs()
import java.io.File; import java.io.IOException; public class FileTest { public static void main(String[] args) { File file = new File("src/blogs/blog9/test/test2/"); boolean b = file.mkdirs(); // 如果对应的 test2目录的上级目录test不存在,则目录一并都创建 System.out.println(b); } }
File类的删除功能:
- delete() : 删除文件或者文件夹
public boolean delete(); // 删除此抽象路径名表示的文件或目录。如果此路径名表示一个目录,则该目录必须为空才能删除。
删除注意事项:Java中的删除不走回收站。要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录。如果含有无法删除的。
举例:
import java.io.File; public class FileTest { public static void main(String[] args) { File file = new File("src/blogs/blog9/test"); boolean b = file.delete(); // test 目录下不能有文件/目录,有的话无法删除 System.out.println(b); } }
小结 :
1.4 实用案例:
判断指定目录下是否有后缀名为.jpg的文件,如果有,就输出该文件名称
package com.atguigu.exer2; import java.io.File; import java.io.FileFilter; import java.io.FilenameFilter; import org.junit.Test; /** 判断指定目录下是否有后缀名为.jpg的文件,如果有,就输出该文件名称 */ public class FindJPGFileTest { @Test public void test1(){ File srcFile = new File("d:\code"); String[] fileNames = srcFile.list(); for(String fileName : fileNames){ if(fileName.endsWith(".jpg")){ System.out.println(fileName); } } } @Test public void test2(){ File srcFile = new File("d:\code"); File[] listFiles = srcFile.listFiles(); for(File file : listFiles){ if(file.getName().endsWith(".jpg")){ System.out.println(file.getAbsolutePath()); } } } /* * File类提供了两个文件过滤器方法 * public String[] list(FilenameFilter filter) * public File[] listFiles(FileFilter filter) */ @Test public void test3(){ File srcFile = new File("d:\code"); File[] subFiles = srcFile.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.endsWith(".jpg"); } }); for(File file : subFiles){ System.out.println(file.getAbsolutePath()); } } }
遍历指定目录所有文件名称,包括子文件目录中的文件。
拓展1:并计算指定目录占用空间的大小
拓展2:删除指定文件目录及其下的所有文件
package com.atguigu.exer2; import java.io.File; /** * 3. 遍历指定目录所有文件名称,包括子文件目录中的文件。 拓展1:并计算指定目录占用空间的大小 拓展2:删除指定文件目录及其下的所有文件 * @author shkstart 邮箱:shkstart@126.com * @version 创建时间:2019年2月23日 上午1:55:31 * */ public class ListFilesTest { public static void main(String[] args) { // 递归:文件目录 /** 打印出指定目录所有文件名称,包括子文件目录中的文件 */ // 1.创建目录对象 File dir = new File("E:\teach\01_javaSE\_尚硅谷Java编程语言\3_软件"); // 2.打印目录的子文件 printSubFile(dir); } public static void printSubFile(File dir) { // 打印目录的子文件 File[] subfiles = dir.listFiles(); for (File f : subfiles) { if (f.isDirectory()) {// 文件目录 printSubFile(f); } else {// 文件 System.out.println(f.getAbsolutePath()); } } } // 方式二:循环实现 // 列出file目录的下级内容,仅列出一级的话 // 使用File类的String[] list()比较简单 public void listSubFiles(File file) { if (file.isDirectory()) { String[] all = file.list(); for (String s : all) { System.out.println(s); } } else { System.out.println(file + "是文件!"); } } // 列出file目录的下级,如果它的下级还是目录,接着列出下级的下级,依次类推 // 建议使用File类的File[] listFiles() public void listAllSubFiles(File file) { if (file.isFile()) { System.out.println(file); } else { File[] all = file.listFiles(); // 如果all[i]是文件,直接打印 // 如果all[i]是目录,接着再获取它的下一级 for (File f : all) { listAllSubFiles(f);// 递归调用:自己调用自己就叫递归 } } } // 拓展1:求指定目录所在空间的大小 // 求任意一个目录的总大小 public long getDirectorySize(File file) { // file是文件,那么直接返回file.length() // file是目录,把它的下一级的所有大小加起来就是它的总大小 long size = 0; if (file.isFile()) { size += file.length(); } else { File[] all = file.listFiles();// 获取file的下一级 // 累加all[i]的大小 for (File f : all) { size += getDirectorySize(f);// f的大小; } } return size; } // 拓展2:删除指定的目录 public void deleteDirectory(File file) { // 如果file是文件,直接delete // 如果file是目录,先把它的下一级干掉,然后删除自己 if (file.isDirectory()) { File[] all = file.listFiles(); // 循环删除的是file的下一级 for (File f : all) {// f代表file的每一个下级 deleteDirectory(f); } } // 删除自己 file.delete(); } }
2. I/O 流的概述
一个 I / O流 代表输入源或输出目的地。流可以表示许多不同种类的源和目的地,包括磁盘文件,设备,其他程序和存储器阵列。
流支持许多不同类型的数据,包括简单字节,原始数据类型,本地化字符和对象。一些流简单地传递数据; 其他人以有用的方式操纵和转换数据。
I/O 其中的 I
是 Input 的缩写,O
是 Output 的缩写。I/O 技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。
Java 程序中,对于数据的输入/输出操作以 流(stream)
的方式进行。
java.io
包下提供了各种 “流”类和接口,用以获取不同种类的数据,并通过方法输入或输出数据。
无论内部工作如何,所有流都会使用与使用它们的程序相同的简单模型:
流是一系列数据。程序使用 输入流 从源中读取数据:**input**
输入流以内存为参考对象(将文件中的数据内容写入到内存当中) 以及 **Read**
(以文件为参考对象,将读取文件中的数据到内存当中)。这两个都是将意思都是一样的将文件中的数据读取出来写入到内存当中。
程序使用 输出流 将数据写入目的地。**Output**
输出流以内存为参考对象(将内存中的数据内容输出到硬盘文件当中) 以及 **Write**
(以文件为参考对象,将内存中的数据到写入到硬盘文件当中)。这两个都是将意思都是一样的:将内存中的数据输出到硬盘文件当中。
2.1 I/O的分类和体系结构
按照不同的分类方式, 可以将流分为不同的类型。
2.1.1 输入流 和 输出流
按照流的流向来分, 可以分为输入流和输出流:
输入流: 只能从中读取数据, 而不能向其写入数据。
输出流: 只能向其写入数据, 而不能从中读取数据。
此处的输入、 输出涉及一个方向问题, 对于如图 1 所示的数据流向, 数据从内存到硬盘, 通常称为输出流——也就是说, 这里的输入、 输出都是从程序运行所在内存的角度来划分的。
对于如图 2 所示的数据流向, 数据从服务器通过网络流向客户端, 在这种情况下, Server 端的内存负责将数据输出到网络里, 因此 Server 端的程序使用输出流; Client 端的内存负责从网络里读取数据, 因此 Client 端的程序应该使用输入流。
2.1.2 字节流 和 字符流
按操作数据单位不同分为:字节流(8 bit),字符流(16 bit)。
字节流和字符流的用法几乎完全一样, 区别在于字节流和字符流操作的数据单元的不同:字节流是 8 位的字节, 而字符流操作的数据单元是 16 位的字符。其中还有一点不同的就是:
- 字符流:只能读取操作文本文件,因为字符流读取的是文件中的 char 字符信息。
.c,.java,.c++,.txt
等等这些都是文本文件不仅仅只是 txt文件,而特别注意的是 :.wrod 不是文本文件,wrod中的文字是经过特殊处理的存在一定的规范格式,不是纯文本文件。 - 字节流:可以操作任何的文件,因为字节流读取的是二进制信息,读取1个字节byte,等同于一次读取8个二进制,这种流是万能的,什么类型的文件都可以读取到,因为文件都是有二进制组成的。包括: 文本文件,图片,声音文件。
2.1.3 节点流 和 处理流(包装流)
按流的角色的不同分为:节点流,处理流。
- 节点流: 所谓的节点流:就是最基本的一个流到底目的的流向,其中的流没有被其它的流所包含住。直接从数据源或目的地读写数据。如下图所示
- 处理流: 处理流又称为包装流 ,处理流对一个己存在的流进行连接或封装/包装, 通过封装后的流来实现数据读/写功能。不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提 供更为强大的读写功能。
- 如下图所示:
注意: 节点流和包装流是相对的,有时候,相对于不同的流的,一个节点流变成了是另一个流的包装流。一个包装流变成了另一个流的节点流 。如下图所示:
区分一个流是节点流还是包装流:大家可以:以谁包装谁作为参考,被包装的流就是节点流,包装了其它的流的就是包装流,当然注意这是相对的。
2.1.4 流的概念模型
Java 把所有设备里的有序数据抽象成流模型, 简化了输入/输出处理, 理解了流的概念模型也就了解了Java IO。
通过使用处理流, Java 程序无须理会输入/输出节点是磁盘、 网络还是其他的输入/输出设备, 程序只要将这些节点流包装成处理流, 就可以使用相同的输入/输出代码来读写不同的输入/输出设备的数据。
2.1.5 I/O 的体系结构
- Java的IO流共涉及40多个类,实际上非常规则,都是从如下 Java Io 流四大家族 个 抽象基类派生的。
- 由以下这四个类派生出来的子类。其名称都是以其父类名作为子类名后缀。这样用于我们辨认。
- Java IO流四大家族:
- java.io.InputStream 字节输入流,类名是以
"stream"
结尾的。
- java.io.InputStream 字节输入流,类名是以
public abstract class InputStream implements Closeable {}
- java.io.OutputStream 字节输出流,类名是以
"stream"
结尾的。
public abstract class OutputStream implements Closeable, Flushable {}
- java.io.Reader 字符输入流,类名是以
"Reader/Writer"
结尾的。
public abstract class Reader implements Readable, Closeable {}
- java.io.Writer 字符输出流,类名是以以
"Reader/Writer"
结尾的。
public abstract class Writer implements Appendable, Closeable, Flushable {}
- 上述四大家族的首领都是抽象类
abstract class
。这四个类都实现了java.io.Closeable
接口,都是可以关闭的,都有close()
方法。流毕竟是一个管道,这个内存和硬盘之间的通道,用完之后一定要关闭。不然会耗费很多资源(因为Java打开的资源是有限的,当你打开过多的资源超过限制时就无法打开其它的资源了)。养成好习惯,用完之后一定要关闭(必须关闭)。
- 这四个类都实现了
java.io.Flushable
接口,都是可刷新 的,都是有 flush() 方法的,养成一个好习惯,输出流(将内存当中的数据输出到硬盘文件当中)在最终(输出完)之后,一定要记得 flush() ,刷新一下,这个刷新的作用就是清空管道(强制将内存当中的数据输出到文件中)。为什么要清空管道呢:因为:如果没有 flush() 可以会导致内存中一部分的数据并没有全部输出到硬盘当中,从而导致一部分的数据丢失。 - 注意:在Java中只要 类名是以
**"stream"**
结尾的都是字节流,以**"Reader/Writer"**
结尾的都是字符流。
java.io包下需要掌握的流有 16个 文件专属: java.io.FileInputStream java.io.FileOutputStream 字节流无法读取到: 文件中的空格的 java.io.FileReader java.io.FileWriter 字符流可以读取到:文件中的空格的 转换流: (将字节流转换字符流) java.io.InputStreamReader java.io.OutputStreamWriter 缓冲流专属: java.io.BufferedReader java.io.BufferedWriter java.io.BufferedInputStream java.io.BufferedOutputStream java.io.BufferedOutputStream 数据流专属: java.io.DataInputStream java.io.DataOutputStream 标准输出流: java.io.PrintWtiter java.io.PrinStream 对象专属流: java.io.ObjectInputStream java.io.ObjectOutputStream
2.2 字符流
2.2.1 java.io.FileReader 字符输入流
关于字符输入流的类都是继承了:java.io.Writer(字符输出流)/ java.io.Reader (字符输入流) 来使用的,但是这个两个类是抽象类,是无法 new 对象来使用的。所以我们就需要使用其实现的子类:对于文件字符输入流比较常用的就是: java.io.FileReader 这个子类了。
字符流: 只能读取文本文件,不能读取其它格式的文件,文本文件不仅仅是 .txt
后缀的文件,.c,.java,.c++
都是文本文件,注意 : word 不是文本文件,因为 word 中的文本字符是有一个规范格式设置的。不是纯的文本文件。字符流操作字符,只能操作普通文本文件。最常见的文本文件:.txt,.java,.c,.cpp 等语言的源代码。尤其注意.doc,excel,ppt这些不是文本文件。
在读取文件时,必须保证该文件已存在,否则报异常。
其中继承的 InputStreamReader 是个转换流,继承了 Reader 抽象类的。
FileReader的构造器
public FileReader(File file) throws FileNotFoundException; // 在给定从中读取数据的 File 的情况下创建一个新 FileReader对象 public FileReader(String fileName) throws FileNotFoundException; // 根据文件的相对路径名/绝对路径创建一个新 FileReader对象
举例:
import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; public class FileReaderTest { public static void main(String[] args) { File file = new File("E:\Java\JavaRebuilt\src\blogs\blog9\hello.txt"); // 绝对路径 try { FileReader fileReader = new FileReader(file); } catch (FileNotFoundException e) { e.printStackTrace(); } try { FileReader fileReader2 = new FileReader("src/blogs/blog9/hello.txt"); // 相对路径 } catch (FileNotFoundException e) { e.printStackTrace(); } } }
如下是 FileReader 继承 java.io.InputStreamReader** 继承的方法**
- read() : 读取单个字符。作为整数读取的字符,范围在 0 到 65535 之间 (0x00-0xffff)(2个字节的Unicode码),如果已到达流的末尾,则返回 -1。
public int read() throws IOException; // 读取单个字符。
- int read(char[] cbuf) : 将字符读入数组。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。
public int read(char[] cbuf) throws IOException; // 将文件中的数据读取到char[] cbuf的字符数组当中,返回读取到的个数。
- int read(char[] cbuf,int off,int len) : 将字符读入数组的某一部分。存到数组cbuf中,从off处开始存储,最多读len个字 符。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。
public int read(char[] cbuf,int offset, int length) throws IOException; // 将字符读入数组中的某一部分.
- public void close() throws IOException :关闭此输入流并释放与该流关联的所有系统资源。
public void close() throws IOException; // 关闭此输入流并释放与该流关联的所有系统资源。
需要明白:对于 read() 读取文件内容的方式是,一个一个字符的读取的,每调用一次 read()对于的文件中的光标就会往后移动一下。对于特殊的 read(char[ ] cbuf) 读取的个数是 char[] 数组的长度,往后移动光标的位置也是 char[] 数组的长度。以及返回的是对于字符的编码值。
设文件 file1.txt ,采用字符流的话是这样读的: a中国bo张三 第一次读: ‘a’字符 第二次读: ‘中’字符
举例:
import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; public class FileReaderTest { public static void main(String[] args) { FileReader fileReader = null; // 相对路径 try { fileReader = new FileReader("src/blogs/blog9/hello.txt"); int read = fileReader.read(); // 返回的是编码值 System.out.println(read); read = fileReader.read(); System.out.println(read); read = fileReader.read(); System.out.println(read); read = fileReader.read(); System.out.println(read); read = fileReader.read(); System.out.println(read); read = fileReader.read(); System.out.println(read); read = fileReader.read(); System.out.println(read); } catch (IOException e) { e.printStackTrace(); } finally { // fileReader 防止 null引用 if(fileReader != null) { try { fileReader.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
举例: 使用 while() 循环处理
import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; public class FileReaderTest { public static void main(String[] args) { FileReader fileReader = null; // 相对路径 try { fileReader = new FileReader("src/blogs/blog9/hello.txt"); int len = 0; // 当read()读取到 文件末尾返回 -1,跳出循环 while((len = fileReader.read()) != -1) { System.out.println(len); } } catch (IOException e) { e.printStackTrace(); } finally { // fileReader 防止 null引用 if(fileReader != null) { try { fileReader.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
举例: 使用 int read(char[] cbuf) : 将字符读入数组。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数
import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; public class FileReaderTest { public static void main(String[] args) { FileReader fileReader = null; // 相对路径 try { fileReader = new FileReader("src/blogs/blog9/hello.txt"); int len = 0; char [] chars = new char[4]; // read(chars) 一次性读取数组长度个字符,返回读取到的字符个数。到达文件末尾返回-1 while((len = fileReader.read(chars)) != -1) { // 将char[] 数组转换为字符串 System.out.println(new String(chars)); } } catch (IOException e) { e.printStackTrace(); } finally { // fileReader 防止 null引用 if(fileReader != null) { try { fileReader.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
read(char[]) 读取数据时的覆盖效果的讲解
举例: 去除 read(char[] cduf) 的覆盖效果,我们读取多到了多少个字符,就 new String 转换多少个字符
import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; public class FileReaderTest { public static void main(String[] args) { FileReader fileReader = null; // 相对路径 try { fileReader = new FileReader("src/blogs/blog9/hello.txt"); int len = 0; char [] chars = new char[4]; // read(chars) 一次性读取数组长度个字符,返回读取到的字符个数。到达文件末尾返回-1 while((len = fileReader.read(chars)) != -1) { // 将char[] 数组转换为字符串 // 这里我们 读取到了多少个字符,就将 chars数组中的前多少个转换为字符串 System.out.println(new String(chars,0,len)); } } catch (IOException e) { e.printStackTrace(); } finally { // fileReader 防止 null引用 if(fileReader != null) { try { fileReader.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
2.2.2 java.io.FileWriter 字符输出流
于字符流输出的类都是继承了:java.io.Writer(字符输出流)/ java.io.Reader (字符输入流) 来使用的,但是这个两个类是抽象类,是无法 new 对象来使用的。所以我们就需要使用其实现的子类:对于文件字符输出流比较常用的就是: java.io.FileWriter 这个子类了。
其中继承的 OutputStreamWriter 是个转换流,继承了 Writer 抽象类的。
OutputStreamWriter 的构造器:
public FileWriter(File file) throws IOException; // 根据给定的 File 对象构造一个 FileWriter 对象 public FileWriter(String fileName) throws IOException; // 根据给定的文件名构造一个 FileWriter 对象。 public FileWriter(File file,boolean append) throws IOException; // 根据给定的 File 对象构造一个 FileWriter 对象。如果第二个参数为 true,则将字节写入文件末尾处,而不是写入文件开始处。 // file - 要写入数据的 File 对象 // append - 如果为 true,则将字节写入文件末尾处,而不是写入文件开始处 ,默认是 false,不写的话也是 false
举例:
package blogs.blog9; import java.io.File; import java.io.FileWriter; import java.io.IOException; public class FileWriterTest { public static void main(String[] args) { File file = new File("E:\Java\JavaRebuilt\src\blogs\blog9\hello.txt"); // 绝对路径 try { FileWriter fileWriter = new FileWriter(file); } catch (IOException e) { e.printStackTrace(); } try { FileWriter fileWriter2 = new FileWriter("src/blogs/blog9/hello.txt"); // 相对路径 } catch (IOException e) { e.printStackTrace(); } try { FileWriter fileWriter3 = new FileWriter("src/blogs/blog9/hello.txt",true); } catch (IOException e) { e.printStackTrace(); } } }
如下是 FileWriter 继承 java.io.OutputStreamWriter继承的方法
- void write(int c) : 写入单个字符。要写入的字符包含在给定整数值的 16 个低位中,16 高位被忽略。 即写入0 到 65535 之间的Unicode码。
public void write(int c) throws IOException; // 写入单个字符。
- void write(char[] cbuf) : 将字符数组的内容,写入到对应的硬盘文件中去
public void write(char[] cbuf) throws IOException; // 将字符数组的内容,写到文件中
- void write(char[] cbuf,int off,int len) : 写入字符数组的某一部分。从off开始,写入len个字符。
public void write(char[] cbuf,int off,int len) throws IOException // 写入字符数组的某一部分。
- void write(String str) :将字符串的内容,写入到对应的硬盘文件中去
public void write(Stirng str) throws IOException
- void write(String str,int off,int len) :写入字符串的某一部分。从off开始,写入len个结束
public void write(String str,int off,int len) throws IOException
- void flush() :刷新该流的缓冲,立即内存中的数据写入预期目标硬盘文件中去。
public void flush() throws IOException; // 刷新该流的缓冲。
- public void close() :关闭此输出流并释放与该流关联的所有系统资源
public void close() throws IOException; // 关闭此流,但要先刷新它。在关闭该流之后,再调用 write() 或 flush() 将导致抛出 IOException。关闭以前关闭的流无效。
注意: 如果写入到的文件不存在,是会自动创建的。如果文件已经存在了,在创建FileWriter 对象时没有设置为 true 的话,是会将原本文件中已经存在的内容覆盖的,写入新的内容。
举例: 文件不存在,自动创建。
import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; public class FileReaderTest { public static void main(String[] args) { FileReader fileReader = null; // 相对路径 try { fileReader = new FileReader("src/blogs/blog9/hello.txt"); int len = 0; char [] chars = new char[4]; // read(chars) 一次性读取数组长度个字符,返回读取到的字符个数。到达文件末尾返回-1 while((len = fileReader.read(chars)) != -1) { // 将char[] 数组转换为字符串 // 这里我们 读取到了多少个字符,就将 chars数组中的前多少个转换为字符串 System.out.println(new String(chars,0,len)); } } catch (IOException e) { e.printStackTrace(); } finally { // fileReader 防止 null引用 if(fileReader != null) { try { fileReader.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
举例: 文件已经存在,写入的信息覆盖原本文件的全部内容
import java.io.File; import java.io.FileWriter; import java.io.IOException; public class FileWriterTest { public static void main(String[] args) { FileWriter fileWriter = null; // 相对路径 try { // 1. 创建输出流对象: FileWriter fileWriter = new FileWriter("src/blogs/blog9/hello2.txt"); // 2. 将内存当中的内容写入到文件中 fileWriter.write("你好世界"); // 3. 刷新:将内存中没有输出到文件中的内容,强制全部写入到文件中 fileWriter.flush(); } catch (IOException e) { e.printStackTrace(); } finally { // 防止 null 引用 if(fileWriter != null) { // 4. 关闭IO资源 try { fileWriter.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
举例: 创建 **FileWriter ** 对象时,设置 true ,将内存当中的信息写入到文件的末尾去,不会覆盖原本文件中的内容
import java.io.File; import java.io.FileWriter; import java.io.IOException; public class FileWriterTest { public static void main(String[] args) { FileWriter fileWriter = null; // 相对路径 try { // 1. 创建输出流对象: FileWriter,并设置 true 将写入的内容追加到文件的末尾中去 fileWriter = new FileWriter("src/blogs/blog9/hello2.txt",true); // 2. 将内存当中的内容写入到文件中 fileWriter.write("n"); // 换行 fileWriter.write("Hello World"); // 3. 刷新:将内存中没有输出到文件中的内容,强制全部写入到文件中 fileWriter.flush(); } catch (IOException e) { e.printStackTrace(); } finally { // 防止 null 引用 if(fileWriter != null) { // 4. 关闭IO资源 try { fileWriter.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
2.2.3 实例文本文件的拷贝
将同目录中的 hello.txt 文件的内容拷贝到 同目录中的 hello2.txt 中去
思路:
import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; public class FileWriterTest { public static void main(String[] args) { FileWriter descFile = null; FileReader srcFile = null; // 注意文件后缀 try { // 1. 创建hello.txt 文件的字符输入流对象,以及 hello2.txt文件的字符输出流对象 descFile = new FileWriter("src/blogs/blog9/hello2.txt"); srcFile = new FileReader("src/blogs/blog9/hello.txt"); // 2. 一边读,一边写 int len = 0; char[] chars = new char[10]; // 读取hello.txt的数据信息 while((len = srcFile.read(chars)) != -1) { // 将读取到的内容写入到hello2.txt文件中 descFile.write(chars,0,len); } // 3. 刷新:将内存中遗留没有写入到文件中的信息,全部强制写入到文件中去 descFile.flush(); } catch (IOException e) { e.printStackTrace(); } finally { // 5. 关闭IO资源 // 分开 try,如果两个一起try的话其中一个出现异常了,后面的一个IO就无法关闭了 if(srcFile != null) { // 防止 null引用 try { srcFile.close(); } catch (IOException e) { e.printStackTrace(); } } if(descFile != null) { // 防止 null引用 try { descFile.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
2.3 字节流
2.3.1 FileInputStream 字节输入流
关于字节输入流的类都是继承了:InputStream(字节输入流) 来使用的,但是个类是抽象类,是无法 new 对象来使用的。所以我们就需要使用其实现的子类:对于文件字符输入流比较常用的就是: **java.io.FileInputStream ** 这个子类了。
字节流: 可以操作任何的文件,因为字节流读取的是二进制信息,读取1个字节byte,等同于一次读取8个二进制,这种流是万能的,什么类型的文件都可以读取到,因为文件都是有二进制组成的。包括: 文本文件,图片,声音文件。再比如:比如:.mp3,.avi,.rmvb,mp4,.jpg,.doc,.ppt等文件。
FileInputStream的构造器
public FileInputStream(File file) throws FileNotFoundException; // 通过打开一个到实际文件的连接来创建一个 FileInputStream,该文件通过文件系统中的 File 对象 file 指定。 public FileInputStream(String name) throws FileNotFoundException; // 通过打开一个到实际文件的连接来创建一个 FileInputStream,该文件通过文件系统中的路径名 name 指定。
和 字符流中的 FileReader 基本上是一样的。
举例:
import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; public class FileInputStreamTest { public static void main(String[] args) { File file = new File("E:\Java\JavaRebuilt\src\blogs\blog9\test.png"); // 绝对路径 try { FileInputStream fileInputStream = new FileInputStream(file); } catch (FileNotFoundException e) { e.printStackTrace(); } try { FileInputStream fileInputStream2 = new FileInputStream("src/blogs/blog9/test.png"); // 相对路径 } catch (FileNotFoundException e) { e.printStackTrace(); } } }
如下是 FileInputStream 继承 java.io.InputStream 继承的方法
字节流和字符流的用法几乎完全一样, 区别在于字节流和字符流操作的数据单元的不同:字节流是 8 位的字节, 而字符流操作的数据单元是 16 位的字符。方法是上的使用也是一样的。
- read() :读取文件的一个字节,返回值是: 读取到"字节"本身,到达文件末尾返回 -1。
public int read() throws IOException ; // 从此输入流中读取一个数据字节。如果没有输入可用,则此方法将阻塞。
- read(byte[] b) : 读取文件中 byte[] 数组长度的字节个数,返回读取的的字节个数,到位文件末尾返回 -1。
public int read(byte[] b) throws IOException; // 从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。在某些输入可用之前,此方法将阻塞。
- read(byte[] b int off, int len) : 将字节读入数组的某一部分。存到数组b中,从off处开始存储,最多读len个字 符。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。
public int read(byte[] b, int off, int len) throws IOException; // 从此输入流中将最多 len 个字节的数据读入一个 byte 数组中。如果 len 不为 0,则在输入可用之前,该方法将阻塞;否则,不读取任何字节并返回 0。
- public void close() throws IOException :关闭此输入流并释放与该流关联的所有系统资源。
public void close() throws IOException; // 关闭此输入流并释放与该流关联的所有系统资源。
举例:
import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; public class FileInputStreamTest { public static void main(String[] args) { FileInputStream fileInputStream = null; try { // 1. 创建字节流对象 fileInputStream = new FileInputStream("src/blogs/blog9/hello.txt"); int len = 0; // 2.读取文件信息 while((len = fileInputStream.read()) != -1) { System.out.println(len); } } catch (IOException e) { e.printStackTrace(); } finally { // 3. 关闭资源 // 防止null引用 if (fileInputStream != null) { try { fileInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
举例: 使用 byte[] 字节数组
import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; public class FileInputStreamTest { public static void main(String[] args) { FileInputStream fileInputStream = null; try { // 1. 创建字节流对象 fileInputStream = new FileInputStream("src/blogs/blog9/hello.txt"); // 2.读取文件信息 int len = 0; byte[] bytes = new byte[1024]; // 1KB // read(bytes) 一次性读取byte[]数组大小的字节个数,并存储到 bytes 数组中,返回读取的字节个数 while((len = fileInputStream.read(bytes)) != -1) { // 将 bytes 数组转换为字符串,读取多少,转换多少 String s = new String(bytes,0,len); System.out.println(s); } } catch (IOException e) { e.printStackTrace(); } finally { // 3. 关闭IO资源 if (fileInputStream != null) { try { fileInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
2.3.2 FileOutputStream 字节输出流
关于字节输出流的类都是继承了:OutputStream(字节输出流) 来使用的,但是个类是抽象类,是无法 new 对象来使用的。所以我们就需要使用其实现的子类:对于文件字符输出流比较常用的就是: java.io.FileOutputStream 这个子类了。
FileOutputStream的构造器
public FileOutputStream(File file) throws FileNotFoundException // 创建一个向指定 File 对象表示的文件中写入数据的文件输出流。创建一个新 FileDescriptor 对象来表示此文件连接。 public FileOutputStream(String name) throws FileNotFoundException // 创建一个向具有指定名称的文件中写入数据的输出文件流。创建一个新 FileDescriptor 对象来表示此文件连接 public FileOutputStream(File file,boolean append)throws FileNotFoundException // 创建一个向指定 File 对象表示的文件中写入数据的文件输出流。如果第二个参数为 true,则将字节写入文件末尾处,而不是写入文件开始处。创建一个新 FileDescriptor 对象来表示此文件连接。 // file - 为了进行写入而打开的文件。 // append - 如果为 true,则将字节写入文件末尾处,而不是写入文件开始处
举例:
import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; public class FileOutputStreamTest { public static void main(String[] args) { File file = new File("E:\Java\JavaRebuilt\src\blogs\blog9\test.png"); // 绝对路径 try { FileOutputStream fileOutputStream = new FileOutputStream(file); } catch (FileNotFoundException e) { e.printStackTrace(); } try { FileOutputStream fileOutputStream2 = new FileOutputStream("src/blogs/blog9/test.png"); // 相对路径 } catch (FileNotFoundException e) { e.printStackTrace(); } } }
如下是 FileOutputStream 继承 java.io.OutputStream继承的方法
- void write(int c) : 写入单个字符。要写入的字符包含在给定整数值的 16 个低位中,16 高位被忽略。 即写入0 到 65535 之间的Unicode码。
public void write(int b) throws IOException; // 写入单个字符。
- void write(byte[] b) : 将字符数组的内容,写入到对应的硬盘文件中去
public void write(byte[] cbuf) throws IOException; // 将字符数组的内容,写到文件中
- void write(byte[] b,int off,int len) : 写入字符数组的某一部分。从off开始,写入len个字符。
public void write(byte[] b,int off,int len) throws IOException // 写入字符数组的某一部分。
- void flush() :刷新该流的缓冲,立即内存中的数据写入预期目标硬盘文件中去。
public void flush() throws IOException; // 刷新该流的缓冲。
- public void close() :关闭此输出流并释放与该流关联的所有系统资源
public void close() throws IOException; // 关闭此流,但要先刷新它。在关闭该流之后,再调用 write() 或 flush() 将导致抛出 IOException。关闭以前关闭的流无效。
注意: 如果写入到的文件不存在,是会自动创建的。如果文件已经存在了,在创建 FileOutputStream 对象时没有设置为 true 的话,是会将原本文件中已经存在的内容覆盖的,写入新的内容。
举例: 创建 FileOutputStream 对象时没有设置为 true 的,在文件的末尾添加信息,不会覆盖原来文件的信息。
import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; public class FileOutputStreamTest { public static void main(String[] args) { File file = new File("src/blogs/blog9/hello.txt"); FileOutputStream fileOutputStream = null; try { // 1. 创建FileOutputStream对象 fileOutputStream = new FileOutputStream(file,true); // 2. 写入信息到文件中 byte[] bytes = new byte[]{'A','B'} ; fileOutputStream.write(bytes); // 3. 刷新:将遗留在内存当中没有写入到文件的信息,强制全部写入到文件中 fileOutputStream.flush(); } catch (IOException e) { e.printStackTrace(); } finally { // 4. 关闭IO资源 if (fileOutputStream != null) { try { fileOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
2.3.3 实例图片文件的拷贝
思路:
import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; public class FileOutputStreamTest { public static void main(String[] args) { FileOutputStream fileOutputStream = null; FileInputStream fileInputStream = null; try { // 1.创建 test2.png 文件的输入字节流对象 fileOutputStream = new FileOutputStream("src/blogs/blog9/test2.png"); // 2.创建 test.png 文件的输出字节流对象 fileInputStream = new FileInputStream("src/blogs/blog9/test.png"); // 2. 一边读,一边写 int len = 0; byte[] bytes = new byte[1024 * 1024]; // 1MB // 读 while ((len = fileInputStream.read(bytes)) != -1) { // 读取多少写入多少 fileOutputStream.write(bytes, 0, len); } // 3. 刷新: fileOutputStream.flush(); } catch (IOException e) { e.printStackTrace(); } finally { // 4.关闭IO资源 // 分开 try 防止,如果一起try的话,其中一个出现了异常,后面的IO资源就无法关闭了。 if (fileInputStream != null) { // 防止null引用 try { fileInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (fileOutputStream != null) { try { fileOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
2.3.4 实例对图片的简单加密
思路:
加密:这里我们通过字节流,获取到图片中的每个 byte 字节信息,再对获取到的每个 byte 字节信息进行 ^
5 运算加密,新生成一个加密后的图片(这个图片是加密了的,是无法打开的)。
解密:同样获取到图片中每个 byte 字节信息,再对获取到的每个 byte 字节信息进行 ^
5 运算解密。新生成一个解密后的图片(这个图片就可以正常打开了)
核心 : 就是利用 对于一个数^
两个异或同一个数值,返回原来的数值,例如:6 ^ 2 = 4;4 ^ 2 = 6;
举例:
import org.junit.Test; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; /** * 图片的加密解密操作 */ public class ImageEncryDecry { /** * 对图片中的每个像素点中的 byte 进行 ^ 5 的加密 */ @Test public void test() { FileInputStream fileInputStream = null; // 注意文件后缀 FileOutputStream fileOutputStream = null; // try { fileInputStream = new FileInputStream("src/day27/test2.jpg"); fileOutputStream = new FileOutputStream("src/day27/test3.jpg"); // 一边读,一边加密,一边写 byte[] bytes = new byte[20]; int len = 0; // 读取 while((len = fileInputStream.read(bytes)) != -1) { // 读取多少,加密多少,注意了不是 bytes数组的长度,因为存在重复的覆盖效果 for (int i = 0; i < len; i++) { bytes[i] = (byte)(bytes[i] ^ 5); // 加密 } // 加密完后,写入到文件中:读取多少,写入多少 fileOutputStream.write(bytes,0,len); } // 刷新 fileOutputStream.flush(); } catch (IOException e) { e.printStackTrace(); } finally { // 关闭: if(fileInputStream != null) { try { fileInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if(fileOutputStream != null) { try { fileOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
package day27; import org.junit.Test; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; /** * 图片的加密解密操作 */ public class ImageEncryDecry { /** * 对加密 ^ 5 的文件解密 */ @Test public void test2() { FileInputStream fileInputStream = null; // 注意文件后缀 FileOutputStream fileOutputStream = null; // try { fileInputStream = new FileInputStream("src/day27/test3.jpg"); fileOutputStream = new FileOutputStream("src/day27/test4.jpg"); // 一边读,一边加密,一边写 byte[] bytes = new byte[20]; int len = 0; // 读取 while((len = fileInputStream.read(bytes)) != -1) { // 读取多少,加密多少,注意了不是 bytes数组的长度,因为存在重复的覆盖效果 for (int i = 0; i < len; i++) { bytes[i] = (byte)(bytes[i] ^ 5); // 加密 } // 加密完后,写入到文件中:读取多少,写入多少 fileOutputStream.write(bytes,0,len); } // 刷新 fileOutputStream.flush(); } catch (IOException e) { e.printStackTrace(); } finally { // 关闭: if(fileInputStream != null) { try { fileInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if(fileOutputStream != null) { try { fileOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
3.1 缓冲流
为了提高数据读写的速度,Java API 提供了带缓冲区功能的流类。在使用这些流类时,会创建一个内部缓冲区数组,缺省使用 8192个字节(8kb)的缓冲区 。
- 缓冲流要 ”套接“ 在相应的节点流之上,根据数据操作单位可以把缓冲流分为 :
- 字节缓冲流 :
- java.io.BufferedInputStream : 字节输入缓冲流
- java.io.BufferedOutputStream :字节输出缓冲流
- 字符缓冲流 :
- java.io.BufferedReader : 字符输入缓冲流
- java.io.BufferedWriter : 字符输出缓冲流
- 缓冲流的使用:当读取数据时,数据按块读入缓冲区 ,其和的读操作则直接访问缓冲区。
- 当 BufferedInputStream 读取字节文件时,BufferedInputStream 会一次性从文件中读取 8192 个 ( 1024 * 8 = 8KB) ,存在缓冲区中,直到缓冲区装满了,才重新从文件中读取下一个 8192 个字节数组。
- 向流中写入字节时,不会直接写到文件中,先写道缓冲区中直到缓冲区写满,BufferedOutputStream 才会把缓冲区中的数据一次性写到文件中里。使用方法 flush() 可以强制将缓冲区的内容全部写入输出流。
- 关闭流的顺序和打开流的顺序相反。只要关闭最外层流即可,关闭最外层流也会相应关闭内层节点流。因为从源码中可以看出。
BufferedOutputStream.close() 关闭的同时,会将其中对应的节点流关闭。如下源码:
3.1.1 BufferedReader (字符输入缓冲流) / BufferedWriter (字符输出缓冲流)
BufferedReader的构造器
public BufferedReader(Reader in); // 创建一个使用默认大小输入缓冲区的缓冲字符输入流。 public BufferedReader(Reader in,int sz); // 创建一个使用指定大小输入缓冲区的缓冲字符输入流。
BufferedReader 中的方法和FileReader是一样的因为都是继承了 Reader的抽象类的方法的 所以这里就不多介绍说明了。
举例: 使用字符缓冲区读取文件信息
import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; public class BufferedReaderWriterTest { public static void main(String[] args) { BufferedReader bufferedReader = null; try { FileReader fileReader = new FileReader("src/blogs/blog9/hello.txt"); // 1. 创建字符输入缓冲区流对象 // 参数是:Reader 抽象类,这里我们使用 FileReader 同样也是 Reader 的子类作为参数 bufferedReader = new BufferedReader(fileReader); // 2. 读取文件信息 int len = 0; char [] chars = new char[3]; while((len = bufferedReader.read(chars))!= -1) { // 将 char 转换为字符串 // 读多少转换多少 String s = new String(chars,0,len); System.out.println(s); } } catch (IOException e) { e.printStackTrace(); } finally { // 3. 关闭IO资源 if (bufferedReader != null) { try { // 只需要关闭外层缓冲区的资源就可以,内层的会自动一起关闭 bufferedReader.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
BufferedWriter的构造器
public BufferedWriter(Writer out); // 创建一个使用默认大小输出缓冲区的缓冲字符输出流。 public BufferedWriter(Writer out,int sz); // 创建一个使用给定大小输出缓冲区的新缓冲字符输出流。
BufferedWriter 中的方法和FileReader是一样的因为都是继承了 Writer的抽象类的方法的 所以这里就不多介绍说明了。
举例: 使用字符缓冲区写入文件信息
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; public class BufferedReaderWriterTest { public static void main(String[] args) { BufferedWriter bufferedWriter = null; try { FileWriter fileWriter = new FileWriter("src/blogs/blog9/hello.txt",true); // 1. 创建字符输出缓冲流对象 // 参数是:Writer 抽象类,这里我们使用 BufferedWriter 同样也是 Writer 的子类作为参数 bufferedWriter = new BufferedWriter(fileWriter); // 2. 写入文件信息 char[] chars = new char[]{'H','H'}; bufferedWriter.write("n"); // 换行 bufferedWriter.write(chars); } catch (IOException e) { e.printStackTrace(); } finally { // 3. 关闭IO资源这里不用flush()因为缓冲流会自动刷新 if (bufferedWriter != null) { try { bufferedWriter.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
3.1.2 BufferedInputStream(字节输入缓冲流) / BufferedOutputStream (字节输出缓冲流)
这里字节缓冲流和上面的字符缓冲流是一样的这里就不多说明了。
BufferedOutputStream / BufferedInputStream 字节的构造器
public BufferedInputStream(InputStream in); //创建一个 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用。创建一个内部缓冲区数组并将其存储在 buf 中。 public BufferedInputStream(InputStream in,int size); // 创建具有指定缓冲区大小的 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用。创建一个长度为 size 的内部缓冲区数组并将其存储在 buf 中。 public BufferedOutputStream(OutputStream out); // 创建一个新的缓冲输出流,以将数据写入指定的底层输出流。 public BufferedOutputStream(OutputStream out, int size); // 创建一个新的缓冲输出流,以将具有指定缓冲区大小的数据写入指定的底层输出流。
举例: 使用BufferedInputStream 输入缓冲流读取文件的信息
import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileInputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; public class BufferedReaderWriterTest { public static void main(String[] args) { BufferedInputStream bufferedInputStream = null; try { FileInputStream fileInputStream = new FileInputStream("src/blogs/blog9/hello.txt"); // 1. 创建字节输入缓冲流对象 bufferedInputStream = new BufferedInputStream(fileInputStream); // 2. 读取文件信息 int len = 0; byte[] bytes = new byte[3]; while ((len = bufferedInputStream.read(bytes)) != -1) { // 将 bytes 转换为字符串,读取了多少转换为多少 String s = new String(bytes,0,len); System.out.println(s); } } catch (IOException e) { e.printStackTrace(); } finally { // 3. 关闭IO资源 // 关闭IO资源这里只需要关闭外层缓冲区的资源就可以,内层的会自动一起关闭 if (bufferedInputStream != null) { try { bufferedInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
举例: 使用BufferedOutputStream 输出缓冲流。
import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; public class BufferedReaderWriterTest { public static void main(String[] args) { BufferedOutputStream bufferedOutputStream = null; try { FileOutputStream fileOutputStream = new FileOutputStream("src/blogs/blog9/hello.txt",true); // 1.创建字节输出缓冲流对象 bufferedOutputStream = new BufferedOutputStream(fileOutputStream); // 2. 写入信息 byte[] bytes = new byte[]{'K','K'}; bufferedOutputStream.write(bytes); } catch (IOException e) { e.printStackTrace(); } finally { // 3. 关闭IO资源这里不用flush()因为缓冲流会自动刷新 if (bufferedOutputStream != null) { try { bufferedOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
4.1 转换流
- 转换流提供了在字节流和字符流之间的转换
- Java API 提供了两个转换流:
- java.io.InputStreamReader :将 InputStream 转换为 Reader
- Java.io.OutputStreamWriter : 将 OutputStream 转换为 Writer
- 字节流中的数据都是字符时,转成字符流操作更高效。
- 很多时候我们使用转换流来处理文件乱码问题。实现编码和 解码的功能。
InputStreamReader
实现将字节的输入流按指定字符集转换为字符的输入流。
需要和 InputStream ”套接“
InputStreamReader 构造器:
public InputStreamReader(InputStream in); // 创建一个使用默认字符集的 InputStreamReader。 public InputStreamReader(InputStream in,String charsetName) throws UnsupportedEncodingException; //创建使用指定字符集的 InputStreamReader。
举例:
import java.io.FileInputStream; import java.io.InputStreamReader; import java.io.FileNotFoundException; public class StreamWriterTest { public static void main(String[] args) { FileInputStream fileInputStream = null; try { fileInputStream = new FileInputStream("src/blogs/blog9/hello.txt"); } catch (FileNotFoundException e) { e.printStackTrace(); } // 将 FileInputStream 字节输入流转换为 InputStreamReader 字符输入流 InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream); } }
OutputStreamWriter
- 实现将字符的输出流按指定字符集转换为字节的输出流。
- 需要和OutputStream“套接”。
OutputStreamWriter的构造器
public OutputStreamWriter(OutputStream out); // 创建使用默认字符编码的 OutputStreamWriter public OutputStreamWriter(OutputStream out,String charsetName) throws UnsupportedEncodingException; // 创建使用指定字符集的 OutputStreamWriter。
举例:
import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.OutputStreamWriter; public class StreamWriterTest { public static void main(String[] args) { FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream("src/blogs/blog9/hello.txt"); } catch (FileNotFoundException e) { e.printStackTrace(); } // 将 FileOutputStream 字节输出流转换为 OutputStreamWriter 字符输出流 OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream); }
4.2 标准输入输出流
- System.in 和 System.out 分布代表了系统标准的输入和输出设备。
- 默认输入设备时:键盘,输出设备时:显示器
- System.in 实际上是一个 InputStream 字节输入流。将控制台的数据读取到。一个System 类中的静态属性
- System.out 的类型实际上是 PrintStream 字节输出流,其是OutputSteam 的子类,FilterOutputStream 的子类。
- 重定向:通过 System 类的 setIn ,setOut 方法对默认设备进行改变。
public static void setIn(InputStream in); //重新分配“标准”输入流。 首先,如果有安全管理器,则通过 RuntimePermission("setIO") 权限调用其 checkPermission 方法,查看是否可以重新分配“标准”输入流。 public static void setOut(PrintStream out); //重新分配“标准”输出流。 首先,如果有安全管理器,则通过 RuntimePermission("setIO") 权限调用其 checkPermission 方法,查看是否可以重新分配“标准”输出流
举例: 将 System.out 输出的内容,不显示在控制台中,而是写入到文件中,制作一个日志文件信息
package blogs.blog9; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.PrintStream; import java.text.SimpleDateFormat; import java.util.Date; public class PrintWriterStreamTest { public static void main(String[] args) { log("调用了System类的gc()方法,建议启动垃圾回收"); log("调用了UserService的doSome()方法"); log("用户尝试进行登录,验证失败"); } public static void log(String msg) { // 1. 创建一个输出的字节的文件对象,用于 System的重定向 PrintStream printStream = null; try { printStream = new PrintStream(new FileOutputStream("src/blogs/blog9/log.txt",true)); } catch (FileNotFoundException e) { e.printStackTrace(); } // 2.改变 System.out()的输出方向使用:SetOut()方法改为重定向到 printSteam的文件中 System.setOut(printStream); // 3. 设置输入的日期时间 Date date = new Date(); // 获取当前系统的时间(毫秒值)时间戳 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss SSS"); // 将 Date 转换为规定格式的字符串 String strTime = simpleDateFormat.format(date); System.out.println(strTime +": " + msg); // 4. 关闭IO资源 System.out.close(); } }
举例:
从键盘输入字符串,要求将读取到的整行字符串转成大写输出,然后继续进行输入操作。
- 直至当输入 "e" 或者 "exit" 时,退出程序,
- 方法一: 使用Scanner 实现,调用next()返回一个字符串
- 方法二:使用System.in 实现,System.in ---> 转换流 ---> BufferedReader 的readLine
这里我们使用方法二的方式:
import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.text.SimpleDateFormat; import java.util.Date; public class PrintWriterStreamTest { public static void main(String[] args) { // System.in 就是 public final static InputStream in = null; InputStreamReader isr = new InputStreamReader(System.in); // System.in 控制台 // 转换为该字符输出流,可以读取一行的信息 BufferedReader bufferedReader = new BufferedReader(isr); while(true) { String data = null; try { data = bufferedReader.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(); } } if(bufferedReader != null) { try { bufferedReader.close(); } catch (IOException e) { e.printStackTrace(); } } } }
4.3 数据流
数据流支持原始数据类型值的二进制 I / O 操作,boolean
、char
、byte
、short
、int
、 long
、float
and double
以及字符串值。所有数据流都实现 DataInput
接口或 DataOutput
接口。
为了方便地操作Java语言的基本数据类型和String的数据,可以使用数据流。
- 数据流有两个类:(用于读取和写出基本数据类型、String类的数据)
- DataInputStream 和 DataOutputStream
- 分别“套接”在 InputStream 和 OutputStream 子类的流上
- 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改为相应的即可。
boolean writeBoolean() byte writeByte() char writeChar() float writeFloat() double writeDouble() short writeShort() long writeLong() int writeInt() String writeUTF() void writeFully(byte[] b)
需要特别注意的是:
- 输入流判断是否结束,不是按普通流那样判断一个返回值,而是通过:
EOFException
异常。
此异常主要被数据输入流用来表明到达流的末尾。注意,其他许多输入操作返回一个特殊值表示到达流的末尾,而不是抛出异常。 - write 和 read 的取值顺序一定要匹配。
输入流由简单的二进制数据组成,没有指示单个值的类型,或者它们在流中开始的位置。
该示例使用一个非常糟糕的编程技术:它使用浮点数来表示货币值。一般来说,浮点对精确值是不利的。它对于小数分数特别不利,因为常用值(如 0.1)不具有二进制表示形式。
举例: DataOutputStream的使用
import java.io.DataOutputStream; import java.io.FileOutputStream; import java.io.IOException; public class DataInputStreamWriterTest { public static void main(String[] args) { DataOutputStream dataOutputStream = null; try { FileOutputStream fileOutputStream = new FileOutputStream("src/blogs/blog9/hello.txt"); // 1.创建对应的 DataOutputStream对象 dataOutputStream = new DataOutputStream(fileOutputStream); byte b = 100; short s = 200; int i = 300; long l = 400; float f = 2.0f; double d = 3.14; boolean bool = true; char c = 'A'; String str = "hello"; // 2. 写数据,将 内存当中的数据写入到文件中 dataOutputStream.writeByte(b); dataOutputStream.writeShort(s); dataOutputStream.writeInt(i); dataOutputStream.writeLong(l); dataOutputStream.writeFloat(f); dataOutputStream.writeDouble(d); dataOutputStream.writeBoolean(bool); dataOutputStream.writeChar(c); dataOutputStream.writeChars(str); dataOutputStream.writeChars(str); // 3.刷新 dataOutputStream.flush(); } catch (IOException e) { e.printStackTrace(); } finally { // 4.关闭IO资源 if (dataOutputStream != null) { try { dataOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
举例: DataInputStream 读取被 DataOutputStream 写入到文件中的信息
注意: write 和 read 的取值顺序一定要匹配。 DataOutputStream 依次写入到文件的 类型是什么顺序,后面的 DataInputStream 读取文件的类型的顺序就是什么要的,必须保持一致性,不然 读取获取到的数据是错误的。
import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class DataInputStreamWriterTest { public static void main(String[] args) { DataInputStream dataInputStream = null; try { FileInputStream fileInputStream = new FileInputStream("src/blogs/blog9/hello.txt"); // 1.创建对应的DataInputStream 对象 dataInputStream = new DataInputStream(fileInputStream); // 2. 读取其中文件的类型信息:注意:write 和 read 的取值顺序一定要匹配 byte b = dataInputStream.readByte(); System.out.println(b); short s = dataInputStream.readShort(); System.out.println(s); int i = dataInputStream.readInt(); System.out.println(i); long l = dataInputStream.readLong(); System.out.println(l); float f = dataInputStream.readFloat(); System.out.println(f); double d = dataInputStream.readDouble(); System.out.println(d); boolean bool = dataInputStream.readBoolean(); System.out.println(bool); char c = dataInputStream.readChar(); System.out.println(c); String str = dataInputStream.readLine(); System.out.println(str); } catch (IOException e) { e.printStackTrace(); } finally { // 3. 关闭IO资源 if (dataInputStream != null) { try { dataInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
4.4 对象流 (序列化 ObjectOutputStream,反序列化 ObjectInputStream)
就像数据流支持原始数据类型的 I / O,对象流支持 I / O 的对象。大部分,但不是全部,标准类支持对象的序列化。 都实现了标记接口 Serializable
对象流类是
- java.io.ObjectInputStream
- java.io.OjbectOutputSteam
- 用于存储和读取基本数据类型数据对象的处理流。它的强大之处就是可以把 Java中的对象写入到硬盘当中,也能把对象从硬盘当中还原回来。
- 序列化: 用
ObjectOutputStream
类将基本数据类型或对象类型存储保存到硬盘文件的当中。 - 反序列化: 用
ObjectInputStream
类 读取存储到硬盘文件当中的基本数据类型或对象类型还原回来。存储到内存当中。 - 对象序列化机制 :允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的 Java对象 。
- 序列化是 RMI(Remote Method Invoke – 远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是 JavaEE 平台的基础。
- Java的序列化与反序列化的图示如下:
什么时候需要用到序列化和反序列化呢?
在本地 JVM 里运行下 Java 实例,这个时候是不需要什么序列化和反序列化的
但当我们需要将内存中的对象持久化到磁盘,数据库中时, 当我们需要与浏览器进行交互时,或者当我们需要实现 RPC 时, 这个时候就需要序列化和反序列化了。
只要我们对 JVM 堆内存中的对象进行持久化或网络传输, 这个时候都需要序列化和反序列化。
序列化的好处: 在于可将任何实现了 Serializable 接口的对象转化为 字节数据 ,使其在保存和传输时可被还原。
JSON 格式实际上就是将一个对象转化为字符串, 所以服务器与浏览器交互时的数据格式其实是字符串,我们来看来 String 类型的源码:
String 类型实现了 Serializable 接口,并显示指定 serialVersionUID 的值。也就是说我们使用Json进行传输字符串数据的时候, JVM已经将字符串数据序列化了
4.4.1 对象的序列化
如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一。 否则,会抛出NotSerializableException
异常。
- java.io.Serializable 接口一般使用这个接口,序列化
- java.io.Externalizable
凡是实现了 Serializable 接口的类都有一个表示序列化版本标识符的静态变量:
private static final long serialVersionUID;
- Java当中所有的包装类以及 String 都实现了
java.io.Serializable
接口。所以一般要实现该接口的都是我们自定义的类。
- serialVersionUID用来表明类的不同版本间的兼容性。简言之,其目的是以序列化对象 进行版本控制,有关各版本反序列化时是否兼容。在 Java 中实现了 Serializable 接口后, JVM 在类加载的时候就会发现我们实现了这个接口, 然后在初始化实例对象的时候就会在底层帮我们实现序列化和反序列化
- 如果一个类实现了了
java.io.Serializable
接口,但是却没有定义这个 serialVersionUID 静态变量,以及也没有从父类中继承这个静态变量。那么它的值会由 Java运行时环境根据类的内部细节自动生成。这个自动生成的值,我们是看不到的,导致我们无法手动修改,而且每次生成的都不太一样。若类中的实例变量做了修改,那么 **serialVersionUID ** 可能会发生变化,就不是原来的了。故建议,手动显式定义该静态变量,不要让Java自动生成。 - 简单来说,Java的序列化机制是通过在运行时,判断类的 serialVersionUID 来验证版本是否一致性的,在进行反序列化时,JVM会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)
- Java 序列化机制采用了一种特殊的序列化算法, 其算法内容如下:
所有保存到磁盘中的对象都有一个序列化编号。
当程序试图序列化一个对象时, 程序将先检查该对象是否己经被序列化过, 只有该对象从未(在本次虚拟机中) 被序列化过, 系统才会将该对象转换成字节序列并输出。
如果某个对象已经序列化过, 程序将只是直接输出一个序列化编号, 而不是再次重新序列化该对象。
举例:使用:ObjectOutputStream 类 将自定义类序列化(将类对象类型存储到硬盘文件中),该类没有实现 java.io.Serializable
这个 接口的错误无法序列化的错误演示:
package blogs.blog9; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; public class ObjectOutputStreamTest { public static void main(String[] args) { ObjectOutputStream objectOutputStream = null; try { FileOutputStream fileOutputStream = new FileOutputStream("src/blogs/blog9/temp"); // 1. 创建 ObjectOutputStream 序列化输出流对象 // 这里的参数是 OutputStream ,而 FileOutputSteam 实现了该接口 objectOutputStream = new ObjectOutputStream(fileOutputStream); Person person = new Person("Tom",99); // 2. 将自定义的Person 对象序列化:存储到硬盘文件当中去. objectOutputStream.writeObject(person); // 3. 刷新 objectOutputStream.flush(); } catch (IOException e) { e.printStackTrace(); } finally { // 4 关闭IO资源 if (objectOutputStream != null) { try { objectOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } } // 该自定义类没有实现 java.io.Serializable 接口,无法序列化 class Person { String name; int age; public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + ''' + ", age=" + age + '}'; } }
举例: 自定义类 Person 实现了 Serailaizable 接口,但是没有显式定义 serialVersionUID 静态属性,而是由Java自动生成的。
package blogs.blog9; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.Serializable; public class ObjectOutputStreamTest { public static void main(String[] args) { ObjectOutputStream objectOutputStream = null; try { FileOutputStream fileOutputStream = new FileOutputStream("src/blogs/blog9/temp"); // 1. 创建 ObjectOutputStream 序列化输出流对象 // 这里的参数是 OutputStream ,而 FileOutputSteam 实现了该接口 objectOutputStream = new ObjectOutputStream(fileOutputStream); Person person = new Person("Tom",99); // 2. 将自定义的Person 对象序列化:存储到硬盘文件当中去. objectOutputStream.writeObject(person); // 3. 刷新 objectOutputStream.flush(); } catch (IOException e) { e.printStackTrace(); } finally { // 4 关闭IO资源 if (objectOutputStream != null) { try { objectOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } } // 该自定义类没有实现 java.io.Serializable 接口,无法序列化 class Person implements Serializable { String name; int age; public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + ''' + ", age=" + age + '}'; } }
4.4.2 对象的反序列化
举例: 使用 ObjectInputStream 类读取存储到对象的序列化文件 temp 。
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class ObjectOutputStreamTest { public static void main(String[] args) { ObjectInputStream objectInputStream = null; try { FileInputStream fileInputStream = new FileInputStream("src/blogs/blog9/temp"); // 注意文件后缀 // 1. 创建反序列化输入对象 // 注意: 这里的参数是: InputSteam ,而FileInputStream 实现了该接口 objectInputStream = new ObjectInputStream(fileInputStream); // 2.读取其中序列化对象信息 // Object object = objectInputStream.readObject(); // 这里因为我们知道该文件中序列化的对象是 Person类型的,所以可以直接进行强制转化 Person person = (Person) objectInputStream.readObject(); System.out.println(person); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { // 3. 关闭IO资源 if (objectInputStream != null) { try { objectInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } } // 该自定义类实现 java.io.Serializable 接口 class Person implements Serializable { String name; int age; public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + ''' + ", age=" + age + '}'; } }
4.4.3 不显式定义 serialVersionUID 值的问题
不指定serialVersionUID出现的问题
- 如果我们自定义的类想要序列化,实现了
java.io.Serializable
接口,但是却没有显式定义 serialVersionUID 静态属性,而是由Java自动 生成的。但是该自动生成的 serialVersionUID,我们无法修改,当我们对应的类中是属性发生了改变时,由于我们没有自行显式定义 serialVersionUID静态属性,导致Java会重新生成一个新的 serialVersionUID 属性值,该属性值与原先就的属性值的版本不一致,导致再次想序列化对象时,会出错。 - 如果我们在不同的电脑上都有一个Person类, 我们想通过网络进行传输, 那就必须现在A电脑实现序列化, 在B电脑实现反序列化, 那么如果我们不指定serialVersionUID就就有可能反序列化失败
- 在实例开发过程中, 我们的类会经常改变, 如果我们使用JVM帮我们自动生成的serialVersionUID, 那么如果这个类已经有一些序列化对象, 那我们一旦修改了这个类,这些对象反序列化的时候就都会报错。
为什么要显式定义serialVersionUID的值?
如果不显示指定 serialVersionUID, JVM 在序列化时会根据属性自动生成一个 serialVersionUID, 然后与属性一起序列化,再进行持久化或网络传输。
在反序列化时,JVM 会再根据属性自动生成一个新版 serialVersionUID,然后将这个新版 serialVersionUID 与序列化时生成的旧版 serialVersionUID 进行比较,如果相同则反序列化成功, 否则报错.
如果显示指定了 serialVersionUID, JVM 在序列化和反序列化时仍然都会生成一个 serialVersionUID, 但值为我们显示指定的值, 就会进行serialVersionUID值的覆盖,这样在反序列化时新旧版本的 serialVersionUID 就一致了。
举例: 在没有自定义 serialVersionUID 的值的前提:这里我们修改了Person类中是属性,多加一个 int id
属性后,反序列化,读取存储序列化文件时的报错提示:java.io.InvalidClassException:
package blogs.blog9; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class ObjectOutputStreamTest { public static void main(String[] args) { ObjectInputStream objectInputStream = null; try { FileInputStream fileInputStream = new FileInputStream("src/blogs/blog9/temp"); // 注意文件后缀 // 1. 创建反序列化输入对象 // 注意: 这里的参数是: InputSteam ,而FileInputStream 实现了该接口 objectInputStream = new ObjectInputStream(fileInputStream); // 2.读取其中序列化对象信息 // Object object = objectInputStream.readObject(); // 这里因为我们知道该文件中序列化的对象是 Person类型的,所以可以直接进行强制转化 Person person = (Person) objectInputStream.readObject(); System.out.println(person); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { // 3. 关闭IO资源 if (objectInputStream != null) { try { objectInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } } // 该自定义类实现 java.io.Serializable 接口 class Person implements Serializable { String name; int age; // 多加一个属性 int id; public Person() { } public Person(String name, int age, int id) { this.name = name; this.age = age; this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public int getId() { return id; } public void setId(int id) { this.id = id; } @Override public String toString() { return "Person{" + "name='" + name + ''' + ", age=" + age + ", id=" + id + '}'; } }
显式定义serialVersionUID 的值:
- 定义的 serialVersionUID 的值是 private 私有化的,static 静态的所以对象共用,final 常量不可修改的,值是 Long 类型的
private static final long serialVersionUID = -6849794470754667710L;
- Java比较判断版本号时,是在同一个项目中,先判断类名是否相等,类名相等的前提下,再比较判断对应的类名的版本号。所以版本号可以不用设置的太大 ,1L 也是可以的。
- 注意静态属性名是:serialVersionUID 这是固定的。不要修改了。
private static final long serialVersionUID = 1L;
举例: 在显式定义 serialVersionUID 的值的前提:这里我们修改了Person类中是属性,多加一个 int id
属性后,反序列化,读取存储序列化文件时的。读取正常。注意: 要先将我们已经显式定义的 serialVersionUID 的值,先序列化一下,再添加 int id 属性,后在反序列化。不然你就是还是用的是 serialVersionUID 由Java自行生成的版本号。还是会报错的。
package blogs.blog9; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class ObjectOutputStreamTest { public static void main(String[] args) { ObjectInputStream objectInputStream = null; try { FileInputStream fileInputStream = new FileInputStream("src/blogs/blog9/temp"); // 注意文件后缀 // 1. 创建反序列化输入对象 // 注意: 这里的参数是: InputSteam ,而FileInputStream 实现了该接口 objectInputStream = new ObjectInputStream(fileInputStream); // 2.读取其中序列化对象信息 // Object object = objectInputStream.readObject(); // 这里因为我们知道该文件中序列化的对象是 Person类型的,所以可以直接进行强制转化 Person person = (Person) objectInputStream.readObject(); System.out.println(person); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { // 3. 关闭IO资源 if (objectInputStream != null) { try { objectInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } } // 该自定义类实现 java.io.Serializable 接口 class Person implements Serializable { String name; int age; // 多加一个属性 int id; // 显式定义了 serialVersionUID 的版本号值 private static final long serialVersionUID = 1L; public Person() { } public Person(String name, int age, int id) { this.name = name; this.age = age; this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public int getId() { return id; } public void setId(int id) { this.id = id; } @Override public String toString() { return "Person{" + "name='" + name + ''' + ", age=" + age + ", id=" + id + '}'; } }
4.4.3.1 设置不被序列化的类型
在一些特殊的场景下, 如果一个类里包含的某些实例变量是敏感信息, 例如银行账户信息等, 这时不希望系统将该实例变量值进行序列化; 或者某个实例变量的类型是不可序列化的, 因此不希望对该实例变量进行递归序列化, 以避免引java.io.NotSerializableException
异常。
通过在实例变量前面使用 transient 关键字修饰, 可以指定 Java 序列化时无须理会该实例变量。 如下 Person 类与前面的 Person 类几乎完全一样, 只是它的 age 使用了 transient 关键字修饰。
简单的说就是:被 transient 关键字修饰的属性不会被序列化到文件中,更不会被反序列化读取到
举例: 将 Person 类中的 age 被 transizent 修饰,不会被序列化。
transient int age;
先序列化一下:将对象存储到文件中
package blogs.blog9; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class ObjectOutputStreamTest { public static void main(String[] args) { ObjectOutputStream objectOutputStream = null; try { FileOutputStream fileOutputStream = new FileOutputStream("src/blogs/blog9/temp"); // 1. 创建 ObjectOutputStream 序列化输出流对象 objectOutputStream = new ObjectOutputStream(fileOutputStream); Person person = new Person("Tom", 99,001); //2. 将自定义的Person 对象序列化:存储到硬盘文件当中去. objectOutputStream.writeObject(person); // 3. 刷新 objectOutputStream.flush(); } catch (IOException e) { e.printStackTrace(); } finally { // 4 关闭IO资源 if (objectOutputStream != null) { try { objectOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } } // 该自定义类实现 java.io.Serializable 接口 class Person implements Serializable { String name; transient int age; // 被transient 修饰不会序列化 // 多加一个属性 int id; // 显式定义了 serialVersionUID 的版本号值 private static final long serialVersionUID = 1L; public Person() { } public Person(String name, int age, int id) { this.name = name; this.age = age; this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public int getId() { return id; } public void setId(int id) { this.id = id; } @Override public String toString() { return "Person{" + "name='" + name + ''' + ", age=" + age + ", id=" + id + '}'; } }
再反序化查看效果:
package blogs.blog9; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class ObjectOutputStreamTest { public static void main(String[] args) { ObjectInputStream objectInputStream = null; try { FileInputStream fileInputStream = new FileInputStream("src/blogs/blog9/temp"); // 注意文件后缀 // 1. 创建反序列化输入对象 // 注意: 这里的参数是: InputSteam ,而FileInputStream 实现了该接口 objectInputStream = new ObjectInputStream(fileInputStream); // 2.读取其中序列化对象信息 // Object object = objectInputStream.readObject(); // 这里因为我们知道该文件中序列化的对象是 Person类型的,所以可以直接进行强制转化 Person person = (Person) objectInputStream.readObject(); System.out.println(person); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { // 3. 关闭IO资源 if (objectInputStream != null) { try { objectInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } } // 该自定义类实现 java.io.Serializable 接口 class Person implements Serializable { String name; transient int age; // 多加一个属性 int id; // 显式定义了 serialVersionUID 的版本号值 private static final long serialVersionUID = 1L; public Person() { } public Person(String name, int age, int id) { this.name = name; this.age = age; this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public int getId() { return id; } public void setId(int id) { this.id = id; } @Override public String toString() { return "Person{" + "name='" + name + ''' + ", age=" + age + ", id=" + id + '}'; } }
注意:同样的被 **static**
修饰为静态的属性也不会被序列化
static 属性为什么不会被序列化?
因为序列化是针对实例对象而言的,而 static 属性优先于对象存在, 随着类的加载而加载, 所以不会被序列化.
是不是有人会问, serialVersionUID 也被 static 修饰, 为什么 serialVersionUID 会被序列化?
其实 serialVersionUID 属性并没有被序列化, JVM 在序列化对象时会自动生成一个 serialVersionUID, 然后将我们显示指定的 serialVersionUID 属性值赋给自动生成的 serialVersionUID。
补充:
一次性序列化多个对象:可以,可以将对象放到集合当中,序列化集合。如下
创建多个 Person 对象,并存储到 List 集合中并,反序列化到 temp 文件中。
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.List; public class ObjectOutputStreamTest { public static void main(String[] args) { ObjectOutputStream objectOutputStream = null; try { FileOutputStream fileOutputStream = new FileOutputStream("src/blogs/blog9/temp"); // 1. 创建 ObjectOutputStream 序列化输出流: 注意构造器的参数是 OutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); // 2. 创建多个 Person 对象类型,存储到 List 集合中 Person person1 = new Person("Tom",18,001); Person person2 = new Person("zhangsan",28,002); Person person3 = new Person("lisi",20,003); // <Person>泛型限定存储对象类型 List<Person> list = new ArrayList<Person>(); // 添加元素 list.add(person1); list.add(person2); list.add(person3); // 3. 序列化:将存储到 List集合中的元素,写入到 temp 硬盘文件中 objectOutputStream.writeObject(list); // 4. 刷新:将遗留在内存中没有写入到文件的信息,强制全部写入到文件中,防止数据丢失 objectOutputStream.flush(); } catch (IOException e) { e.printStackTrace(); } finally { // 5. 关闭IO资源 if (objectOutputStream != null) { try { objectOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } } // 该自定义类实现 java.io.Serializable 接口 class Person implements Serializable { String name; transient int age; // 多加一个属性 int id; // 显式定义了 serialVersionUID 的版本号值 private static final long serialVersionUID = 1L; public Person() { } public Person(String name, int age, int id) { this.name = name; this.age = age; this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public int getId() { return id; } public void setId(int id) { this.id = id; } @Override public String toString() { return "Person{" + "name='" + name + ''' + ", age=" + age + ", id=" + id + '}'; } }
读取文件中的多个序列化对象 :将我存储到 List 集合中的多个对象,从文件中读取到内存当中
package blogs.blog9; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.List; public class ObjectOutputStreamTest { public static void main(String[] args) { ObjectInputStream objectInputStream = null; try { FileInputStream fileInputStream = new FileInputStream("src/blogs/blog9/temp"); // 1.创建 ObjectInputStream 反序列化输入流对象,构造器参数是 InputStream objectInputStream = new ObjectInputStream(fileInputStream); // 2. 读取存储序列信息的文件到内存当中 // 因为这里我们知道该文件中存储的是 List<Person> 集合类型的所以可以直接强制转化 List<Person> list = (List<Person>)objectInputStream.readObject(); // 遍历集合 for (Person person : list) { System.out.println(person); } } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { // 3. 关闭IO资源 if (objectInputStream != null) { try { objectInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } } // 该自定义类实现 java.io.Serializable 接口 class Person implements Serializable { String name; transient int age; // 多加一个属性 int id; // 显式定义了 serialVersionUID 的版本号值 private static final long serialVersionUID = 1L; public Person() { } public Person(String name, int age, int id) { this.name = name; this.age = age; this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public int getId() { return id; } public void setId(int id) { this.id = id; } @Override public String toString() { return "Person{" + "name='" + name + ''' + ", age=" + age + ", id=" + id + '}'; } }
5. 实用案例:
拷贝目录以及目录下的所有文件
package day26; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; /** * 拷贝目录以及目录下的所有文件 */ public class CopyAll { /** * 拷贝目录下的所有文件 * @param srcFile * @param destFile */ private static void copyDir(File srcFile, File destFile) { // 递归结束条件。(是文件是最后一层了,不用再递归下去了) // 判断该拷贝对象是否是文件, if(srcFile.isFile()) { // srcFile如果是文件的话,将文件拷贝完,就返回了递归结束,因为文件都是最后的东西的 // 是文件拷贝,一边读一边写 FileInputStream in = null; FileOutputStream out = null; try { in = new FileInputStream(srcFile); String path = destFile.getAbsolutePath().endsWith("\") ? destFile.getAbsolutePath() : "\" + srcFile.getAbsolutePath().substring(3); out = new FileOutputStream(destFile); // 一边读,一边写 byte[] bytes = new byte[1024*1024]; // 一次复制1mb int readCout = 0; while((readCout = in.read(bytes)) != -1) { out.write(bytes,0,readCout); // 读到多少返回多少 } // 刷新 out.flush(); } catch (IOException e) { e.printStackTrace(); } finally { try { in.close(); } catch (IOException e) { e.printStackTrace(); } try { out.close(); } catch (IOException e) { e.printStackTrace(); } } return; } // 获取被拷贝的目录下面的子目录:该目录下的目录和文件 File[] files = srcFile.listFiles(); for (File file : files) { // 判断该文件是否在同一个目录下 if(file.isDirectory()) { // D:/curse/02-javaSe/ 被拷贝的目录 // C:/curse/02-javaSe/ 到的目录 两者之间的目录盘必须是一样的 // 获取所有文件的(包括目录和文件)绝对路径 String srcDir = file.getAbsolutePath(); String destDir = file.getAbsolutePath().endsWith("\") ? destFile.getAbsolutePath() : "\" + srcDir.substring(3); File newFile = new File(destDir); // 判断该 目录是否存在,不存在创建 if(!newFile.exists()) { newFile.mkdirs(); // 多重目录的创建 } } // 递归调用 copyDir(file,destFile); // 将其中的文件/目录,拷贝到 destFile目录中 } } }
6. 文件 I/O(nio.2)
关于这部分内容,大家可以移步至:🔜🔜🔜 https://zq99299.github.io/java-tutorial/essential/io/fileio.html 观看学习。
7. 总结:
- File 类中的方法的使用。
- I/O 流的分类,输入流:将硬盘文件的数据信息写入到内存当中;输出流:将内存当中的信息写入到硬盘文件中。
- Java的IO流共涉及40多个类,实际上非常规则,都是从如下 Java Io 流四大家族 个 抽象基类派生的。
- I/O流的四大首领:java.io.InputStream ,java.io.OutputStream,java.io.Reade,java.io.Writer。
- 在Java中只要 类名是以
"stream"
结尾的都是字节流,以"Reader/Writer"
结尾的都是字符流。 - 字节流和字符流的用法几乎完全一样, 区别在于字节流和字符流操作的数据单元的不同:字节流是 8 位的字节, 而字符流操作的数据单元是 16 位的字符。其中还有一点不同的就是:
- 字符流:只能读取操作文本文件,因为字符流读取的是文件中的 char 字符信息。
.c,.java,.c++,.txt
等等这些都是文本文件不仅仅只是 txt文件,而特别注意的是 :.wrod 不是文本文件,wrod中的文字是经过特殊处理的存在一定的规范格式,不是纯文本文件。 - 字节流:可以操作任何的文件,因为字节流读取的是二进制信息,读取1个字节byte,等同于一次读取8个二进制,这种流是万能的,什么类型的文件都可以读取到,因为文件都是有二进制组成的。包括: 文本文件,图片,声音文件。
- 灵活使用:字节流,字符流,缓冲流,转换流,标准输入、输出流,数据流
- 序列化:把对象转换为字节序列的过程称为对象的序列化。将对象写入到硬盘文件中
反序列化:把字节序列恢复为对象的过程称为对象的反序列化。将存储到硬盘文件中的对象,读取到内存当中。 - 如果某个类的属性不是基本数据类型或 String 类型,而是另一个 引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的Field 的类也不能序列化。
- 如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口(
java.io.Serializable(常用),java.io.Externalizable
)之一。 否则,会抛出NotSerializableException
异常。 - 建议显式定义 serialVersionUID 版本值。
private static final long serialVersionUID = -6849794470754667710L;
- 被 transient 关键字修饰的属性不会被序列化, static 属性也不会被序列化.
10. 最后:
👍👍👍 ✏️✏️✏️✏️✏️✏️✏️✏️✏️✏️ 感谢以下大佬提供的参考资料 ✏️✏️✏️✏️✏️✏️✏️✏️✏️✏️ 👍👍👍
【1】: https://fighter3.blog.csdn.net/article/details/103554407
【2】:https://zq99299.github.io/java-tutorial/essential/io/streams.html
【3】:https://blog.csdn.net/Shangxingya/article/details/113744323
限于自身水平,其中存在的错误,希望大家给予指教,韩信点兵——多多益善,谢谢大家,后会有期,江湖再见 !!!