侧边栏壁纸
博主头像
liveJQ博主等级

沒有乐趣,何来开始

  • 累计撰写 146 篇文章
  • 累计创建 60 个标签
  • 累计收到 2 条评论

IO流

liveJQ
2019-09-06 / 0 评论 / 0 点赞 / 606 阅读 / 19,529 字 / 正在检测是否收录...
广告 广告

Java IO

体系结构

IO实际上分为阻塞型IO(Blocking IO)和非阻塞型IO(Non-Blocking IO 简称NIO)

阻塞型IO在读取数据时,如果数据未到达,会一直阻塞直到读取到数据为止,所以称为阻塞型IO,在高并发的环境下性能不佳。

NIO不是使用 “流” 来作为数据的传输方式,而是使用通道,通道的数据传输是双向的,而且NIO的数据写入和读取都是异步的,能读/写多少就读/写多少,不会阻塞线程,所以称为非阻塞型IO,在高并发的环境下性能很好。

本章主要分析阻塞型IO

分门别类

20190906_io1.png

20190906_io2.png

注意:在JDK8,InputStream淘汰了StringBufferInputStream和FilterInputStream中的LineNumberInputStream

  1. 按照数据流的方向不同可以分为:输入流和输出流。
  2. 按照处理数据单位不同可以分为:字节流和字符流。
  3. 按照实现功能不同可以分为:节点流和处理流。
  • 输入输出是相对于“当前程序”来说的,输入要有“源”(本地文件/终端输入/网络传输 -> 程序),输出要有“目的地”(程序数据 -> 本地文件/终端输出/网络传输)。
  • 字节流一次读取一个字节的数据,而字符流一次读取两个字节的数据。字符流实际上等于字节流+查询相应编码(UTF-8/GBK/gb2312等),主要是为了方便处理文字数据;而字节流可以处理任何数据(因为avi/mp4视频格式、png/jpg图片等都是转化为二进制存储在计算机中的)。
  • 处理流是为了更好的处理节点流而存在的,例如功能介绍中的BufferedReader代码块:其中的InputStreamReader和BufferedReader就是处理流,而FileInputStream是节点流。“更好地处理”体现为:InputStreamReader是连接字节流和字符流的桥梁,采用UTF-8字符集将文件输入流中的字节转为字符;而BufferedReader可以形成缓冲区,一次性读取到缓冲区后再刷出,提高了处理效率,只是最后别忘了flush刷出缓存数据。

上面提到的代码块中值得注意的地方就是其关闭流的方式。一般我们常用的关闭流的方式:先打开的后关闭;而当存在处理流时,直接关闭最外层的处理流即可。由于装饰器模式的缘故,其内部的close方法直接将与其相关的流通通关闭掉了,需要注意的是,关闭流得包含在finally语句中,避免因发生异常而无法正常关闭资源。(若打开的资源较多时可以使用线程中的Hook来执行资源的回收任务,这里使用了JDK 7中提供的try-with-resources,本站有提供相关说明)。

功能展示

20190906_io3.png

采用装饰器的基本上都是处理流了

字符流

BufferedReader

  • 实例一:
    File file = new File("README.md");
    try (BufferedReader in =
        new BufferedReader(
            new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
      String line;
      while ((line = in.readLine()) != null) {
        System.out.println(line);
      }
    } catch (Exception e) {
      e.printStackTrace();
    }

// 与上面功能相同
File file = new File("README.md");
    Charset charset = Charset.forName("utf8");
    // 解码器
    CharsetDecoder decoder = charset.newDecoder();
    try (BufferedReader in =
        new BufferedReader(new InputStreamReader(new FileInputStream(file), decoder))) {
      String line;
      while ((line = in.readLine()) != null) {
        System.out.println(line);
      }
    } catch (Exception e) {
      e.printStackTrace();
    }

这三个流经常这样搭配着使用。由上文所述,InputStreamReader是字节流到字符流的桥梁,那么OutputStreamWriter就是字符流到字节流的桥梁了,因用法相同,不再赘述。

BufferedWriter

  • 实例一:
    File file = new File("temp/test.txt");
    try (BufferedWriter out =
        new BufferedWriter(
            new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
      int count = 10;
      int index = 0;
      while (++index != count) {
        System.out.println("正在写入第 " + index + " 行");
        out.write(index + " " + "测试行测试行.......");
        out.newLine();
      }
    } catch (Exception e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }

LineNumberReader

当传输的文本信息中包含某个错误时,向用户反馈错误时指出错误所在行号则更容易找到错误。

  • 实例一:
try (LineNumberReader lineNumberReader =
        new LineNumberReader(new FileReader("temp/test.txt"))) {

      int data = lineNumberReader.read();
      StringBuilder sb = new StringBuilder();
      int index = -1;
      while (data != -1) {
        char dataChar = (char) data;
        sb.append(dataChar);
        // 从0开始,对应文件中的第1行
        int tempIndex = lineNumberReader.getLineNumber();
        if (index != tempIndex) {
          System.out.println(tempIndex);
          index = tempIndex;
        }
        data = lineNumberReader.read();
      }
      System.out.println(new String(sb));
    } catch (IOException e) {
      e.printStackTrace();
    }

身为BufferedReader的子类,上面同样可以使用readLine()读取一整行数据。

  • 实例二:
try (LineNumberReader lineNumberReader =
        new LineNumberReader(new FileReader("temp/test.txt"))) {

      String data = lineNumberReader.readLine();
      StringBuilder sb = new StringBuilder();
      while (data != null) {
        sb.append(data).append(System.lineSeparator());
        System.out.println(lineNumberReader.getLineNumber());
        data = lineNumberReader.readLine();
      }
      System.out.println(sb);
    } catch (IOException e) {
      e.printStackTrace();
    }

PrintWriter

  • 实例一:
try (PrintWriter printWriter = 
                 new PrintWriter(new FileWriter("temp/test.txt"))) {
      printWriter.println(true);
      printWriter.println(123);
      printWriter.println((float) 123.456);
      printWriter.printf(Locale.CHINA, "data: %d$", 123);
    } catch (IOException e) {
      e.printStackTrace();
    }

文件内容:

true
123
123.456
data: 123$

PrintWriter其实也是Decorator,其中包含了BufferedWriter和OutputStreamWriter,若写出到文件,则还使用了FileOutputStream。除了集成上面这些类的功能外,其另一强大之处在于包含了许多format方法和printf,可以很容易地对数据进行格式化。

FileReader

  • 实例一:
// 上面的 BufferedReader 实例可以简写成
File file = new File("README.md");
    try (BufferedReader in = new BufferedReader(new FileReader(file))) {;
      String line;
      while ((line = in.readLine()) != null) {
        System.out.println(line);
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  • 实例二:
FileReader fileReader = null;
    try {
      fileReader = new FileReader(new File("README.md"));
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    }
    char[] destination = new char[1024];
    int charsRead = 0;
    try {
      charsRead = fileReader.read(destination, 0, destination.length);
    } catch (NullPointerException | IOException e) {
      e.printStackTrace();
    }
    try {
      while (charsRead != -1) {
        System.out.println(new String(destination));
        // 会尝试读取指定长度的字符数据到指定的字符数组中
        // 如果文件中的字符数少于指定的字符数,则读取的字符数将少于指定的字符数
        charsRead = fileReader.read(destination, 0, destination.length);
      }
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      if (fileReader != null) {
        try {
          fileReader.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }

由上面可知,FileReader实际上是InputStreamReader与FileInputStream的组合。

StringReader

  • 实例一:
String str = "中文";
    try (StringReader stringReader = new StringReader(str)) {

      int data = 0;
      try {
        // 一个字符一个字符的读取,也就是一次读取两个字节
        data = stringReader.read();
      } catch (IOException e) {
        e.printStackTrace();
      }
      while (data != -1) {
        char c = (char) data;
        System.out.println("编码值:" + data + ", 字符:" + c);
        try {
          data = stringReader.read();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }

read()方法是其主要的功能,可以将字符串转为Reader来进行操作,这在需要传入Reader作为参数的组件时是非常有用的。这个实例将StringReader换成CharArrayReader时,只要将字符串转成char[]数组后传入一样运行;若换成FileReader,则将从字符串中读取的方式转成从文件中读取后一样运行。

  • 实例二:
String str = "abcd";
    char[] chars = new char[str.length()];
    try (StringReader stringReader = new StringReader(str)) {

      int num = 0;
      int off = 0;
      int len = 2;
      try {
          // 读取一部分字符到字符数组中
          num = stringReader.read(chars, off, len);
      } catch (IOException e) {
        e.printStackTrace();
      }
      if (num != 0) {
        System.out.println("字符:" + new String(chars, 0, num));
      }
    }

以上两个实例与CharArrayReader功能相同,直接替换即可,不再赘述。

CharArrayReader

  • 实例一:
char[] chars = "abcd".toCharArray();
        int data = 0;
        int off = 0;
        int len = 2;

        try (CharArrayReader charArrayReader = new CharArrayReader(chars, off, len)) {

            try {
                data = charArrayReader.read();
            } catch (IOException e) {
                e.printStackTrace();
            }
            while (data != -1) {
                char c = (char) data;
                System.out.println("编码值:" + data + ", 字符:" + c);
                try {
                    data = charArrayReader.read();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

唯一与StringReader不同的就是CharArrayReader可以在构造方法中指定读取某段字符。

PushbackReader

  • 实例一:
      // 指定一次可以放回多少个char,默认为1个
      int limit = 1;
        try (PushbackReader pushbackReader =
                     new PushbackReader(new FileReader("temp/test.txt"), limit)) {

            int data = pushbackReader.read();
            if (data != -1) {
                char dataChar = (char) data;
                System.out.println("编码:" + data + ", 字符:" + dataChar);
            }
            pushbackReader.unread(data);
            data = pushbackReader.read();
            if (data != -1) {
                char dataChar = (char) data;
                System.out.println("编码:" + data + ", 字符:" + dataChar);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

由于读取字符数据后又将其放回,所以两次的输出结果是一样的。

编码:27979, 字符:测
编码:27979, 字符:测

关键SourceCode:

public int read() throws IOException {
        synchronized (lock) {
            ensureOpen();
            if (pos < buf.length)
                return buf[pos++];
            else
                return super.read();
        }
    }

    public void unread(int c) throws IOException {
        synchronized (lock) {
            ensureOpen();
            if (pos == 0)
                throw new IOException("Pushback buffer overflow");
            buf[--pos] = (char) c;
        }
    }

简单查看SourceCode可知,其读取字符数据时是先创建了指定大小的char[] buf数组,用来保存需要放回的字符(由limit参数指定大小),然后用一个int型pos变量记录与buf的相对位置(相同即表明并未放回任何数据)。当调用了unread方法时,先前所创建的char[] buf数组将用来保存指定的字符(读取出来的字符编码),回退字符顺序与保存的字符顺序一致。当再次read时,每次先进行判断,若pos与buf不等,则先从buf字符数组中读取。

字符流线程安全问题

/**
     * The object used to synchronize operations on this stream.  For
     * efficiency, a character-stream object may use an object other than
     * itself to protect critical sections.  A subclass should therefore use
     * the object in this field rather than <tt>this</tt> or a synchronized
     * method.
     */
    protected Object lock;

Reader 和 Writer 抽象类中定义的read和write方法中都有同步锁,所以都是线程安全的。默认使用对象本身进行初始化lock,但其表明为了使效果更好,可以使用需要同步的对象而非本身,然后子类沿用此lock。

Pipe

Java IO中的管道提供了在同一JVM中运行的两个线程进行通信的能力。因此,管道也可以是数据的来源或目的地。Java中的管道概念不同于Unix / Linux中的管道概念,其中在不同地址空间中运行的两个进程可以通过管道进行通信。在Java中,通信方必须在同一进程中运行,并且应该是不同的线程(在一个线程中调用下面两个对象容易发生deadlock!)。

  • 实例一:
    ThreadGroup group1 = new ThreadGroup("group1");
    PipedOutputStream output = new PipedOutputStream();
    try (PipedInputStream input = new PipedInputStream(output)) {

      Thread thread1 =
          new Thread(
              group1,
              () -> {
                try {
                  output.write("Hello world, pipe!".getBytes());
                } catch (IOException e) {
                  e.printStackTrace();
                }
              });

      Thread thread2 =
          new Thread(
              group1,
              () -> {
                try {
                  int data = input.read();
                  while (data != -1) {
                    System.out.print((char) data);
                    data = input.read();
                  }
                } catch (IOException e) {
                  e.printStackTrace();
                }
              });
      group1.setDaemon(true);
      thread1.start();
      thread2.start();
      TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException | IOException e) {
      e.printStackTrace();
    }

输出结果:

> Task :PipeDemo.main()
java.io.IOException: Write end dead
	at java.io.PipedInputStream.read(PipedInputStream.java:310)
	at bio.pipe.PipeDemo.lambda$main$1(PipeDemo.java:35)
	at java.lang.Thread.run(Thread.java:748)
Hello world, pipe!

20190906_pipe.png

源码简单解读

  • 实例化
// PipedInputStream中的connect方法还是调用了下面PipedOutputStream中的connect方法
public void connect(PipedOutputStream src) throws IOException {
        src.connect(this);
    }

// PipedOutputStream中的connect方法
public synchronized void connect(PipedInputStream snk) throws IOException {
        if (snk == null) {
            throw new NullPointerException();
        } else if (sink != null || snk.connected) {
            throw new IOException("Already connected");
        }
    // 持有PipedInputStream对象
        sink = snk;
        snk.in = -1;
        snk.out = 0;
        snk.connected = true;
    }

在实例化的过程中,不管是在 PipedOutputStream 中传入 PipedInputStream,还是在 PipedInputStream 中传入 PipedOutputStream,最终都是调用 PipedOutputStream 中的 connect 方法来建立管道连接的。上面出现的in、out等变量挺重要的,有必要了解一下PipedInputStream在实例化的过程中初始化了一些变量:

// 一些状态标识,如:读/写是否关闭,连接是否创建
boolean closedByWriter = false;
    volatile boolean closedByReader = false;
    boolean connected = false;

// 这是标准配置,一写一读两个线程
    Thread readSide;
    Thread writeSide;

// 初始化读取数据后暂时保存的循环缓冲区大小
    private static final int DEFAULT_PIPE_SIZE = 1024;

// 默认1024,可在构造函数的参数中设置以更改默认值
    protected static final int PIPE_SIZE = DEFAULT_PIPE_SIZE;

// 读取数据后暂时保存的循环缓冲区
    protected byte buffer[];

// 保存循环缓冲区中当前保存的位置,int<0代表空,int==out代表满
    protected int in = -1;

    // 保存循环缓冲区中将要保存的下一个空位置
    protected int out = 0;
  • 数据传输
// PipedOutputStream
public void write(int b)  throws IOException {
        if (sink == null) {
            throw new IOException("Pipe not connected");
        }
        sink.receive(b);
    }

// PipedInputStream
protected synchronized void receive(int b) throws IOException {
        checkStateForReceive();
        writeSide = Thread.currentThread();
        if (in == out)
            awaitSpace();
        if (in < 0) {
            in = 0;
            out = 0;
        }
        buffer[in++] = (byte)(b & 0xFF);
        if (in >= buffer.length) {
            in = 0;
        }
    }

由上可知,PipedOutputStream中持有PipedInputStream对象,在调用write方法写入数据时其实就是调用PipedInputStream中的receive方法来接收数据的,所以可以知道为啥SourceCode中说道的”若PipedInputStream不存在了,则证明这个管道被破坏了“的意思了吧~

  • 关闭管道
// PipedOutputStream
public void close()  throws IOException {
        if (sink != null) {
            sink.receivedLast();
        }
    }

// PipedInputStream
synchronized void receivedLast() {
        closedByWriter = true;
        notifyAll();
    }

    public void close()  throws IOException {
        closedByReader = true;
        synchronized (this) {
            in = -1;
        }
    }

PipedOutputStream 调用close方法关闭时会将 PipedInputStream 中的closedByWriter标识为true且通知其它阻塞的读取线程继续读完写入的数据(这一功能和其flush方法相同)。PipedInputStream 关闭时同样将closedByReader标识为true,只不过他顺带清空了循环缓冲区中的数据。

PipedReader和PipedWriter亦是如此。

字节流

DataOutputStream

DataInputStream 和 DataOutputStream除了实现抽象类InputStream和OutputStream中基本的read(byte[])和write(int )抽象方法外,还实现了DataInput和DataOutput接口,他们的线程安全问题依赖于使用者本身

主要功能是可以直接输入和输出Java原始数据类型(如:int、long、float、Double等)。文档翻译:使用修改后的UTF-8编码以与机器无关的方式将字符串写入基础输出流。

  • 实例一:
    try (DataOutputStream out = new DataOutputStream(new FileOutputStream("temp/test3.txt"));
        DataInputStream in = new DataInputStream(new FileInputStream("temp/test3.txt"))) {

      out.writeByte(123);
      out.writeChar('a');
      out.writeShort(123);
      out.writeInt(123);
      out.writeFloat(123.45F);
      out.writeLong(999999999);

      byte byte97 = in.readByte();
      char chara = in.readChar();
      short short97 = in.readShort();
      int int123 = in.readInt();
      float float12345 = in.readFloat();
      long long999999999 = in.readLong();

      System.out.println("byte97 = " + byte97);
      System.out.println("chara = " + chara);
      System.out.println("short97 = " + short97);
      System.out.println("int123     = " + int123);
      System.out.println("float12345 = " + float12345);
      System.out.println("long999999999    = " + long999999999);
    } catch (IOException e) {
      e.printStackTrace();
    }

输出:

byte97 = 123
chara = a
short97 = 123
int123     = 123
float12345 = 123.45
long999999999    = 999999999 

DataOutputStream中调用的每个上述writeXX方法都会保存写出的字节数(DataInputStream没有提供),然后可以调用size()方法返回目前为止写出的总字节数(最大值为Integer.MAX_VALUE)。

    /**
     * The number of bytes written to the data output stream so far.
     * If this counter overflows, it will be wrapped to Integer.MAX_VALUE.
     */
    protected int written;
    
/**
     * Increases the written counter by the specified value
     * until it reaches Integer.MAX_VALUE.
     */
    private void incCount(int value) {
        int temp = written + value;
        // 判断是否溢出
        if (temp < 0) {
            temp = Integer.MAX_VALUE;
        }
        written = temp;
    }

    /**
     * Returns the current value of the counter <code>written</code>,
     * the number of bytes written to this data output stream so far.
     * If the counter overflows, it will be wrapped to Integer.MAX_VALUE.
     *
     * @return  the value of the <code>written</code> field.
     * @see     java.io.DataOutputStream#written
     */
    public final int size() {
        return written;
    }

test3.txt文件打开来是这样子的,并且提示编码错误:

{ a {   {B��f    ;���

这也说明了DataInputStream和DataOutputStream往往搭配着一起使用的缘故(只有它看得懂:T)

注意:DataOutputStream中writeUTF(String)方法较为特别,与此对应的为readUTF()。

  • 实例二:
DataInputStream in = null;
    try {
      in = new DataInputStream(new FileInputStream("temp/test.txt"));
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    }

    try (DataOutputStream out = new DataOutputStream(new FileOutputStream("temp/test2.txt"));
        BufferedReader line =
            new BufferedReader(new InputStreamReader(Objects.requireNonNull(in)))) {

      String count;
      while ((count = line.readLine()) != null) {
        for (int i = 0; i < count.getBytes().length; i++) {
          System.out.println(count.getBytes()[i]);
        }
        System.out.println("已读入字符:" + count);
        System.out.println("已读入字节数:" + count.getBytes().length);

        out.writeUTF(count);
        System.out.println("已写出字节数 written:" + out.size());
        System.out.println("=======================");
        System.out.println("=======================");
      }
    } catch (IOException e) {
      e.printStackTrace();
    }

    DataInputStream in2 = null;
    try {
      in2 = new DataInputStream(new FileInputStream("temp/test2.txt"));
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    }
    try (BufferedReader line2 =
        new BufferedReader(new InputStreamReader(Objects.requireNonNull(in2)))) {

      String count2;
      while ((count2 = line2.readLine()) != null) {
        for (int i = 0; i < count2.getBytes().length; i++) {
          System.out.println(count2.getBytes()[i]);
        }
        System.out.println("已读入字字符:" + count2);
        System.out.println("已读入字节数:" + count2.getBytes().length);
      }

    } catch (IOException e) {
      e.printStackTrace();
    }

英文测试

读取的test.txt文件:

a
b

输出:

97
已读入字符:a
已读入字节数:1
已写出字节数 written:3
=======================
=======================
98
已读入字符:b
已读入字节数:1
已写出字节数 written:6
=======================
=======================
0
1
97
0
1
98
已读入字字符: [][]a[][]b
已读入字节数:6

写出的test2.txt文件内容:

 []a[]b
// 原版doc说明
* Writes a string to the specified DataOutput using
* <a href="DataInput.html#modified-utf-8">modified UTF-8</a>
* encoding in a machine-independent manner.
* <p>
* First, two bytes are written to out as if by the <code>writeShort</code>
* method giving the number of bytes to follow. This value is the number of
* bytes actually written out, not the length of the string. Following the
* length, each character of the string is output, in sequence, using the
* modified UTF-8 encoding for the character. If no exception is thrown, the
* counter <code>written</code> is incremented by the total number of
* bytes written to the output stream. This will be at least two
* plus the length of <code>str</code>, and at most two plus
* thrice the length of <code>str</code>.

// 中文翻译
首先,将两个字节写入输出流,就像通过writeShort给出要遵循的字节数的方法一样。该值是实际写出的字节数,而不是字符串的长度。在该长度之后,使用针对该字符的修改的UTF-8编码依次输出该字符串的每个字符。如果没有抛出异常,则计数器written将增加写入输出流的总字节数。这将是至少两个加上长度`str`,并且最多两倍加三倍的长度str

输出结果结合文档可以知道,每次将读取到的字符通过writeUTF(String)输出时其前面都会带有两个字节的奇怪数据。01为十六进制,转为二进制,一位表示2的4次方,所以为一个字节,然后在方法中像writeShort()方法中那样进行了无符号右移操作>>> 8,所以总的用了两个字节进行存储,细节暂不深究。

中文测试

读取的test.txt文件:

中
b

输出:

-28
-72
-83
已读入字符:中
已读入字节数:3
已写出字节数 written:5
=======================
=======================
98
已读入字符:b
已读入字节数:1
已写出字节数 written:8
=======================
=======================
0
3
-28
-72
-83
0
1
98

已读入字字符: [][]中 [][]b
已读入字节数:8

猜想:01后面跟着的是占一个字节的字符,03后面跟着的是占三个字节的字符。

扩展

占2个字节的:带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要二个字节编码

占3个字节的:基本等同于GBK,含21000多个汉字

占4个字节的:中日韩超大字符集里面的汉字,有5万多个

一个utf8数字占1个字节

一个utf8英文字母占1个字节

少数是汉字每个占用3个字节,多数占用4个字节。

ByteArrayOutputStream

OutputStream中有一个需要传入byte数组的方法来写出数据:

    public void write(byte b[]) throws IOException {
        write(b, 0, b.length);
    }

如果我们的数据是一个一个int型的,那么用ByteArrayOutputStream就可以很简单地将一个一个int数据集中到一个数组中去,然后很方便的调用上面这个方法写出。

实例一:

    // 未指定初始化buf大小,则默认创建一个 buf[32] 大小的数组存储空间
    try (ByteArrayOutputStream output = new ByteArrayOutputStream();
        DataOutputStream dataOutput =
            new DataOutputStream(new FileOutputStream("temp/test3.txt"))) {
      output.write(97);
      output.write(65);
      System.out.println("buf 数组目前存储的数据大小:" + output.size() + " 个字节");

      byte[] bytes = output.toByteArray();
      dataOutput.write(bytes);
      System.out.println("write successfully!");
    } catch (IOException e) {
      e.printStackTrace();
    }

test3.txt文件:

aA

输出:

buf 数组目前存储的数据大小:2 个字节
write successfully!

其中需要注意的就是溢出问题

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

扩展:来自StackOverflow中的解释

数组对象的形状和结构(例如int值数组)类似于标准Java对象的形状和结构。主要区别在于数组对象有一段额外的元数据,表示数组的大小。然后,数组对象的元数据包括:类:指向类信息的指针,它描述对象类型。对于int字段数组,这是一个指向int []类的指针。

标志:描述对象状态的标志集合,包括对象的哈希码(如果有),以及对象的形状(即对象是否为数组)。

锁定:对象的同步信息 - 即对象当前是否已同步。

大小:数组的大小。

最大尺寸

2^31 = 2,147,483,648 
作为数组,它自己需要8 bytes存储大小  2,147,483,648

所以

2^31 -8 (for storing size ), 
所以最大数组大小定义为Integer.MAX_VALUE - 8

所以,那减去的8个字节是用来存储数组所需的各种元数据的。

private void ensureCapacity(int minCapacity) {
        // overflow-conscious code
        if (minCapacity - buf.length > 0)
            grow(minCapacity);
    }

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = buf.length;
        int newCapacity = oldCapacity << 1;
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        buf = Arrays.copyOf(buf, newCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

newCapacity -> 即将增加后的数组大小

minCapacity -> 当前写入一个字节后的大小

每次写到buf中时都会调用 ensureCapacity 方法检查写出后的大小,若溢出,则将buf数组扩大10倍;若此扩大10倍后的数组大小溢出了(int型数据变成了负数),则直接等于刚才写入一个字节数据后的数组大小(也就是不一下子增很大,而是写一个增大一个);若超过 Integer.MAX_VALUE - 8 时,直接将buf增至Integer.MAX_VALUE,但这样的话就将元数据覆盖了呀,暂时不清楚为何要这样做:(

SequenceInputStream

实例一:

    try (InputStream input1 = new FileInputStream("temp/test.txt");
        InputStream input2 = new FileInputStream("temp/test2.txt")) {

      SequenceInputStream sequenceInputStream = new SequenceInputStream(input1, input2);

      int data = sequenceInputStream.read();
      while (data != -1) {
        System.out.println(data);
        data = sequenceInputStream.read();
      }
    } catch (IOException e) {
      e.printStackTrace();
    }

test.txt:

ab

test2.txt:

cd

输出:

97
98
99
100

ObjectOutputStream

实例一:

/**
 * 序列化就是将一个对象转换成字节序列,方便存储和传输。 
 * 序列化:ObjectOutputStream.writeObject()
 * 反序列化:ObjectInputStream.readObject()
 */
public class SerializableDemo {
  public static void main(String[] args) {
    String objectFile = "test4.txt";

    // 序列化
    serialize(objectFile);
    // 反序列化
    deserialize(objectFile);
  }

  // 序列化
  public static void serialize(String objectFile) {
    A a = new A(1, "aaa");
    try (ObjectOutputStream objectOutputStream =
        new ObjectOutputStream(new FileOutputStream(objectFile))) {
      objectOutputStream.writeObject(a);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  // 反序列化
  public static void deserialize(String objectFile) {
    try (ObjectInputStream objectInputStream =
        new ObjectInputStream(new FileInputStream(objectFile))) {
      A a2 = (A) objectInputStream.readObject();
      System.out.println(a2);
    } catch (IOException | ClassNotFoundException e) {
      e.printStackTrace();
    }
  }

  private static class A implements Serializable {

    private int x;
      // transient关键字声明不需要序列化的属性
    private transient String y;

    A(int x, String y) {
      this.x = x;
      this.y = y;
    }

    @Override
    public String toString() {
      return "x = " + x + "  " + "y = " + y;
    }
  }
}

输出:

x = 1  y = null

test4.txt文件:

��[]sr[]bio.bytes.SerializableDemo$AN��xY���[]I[]xxp   []

字节流线程安全问题

两个抽象类中只有InputStream中的reset()和mark(int)方法存在synchronized,所以线程安全基本依赖于他们的子类。

扩展

Guava IO

难免错漏,敬请指正 :T

参考资料

  1. JAVA IO操作总结——节点流和处理流
  2. Java IO学习整理
  3. Java IO流分析整理
  4. 装饰器模式
  5. Java IO包装流如何关闭?(关闭顺序)
  6. Java中CharSet字符集
  7. java io系列04之 管道简介
  8. DataOutputStream的writeUTF()方法与OutputStreamWriter的write()区别
  9. byte[i] & 0xFF原因(byte为什么要&上0xff?)
  10. Java IO 教程
0

评论区