Java中的IO流概念介绍及使用方法,此IO只针对于磁盘IO,没有网络IO相关知识
概述
IO就是input/output,这个是相对于内存而言的;
- iniput就是往内存里面放数据,数据从哪里来的呢?可以是本地磁盘,也可以是从网络获取的数据
- output就是从内存里往外面传数据,数据要传到哪里去呢?可以是本地磁盘,也可以是网络
IO是一种按照顺序读写的数据的模式,特点就是单向流动,就像自来水在水管里面流动一样,所以叫IO流
注意:InputStream流获取之后只能用一次,读取完了这个流就是空的了
InputStream/OutputStream
InputStream和OutputStream是以字节为单位读数据的,就是针对于字节流来做输入输出的,最小单位是byte,注意:nputStream/OutputStream这是两个抽象类
Reader/Writer
Reader和Writer是以字符为单位读数据的,针对于字符数据来做输入输出很方便,底层其实还是字节数据,只不过加了一层字节转字符和字符转字节的转化,最小单位是char。注意:Reader/Writer是抽象类
同步和异步
同步:读写IO代码时必须得等待数据返回后,才能执行后续代码,优点是代码编写简单,缺点是cpu利用率不足,InputStream/OutputStream和Reader/Writer都是同步IO
异步:读写IO时,仅仅发出请求,然后就可以执行后续代码了,优点是cpu利用率高,缺点是代码编写复杂
File使用
java.io提供了File类来操作文件和目录
创建File对象
File对象,创建的时候构造方法,可以传路径(相对/绝对),也可以传具体文件的路径(”D:\test.txt”),传目录就表示目录,传具体文件file就代表具体文件了,所以file对象可以表示目录,也可以表示文件
路径说明:
- 传一个 “.” 代表的是当前目录,当前目录就是你的java项目的目录
- 传一个 “/“ 或 “\“ 就是表示的java项目所在磁盘的根目录
- 传一个 “..” 就是代表着上一级目录,也就是项目所属文件夹
- 也可以传绝对路径,就是表示的绝对路径目录
- 如果里面传的是具体文件file就是表示的具体文件,传目录就是表示的目录,file就是用来操作文件和目录的
API
- 创建File相关 - File file = new File(“..”); 构造方法
- file.getAbsolutePath() 返回绝对路径
- file.getPath() 返回创建file时传入构造方法的路径
- file.getCanonicalPath() 返回规范路径
- file.isFile() 是否是文件
- file.isDirectory() 是否是做
- File.separator 可以获取当前系统路径分隔符的表示符号,比如win是 “" ,linux是 “/“
 
- file相关操作:判断 文件/目录 读写权限、创建删除 文件/目录 - file.canRead():是否可读
- file.canWrite():是否可写
- file.canExecute():是否可执行,如果file是目录,canExecute代表就是是否可以列出它包含的文件夹和子目录
- long length():文件字节大小。
- file.exists() 文件是否存在
- file.createNewFile() 创建一个新文件
- file.delete() 删除文件或目录,如果是目录的话,目录下必须为空才会删除
- file.mkdir() 创建目录
- file.mkdirs() 创建当前file对象表示的目录,如果父目录不存在也会把不存在的父目录创建出来
 
- 遍历目录 - 方式一:File[] listFiles = file.listFiles() 直接列出当前目录下的所有东西 
- 方式二:通过new FileFilter()一个过滤器,过滤出自己想要的东西 - 1 
 2
 3
 4
 5
 6
 7- File[] listFiles = file.listFiles(new FileFilter() { 
 
 public boolean accept(File pathname) {
 // 这里只列出目录下所有文件,不列出目录
 return pathname.isFile();
 }
 });
- 方式三:方式二的lambda表达式写法:File[] listFiles1 = file.listFiles(File::isFile); 
 
InputStream使用
基本说明
InputStream就是Java标准库提供的最基本的输入流,要特别注意的一点是,InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。这个抽象类定义的一个最重要的方法就是int read()
| 1 | public abstract int read() throws IOException; | 
这个方法会读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回-1表示不能继续读取了。
使用方法
- 普通读取,一个字节一个字节读取,效率低 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26- /** 
 * InputStream基本使用
 */
 private static void baseInputStream() throws IOException {
 InputStream inputStream = null;
 int n;
 try {
 inputStream = new FileInputStream("d:\\\\test.txt");
 // read正常情况下返回的是读取的字节的int值(0-255),如果读完了就返回-1
 while ( (n = inputStream.read()) != -1 ) {
 System.out.println(n);
 }
 } finally {
 // 记得及时释放资源,不释放资源得话程序就会一直占用着资源
 if (inputStream != null) {
 inputStream.close();
 }
 }
 // 使用try(resources) 这种方式会自动释放资源,也就是编译器会自己添加finally块调用close方法
 try ( InputStream inputStream1 = new FileInputStream("d:\\\\test.txt")) {
 while ( (n = inputStream1.read()) != -1 ) {
 System.out.println(n);
 }
 }
 }
- 使用缓冲区读取 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17- /** 
 * 普通的read方法一次读取一个字节,这样读取效率太低
 * 现在搞一个缓冲区来读取,就是一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节的效率不错
 * InputStream有两个重载方法来支持读取多个字节
 * int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
 * int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数,这个偏移量是针对byte数据来说的
 */
 private static void readFile() throws IOException {
 try (InputStream inputStream = new FileInputStream("d:\\\\test.txt")) {
 // 定义一个1000个字节的缓冲区
 byte[] buffer = new byte[1000];
 int n;
 while ( (n = inputStream.read(buffer)) != -1 ) {
 System.out.println("read " + n + " bytes");
 }
 }
 }
注意事项
- read方法是阻塞的,意思就是必须得等read这个方法执行完之后才能执行后面的代码,因为读取IO流相比执行普通代码,速度会慢很多,因此,无法确定read()方法调用到底要花费多长时间。
- 实际上,InputStream也有缓冲区。例如,从FileInputStream读取一个字节时,操作系统往往会一次性读取若干字节到缓冲区,并维护一个指针指向未读的缓冲区。然后,每次我们调用int read()读取下一个字节时,可以直接返回缓冲区的下一个字节,避免每次读一个字节都导致IO操作。当缓冲区全部读完后继续调用read(),则会触发操作系统的下一次读取并再次填满缓冲区。
- 这个IO流里read和write的重载方法说明【重要】
OutputStream使用
基本说明
和InputStream类似,OutputStream也是抽象类,它是所有输出流的超类。这个抽象类定义的一个最重要的方法就是void write(int b),签名如下:
| 1 | public abstract void write(int b) throws IOException; | 
这个方法会写入一个字节到输出流。要注意的是,虽然传入的是int参数,但只会写入一个字节,即只写入int最低8位表示字节的部分(相当于b & 0xff)。
和InputStream类似,OutputStream也提供了close()方法关闭输出流,以便释放系统资源。要特别注意:OutputStream还提供了一个flush()方法,它的目的是将缓冲区的内容真正输出到目的地。
flush
为什么要有flush()?因为向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个byte[]数组),等到缓冲区写满了,再一次性写入文件或者网络。对于很多IO设备来说,一次写一个字节和一次写1000个字节,花费的时间几乎是完全一样的,所以OutputStream有个flush()方法,能强制把缓冲区内容输出。
通常情况下,我们不需要调用这个flush()方法,因为缓冲区写满了OutputStream会自动调用它,并且,在调用close()方法关闭OutputStream之前,也会自动调用flush()方法。
使用方法
- 普通写入:write(byte[]) / write(int n) / write(byte[], off, len) - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21- /** 
 * OutputStream基础使用
 * @throws IOException IOException
 */
 private static void baseUse() throws IOException {
 OutputStream outputStream = new FileOutputStream("d:\\\\test.txt");
 // 如果test.txt里面有值的话就会把内容覆盖掉!
 // 和read一样,write也有三个方法,都是类似的
 // write(byte[]) / write(int n) / write(byte[], off, len)
 outputStream.write("覆盖内容".getBytes(StandardCharsets.UTF_8));
 // 事实上,如果写入数据得话,会先写入缓冲区,然后攒的差不多了,才会将缓冲区内容真正写进磁盘,做一次磁盘IO
 // flush就是强制将缓冲区中的内容刷进磁盘,不过close方法调用时也会将缓冲区的内容刷进磁盘
 // 其实read的时候也有缓冲区,先读若干个字节到缓冲区,再执行接下来的read方法
 outputStream.flush();
 outputStream.close();
 // 建议使用try(resources)方式来操作,编译器会帮我们在finally块中进行close操作~
 try(OutputStream outputStream1 = new FileOutputStream("d:\\\\test.txt")) {
 outputStream1.write("hello world!".getBytes(StandardCharsets.UTF_8));
 }
 }
注意事项
注意flush方法的使用
FilterStream
这个是干嘛的:当我们需要inputstream具有很多功能的时候,比如需要带缓冲,计算签名功能,我们不能搞出来多个子类继承inputstream然后再操作,所以就有了这个
具体使用
当我们需要给一个“基础”InputStream附加各种功能时,我们先确定这个能提供数据源的InputStream,因为我们需要的数据总得来自某个地方,例如,FileInputStream,数据来源自文件:
| 1 | InputStream file =new FileInputStream("test.gz"); | 
紧接着,我们希望FileInputStream能提供缓冲的功能来提高读取的效率,因此我们用BufferedInputStream包装这个InputStream,得到的包装类型是BufferedInputStream,但它仍然被视为一个InputStream:
| 1 | InputStream buffered =new BufferedInputStream(file); | 
最后,假设该文件已经用gzip压缩了,我们希望直接读取解压缩的内容,就可以再包装一个GZIPInputStream:
| 1 | InputStream gzip =new GZIPInputStream(buffered); | 
无论我们包装多少次,得到的对象始终是InputStream,我们直接用InputStream来引用它,就可以正常读取:
| 1 | ┌─────────────────────────┐ | 
上述这种通过一个“基础”组件再叠加各种“附加”功能组件的模式,称之为Filter模式(或者装饰器模式:Decorator)。它可以让我们通过少量的类来实现各种功能的组合,类似的,OutputStream也是以这种模式来提供各种功能:
自己编写一个定制化的filterstream
只需要继承FilterInputStream,即可,关键就是构造方法传入的是inputstream,所以可以进行各种包装(装饰)
操作zip
基本说明
明白一个概念,zipEntry,可以看作是一个zip包中的具体文件,也可以看作是目录,但是entry并不存储数据,读取数据或写入数据还是操作的ZipInputStream或ZipOutputStream对象
ZipInputStream和ZipOutputStream都是filterstream
| 1 | ┌───────────────────┐ | 
具体使用
- 创建zip文件 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39- /** 
 * 将一个目录中的文件打包
 */
 private static void createZip() throws IOException {
 // 这里构造方法填的是目标目录,就是压缩包放的地方
 try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream("d:\\\\test.zip"))) {
 // 获取要打包的目录
 File dir = new File("d:\\\\test");
 File[] files = dir.listFiles();
 if (files != null) {
 for (File file : files) {
 // 放入一个实体,new ZipEntry中如果传名字就是默认打包到zip的根目录下,若传相对路径,就打包到对应相对路径下,zip包是根目录
 zip.putNextEntry(new ZipEntry(file.getName()));
 zip.write(getFileDataAsBytes(file));
 zip.closeEntry();
 }
 }
 }
 }
 /**
 * 读取文件中的字节流
 * @param file File对象
 * @return byte[]
 * @throws IOException IOException
 */
 private static byte[] getFileDataAsBytes(File file) throws IOException {
 byte[] bytes = new byte[1024];
 int length = 0;
 // 神器,可以临时搞一个输出流存字节数据
 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
 try(InputStream inputStream = new FileInputStream(file)) {
 while ( (length = inputStream.read(bytes)) != -1 ) {
 // 只写入读取了的长度,因为可能读不满缓冲池,会存在空字节
 byteArrayOutputStream.write(bytes, 0, length);
 }
 }
 return byteArrayOutputStream.toByteArray();
 }
- 读取zip文件 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24- /** 
 * 读取zip包中的文件数据
 * @throws IOException IOException
 */
 private static void readZip() throws IOException {
 try(ZipInputStream zip = new ZipInputStream(new FileInputStream("d:\\\\test.zip"))) {
 ZipEntry entry;
 // 缓冲区读
 byte[] cache = new byte[1024];
 // 存储读取出来的数据
 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
 while ( (entry = zip.getNextEntry()) != null) {
 System.out.println("entryName: " + entry.getName());
 if (!entry.isDirectory()) {
 int n;
 while ((n = zip.read(cache)) != -1) {
 System.out.println("read " + n + " bytes");
 byteArrayOutputStream.write(cache, 0, n);
 }
 }
 }
 System.out.println(byteArrayOutputStream);
 }
 }
读取ClassPath资源
基本说明
classPath就是编译后的classes文件,相对路径就是相对于classes文件夹来说的,classes文件夹就是classPath的根目录 “/“ ,所以从这里获取文件也是有特殊的方法的,一行代码
| 1 | InputStream resourceAsStream = ClassPathDemo.class.getResourceAsStream("/default.properties") | 
使用方法
- 基本使用 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24- /** 
 * classPath文件基本读取
 */
 private static void baseUse() throws IOException {
 try (InputStream resourceAsStream = ClassPathDemo.class.getResourceAsStream("/default.properties")) {
 Properties properties = new Properties();
 properties.load(resourceAsStream);
 System.out.println(properties.getProperty("name"));
 System.out.println(properties.getProperty("age"));
 System.out.println(properties.getProperty("gender"));
 // 使用过这个输入流之后,输入流resourceAsStream就是空了,下面再用这个流的话还需要重新获取,因为输入流只能用一次
 byte[] cache = new byte[1024];
 int length = 0;
 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
 // 一定要判空
 if (resourceAsStream != null) {
 while ( (length = resourceAsStream.read(cache)) != -1) {
 // 只往输出流中写入读取的字节长度,因为cache存在空字节问题
 byteArrayOutputStream.write(cache, 0, length);
 }
 }
 System.out.println(byteArrayOutputStream);
 }
 }
- 从classPath获取,从外部获取;场景:jar包里打进去默认的配置文件,也就是classPath中的,然后还有一个方法是可以获取外部配置文件,给用户自定义配置 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30- // 从classPath获取和从外部文件获取 
 Properties props = new Properties();
 props.load(inputStreamFromClassPath("/default.properties"));
 props.load(inputStreamFromFile(".conf.properties"));
 /**
 * 从classpath中获取配置文件
 * @param filePath 文件相对路径
 * @return InputStream
 */
 private static InputStream inputStreamFromClassPath(String filePath) {
 return ClassPathDemo.class.getResourceAsStream(filePath);
 }
 /**
 * 从外部文件获取配置文件
 * @param filePath 文件路径:相对/绝对
 * @return InputStream
 * @throws FileNotFoundException FileNotFoundException
 */
 private static InputStream inputStreamFromFile(String filePath) throws IOException {
 File file = new File(filePath);
 // 如果文件不存在就新建
 if (!file.exists()) {
 System.out.println("file " + filePath + " is not exists!");
 boolean success = file.createNewFile();
 System.out.println(success ? "文件创建成功!" : "文件创建失败!");
 }
 return new FileInputStream(file);
 }