Skip to main content

Java I/O详解

PPLongAbout 10 min

JAVA I/O详解

IO这块一直是自己所忽略的部分,抽时间好好系统性地学习一下

建议回顾这篇文章之前,先回顾Unicode\UTF-8\ASCII等不同的编码方式

概述

编码

字符和字节到底怎么区分的呢

字节,基本的数据量单位,一字节 = 8 位
字符是计算机中使用的字母、数字、字和符号

  • ASCII 码中,一个英文字母(不分大小写)为一个字节,一个中文汉字为两个字节。
  • Unicode 编码中,一个英文为一个字节,一个中文为两个字节。
  • GBK 编码中,一个英文字符占一个字节,一个中文字符占两个字节
  • 符号:英文标点为一个字节,中文标点为两个字节。例如:英文句号 . 占1个字节的大小,中文句号 **。**占2个字节的大小。
  • UTF-8 编码中,一个英文字为一个字节,一个中文为三个字节
  • UTF-16 编码中,一个英文字母字符或一个汉字字符存储都需要 2 个字节(Unicode 扩展区的一些汉字存储需要 4 个字节)。
  • UTF-32 编码中,世界上任何字符的存储都需要 4 个字节。

对各种编码格式的解释

  • ASCII,美国信息交换标准代码

    • 标准ASCII码,低128位
    • 扩展ASCII码,高128位,允许将每个字符的第8位用于确定附加的128个特殊符号字符、图形符号
  • Unicode 万国码、单一码。各国合作开发,为每种语言的每一个字符设置了唯一的编码 。Unicode用数字0-0x10FFFF(1114112个位置)来映射这些字符

    • Unicode的学名是"Universal Multiple-Octet Coded Character Set",简称为UCS。
    • 现在用的是UCS-2,即2个字节编码,而UCS-4是为了防止将来2个字节不够用才开发的。
  • UTF : UCS Transformation Format的缩写,可以翻译成Unicode字符集转换格式,,即怎样将Unicode定义的数字转换成程序数据。由于Unicode无法区分三个字节表示三个英文还是一个其他字符和因此造成的臃肿。所以可以理解UTF为Unicode的优化编码形式,并且又衍生出了多种编码方式 UTF-8、UTF-16、UTF-32等。

    • 例如,“汉字”对应的数字是0x6c49和0x5b57,而编码的程序数据是:

      BYTE data_utf8[] = {0xE6, 0xB1, 0x89, 0xE5, 0xAD, 0x97}; // UTF-8编码,以八字节对Unicode进行编码,可以多达4个字节,有对应的编码规则
      
      WORD data_utf16[] = {0x6c49, 0x5b57}; // [UTF-16](https://baike.baidu.com/item/UTF-16/9032026)编码
      
      DWORD data_utf32[] = {0x6c49, 0x5b57}; // [UTF-32](https://baike.baidu.com/item/UTF-32/734460)编码
      

注意UTF-16be和UTF-16le的区别

be: Big Endian 大端存储(先存高位再存低位) le: Little Endian 小端存储(先存低位再存高位)

Java char类型使用双字节编码UTF-16be编码

分类

传输方式

  • 字节流。处理二进制文件(图片、mp3、视频)
    • InputStream/OutputStream
  • 字符流。处理文本文件
    • Reader/Writer

要注意的是, InputStream相当于数据流入到外界(read),OutputStream是指把数据流入到内部(write)

数据操作

  • 文件 File...
  • 数组 I/O: ByteArray... R/W: CharArray...
  • 管道 Piped...
  • 基本数据类型 I/O: Data...
  • 缓冲操作 Buffered...
  • 打印: PrintStream/PrintWriter
  • 对象序列化/反序列化: I/O: Object...
  • 转换: InputStreamReader/OutputStreamWriter

设计模式

装饰者设计模式

使用场景: 运行时动态给一个对象增加额外的功能,同时不通过继承的方式来实现

理解案例: 咖啡厅加工咖啡时,用原始咖啡,可以加糖或者加奶,也可以既加糖又加奶。只需要加糖/奶时,把原始咖啡做好后,加糖就行了(将原味咖啡(被装饰者)对象传递到对应装饰者中装饰就好)。如果既需要加糖又需要加奶,则可以先把原味咖啡加奶,然后又加糖(即再次把加工好的半成品放进加糖装饰者中装饰一下)。
也就是说,我不用重做一杯一开始就放糖加奶的咖啡,我只需要把原味咖啡做好,再加糖加奶即可

Java I/O的装饰者模式

一般会使用这样的方式去初始化一个BufferedInputStream

FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);

那么FilterInputStream有何作用?

A FilterInputStream contains some other input stream, which it uses as its basic source of data, possibly transforming the data along the way or providing additional functionality. The class FilterInputStream itself simply overrides all methods of InputStream with versions that pass all requests to the contained input stream. Subclasses of FilterInputStream may further override some of these methods and may also provide additional methods and fields.

public class FilterInputStream extends InputStream {
    protected volatile InputStream in;

    protected FilterInputStream(InputStream in) {
        this.in = in;
    }

    public int read() throws IOException {
        return in.read();
    }

    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

    public int read(byte b[], int off, int len) throws IOException {
        return in.read(b, off, len);
    }
  
    public long skip(long n) throws IOException {
        return in.skip(n);
    }

    public int available() throws IOException {
        return in.available();
    }

    public void close() throws IOException {
        in.close();
    }

    public synchronized void mark(int readlimit) {
        in.mark(readlimit);
    }

    public synchronized void reset() throws IOException {
        in.reset();
    }

    public boolean markSupported() {
        return in.markSupported();
    }
}

而BufferedInputStream中又有自己的逻辑,基于成员变量InputStream(后续实际为FileInputStream),最终实现在FileInputStream的基础上添加Buffer功能。

Unix IO模型

Unix五种IO模型

  • 阻塞式IO
  • 非阻塞式IO
  • IO服用
  • 信号驱动式
  • 异步

阻塞式I/O

应用进程被阻塞(其他进程还可以执行,不消耗CPU时间),直到数据复制到应用进程缓冲区才返回。效率高

非阻塞式I/O

应用程序执行系统调用后,内核返回一个错误码,应用程序继续执行,但需要不断的执行系统调用来获知IO是否完成, 称为轮询的方式(polling), 效率低

I/O复用

使用select或者poll等待数据,可等待多个套接字中的任何一个变为可读,这一过程会被阻塞。当某个套接字可读返回时,再使用recvfrom把数据从内核复制到进程中。让单个进程拥有处理多个IO事件的能力,称为事件驱动IO(Event Driven I/O)

如果一个Web服务器没有IO复用,则每隔Socket连接都需要创建一个线程去处理

信号驱动I/O

应用进程使用sigaction系统调用,内核立即返回,应用进程继续执行,在等待数据阶段是非阻塞的,内核在数据到达时向应用进程发送SIG IO信号,应用进程收到信号后调用recvfrom将数据从内核复制到应用进程中。

异步I/O

进行aio_read系统调用会立即返回,应用程序不被阻塞,内核在操作完成后向应用进程发送信号。注意与信号驱动IO的区别是,异步IO的信号是通知应用进程IO完成而信号驱动IO的信号是通知应用程序可以开始IO

分类

BIO

BIO: Blocking IO,应用程序向OS请求网络IO操作时,此时应用程序就会一直等待,直到受到数据

传统BIO

  • 客户端向服务端发出请求,客户端便等待,直到服务端返回结果或者网络出现问题
  • 服务端在处理某个客户端发来的请求时,此时另一个客户端发来网络请求时则会等待服务端处理好之前客户端的请求才会开始处理当前客户端请求

缺陷: 串行等待结果,高并发下不可用

多线程-伪异步方式

服务端主线程接收到客户端A发来的请求时,单独起一个线程进行处理(监听/观察模式)

缺陷

  • 高并发条件下,线程创建过多,耗费资源多且CPU切换所需时间长,真正处理业务的时间少
  • 服务器接收到数据报文并分配线程 这一步是单线程的、串行的

NIO

源码解析

InputStream

// 实现Closeable接口, 代表是可关闭的
public abstract class InputStream implements Closeable {
    private static final int MAX_SKIP_BUFFER_SIZE = 2048;
  	
    // 读取下一个字节(范围在0-255), 如果没有则返回-1
    public abstract int read() throws IOException;
  	// 将读到的数据放在参数数组中
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }
  	// 从off位置开始读取最多为len长度(实际可能小于len)的字节到参数数组中
    public int read(byte b[], int off, int len) throws IOException {
        // ...
    }
  	// 跳过指定个数的字节不读取, 返回实际跳过的字节数
    public long skip(long n) throws IOException {
        // ...
    }
  	// 返回可读的字节数目
    public int available() throws IOException {
        return 0;
    }
		// 关闭流, 释放资源
    public void close() throws IOException {}
		// 重置读取位置为上次mark标记的位置,当前流不一定支持
   	public synchronized void reset() throws IOException {
        throw new IOException("mark/reset not supported");
    }
  	// 标记读取位置,下次还可以从这里开始读取,当前流不一定支持
    public synchronized void mark(int readlimit) {}
		// 判断当前流是否支持标记流
    public boolean markSupported() {
        return false;
    }

read方法解读

按序调用抽象方法read一个个地读取字节到数组中,代码比较简单

public int read(byte b[], int off, int len) throws IOException {
    if (b == null) {
        throw new NullPointerException();
    } else if (off < 0 || len < 0 || len > b.length - off) {
        throw new IndexOutOfBoundsException();
    } else if (len == 0) {
        return 0;
    }

    int c = read();
    if (c == -1) {
        return -1;
    }
    b[off] = (byte)c;

    int i = 1;
    try {
        for (; i < len ; i++) {
            c = read();
            if (c == -1) {
                break;
            }
            b[off + i] = (byte)c;
        }
    } catch (IOException ee) {
    }
    return i;
}

ByteArrayInputStream

本质: 读取的对象是byte数组(传入的对象是数组,之后就从数组中读,数组中元素是什么就读什么)。内部用指针维护当前读取的位置

OutputStream

// 实现Closeable接口, 代表是可关闭的
public abstract class OutputStream implements Closeable, Flushable {
  		// 写入一个字节
      public abstract void write(int b) throws IOException;
  		// 将数组中的字节都写入
      public void write(byte b[]) throws IOException {
          write(b, 0, b.length);
      }
  		// 写入数组中指定位置开始长度为len的字节
      public void write(byte b[], int off, int len) throws IOException {
            if (b == null) {
                throw new NullPointerException();
            } else if ((off < 0) || (off > b.length) || (len < 0) ||
                       ((off + len) > b.length) || ((off + len) < 0)) {
                throw new IndexOutOfBoundsException();
            } else if (len == 0) {
                return;
            }
            for (int i = 0 ; i < len ; i++) {
                write(b[off + i]);
            }
        }
  		// 将缓冲中的数据写入
      public void flush() throws IOException {}
      public void close() throws IOException {}

ByteArrayOutputStream

本质: 内部维护buf数组接收要写进来的字节,在实际长度超过容量时进行扩容。

需要注意的是,与ByteArrayInputStream不同,该OutputStream的write方法是synchronized,因为要考虑并发情况下写入可能引发的不确定。

public class ByteArrayOutputStream extends OutputStream {
		private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
  	// 内部维护的byte数组
    protected byte buf[];
		// buf中实际有效的byte个数
    protected int count;
  	// 默认初始化buf长度为32个
    public ByteArrayOutputStream() {
      this(32);
    }
  	// 接受初始化指定长度的buf数组
    public ByteArrayOutputStream(int size) {
        if (size < 0) {
            throw new IllegalArgumentException("Negative initial size: "
                                               + size);
        }
        buf = new byte[size];
    }
  	// 确保当前数组总共能容纳下指定长度的元素
    private void ensureCapacity(int minCapacity) {
        // overflow-conscious code
        if (minCapacity - buf.length > 0)
            grow(minCapacity);
    }
  	// 实现buf数组的扩容(2倍)
    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);
    }
  	// 最大化buf数组长度(极端情况)
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
  	 // 写入一个字节到buf数组中
     public synchronized void write(int b) {
       		// 先判断写入后数组是否溢出
          ensureCapacity(count + 1);
          buf[count] = (byte) b;
          count += 1;
      }
  		// 同步地写入byte数组中的指定字符
      public synchronized void write(byte b[], int off, int len) {
          if ((off < 0) || (off > b.length) || (len < 0) ||
              ((off + len) - b.length > 0)) {
              throw new IndexOutOfBoundsException();
          }
          ensureCapacity(count + len);
          System.arraycopy(b, off, buf, count, len);
          count += len;
      }
  		// 将本outputstream中的有效数据写入到指定的outputstream中
      public synchronized void writeTo(OutputStream out) throws IOException {
          out.write(buf, 0, count);
      }
  		public synchronized void reset() {
        count = 0;
    	}
    	public synchronized int size() {
          return count;
      }
  		public void close() throws IOException {}

参考文章

🔗 : Java IO知识体系详解open in new window

🔗 : 知乎-秒懂设计模式之装饰者模式(Decorator Pattern)open in new window