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方法,有很多也不太懂,云山雾罩的。
但是不管怎么样吧,如何使用才是重头戏!