文章目录
- Demo16-File、递归、IO流
- 1.总体概述
- 2.File类
- 2.1File类概述
- 2.2File类常见操作API
- 2.2.1判断文件信息获取文件信息
- 2.2.2创建文件,删除文件功能
- 2.2.3File类的遍历功能
- 3.方法递归
- 3.1递归的形式和特点
- 3.2递归的算法流程,核心要素
- 3.3递归的经典问题
- 3.5非规律化递归案例-文件搜索
- 3.6非规律化递归案例-啤酒问题
- 4.字符集
- 4.1常见字符集介绍
- 4.1.1字符集基础知识
- 4.1.2常见字符集
- 4.2字符集的编码,解码操作
- 5.IO流
- 5.1IO流概述
- 5.2字节流的使用
- 5.2.1字节输入流
- 5.2.2字节输出流
- 5.2.3文件拷贝
- 5.2.4资源释放的方式
- 5.2.4.1try-catch-finally
- 5.2.4.2try-with-resource
- 5.3字符流的使用
- 5.3.1字符输入流
- 5.3.2字符输出流
- 6.缓冲流
- 6.1缓冲流概述
- 6.2字节缓冲流
- 6.3字符缓冲流
- 7.转换流
- 7.1问题引出
- 7.2字符输入转换流
- 7.3字符输出转换流
- 8.对象序列化与反序列化
- 9.打印流
- 10.Properties
- 11.IO框架
Demo16-File、递归、IO流
1.总体概述
目前我们是在内存存储数据的,在程序运行中进行处理,修改,运算等操作,这样的存储方式不能长久保存数据
想要长久保存数据我们可以将数据存储到文件中,磁盘中数据的形式就是文件,文件是数据的载体
- 先要定位文件
- File类可以定位文件:进行删除,创建文件夹,获取文件信息等操作
- 但是File类不能读写文件内容
- 想要读写文件内容需要用到IO流技术对硬盘中的文件进行读写
关于File,IO流,我们需要学会什么
- File类使用
- 能够使用File的对象操作文件,如:删除,获取文件信息,创建文件夹等
- 方法递归
- 理解递归算法思想并能完成常见递归题目,以及使用递归进行文件搜索
- 字符集
- 程序中经常要读取文件中的数据,程序员必须先知道数据的底层形式才能够去学习如何读写数据
- IO流的作用,分类
- 能够使用IO流完成文件数据的读写操作
- 字节流,字符流
- 数据的类型很多,要学会选择不同的流进行读写操作
2.File类
2.1File类概述
- File类在包java.io.File下,代表操作系统的文件对象(文件,文件夹)
- File类提供了诸如:定位文件,获取文件本身信息,如删除文件,创建文件(文件夹)等功能
File类创建对象:
方法名称 | 说明 |
public File(String pathname) | 根据文件路径创建文件对象 |
public File(String parent, String child) | 从父路径名字符串和子路径名字符串创建文件对象 |
public File(File parent, String child) | 根据父路径对应文件对象和子路径名字符串创建文件对象 |
public class FileDemo {
public static void main(String[] args) {
//1.创建文件对象(依据绝对路径)
//依据绝对路径有三种方式:
//方式一:反斜杠前面都再加一个反斜杠是因为可能文件夹名或文件名是n开头,而\n代表换行,就读取不到后续的路径了
File f = new File("D:\\study\\java\\Markdown\\javaSE\\images\\nscreen.png");
//方式二:(File.separator代表系统的分隔符)
File f22 = new File("D:" + File.separator + "study" +
File.separator + "java" + File.separator + "Markdown" +
File.separator + "javaSE" + File.separator + "images" +
File.separator + "nscreen.png");
//方式三:
File f33 = new File("D:/study/java/Markdown/javaSE/images/nscreen.png");
long size = f.length();//计算机的一切都是都字节组成的,所以这里拿到的是文件的字节大小
System.out.println(size);
System.out.println(f22.length());
System.out.println(f33.length());
//2.创建文件对象(依据相对路径,相对路径一般用来定位模块中的文件)
File f2 = new File("Demo19-file-io/src/data.txt");
System.out.println(f2.length());
//3.File创建对象,可以是文件也可以是文件夹,下面用创建一个文件夹对象
File f3 = new File("D:\\study\\java\\Markdown\\javaSE\\images");
System.out.println(f3.length());
System.out.println(f3.exists());//判断该路径下的文件夹是否存在
}
}
注意点:
- File封装的对象仅仅是一个路径名,也就是说创建File对象时并没有和计算机硬盘进行交互,所以也就不存在说:这个路径不存在,new File对象时报错
- 依据相对路径new File对象时相对路径是相对于工程的,我创建的工程名是javaSE,那么new File(“Demo19-file-io/src/data.txt”);就表示去当前工程下找Demo19-file-io模块下的src文件件下的data.txt文件
- 也就是说相对路径是直接到当期工程下的目录寻找文件
- 相对路径的好处就是你把我这个模块放到你的工程下面,你不需要改任何代码就可以定位到文件
- 我们不要输出打印文件夹的字节大小,文件和文件夹在File面前被统一视为文件,所以并不会去计算文件夹下所有文件的大小并求和,而是直接获取文件夹本身的大小,获取的这个大小可能是0,也可能很小,反正不是该文件夹下所有文件的大小之和,所以输出打印文件夹的大小没有任何意义
- 计算中直接查看文件夹大小可以显示该文件夹下所有文件的大小之和,那是人家系统自己计算的
2.2File类常见操作API
2.2.1判断文件信息获取文件信息
方法名称 | 说明 |
public boolean isDirectory() | 测试此抽象路径名表示的File是否为文件夹 |
public boolean isFile() | 测试此抽象路径名表示的File是否为文件 |
public boolean exists() | 测试此抽象路径名表示的File是否存在 |
public String getAbsolutePath() | 返回此抽象路径名的绝对路径字符串 |
public String getPath() | 将此抽象路径名转换为路径名字符串 |
public String getName() | 返回由此抽象路径名表示的文件或文件夹名称(文件的话带后缀) |
public long lastModified() | 返回文件最后修改的时间毫秒值 |
public class FileDemo02 {
public static void main(String[] args) {
// 1.绝对路径创建一个文件对象
File f1 = new File("Demo19-file-io/src/data.txt");
// a.获取它的绝对路径。
System.out.println(f1.getAbsolutePath());
// b.获取文件定义的时候使用的路径。
System.out.println(f1.getPath());
// c.获取文件的名称:带后缀。
System.out.println(f1.getName());
// d.获取文件的大小:字节个数。
System.out.println(f1.length()); // 字节大小
// e.获取文件的最后修改时间
long time = f1.lastModified();//获取的是毫秒值
System.out.println("最后修改时间:" + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(time));
// f、判断文件是文件还是文件夹
System.out.println(f1.isFile()); // true
System.out.println(f1.isDirectory()); // false
File file = new File("D:/");
System.out.println(file.isFile()); // false
System.out.println(file.isDirectory()); // true
System.out.println(file.exists()); // true
File file1 = new File("D:/aaa");
System.out.println(file1.isFile()); // false
System.out.println(file1.isDirectory()); // false
System.out.println(file1.exists()); // false
}
}
2.2.2创建文件,删除文件功能
File类创建文件的功能
方法名称 | 说明 |
public boolean createNewFile() | 创建一个新的空的文件 |
public boolean mkdir() | 只能创建一级文件夹 |
public boolean mkdirs() | 可以创建多级文件夹 |
File类删除文件的功能
方法名称 | 说明 |
public boolean delete() | 删除由此抽象路径名表示的文件或空文件夹 |
- delete方法直接删除不走回收站
- 删除文件时即使文件已经被占用(打开状态)仍可删除
- delete方法默认只能删除空文件夹
//以下代码运行后我的计算机中
//1.创建了D:\study\java\Markdown\javaSE下的ccc文件夹及其子文件夹
//2.删除了D:\study\java\Markdown\javaSE\images下的idea.png
//3.其他无改变
public class FileDemo03 {
public static void main(String[] args) throws IOException {
File f = new File("Demo19-file-io\\src\\data.txt");
// a.创建新文件,创建成功返回true(几乎不用这个API,因为以后通过IO流写入数据时如果文件不存在会自动创建新文件)
System.out.println(f.createNewFile());//false(该文件已存在)
File f1 = new File("Demo19-file-io\\src\\data02.txt");
System.out.println(f1.createNewFile());//true
// b.mkdir创建一级目录
File f2 = new File("D:\\study\\java\\Markdown\\javaSE\\aaa");
System.out.println(f2.mkdir());//true
// c.mkdirs创建多级目录(重点)
File f3 = new File("D:\\study\\java\\Markdown\\javaSE\\ccc\\ddd\\eee\\ffff");
System.out.println(f3.mkdir());//false,因为mkdir不支持多级创建
System.out.println(f3.mkdirs());//true
// d.删除文件或者空文件夹
System.out.println(f1.delete());//true
File f4 = new File("D:\\study\\java\\Markdown\\javaSE\\images\\idea.png");
System.out.println(f4.delete());//true,占用一样可以删除
// 只能删除空文件夹,不能删除非空文件夹.
File f5 = new File("D:\\study\\java\\Markdown\\javaSE\\aaa");
System.out.println(f5.delete());//true
File f6 = new File("D:\\study\\java\\Markdown\\javaSE\\ccc");
System.out.println(f6.delete());//false
}
}
2.2.3File类的遍历功能
方法名称 | 说明 |
public String[] list() | 获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回 |
public File[] listFiles() | 获取当前目录下所有的"一级文件对象"到一个文件对象数组中去返回 |
listFiles方法注意事项
- 当调用者不存在时,返回null
- 当调用者是一个文件时,返回null
- 当调用者是一个空文件夹时,返回一个空数组
- 当调用者是一个有内容的文件夹时,将里面所有文件和文件夹路径放在File数组中返回
- 当调用者是一个有隐藏文件的文件夹时,将里面所有文件和文件夹路径放在File数组中返回,包含隐藏内容
- 当调用者是一个需要权限才能进入的文件夹时,返回null
public class FileDemo04 {
public static void main(String[] args) {
File f1 = new File("D:\\study\\java\\Markdown\\javaSE\\images");
//1.使用list方法遍历
String[] names = f1.list();
for (String name : names) {
System.out.println(name);
}
//2.使用listFiles遍历
File[] files = f1.listFiles();
for (File f : files) {
System.out.println(f.getAbsolutePath());
}
//注意事项
//1.调用者不存在
File f2 = new File("D:/ddd");
File[] files2 = f2.listFiles();
System.out.println(files2);//null
//2.调用者是一个文件
File f3 = new File("D:\\study\\java\\Markdown\\javaSE\\images\\出师表.txt");
File[] files3 = f3.listFiles();
System.out.println(files3);//null
//3.调用者是一个空文件夹
File f4 = new File("D:\\study\\java\\Markdown\\javaSE\\images\\aaa");
File[] files4 = f4.listFiles();
System.out.println(Arrays.toString(files4));//[]
}
}
3.方法递归
3.1递归的形式和特点
1.方法直接调用自己或者间接调用自己的形式称为方法递归
2.递归的形式:
- 直接递归:方法自己调用自己
- 间接递归:方法调用其他方法,其他方法又回调方法自己
3.递归如果没有控制好终止,会出现递归死循环,导致栈内存溢出现象
3.2递归的算法流程,核心要素
public class RecursionDemo02 {
public static void main(String[] args) {
System.out.println(f(5));
}
public static int f(int n){
if(n == 1){
return 1;
}else {
return f(n - 1) * n;
}
}
}
递归算法三要素:
- 递归的公式:f(n) = f(n-1) * n
- 递归的终点:f(1)
- 递归的方向必须走向终结点
3.3递归的经典问题
猴子第一天摘下若干桃子,当即吃了一半,觉得好不过瘾,于是又多吃了一个,第二天又吃了前一天剩余桃子数量的一半,觉得好不过瘾,于是又多吃了一个,以后每天都是吃前一天剩余桃子数量的一半,觉得好不过瘾,又多吃了一个,等到第10天的时候发现桃子只有1个了
问:猴子第一天摘了多少个桃子
推理公式:
f(x) - f(x)/2 - 1 = f(x+1)—>f(x) = 2f(x+1) + 2
也就是求f(1)
我们已经知道f(10) = 1
所以满足三要素:有递归公式,有终点,递归的方向是走向终结点f(10)的,所以可以用递归
public class RecursionDemo04 {
public static void main(String[] args) {
System.out.println(f(1));
}
public static int f(int n){
if(n == 10){
return 1;
}else {
return 2 * f(n + 1) + 2;
}
}
}
3.5非规律化递归案例-文件搜索
在上述的案例中递归算法都是针对存在规律化的递归问题,但实际场景中有很多问题是非规律化的递归问题,这种就只能自己看着办,需要流程化的编程思维
需求:从D:盘中,搜索出某个文件名称并输出绝对路径
分析:
- 先定位出的应该是一级文件对象
- 遍历全部一级文件对象,判断是否是文件
- 如果是文件,判断是否是自己想要的
- 如果是文件夹,需要继续递归进去重复上述过程
//目标:去E盘搜索WeChat.exe
public class RecursionDemo05 {
public static void main(String[] args) {
// 2、传入目录 和 文件名称
searchFile(new File("E:/") , "WeChat.exe");
}
/**
* 1、搜索某个目录下的全部文件,找到我们想要的文件。
* @param dir 被搜索的源目录
* @param fileName 被搜索的文件名称
*/
public static void searchFile(File dir,String fileName){
// 3、判断dir是否是目录(用户初次传时,可能给文件对象
// 赋null,或者传一个不存在的路径,或者传入的路径不是文件夹)
if(dir != null && dir.isDirectory()){
// 4、提取当前目录下的一级文件对象
File[] files = dir.listFiles();//files为null三种情况:dir的路径不存
// 在,dir是一个文件,dir需要权限才能进入,前两个已经在外层if语句判断过了所以
// 不再可能出现,目前引起files为null的原因只有第三种,将在下一下if语句判断
// 5、进行判断
if(files != null && files.length > 0) {//可能为null或空数组
for (File file : files) {
// 6、判断当前遍历的一级文件对象是文件 还是 目录
if(file.isFile()){
// 7、是不是咱们要找的,是把其路径输出即可
if(file.getName().contains(fileName)){//这是模糊查找,也就是即使我传的fileName是eChat.exe也一样可以找到
System.out.println("找到了:" + file.getAbsolutePath());
// 启动它。
try {
Runtime r = Runtime.getRuntime();
r.exec(file.getAbsolutePath());
} catch (IOException e) {
e.printStackTrace();
}
}
}else {
// 8、是文件夹,需要继续递归寻找
searchFile(file, fileName);
}
}
}
}else {
System.out.println("对不起,当前搜索的位置不是文件夹!");
}
}
}
3.6非规律化递归案例-啤酒问题
啤酒2元1瓶,4个盖子可以换一瓶,2个空瓶可以换一瓶,请问10元钱可以喝多少瓶酒,剩余多少空瓶和盖子
public class RecursionDemo06 {
// 定义一个静态的成员变量用于存储可以买的酒数量
public static int totalNumber; // 总数量
public static int lastBottleNumber; // 记录每次剩余的瓶子个数
public static int lastCoverNumber; // 记录每次剩余的盖子个数
public static void main(String[] args) {
// 1、拿钱买酒
buy(10);
System.out.println("总数:" + totalNumber);
System.out.println("剩余盖子数:" + lastCoverNumber);
System.out.println("剩余瓶子数:" + lastBottleNumber);
}
public static void buy(int money){
// 2、看可以立马买多少瓶
int buyNumber = money / 2; // 5
totalNumber += buyNumber;
// 3、把盖子 和瓶子换算成钱
// 统计本轮总的盖子数 和 瓶子数
int coverNumber = lastCoverNumber + buyNumber;
int bottleNumber = lastBottleNumber + buyNumber;
// 统计可以换算的钱
int allMoney = 0;
if(coverNumber >= 4){
allMoney += (coverNumber / 4) * 2;
}
lastCoverNumber = coverNumber % 4;
if(bottleNumber >= 2){
allMoney += (bottleNumber / 2) * 2;
}
lastBottleNumber = bottleNumber % 2;
if(allMoney >= 2){
buy(allMoney);
}
Integer[] arr2 = new Integer[]{11, 22, 33};
Arrays.sort(arr2);
}
}
本来我想的是传入11和传入10结果一样以为有bug,后来一想,如果传入11,买酒话10元,剩下的1元是花不出去的,实际上就和传入10元是一样的
4.字符集
4.1常见字符集介绍
4.1.1字符集基础知识
- 计算机底层是不可以直接存储字符的.计算机中底层只能存储二进制(0,1)
- 二进制是可以转换成十进制的
由上两点可得结论:计算机底层可以将二进制转换为十进制进而表示十进制编号,所以计算机可以给人类字符进行编号存储,这套编号规则就是字符集
4.1.2常见字符集
1.ASCII字符集:
- ASCII(美国信息交换标准代码):包括了数字,英文,符号
- ASCII使用1个字节存储一个字符,一个字节是8位,总共可以表示128(去除负数位,只要整数位,就是2的7次方等于128)个字符信息,对于英文,数字,常见符号来说是够用的
- 美国人总共就10个数字和52个英文,再外加一些常见符号,也就百十来个,所以一个字节是绰绰有余的
- ASCII表中的128-255是扩展码,由IBM制定,不是标准的ASCII码
2.GBK
- Windows中文系统默认的码表,兼容ASCII码表(ASCII表中的字符在UTF-8中都要占1个字节且编号一致,从这里我们也知道了GBK不是固定字长编码:中文2个字节,ASCII表中的1个字节),也包含了几万个汉字,并支持繁体汉字以及部分日韩文字
- **GBK是中国的码表,一个中文以两个字节的形式存储,**但不包含世界上所有国家的文字
3.Unicode码表:
- Unicode(又称统一码,万国码,单一码):是计算机科学领域里的一项业界字符编码标准
- 容纳上世界上大多数国家的所有常见文字和符号
- Unicode会先通过UTF-8,UTF-16,以及UTF-32的方式编码成二进制再存储到计算机,其中最常见的是UTF-8
- 先从Unicode中找到每一个字符对应的编号
- 通过UTF-8的方式将编号编码成二进制
- 将二级制存储到计算机中
- UTF-8不是固定字长编码的,而是一种变长的编码方式,它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度
字符集注意点:
- Unicode是万国码,以UTF-8编码后一个中文一般以三个字节的形式存储
- UTF-8也要兼容ASCII编码表(ASCII表中的字符在UTF-8中都要占1个字节且编号一致)
- 技术人员都应该使用UTF-8的字符集编码
- 英文和数字等在任何国家的字符集中都占1个字节
- 编码前和编码后的字符集需要一致,否则会出现中文乱码(英文和数字在任何国家的编码中都不会乱码)
4.2字符集的编码,解码操作
String编码
方法名称 | 说明 |
byte[] getBytes() | 使用平台默认字符集将该String编码为一系列字节,将结果存储到新的字节数组中 |
byte[] getBytes(String charsetName) | 使用指定字符集将该String编码为一系列字节,将结果存储到新的字节数组中 |
String解码
构造器 | 说明 |
String(byte[] bytes) | 通过使用平台默认字符集解码指定的字符数组来构造新的String |
String(byte[] bytes, String charsetName) | 通过指定默认字符集解码指定的字符数组来构造新的String |
public class Test {
public static void main(String[] args) throws Exception {
String name = "abc我爱你中国";//18
byte[] bytes = name.getBytes();//idea右下角是UTF-8,所以这里默认使用UTF-8编码
System.out.println(bytes.length);
System.out.println(Arrays.toString(bytes));
String rs = new String(bytes);
System.out.println(rs);
}
}
5.IO流
5.1IO流概述
IO流也称为输入,输出流,就是用来读写数据的
IO流的分类:
- 按流的方向分:
- I表示input,是数据从硬盘文件读入到内存的过程,称之为输入,负责读
- O表示output,是内存程序的数据从内存写出到硬盘文件的过程,称之输出,负责写
- 按流中的数据最小单位分
- 字节流
- 操作所有类型的文件(因为任何类型的文件中的数据都是由字节组成的)
- 字符流
- 只能操作纯文本文件如java文件,txt文件等
总结流的四大类:
- 字节输入流:以内存为基准,来自磁盘文件/网络中的数据以字节的形式读入到内存中去的流称为字节输入流
- 字节输出流:已内存为基准,把内存中的数据以字节写出到磁盘文件或者网络中去的流称为字节输出流
- 字符输入流:以内存为基准,来自磁盘文件/网络中的数据以字符的形式读入到内存中去的流称为字符输入流
- 字符输出流:已内存为基准,把内存中的数据以字符写出到磁盘文件或者网络介质中去的流称为字符输出流
IO流体系:
- 字节流
- InputStream
- 对应实现类:FileInputStream
- OutputStream
- 对应实现类:FileOutputStream
- 字符流
- Reader
- 对应实现类:FileReader
- Writer
- 对应实现类:FileWriter
5.2字节流的使用
5.2.1字节输入流
构造器 | 说明 |
public FileInputStream(File file) | 创建直接输入流管道与源文件对象接通 |
public FileInputStream(String pathname) | 创建字节输入流管道与源文件路径接通 |
方法名称 | 说明 |
public int read() | 每次读取一个字节返回,如果字节已经没有可读的返回-1 |
public int read(byte[] buffer) | 每次读取一个字节数组返回,如果字节已经没有可读的返回-1 |
1.看一下public FileInputStream(String pathname)方法源码:
public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);
}
可以看出这其实就是方法内部new了一个File对象后传给public FileInputStream(File file)方法,所以我们以后就用public FileInputStream(String pathname)方法,不需要再自己new File了,方便!
2.为什么public int read()方法返回值是int呢
- 每次都是一个字节一个字节读,而int可以装4个字节的数据,所以int类型变量中装的下每次读到的这一个字节
- 那为什么不用byte和char类型接收呢,不是也能装的下吗
- char的取值范围是从0到65535,这个范围内的所有字符, 都有可能在数据(包含但不仅限于照片音视频)中出现,那么就没有办法设置读取到末尾的标志符了
- byte的取值范围是从-128到127,这个范围内所有的数据, 同样都有可能在数据中出现(包含但不仅限于照片音视频会有-1(11111111)情况的出现,反正ASCII的标准码(0-127)不会出现-1的情况,ASCII标准码全是非负数)
- 解决办法是read()方法内部将读取到的所有字节高位补24个0转为int返回, 这样做所有的数据都会是非负数,这时就可以用-1表示流末尾了,然后将字节写入文件的时候write方法内部会去掉最高位的24位,这样就保证了数据的不变
3.关于public int read(byte[] buffer)方法:
先看一下源码:
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int c = read();
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
- 为什么public int read()返回值是int类型,而public int read(byte[] buffer)传参不是int类型的数组呢
- 可以发现该方法内部又调用了read(byte b[], int off, int len)方法并传参,然后在这个方法的第27行将读取到的数据进行强制类型转换为byte类型然后再赋值到数组中,所以public int read(byte[] buffer)传参应是byte类型的数组
- 调用read()方法没读取到字节分(返回-1)两种情况:
- 第一种:14-17行:准备向数组的索引0添加或修改元素,此时调用read()方法返回-1后,说明此时已经没有字节可读了,所以if分支语句中返回-1,层层向上返回-1直到返回-1到main方法中,此时我们就知道了已经没有字节可读了
- 第二种21-31行:准备向数组的索引off + i(该索引一定≠0)修改元素,此时调用read()方法返回-1后,说明此时已经没有字节可读了,跳出for循环,执行第31行的return i(i>=1),层层返回i直到返回i到main方法中
- 注意,这种情况因为我们定义的byte类型数组是在main方法中,只要main方法不结束,数组中存储的数据都一直存在,所以如果最后一次读取到的字节个数m是小于n的(n是数组长度),则数组后n-m个字节不会被修改,会与倒数第二次的字节相同
- 综上所述,给这个方法传入一个byte类型数组,每次读取到的n个字节(n是数组长度)都放到这个数组中,然后返回这个数组中字节的个数,当然,如果已经没有字节可读的话返回-1,并且,如果最后一次读取到的字节个数m是小于n的,则数组后n-m个字节与倒数第二次的字节相同
1.每次读取一个字节:
public class FileInputStreamDemo01 {
public static void main(String[] args) throws Exception {
InputStream is = new FileInputStream("Demo19-file-io\\src\\data.txt");
int b;
while (( b = is.read() ) != -1){
System.out.print((char) b);
}
}
}
2.每次读取一个字节数组:
public class FileInputStreamDemo02 {
public static void main(String[] args) throws Exception {
InputStream is = new FileInputStream("Demo19-file-io\\src\\data.txt");
byte[] buffer = new byte[3];
int len;
while ((len = is.read(buffer)) != -1) {
//String(byte[] bytes, int offset, int length)
System.out.print(new String(buffer, 0 , len));//以默认字符集进行解码
}
}
}
如何使用字节输入流读取中文内容输出不乱码呢:
- 定义一个与文件一样大的字节数组,一次性读取完文件的全部字节
- 有一定问题:如果文件过大,字节数组可能引起内存溢出
3.一次将文件中字节读取完:
方式一:自己定义一个字节数组与文件的大小一样大,然后使用读取字节数组的方法,一次性读取完成
方式二:官方为字节输入流InputStream提供了如下API可以直接把文件的全部数据读取到一个字节数组中
方法名称 | 说明 |
pubic byte[] readAllByes() throws IOException | 直接将当前字节输入流对应的文件对象的字节数据装到一个字节数组返回 |
public class FileInputStreamDemo03 {
public static void main(String[] args) throws Exception {
File f = new File("Demo19-file-io\\src\\data.txt");
InputStream is = new FileInputStream(f);
byte[] buffer = new byte[(int) f.length()];//创建数组时长度必须是int型,所以f.length()需要强转
int len = is.read(buffer);
System.out.println("读取了多少个字节:" + len);
System.out.println("文件大小:" + f.length());
System.out.println(new String(buffer));
// 读取全部字节数组
byte[] buffer2 = is.readAllBytes();
System.out.println(new String(buffer2));
}
}
5.2.2字节输出流
构造器 | 说明 |
public FileOutputStream(File file) | 创建一个字节输出流管道通向目标文件对象 |
public FileOutputStream(String file) | 创建一个字节输出流管道通向目标文件路径 |
public FileOutputStream(File file , boolean append) | 创建一个追加数据的字节输出流管道通向目标文件对象 |
public FileOutputStream(String file , boolean append) | 创建一个追加数据的字节输出流管道通向目标文件路径 |
方法名称 | 说明 |
public void write(int a) | 写一个字节出去 |
public void write(byte[] buffer) | 写一个字节数组出去 |
public void write(byte[] buffer, int pos, int len) | 写一个字节数组的一部分出去 |
为什么public void write(int a)方法参数是int类型而public void write(byte[] buffer)参数不是int类型的数组:
等我回过头来发现因为多态我看的源码是OutputStream的源码啊,而FileOutputStream的源码调用的是其他语言的,没有方法体,下面这些可以不看了,但留着是以后的思路,我这里就不删掉了
- 大致看一下public void write(byte[] buffer)源码
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
public void write(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
for (int i = 0 ; i < len ; i++) {
write(b[off + i]);
}
}
可以看出来实际底层还是通过第15行write(b[off + i]);向文件中写入数据,而write(int a)方法的形参是int类型,所以会将b[off + i]由byte类型自动强制转换为int类型并传给write(int a)方法
- 可能有人会说了,那如果把public void write(byte[] buffer)方法的参数设为int类型的数组岂不是一样可以,并且在调用write(int a)方法时也不需要自动类型转换了?
- 如果我想将一个字符串写入文件,我可能不知道其中每个字符所对应的字符集编码,这时如果public void write(byte[] buffer)的参数是int类型数组,我总不能创建数组的时候依次查每个字符对应的字符集编码吧?
- 但是,因为public void write(byte[] buffer)方法的参数是byte类型的数组,所以我可以直接调用String类的静态方法getBytes()获取该字符串的byte类型的数组,岂不是美滋滋!
- 还有一个比较难懂的点:为什么public void write(int a)方法参数类型不是byte?目前不太动能
流的关闭与刷新:
方法 | 说明 |
flush() | 刷新流,还可以继续写数据 |
close() | 关闭流,释放资源,在关闭前会先刷新.一旦关闭,就不能再写数据 |
java中为何要关闭流:
若是你再堆区new一个对象的时候,若是等号左边是对端口、显存、文件进行操作的时候,超出了虚拟机可以释放资源的界限,虚拟机就没法利用垃圾回收机制对堆区占用的资源进行释放
public class OutputStreamDemo04 {
public static void main(String[] args) throws Exception {
// 1、创建一个文件字节输出流管道与目标文件接通
// OutputStream os = new FileOutputStream("Demo19-file-io\\src\\data04.txt" , true);// 追加数据管道
FileOutputStream os = new FileOutputStream("Demo19-file-io\\src\\data04.txt"); //先清空之前的数据,写新数据进入
//这行代码与案例无关,只是想告诉:创建输出流管道时如果文件不存在,会自己创建)
System.out.println(new File("Demo19-file-io\\src\\data04.txt").exists());//true
// 2、写数据出去
// a.public void write(int a):写一个字节出去
os.write('a');
os.write(98);
os.write("\r\n".getBytes()); // 换行(getBytes()方法以默认字符集进行编码)
// b.public void write(byte[] buffer):写一个字节数组出去。
byte[] buffer = {'a' , 97, 98, 99};
os.write(buffer);
os.write("\r\n".getBytes());
byte[] buffer2 = "我是中国人".getBytes();
os.write(buffer2);
os.write("\r\n".getBytes()); // 换行
// c. public void write(byte[] buffer , int pos , int len):写一个字节数组的一部分出去。
byte[] buffer3 = {'a',97, 98, 99};
os.write(buffer3, 0 , 3);
os.write("\r\n".getBytes()); // 换行
// os.flush();
os.close();
}
}
5.2.3文件拷贝
字节流可以进行任何文件的拷贝,因为文件拷贝就是将文件一的字节读取出来再写入文件二,期间不涉及到我们操作编码和解码(没有new String()和"".getBytes()),只要保证前后文件格式,编码一致就没问题了,即使是拷贝全是中文的txt文件仍不会乱码
public class CopyDemo05 {
public static void main(String[] args) {
try {
// 1、创建一个字节输入流管道与原视频接通
InputStream is = new FileInputStream("Demo19-file-io\\src\\data.txt");
// 2、创建一个字节输出流管道与目标文件接通
OutputStream os = new FileOutputStream("Demo19-file-io\\src\\data05.txt");
// 3、定义一个字节数组转移数据
byte[] buffer = new byte[5];
int len; // 记录每次读取的字节数。
while ((len = is.read(buffer)) != -1){
os.write(buffer, 0 , len);
}
System.out.println("复制完成了!");
// 4、关闭流(后开的先关)
os.close();
is.close();
} catch (Exception e){
e.printStackTrace();
}
}
}
5.2.4资源释放的方式
以前我们是在程序基本业务逻辑执行完才进行资源释放的,那如果业务逻辑出了问题,就不会进行资源释放了呀,所以需要进行改进
5.2.4.1try-catch-finally
- finally:在异常处理时提供finally块来执行所有清除操作比如说IO流的释放资源
- 特点:被finally控制的语句最终一定会执行,除非JVM退出
public class TryCatchFinallyDemo1 {
public static void main(String[] args) {
InputStream is = null;//需要扩大变量的作用域
OutputStream os = null;//同上
try {
//System.out.println( 10 / 0);
// 1、创建一个字节输入流管道与原视频接通
is = new FileInputStream("Demo19-file-io\\src\\data.txt");
// 2、创建一个字节输出流管道与目标文件接通
os = new FileOutputStream("Demo19-file-io\\src\\data05.txt");
// 3、定义一个字节数组转移数据
byte[] buffer = new byte[1024];
int len; // 记录每次读取的字节数。
while ((len = is.read(buffer)) != -1){
os.write(buffer, 0 , len);
}
System.out.println("复制完成了!");
} catch (Exception e){
e.printStackTrace();
} finally {
System.out.println("========finally=========");
try {
// 4、关闭流。
if(os!=null)os.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if(is != null) is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println(test(10, 2));//没有输出5而是输出100
}
public static int test(int a , int b){
try {
int c = a / b;
return c;
}catch (Exception e){
e.printStackTrace();
return -111111; // 计算出现bug.
}finally {
System.out.println("--finally--");
return 100;
}
}
}
注意点:
- 需要扩大is和os的作用域至main方法内部
- 在关闭流时会有一种情况:在try语句中的创建管道流之前如果有一些其他业务逻辑出问题了(比如第6行注释掉的输出语句)就不会再创建管道流了,那么再关闭就会空指针异常,所以必须判断不为空才关闭
- 只要finally内有return语句,那么try和catch中即使有return语句也形同虚设,一定会返回finally中要返回的数据,所以开发中不建议在finally中加return
5.2.4.2try-with-resource
JDK7和JDK9都简化了资源释放操作
JDK7改进方案:
try(定义流对象){
可能出现异常的代码
}catch(异常类名 变量名){
异常的处理代码
}资源用完最终自动释放
JDK9改进方案(很鸡肋,唯一作用就是如果流对象是别人传过来的可以用这种方法,但用JDK7的也可以:InputStream is = xingcan_liu):
定义输入流对象;
定义输出流对象;
try(输入流对象 ; 输出流对象){
可能出现异常的代码
}catch(异常类名 变量名){
异常的处理代码
}资源用完最终自动释放
注意:JDK7及JDK9的()中只能放置资源对象,否则报错,什么是资源对象呢:
- 资源都是实现了Closeable/AutoCloseable接口的对象
public abstract class InputStream implements Closeable{}
public abstract class OutputStream implements Closeable, Flushable{}
public class TryCatchResouceDemo2 {
public static void main(String[] args) {
try (
//这里面只能放置资源对象,用完自动调用资源对象的close方法关闭资源(即使出现异常也会做关闭操作)
// 1、创建一个字节输入流管道与原视频接通
InputStream is = new FileInputStream("Demo19-file-io\\src\\data.txt");
// 2、创建一个字节输出流管道与目标文件接通
OutputStream os = new FileOutputStream("Demo19-file-io\\src\\data05.txt");
// int age = 23; // 报错,这里只能放资源
MyConnection connection = new MyConnection(); //最终会自动调用资源的close方法
) {
// 3、定义一个字节数组转移数据
byte[] buffer = new byte[1024];
int len; // 记录每次读取的字节数。
while ((len = is.read(buffer)) != -1){
os.write(buffer, 0 , len);
}
System.out.println("复制完成了!");
} catch (Exception e){
e.printStackTrace();
}
}
}
class MyConnection implements AutoCloseable{
@Override
public void close() throws IOException {
System.out.println("连接资源被成功释放了!");
}
}
5.3字符流的使用
5.3.1字符输入流
- 字节流读取中文输出可能存在什么问题:
- 会乱码,或者内存溢出
- 读取中文,哪个流更合适,为什么
- 字符流更合适,因为字符流是按照单个字符读取的(但注意代码要和文件编码一致,否则乱码)
构造器 | 说明 |
public FileReader(File file) | 创建字符输入流管道与源文件对象接通 |
public FileReader(String pathname) | 创建字符输入流管道与源文件路径接通 |
方法名称 | 说明 |
public int read()返回的是字符编号 | 每次读取一个字符返回,如果字符已经没有可读的返回-1 |
public int read(char[] buffer) | 每次读取一个字符数组,返回读取的字符个数,如果字符已经没有可读的返回-1 |
1.一次读取一个字符
public class FileReaderDemo01 {
public static void main(String[] args) throws Exception {
FileReader fr = new FileReader("Demo19-file-io\\src\\data.txt");
int code;
while ((code = fr.read()) != -1){
System.out.print((char) code);//不需要输出时换行,因为读取文件字符时如果文本中有换行符,那么也会读取出来换行符(以前学的字节流同理)
}
}
}
public class FileReaderDemo02 {
public static void main(String[] args) throws Exception {
Reader fr = new FileReader("Demo19-file-io\\src\\data.txt");
char[] buffer = new char[1024]; // 1K字符
int len;
while ((len = fr.read(buffer)) != -1) {
String rs = new String(buffer, 0, len);
System.out.print(rs);
}
}
}
5.3.2字符输出流
构造器 | 说明 |
public FileWriter(File file) | 创建一个字符输出流管道通向目标文件对象 |
public FileWriter(String filePath) | 创建一个字符输出流管道通向目标文件路径 |
public FileWriter(File file,boolean append) | 创建一个追加数据的字符输出流管道通向目标文件对象 |
public FileWriter(String filePath,boolean append) | 创建一个追加数据的字符输出流管道通向目标文件路径 |
方法名称 | 说明 |
void write(int c) | 写一个字符 |
void write(char[] cbuf) | 写入一个字符数组 |
void write(char[] cbuf, int off, int len) | 写入字符数组的一部分 |
void write(String str) | 写一个字符串 |
void write(String str, int off, int len) | 写一个字符串的一部分 |
方法 | 说明 |
flush() | 刷新流 |
close() | 关闭流 |
public class FileWriterDemo03 {
public static void main(String[] args) throws Exception {
// 1、创建一个字符输出流管道与目标文件接通
// Writer fw = new FileWriter("file-io-app/src/out08.txt"); // 覆盖管道,每次启动都会清空文件之前的数据
Writer fw = new FileWriter("Demo19-file-io\\src\\data06.txt"); // 覆盖管道,每次启动都会清空文件之前的数据
// a.public void write(int c):写一个字符出去
fw.write(98);
fw.write('a');
fw.write('徐');
fw.write("\r\n"); // 换行
// b.public void write(String c)写一个字符串出去
fw.write("abc我是中国人");
fw.write("\r\n");
// c.public void write(char[] buffer):写一个字符数组出去
char[] chars = "abc我是中国人".toCharArray();
fw.write(chars);
fw.write("\r\n");
// d.public void write(String c ,int pos ,int len):写字符串的一部分出去
fw.write("abc我是中国人", 0, 5);
fw.write("\r\n");
// e.public void write(char[] buffer ,int pos ,int len):写字符数组的一部分出去
fw.write(chars, 3, 5);
fw.write("\r\n");
// fw.flush();// 刷新后流可以继续使用
fw.close(); // 关闭包含刷新,关闭后流不能使用
}
}
6.缓冲流
6.1缓冲流概述
缓冲流也称为高效流或高级流,之前学习的字节流,字符流可以称为原始流
作用:缓冲流自带缓冲区,可以提高原始字节流,字符流读写数据的性能
缓冲流的基本原理:创建流对象时候,会创建一个内置的默认大小的缓冲区,通过缓冲区读写,减少系统IO次数,从而提高读写的效率
- InputStream
- 子孙类:BufferedInputStream(字节缓冲输入流)
- OutputStream
- 子孙类:BufferedOutputStream(字节缓冲输出流)
- Reader
- 子孙类:BufferedReader(字符缓冲输入流)
- Writer
- 子孙类:BufferedWriter(字符缓冲输出流)
6.2字节缓冲流
- 字节缓冲输入流:BufferedInputStream,提高字节输入流读取数据的性能,读写功能上并无变化(方法来自于其祖宗类InputStream)
- 字节缓冲输出流:BufferedOutputStream,提高字节输出流读取数据的性能,读写功能上并无变化(方法来自于其祖宗类OutputStream)
字节缓冲流性能优化原理:
- 字节缓冲输入流自带了8KB缓冲池,以后我们直接从缓冲池读取数据,所以性能较好
- 字节缓冲输出流自带了8KB缓冲池,数据直接写入到缓冲池中取,写数据性能极高
- 如果使用管道,那我们每次读写都要与磁盘进行数据传输,性能低,而缓冲池是在内存中的,缓冲池会自动从管道读8KB数据放到缓冲池,同理也会自动写8KB数据写到磁盘,也就是说有了缓冲池以后我们进行数据读写就不再需要通过输入管道或输出管道,性能极大提高
构造器 | 说明 |
public BufferedInputStream(InputStream is) | 可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道,从而提高字节输入流读数据的性能 |
public BufferedOutputStream(OutputStream os) | 可以把低级的字节输出流包装成一个高级的缓冲字节输出流,从而提高写数据的性能 |
public class ByteBufferDemo {
public static void main(String[] args) {
try (
// 这里面只能放置资源对象,用完会自动关闭:自动调用资源对象的close方法关闭资源(即使出现异常也会做关闭操作)
// 1、创建一个字节输入流管道与原视频接通
InputStream is = new FileInputStream("Demo20-io2\\src\\data.txt");
// a.把原始的字节输入流包装成高级的缓冲字节输入流
InputStream bis = new BufferedInputStream(is);
// 2、创建一个字节输出流管道与目标文件接通
OutputStream os = new FileOutputStream("Demo20-io2\\src\\data01.txt");
// b.把字节输出流管道包装成高级的缓冲字节输出流管道
OutputStream bos = new BufferedOutputStream(os);
) {
// 3、定义一个字节数组转移数据
byte[] buffer = new byte[1024];
int len; // 记录每次读取的字节数。
while ((len = bis.read(buffer)) != -1){
bos.write(buffer, 0 , len);
}
System.out.println("复制完成了!");
} catch (Exception e){
e.printStackTrace();
}
}
}
6.3字符缓冲流
字符缓冲输入流:BufferedReader,提高字符输入流读取数据的性能,除拥有其祖宗类Reader的方法外还多了按照行读取数据的功能
字符缓冲输出流:BufferedWriter,提高字符写入数据的性能,除拥有其祖宗类Writer的方法外还多了换行功能
优化原理与字节缓冲流类似,不一样的点就是字节缓冲流知道8KB(8K个字节)缓冲区,而字符缓冲流自带8K(8K个字符)缓冲区
构造器 | 说明 |
pubic BufferedReader(Reader r) | 可以把低级的字符输入流包装成一个高级的缓冲字符输入流管道,从而提高字符输入流读数据的性能 |
public BufferedWriter(Writer w) | 可以把低级的字符输出流包装成一个高级的缓冲字符输出流管道,从而提高字符输出流读数据的性能 |
字符缓冲输入流新增功能:
方法 | 说明 |
public String readLine() | 读取一行数据返回,无行可读返回null |
字符缓冲输出流新增功能:
方法 | 说明 |
public void newLine() | 换行操作 |
newLine()源码:实际上就是内部写了一个换行符
public void newLine() throws IOException {
write(lineSeparator);
}
案例需求:把《出师表》的文章顺序进行恢复到一个新的文件中
分析:
1.定义一个缓冲字符输入流管道与源文件接通
2.定义一个List集合存储读取的每行数据
3.定义一个循环按照行读取数据,存到List集合中
4.对List集合中的每行数据按照首字母编号升序排序
5.定义一个缓冲字符输出管道与目标文件接通
6.遍历List集合中的每个元素,用缓冲输出管道写出并换行
public class BufferedCharTest3 {
public static void main(String[] args) {
try(
// 1、创建缓冲字符输入流管道与源文件接通
BufferedReader br = new BufferedReader(new FileReader("Demo20-io2\\src\\csb.txt"));
// 5、定义缓冲字符输出管道与目标文件接通
BufferedWriter bw = new BufferedWriter(new FileWriter("Demo20-io2/src/new.txt"));
) {
// 2、定义一个List集合存储每行内容
List<String> data = new ArrayList<>();
// 3、定义循环,按照行读取文章
String line;
while ((line = br.readLine()) != null){
data.add(line);
}
// System.out.println(data);
// 4、排序
// 自定义排序规则
List<String> sizes = new ArrayList<>();
Collections.addAll(sizes, "一","二","三","四","五","陆","柒","八","九","十","十一");
Collections.sort(data, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
// o1 八.,....
// o2 柒.,....
return sizes.indexOf(o1.substring(0, o1.indexOf(".")))
- sizes.indexOf(o2.substring(0, o2.indexOf(".")));
}
});
// System.out.println(data);
// 6、遍历集合中的每行文章写出去,且要换行
for (String datum : data) {
bw.write(datum);
bw.newLine(); // 换行
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
7.转换流
7.1问题引出
- 之前我们使用字符流读取中文是否有乱码
- 没有,因为代码编码和文件编码都是UTF-8
- 如果代码编码和文件编码不一致,使用字符流直接读取还能不乱码吗?
- 会乱码
- 如何解决呢:
- 使用字符输入转换流
- 可以提取文件的原始字节流,原始字节流不会存在问题
- 然后把字节流以指定编码转化成字符输入流,这样字符输入流中的字符就不乱码了
- Reader
- 子类:InputStreamReader(字符输入转化流)
- Writer
- 子类:OutputStreamWriter(字符输出转换流)
7.2字符输入转换流
字符输入转换流:InputStreamReader,可以把原始的字节流按照指定编码转换成字符输入流
作用:以指定编码将原始字节流转换成字符输入流,如此字符流中的字符不会乱码
构造器 | 说明 |
public InputStreamReader(InputStream is) | 可以把原始的字节流按照代码默认编码转换成字符输入流.几乎不用,因为这个与默认的FileReader一样 |
public InputStreamReader(InputStream is, String charset) | 可以把原始的字节流按照指定编码转换成字符输入流.(重点) |
为什么说public InputStreamReader(InputStream is)与默认的FileReader方法结果一样呢,看FileReader源码:
public FileReader(String fileName) throws FileNotFoundException {
super(new FileInputStream(fileName));
}
public InputStreamReader(InputStream in) {......}
可以知道FileReader方法最后实际上就是套娃式调用了public InputStreamReader(InputStream is)方法
执行的流程:
1.从文件中读取字节输入流
2.将字节输入流转换为指定编码格式的字符输入流并写入缓冲区
3.从缓冲流读取字符
public class InputStreamReaderDemo01 {
public static void main(String[] args) throws Exception {
//1.从文件中读取原始字节流
InputStream is = new FileInputStream("D:\\study\\java\\Markdown\\javaSE\\images\\luanma.txt");
//2.把原始字节流转换成字符输入流
Reader isr = new InputStreamReader(is , "GBK"); //以指定的GBK编码转将原始字节流换成字符输入流
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null){
System.out.println(line);
}
}
}
7.3字符输出转换流
字符输出转换流:OutputStreamReader,可以把字节输出流按照指定编码转换成字符输出流
作用:以指定编码把字节输出流转换成字符输出流,从而可以指定写出去的字符编码
构造器 | 说明 |
public OutputStreamReader(OutputStream os) | 可以把原始的字节输出流按照代码默认编码转换成字符输出流.几乎不用 |
public OutputStreamReader(OutputStream os, String charset) | 可以把原始的字节输出流按照指定编码转换成字符输出流.(重点) |
public class OutputStreamWriterDemo02 {
public static void main(String[] args) throws Exception {
// 1、定义一个字节输出流
OutputStream os = new FileOutputStream("Demo20-io2\\src\\data02.txt");
// 2、把原始的字节输出流转换成字符输出流
// Writer osw = new OutputStreamWriter(os); // 以默认的UTF-8写字符出去 跟直接写FileWriter一样
Writer osw = new OutputStreamWriter(os, "GBK"); // 指定GBK的方式写字符出去
// 3、把低级的字符输出流包装成高级的缓冲字符输出流。
BufferedWriter bw = new BufferedWriter(osw);
bw.write("我爱中国1~~");
bw.write("我爱中国2~~");
bw.write("我爱中国3~~");
bw.close();//因为os和osw管道是交给bw管理的,只要关了bw,伴随着也会关os和osw
}
}
8.对象序列化与反序列化
1.对象序列化:
- 作用:以内存为基准,把内存中的对象存储到磁盘文件中去,称为对象序列化
- 使用到的流是对象字节输出流:ObjectOutputStream(其父类OutputStream)
构造器 | 说明 |
public ObjectOutputStream(OutputStream out) | 把低级字节输出流包装成高级的对象字节输出流 |
ObjectOutputStream序列化方法:
方法名称 | 说明 |
public final void writeObject(Object object) | 把对象写出去到对象序列化流的文件中去 |
2.对象反序列化:
- 作用:以内存为基准,把存储到磁盘文件中去的对象数据恢复成内存中的对象,称为对象反序列化
- 使用到的流是对象字节输入流:ObjectInputStream(其父类InputStream)
- 注意:反序列化的对象必须实现Serializable序列化接口
构造器 | 说明 |
public ObjectInputStream(InputStream out) | 把低级字节输入流包装成高级的对象字节输入流 |
ObjectInputStream反序列化方法:
方法名称 | 说明 |
public Object readObject() | 把存储到磁盘文件中去的对象数据恢复成内存中的对象返回 |
public class ObjectOutputStreamDemo1 {
public static void main(String[] args) throws Exception {
// 1、创建学生对象
Student s = new Student("陈磊","1314520");
// 2、对象序列化:使用对象字节输出流包装字节输出流管道
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Demo20-io2/src/obj.txt"));
// 3、直接调用序列化方法
oos.writeObject(s);
// 4、释放资源
oos.close();
System.out.println("序列化完成了~~");
// 1、创建对象字节输入流管道包装低级的字节输入流管道
ObjectInputStream is = new ObjectInputStream(new FileInputStream("Demo20-io2/src/obj.txt"));
// 2、调用对象字节输入流的反序列化方法
Student s2 = (Student) is.readObject();
System.out.println(s2);
}
}
//对象如果要序列化,必须实现Serializable序列化接口
public class Student implements Serializable {
// 申明序列化的版本号码
// 序列化的版本号与反序列化的版本号必须一致才不会出错!
private static final long serialVersionUID = 1;
private String name;
// transient修饰的成员变量不参与序列化了
private transient String passWord;
public Student(String name, String passWord) {
this.name = name;
this.passWord = passWord;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", passWord='" + passWord + '\'' +
'}';
}
}
注意:
- 序列化后看文件会发现看不懂,这并不是乱码,序列化只是存储数据以便下次使用,又不是给你看的
- 注意:对象如果要序列化,必须实现Serializable序列化接口
- 用transient修饰的成员变量不参与序列化,如第34行的password就不会参与序列化
- 序列化的版本号与反序列化的版本号必须一致才不会出错,版本号有什么意义呢?
- 某天我给实体类加了一些成员变量,然后我把版本号改了,这时如果你直接反序列化从内存中拿数据就会报错,这就强制你必须先序列化更新内存中的数据
9.打印流
- 打印流可以实现打印什么数据就是什么数据,例如打印97写出去就是97,打印boolean的true,写出去就是true
- 打印流一般是指:PrintStream,PrintWriter两个类
- 打印流是目前为止最方便,最高效的打印数据到文件中的流,为什么说最方便,最高效呢,看public PrintStream(String filepath)的源码(PrintWriter也是同理):
public PrintStream(String fileName) throws FileNotFoundException {
this(false, new FileOutputStream(fileName));
}
private PrintStream(boolean autoFlush, OutputStream out) {
super(out);
this.autoFlush = autoFlush;
this.charOut = new OutputStreamWriter(this);
this.textOut = new BufferedWriter(charOut);
}
先说高效:
- 可以大概看出来PrintStream内部是有一个缓冲流的,说明人家底层自己就已经搞定了缓冲区,所以当然高效
再说方便:
- 其一:人家内部已经自己搞定了缓冲区,这样我们不用手动创建缓冲流
- 其二:从第2行可知人家底层给我们做了优化,会自己去创建一个低级流FileOutputStream,这样我们就不用手动创建低级流(但是,如果是要向文件中追加打印数据,只能让打印流通向低级管道而不是文件对象/文件路径,因为打印流没有既可以通向文件对象/路径又可以追加打印的构造器)
- 由以上两点可知打印流确实方便
总结一下:我们以前学的其他高级流(缓冲流,转换流,对象字节输入/出流)都只能通向低级流,且没有自己搞缓冲区,而打印流这种高级流则可以直接通向文件对象或文件路径,且自己搞了缓冲区,方便,高效!
1.PrintStream(OutputStream的孙子类)
构造器 | 说明 |
public PrintStream(OutputStream os) | 打印流直接通向字节输出流管道 |
public PrintStream(File f) | 打印流直接通向文件对象 |
public PrintStream(String filepath) | 打印流直接通向文件伦路径 |
方法 | 说明 |
public void print(Xxx xx) | 打印任意类型的数据出去 |
public class PrintDemo1 {
public static void main(String[] args) throws Exception {
// 创建一个打印流对象
// PrintStream ps = new PrintStream(new FileOutputStream("Demo20-io2/src/ps.txt"));
// PrintStream ps = new PrintStream(new FileOutputStream("Demo20-io2/src/ps.txt" , true)); // 追加数据,在低级管道后面加True
PrintStream ps1 = new PrintStream("Demo20-io2/src/ps.txt");
ps1.println(97);
ps1.println('a');
ps1.println(23.3);
ps1.println(true);
ps1.println("我是打印流ps1输出的,我是啥就打印啥");
//相比于PrintWriter的独特点:
ps1.write(97);//写一个字节
ps1.write("我爱中国".getBytes());//写一个字节数组
ps1.close();
}
}
2.PrintWriter(Writer的子类)
构造器 | 说明 |
public PrintWriter(OutputStream os) | 打印流直接通向字节输出流管道 |
public PrintWriter(Writer w) | 打印流直接通向字符输出流管道 |
public PrintWriter(File f) | 打印流直接通向文件对象 |
public PrintWriter(String filepath) | 打印流直接通向文件伦路径 |
方法 | 说明 |
public void print(Xxx xx) | 打印任意类型的数据出去 |
PrintStream和PrintWriter的区别:
- 打印数据功能上是一摸一样的,都是使用方便,性能高效(核心优势)
- PrintStream是OutputStream孙子类,支持写字节数据出去
- PrintWriter继承自Writer,支持写字符数据出去
- 我们用打印流主要就用来打印数据了,打印字节或字符多数用的还是字节流或字符流
public class PrintDemo11 {
public static void main(String[] args) throws Exception {
PrintWriter ps2 = new PrintWriter("Demo20-io2/src/ps2.txt"); //打印功能上与PrintStream的使用没有区别
ps2.println(97);
ps2.println('a');
ps2.println(23.3);
ps2.println(true);
ps2.println("我是打印流ps2输出的,我是啥就打印啥");
//相比于PrintStream的独特点:
ps2.write(97);//写一个字符
ps2.write('中');//写一个字符
ps2.write("我爱你中国");//写一个字符串
ps2.write("我是中国人".toCharArray());//写一个字符数组
ps2.close();
}
}
输出语句重定向属于打印流的一种应用,可以把输出语句打印位置改到文件
- System.out.println()中点进out中:**public final static PrintStream out = null;**发现out是一个PrintStream对象,加载System类时会在静态代码块中初始化出来一个PrintStream对象并赋值给out,这个打印流是和控制台(控制台也是一个文件)连通的,所以数据默认会打到控制台
- 所以我们可以通过改变打印流流向的文件进而把输出语句打印位置改到文件
public class PrintDemo2 {
public static void main(String[] args) throws Exception {
System.out.println("锦瑟无端五十弦");
// 改变输出语句的位置(重定向)
PrintStream ps = new PrintStream("Demo20-io2/src/log.txt");
System.setOut(ps); //给out对象重新赋值
System.out.println("庄生晓梦迷蝴蝶");
}
}
10.Properties
Properties是HashTable的子类,其实就是一个Map集合,但是我们一般不会当集合使用,因为HashMap更好用
Properties核心作用:
- Properties代表的是一个属性文件,可以把自己对象中的键值对信息存入到一个属性文件中
- 属性文件是后缀是.properties结尾的文件,里面的内容都是key=value,后续做系统配置信息的
Properties和IO流结合的API:
构造器 | 说明 |
void load(InputStream inStream) | 从输入字节流读取属性列表(键元素对) |
void load(Reader reader) | 从输入字符流读取属性列表(键元素对) |
void store(OutputStream out, String comments) | 将此属性列表(键和元素对)写入此Properties表中,以适合于使用load(InputStream inStream)方法的格式写入输出字节流 |
void store(Writer writer, String comments) | 将此属性列表(键和元素对)写入此Properties表中,以适合使用load(Reader reader)方法的格式写入输出字符流 |
public Object setProperty(String key, String value) | 保存键值对 |
public String getProperty(String key) | 使用此属性列表中指定的键搜索属性值 |
public Set setProperty() | 所有键的名称的集合 |
- load(InputStream inStream)和load(Reader reader)作用一样,建议用后者
- store(OutputStream out, String comments)和void store(Writer writer, String comments)作用一样,建议用后者
- setProperty/setProperty/setProperty其实就是Map集合的put/get/keySet()方法,Properties离家出走了不想和家族再有关系,所以就把方法换了名
public class PropertiesDemo01 {
public static void main(String[] args) throws Exception {
Properties properties = new Properties();
properties.setProperty("admin", "123456");
properties.setProperty("dlei", "003197");
properties.setProperty("zhognguo", "china");
System.out.println(properties);
properties.store(new FileWriter("Demo20-io2/src/users.properties"), "zhu_shi,mei_sha_yong");
}
1.store方法的第二个参数就相当于一个注释,无实际意义
2.new的这个流FileWriter不用手动关闭:
- 因为当流作为参数传给别的流构造器new出来一个新流时,作为参数的流不用手动关闭,因为调用的这个构造器肯定是很负责的呀,它会在内部将作为参数传入的流自动关闭
- 哪种情况流需要手动关闭呢
- 当new出来的这个流没有作为参数有传给其他new流的构造器时,就需要手动关闭这个流
public class PropertiesDemo02 {
public static void main(String[] args) throws Exception {
Properties properties = new Properties();
// 加载属性文件中的键值对数据到属性对象properties中去
properties.load(new FileReader("Demo20-io2/src/users.properties"));
System.out.println(properties);
String rs = properties.getProperty("dlei");
System.out.println(rs);
}
}
注意:第9行如果用Map集合的get方法得到的是Object需要强转,而如果用getProperty返回的类型就是String类型(因为properties中的键值对都一定是String类型,所以这个API直接就返回String类型的数据)所以我们用getProperty而不用get
11.IO框架
- commons-io是apache开源基金组织提供的一组有关IO操作的类库,可以提高IO功能开发的效率
- commons-io工具包提供了很多有关io操作的类,有两个主要的类FileUtils和IOUtils
FileUtils主要有如下用法:
方法名 | 说明 |
String readFileToString(File file, String encoding) | 读取文件中的数据,返回字符串 |
void copyFile(File srcFile, File destFile) | 复制文件 |
void copyDirectoryToDirectory(File srcDir, File destDir) | 复制文件夹 |
使用comments-io简化io读写的步骤:
- 在项目中创建一个文件夹:lib
- 将commons-io-2.11.0.jar文件复制到lib文件夹
- 在jar文件上点右键,选择Add as Library -> 点击OK
- 在类中导包使用
public class CommonsIODemo01 {
public static void main(String[] args) throws Exception {
// 1.完成文件复制(读取源文件的字节,然后将字节写入目标文件)
IOUtils.copy(new FileInputStream("Demo20-io2\\src\\data.txt"),
new FileOutputStream("Demo20-io2\\src\\data01.txt"));
// 2.完成文件复制到某个文件夹下(这个我们以前实现不了,并且人家这个即使文件夹不存在也会自动创建,无论几级文件夹)
FileUtils.copyFileToDirectory(new File("Demo20-io2\\src\\data.txt"), new File("Demo20-io2\\src\\copy\\copy\\copy\\copy"));
// 3.完成文件夹复制到某个文件夹下!(文件夹中的文件或文件夹也会被复制)
FileUtils.copyDirectoryToDirectory(new File("Demo20-io2\\src\\copy") , new File("Demo20-io2\\src\\copy2"));
FileUtils.copyDirectoryToDirectory(new File("Demo20-io2\\src\\copy") , new File("Demo20-io2\\src\\copy3"));
// 4.删除文件夹(即使非空也可以删)
FileUtils.deleteDirectory(new File("Demo20-io2\\src\\copy3"));
}
}