Java IO流理解指南(二)字节流

深入理解Java中IO流的使用技巧

Posted by yourzeromax on April 17, 2018

本文是系列文章第二篇,主要介绍字节输入流和字节输出流

字节流和字符流

在Java语言环境中,字节和字符的所占空间大小为:

名称 字节数 bit数
byte 1 8
char 2 16

字节是数据最小的基本单位,并且计算机系统中只有字节的概念,但是往往在非英语系语言中是需要进行编码的,这时候就出现了字符的概念,Java环境中一个字符相当于两个字节。

应该说,字节流和字符流本质上是没有区别的,并且在一定程度上是可以进行转换的,但是字节流更加面向的是图像、二进制文件等原始数据,而字符流面向的是根据编码集对字节流翻译之后的产物。

有时候,Java初学者搞不清楚究竟哪个是字节流,哪个是字符流,我给大家科普一下:一般XXXXStream的便是字节流,而Reader和Writer结尾的就是字符流。

字节流

我们先来看一下继承图:


好好看,认真学!所有的输入(输出)字节流都是继承自InputStream(OutputStream)的哦。并且输入输出流基本上是成对称式的,比如有一个FileInputStream,就肯定有一个FileOutStream,也就是说,在学习字节流的时候,可以对比着来学,这样记忆要简单些。

输入字节流

输入字节流的关键方法是在InputStream中实现的,我们先来看看InputStream所提供的抽象方法们。
一般情况下,我们关注的是FileInputStream、DataInputStream、BufferedInputStream,其余的流,之后再介绍。

InputStream

源码

源码贴出来是方便大家进阶时候看的,在初学的时候,建议直接跳过源码内容。

/**
 * 该抽象类是所有字节输入流的超类。
 */
public abstract class InputStream implements Closeable {

    // 该变量用于确定在skip方法中使用的最大缓存数组大小。
    private static final int MAX_SKIP_BUFFER_SIZE = 2048;

    /**
     * 从输入流中读取下一字节数据。返回的字节值为一个范围在0-255之间的int数。若由于到达流的尾部而没有字节可获取,则返回-1.
     * 直到数据可达,检测到流的末尾或者抛出一个异常,该方法才停止。
     *
     * @return     下一个字节,若到达输入流结尾,则返回-1.
     * @exception  IOException  if an I/O error occurs.
     */
    public abstract int read() throws IOException;

    /**
     * 从输入流中读取一些字节并将它们存储到缓存数组b中。
     * 若b的长度为0,则没有字节被读取,返回0;若由于流在文件的尾部,没有字节可读,则返回-1。
     * 读取的第一个字节保存到b[0],下一个保存到b[1],以此类推。读取字节的数组小于等于数组b的长度。
     * 
     * @param      b   缓存所读取的数据
     * @return     读取字节数到缓存的数目。由于到达流的结尾而不可读取数据时,返回-1.
     * @exception  IOException  若输入流已经关闭或者其他I/O错误发生所导致第一个字节不能被读取而抛出的异常。不考虑到达文件尾部的情况。
     * @exception  NullPointerException  b==null的情况
     * @see        java.io.InputStream#read(byte[], int, int)
     */
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

    /**
     * 从输入流读取len个字节数据,并保存到数组中。读取的数组最多为len。真正读取的字节数目将作为返回值返回。
     * 若len=0,则没有字节需要读取,返回为0;若字节不可达,则返回-1。
     * 读取到的第一个字节保存到b[off]中,下一个保存在b[off+1],以此类推。
     *
     * @param      b     缓存所读取的数据
     * @param      off   将读取的数据写入数组b的起始偏移地址
     * @param      len   读取的最大字节数组
     * @return     保存到buffer中真正的字节数,若由于到达流的结尾而无法读取数据,返回-1.
     * @exception  IOException If the first byte cannot be read for any reason
     * other than end of file, or if the input stream has been closed, or if
     * some other I/O error occurs.
     * @exception  NullPointerException b==null的情况.
     * @exception  IndexOutOfBoundsException 若off或len为负,或者len>b.length-off时,抛出该异常。
     * @see        java.io.InputStream#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;
    }

    /**
     * 跳过输入流的n个字节的数据,并返回真正跳过的字节数目。若n为负,则没有字节被跳过。
     * 方法skip会创建一个字节数组,不断读取字节到该数组中,直到n个字节被读取或到达输入流的结尾。
     *
     * @param      n   被跳过的字节数目.
     * @return     真正被跳过的字节数目.
     * @exception  IOException  if the stream does not support seek,
     *                          or if some other I/O error occurs.
     */
    public long skip(long n) throws IOException {

        long remaining = n;
        int nr;

        if (n <= 0) {
            return 0;
        }

        int size = (int)Math.min(MAX_SKIP_BUFFER_SIZE, remaining);
        byte[] skipBuffer = new byte[size];
        while (remaining > 0) {
            nr = read(skipBuffer, 0, (int)Math.min(size, remaining));
            if (nr < 0) {
                break;
            }
            remaining -= nr;
        }

        return n - remaining;
    }

    /**
     * 返回一个可以在该输入流中读取(或跳过)的字节数的估计,而不阻塞此输入流的下一次调用。
     *
     * @return     an estimate of the number of bytes that can be read (or skipped
     *             over) from this input stream without blocking or {@code 0} when
     *             it reaches the end of the input stream.
     * @exception  IOException if an I/O error occurs.
     */
    public int available() throws IOException {
        return 0;
    }

    /**
     * 关闭输入流并释放与其相关的系统资源。
     *
     *
     * @exception  IOException  if an I/O error occurs.
     */
    public void close() throws IOException {}

    /**
     * 标记输入流中当前的位置。当调用reset方法可以回到上次标记的位置,使得后面可以重新读取相同的字节。
     * 参数readlimit告诉输入流允许被读取的字节数目。超过该数目,则标记位失效(invalid)
     *
     * @param   readlimit   在标志位变为invalid之前,可被读取的最大字节数目.
     * @see     java.io.InputStream#reset()
     */
    public synchronized void mark(int readlimit) {}

    /**
     * 将流重定位到最后一次对此输入流调用mark方法时的位置。
     * 当markSupported方法返回true,则
     * 1、若mark方法自创建流以来从未被调用或者从流中读取的字节数目超过上次调用mark方法所定义的readlimit,则抛出IOException异常。
     * 2、若未抛出IOException,则将该流重新设置为状态:最近一次调用mark后(若未调用过mark,则从文件开头开始)读取的所有字节重新提供
     * 给read方法的后续调用者,如何从调用reset时起将作为下一输入数据的字节。
     * 当markSupported方法返回false,则
     * 1、调用reset方法会抛出IOException
     * 2、若IOException未被抛出,则流将被重置到一个固定状态,该状态依赖于输入流的类型和被创建的方式。。。。
     * The bytes that will be supplied to subsequent callers of the <code>read</code> method depend on the
     * particular type of the input stream. 
     *
     * @exception  IOException  若流未被标记或者标记失效。
     * @see     java.io.InputStream#mark(int)
     * @see     java.io.IOException
     */
    public synchronized void reset() throws IOException {
        throw new IOException("mark/reset not supported");
    }

    /**
     * 测试输入流是否支持test和reset方法。所支持的mark和reset是否是输入流实例的不变属性。
     * InputStream的markSupported方法返回false。
     *
     * @return  若流实例支持mark和reset方法,返回true;否则返回false。
     * @see     java.io.InputStream#mark(int)
     * @see     java.io.InputStream#reset()
     */
    public boolean markSupported() {
        return false;
    }

}

方法

通过源码的分析,我们发现其中有一个必须要被子类实现的方法:

Java描述 功能描述
public abstract int read() 从输入流中读取下一字节数据。返回的字节值为一个范围在0-255之间的int数。若由于到达流的尾部而没有字节可获取,则返回-1.直到数据可达,检测到流的末尾或者抛出一个异常,该方法才停止。

其余方法的话,只需要关注功能即可:

header 1 header 2
int available() 返回此输入流下一个方法调用可以不受阻塞地从此输入流读取(或跳过)的估计字节数。
void close() 关闭此输入流并释放与该流关联的所有系统资源。
void mark(int readlimit) 在此输入流中标记当前的位置。
boolean markSupported() 测试此输入流是否支持 mark 和 reset 方法。
int read(byte[] b) 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
int read(byte[] b, int off, int len) 将输入流中最多 len 个数据字节读入 byte 数组。
void reset() 将此流重新定位到最后一次对此输入流调用 mark 方法时的位置。
long skip(long n) 跳过和丢弃此输入流中数据的 n 个字节。

友情提示:请思考各种read()的区别哦~

InputStream是所有字节输入流类型的超类,一般在使用的时候更多的采用向上转型的方法;还记得第一篇文章中讲到的流的流入对象和流出对象吗?接下来针对常用的流类型,我们进行一个分类:第一类有FileInputStream;第二类有DataInputStream、BufferedInputStream。第一类的流入对象是文件,而第二类的话是另一种流,也就是我们所说的嵌套流的概念,什么?还是不懂?看代码一目了然!

FileInputStream

FileInputStream 从文件系统中的某个文件中获得输入字节, 用于读取诸如图像数据之类的原始字节流。要读取字符流,请考虑使用 FileReader。

构造方法

见下表格:

Java描述 功能描述
FileInputStream(File file) 通过打开一个到实际文件的连接来创建一个 FileInputStream,该文件通过文件系统中的 File 对象 file 指定
FileInputStream(String name) 通过打开一个到实际文件的连接来创建一个 FileInputStream,该文件通过文件系统中的路径名 name 指定

常用方法

其实几乎所有输入字节流的方法都已经规定在InputStream这个超类之中了,所以我们在使用的时候,只用关心列出来的方法就行,看代码很容易了解:

public class FISDemo01 {
    public static void main(String[] args){
        String content=null;
        try {
            int size=0;
            //定义一个字节缓冲区,该缓冲区的大小根据需要来定义
            byte[] buffer=new byte[1024];
            FileInputStream fis=new FileInputStream("FOSDemo.txt");
            //循环来读取该文件中的数据
            while((size=fis.read(buffer))!=-1){
                content=new String(buffer, 0, size);
                System.out.println(content);
            }
        //关闭此文件输入流并释放与此流有关的所有系统资源。 
        fis.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

嵌入流

常用的DataInputStream、BufferedInputStream,在这里统一称之为嵌入流。我们先来看用法:

public static void main(String[] arg)
FileInputStream fileInput = null;
    BufferedInputStream bufferInput = null;
    int len = 0;
    byte [] b = new byte[1024];
    File file = new File("C:/Users/加盐/Desktop/IO.txt");
    try {
        fileInput = new FileInputStream(file);
        bufferInput = new BufferedInputStream(fileInput); //把FileInputStream包装成 BufferInputStream
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }        
    try {
        len = bufferInput.read(b);
        bufferInput.close();
        fileInput.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
    for(int i=0;i<len;i++){
        System.out.print((char) b[i]+"");
    }
}
}

再来看看两种嵌入流的比较和具体的知识内容。

BufferedInputStream

如果我们传输一个比较大的文件,当我们使用FileInputStream的时候,是CPU直接和硬盘进行交互的,硬盘是所有存储设备最慢的,这样就会非常耗时,但是,如果我们把要存放的数据先存放在内存,等内存满了过后再统一操作硬盘,这样是不是会快很多?正是如此,BufferedInputStream实现了它的价值。它的构造方法主要有两个,都是以InputStream封装:

Java描述 功能描述
BufferedInputStream(InputStream in) 创建一个 BufferedInputStream ,并把它的参数 in 保存起来,用以后续的使用
BufferedInputStream(InputStream in, int size) 同上,但是指定了大小

DataInputStream

偷个懒,自己查。

输出字节流

再偷个懒,对比学习吧!如果有问题的话,欢迎mail我。

个人主页