IO简介

IO是指Input/Output,即输入和输出。以内存为中心:

  • Input指从外部读入数据到内存,例如,把文件从磁盘读取到内存,从网络读取数据到内存等等。
  • Output指把数据从内存输出到外部,例如,把数据从内存写入到文件,把数据从内存输出到网络等等

IO流是一种顺序读写数据的模式,它的特点是单向流动

同步和异步

  • 同步IO是指,读写IO时代码必须等待数据返回后才继续执行后续代码,它的优点是代码编写简单,缺点是CPU执行效率低。如read()方法必须等返回结果后才会再次执行
    • 方法同步也称之为阻塞
  • 而异步IO是指,读写IO时仅发出请求,然后立刻执行后续代码,它的优点是CPU执行效率高,缺点是代码编写复杂。

Java标准库的包java.io提供了同步IO,而java.nio则是异步IO。上面我们讨论的InputStreamOutputStreamReaderWriter都是同步IO的抽象类,对应的具体实现类,以文件为例,有FileInputStreamFileOutputStreamFileReaderFileWriter

IOException

IO异常可能是流找不到文件(即可能会出现文件存在但是因为某些原因读取不到的情况)等等

  • 需要注意的一点是:输入流可以正常读取的情况下,如果还没有读取完就关闭(read还没返回-1),是会产生IO异常的

    字节与字符

  • 字节是内存占用空间,字符是可以看到的占用空间

  • IO流中字节默认都是Ascii编码。string默认编码是utf-8

    • ASCII 码中,一个英文字母(不分大小写)为一个字节,一个中文汉字为两个字节。
    • Unicode 编码中,一个英文为一个字节,一个中文为两个字节。
    • UTF-8 编码中,一个英文字为一个字节,一个中文为三个字节。

      IO结构图

      image.png

      Hutool中的io工具类

      ```xml

    cn.hutool hutool-all 5.4.5 test

    1. ```java
    2. FileReader fileReader = new FileReader("public/index.html"); //会从classpath下查找文件,开头不要加/ //默认UTF-8编码,可以在构造中传入第二个参数做为编码
    3. String result = fileReader.readString(); //获取整个文件内容
    4. List<String> res=fileReader.readLines(); //按行读取

    File

  • 要注意的是,构造一个**File**对象,存储的仅仅只是些基本信息,并没有创建流。所以即使传入的文件或目录不存在,代码也不会出错。即File没有保存文件内容数据(一般用于构造流,file作为流的数据来源信息,当然也可以直接传入字符串路径)

    • 用于创建文件/文件夹对象,存储文件路径与文件名信息
  • isFile()判断该File对象是否是一个已存在的文件
  • isDirectory()判断该File对象是否是一个已存在的目录
  • getAbsolutePath()获取文件路径
  • 文件信息判断:

    • boolean canRead():是否可读;
    • boolean canWrite():是否可写;
    • boolean canExecute():是否可执行;对目录而言,是否可执行表示能否列出它包含的文件和子目录。
    • long length():文件字节大小。

      路径表示

  • 创建对象时路径可以传绝对路径或者相对路径

    • 绝对路径File f = new File("C:\\Windows\\notepad.exe");

windows平台使用\作为路径分隔符,在Java字符串中需要用\\或者/。Linux平台使用/
java使用2个\理解为转义字符就好记了

  • 相对路径 可以用.表示当前目录,..表示上级目录。相对路径前面加上当前目录就是绝对路径

路径方法:
getPath(),返回构造方法传入的路径
getAbsolutePath(),返回绝对路径
getCanonicalPath,它和绝对路径类似,但是返回的是规范路径。

文件的创建与删除

  • 创建文件:createNewFile()
  • 创建临时文件:createTempFile()
    • JVM退出时自动删除临时文件deleteOnExit()
  • 删除文件:delete()

    File f = File.createTempFile("tmp-", ".txt"); // 提供临时文件的前缀和后缀
          f.deleteOnExit(); // JVM退出时自动删除
    

    目录的创建和删除

    boolean mkdir():创建当前File对象表示的目录;
    boolean mkdirs():创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;
    boolean delete():删除当前File对象表示的目录,当前目录必须为空才能删除成功。

    遍历文件和目录

  • **list()**返回一个目录下所有文件的文件名信息 返回值String

  • **listFiles()**返回一个目录下所有文件和目录的完整路径信息 返回值为File类型数组
    • 该方法还有很多重载方法可以提供过滤功能。使用过滤功能必须传入FilenameFilter()接口的实现类对象并实现接口方法**accept()**
      • FilenameFilter:文件名过滤器
      • accept(File file,String name) 将file对象中的文件名name逐个提取出来进行比对过滤
        • endswith(String str):判断字符串是否以给定字符串结尾
      • FilenameFilter()是单方法接口。所以可以借助匿名内部类或者Lambda简化
  • 注意list只返回目录下的文件!

    public static void main(String[] args) throws IOException {
          File f = new File("C:\\Windows");
          File[] fs1 = f.listFiles(); // 列出所有文件和子目录
          printFiles(fs1);
          File[] fs2 = f.listFiles(new FilenameFilter() { // 仅列出.exe文件   匿名内部类
              public boolean accept(File dir, String name) {
                  return name.endsWith(".exe"); // 返回true表示接受该文件
              }
          });
          printFiles(fs2);
      }
    
      static void printFiles(File[] files) {
          System.out.println("==========");
          if (files != null) {   //避免null文件对象进行操作引起错误
              for (File f : files) {  //将File数组内的File对象逐个提取出来删除
                  System.out.println(f);
              }
          }
          System.out.println("==========");
      }
    

    Path

  • Path既是一个类,也是一个接口。它跟File功能类似,但是操作更加简单

    • 如果需要对目录进行复杂的拼接、遍历等操作,使用Path对象更方便。
  • Path因为是一个接口,所以构建对象不能new得借助Path.get() toPath() 等方法 ```java public class Main { public static void main(String[] args) throws IOException {
      Path p1 = Paths.get(".", "project", "study"); // 构造一个Path对象
      System.out.println(p1);
      Path p2 = p1.toAbsolutePath(); // 转换为绝对路径
      System.out.println(p2);
      Path p3 = p2.normalize(); // 转换为规范路径
      System.out.println(p3);
      File f = p3.toFile(); // 转换为File对象
      System.out.println(f);
      for (Path p : Paths.get("..").toAbsolutePath()) { // 可以直接遍历Path
          System.out.println("  " + p);
      }
    
    } }
<a name="JacJT"></a>
# Files
见廖雪峰
<a name="JM2Le"></a>
# IO流通用方法

- 输入输出流通用方法:`close()`   关闭流 
   - 可借助try(resource)来自动关闭close()
- 输入流通用方法:`read()`   读取字节或者字符
   - `read(byte/char[])`  缓冲输入
- 输出流通用方法:
   - `write()`  写入字节或者字符
   - `flush()`  强制输出缓冲区   
      - **IO缓冲区本质上都是一个数组**
      - **一般不需要使用,缓冲区满了输入流会自动调用。另外**`**close()**`**执行时也会自动调用**`**flush()**`
         - **在一些聊天应用中会主动调用flush(),因为一般输入完一句话还没有达到缓冲区上限,也就不会将消息发送出去,这时就需要主动清空缓冲区让消息发送**
<a name="3fh7c"></a>
# InputStream字节输入流

- `InputStream`就是Java标准库提供的最基本的输入流。
- `**InputStream**`**并不是一个接口,而是一个抽象类,它是所有输入流的超类。**

---

- 字节输入流定义的一个最重要的方法就是`read`方法:`public abstract ``**int**`` read() throws IOException;`
- `int available()`   获取输入流来源的总长度
   - **一般用于读取磁盘文件,对于网络数据可能不适合,因为网络数据一般有缓冲区,读的可能只是抵达缓冲区的数据就结束了**
- `read(byte[])`    读取字节数组长度的数据,并额外保存到数组中,即流和数组都有数据了
- **类很多记不住?其实文件输入流,字节数组输入流等等就是数据来源的区别,使用方法还是差不多的。输出流同**
<a name="nMzHP"></a>
## FileInputStream文件输入流

- `FileInputStream`是`InputStream`的一个实现类。顾名思义,`FileInputStream`就是从文件流中读取数据。
```java
  public static void main(String[] args) {
        int n;
        try {
            File file =new File("C:\\Users\\Lw\\Desktop\\lhy.cpp");
            // 构造一个输入流,数据来源为file,此时还为null流
            InputStream input = new FileInputStream(file);  
         while ((n = input.read()) != -1){
                System.out.println(n); // 打印byte的值
                //要输出字符可以(char)n
            input.close(); // 关闭流
        }
        }catch (IOException e) {
            System.out.println("文件不存在");
        }
}
也可以把close写在finally里面 
finally {
        if (input != null)  input.close(); 
        }

ByteArrayInputStream

  • 输入源是数组的字节输入流,可以在内存中模拟一个InputStream
  • 实际上一般不会用输入流输出数组,更多的是用于将文件转为数组或者字符串
    public class Main {
      public static void main(String[] args) throws IOException {
          String s;
          try (InputStream input = new FileInputStream("C:\\test\\README.txt")) {
              int n;
              StringBuilder sb = new StringBuilder();
              while ((n = input.read()) != -1) {
                  sb.append((char) n);
              }
              s = sb.toString();
          }
          System.out.println(s);
      }
    }
    

缓冲输入

  • 只有输入流才有缓冲

在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。InputStream提供了两个重载方法来支持读取多个字节。Reader也一样的用法,只不过字节换成了字符

  • int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
  • int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数

利用上述方法一次读取多个字节时,需要先定义一个byte[]数组作为缓冲区,read()方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。read()方法的返回值不再是字节的int值,而是返回实际读取了多少个字节。如果返回-1,表示没有更多的数据

public void readFile() throws IOException {
    try (InputStream input = new FileInputStream("src/readme.txt")) {
        // 定义1000个字节大小的缓冲区:
        byte[] buffer = new byte[1000];
        int n;
        while ((n = input.read(buffer)) != -1) { // 读取到缓冲区
            System.out.println("read " + n + " bytes.");
        }
    }
}
public class Main {
    public static void main(String[] args) throws IOException {
        String s;
        try (InputStream input = new FileInputStream("C:\\test\\README.txt")) {
            s = readAsString(input);
        }
        System.out.println(s);
    }
    public static String readAsString(InputStream input) throws IOException {
        int n;
        StringBuilder sb = new StringBuilder();
        while ((n = input.read()) != -1) {
            sb.append((char) n);   /*将字节逐个读取并转为char类型,然后追加到可变字符串sb后,然后就可以
            组合为一个字符串  */
        }
        return sb.toString();
    }
}

OutputStream字节输出流

  • 跟InputStream差不多的结构与定义,也是一个抽象类。
  • 最重要的一个方法是

    • public abstract ``**void**`` write(int b) throws IOException;
      • 虽然传入的是int参数,但只会写入一个字节 即写入的是int值对应的字节表示
    • write的重载方法write(byte[])方法更加方便的输出文件。可以使用Byte[] getByte(String 编码方式)方法将字符串转为字节数组
      • 如果是文件上传时输出到本地,可以使用**MultipartFile**的方法**getBytes()**,将上传文件转为字节数组
      • 一般使用utf-8的编码方式
    • write()也是阻塞的

      FileOuputStream文件输出流

  • 是OutputStream的一个实现类。即写入对象是文件的输出流

    OutputStream output = new FileOutputStream("C:\\Users\\Lw\\Desktop\\12.txt");
          output.write(72); // H    ascii编码
          output.close();
          }
    
  • 将字符串转为数组然后写入

    OutputStream output = new FileOutputStream("out/readme.txt");
      output.write("Hello".getBytes("UTF-8")); // Hello
      output.close();
    

    ByteArrayOutputStream

  • 即写入对象是字节数组的输出流。可以通过toByteArray()将字节数组输出流对象转变为字节数组

    public class Main {
      public static void main(String[] args) throws IOException {
          byte[] data;
          try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
              output.write("Hello ".getBytes("UTF-8"));
              output.write("world!".getBytes("UTF-8"));
              data = output.toByteArray();
          }
          System.out.println(new String(data, "UTF-8"));
      }
    }
    

    transferTo

  • 该方法可以将输入流复制给一个输出流,从而实现同时读和写

    • 此方法使用时不建议手动关闭流,所以借助try(resource)自动关闭
      // 读取input.txt,写入output.txt:
      try (InputStream input = new FileInputStream("input.txt");
      OutputStream output = new FileOutputStream("output.txt"))
      {
      input.transferTo(output); 
      }
      

      Reader字符输入流

  • 用法和IntPutStream差不多

  • Reader是Java的IO库提供的另一个输入流接口。和InputStream的区别是,InputStream是一个字节流,即以byte为单位读取,而Reader是一个字符流,即以char为单位读取 它是所有字符类的超类,
  • 最主要的方法public ``**int**`` read() throws IOException;读取字符流的下一个字符,并返回字符表示的int,范围是0~65535。如果已读到末尾,返回-1。
    • public int read(char[] c) throws IOException 读取若干字符并存入数组

StringReader

可以直接把String作为数据源进行读取
Reader reader = new StringReader("Hello");

FileReader

  • FileReaderReader的一个子类,它的数据源为文件
  • 如果我们读取一个纯ASCII编码的文本文件,上述代码工作是没有问题的。但如果文件中包含中文,就会出现乱码,因为FileReader默认的编码与系统相关。要避免乱码问题,我们需要在创建FileReader时指定编码:

    • Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8);
      public void readFile() throws IOException {
      // 创建一个FileReader对象:
      Reader reader = new FileReader("src/readme.txt"); // 字符编码是???
      for (;;) {
         int n = reader.read(); // 反复调用read()方法,直到返回-1
         if (n == -1) {
             break;
         }
         System.out.println((char)n); // 打印char
      }
      reader.close(); // 关闭流
      }
      

      CharArrayReader

  • ByteArrayInputStream 就是数据源是字符数组的字节输入流

    try (Reader reader = new CharArrayReader("Hello".toCharArray())) {
    }
    

    InputStreamReader字节流转字符流

  • 除了特殊的CharArrayReaderStringReader,普通的Reader实际上是基于InputStream构造的,因为Reader需要从InputStream中读入字节流(byte),然后,根据编码设置,再转换为char就可以实现字符流。如果我们查看FileReader的源码,它在内部实际上持有一个FileInputStream

  • 既然Reader本质上是一个基于InputStreambytechar的转换器,那么,如果我们已经有一个InputStream,想把它转换为Reader,是完全可行的。InputStreamReader就是这样一个转换器,它可以把任何InputStream转换为Reader。示例代码如下:

    InputStream input = new FileInputStream("src/readme.txt");
    // 变换为Reader:
    Reader reader = new InputStreamReader(input, "UTF-8");
    
  • 构造InputStreamReader时,我们需要传入InputStream,还需要指定编码,就可以得到一个Reader对象。上述代码可以通过try (resource)更简洁地改写如下:

    try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), "UTF-8")) {
      // TODO:
    }
    
  • 上述代码实际上就是FileReader的一种实现方式。

  • 使用try (resource)结构时,当我们关闭Reader时,它会在内部自动调用InputStreamclose()方法,所以,只需要关闭最外层的Reader对象即可。
  • 使用InputStreamReader,可以把一个InputStream转换成一个Reader。

    Write

    Writer是所有字符输出流的超类,它提供的方法主要有:

    • 写入一个字符(0~65535):void write(int c)
    • 写入字符数组的所有字符:void write(char[] c)
    • 写入String表示的所有字符:void write(String s)
  • Write用法跟OutputStream类似

    FileWrite

    FileWriter就是向文件中写入字符流的Writer。它的使用方法和FileReader类似:

    try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
      writer.write('H'); // 写入单个字符
      writer.write("Hello".toCharArray()); // 写入char[]
      writer.write("Hello"); // 写入String
    }
    

    StringWriter

  • 用法见StringReader

    OutputStreamWriter

    除了CharArrayWriterStringWriter外,普通的Writer实际上是基于OutputStream构造的,它接收char,然后在内部自动转换成一个或多个byte,并写入OutputStream。因此,OutputStreamWriter就是一个将任意的OutputStream转换为Writer的转换器:

    try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) { //... }
    

    PrintStream和PrintWriter

    https://www.liaoxuefeng.com/wiki/1252599548343744/1302299230076961

    序列化

  • 序列化即把java对象信息转变为二进制内容(byte[]) . 这样以达到持久化对象的实现 显而易见需要使用ByteArrayOutputStream

    • 一个对象能否进行序列化需要看它的类是否实现了**java.io.Serializable** 该接口未定义任何方法,仅仅是给实现类贴上一个“可以序列化”的标签
  • 反序列化即相反
  • 除二进制以外还有很多其他类型的序列化,如xml,json。虽然 JSON 和 XML 可读性比较好,但是性能较差

    • Java的序列化机制仅适用于Java,如果需要与其它语言交换数据,必须使用通用的序列化方法,例如JSON

      序列化

  • 除了字节数组输出流,还需要用到对象输出流ObjectOutputStream 字节数组输出流作为对象输出流的参数,对象输出流会将序列化后的信息写入到字节数组输出流中

  • ObjectOutputStream可以写入基本类型和String还有实现了Serializable接口的Object
    • 序列化和反序列化的String都应该使用UTF-8
  • 可以使用**transient**对变量修饰,这样该变量不会被序列化(static教程上说有无transient都不会被序列化,实践是有无都会被序列化)

    • transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。如int变为默认值0

      反序列化

  • 反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行。

  • read?() 从对象输出流中读取一个?类型的对象。String类型对象的读取方法为readUTF()
    • 还可以直接读取一个Object对象,强制转换后进行使用。可能抛出以下异常
      • ClassNotFoundException:没有找到对应的Class;
      • InvalidClassException:Class不匹配。
        • 对于ClassNotFoundException,这种情况常见于一台电脑上的Java程序把一个Java对象,例如,Person对象序列化以后,通过网络传给另一台电脑上的另一个Java程序,但是这台电脑的Java程序并没有定义Person类,所以无法反序列化。
        • 对于InvalidClassException,这种情况常见于序列化的Person对象定义了一个int类型的age字段,但是反序列化时,Person类定义的age字段被改成了long类型,所以导致class不兼容。
  • 为了避免这种class定义变动导致的不兼容,Java的序列化允许class定义一个特殊的serialVersionUID静态变量,用于标识Java类的序列化“版本”,通常可以由IDE自动生成。如果增加或修改了字段,可以改变serialVersionUID的值,这样就能自动阻止不匹配的class版本。即相同的类才具有相同的版本号

    • 序列化号 serialVersionUID 属于版本控制的作用。序列化的时候 serialVersionUID 也会被写入二级制序列,当反序列化时会检查 serialVersionUID 是否和当前类的 serialVersionUID 一致。如果 serialVersionUID 不一致则会抛出 InvalidClassException 异常。强烈推荐每个序列化类都手动指定其 serialVersionUID,如果不手动指定,那么编译器会动态生成默认的序列化号
      public class Person implements Serializable {     private static final long serialVersionUID = 2709425275741743919L; }
      

      实操

      //-----------------------序列化----------
      ByteArrayOutputStream buffer = new ByteArrayOutputStream();
      //ObjectOutputStream用于把对象转为字节,下面是转为数组并写入到字节数组输出流,字节数组输出流再获取字节数组
      ObjectOutputStream output = new ObjectOutputStream(buffer);
      int n = input.readInt();
      String s = input.readUTF();
      Double d = (Double) input.readObject();
      byte[] res=buffer.toByteArray();
      //-----------------------反序列化------------
      //ObjectInputStream用于字节转对象
      ObjectInputStream input = new ObjectInputStream(new ByteArrayInputStream(res));
      //注意反序列取得顺序要和存得顺序一致,不能跳过某个元素取
                 int n = input.readInt();
                 String s = input.readUTF();
                 Double d = (Double) input.readObject();
      

      第三方二进制序列化

  • 上述的二进制序列化是jdk自身功能的实现,但是存在2个问题:

    • 不支持跨语言调用
    • 性能差

常用的第三方的二进制反/序列化有如下框架:

  • Kryo
  • Protobuf
  • ProtoStuff:基于Protobuf,但是提供了更多的功能和更简易的用法。虽然更加易用,但是不代表 ProtoStuff 性能更差
  • hessian :dubbo RPC 默认启用的序列化方式是 hessian2 ,但是,Dubbo 对 hessian2 。hessian 2类似hessian 。不过Dubbo是推荐使用Kryo 替代hessian2