IO第四回:访问文件的字节输入流—FileInputStream

IO第四回:访问文件的字节输入流—FileInputStream

标签: IO流


FileInputStream介绍

public class FileInputStream extends InputStream

FileInputStream是我们在IO操作中常用的类。

FileInputStream 是文件输入流,它继承于InputStream。通常,我们使用FileInputStream从某个文件中获得输入字节。FileInputStream用于读取诸如图像数据的原始字节流。 要阅读字符串,请考虑使用FileReader 。

既然是字节流,那么每次从文件向程序中输入的是一个字节(Byte),八位的二进制数,因此有时候我们需要对字节Byte进行处理。

FileInputStream的使用步骤

  • 第一步:打开流(即创建流)

  • 第二步:通过流读取内容

  • 第三步:用完后,关闭流资源

显然流是Java中的一类对象,要打开流其实就是创建具体流的对象,由于是读取硬盘上的文件,应该使用输入流。所以找到了InputStream类,但是InputStream是抽象类,需要使用它的具体实现类来创建对象就是FileInputStream。

通过new调用FileInputStream的构造方法来创建对象。FileInputStream的构造方法需要指定文件的来源,可以接受字符串也可以接受File对象。

使用流就像使用水管一样,要打开就要关闭。所以打开流和关闭流的动作是比不可少的。如何关闭流?使用close方法即可,当完成流的读写时,应该通过调用close方法来关闭它,这个方法会释放掉十分有限的操作系统资源.如果一个应用程序打开了过多的流而没有关闭它们,那么系统资源将被耗尽。

代码演示

源码里面写的有点看不懂,先上代码演示,看看怎么用,然后再分析源码

1. 基本版

从硬盘存在的文件,读取内容到程序

package charIO;

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

/**
 * Created by japson on 8/18/2017.
 */
public class testFileInputSteam {
    public static void main(String[] args) throws IOException {

        //1. 创建一个File类的对象,对象对应着一个文件,通过形参的路径来表示
        File file = new File("E:\\IntelliJ IDEA\\TIJ\\src\\charIO\\test2.txt");

        System.out.println("文件存不存在?" + file.exists());

        //2. 创建一个FileInputStream类的对象,该对象的构造器包含着一个文件,或者用file,或者用路径
        FileInputStream fis = new FileInputStream(file);

        //3. 调用FileInputStream的方法,实现file文件的读取,使用read()

//        int b = fis.read();   read()的返回值是int类型的
//        while (b != -1) {
//        System.out.print(b); 这样打印出来的是int类型的 979899100101102103,这是ASCII值
//            System.out.print((char)b); 转成char
//            b = fis.read();
//        }

        //但是上面那么写太麻烦了,简化一下:
        int b;
        while ((b = fis.read()) != -1 ) {
            System.out.print((char)b);
        }

        //必须显式的关闭流,不会自动关
        fis.close();
    }
}

2. try-catch-fianlly版

在上一版中,读取的文件一定要存在,否则要抛出FileNotFoundException异常,因此我们可以用try-catch-finally处理一下。而且“流”也是一种稀缺资源,在finally中一定要关掉流。

3. 数组版

我们可以想一下,在上面的两个例子中,我们使用b = fis.read(),每次读一个字节,然后输出,然后再读一个字节,再输出……

这样效率太慢了,比如说板砖:我每次只搬一块砖,从工地一头跑到工地另一头,这样肯定是不行的。我应该找一个板车,我先把砖搬到板车上,把板车装满后再推到工地另一头卸下,这样岂不是美滋滋?

同理,我们使用字节数组:byte[] b, 先把字节装到数组里,装满了之后对数组b进行操作,这样一次能处理一板车的字节:

(下面的代码有错误!!)

有问题的代码
package charIO;

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

/**
 * Created by japson on 8/18/2017.
 */
public class testFileInputSteam {
    public static void main(String[] args){
        FileInputStream fis = null;
        try {
            File file = new File("E:\\IntelliJ IDEA\\TIJ\\src\\charIO\\test2.txt");
            fis = new FileInputStream(file);
            byte[] b = new byte[5];    //读取到的数据所写入的数组
            int len;    //用来装read(byte[] b)的返回值,即读取并装入数组中的总字节的长度
            while ((len = fis.read(b)) != -1) {
                //这里使用foreach,等价于 for(int i = 0;i < b.length;i++) 没问题吗?
                for (byte c : b) {      
                    System.out.print((char)c);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

难道这样就板砖成功了?? 不存在的!我们来跑一下程序(test2.txt中的内容是:abcdefg)

输出:

abcdefgcde

What??? 为什么会多出 cde

下面我们对于“问题代码”着重分析:

问题分析
for (byte c : b) {      
    System.out.print((char)c);
}

这里等同于:

for(int i = 0;i < b.length;i++) {
    System.out.print((char)c);
}

分析:

首先我们要明确:文件中有7个字节,用来装字节的数组有5个位置,len = fis.read(b)表示len为所读取的字节个数。那么如果使用以上的代码,在进行一次读取时,byte[] b中装着 [a,b,c,d,e],第二次读取时,由于读取到g之后没有了,返回-1,因此只读取了f,g两个字节,放在数组中是[f,g,c,d,e]。

在以上的代码中,我们在for循环时使用了b.length作为条件,那么第二次时将数组[f,g,c,d,e]中的全部内容都打印出来了,因此出现 abcdefgcde.

正确的做法是:

 while ((len = fis.read(b)) != -1) {
    for (int i = 0;i < len;i++) {      //因为len表示的是每次读入的字节长度
        System.out.print((char)b[i]);
    }
 }

或者我们换一种打印方法,不用for循环:

while ((len = fis.read(b)) != -1) {
    String str = new String(b,0,len);   //同理不能写成b.length
    System.out.print(str);
 }
完整版

来个完整版:

package charIO;

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

/**
 * Created by japson on 8/18/2017.
 */
public class testFileInputSteam {
    public static void main(String[] args){
        FileInputStream fis = null;
        try {
            File file = new File("E:\\IntelliJ IDEA\\TIJ\\src\\charIO\\test2.txt");
            fis = new FileInputStream(file);
            byte[] b = new byte[5];    //读取到的数据所写入的数组
            int len;    //用来装read(byte[] b)的返回值,即读取并装入数组中的总字节的长度
            while ((len = fis.read(b)) != -1) {
                String str = new String(b,0,len);   //同理不能写成b.length
                System.out.print(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

源码分析

1. 成员变量

 /* 文件描述符类---此处用于打开文件的句柄 */
    private final FileDescriptor fd;

    /* 引用文件的路径 */
    private final String path;

    /* 文件通道,NIO部分 */
    private FileChannel channel = null;

    private final Object closeLock = new Object();
    private volatile boolean closed = false;

    private static final ThreadLocal<Boolean> runningFinalize =
        new ThreadLocal<>();

2. 构造器

FileInputStream是一个类,我们要想使用,就要创建一个对象,FileInputStream的构造方法需要指定文件的来源,可以接受字符串也可以接受File对象。

  • 通过文件路径名来创建FileInputStream,该文件路径不为null。
 public FileInputStream(String name) throws FileNotFoundException {
    this(name != null ? new File(name) : null);
}
  • 通过File对象来创建FileInputStream
    如果file不存在,是一个目录而不是一个常规文件,不能打开,则抛出异常
 public FileInputStream(File file) throws FileNotFoundException {
    String name = (file != null ? file.getPath() : null);
    //安全管理器
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkRead(name);
    }
    //如果路径不存在抛出异常
    if (name == null) {
        throw new NullPointerException();
    }
    //如果文件失效,排除异常
    if (file.isInvalid()) {
         throw new FileNotFoundException("Invalid file path");
    }

    fd = new FileDescriptor();
    fd.attach(this);
    path = name;
    open(name);
}
  • 通过文件描述符类来创建FileInputStream
    FileDescriptor我不太懂,看文档也没看明白

指定文件名字创建流是一种常见和主要的方式。但是,在某些情况下,用户不知道具体文件路径,只知道系统提供的一个“文件描述”。FD 的概念应该是从POSIX 来的。

因此,用FD创建文件读写流只是提供了针对这种特殊情况的一个选择。不要期望着在你用文件名字创建的流里面 get 出 FileDescriptor 乐,(如果调用了,肯定返回空)毫无意义。而特殊情况其实非常少,javadoc 说得很明确:用户不能自己创建这个类的对象,只能使用定义好的FileDescriptor.err, in 或 out。

但即使这三个定义好的FD, 用户也可以不必使用,因为,System.err, in 和 out 可以替代他们。
因此,不必对 java.io.FileDescriptor 过多纠缠。

 public FileInputStream(FileDescriptor fdObj) {
        SecurityManager security = System.getSecurityManager();
        if (fdObj == null) {
            throw new NullPointerException();
        }
        if (security != null) {
            security.checkRead(fdObj);
        }
        fd = fdObj;
        path = null;

        /*
         * FileDescriptor is being shared by streams.
         * Register this stream with FileDescriptor tracker.
         */
        fd.attach(this);
    }

3. 打开文件

打开文件,为了下一步读取文件内容。native方法

    /**
     * Opens the specified file for reading.
     * @param name the name of the file
     */
    private native void open0(String name) throws FileNotFoundException;

    // wrap native call to allow instrumentation
    /**
     * Opens the specified file for reading.
     * @param name the name of the file
     */
    private void open(String name) throws FileNotFoundException {
        open0(name);
    }

4. read()方法

read是指站在程序的角度,从输入流中读取字节的方法。首先read()方法返回的是下一个字节,但是read()方法都是int类型的,因此返回的是下一个字节的int表示形式。

  • read()方法
    /**
     * 从该输入流中读取一个字节的数据,如果没有可用的输入,则停止
     * 返回数据的下一个字节,如果达到文件的末尾则返回-1 。 
     */
    public int read() throws IOException {
        return read0();
    }

read0()是一个native方法

    private native int read0() throws IOException;
  • readBytes(byte b[], int off, int len)方法
    /**
     * 读取子数组作为字节序列。
     * 
     * @param b 要写入的数据
     * @param off 数据中偏移量的起始位置
     * @param len 要写入的字节的个数
     * @exception IOException If an I/O error has occurred.
     */
    private native int readBytes(byte b[], int off, int len) throws IOException;
  • read(byte b[])
    /**
     * 从文件输入流中读取最多b.length个字节到byte数组b中。
     * 返回读入缓冲区的总字节数,如果没有更多的数据,即文件结束了,返回-1。
     *
     * @param      b   要读取的数据的缓冲数组。
     */
    public int read(byte b[]) throws IOException {
        return readBytes(b, 0, b.length);
    }
  • read(byte b[], int off, int len)
    /**
     * 将该输入流中的len长度的字节读入一个字节数组中
     *
     * @param      b     要读取的数据的缓冲数组
     * @param      off   目标数组b中的起始偏移量
     * @param      len   读取的最大字节数。
     * @return     返回读入缓冲区的总字节数,如果读完,返回-1
     */
    public int read(byte b[], int off, int len) throws IOException {
        return readBytes(b, off, len);
    }

5. skip(long n)

跳过并从输入流中丢弃n个字节的数据。n可以为0,也可以为负,为负时,则该方法将尝试向后跳。

该方法返回实际跳过的字节数。 如果它向前跳,它返回一个正值。如果它向后跳,它返回一个负值。

 public native long skip(long n) throws IOException;

6. available()

返回下一次对此输入流调用的方法可以不受阻塞地从此输入流读取(或跳过)的估计剩余字节数。

public native int available() throws IOException;

7. close()

关闭此文件输入流并释放与此流有关的所有系统资源。

    public void close() throws IOException {
        synchronized (closeLock) {
            if (closed) {
                return;
            }
            closed = true;
        }
        if (channel != null) {
           channel.close();
        }

        fd.closeAll(new Closeable() {
            public void close() throws IOException {
               close0();
           }
        });
    }

8. getChannel()

获取此文件输入流的唯一FileChannel对象,Channel涉及NIO

    public FileChannel getChannel() {
        synchronized (this) {
            if (channel == null) {
                channel = FileChannelImpl.open(fd, path, true, false, this);
                fd.incrementAndGetUseCount();
            }
            return channel;
        }
    }

9. finalize()

    protected void finalize() throws IOException {
        if ((fd != null) &&  (fd != FileDescriptor.in)) {

            close();
        }
    }

对于以上这些方法,很多都是native方法,有很多也不太懂,云山雾罩的。

但是不管怎么样吧,如何使用才是重头戏!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值