十四、IO流
1. 概念
File存在原因:
变量、数组、对象、集合都是内存中的数据容器,它们在断电、或者程序终止时会丢失。File可以把数据长久保存,存储在计算机硬盘中。
File类可以操作文件
File概念:
File是java.io.包下的类,File类的对象,用于代表当前操作系统的文件(可以是文件、或文件夹)
注意:
File类只能对文件本身进行操作,不能读写文件里面存储的数据。可以进行创建文件/文件夹,删除文件/文件夹,获取文件大小、文件名等信息,判断文件类型等操作。
IO流:
读写文件中或网络中的数据
2. File文件对象的创建
public class FileTest1 {
public static void main(String[] args) {
// 1. 创建一个File对象,指代某个具体的文件
// 路径分隔符
// File f1 = new File("E:\\want\\code\\javasepromax\\test_file_IO.txt");
// File f1 = new File("E:/want/code/javasepromax/test_file_IO.txt");
File f1 = new File("E:" + File.separator + "want" + File.separator + "code" + File.separator + "javasepromax" + File.separator + "test_file_IO.txt");
System.out.println(f1.length()); // 输出文件的大小 文件夹也可以输出大小,只不过是文件夹信息的大小,不是文件夹中所有内容的大小
// 2. File对象可以指代一个不存在的文件路径
File f2 = new File("E:\\hhh.txt");
System.out.println(f2.length());
// 3. 相对路径
File f3 = new File("test_file_IO.txt"); // 相对路径工程名以及前面的路径都不要写!!!直接从模块名开始,默认到当前工程下的模块路径找
System.out.println(f3.length());
System.out.println(f3.getAbsolutePath());
}
}
3. File对象常用方法
判断文件的类型、获取文件信息
public class FileTest2 {
public static void main(String[] args) {
// 1. 创建一个File对象,指代某个具体的文件
// 路径分隔符
File f1 = new File("E:\\want\\code\\javasepromax\\test_file_IO.txt");
// 2. exists():判断当前文件对象对应的文件路径是否存在,如果存在,返回true
System.out.println(f1.exists()); // true
// 3. isFile():判断当前文件对象指代的是否是文件,是文件返回true
System.out.println(f1.isFile()); // true
// 4. isDirectory():判断当前文件对象指代的是否是文件夹,是文件夹返回true
System.out.println(f1.isDirectory()); // false
// 5. getName():获取文件名称(包含后缀)
System.out.println(f1.getName());
// 6. length():获取文件大小,返回字节个数
System.out.println(f1.length());
// 7. lastModified():获取文件最后的修改时间
System.out.println(f1.lastModified());
// 8. getPath():获取创建文件对象时,使用的路径
System.out.println(f1.getPath()); // 绝对路径/相对路径
// 9. getAbsolutePath()获取绝对路径
System.out.println(f1.getAbsolutePath());
}
}
创建文件、删除文件
public class FileTest3 {
public static void main(String[] args) throws IOException {
File f1 = new File("E:\\want\\code\\javasepromax\\test_file_IO1.txt");
System.out.println(f1.createNewFile());
// mkdir()创建一级文件夹——意思不是不能跨文件夹创建,而是文件的上一层文件夹必须存在
File f2 = new File("E:\\want\\code\\javasepromax\\oop-app5\\src\\test_file_IO3.txt");
System.out.println(f2.mkdir());
// mkdires()创建多级文件
// delete()只能删除文件、空文件夹,不能删除非空文件夹
}
}
遍历文件夹
public class FileTest4 {
public static void main(String[] args) {
// list():获取一级文件名称到一个字符串数组中去返回
File f1 = new File("E:\\want\\code\\javasepromax");
String[] names = f1.list();
System.out.println(Arrays.toString(names));
// listFiles():重点,获取当前目录下所有的一级文件对象到一个文件对象数组中去返回
File[] files = f1.listFiles();
for (File file : files) {
System.out.println(file.getAbsoluteFile());
}
}
}
注意事项:
- 当主调是文件,或者路径不存在时,返回null
- 当主调是空文件夹时,返回一个长度为0的数组
- 当主调时有内容的文件夹时,将里面所有一级文件和文件夹的路径放在File数组中返回
- 当主调是一个文件夹,里面隐藏文件也会返回在File数组中
- 当主调是一个文件夹,但是没有权限访问该文件夹时,返回null
4. 方法递归
需要注意:要控制好终止条件,不然会出现递归死循环,导致栈内存溢出错误
文件搜索案例——没有公式的案例
public class FileTest5 {
public static void main(String[] args) {
searchFile(new File("D:/"), "wemeetapp.exe");
}
/**
* 去目录下搜索某个文件
* @param dir 目录
* @param fileName 要搜索的文件名称
*/
public static void searchFile(File dir, String fileName) {
// 1. 把非法的情况拦截
if (dir == null || !dir.exists() || dir.isFile()) {
return; // 无法搜索
}
// 2. dir不是null,存在且一定是目录对象
File[] files = dir.listFiles();
// 3. 判断当前目录下是否存在一级文件对象,以及是否可以拿到一级文件对象
if (files != null && files.length > 0) {
// 4. 遍历全部一级文件对象
for (File file : files) {
// 5. 判断文件是文件,还是文件夹
if (file.isFile()) {
// 是文件,判断这个文件名是否是我们要找的,contains可以多个
if (file.getName().contains(fileName)) {
System.out.println(file.getAbsoluteFile());
}
} else {
// 是文件夹,继续重复这个过程(递归)
searchFile(file, fileName);
}
}
}
}
}
5. 字符集
ASCLL字符集: 英文数字特殊符号等,使用一个字节 存储一个字符,首位是0,总共可表示128个字符
GBK:汉字内码扩展规范,国标,包含两万多个汉字等字符,GBK中一个中文字符编码称两个字节 的形式存储,首位必须是1。其兼容了ASCLL字符集
Unicode字符集(统一码,万国码): 世界上所有文字、符号的字符集。
- UTF-32:4个字节表示一个字符,有容乃大——浪费存储空间,降低通讯效率
- UTF-8: 采取可变长编码方案,共分为四个长度区:1个字节,2个字节,3个字节,4个字节;英文字符、数字只占1个字节,汉字字符占3个字节
开发时都应该用UTF-8编码!!!
注意:
- 编码和解码时使用的字符集必须一致,否则会出现乱码
- 英文,数字一般不会乱码,因为很多字符集都兼容了ASCLL编码
public class FileTest6 {
public static void main(String[] args) throws UnsupportedEncodingException {
// 1. 编码
String data = "a我b";
byte[] bytes = data.getBytes(); // 默认的UTF-8
System.out.println(Arrays.toString(bytes)); // [97, -26, -120, -111, 98]
// 按照指定 字符集编码
byte[] bytes1 = data.getBytes("GBK");
System.out.println(Arrays.toString(bytes1)); // [97, -50, -46, 98];
// 2. 解码
String s1 = new String(bytes); // 默认UTF-8
System.out.println(s1);
String s2 = new String(bytes1); // 乱码
System.out.println(s2);
String s3 = new String(bytes1, "GBK"); // 指定解码字符集
System.out.println(s3);
}
}
6. IO流
输入输出流,读写数据的
输入流 Input:把磁盘\网络中的数据读到内存中去
输出流Output:把内存中的数据写到磁盘/网络中
应用场景:
通信/拷贝文件
6.1 分类
按流的方向: 输入流、输出流
按流中数据的最小单位:
- 字节流:可以操作所有类型的文件——音频、视频、图片、文件复制等
- 字符流:只适合操作纯文本文件——读写txt、java文件等
将四个流结合
6.2 IO流的体系
6.2.1 FileInputStream(文件字节输入流)
作用:把磁盘文件中的数据以字节的形式读入到内存中去
每次读取一个字节
public class FileInputStreamTest1 {
public static void main(String[] args) throws IOException {
// 每次读取一个字节
// 1. 创建文件字节输入流管道,与源文件接通
// FileInputStream is = new FileInputStream("file-io-app\\src\\itheima01.txt");
// 多态
// InputStream is = new FileInputStream(new File("file-io-app\\src\\itheima01.txt"));
InputStream is = new FileInputStream("file-io-app\\src\\itheima01.txt"); // 简化写法,推荐使用
// 2. 读取文件的字节数据
// 每次读取一个字节,如果没有数据,返回-1
// int b1 = is.read();
// System.out.println(b1); // 97
// System.out.println((char)b1); // a
//
// int b2 = is.read();
// System.out.println(b2); // 98
// System.out.println((char)b2); // b
//
// int b3 = is.read();
// System.out.println(b3); // -1
// 3. 循环读取文件
int b;
while ((b = is.read()) != -1) {
System.out.print((char)b);
}
// 问题:
// 读取数据的性能很差,每次读取一个字节时,要去硬盘上,要找还要调用硬盘资源
// 读取汉字会乱码
// 流使用完毕后,必须关闭!释放系统资源!!!
// 3. 释放资源
is.close();
}
}
每次读取多个字节
public class FileInputStreamTest2 {
public static void main(String[] args) throws Exception {
// 每次读取多个字节
// 1. 创建FileInputStream输入流对象,代表字节输入流管道与源文件相通
InputStream is = new FileInputStream("file-io-app\\src\\itheima02.txt"); // abc66
// // 2. 一次读取多个字节
// byte[] buffer = new byte[3];
// int len = is.read(buffer); // 返回当前读了多少个字节
// // String的构造器
// String rs = new String(buffer);
// System.out.println(rs); // abc
// System.out.println(len); // 3
//
// int len2 = is.read(buffer);
System.out.println(new String(buffer)); // 66c
System.out.println(len2); // 2
//
// // 为了实现读多少,打印多少
// System.out.println(new String(buffer, 0, len2)); // 66
// System.out.println(len2); // 2
// 循环改造 —— 降低系统调用的次数
byte[] buffer = new byte[3];
int len;
while((len = is.read(buffer)) != -1) {
String rs = new String(buffer, 0, len);
System.out.print(rs);
}
// 3. 关闭资源
is.close();
// 问题:读取汉字有乱码,与指定的数组长度有关
// 适合文件拷贝操作
}
}
每次读取完文件的全部字节——使用字节流读取中文,保证输出不乱码
public class FileInputStreamTest3 {
public static void main(String[] args) throws Exception {
// 使用文件字节输入流一次读取完文件的全部字节
// 1. 创建字节输入流管道
InputStream is = new FileInputStream("file-io-app\\src\\itheima03.txt");
// 2. 准备一个与文件大小一致的字节数组
// File f = new File("file-io-app\\src\\itheima03.txt");
// long size = f.length();
// byte[] buffer = new byte[(int)size]; // 假定不会出现超过内存的文件
// int len = is.read(buffer);
// System.out.println(new String(buffer));
//
// System.out.println(size);
// System.out.println(len);
// 改进:文件字节输入流提供了方法
byte[] buffer = is.readAllBytes(); // 内部有机制看文件过大的问题——>内存溢出
System.out.println(new String(buffer));
// 关闭资源
is.close();
}
}
但是读取文件全部内容可能会引起内存溢出
总结:字节流适合做数据的转移,如文件复制;字符流更适合读写文本内容
6.2.2 FileOutputStream(文件字节输出流)
作用:把内存中的数据以字节的形式写出到文件中去
public class FileOutPutStreamTest4 {
public static void main(String[] args) throws Exception {
// 掌握文件字节输出流FileOutputStream的使用
// 1. 创建一个文件字节输出流管道
// OutputStream os = new FileOutputStream("file-io-app\\src\\itheima04.txt"); // 覆盖
OutputStream os = new FileOutputStream("file-io-app\\src\\itheima04.txt", true); // 添加
// 2. 开始写字节出去文件
// 每次写一个字节:不能写汉字,字符
// os.write(97);
// os.write('b');
// 每次写多个字节——新执行一次会覆盖原来文件里面的内容
byte[] bytes = "我爱你中国abc".getBytes();
os.write(bytes);
// 写一部分
os.write(bytes, 0, 15); // 这里的长度是以字节为单位,一个中文字符在UTF-8下是3个字节,一共五个中文字符,所以长度是15个字节
// 换行符
os.write("\r\n".getBytes());
// 3. 关闭流
os.close();
}
}
6.2.3 案例:文件复制
6.2.4 释放资源的方式
原:.close();——问题:如果中间代码出现异常,关闭流的代码就执行不到
try-catch-finally:无论正常执行还是出现异常,都一定会执行finally区,除非JVM终止——不要在finally中返回数据
格式:
try {
……
} catch(异常名称) {
e.printStackTrace();
} finally {
}
public class Test1 {
public static void main(String[] args) {
try {
System.out.println(10 / 2);
System.exit(0); // 结束虚拟机,finally不执行了
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("finally执行了一次"); // 不能在finally中返回数据
}
}
}
作用:一般用于在程序执行完成后进行资源释放
缺点:代码臃肿
public class FileOutPutStreamTest4 {
public static void main(String[] args) {
// 作用域扩大,在finally中能找到
OutputStream os = null;
try {
// 掌握文件字节输出流FileOutputStream的使用
// 1. 创建一个文件字节输出流管道
// OutputStream os = new FileOutputStream("file-io-app\\src\\itheima04.txt"); // 覆盖
os = new FileOutputStream("file-io-app\\src\\itheima04.txt", true); // 添加
// 2. 开始写字节出去文件
// 每次写一个字节:不能写汉字,字符
// os.write(97);
// os.write('b');
// 每次写多个字节——新执行一次会覆盖原来文件里面的内容
byte[] bytes = "我爱你中国abc".getBytes();
os.write(bytes);
// 写一部分
os.write(bytes, 0, 15); // 这里的长度是以字节为单位,一个中文字符在UTF-8下是3个字节,一共五个中文字符,所以长度是15个字节
// 换行符
os.write("\r\n".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
// 3. 关闭流
// os.close(); // 报错的原因是怕流没有创建好
try {
if (os != null) os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
try-with-resource
格式:
try (定义资源1; 定义资源2; ……) {
可能出现异常的代码;
} catch (异常类名 变量名) {
异常处理的代码;
}
将流对象放在try的小括号中,结束后系统自动调用.close
小括号中只能放置资源对象——资源都会实现AutoCloseable接口
6.2.5 FileReader(文件字符输入流)
作用:把文件中的数据以字符的方式读入到内存中去
public class FileReaderTest1 {
public static void main(String[] args) {
// 1. 创建一个文件字符输入流
try (
Reader fr = new FileReader("file-io-app\\src\\itheima01.txt");
) {
// 2. 读取文件内容
// int c; // 记住每次读取的字符编号
// while ((c = fr.read()) != -1) {
// System.out.print((char)c);;
// }
// 每次读取多个字符
char[] buffer = new char[3];
int len;
while ((len = fr.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, len)); // 注意写范围,为了避免报错
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
6.2.6 FileWriter(文件字符输出流)
作用:把内存中的数据以字符的形式输出到文件中去
public class FileWriterTest1 {
public static void main(String[] args) {
// 0. 创建一个文件字符输出流对象,与目标文件接通
try (
Writer fw = new FileWriter("file-io-app\\src\\itheima05.txt")
) {
// 1. write(int c) :写一个字符进去
fw.write('a');
fw.write(97);
fw.write('与');
// 2. write(String c):写一个字符串进去
fw.write("小鱼0135");
// 3. write(String c, int pops, int len):写字符串的一部分出去
fw.write("小鱼0135", 0, 2);
// 4. write(char[] buffer):写一个字符数组出去
char[] buffer = {'小', '黑'};
fw.write(buffer);
// 5. write(char[] buffer, int pos, int len):写字符数组的一部分出去
fw.write(buffer, 1, 1);
// 换行
fw.write("\r\n");
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意事项: 在字符输出流写出数据后,必须刷新或者关闭流,写出去的数据才能生效;缓存区满了会自己刷新
fw.flush(); // 刷新流
7 缓冲流
对原始流进行包装,以提高原始流读写数据的性能——增加内存中的空间,减少操作硬盘的次数,转而在内存中操作
原理:字节缓存输入流自带8KB缓冲池,字节缓存输出流自带8KB缓冲池
(缓存:提高读写速度;缓冲:方便数据传输,平衡传输速度)
字节缓冲流——增加操作:new BufferedInputStream(文件字节输入流对象)
字符缓冲流——自带8ak字符缓冲池
- 字符缓冲输入流——新增 readLine():读取一行——>不要使用多态的写法,因为要用实现类里面特有的方法
- 字符缓冲输出流——新增newLine():换行
性能分析:
缓冲的一般会快,字节数组较大的话,性能也差不多。但是字节数组扩太大对性能影响不明显
8. 转换流(字符流下面的实现类,因为字符有编码)
问题:文件输入编码和读取的编码不一致时,就会出现乱码
字符输入转换流
思路:先获取文件的原始字节流,再将其按真实的字符集编码转成字符输入流
public class InputStreamReaderTest2 {
public static void main(String[] args) {
// 字符输入转换流
// 1. 得到文件的原始字节输入流
try (
InputStream is = new FileInputStream("file-io-app\\src\\itheima06.txt");
// 2. 把原始的字节输入流按照指定的编码转换成字符输入流
Reader isr = new InputStreamReader(is, "GBK"); // 第二个参数是文件原始的编码方式
// 3. 把字符输入流包装成缓冲字符输入流
BufferedReader bisr = new BufferedReader(isr);
){
String line;
while ((line = bisr.readLine()) != null) {
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
字符输出转换流
- byte[] bytes = data.getBytes(“GBK”);
- 字符输出转换流
9. 打印流
方便高效的打印出去——>写出去,能实现打印啥就是啥
public class PrintStreamTest1 {
public static void main(String[] args) {
// 1. 创建一个打印流管道
try (
PrintStream ps = new PrintStream("file-io-app\\src\\itheima07.txt");
// PrintStream ps = new PrintStream("file-io-app\\src\\itheima07.txt", Charset.forName("GBK")); // 指定字符集编码形式
){
ps.println(97); // 文件是97
ps.println('a');
ps.println("小鱼0135");
ps.println(true);
ps.write(97); // a
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class PrintStreamTest1 {
public static void main(String[] args) {
// 1. 创建一个打印流管道
try (
PrintWriter ps = new PrintWriter("file-io-app\\src\\itheima07.txt");
// PrintStream ps = new PrintStream("file-io-app\\src\\itheima07.txt", Charset.forName("GBK")); // 指定字符集编码形式
){
ps.println(97); // 文件是97
ps.println('a');
ps.println("小鱼0135");
ps.println(true);
ps.write(97); // a
ps.write("嘎嘎嘎"); // 比PriintStream多的可以打印字符、字符串等
} catch (Exception e) {
e.printStackTrace();
}
}
}
要想模式变成追加而不是覆盖,可以先使用低级流,再包装成高级流
应用:输出语句的重定向
10. 数据流(通讯的时候)
可以输出/输入数据类型
DataOutStream(数据输出流)——允许数据和其类型一并写出去
public class DataOutPutStreamTest1 {
public static void main(String[] args) {
// 1. 创建一个数据输出流包装低级的字节输出流
try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("file-io-app\\src\\itheima08.txt"))) {
dos.writeInt(97);
dos.writeBoolean(true);
dos.writeUTF("小鱼0135");
} catch (Exception e) {
e.printStackTrace();
}
// 2. 创建数据输入流
try (
DataInputStream dis = new DataInputStream(new FileInputStream("file-io-app\\src\\itheima08.txt"));
){
int i = dis.readInt(); // 按写入的顺序
System.out.println(i);
Boolean b = dis.readBoolean();
System.out.println(b);
String s = dis.readUTF();
System.out.println(s);
} catch (Exception e) {
e.printStackTrace();
}
}
}
11. 序列化流
用来把对象写入文件,或者将对象从文件中读取出来
- 对象序列化:把Java对象写入文件
- 对象反序列化:把文件里的Java对象读出来