IO流

Java中的IO流概念介绍及使用方法,此IO只针对于磁盘IO,没有网络IO相关知识

概述

IO就是input/output,这个是相对于内存而言的;

  1. iniput就是往内存里面放数据,数据从哪里来的呢?可以是本地磁盘,也可以是从网络获取的数据
  2. 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对象可以表示目录,也可以表示文件

路径说明:

  1. 传一个 “.” 代表的是当前目录,当前目录就是你的java项目的目录
  2. 传一个 “/“ 或 “\“ 就是表示的java项目所在磁盘的根目录
  3. 传一个 “..” 就是代表着上一级目录,也就是项目所属文件夹
  4. 也可以传绝对路径,就是表示的绝对路径目录
  5. 如果里面传的是具体文件file就是表示的具体文件,传目录就是表示的目录,file就是用来操作文件和目录的

API

  • 创建File相关

    1. File file = new File(“..”); 构造方法
    2. file.getAbsolutePath() 返回绝对路径
    3. file.getPath() 返回创建file时传入构造方法的路径
    4. file.getCanonicalPath() 返回规范路径
    5. file.isFile() 是否是文件
    6. file.isDirectory() 是否是做
    7. File.separator 可以获取当前系统路径分隔符的表示符号,比如win是 “" ,linux是 “/“
  • file相关操作:判断 文件/目录 读写权限、创建删除 文件/目录

    1. file.canRead():是否可读
    2. file.canWrite():是否可写
    3. file.canExecute():是否可执行,如果file是目录,canExecute代表就是是否可以列出它包含的文件夹和子目录
    4. long length():文件字节大小。
    5. file.exists() 文件是否存在
    6. file.createNewFile() 创建一个新文件
    7. file.delete() 删除文件或目录,如果是目录的话,目录下必须为空才会删除
    8. file.mkdir() 创建目录
    9. file.mkdirs() 创建当前file对象表示的目录,如果父目录不存在也会把不存在的父目录创建出来
  • 遍历目录

    1. 方式一:File[] listFiles = file.listFiles() 直接列出当前目录下的所有东西

    2. 方式二:通过new FileFilter()一个过滤器,过滤出自己想要的东西

      1
      2
      3
      4
      5
      6
      7
      File[] listFiles = file.listFiles(new FileFilter() {
      @Override
      public boolean accept(File pathname) {
      // 这里只列出目录下所有文件,不列出目录
      return pathname.isFile();
      }
      });
    3. 方式三:方式二的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");
    }
    }
    }

注意事项

  1. read方法是阻塞的,意思就是必须得等read这个方法执行完之后才能执行后面的代码,因为读取IO流相比执行普通代码,速度会慢很多,因此,无法确定read()方法调用到底要花费多长时间。
  2. 实际上,InputStream也有缓冲区。例如,从FileInputStream读取一个字节时,操作系统往往会一次性读取若干字节到缓冲区,并维护一个指针指向未读的缓冲区。然后,每次我们调用int read()读取下一个字节时,可以直接返回缓冲区的下一个字节,避免每次读一个字节都导致IO操作。当缓冲区全部读完后继续调用read(),则会触发操作系统的下一次读取并再次填满缓冲区。
  3. 这个IO流里read和write的重载方法说明【重要】

教你完全理解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
2
3
4
5
6
7
8
9
┌─────────────────────────┐
│GZIPInputStream │
│┌───────────────────────┐│
││BufferedFileInputStream││
││┌─────────────────────┐││
│││ FileInputStream │││
││└─────────────────────┘││
│└───────────────────────┘│
└─────────────────────────┘

上述这种通过一个“基础”组件再叠加各种“附加”功能组件的模式,称之为Filter模式(或者装饰器模式:Decorator)。它可以让我们通过少量的类来实现各种功能的组合,类似的,OutputStream也是以这种模式来提供各种功能:

自己编写一个定制化的filterstream

只需要继承FilterInputStream,即可,关键就是构造方法传入的是inputstream,所以可以进行各种包装(装饰)

操作zip

基本说明

明白一个概念,zipEntry,可以看作是一个zip包中的具体文件,也可以看作是目录,但是entry并不存储数据,读取数据或写入数据还是操作的ZipInputStream或ZipOutputStream对象

ZipInputStream和ZipOutputStream都是filterstream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌───────────────────┐
│ InputStream │
└───────────────────┘


┌───────────────────┐
│ FilterInputStream │
└───────────────────┘


┌───────────────────┐
│InflaterInputStream│
└───────────────────┘


┌───────────────────┐
│ ZipInputStream │
└───────────────────┘


┌───────────────────┐
│ JarInputStream │
└───────────────────┘

具体使用

  • 创建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);
    }