Java基础知识-03-C-2020-07-22

Java基础知识-03-C-2020-07-22

日志编号说明
C-2020-07-22第一次创建

写在前头

针对下面的内容,我并没有说推荐按顺序看,或者挑需要的类看。
唯一的建议,假设想挑需要的类去看的话,至少在看你需要的类之前,先耐着性子的看一下I/O-字符流-ByteArray这个小章节。作为正片的第一个小节,我有详细的介绍它里面的每个方法,每一个构造方法,还有各种各样的概念。这些方法和概念很多都是继承自InputStream,OutputStream,因此越往后,随着篇幅的增长,这些重复的基本概念和方法我要么没写,要么一笔带过,有时候不利于理解。因此推荐看一下ByteArray章节之后,再挑选需要的类去看。

I/O

先上图
I/O分类

I/O开始正片之前的杂序

首先,上图中的内容,非常的不全。我只是列举了基础的应知应会的子类。

I/O流,一个从基础到高阶,自始至终都会存在的一个必须具备的知识。I/O流中有很多,各式各样的知识,各式各样的坑。这里光是记录基础知识。
从图上可以看到,I/O基本可以分为两大类,字符流和字节流。

  • 字节流
    按照byte[]字节数组去读取和写入的流,这种流啥都能干,一般以Stream结尾。
  • 字符流
    按照字符去读取和写入的流,更适合于处理文本信息,一般以Reader/Writer结尾。

除去字节流和字符流,还可以划分成节点流和处理流(包装流)。节点流是实际干活的流,如FilterInputStream,处理流是在节点流上包装的流,如InputStreamReader。这种划分,在实际使用里有更好的体会,因此对编码经验也比上面那种分类提出了更高的要求。为什么会出现节点流和处理流?因为节点流的API虽然啥都能干,但是干啥都觉得不好用。于是处理流的存在,就是在开发人员和节点流之间构建一道友善的桥梁。处理流对外提供的功能非常友善。
除此之外,还有一种常见的分类,就是输入流和输出流。输入流是InputStream和Reader作为基类,输出流是OutputStream和Writer作为基类。
本文采用了字节流和字符流的分类方式。

仔细观察上图会发现,不论是在字符流或者字节流的分类下,里面的内容基本上都是成对出现的,也就是一个input一般都会有一个output,意义是输入/输出。对于计算机而言,什么是输入,什么是输出?通俗来讲,往程序里面读是输入流,从程序里往其他地方写入,是输出流。
上面的描述中写道了基本上,结合图例会发现在FilterOutputStream中,比FilterInputStream里多了一个PrintStream类。其实这个类在日常工作中经常用到,就是System.out 。在Java源码中,是如下定义的。
标准输出流
如上图所示,常用的System.out和System.err都是PrintStream对象。除过这两个,还有个标准输入流System.in,在Java源码中,定义的是InputStream类型,但是InputStream是个抽象类,因此在最终实例化的过程里肯定是用InputStream的一个子类。
标准输入流图例从代码上的注释和明确看到,这个对象是响应键盘或者其他输入设备的输入信息。因此是更底层的东西。之所以猜测是更底层的东西去实现,如果要深究,我觉得是需要从System类的initializeSystemClass方法作为入口往里看。

下面I/O部分写作说明,下面的次级目录,按照xmind中上而下,从字节流到字符流,把成对的输入输出流放在一起进行记录。比如I/O-字节流-ByteArray表示的是,这个子目录里面会将I/O字节流中的ByteArrayInputStream和ByteArrayOutputStream。

I/O-字节流-ByteArray

ByteArray,字面意思是字节数组,说白了就是byte[]。
ByteArrayInputStream属于java.io包,是InputStream类的子类。
在ByteArrayInputStream类中,除过自身的构造方法之外并没有自己新定义什么方法,它的方法均是继承自InputStream中的方法(JDK8)

  • 构造方法
    ByteArrayInputStream中提供了两个构造方法。
/**
     * Creates a <code>ByteArrayInputStream</code>
     * so that it  uses <code>buf</code> as its
     * buffer array.
     * The buffer array is not copied.
     * The initial value of <code>pos</code>
     * is <code>0</code> and the initial value
     * of  <code>count</code> is the length of
     * <code>buf</code>.
     *
     * @param   buf   the input buffer.
     */
public ByteArrayInputStream(byte buf[]) {
        this.buf = buf;
        this.pos = 0;
        this.count = buf.length;
    }

上述构造方法里传入一个byte[]作为入参,官方注解中讲buf[]是输入缓冲区。通俗一些的解释,ByteArrayInputStream是一个输入流,输入流就是说明要从某个地方A读取信息,那此时,就是说把A的整体或者通过循环之后,把A的部分内容转成buf[],然后操作流对象从buf[]中读取。这个就是输入缓冲区的理解。
pos是表示从缓冲区的什么位置开始读取,count读取到什么位置为止。从代码上可以看出,每次读取了buf缓冲区的长度。

public ByteArrayInputStream(byte buf[], int offset, int length) {
        this.buf = buf;
        this.pos = offset;
        this.count = Math.min(offset + length, buf.length);
        this.mark = offset;
    }

和第一个构造方法不同的是,第二个构造方法中多了两个参数。offset表示读取的偏转量,原先是从第一个位置开始,现在是从指定位置开始。length是每次读取的长度。从代码中可以看到count此时等于(偏转量+长度)或者(buf长度)两者中的最小值。并且把mark的值设置成偏移量。

  • 其他方法
    • read():int
      read()表示每次从上一次读取的位置开始,读取下一个数据字节,并返回一个int值(0~255),如果已经把整个buf[]都读取完了,会返回(-1)这个特征值表示读取结束。

    • read(byte[],int,int):int
      和上面的read()方法类似,都是数据的功效,但是在实际使用中存在不同的地方。
      方法声明部分和参数注释如下:
      方法声明部分和参数注解第一个参数byte[] b 的作用是将ByteArrayInputStream中的数据读取到入参b中。
      第二个参数 int off,是起始偏移量。
      第三个参数 int len,每次要读取的长度。如果剩余长度小于len,会把剩余长度赋予len。
      这个方法和read方法类似,都是返回一个int值,不同的是,read()返回的是读取的内容,这个方法返回的是读取的长度,而读取的内容则是在入参 b中。
      这个方法的返回值有如下意义:
      如果返回-1,说明已经读取到最后的内容,这一次没有读到新的内容。
      如果返回0,则说明没有可以读取的内容了。
      如果返回其他值,则表示读取的字节串长度。

    • skip(long):long
      传入一个long值 n,从输入流中当前执行的位置跳过n个字节。如果输入流中剩余字节数 < n 的值,则只会跳过剩余的字节数。
      这个方法的返回值也是个long值,会把实际跳过的字节个数进行返回。

    • available():int
      这个方法会把从当前执行的位置,到流对象的结尾,中间所有不阻塞地可读取(或者可跳过)的字节数量进行返回。判断这个流有没有内容的时候可以用这个方法。

    • markSupported():boolean
      这个方法是和下面的mark和reset方法相关联的。
      首先,先看一下ByteArrayInputStream,从名字上看有两个核心意思,一个是ByteArray一个是InputStream。一个是字节数组,一个是输入流。这个方法和下面两个方法,是应对ByteArrayInputStream中ByteArray特性而言的。
      数组数据,说个简单的,一个int[],在数组里,有自己的下标,我们可以通过下标遍历数组里的对象,同时也可以说从第几个开始遍历。那这里面就牵扯到两个概念,一个是mark(标记),一个是重置(reset)。标记的意思就是,从第几个开始。重置的意思就是把下标重新放在最开始。
      因为ByteArrayInputStream是InputStream的子类,而InputStream有好多好多子类,那么就需要一个方法,来表示各个子类(如ByteArrayInputStream)是否满足标记和重置操作。
      对于ByteArrayInputStream来讲,它是支持标记和重置的,因此这个方法永远返回true。

    • mark(int):void
      标记,也就是指定下标的位置。

    • reset():void
      重置,把pos当前位置设置成之前mark时候的位置。如果之前没有mark过,那就会把pos设置成0。

    • close():void
      close方法继承自InputStream,InputStream类是实现了Closeable接口。
      这个方法有些特殊,具体代码如下:

/**
     * Closing a <tt>ByteArrayInputStream</tt> has no effect. The methods in
     * this class can be called after the stream has been closed without
     * generating an <tt>IOException</tt>.
     */
    public void close() throws IOException {
    }

这个方法是空的,关闭ByteArrayInputStream没有任何效果。可以在流关闭后调用此类中的方法,而不生成IOException。 这个部分我没有深究什么,使用中也是在read()之类的方法返回-1的时候,调一下。

ByteArrayOutputStream是java.io包下的类,是OutputStream的子类。
与ByteArrayInputStream的介绍顺序一样,还是从构造函数和其他方法这两个维度进行讲解。

  • 构造方法
    ByteArrayOutputStream类中有两个构造方法,代码如下。
/**
     * Creates a new byte array output stream. The buffer capacity is
     * initially 32 bytes, though its size increases if necessary.
     */
    public ByteArrayOutputStream() {
        this(32);
    }

第一个构造方法不用传递参数,在初始化ByteArrayOutputStream的时候创建一个对象,并且把输出缓冲区的长度设置成32

/**
     * Creates a new byte array output stream, with a buffer capacity of
     * the specified size, in bytes.
     *
     * @param   size   the initial size.
     * @exception  IllegalArgumentException if size is negative.
     */
    public ByteArrayOutputStream(int size) {
        if (size < 0) {
            throw new IllegalArgumentException("Negative initial size: "
                                               + size);
        }
        buf = new byte[size];
    }

第二个构造方法是创建一个指定缓冲区大小的ByteArrayOutputStream对象。

  • 其他方法
    • write(int):void
      将指定的字节(就是传入的int值)写入到当前的字节流中。
    • write(byte[]): void
      把byte[]中的数据写到当前输出流中。
    • write(byte[],int,int):void
      这个方法里有三个参数。
      第一个参数byte[],表示的意思是要把这个数组中的内容写入到当前的字节流对象中。
      第二个int off,针对byte[] 要从第几个元素开始把它的内容写入到字节流中,即起始的偏移量。
      第三个int len,表示从off开始,将多长(len)的数组元素写入到字节流中。
    • writeTo(OutputStream):void
      将当前的输出流中的全部内容写入到传入的输出流对象中。
    • reset():void
      将ByteArrayOutputStream对象中的count值设成0,意味着当前输出流里已经写的所有内容将被丢弃,当前的输出流可以被重用,并且可以重用之前声明的输出缓冲区。
    • toByteArray():byte[]
      创建一个新的byte[],将当前输出流的内容拷贝到新创建的byte[]中,并返回。
    • size():int
      返回当前输出流的大小,这里说的大小,可以直观的理解为当前输出流内已经有了多少个要用于数据的字节。
    • toString(String):String
      按照传入的字符集(这个方法的入参是字符集的名称)将输出流中的内容创建成一个String对象,并返回。
    • toString(int):String
      已经过时的方法,不进行赘述
    • close():void
      这个方法与ByteArrayInputStream中的一样,没有执行的代码。我对他的理解也只是结束的时候调用一下。
      除去上述方法之外,在ByteOutputStream内部还有ensureCapacity,grow,hugeCapacity这三个私有方法进行扩容。

I/O-字节流-Piped

PipedInputStream和PipedOutputStream是管道字节输入流和管道字节输出流。这两个类的名字与上两个类中最大的差异就在于Piped。
管道字节输入/输出流有一个明确的使用场景,就是在多线程之间用流的形式进行数据传递。假设有两个线程Thread-1和Thread-2,这两个线程之间通过流从Thread-1将数据传递给Thread-2,这种场景下, 需要在Thread-1中使用PipedOutputStream将Thread-1的数据写入管道内,再在Thread-2中使用PipedInputStream从管道中将数据读取出来。

PipedOutputStream
PipedOutputStream属于java.io包,OutputStream的子类。

  • 构造方法
    在PipedOutputStream类中,提供了两个构造方法。代码如下:
/**
     * Creates a piped output stream that is not yet connected to a
     * piped input stream. It must be connected to a piped input stream,
     * either by the receiver or the sender, before being used.
     *
     * @see     java.io.PipedInputStream#connect(java.io.PipedOutputStream)
     * @see     java.io.PipedOutputStream#connect(java.io.PipedInputStream)
     */
    public PipedOutputStream() {
    }

第一个构造方法是无参构造方法,在方法注释上可以看出来,这个方法只是创建了一个PipedOutputStream对象,并且这个对象没有连接到一个PipedInputStream上。
对于PipedOutputStream对象而言,在使用之前,必须连接到一个PipedInputStream对象上,不论是通过接受者或者发送者执行这个链接操作。

/**
     * Creates a piped output stream connected to the specified piped
     * input stream. Data bytes written to this stream will then be
     * available as input from <code>snk</code>.
     *
     * @param      snk   The piped input stream to connect to.
     * @exception  IOException  if an I/O error occurs.
     */
    public PipedOutputStream(PipedInputStream snk)  throws IOException {
        connect(snk);
    }

这个构造方法中传入了一个PipedInputStream对象,在构造方法里执行了connect()方法。这个方法就是在第一个构造方法的解释里提到的,连接的方法。

  • 其他方法
    • connect(PipedInputStream): void
      方法中传入一个PipedInputStream对象,把传入对象连接到PipedOutputStream上。这个连接的过程里,有一个要求。连接的双方不能都不能连接或者被连接到其他管道流上。
      在PipedInputStream类中,也有一个connect方法,起到了同样的作用。
      在连接上之后,才可以进行写操作,读操作(PipedInputStream)。
    • write(int): void
    • write(byte[],int,int): void,
      这两个方法都是重写父类OutputStream中的方法,具体的用法和ByteArrayOutputStream中的一样,这里不进行赘述。
    • flush(): void
      flush方法很重要。代码如下
      flush方法代码方法注解上写的很明白,这个方法会强制流中所有的字节写出,并且通知关联到这个管道的,等待读取数据的readers(也就是之前连接上的PipedInputStream)
    • close(): void
      在close方法里,调用了关联在这个输出流上的PipedInputStream中的receivedLast()方法。这个方法的具体内容,会在下面的PipedInputStream中讲解。

PipedInputStream
PipedInputStream,java.io包,InputStream类的子类。

  • 构造方法
    这个类中有四个构造方法。
/**
     * Creates a <code>PipedInputStream</code> so
     * that it is not yet {@linkplain #connect(java.io.PipedOutputStream)
     * connected}.
     * It must be {@linkplain java.io.PipedOutputStream#connect(
     * java.io.PipedInputStream) connected} to a
     * <code>PipedOutputStream</code> before being used.
     */
    public PipedInputStream() {
        initPipe(DEFAULT_PIPE_SIZE);
    }

第一个构造方法,创建一个PipedInputStream对象,初始的循环缓冲区大小1024。

/**
     * Creates a <code>PipedInputStream</code> so that it is not yet
     * {@linkplain #connect(java.io.PipedOutputStream) connected} and
     * uses the specified pipe size for the pipe's buffer.
     * It must be {@linkplain java.io.PipedOutputStream#connect(
     * java.io.PipedInputStream)
     * connected} to a <code>PipedOutputStream</code> before being used.
     *
     * @param      pipeSize the size of the pipe's buffer.
     * @exception  IllegalArgumentException if {@code pipeSize <= 0}.
     * @since      1.6
     */
    public PipedInputStream(int pipeSize) {
        initPipe(pipeSize);
    }

第二个构造方法,创建一个PipedInputStream对象,自定义了循环缓冲区的大小。

/**
     * Creates a <code>PipedInputStream</code> so
     * that it is connected to the piped output
     * stream <code>src</code>. Data bytes written
     * to <code>src</code> will then be  available
     * as input from this stream.
     *
     * @param      src   the stream to connect to.
     * @exception  IOException  if an I/O error occurs.
     */
    public PipedInputStream(PipedOutputStream src) throws IOException {
        this(src, DEFAULT_PIPE_SIZE);
    }

第三个方法,创建一个PipedInputStream对象,循环缓冲区大小1024,传入一个PipedOutputStream对象,并且把这两个对象进行连接。

/**
     * Creates a <code>PipedInputStream</code> so that it is
     * connected to the piped output stream
     * <code>src</code> and uses the specified pipe size for
     * the pipe's buffer.
     * Data bytes written to <code>src</code> will then
     * be available as input from this stream.
     *
     * @param      src   the stream to connect to.
     * @param      pipeSize the size of the pipe's buffer.
     * @exception  IOException  if an I/O error occurs.
     * @exception  IllegalArgumentException if {@code pipeSize <= 0}.
     * @since      1.6
     */
    public PipedInputStream(PipedOutputStream src, int pipeSize)
            throws IOException {
         initPipe(pipeSize);
         connect(src);
    }

创建一个PipedInputStream,自定义它的循环缓冲区大小,并且传入了PipedOutputStream对象,将这两个管道进行连接。

  • 其他方法
    • connect(PipedOutputStream): void
    • read():int
    • read(byte[],int,int):int
    • available() :in
      上面的方法与ByteArrayInputStream中的一样,从输入流中读取数据,不进行赘述。
      这里有一个点是和先前讲过的I/O流不同的地方。在之前介绍的内容里,输入和输出流是独立的存在的,但是管道流则不是。管道输入输出流二者必须作为一个整体才能使用。并且在数据传输中是通过三步走的形式,即:写入->接收->读取。下面的三个方法就是中间(接收环节)方法。
    • receive(int):void
      一次往输入流中读取一个字节。如果在读取时,没有任何一个可被读取的字节,当前方法会阻塞(线程的内容下篇会讲)。
    • receive(byte[],int,int):void
      和read(byte[],int,int)方法类似,只是这个方法不会把接收长度当做返回值进行返回。这个方法是说,按照起始偏转量,接收一段指定长度的字节,并且把接收到的字节放在传入的byte[]中去。在没有足够多的输入元素之前,这个方法会被阻塞。
    • receiveLast():void
      这个方法,会在PipedOutputStream对象调用它的close()方法时被调用,表示已经把所有要的数据写入到管道流对象中。并且,会唤醒等待的receive方法。
    • close(): void
      这个方法表示接受者(PipedInputStream)主动进行关闭。

I/O-字节流-Filter

在这篇博客开头讲到过,除过按照字节流字符流,输入流输出流之外,还有一个分类方式就是节点流和处理流。节点流是真实干活的流,处理流是包装在节点流上一层,给用户提供更便利API的流。
这里要讲到的FilterInputStream就是这样的一个处理流。
FIlterInputStream和FilterOutputStream里包含了多个子类对象。其中最常用到的就是BufferdInputStream,BufferedOutputStream,DataInputStream和DataOutputStream。
顺嘴一提,FilterInputStream,FilterOutputStream应用了装饰者模式,后续写设计模式的时候会详细介绍这些内容。
在介绍具体的子类之前,先说一下这两个父类,也就是FilterInputStream和FilterOutputStream。
这两个父类中,最值得提到的就是构造方法,并且这种形式的构造方法在他们的子类中也得到了具体的体现。
FilterInputStream构造方法:

/**
     * Creates a <code>FilterInputStream</code>
     * by assigning the  argument <code>in</code>
     * to the field <code>this.in</code> so as
     * to remember it for later use.
     *
     * @param   in   the underlying input stream, or <code>null</code> if
     *          this instance is to be created without an underlying stream.
     */
    protected FilterInputStream(InputStream in) {
        this.in = in;
    }

在上面的构造方法里可以看出来, 这个构造方法里传入了一个InputStream作为参数。这一点和之前提到的都不一样。
FilterOutputStream构造方法:

/**
     * Creates an output stream filter built on top of the specified
     * underlying output stream.
     *
     * @param   out   the underlying output stream to be assigned to
     *                the field <tt>this.out</tt> for later use, or
     *                <code>null</code> if this instance is to be
     *                created without an underlying stream.
     */
    public FilterOutputStream(OutputStream out) {
        this.out = out;
    }

同样,在FilterOutputStream中也传入了一个OutputStream对象作为参数。

I/O-字节流-Filter-Buffered

在FilterInput/OutputStream中,包含多个常用子类,这里先介绍第一对,BufferedInputStream和BufferedOutputStream。

BufferedInputStream
BufferedInputStream属于java.io包下,是FilterInputStream类的子类。

  • 构造方法
    在BufferedInputStream中提供了两个构造方法,具体代码:
/**
     * Creates a <code>BufferedInputStream</code>
     * and saves its  argument, the input stream
     * <code>in</code>, for later use. An internal
     * buffer array is created and  stored in <code>buf</code>.
     *
     * @param   in   the underlying input stream.
     */
    public BufferedInputStream(InputStream in) {
        this(in, DEFAULT_BUFFER_SIZE);
    }

上面的构造方法里传入一个InputStream对象作为实际操作数据的对象。

/**
     * Creates a <code>BufferedInputStream</code>
     * with the specified buffer size,
     * and saves its  argument, the input stream
     * <code>in</code>, for later use.  An internal
     * buffer array of length  <code>size</code>
     * is created and stored in <code>buf</code>.
     *
     * @param   in     the underlying input stream.
     * @param   size   the buffer size.
     * @exception IllegalArgumentException if {@code size <= 0}.
     */
    public BufferedInputStream(InputStream in, int size) {
        super(in);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }

这个构造方法里不仅传入了InputStream,还制定了缓存的大小。

  • 其他方法

    • available():int
    • markSupported():boolrean
    • mark(int):void
    • read():int
    • read(byte[],int,int):int
    • reset():void
    • skpi(long):long
      上述方法均是从父类中继承来的,整体使用和意义不过多进行赘述。
      这里主要讲一下两个read中的内部过程。
      对这个方法进行讲解的时候,会结合ByteArrayInputStream进行对比讲解。
    对象类ByteArrayInputStreamBufferedInputStream
    是否需要装载其他InputStream对象
    read()内部执行过程这个过程里使用到了ByteArrayInputStream中的三个成员变量。如果当前位置(pos)<总数(count),就返回buf[](初始化时传入的)的下一个元素。如果>=总数,则返回-1这个过程里使用到了BufferedInputStream中的三个成员变量。如果当前位置(pos)>=结尾(count),调用BufferedInputStream中的fill()方法,调用之后再次对pos和count进行比对。如果同样是pos>=count,则返回-1.如果没有超过,则调用内部方法getBufIfOpen():byte[]返回buf中的下一个元素
    read(byte[],int.int)内部执行过程同样先是比较当前位置和总数的关系。如果pos>=count,返回-1.如果不是,用count-pos的结果与当前方法的第三个入参去最小值。调用System.arraycoyp()方法,将ByteArrayInputStream中的buf[]按照起始位置,偏移量,读取长度这三个值拷贝到当前方法的第一个入参(byte[])中,并将copy的长度进行返回这个方法中实际上调用到了BufferedInputStream中的四个私有方法,分别是fill()加载数据,getBufIfOpen()返回buf[]对象,getInIfOpen()返回InputStream对象,read1(byte[],int,int)从流中读取数据。这些方法内部执行逻辑详情推荐自己看内部执行过程,整个过程不太好用原因说清楚
    • fill():void
      核心方法
      简单而言这个方法是给缓冲区里加载数据用的。
      内部逻辑比较长,不方便用语言描述整体思想。主要讲,就是给缓冲区加载数据,并且在这个位置中把pos(当前位置),count(总数),markpos(调用mark方法时的当前位置pos)marklimit(调用mark时的入参)进行了一连串的赋值和比较。
    • getBufIfOpen():byte[]
      从方法名可以明白这个方法的意思,get Buf If Open,判断当前对象内的成员变量buf[]是否为null,如果是null抛出IOException,如果不是,返回buf[]。这个buf[],是在构造方法中的InputStream的buf[]。
    • getInIfOpen():InputStream
      判断构造方法中传入的InputStream是否为null,如果是,则抛出IOException,否则返回InputStream
    • read1(byte[],int,int):int
      核心方法
      从read1方法的入参就可以猜测到,在调用read(byte[],int,int)方法的内部是调用了read1方法。
      在read1方法内部,根据总数(count),当前位置(pos),要读取的长度(这个方法的第三个入参)进行各类比较,并且在有必要的时候执行fill()方法加载数据,最终将数据数据从输入流里写入到第一个入参里。

综述,使用BufferedInputStream会带来最大的好处:
提升效率。由于缓冲区的存在,因此在使用BufferedInputStream的时候会比直接使用节点流的效率更高。
实例代码如下:

package com.phl.test;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class IOTest {
    //替换成自己的文件地址即可
    private final String path = "file_path";

    public IOTest() {
    }

    public void readByBuffer(){

        try {
            FileInputStream fileInputStream = new FileInputStream(new File(this.path));
            //不指定缓冲区大小的话,默认大小是8192
            BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
            byte[] bytes = new byte[1024];
            //缓冲区大小8192,每次读取大小1024,也就是说我读8次,才会执行一次IO
            while (bufferedInputStream.read(bytes) != -1){ }
            bufferedInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void readByInput(){
        try {
            FileInputStream fileInputStream = new FileInputStream(new File(this.path));
            byte[] bytes = new byte[1024];
            //每次读取1024,因为没有缓冲区存在,所以每读一个1024,我就会执行一次IO
            while (fileInputStream.read(bytes) != -1){ }
            fileInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        IOTest ioTest = new IOTest();
        long startTime1 = System.currentTimeMillis();
        ioTest.readByBuffer();
        long endTime1 = System.currentTimeMillis();
        System.out.println(endTime1 - startTime1);

        long startTime2 = System.currentTimeMillis();
        ioTest.readByInput();
        long endTime2 = System.currentTimeMillis();
        System.out.println(endTime2 - startTime2);
    }
}

核心点解释:

  1. 上方两种读取操作过程,BufferedInputStream是IO->缓冲区->读取块,FileInputStream是IO->读取块。
  2. 从上面的逻辑来看,当我的缓冲区远大于读取块时,会明显降低IO开销,同时由此产生的缓冲区->读取块的时间消耗(从内存A往内存B读取)可以忽略不计。但是如果缓冲区比读取块大不了多少(如缓冲区1024,读取块1000),那么不仅不能明显的降低IO开销,同时比起直接用InputStream而言,增加了一次缓冲区->读取块的过程,就得不偿失了。
  3. 针对上面的例子,你可以更改path成你电脑中的某个歌曲,某个电影的地址,并且更改缓冲区,读取块的参数值,对多次结果进行观察,就会证明我上述结论。

BufferedOutputStream
BufferedOutputStream与BufferedInputStream类似,都是内部加载了一个真实处理数据的节点流,自己本身作为处理流存在。
在BufferedOutputStream中,同样提供了两个构造方法。

/**
     * Creates a new buffered output stream to write data to the
     * specified underlying output stream.
     *
     * @param   out   the underlying output stream.
     */
    public BufferedOutputStream(OutputStream out) {
        this(out, 8192);
    }

上面的构造方法中,不指定缓冲区大小,默认8192,下面的构造方法会指定缓冲区大小。

/**
     * Creates a new buffered output stream to write data to the
     * specified underlying output stream with the specified buffer
     * size.
     *
     * @param   out    the underlying output stream.
     * @param   size   the buffer size.
     * @exception IllegalArgumentException if size &lt;= 0.
     */
    public BufferedOutputStream(OutputStream out, int size) {
        super(out);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }

其他方法不过多进行赘述,通过之前的BufferedInputStream中就可以看出来,在使用这些处理流的时候,基础API使用都是一个路数,毕竟他们都直接或者间接继承自InputStream或者OutputStream。真正核心问题往往是处理流内部的private方法。

  • flushBuffer()
    这个方法内部,调用了节点流的write(byte[],int,int)方法。flushBuffer()方法本身会在调用处理流的write(int),write(buf[],int,int),flush()方法时,在这些方法内部被调用。

同样,在输出流的使用时,使用带缓冲区的流比直接使用节点流的效率更高,测试例子可以仿照上面读取的例子进行编写。

I/O-字节流-Filter-Data

作为另一个处理流,DataInputStream和DataOutputStream的构造方法中都需要传入一个输入\输出流对象作为入参。
DataInputStream和DataOutputStream最大的特点在于,这两个类分别实现了接口DataInput和DataOutput。

注意,DataInputStream和DataOutputStream需要一起使用。下面就介绍为什么需要一起使用。
作为处理流,这两个流对象提供了他们所特有的方法。
DataInputStream方法如下:
DataInputStream方法结构图
通过截图可以直观的发现,在DataInputStream中没有重写read(),read(byte[],int,int)这两个核心读取数据的方法,而是在类中通过实现DataInput接口中的方法来进行数据读取,同时还有自己定义的readUTF()方法进行数据读取。

接口中的方法,引用readInt()为例,对整体接口方法进行解读。在readInt()代码如下:

/**
     * See the general contract of the <code>readInt</code>
     * method of <code>DataInput</code>.
     * <p>
     * Bytes
     * for this operation are read from the contained
     * input stream.
     *
     * @return     the next four bytes of this input stream, interpreted as an
     *             <code>int</code>.
     * @exception  EOFException  if this input stream reaches the end before
     *               reading four bytes.
     * @exception  IOException   the stream has been closed and the contained
     *             input stream does not support reading after close, or
     *             another I/O error occurs.
     * @see        java.io.FilterInputStream#in
     */
    public final int readInt() throws IOException {
        int ch1 = in.read();
        int ch2 = in.read();
        int ch3 = in.read();
        int ch4 = in.read();
        if ((ch1 | ch2 | ch3 | ch4) < 0)
            throw new EOFException();
        return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
    }

在readInt()方法里,因为int类型数据占4个字节,因此在readInt()方法中,通过节点流执行了4次read()方法,并把执行结果拼接成最终的int类型数据进行返回。
至于每个读取,以及后续会说的写入方法里,针对不同类型到底是多少字节,这里不过多讨论,感兴趣可以通过源码进行了解。
除过readInt()这种,直接通过节点流执行read()方法之外,比如readLong()方法内部,通过调用同样是实现自接口的readFully(byte[],int,int)方法,而在readFully()中,则是通过节点流调用read(byte[],int,int)的形式完成数据读取。
综上,可以看到在DataInputStream中,通过读取定长的字节,以及对读取结果的拼接和转换,继而实现了从输入流中直接读取基础数据类型的功能。而先前提到的==readUTF()==方法就更不可思议了,会从输入流中读取到String类型。

DataOutputStream方法如下:

DataOutputStream方法结构
通过上图可以发现,DataOutputStream类中与DataInputStream类似,实现了很多对基础数据类型以及String数据类型的写入方法。
对于其他的节点流而言,想要精准的控制输入或者输出结果的类型,都需要把要写入或者读取的数字转byte[],才能执行。但是在使用DataInputStream和DataOutputStream的过程里,这个过程很明显被简化了,操作更顺畅。

现在说,为什么这两个要一起使用?
与其说要一起使用,更应该说是非常推荐一起使用,不论是在输入还是输出的过程中,因为实际干活的都是节点流,所以使用节点流同样可以完成现在的过程。但是,明显在使用这样的处理里极大的降低了计算字节length与数据类型关系时出现的错误。

I/O-字节流-Object

ObjectInputStream和ObjectOutputStream是操作对象的流。这里就得提到一个知识点,序列化和反序列化。我们都知道在对Java对象进行序列化和反序列化的时候,需要实现Serializable接口或者Externalizable接口。我一般都会选择Serializable接口,这个接口使用起来更简单,并且常规的序列化功能就能满足我的场景。
在实现了Serializable接口之后,还需要给当前类指定一个serialVersionUID,不论这个serialVersionUID是生成的也好,自己写的1L也罢,这个不是必须有的,但是如果不写,所有的IDE都会有个提示,比较闹心。同时,如果这个类中的部分属性不要进行序列化,那么需要给这个属性前面加上transient关键字,这样在反序列化的时候,有被transient关键字修饰的属性,如果是基础数据类型,那对应的值就是默认值,如果不是基础数据类型,那对应的就是null。这个是对序列化和反序列化最基础的理解了。
当做完上述内容,就满足了序列化和反序列化的基础条件。在进行序列化和反序列化的操作是,就会用到上述提到的两个流对象:ObjectInputStream和ObjectOutputStream。其中,序列化的时候是把对象写到某个地方,那对应的ObjectOutputStream就是序列化流,反之,ObjectInputStream就是反序列化流。

ObjectInputStream和ObjectOutputStream源码内部很是复杂,这里采用简单的语言进行描述,如有兴趣,可通过阅读源码进行了解。
举一个最常见的例子,我现在需要通过文件进行序列化和反序列化。

  1. 序列化过程
    在进行序列化时,首先我需要创建一个节点流来干活,因为是通过文件进行序列化,那我可以创建一个FileOutputStream作为节点流,再创建一个ObjectOutputStream(处理流)进行操作(节点流对象是这个构造方法的入参)。假设要传递的对象是A,那么我可以通过调用ObjectOutputStream对象的writeObject(A a)方法实现序列化。执行返程后,在FileOutputStream指定的位置就会有我的序列化文件。
  2. 反序列化过程
    在进行反序列化时,因为场景中说的是通过文件,因此我要创建一个FileInputStream作为节点流,同时创建一个ObjectInputStream(处理流)进行操作(节点流对象是这个构造方法的入参)。假设要进行反序列化的对象是A,那么就可以调用ObjectInputStream对象的readObject()方法,并对返回的结果进行强制类型转换,就能得到反序列化的A对象。
    当然,上述整个过程是一个非常简单和入门的操作,只能当个demo乐呵乐呵。更难的使用方法,不打算在基础知识这个分栏下写,当以后写Java高级应用的时候,会对这部分内容进行拔高。

I/O-字节流-File

之所以把FileInputStream和FileOutputStream放在最后讲,因为这两个流对象在日常使用中最为频繁,网上各类资料,各种写法应该也是最多的。放在这里是希望通过上面多个字节流的讲解,让大家对字节流有了一个更深的理解后再去看这里的内容,希望得到的效果就是让大家觉得FileInputStream和FileOutputStream本身并不神秘。
在将这部分内容的时候,我打算通过以下的形式将,而不是像上面一样常用方法都分开去写。因为当大家耐着心思从上面一路读下来之后,我这种写法只会让你觉得清爽,省事儿。

  • FileInputStream
    FileInputStream内部结构图
    FileInputStream内部结构图
    上图中用三种不同颜色的框进行了分类。
    红色框,重写了自InputStream的方法和抽象方法,这些方法就是我们平时使用时经常要用的API。因为是继承自InputStream类,红色框里的方法与上面写道的每个InputStream的子类都是大同小异,不过多进行赘述。

    蓝色框,这个是整个类中唯一需要留心的地方。类似于BufferedInputStream,他在使用的时候也是直接调用InputStream提供的方法,但是在那些方法内部都在调用自己提供的类似fill(),getInIfOpen(),getBufIfOpen()这些方法来完成内部逻辑。在FileInputStream中也是如此,红色框内的方法在具体实现的过程里,调用了蓝色框内的方法(除了initDs()这是一个native方法,写在静态代码块里,这个是JVM层面的东西,这里不提了)。
    绿色框,构造方法。

  • FileOutputStream
    FileOutputStream内部结构图

FIleOutputStream内部结构
与FileInputStream类内部惊人的相似。
红色框,继承自InputStream类的方法,也是我们最常用的API。
蓝色框,红色框中的方法的内部调用了蓝色框里的方法。
绿色框,构造方法。

至此,这篇I/O的博客中,字节流的部分已经讲完了,写下来的会是字符流。

开始之前,说一个承上启下的话。字节流和字符流从字面来看,一字之差,这也说明了字符流只能处理纯文本的内容。而对于计算机数据来说,不论是什么内容,什么类型,什么后缀,都可以转化成字节数组的形式存在,因此对于任意一个要用流去处理的数据而言,使用字节流都不会出错。只是,在应对纯文本内容是,字符流有更便捷的使用体验。

前面介绍字符流的时候,那些类都是java.io包下,直接或者间接继承自InputStream或者OutputStream。
下面的字符流,这些类都是java.io包下,直接或者间接继承自Reader或者Writer。

I/O-字符流-CharArray

CharArrayReader和CharArrayWriter,从名字就能看出来,这个是应对字符的,并且是Char[]。注意了,注意了,在正文一开始的一个类是ByteArray,现在这里是CharArray因此他们的用法,理解上都是非常的相似。
最为字符流整体章节中的第一个小章节,我会把这里的每个方法都仔细写一遍,但是剩下的几个类就只是挑应知应会去写。

CharArrayReader
CharArrayReader类,是java.io包下,继承自Reader类。在CharArrayReader类中,提供了两个构造方法。

/**
     * Creates a CharArrayReader from the specified array of chars.
     * @param buf       Input buffer (not copied)
     */
    public CharArrayReader(char buf[]) {
        this.buf = buf;
        this.pos = 0;
        this.count = buf.length;
    }

创建一个CharArrayReader对象,入参是要进行读取的char[],并且将CharArrayReader对象的开始位置设成0,总数设成入参的length

/**
     * Creates a CharArrayReader from the specified array of chars.
     *
     * <p> The resulting reader will start reading at the given
     * <tt>offset</tt>.  The total number of <tt>char</tt> values that can be
     * read from this reader will be either <tt>length</tt> or
     * <tt>buf.length-offset</tt>, whichever is smaller.
     *
     * @throws IllegalArgumentException
     *         If <tt>offset</tt> is negative or greater than
     *         <tt>buf.length</tt>, or if <tt>length</tt> is negative, or if
     *         the sum of these two values is negative.
     *
     * @param buf       Input buffer (not copied)
     * @param offset    Offset of the first char to read
     * @param length    Number of chars to read
     */
    public CharArrayReader(char buf[], int offset, int length) {
        if ((offset < 0) || (offset > buf.length) || (length < 0) ||
            ((offset + length) < 0)) {
            throw new IllegalArgumentException();
        }
        this.buf = buf;
        this.pos = offset;
        this.count = Math.min(offset + length, buf.length);
        this.markedPos = offset;
    }

创建一个CharArrayReader对象,有三个参数。
第一个参数是要读取的char[].
第二个参数是起始偏移量。
第三个参数是(偏移量+长度)与要读取的char[]长度之间的最小值,并且把markedPos设置成偏移量。
除去上述的构造方法,CharArrayReader类中还有如下方法

  • ensureOpen():void
    这个方法在CharArrayReader中除了close()方法之外的其他方法里都被调用过。
    这个方法内部是判断当前对象中的buf(初始化时传入的char[])是否为null。如果是null,会弹出IO异常,如果不是null,没有任何操作。这个方法的意义是在进行其他真实数据操作之前,先确认数据是存在的。
  • ready():boolean
    返回boolrean值,true标识当前流对象可以进行读取,false表示不能读取。这个方法是个线程同步方法,对于CharArrayReader而言,这个对象永远可以被读取。但是,如果已经把所有内容都读取完了,则会返回false。
  • read():int
    从buf中根据当前pos读取下一个元素。如果已经到了末尾,会返回-1作为结束标识。如果不是,则返回对应的元素(这里虽然是char,但是返回了int,所以要返回结果的时候,需要进行转换)。
  • read(char[],int,int):int
    这个方法是把CharArrayReader中的内容读取到一个目标char[]中去。这个方法有三个参数。
    第一个参数,char[],用来接收从CharArrayReader中读取出来的内容。
    第二个参数,起始偏移量。
    第三个参数,要读取的长度。如果CharArrayReader里的buf剩余内容不够读取的长度,则只读取剩余的长度。
    这个方法的返回值是每一次读取的长度。
  • markSupported():boolean
    这个方法表示CharArrayReader类是否支持mark和reset方法。永远返回 true。
  • mark(int):void
    mark方法,之前很多流对象的mark方法,都是把传入的参数设成pos,把当前位置设成markpos,但是在这里却不一样。源码的注解上写,因为这个流对象中传入的是一个字符数组,没有实际的限制,因此传入的参数被忽略。代码如下:
/**
     * Marks the present position in the stream.  Subsequent calls to reset()
     * will reposition the stream to this point.
     *
     * @param  readAheadLimit  Limit on the number of characters that may be
     *                         read while still preserving the mark.  Because
     *                         the stream's input comes from a character array,
     *                         there is no actual limit; hence this argument is
     *                         ignored.
     *
     * @exception  IOException  If an I/O error occurs
     */
    public void mark(int readAheadLimit) throws IOException {
        synchronized (lock) {
            ensureOpen();
            markedPos = pos;
        }
    }

真正做的事情只是记录下了在这里执行了mark操作,而没有理会传入的参数。

  • reset():void
    把当前位置设置成最近一次执行mark的位置。如果没有,会把当前位置设置成最开始的位置。
  • skip(long):long
    跳过指定长度的元素,返回值是真实跳过的元素。因为可能buf中剩余的元素小于要跳过的元素个数。
  • close():void
    这个close方法里面将buf设置成了null。

CharArrayWriter
CharArrayWriter类,是java.io包下,继承自Writer类。
CharArrayWriter类中提供了两个构造方法。

/**
     * Creates a new CharArrayWriter.
     */
    public CharArrayWriter() {
        this(32);
    }

创建CharArrayWriter对象,并给一个默认的缓冲器,大小为32.

/**
     * Creates a new CharArrayWriter with the specified initial size.
     *
     * @param initialSize  an int specifying the initial buffer size.
     * @exception IllegalArgumentException if initialSize is negative
     */
    public CharArrayWriter(int initialSize) {
        if (initialSize < 0) {
            throw new IllegalArgumentException("Negative initial size: "
                                               + initialSize);
        }
        buf = new char[initialSize];
    }

创建一个CharArrayWriter对象,自定义了缓冲区大小。
CharArrayWriter类中提供了如下方法

  • append(char):CharArrayWriter
    方法内部调用了write(int)
  • append(CharSequence):CharArrayWriter
    方法内部调用了write(String,int,int),其中第二个参数off是0,第三个参数是CharSequence的长度。
  • append(CharArrayWriter,int,int):CharArrayWriter
    方法内容调用了write(String,int,int)
  • write(int):void
    给CharArrayWriter对象里的buf后面续上传入的参数(强转成char)
  • write(char[],int,int):void
    给CharArrayWriter对象里的buf后面续上传入的参数。
    这个方法中有三个参数。
    第一个参数,char[],要写入到buf中的内容。
    第二个参数,int,要从char[]的那个位置开始往buf里写。
    第三个参数,int,从开始的位置往buf里面总共写多少个字符。
  • write(String,int,int):void
    和上一个方法一样,只是第一个参数从char[]换成了String类型。
  • toCharArray():char[]
    把buf中的内容以char[]的格式进行返回。
  • wtireTo(Writer):void
    把CharArrayWriter中buf里的内容写到传入的Writer中。
  • flush():void
    冲刷数据
  • reset():void
    重置缓冲区,以便您可以再次使用它,而不会丢弃已分配的缓冲区。
  • size():int
    返回buf中的元素个数。
  • close():void
    关闭。

I/O-字符流-Piped

PipedReader,PipedWriter用于在不同线程之间通过字符流传递数据。他们都是java.io包下,分别继承自Reader和Writer类。PipedReader与PipedWriter这一组和PipedInputStream与PipedOutputStream非常类似。PipedReader和PipedWriter需要组队使用。

PipedWriter类内部结构如下。
PipedWriter内部结构
在PipedWriter类中,提供了两个构造方法。
无参构造方法会创建一个没有连接到PipedReader的PipedWriter对象。
有参构造方法中会传入一个PipedReader,创建的PipedWriter会与入参的PipedReader连接到一起。
在写入之前,必须确保当前的Writer已经和一个Reader连接在一起,手动连接通过connect(PipedReader)方法。
连接之后,通过write(int),write(char[],int,int)方法进行数据写入。
flush()方法进行冲刷数据,close()方法关闭输出流。

PipedReader类内部结构如下。
PipedReader内部结构
同样都是管道流的形式,因此在管到字符流的写入和读取过程里,与上面提到的管道字节流的顺序一样,写入->接收->读取,三步走。
在PipedReader中有四个构造方法,这四个构造方法用来创建PipedReader对象。在构造方法了可以指定接收缓冲区的大小,要进行连接的PipedWriter对象。
这个类中的方法与Piped字节流中的非常类似,详情见上面的Piped字节流章节。

I/O-字符流-Filter

FilterReader和FilterWriter只是一个非常简单,且,在我实际应用里也非常用不太到的处理流,切记,这两个是处理流。唯一需要注意的,处理流在初始化的时候,需要传入节点流。这两个初始化的时候,分别需要传入Reader对象和Writer对象。

I/O-字符流-Buffered

BufferedReader和BufferedWriter在日常使用中很频繁。首先,这两个是处理流,因此在创建的时候需要传入节点流作为入参。
BufferedReader类中最常用个方法是readLine():String,一次读取一行文本。
对应的,BufferedWriter中最常用的方法是writeLine(String,int,int):void
这两个类在网上的解释,用法,demo一搜一大把,并且这两个类与上面的BufferedInputStream和BufferedOutputStream非常类似,这里就过多介绍。不论是从其他地方查,或者是根据上面的BufferedInputStream,BufferedOutputStream进行类比,都可以得到详实的解释。

I/O-字符流-InputStreamReader/OutputStreamWriter

InputStreamReader和OutputStreamWriter是处理流。
单单把这两个处理流标黄,是因为这两个处理流做了一个很厉害的事情。
从最开始洋洋洒洒写了这么多,文章的标题都是字节流,字符流这样进行分类,并且中间也说了,字节流可以处理所有情况,字符流只能处理文本内容。那么,既然字节流可以应对所有的情况,那怎么才能把一个字节流,转成一个字符流?
InputStreamReader,能把InputStream包装成Reader。
OutputStreamWriter,能把OutputStream包装成Writer。
这是这两个类最大,最厉害,最需要记忆的地方。其他没啥。

I/O-字符流-File

FileReader,FileWriter用来写入写出文件的字符流。
没太多值得介绍的,网上的东西太多,并且他的用法也与上面讲过的FileInputStream和FileOutputStream非常类似。这里不过多进行赘述。

I/O-图上有两个没讲的在这里

PrintStream,处理流,我们经常写的sout,里面用到的就是PrintStream对象。
这个对象很厉害,有很多打印的方法,并且可以打印到很多地方。
PrintWriter,处理流,构造方法里可以传Writer和OutputStream对象。用法也很广泛。

综述,上面讲了很多种IO对象,他们普遍成对出现,字节字符也成对出现。内部的结构,思想都很相似,很推荐去看看源码,这样就非常好理解IO是什么了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值