12. IO流
文件操作的简介
操作磁盘上的某⼀个⽂件或者⽂件夹。可以对他们进⾏创建、删除、移动、属性获取、属性设置等操
作。但是,并不包含读取⽂件的内容,拷⻉⽂件。
在Java中,使⽤ java.io.File 类描述⽂件(夹)。
绝对路径和相对路径
路径:⽤来描述⼀个⽂件所存在的地址,可以分为 绝对路径 和 相对路径。
绝对路径:从磁盘的根⽬录开始,⼀层层的向内查找,直到找到这个⽂件。
相对路径:某⼀个⽂件,相对于指定⽂件的路径(),在项目中,所有的相对路径都是针对项目的根目录的,例:File file = new File(“src\out\day23.java”)。
路径表示 优点 缺点 绝对路径 ⽤来表示⼀个路径,只要还在这个磁盘上,肯定可以找到指定的⽂件的。 ⼀旦换⼀个⽂件系统,此时这个路径表示的⽂件将⽆法找到。 相对路径 只要两者的相对位置不变,⽆论在哪⼀个⽂件系统中,都可以找到这个⽂件。 ⽤来表示路径,只要两者的相对位置发⽣了改变,这个⽂件将⽆法找到。
12.1 IO流的简介
流:在文件和程序之间建立的让数据在其中流通(单向流动)的管道,就是一个流对象。
IO流:Input、Output,输入输出。
使用IO流,实现对磁盘上的某个文件进行读写的操作。
IO流依据不同的分类方式可分为不同类型。
按方向划分 | 按流中的数据单位划分 |
---|---|
输入流 | 字节流 |
输出流 | 字符流 |
必须掌握的流:父类流,Java中的流非常多,但均继承自以下四个父类之一。
父类流 | 含义 |
---|---|
InputStream | 字节输入流 |
OutputStream | 字节输出流 |
Reader | 字符输入流 |
Writer | 字符输出流 |
注意事项:
- 上述四个⽗类流,他们都是抽象类,不能直接实例化对象,需要借助⼦类对象。
- 流对象⼀旦实例化完成,此时这个流是会持有这个⽂件的。此时将不能对这个⽂件进⾏某些操作。
- ⼀个流在使⽤结束后,⼀定要释放流资源。
12.2 File类
File:对磁盘上某一文件(目录)的描述,因为IO流需要对文件进行操作,因此在使用IO流的时候,一定会使用到File类。File类还包含若干对文件的操作方法。
File类在Java.io包里
关于⽬录分隔符,在不同的操作系统中,不⼀样。在windows中,使⽤ \ 作为⽬录分隔符,但是,在⾮windows的操作系统中,例如:Linux、Unix,使⽤ / 作为⽬录分隔符。
关于路径分隔符,在不同的操作系统中,不⼀样。在windows中,使⽤ ; 作为路径分隔符,但是,在⾮windows的操作系统中,例如:Linux、Unix,使⽤ : 作为路径分隔符。
静态属性 | 描述 |
---|---|
separator | 根据不同的操作系统,返回不同的目录分隔符(字符串) |
pathSeparatorChar | 根据不同的操作系统,返回不同的目录分隔符(字符) |
pathSeparator | 根据不同的操作系统,返回不同的路径分隔符(字符串) |
pathSeparatorChar | 根据不同的操作系统,返回不同的路径分隔符(字符) |
Windows操作系统 | 非Windows操作系统 | |
---|---|---|
目录分隔符 | \ | / |
路径分隔符 | ; | : |
构造方法 | 描述 |
---|---|
File(String pathname) | 通过文件的路径来实例化一个File对象 |
File(String parent,String child) | 拼接两个路径得到一个完整的路径 |
File(File parent,String child) | 根据一个父级文件和子路径得到一个新的File对象 |
File文件相关操作:
public class Program1 {
public static void main(String[] args) {
// 1.关于文件的路径
// 不同操作系统内使用的目录分隔符是不同的,Windows中是符号:\ ,路径分隔符也不同,Windows中的是符号:;
String a = File.separator;
char b = File.pathSeparatorChar;
String c = File.pathSeparator;
char d = File.pathSeparatorChar;
File file = updataInfo();
getInfo(file);
String path = "E:\\软件目录\\Eclipse\\JAVA";
show(path);
}
//关于File对象的操作
private static File updataInfo() {
//创建文件
// 构造方法1
File file1 = new File("E:\\软件目录\\Eclipse\\JAVA\\新建文件1");
// 构造方法2
File file2 = new File("E:\\软件目录\\Eclipse","JAVA\\新建文件2"); // 不一定从磁盘开始
try {
//返回尝试创建的结果
boolean result1 = file1.createNewFile();
if (result1) {
System.out.println("文件创建成功");
} else {
System.out.println("文件创建失败");
}
} catch (IOException e) {
e.printStackTrace();
}
//2.1创建文件夹(只能创建一级目录)并返回创建结果,如果创建成功,会返回true
// 创建失败原因:
// 指定路径下,已经有这个⽂件了。
// 没有⽗级⽬录的写权限
// ⽗级路径不存在
//返回尝试创建的结果
boolean result2_1 = file2.mkdir();
//2.2创建文件夹(可以创建多级目录)
//返回尝试创建的结果
boolean result2_2 = new File("E:\\软件目录\\Eclipse","JAVA\\新建文件夹2\\新建文件夹2_2").mkdirs();
//3.删除一个文件或一个空的文件夹(非空文件夹删不掉)
//该删除操作永久删除,不会进入回收站
// new File("E:\\软件目录\\Eclipse\\JAVA\\新建文件1").delete();
//4.重命名一个文件(同时移动一个文件的位置)
// 失败的情况:重命名的⽂件已存在
// 源⽂件不存在
System.out.println(file2.renameTo(new File("E:\\软件目录\\Eclipse","JAVA\\新建文件夹2_2")));
return file1;
}
// 常用关于File对象若干属性的获取方法
private static void getInfo(File file1) {
// 1.判断文件是否存在
boolean boolean1 = file1.exists();
// 2.判断一个路径指向的空间是否是一个文件
boolean boolean2 = file1.isFile();
// 3.判断一个路径指向的空间是否是一个文件夹
boolean boolean3 = file1.isDirectory();
// 4.文件权限判断
boolean boolean4_1 = file1.canRead(); // 判断文件是否可读
boolean boolean4_2 = file1.canWrite(); // 判断文件是否可写
boolean boolean4_3 = file1.canExecute(); // 判断文件是否可执行
// 5.判断文件是否是隐藏文件(隐藏文件以 . 开头)
boolean boolean5 = file1.isHidden();
// 6.获取文件的大小(字节)
Long long6 = file1.length();
// 7.获取文件的名字
file1.getName();
// 8.获取文件的路径
String string8_1 = file1.getPath(); // 相对路径:相对于某文件夹至该文件的路径
// 注:在Eclipse中,相对路径默认是相对于当前项目而言的
String string8_2 = file1.getAbsolutePath(); // 绝对路径:从磁盘的根目录至该文件
// 9.获取父级路径(路径字符串)
String string9 = file1.getParent();
// 10.获取描述父级文件的FIle类
File file10 = file1.getParentFile();
// 11.获取上次修改的时间
Date date11 = new Date(file1.lastModified());
System.out.println(date11);
}
//查询某一文件夹下所有的子文件夹
private static void show(String path) {
//指定的路径做出File对象
File file = new File(path);
//1.列举一个目录下所有文件的名字
String[] files1 = file.list();
//2.listFiles(FileFilter filter)
//将file文件夹下的所有文件都带入到方法中,返回 返回值为true的文件构成的字符串数组
//File file:文件夹名字
//String name:子文件夹的名字
// 获取⼀个路径下满⾜条件的⽂件
File[] files = file.listFiles(f -> !f.isHidden() && f.length() > 100 * 1024 *1024);
//例:列举出指定目录下所有的chw格式文件
//f:父类目录
//s:文件名
String[] files2 = file.list((f,s)->s.endsWith(".chw"));
//3.获取一个文件夹中所有的文件(返回所有子文件构成的File数组)
File[] file3 = file.listFiles();
//将file文件夹下的所有文件都带入到方法中,返回 返回值为true的文件构成的File数组
File[] file4 = file.listFiles(f->f.getName().endsWith(".mp4"));
//3.list()获取到⼀个路径下所有的⼦⽂件,以 String[]的形式返回,数组中存储的是⽂件的名字
String[] files = file.list();
Arrays.stream(files).forEach(System.out::println);
}
}
URI:
协议头,例:http:// ftp:// smb://
主机,例:www.baidu.com
端口,例: :8080/
访问文件的路径,例:root/second/third/…/a.java
分隔文件和参数列表:?
参数列表,例:username=xxx&passwd=123
12.3 InputStream、OutputSteam
重点:(容易出现乱码)
如何使用InputStream进行文件的读取
如何使用OutputStream进行文件的写操作
声明一个字节流对象:
public class Program1 {
public static void main(String[] args) {
//字节输入流:读取某个文件中的数据
// InputStream is = new FileInputStream("file\\source");
// InputStream is = new FileInputStream(new File("file\\source"));
//实际使用中,先声明一个InputStream
InputStream is = null;
try {
//实例化一个输入流
is = new FileInputStream("file\\source");
// E:\软件目录\Eclipse\JAVA\TestProgram\file\source
// E:\软件目录\Eclipse\JAVA\TestProgram\src\bbinary\Program1.java
} catch (FileNotFoundException e) {
e.printStackTrace();
}
finally {
//一个流若不再使用,一定要关闭它,否则不允许对文件进行其他操作,例:删除
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println(new File("file\\source").delete());
}
}
注:所有的流在使用结束后一定要立即关闭
方案一:try代码段结束前加 对象.clear()
方案二:InputStream已经继承了Autocloseable接口可以在try后,大括号前添加小括号,将流的声明放在小括号内
注意事项:过了try代码段,流就关闭了,此时一切对流的操作都无效
语法进阶:
public class Program2 {
public static void main(String[] args) {
//实例化 流对象的进阶语法(不需要手动关闭,因为try后小括号的代码段伴随try的开关):
try(InputStream is = new FileInputStream("file\\source")) {
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
通过流对象读取某文件的内容:
public class Program3 {
public static void main(String[] args) {
//读取流中的数据:
//int read():读取流中的数据,每次调用读取一个字节,返回值代表本次读取到的内容
//int read(byte[] array):读取流中的数据,将读取到的数据存入一个数组,返回值代表本次读取到多少个字节数据
//1.实例化了一个流的对象,在文件和程序之间建立了一个管道
try (InputStream is = new FileInputStream("file\\source")){
//2.实例化一个字节数组,循环读取一个文件中的数据
byte[] array = new byte[10];
//3.为防止最后一组array填不满,而多出乱码等情况
//需声明一个变量,用来记录每次读到了多少数据
int length = 0;
//4.循环读取流中的数据
while ((length = is.read(array))!=-1) { //is.read(array)返回该组array读取的字节数(无数据则返回-1),无参read函数代表每次读取一个元素
//将字节数组中的数据进行处理
String string = new String(array,0,length);//为防止最后一组array填不满,而多出乱码
System.out.print(string);
}
} catch (Exception e) {
e.printStackTrace();;
}
}
}
通过流对象某文件进行写操作:
public class Program4 {
public static void main(String[] args) {
//实例化一个流对象,连接程序和指定的文件
//在写程序的操作中,若目标文件不存在,则会自动创建一个文件
//FileOutputStream实例化的时候,会有一个boolean append参数来控制新的写操作是否会覆盖原有内容
//append:true->在原有内容的后面追加新的数据;
//append:false->删除原有数据并写入新数据,不写则默认值为false;
try (OutputStream os = new FileOutputStream("file\\target",true)){
//如何进行写操作
//注意:写操作并不是将数据写入到文件中,而是写入到输出流,再由输出流将数据流动到文件中
os.write("你好,世界 hello,world".getBytes());
//注意:每次写操作后,都需要添加一个flush操作
//目的:冲刷数据流,加速流中的数据流动到文件中
//在关闭流的时候,系统会自动调用一次flush操作以确保流中的数据都已经流动到文件中
os.flush();
System.out.println("写数据完成!");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
OutputStream注意事项:
- 使⽤⼀个输出流,建⽴程序与⽂件的连接。**如果这个⽂件不存在,会自动的创建。**但是在创建的时候,要求⽗级⽂件夹是存在的。如果父级文件夹不存在,则这个⽂件也会创建失败,会出现FileNotFoundException 异常。
- 使⽤流建议⽂件与程序的连接,当这个流第⼀次向⽂件中写数据的时候,会将⽂件中原来的数据全部清除,再写⼊新的数据。从第⼆次写开始,将会在原来的基础上向后追加。
拷贝文件:
public class Program5 {
public static void main(String[] args) {
copy("E:\\文件目录\\source.xlsx","C:\\Users\\陈永豪\\Desktop\\garget.xlsx");
}
//从文件名source拷贝至文件名target的文件中
private static void copy(String source,String target) {
//循环读取文件中的数据,将读取到的数据写入到目标文件
try (InputStream is = new FileInputStream(source);OutputStream os = new FileOutputStream(target)){
//1.实例化一个字节数组,用来存储每次读到的数据
byte[] array = new byte[1024];
//2.声明一个变量,存储每次读取到的字节数量
int length = 0;
//3.循环读取
while ((length = is.read(array))!=-1) {
//4.将读取到的数据写入到输出流中
os.write(array,0,length);
os.flush();
}
System.out.println("文件拷贝完成");
} catch (FileNotFoundException e) {
e.printStackTrace();
}catch (IOException e) {
e.printStackTrace();
}
}
}
加密操作:
public class Program6 {
public static void main(String[] args) {
String file = "E:\\文件目录\\source.xlsx";
lock(file);
}
//加密代码
private static void lock(String file) {
//思路:在当前路径下,创建一个新的文件,命名为: 原文件的命名.lock
//例:source.mp4->source.mp4.lock
String lockFilePath = file +".lock";
try (InputStream is = new FileInputStream(file);OutputStream os = new FileOutputStream(lockFilePath)){
byte[] array = new byte[1024];
int length = 0;
while ((length = is.read(array))!=-1) {
array = lock(array);
os.write(array,0,length);
os.flush();
}
System.out.println("文件拷贝完成");
} catch (FileNotFoundException e) {
e.printStackTrace();
}catch (IOException e) {
e.printStackTrace();
}
new File(file).delete();
}
private static byte[] lock(byte[] array) {
int secury = 1234;
for (int i = 0; i < array.length; i++) {
array[i] ^=secury;
}
return array;
}
}
解密操作:
public class Program7 {
public static void main(String[] args) {
String file = "E:\\文件目录\\source.xlsx.lock";
copyAndSecure(file);
}
private static void copyAndSecure(String file) {
String lockFilePath = file.substring(0,file.length()-5);
try (InputStream is = new FileInputStream(file);OutputStream os = new FileOutputStream(lockFilePath)){
byte[] array = new byte[1024];
int length = 0;
while ((length = is.read(array))!=-1) {
array = lock(array);
os.write(array,0,length);
os.flush();
}
System.out.println("文件拷贝完成");
} catch (FileNotFoundException e) {
e.printStackTrace();
}catch (IOException e) {
e.printStackTrace();
}
new File(file).delete();
}
private static byte[] lock(byte[] array) {
int secury = 1234;
for (int i = 0; i < array.length; i++) {
array[i] ^=secury; //位异或(两次异或得原码,即第二次解密)
}
return array;
}
}
12.4 Reader、Writer
Reader、Writer是所有的字符流的父类,一般情况下字符流是用来操作文本的。
Reader:是一个输入流。
Writer:是一个输出流。
字符流,流中流动的数据的单位是–字符,一般是对文本进行操作。
使用字符流在读取数据的时候,过程与字节流读取基本相同,不同点在于:使用字节流读取数据,需要用到一个字节数组,将读取到的数据存入到一个字节数组中;使用字节流读取数据,需要用到一个字符数组,将读取到的数据存入到一个字符数组中。
将数据读取到字符输入流
public class Program1 {
public static void main(String[] args) {
//1.实例化一个FileReader对象并向上转型为Reader类型
try (Reader reader = new FileReader("file\\source")) {
//2./实例化一个字符数组,用来存储每次从流中读取的数据
char[] array = new char[10];
//3.声明一个变量,用来存储每次读取到多少个数据(字符)
int length = 0;
//4.循环读取
while ((length=reader.read(array))!=-1) {
//5.将字符数组中的元素拼接成字符串
String string = new String(array,0,length);
System.out.println(string);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}catch (IOException e) {
e.printStackTrace();
}
}
}
将数据写入到字符输出流
public class Program2 {
public static void main(String[] args) {
try (Writer writer = new FileWriter("file\\target",true)) {
writer.write("哈哈哈哈哈");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
字节流与字符流的区别:
- 字节流可以用于任何类型数据的输入输出,字符流只能用于文本数据的输入输出
- 对于文本数据字节流和字符流都能用,只不过用字符流更方便,因为字符流处理了编码
12.5 缓冲流
对父类流进行了一层包装,添加了一个缓冲区(Buffer),目的是提高流操作的效率。
BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter
在所有的缓冲流中,都有一个属性用来记录所包装的流对象,在缓冲流的close中,会对包装的流对象进行自动的关闭操作(使用完缓冲流的时候,通过关闭缓冲流即可)
缓冲字节流的读操作:
public class Program1 {
public static void main(String[] args) {
//实例化
//通过一个InputStream实例化一个BufferedInputStream
try(BufferedInputStream bis = new BufferedInputStream(new FileInputStream("file\\source"))) {
//1.BufferedInputStream继承自InputStream,所以本质上还是字节流,因此读取数组还需字符数组
byte[] array = new byte[1024];
//2.声明一个变量,用来存储每次读取到多少个字节的数据
int length = 0;
//3.循环读取
while ((length=bis.read(array))!=-1) {
//4.将读取到的数据进行处理
String string = new String(array,0,length);
System.out.print(string);
}
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
}
}
缓冲字节流的写操作:
// 1、通过⼀个 OutputStream,实例化⼀个 BufferedOutputStream
// 此时,BufferedOutputStream在进⾏关闭的时候,会对内部的 字节输出流 进⾏
try (BufferedOutputStream bos = new BufferedOutputStream(new
FileOutputStream("files\\dst"))) {
// 将数据写⼊到输出流中
bos.write("hello".getBytes());
bos.flush();
} catch (IOException e) {
e.printStackTrace();
}
缓冲字符流的读操作:
public class Program2 {
public static void main(String[] args) {
//实例化
//通过一个Reader实例化一个BufferedReader
//在使⽤完 BufferedInputStream 之后,直接关闭 bis 即可,不需要⼿动关闭 InputStream字节流
try(BufferedReader br = new BufferedReader(new FileReader("file\\source"))) {
//1.声明一个String类型的变量,用来存储每次读取一行的数据
String line = null;
//2.循环读取
//readLine():读取流中的一行数据,行的区分,是按照\n计算的,但读取的内容不包含换行符\n(读取的内容在一行中)
while ((line=br.readLine())!=null) {
//3.将读取到的数据进行处理
System.out.println(line);
}
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
}
}
缓冲字符流的写操作:
public class Program3 {
public static void main(String[] args) throws IOException {
try (BufferedWriter bw = new BufferedWriter(new FileWriter("file\\target",true))){
bw.write("后宫佳丽三千人,三千宠爱在一身");
//在BufferedWriter:增加了一个方法:newLine();
//在流中写一个换行符,等价于write方法后加一个 \n
bw.newLine();
bw.write("缓歌漫舞凝丝竹,尽日君王看不足");
bw.flush();
}catch (IOException e){
e.printStackTrace();
}
}
}
12.6 Scanner类
扫描器类,从一个流或者从一个文件中扫描数据(不一定是控制台)。
12.6.1 标准输入、输出流
System.in:系统标准字节输入流,本质来讲是一个InputSteam,但是这个对象是系统自己实例化的。连接了程序和控制台,在内部封装了一些逻辑,可以阻塞线程,等待用户的输入,直到用户输入完成,可以继续进行其他的操作。
System.out:系统标准输出流,本质来讲是一个打印流(printSteam),可以将程序中的数据输出到控制台,也可以从控制台向指定文件输入内容,其实连接控制台的操作是由打印流完成的。
重定向标准输入流:
// 在重定向输⼊流之前,先备份原来的输⼊流,以便使⽤结束之后,恢复
InputStream original = System.in;
// 实例化⼀个 InputStream,读取⼀个⽂件中的数据
try (BufferedInputStream inputStream = new BufferedInputStream(new
FileInputStream("files\\src"))) {
// 重定向标准输⼊流
System.setIn(inputStream);
// 此时,再使⽤到 System.in 的时候,其实⽤的是 inputStream,此时从指定文件中读取数据到控制台。
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
}
scanner.close();
} catch (IOException e) {
e.printStackTrace();
}
// 因为此时重定向了标准输⼊流,此时的标准输⼊流就是上⽅try中的inputStream
// 但是!!! try结构结束之后,这个inputStream会被close。
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
}
scanner.close();
标准输入流的相关操作:
public class Program1 {
public static void main(String[] args) {
//Scanner类:读取指定文件中的数据
try (Scanner scanner = new Scanner(new File("file\\source"))) {
//循环并判断是否读完
while (scanner.hasNextLine()) {
String string = scanner.nextLine();
System.out.println(string);
}
//Scanner对象在使用完成后,是需要关闭的
scanner.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
标准输出流的相关操作:
public class Program2 {
public static void main(String[] args) {
//备份最初始的标准输出流
PrintStream tmp = System.out;
//1.实例化一个打印流对象
try (PrintStream ps = new PrintStream(new FileOutputStream("file\\log",true));){
//重定项系统标准输出流
System.setOut(ps);
System.out.println("hello world");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
System.setOut(tmp);
System.out.println("你好 世界");
}
}
12.7 转换流
全称——字节字符转换流,底层的实现是字节流,这样能保证数据读写过程中不会出现字节丢失问题,上层提供的操作⽅法,是字符流的方法。所以,转换流保留了字符操作的便利性,又有字节流的可靠性。最常⻅的使⽤场景,就是读取指定的字符集的⽂本⽂件。
当我们需要读取一个文本数据的时候,由于不同字符集(编码格式)的存在,会导致读取的过程中,出现乱码的问题,可以使用转换流解决这个读写过程中的字符乱码问题。
使用场景:
- 使用指定的字符集,读取某文件中的数据
- 使用指定的字符集,向一个指定的文件中写数据。
涉及到的两个类:InputStreamReader、OutputStreamWriter
用转换流和指定的字符集对某一文本做读操作:
public class Program1 {
public static void main(String[] args) {
//InputStream和一个指定的字符集实例化一个转换流对象
try (InputStreamReader reader = new InputStreamReader(new FileInputStream("file\\source2"), "gbk") ){
//1.从流中读取数据
char[] array = new char[1024];
int length = 0;
while ((length=reader.read(array))!=-1){
System.out.println(new String(array,0,length));
}
}catch (UnsupportedEncodingException e){ //捕捉不支持的编码格式
e.printStackTrace();
}
catch(FileNotFoundException e){
e.printStackTrace();
}
catch (IOException e){
e.printStackTrace();
}
//法二:以下读写会稍微快一点
// try {
// BufferedReader reader = new BufferedReader(new BufferedReader(new InputStreamReader(new FileInputStream("file\target"))))
// }catch (UnsupportedEncodingException e){
// e.printStackTrace();
// }catch (IOException e){
// e.printStackTrace();
// }catch (FileNotFoundException e) {
// e.printStackTrace();
// }
}
}
用转换流和指定的字符集对某一文本做写操作:
public class Program2 {
public static void main(String[] args) {
//1.实例化一个采用了指定字符集的输出流
try(OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("file\\target",true),"utf-8")){
//2.将数据写入到文本中
osw.write("\n落霞与孤鹜齐飞,秋水共长天一色");
osw.flush();
}catch (UnsupportedEncodingException e){
e.printStackTrace();
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
}
}
12.8 Properties
Propeties是集合框架中的一个类,父类是Hashtable,主要作用是读写一个 .properties文件。
properties文件,是一个属性列表文件,在这个文件中,可以存储一些简单的配置数据。
.properties文件的规则:
- 是以键值对的形式进行存储的,键和值以等号分隔,不需要加空格,不同的键值对以换行分隔。
- 重点:所有的键值对都是以String类型存储的,但不需要也不能写双引号。
- 不能写中文(注释可以)
- 除了键、等号、值,不需要出现其他任何字符
使用Properties来读写文件:
public class Program {
public static void main(String[] args) {
//实例化一个Properties类的对象
Properties properties = new Properties();
try {
//1.从指定的流中读取数据到集合中
properties.load(new BufferedReader(new FileReader("file\\config.properties")));
//具有所有Map方法,例:
properties.remove("name");
//2。新添键值对
properties.setProperty("gender","unknown");
//3.遍历这个集合
properties.forEach((key,value)-> System.out.printf("%s=%s\n",key,value));
//4.将一个Properties文件中的数据,写入到指定的Properties文件中
//参数:OutputStream out:文件的存储路径, String comments:提交日志
properties.store(new FileOutputStream("file\\config.properties"),"log");
} catch (IOException e) {
e.printStackTrace();
}
}
}
12.9 对象流
将程序中的某⼀个对象,以⽂件的形式序列化到本地。
两个常用流:ObjectInputStream、ObjectOutputStream
作用:将程序中的某些对象,以文件的形式序列化到本地。(如果没有持久化存储,这个对象将在程序结束的时候被销毁)
序列化:将对象以文件的形式保存到本地,用ObjectOutputStream。
反序列化:将本地的某个文件存储的信息读取出来并给某一个对象的属性赋值,用ObjectInputStream。
注意事项:
- NotSerializableException异常:需要序列化的对象对应的类(包含内部类),必须要实现 Serializable 接口。
- 如果需要序列化多个对象,不能通过public FileOutputStream(String name, boolean append)方法拼接对象的信息,而是将所有对象存入一个集合,将这个集合整体序列化到本地。
通过对象进行序列化和反序列化的操作:
public class Program {
public static void main(String[] args) {
//1.实例化一个Person对象
Person person = new Person("xiaoming",20,Gender.Male,new Dog("小白",2));
//序列化
save(person);
Person xiaoming = loadData();
System.out.println(xiaoming);
}
//序列化:将一个Person对象以文件的形式保存到本地
private static void save(Person person){
//1.实例化一个ObjectOutputStream对象
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("file\\person"))){
//2.序列化
oos.writeObject(person);
oos.flush();
System.out.println("序列化完成"); //生成的二进制文件乱码
}catch (FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
//反序列化:从本地序列化的文件中读取数据,转成对象并返回
private static Person loadData(){
//1.实例化一个ObjectInputStream对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("file\\person"))){
//2.反序列化
Object object = ois.readObject();
//3.判断object对象是否是一个person对象
if(object instanceof Person){
return (Person)object;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
class Person implements Serializable {
private String name;
private int age;
private Gender gender;
private Dog pet;
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", gender=" + gender +
", pet=" + pet +
'}';
}
public Person(String name, int age, Gender gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public Person(String name, int age, Gender gender, Dog pet) {
this.name = name;
this.age = age;
this.gender = gender;
this.pet = pet;
}
}
class Dog implements Serializable{
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
}
12.10 NIO(重点,常见)
NIO:New IO、Non-Blocking IO(非阻塞型IO)
是JDK1.4出现的用来替代传统的IO的一套新的API。NIO与传统的IO有相同的功能,但操作方式不一样,NIO是面向缓冲区(Buffer)、基于通道(Channel)。
在JDK1.7之后,添加了若干新的元素,被称为NIO.2
NIO和IO的区别:
- NIO是面向缓冲区的,IO是面向流的
- NIO是非阻塞型,IO是阻塞型
12.10.1 缓冲区Buffer(双向流动)
缓冲区,其实是一个容器,类似于一个数组,这个容器中只能存储基本数据类型的数据。
缓冲区,按照存储的数据类型不同,可以分为以下几个(缓冲区)类:
ByteBuffer ShortBuffer IntBuffer LongBuffer FloatBuffer DoubleBuffer CharBuffer 以上缓冲区均拥有相同的父类-Buffer类,所以这些类有相同的方式来存储和管理数据。
注意:
- 没有存储boolean的缓冲区
- Buffer是一个抽象类,不能实例化对象。因此,在实际应用中,用的还是子类的对象。
常见操作:
-
获取缓冲区的对象:
由于缓冲区都是抽象类,不能直接通过new的方式实例化,需要通过静态方法allocate()获取对象。
注:缓冲区是一个容器,对缓冲区的操作也就只有读和写操作。
缓冲区的两种模式:读模式 和 写模式
读模式:在这个模式下,一般情况下是进行缓冲区的数据读取操作。
写模式:在这个模式下,一般情况下是进行缓冲区的数据写入操作。
-
缓冲区的常见属性
-
position:当前操作的下标
-
limit:能够操作的空间数(是一次读写操作的上限,当切换成读模式时,数值才会改变)
-
capacity:缓冲区的容量
-
mark:标记,在当前缓冲区的position位置中添加一个标记,配合reset方法,使position重置为mark标记位。
以上四个属性都是private权限,因此不能直接获取
四个属性的大小关系:
mark<=position<=limit<=capacity
一旦不满足以上关系就会出现异常
-
-
缓冲区的常见操作:
-
allocate(int capacity)
因为所有的缓冲区,都是抽象类,不能实例化对象。因此,缓冲区的开辟,需要通过这个⽅法,或者allocateDirect() 进⾏开辟。在开辟缓冲区的时候,需要传⼊⼀个参数,这个参数,就代表这个缓冲区的最⼤容量。如果类⽐到数组,这⾥就是数组的⻓度。这个容量,⼀旦确定了,缓冲区开辟了,就不能改变了。缓冲区,分为“读”模式,和“写”模式。
-
put():往缓冲区写数据,如果写的数据超出缓冲区的容量,会出现异常BufferOverflowException。
-
flip():将缓冲区切换成读模式
-
get():从缓冲区读取数据,若读取的数据超出limit,则会出现异常BufferUnderflowException。
-
rewind():重置操作,将position重置为0,重置mark(写模式时limit的范围也会重置为0,读模式时limit不变)
-
clear():清空,将position、limit、capacity、mark都重置为初始状态,并使缓冲区切换到写模式。
-
mark():在当前的position位置添加一个标记。
-
reset():重置position为mark的位置(如果没有设置mark值就reset则会异常)
常见操作:
public class Program1 { public static void main(String[] args) { //通过allocate方法获取一个缓冲区对象 ByteBuffer buffer = ByteBuffer.allocate(10); // System.out.println("------allocate()-------"); System.out.println("capacity = "+buffer.capacity()); //缓冲区的capacity System.out.println("limit = "+buffer.limit()); System.out.println("position = "+buffer.position()); //向缓冲区中添加数据 buffer.put("hello".getBytes()); System.out.println("------put()-------"); System.out.println("capacity = "+buffer.capacity()); System.out.println("limit = "+buffer.limit()); System.out.println("position = "+buffer.position()); //重置position buffer.rewind(); System.out.println("------rewind()-------"); System.out.println("capacity = "+buffer.capacity()); System.out.println("limit = "+buffer.limit()); System.out.println("position = "+buffer.position()); // //切换读模式 // buffer.flip(); // System.out.println("------flip()-------"); // System.out.println("capacity = "+buffer.capacity()); // System.out.println("limit = "+buffer.limit()); // System.out.println("position = "+buffer.position()); //从缓冲区中读取数据 byte[] dst= new byte[buffer.limit()]; buffer.get(dst); //无参方法时得到一个字节,可以通过新建字节数组一次性读取多个字节 System.out.println(new String(dst)); System.out.println("------get()-------"); System.out.println("capacity = "+buffer.capacity()); System.out.println("limit = "+buffer.limit()); System.out.println("position = "+buffer.position()); buffer.rewind(); System.out.println("------rewind()-------"); System.out.println("position = "+buffer.position()); System.out.println("limit = "+buffer.limit()); byte[] dst2 = new byte[2]; buffer.get(dst2); System.out.println("position = "+buffer.position()); //在当前位置添加一个标记 buffer.mark(); buffer.get(dst2); System.out.println(new String(dst2)); //ll buffer.reset(); System.out.println("position = "+buffer.position()); buffer.get(dst2); System.out.println(new String(dst2)); //ll } }
其实缓冲区就是一个容器,对缓冲区的操作就是读和写。所谓两种模式只是从逻辑上分类。
但出于规范在对应模式进行对应操作。
所谓的flip()方法、clear()方法只是重置了position和limit的值,本质上出发,缓冲区没有所谓的读和写模式。也就是说:“读模式”下可以进行写操作;“写模式”下可以进行读操作。
public class Program2 { public static void main(String[] args) { //开辟一个缓冲区 ByteBuffer buffer = ByteBuffer.allocate(10); //limit:10,position:0 buffer.put("hello".getBytes()); //limit:10,position:5 hello // buffer.flip(); //limit:5,position:0 buffer.rewind(); //limit:10,position:0 byte[] dst = new byte[5]; buffer.get(dst); //limit:10,position:5 System.out.println(new String(dst)); //hello // buffer.get(byte); //limit:10,position:10 buffer.flip(); //limit:5,position:0 buffer.put("world".getBytes()); //limit:5,position:5 world buffer.clear(); //limit:10,position:0 buffer.get(dst); System.out.println(new String(dst)); //world } }
直接缓冲区、非直接缓冲区
区别:
-
直接缓冲区,使用allocate方法实现。
直接缓冲区,使用allocateDirect方法开辟。
-
非直接缓冲区,是建立在JVM内存中的。
直接缓冲区是建立在物理内存中的。
-
直接缓冲区的读写效率高。
虽然直接缓冲区的读写效率远远高于非直接缓冲区,但实际使用中,还是以非直接缓冲区的使用为主。因为使用直接缓冲区有一些弊端:
- 由于直接缓冲区是建立在物理内存的,当我们需要进行写操作的时候,将数据从程序写入到直接缓冲区。此时程序已经丧失了对这个数据操作的可能性。这些数据什么时候写入到文件中,是由操作系统完成的。
- 直接缓冲区建立于物理内存,如果物理内存处理不佳,会导致内存泄漏。
//开辟一个直接缓冲区 //在物理内存中开辟一个大小1024的直接缓冲区 ByteBuffer buffer = ByteBuffer.allocateDirect(1024); //验证一个缓冲区是否是直接缓冲区 System.out.println(buffer.isDirect());
-
12.10.2 通道Channel
是一个文件和程序的连接。通道本身不负责数据的传递、数据的传递由缓冲区负责。
通道(Channel)是程序与⽂件之间的连接,在NIO中,数据传递是由缓冲区完成的,通道不负责数据的传递。通道在 java.nio.channels 包中,常⻅的Channel⼦类有:
本地⽂件通道:FileChannel
⽹络⽂件通道:SocketChannel、ServerSocketChannel、DatagramChannel
Channel是一个接口,常用实现类:FileChannel
FileChannel是一个抽象类,不能直接实例化对象。FileChannel对象的获取需要获得指定的途径:
- 可以使用支持通道的类,所提供的getChannel()方法来获取。
- 本地IO:FileInputStream、FileOutputStream
- ⽹络⽂件通道:Socket、ServerSocket、DatagramSocket
- 在NIO.2中,通过FileChannel类静态方法 open()获取通道。
- 在NIO.2中,使用Files类中的new方法。
12.10.3 路径Path类
一个用来描述文件路径的接口,在实际应用中,很少关心其实现类,Path对象的获取更多情况下,是通过一个工具类Paths获取的。
Path.get(String first,String...more)
方法的作用:会将参数列表中的每一个组成部分拼接起来,组成一个完整的路径,并返回描述这个路径的一个Path对象。
三种通过NIO进行读写数据并传递的操作:
/**
* 使用 FileChannel.open() 方法
*
* Path: 是一个用来描述一个文件路径的接口。常见的使用方法:
* Paths.get(String first, String... more): 拼接文件路径,得到Path对象
*
* StandardOpenOption:
* READ : 读文件
* WRITE : 写文件
* APPEND : 追加数据
* CREATE : 如果没有这个文件,就创建一个文件;如果存在这个文件,就不做任何操作。
* CREATE_NEW : 如果没有这个文件,就创建一个文件;如果存在这个文件,就抛异常。
*/
public class Program1 {
private static String src = "file\\source"; //源文件路径
private static String dst = "file\\target"; //目标文件路径
public static void main(String[] args) {
//1.使用支持通道的类,所提供的getChannel()方法来获取。
method01();
//2.通过NIO.2中FileChannel类的静态方法open()打开一个通道
method02();
//3.通过NIO.2中的Files的new方法,开辟一个通道
method03();
}
//1.使用支持通道的类,所提供的getChannel()方法来获取。
private static void method01(){
try (FileInputStream fis = new FileInputStream(src); FileOutputStream fos = new FileOutputStream(dst)){
//提供的getChannel方法,获取通道
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();
//实例化一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
while(inChannel.read(buffer)!=-1){ //将数据写入到缓冲区
//读取
buffer.flip(); //切换到读模式
outChannel.write(buffer); //将数据通过outChannel通道,写入到指定的文件中
buffer.clear();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
}
//2.通过NIO.2中FileChannel类的静态方法open()打开一个通道
private static void method02(){
FileChannel inChannel = null; //选择StandardOpenOption枚举中你需要的操作
FileChannel outChannel = null;
try {
//打开通道,读取src文件中的内容
inChannel = FileChannel.open(Paths.get(src), StandardOpenOption.READ);
//打开通道,将数据写入到dst中
outChannel = FileChannel.open(Paths.get(dst),StandardOpenOption.WRITE,StandardOpenOption.CREATE,StandardOpenOption.READ);
//如果使用非直接缓冲区读写数据,这个过程参考method01
//以下是使用直接缓冲区进行数据的读写
//使用内存映射文件
MappedByteBuffer min = inChannel.map(FileChannel.MapMode.READ_ONLY,0,inChannel.size());
MappedByteBuffer mout = outChannel.map(FileChannel.MapMode.READ_WRITE,0,inChannel.size());
//对直接缓冲区进行读写
byte[] array = new byte[min.limit()];
min.get(array);
mout.put(array);
} catch (IOException e) {
e.printStackTrace();
}finally {
if(inChannel!=null){
try {
//关闭通道
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(outChannel!=null) {
try {
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//3.通过NIO.2中的Files的new方法,开辟一个通道
private static void method03() {
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
inChannel = (FileChannel) Files.newByteChannel(Paths.get(src), StandardOpenOption.READ);
outChannel = (FileChannel) Files.newByteChannel(Paths.get(dst), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//实现两个通道之间的数据传递(这种方式是使用直接缓冲区操作的)
//法一:
// inChannel.transferTo(0,inChannel.size(),outChannel);
//法二:
outChannel.transferFrom(inChannel,0,inChannel.size());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inChannel != null) {
try {
//关闭通道
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outChannel != null) {
try {
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
12.10.4 分散聚合
分散:将数据写入到多个缓冲区,分散写入。
聚合:从多个缓冲区中读取数据,聚合读取。
public class Program2 {
public static void main(String[] args) {
try {
FileChannel inChannel = FileChannel.open(Paths.get("file\\source"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("file\\target"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
//将多个缓冲区存入一个数组
ByteBuffer[]buffers = new ByteBuffer[10];
for (int i = 0; i < buffers.length; i++) {
buffers[i]=ByteBuffer.allocate(2014);
}
//分散写:将数据写入到多个缓冲区
inChannel.read(buffers);
//聚合读:读取多个缓冲区的数据
outChannel.write(buffers);
} catch (IOException e) {
e.printStackTrace();
}
}
}
12.10.5 Files类
是一个用来操作file的工具类。
Files的常见操作:
public class Program1 {
public static void main(String[] args) {
try {
//1.创建文件
Files.createFile(Paths.get("file\\a.txt"));
//2.创建文件夹
Files.createDirectory(Paths.get("file\\abc"));
//3.创建多级目录
Files.createDirectories(Paths.get("file\\abc\\a\\b\\c"));
//4.删除文件(空文件夹):如果删除的文件(夹)不存在,则抛出异常
Files.delete(Paths.get("file\\a.txt"));
Files.delete(Paths.get("file\\abc\\a\\b"));
//5.尝试删除文件(夹),返回操作结果
boolean ret5 = Files.deleteIfExists(Paths.get("file\\a.txt"));
//6.移动、重命名文件
Files.move(Paths.get("file\\input"),Paths.get("file\\output"));
//7.拷贝文件
Files.copy(Paths.get("file\\source"),Paths.get("file\\dst"));
//获取文件大小
Files.size(Paths.get("file\\source"));
//判断可读
//判断可写
//判断可执行
} catch (IOException e) {
e.printStackTrace();
}
}
}