I/O学习笔记-Java

116 阅读7分钟

流是什么

数据流是指一组有序的、有起点和终点的字节集合。对数据的读写操作都可以使用数据流来完成。

流的分类

处理字节/字符:字节流/字符流 流入/出:输入流/输出流

字节流

读取单位为一个字节(8bit),能读写任意形式的文件

字符流

读取单位为一个字符,只能读写普通文本文件(能用记事本打开的文件)
注:二进制数据——编码——>字符

编码表(ASCII、Unicode)——解码——>二进制数据

常用字符编码所占字节数:

utf8 :英文占 1 字节,中文占 3 字节

unicode:任何字符都占 2 个字节

gbk:英文占 1 字节,中文占 2 字节

JAVA IO 常用类

image.png

InputStream

InputStream用于从源头(通常是文件)读取数据(字节信息)到内存中,java.io.InputStream抽象类是所有字节输入流的父类。

InputStream常用方法:

  • read()
    /**
     * Reads the next byte of data from the input stream. The value byte is
     * returned as an {@code int} in the range {@code 0} to
     * {@code 255}. If no byte is available because the end of the stream
     * has been reached, the value {@code -1} is returned. This method
     * blocks until input data is available, the end of the stream is detected,
     * or an exception is thrown.
     *
     * <p> A subclass must provide an implementation of this method.
     *
     * @return     the next byte of data, or {@code -1} if the end of the
     *             stream is reached.
     * @throws     IOException  if an I/O error occurs.
     */
    public abstract int read() throws IOException;
  • read(byte b[])
     * <p> The {@code read(b)} method for class {@code InputStream}
     * has the same effect as: <pre>{@code  read(b, 0, b.length) }</pre>
     *
     * @param      b   the buffer into which the data is read.
     * @return     the total number of bytes read into the buffer, or
     *             {@code -1} if there is no more data because the end of
     *             the stream has been reached.
     * @throws     IOException  If the first byte cannot be read for any reason
     *             other than the end of the file, if the input stream has been
     *             closed, or if some other I/O error occurs.
     * @throws     NullPointerException  if {@code b} is {@code null}.
     * @see        java.io.InputStream#read(byte[], int, int)
     */
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }
  • read(byte b[], int off, int len)
public int read(byte b[], int off, int len) throws IOException {
        Objects.checkFromIndexSize(off, len, b.length);
        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;
    }

Checks if the sub-range from fromIndex (inclusive) to fromIndex + size (exclusive) is within the bounds of range from 0 (inclusive) to length (exclusive).

int java.util.Objects.checkFromIndexSize(int fromIndex, int size, int length)

OutputStream

OutputStream常用方法:

  • write(int b)
public abstract void write(int b) throws IOException;
  • write(byte b[])
    /**
     * Writes {@code b.length} bytes from the specified byte array
     * to this output stream. The general contract for {@code write(b)}
     * is that it should have exactly the same effect as the call
     * {@code write(b, 0, b.length)}.
     *
     * @param      b   the data.
     * @throws     IOException  if an I/O error occurs.
     * @see        java.io.OutputStream#write(byte[], int, int)
     */
    public void write(byte b[]) throws IOException {
        write(b, 0, b.length);
    }
  • write(byte b[], int off, int len)
     * @param      b     the data.
     * @param      off   the start offset in the data.
     * @param      len   the number of bytes to write.
     * @throws     IOException  if an I/O error occurs. In particular,
     *             an {@code IOException} is thrown if the output
     *             stream is closed.
     */
    public void write(byte b[], int off, int len) throws IOException {
        Objects.checkFromIndexSize(off, len, b.length);
        // len == 0 condition implicitly handled by loop bounds
        for (int i = 0 ; i < len ; i++) {
            write(b[off + i]);
        }
    }

节点流与处理流

节点流直接与节点源头的文件相连,进行读写。字符流在节点流的基础上添加一些处理方法。

分类字节输入流字节输出流字符输入流字符输出流
抽象基类InputStreamOutputStreamReaderWriter
访问文件FileInputStreamFileOutputStreamFileReaderFileWriter
访问数组ByteArrayInputStreamByteArrayOutputStreamCharArrayReaderCharArrayWriter
访问管道PipedInputStreamPipedOutputStreamPipedReaderPipedWriter
访问字符串//StringReaderStringWriter
缓冲流BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter
转换流//InputStreamReaderOutputStreamWriter
对象流ObjectInputStreamObjectOutputStream//
抽象装饰器基类FilterInputStreamFilterOutputStreamFilterReaderFilterWriter
打印流/PrintStream/PrintWriter
推回输入流PushbackInputStream/PushbackReader/
特殊流DataInputStreamDataOutputStream//

BufferedInputStream

BufferedInputStream从源读取的数据先存入字节数组,之后再从字节数组一个字节一个字节地读取。其内部维护了一个字节数组,即缓冲区,默认大小为8192个字节。

public class BufferedInputStream extends FilterInputStream {

    private static int DEFAULT_BUFFER_SIZE = 8192;

    private static final Unsafe U = Unsafe.getUnsafe();

    private static final long BUF_OFFSET 
        = U.objectFieldOffset(BufferedInputStream.class, "buf");

    protected volatile byte[] buf;

    protected int count;

    protected int pos;

    protected int markpos = -1;

    protected int marklimit;

    private InputStream getInIfOpen() throws IOException {

        InputStream input = in;

        if (input == null)

            throw new IOException("Stream closed");

        return input;

    }

    private byte[] getBufIfOpen() throws IOException {

        byte[] buffer = buf;

        if (buffer == null)

            throw new IOException("Stream closed");

        return buffer;

    }

    public BufferedInputStream(InputStream in) {

        this(in, DEFAULT_BUFFER_SIZE);

    }

    public BufferedInputStream(InputStream in, int size) {

        super(in);

        if (size <= 0) {

            throw new IllegalArgumentException("Buffer size <= 0");

        }

        buf = new byte[size];

    }

    ...

}

BufferedOutputStream

同理,其使用方式如下

try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("output.txt"))) {
    byte[] array = "JavaGuide".getBytes();
    bos.write(array);
} catch (IOException e) {
    e.printStackTrace();
}

打印流

控制台打印

System.out.print("Hello!");
System.out.println("Hello!");

System的out是一个PrintStream对象

public static final PrintStream out = null;

它的println方法其实使用的是自己的writeln方法

    public void println(String x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

writeln方法

    // Used to optimize away back-to-back flushing and synchronization when
    // using println, but since subclasses could exist which depend on
    // observing a call to print followed by newLine we only use this if
    // getClass() == PrintStream.class to avoid compatibility issues.
    private void writeln(String s) {
        try {
            synchronized (this) {
                ensureOpen();
                textOut.write(s);
                textOut.newLine();
                textOut.flushBuffer();
                charOut.flushBuffer();
                if (autoFlush)
                    out.flush();
            }
        }
        catch (InterruptedIOException x) {
            Thread.currentThread().interrupt();
        }
        catch (IOException x) {
            trouble = true;
        }
    }

Java IO 中的设计模式

装饰器模式

InputStream是抽象父类,可以当作是定义了字节流输入的规则,装饰器类FilterInputStream持有一个InputStream,这使得所有继承了InputStream类的对象都可以被FilterInputStream装饰。

classDiagram
InputStream <|-- FilterInputStream
InputStream <|-- FileInputStream
InputStream <|-- StringInputStream
InputStream: +...
InputStream: *+read()*
InputStream: +read(byte b[])
InputStream: +read(byte b[], int off, int len)
class FilterInputStream{
#InputStream in
}
class FileInputStream{
+?
+?()
}
class StringInputStream{
+?
+?()
}
FilterInputStream <|-- BufferedInputStream
FilterInputStream <|-- DataInputStream
FilterInputStream <|-- PushBackInputStream

BufferedInputStream等都是FilterInputStream的具体装饰功能,继承关系如下

Object(java.lang)
  -- InputStream(java.io)
    -- FilterInputStream(java.io)
      -- BufferedInputStream(java.io)

然而BufferedInputStream实际上提供的是对字节数组的操作,InputStream本身有缓冲功能。以下两段代码的功能是相同的。

FileInputStream fis=new FileInputStream("d:\\test.txt");  
BufferedInputStream bis=new BufferedInputStream(fis);  
int data=0;  
while((data=bis.read())!=-1){  
    //......          
}  
FileInputStream fis=new FileInputStream("d:\\test.txt");  
byte[] mybuff=new byte[1024];  
int count=0;  
while((count=fis.read(mybuff))!=-1){  
     //......  
}  

适配器模式

适配器的目标是兼容两种不同的接口,要实现这一目的,要么继承目标对象要么持有目标对象

classDiagram
Reader <|-- StreamDecoder
Reader <|-- InputStreamReader
Reader : -int TRANSFER_BUFFER_SIZE
Reader : #Object lock
Reader: +read(CharBuffer target)
Reader: +read()
Reader: +read(char[] cbuf)
Reader: *+read(char[] cbuf, int off, int len)*
Reader: +...()
class StreamDecoder{
+...
+read(char[] cbuf, int offset, int length)
+...()
}
class InputStreamReader{
-final StreamDecoder sd
+...
+read(CharBuffer target)
+...()
}

这里InputStreamReader持有一个final StreamDecoder,其read(CharBuffer target)实际是调用了StreamDecoder的父类Reader的read(CharBuffer target),该方法调用了抽象方法read(char[] cbuf, int offset, int length)的实现,详情如下

InputStreamReader

    public int read(CharBuffer target) throws IOException {
        return sd.read(target);
    }

Reader

    public int read(CharBuffer target) throws IOException {
        if (target.isReadOnly())
            throw new ReadOnlyBufferException();

        int nread;
        if (target.hasArray()) {
            char[] cbuf = target.array();
            int pos = target.position();
            int rem = Math.max(target.limit() - pos, 0);
            int off = target.arrayOffset() + pos;
            nread = this.read(cbuf, off, rem);
            if (nread > 0)
                target.position(pos + nread);
        } else {
            int len = target.remaining();
            char[] cbuf = new char[len];
            nread = read(cbuf, 0, len);
            if (nread > 0)
                target.put(cbuf, 0, nread);
        }
        return nread;
    }

StreamDecoder

    public int read(char[] cbuf, int offset, int length) throws IOException {
        int off = offset;
        int len = length;
        synchronized (lock) {
            ensureOpen();
            if ((off < 0) || (off > cbuf.length) || (len < 0) ||
                ((off + len) > cbuf.length) || ((off + len) < 0)) {
                throw new IndexOutOfBoundsException();
            }
            if (len == 0)
                return 0;

            int n = 0;

            if (haveLeftoverChar) {
                // Copy the leftover char into the buffer
                cbuf[off] = leftoverChar;
                off++; len--;
                haveLeftoverChar = false;
                n = 1;
                if ((len == 0) || !implReady())
                    // Return now if this is all we can produce w/o blocking
                    return n;
            }

            if (len == 1) {
                // Treat single-character array reads just like read()
                int c = read0();
                if (c == -1)
                    return (n == 0) ? -1 : n;
                cbuf[off] = (char)c;
                return n + 1;
            }

            return n + implRead(cbuf, off, off + len);
        }
    }

Java IO 模型

若将I/O过程比作钓鱼,那么磁盘是鱼塘,数据是鱼,鱼杆是内核空间,水桶是用户空间。

image.png

BIO(Blocking I/O)

同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。

image.png

NIO(Non-blocking/New I/O)

NIO有两个模型,同步非阻塞和多路复用。比起同步非阻塞模型,使用多路复用模型显然更节省资源。

image.png

IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。

NIO代码示例:

selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
    SelectionKey key = iterator.next();
    if (key.isReadable()) {
        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
        executor.execute(() -> {
            try {
                channel.read(byteBuffer);
                handle(byteBuffer);
            } catch (Exception e) {
 
            }
        });
    }
}
 
public static void handle(ByteBuffer buffer) {
    // TODO
}

AIO(Asynchronous I/O)

AIO发起监听事件的线程与处理事件的线程是不同的。

image.png

但是即使是AIO也只是在用户态上实现了异步,想达到理想中的异步似乎是不可能的。不过理想中的异步是否比NIO更加好呢,现今大多数场景都需要实时响应,而异步却存在无法实时返回结果的缺点。

NIO与Netty

相关面试题

I/O流为什么要分字符流和字节流呢?

答:字节流到字符流的转换很耗时,而且不同编码表的编码方式不同。在GBK中汉字占2个字节,在UTF-8中汉字占3个字节,所以我们通过字节流读取文件的时候一般都是逐个字节转换就会导致乱码,而根据不同编码人力拼接不方便,所以有字符流。

Java I/O的设计模式有哪些?BIO/NIO/AIO的区别有哪些?

答:参考 Java IO 模型

异步I/O和同步I/O的区别?

答:同步用户程序会阻塞效率地下,异步IO用户程序在发起请求的同时可以处理其他事务。但是同步可以实时返回结果,异步没有确定的返回时刻。

介绍一下Java中的I/O流

答:参考节点流与处理流表

请你说说I/O多路复用

答: