该文章Github地址:https://github.com/AntonyCheng/java-notes
在此介绍一下作者开源的SpringBoot项目初始化模板(Github仓库地址:https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址:https://blog.csdn.net/AntonyCheng/article/details/136555245),该模板集成了最常见的开发组件,同时基于修改配置文件实现组件的装载,除了这些,模板中还有非常丰富的整合示例,同时单体架构也非常适合SpringBoot框架入门,如果觉得有意义或者有帮助,欢迎Star & Issues & PR!
上一章:由浅到深认识Java语言(35):File类
44.I/O流(操作文件中的数据)
相关概念
I/O流:Input/Output;
I/O作用是将数据从一个设备中流入另一个设备中,比如从磁盘流向内存,从磁盘流向移动存储设备,从一台计算机流向另一台计算机;
I/O流中的数据都是字节,任何数据文件都由字节组成,字节是计算机中最小的存储单元;
I/O流对象的分类
按照操作的文件类型分类
- 文件类型文件 – 选择流对象字符流
- 什么是文本文件:使用文本工具(记事本,notepad++,editplus等)打开这个文件能够直接“阅读”的文件;
- 非文件类型文件 – 选择流对象字节流
按照数据流向分类
- 输入流:Java 程序从其他地方读取数据;
- 输出流:Java 程序中的数据写入到其他地方;
I/O流对象的分类归纳
- 字节输出流:OutputStream 抽象超类
- 字节输入流:InputStream 抽象超类
- 字符输出流:Writer 抽象超类
- 字符输出流:Reader 抽象超类
字节流
字节输出流
OutputStream 是所有字节输出流的超类,是一种全能的输出流,可以写入任何类型的文件;
写入字节的方法 write()
- void write(int b) 写入单个字节,参数给到的是 int ,但是会被转型为 byte ,所以数据范围应该在 -128~127;
- void write(byte[] b) 写入字节数组;
- void write(byte[] b,int startIndex,int len) 写入数组的一部分,开始索引,写入的个数;常常用于文件的复制操作;
FileOutputStream 实现类
构造方法:
- FileOutputStream(File file)
- FileOutputStream(String file)
- 创建字节输出流对象,绑定参数就是要写入的数据目的地;
- 如果数据目的地不存在,那么就会创建这样一个数据目的地并且写入数据,如果数据目的地存在,那么就会覆盖原有的数据目的地中的数据;
JVM 很聪明,任何一个操作系统都具有 IO 设备,但是 JVM 没有,因为它是依靠操作系统来实现 IO 功能的,所以当 IO 对象使用完毕后要记得释放资源(对象名.close());
字节输出流写入文件的步骤:
-
创建字节输出流对象,构造方法中需要绑定文件的路径;
-
调用流对象的方法 write() 写入数据;
-
释放资源;
示例如下:
package top.sharehome.Demo; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; public class Demo { public static void main(String[] args) throws IOException { File f = new File("d:\\大学课程学习文档\\java\\Practice\\IO\\test.txt"); FileOutputStream fos = new FileOutputStream(f); byte[] arr = new byte[]{97,98,99,100,101,102}; boolean exists = f.exists(); if (exists){ fos.write(49); fos.write(50); fos.write(51); fos.write(arr); fos.write(arr,0,3); } fos.close(); } }
打印效果如下:
还可以利用字符串中的 getBytes() 方法将字符串转换成 byte[];
package top.sharehome.Demo; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; public class Demo { public static void main(String[] args) throws IOException { File f = new File("d:\\大学课程学习文档\\java\\Practice\\IO\\test.txt"); FileOutputStream fos = new FileOutputStream(f); boolean exists = f.exists(); if (exists){ fos.write("你好 Java".getBytes()); } fos.close(); } }
打印效果如下:
追加写入和换行:
-
追加写入,即 FileOutputStream 构造方法的第二个参数写 true;
package top.sharehome.Demo; import java.io.FileOutputStream; import java.io.IOException; public class Demo { public static void main(String[] args) throws IOException { FileOutputStream fosWrite = new FileOutputStream("d:\\大学课程学习文档\\java\\Practice\\IO\\test.txt"); FileOutputStream fosCover = new FileOutputStream("d:\\大学课程学习文档\\java\\Practice\\IO\\test.txt",true); fosWrite.write(97); fosWrite.write(98); fosWrite.write(99); fosCover.write(100); fosCover.write(101); fosCover.write(102); fosCover.close(); fosWrite.close(); } }
打印效果如下:
-
换行写入,使用 Windows 系统的换行符号
\r(换行符) \n(回车符)
示例如下:
package top.sharehome.Demo; import java.io.FileOutputStream; import java.io.IOException; public class Demo { public static void main(String[] args) throws IOException { FileOutputStream fos = new FileOutputStream("d:\\大学课程学习文档\\java\\Practice\\IO\\test.txt"); byte[] bytes = new byte[]{97,98,99,'\n','\r',100,101,102}; fos.write(bytes); fos.close(); } }
打印效果如下:
字节输出流异常处理
其实以上的示例都存在不恰当异常处理的情况,由于 JVM 借用了 OS 的 IO 功能,所以 JVM 向上抛的异常处理方式并不是一个明智的选择,所以应该选择 try……catch……finally…… 的异常处理方式将其就地处理掉;
异常主要有两个:
- FileOutputStream 对象创建失败后的空指针异常;
- 以及 FileOutputStream 创建时的 IO 流异常;
示例如下:
package top.sharehome.Demo;
import java.io.FileOutputStream;
import java.io.IOException;
public class Demo {
public static void main(String[] args) {
FileOutputStream fos = null;
//这个 try 是为了处理IO流异常
try {
fos = new FileOutputStream("d:\\大学课程学习文档\\java\\Practice\\IO\\test.txt");
fos.write("字节输出流异常处理".getBytes());
fos.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
//这个 try 也是为了处理IO流异常
try {
//这个 if 是为了防止空指针异常输出
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
打印效果如下:
字节输入流
InputStream 是所有字节输入流的超类,是一种全能的输入流,可以读取任何类型的文件;
读取字节的方法 read()
- int read() 读取单个字节,同一个对象的连续 read() 方法会接连读取内容,每次读取返回一个 int 类型值,如果读取到流的末尾就返回 -1,该方法不能正常读取中文,因为一个中文字符所占大小不止一个字节;
- int read(byte[] b) 读取字节数组,同一个对象的连续 read(byte[] b) 方法会接连读取内容并返回读取到的字节个数,同时 byte[] 数组中的会被新读取到的内容所覆盖,如果读取到流的末尾就返回 -1,通常我们要将 byte[] 数组设置成 1024 的整数倍;
FileInputStream 实现类
构造方法:
- FileInputStream(File file)
- FileInputStream(String file)
- 创建字节输入流对象,绑定参数就是要读取的数据源文件;
- 字节流输入流和输出流的方法使用几乎一样;
方法示例如下:
-
int read() 读取单个字节,返回一个 int 类型值;
(文件中内容是 abc)
package top.sharehome.Demo; import java.io.FileInputStream; import java.io.IOException; public class Demo { public static void main(String[] args) { FileInputStream fis = null; try { fis = new FileInputStream("d:\\大学课程学习文档\\java\\Practice\\IO\\test.txt"); int r = 0; while ((r = fis.read()) != -1) { System.out.println("r= "+r); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (fis != null) { fis.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
打印效果如下:
-
int read(byte[] b) 读取字节数组;
(文件中内容是 abcdefghi)
package top.sharehome.Demo; import java.io.FileInputStream; import java.io.IOException; public class Demo { public static void main(String[] args) { FileInputStream fis = null; byte[] arr = new byte[1024]; try { fis = new FileInputStream("d:\\大学课程学习文档\\java\\Practice\\IO\\test.txt"); int i; while ((i = fis.read(arr)) != -1) { System.out.println("i = " + i); System.out.println("new String(arr) = " + new String(arr)); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (fis != null) { fis.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
打印效果如下:
由于原理是 byte[] 数组覆盖后输出,最后一次只读取到了一个 i 字母,所以仅仅只覆盖上一次所读取到的 g 的位置,而 h 的位置保持不变;
为了不出现以上的情况,我们又要运用 String 类当中的重载方法 String(转换的数组,开始索引,转换的个数):
package top.sharehome.Demo; import java.io.FileInputStream; import java.io.IOException; public class Demo { public static void main(String[] args) { FileInputStream fis = null; byte[] arr = new byte[1024]; try { fis = new FileInputStream("d:\\大学课程学习文档\\java\\Practice\\IO\\test.txt"); int i; while ((i = fis.read(arr)) != -1) { System.out.println("i = " + i); System.out.println("new String(arr) = " + new String(arr, 0, i)); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (fis != null) { fis.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
打印效果如下:
文件复制
实现文件的复制功能,和操作系统中的 Ctrl+c,Ctrl+v 一样,原理上就是字节的流动;
我们可以用单个字节输入输出流完成它,假设从 test1.txt 提取内容 123 到 test2.txt 中:
package top.sharehome.Demo;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Demo {
public static void main(String[] args) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream("d:\\大学课程学习文档\\java\\Practice\\IO\\test1.txt");
fos = new FileOutputStream("d:\\大学课程学习文档\\java\\Practice\\IO\\test2.txt");
int flag = 0;
while ((flag = fis.read()) != -1) {
fos.write(flag);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
打印效果如下:
但是这种单个字节复制文件方法是不明智的,较小的文件可以这样,但是一旦文件内存较大,该方法的效率就显得极其低下;
举例如下:
以下是 test3.txt 文件内容,有20000+行内容,我们写程序来验证一下所花费时间;
package top.sharehome.Demo;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Demo {
public static void main(String[] args) {
long start = System.currentTimeMillis();
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream("d:\\大学课程学习文档\\java\\Practice\\IO\\test3.txt");
fos = new FileOutputStream("d:\\大学课程学习文档\\java\\Practice\\IO\\test2.txt");
int flag = 0;
while ((flag = fis.read()) != -1) {
fos.write(flag);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println("end-start = " + (end - start));
}
}
打印效果如下:
足足用了 13 秒,效率极低!所以我们要考虑运用到字节数组去完成这一复制动作!
package top.sharehome.Demo;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Demo {
public static void main(String[] args) {
long start = System.currentTimeMillis();
byte[] arr = new byte[1024];
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream("d:\\大学课程学习文档\\java\\Practice\\IO\\test3.txt");
fos = new FileOutputStream("d:\\大学课程学习文档\\java\\Practice\\IO\\test2.txt");
int flag = 0;
while ((flag = fis.read(arr)) != -1) {
fos.write(arr,0,flag);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println("end-start = " + (end - start));
}
}
打印效果如下:
显然,速度要快很多,这样的数组也就是字节流缓冲流的原理;
字节流的缓冲流
使用字节流的缓冲流,提高原有流对象的读写性能;
字节流缓冲区流,流的本质上也是字节流;
- BufferedOutputStream 继承自 OutputStream;
- 方法 write() 写入单个的字节,或者是字节数组;
- BufferedInputStream 继承自 InputStream;
- 方法 read() 读取单个的字节,或者是字节数组;
Buffered 开头的流被称为缓冲流,FileOutputStream 基础流;这些类的构造器需要传入基础流,传入什么基础流,那就对什么基础流进行高效处理,如果没有基础流,那么这个类就无意义,这样的类设计也有一个特殊的设计模式名称——装饰者,关闭流时仅关闭装饰者即可;
BufferedOutputStream 构造方法
new BufferedOutputStream(OutputStream out)
传递字节输出流;
BufferedInputStream 构造方法
new BufferedInputStream(InputStream in)
传递字节输入流;
字节缓冲流实现文件复制
package top.sharehome.Demo;
import java.io.*;
public class Demo {
public static void main(String[] args) {
long start = System.currentTimeMillis();
FileInputStream fis = null;
FileOutputStream fos = null;
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
fis = new FileInputStream("d:\\大学课程学习文档\\java\\Practice\\IO\\test3.txt");
fos = new FileOutputStream("d:\\大学课程学习文档\\java\\Practice\\IO\\test2.txt");
bis = new BufferedInputStream(fis);
bos = new BufferedOutputStream(fos);
byte[] arr = new byte[1024];
int flag = 0;
while ((flag = bis.read(arr)) != -1) {
bos.write(arr, 0, flag);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bis != null) {
bis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (bos != null) {
bos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println("(end-start) = " + (end - start));
}
}
打印效果如下:
再次相比普通数组的文件复制,这又是一次巨大的优化;