Java中的字节流文件读取教程(一)

前言

上篇文章我们介绍了抽象化磁盘文件的 File 类型,它仅仅用于抽象化描述一个磁盘文件或目录,却不具备访问和修改一个文件内容的能力。

Java 的 IO 流就是用于读写文件内容的一种设计,它能完成将磁盘文件内容输出到内存或者是将内存数据输出到磁盘文件的数据传输工作。

Java IO 流的设计并不是完美的,设计了大量的类,增加了我们对于 IO 流的理解,但无外乎为两大类,一类是针对二进制文件的字节流,另一类是针对文本文件的字符流。而本篇我们就先来学习有关字节流的相关类型的原理以及使用场景等细节,主要涉及的具体流类型如下:

基类字节流 Input/OutputStream

InputStream 和 OutputStream 分别作为读字节流和写字节流的基类,所有字节相关的流都必然继承自他们中任意一个,而它们本身作为一个抽象类,也定义了最基本的读写操作,我们一起来看看:

以 InputStream 为例:

public abstract int read() throws IOException;

这是一个抽象的方法,并没有提供默认实现,要求子类必须实现。而这个方法的作用就是为你返回当前文件的下一个字节。

当然,你也会发现这个方法的返回值是使用的整型类型「int」来接收的,为什么不用「byte」?

首先,read 方法返回的值一定是一个八位的二进制,而一个八位的二进制可以取值的值区间为:「0000 0000,1111 1111」,也就是范围 [-128,127]。

read 方法同时又规定当读取到文件的末尾,即文件没有下一个字节供读取了,将返回值 -1 。所以如果使用 byte 作为返回值类型,那么当方法返回一个 -1 ,我们该判定这是文件中数据内容,还是流的末尾呢?

而 int 类型占四个字节,高位的三个字节全部为 0,我们只使用它的最低位字节,当遇到流结尾标志时,返回四个字节表示的 -1(32 个 1),这就自然的和表示数据的值 -1(24 个 0 + 8 个 1)区别开来了。

接下来也是一个 read 方法,但是 InputStream 提供默认实现:

public int read(byte b[]) throws IOException {
 return read(b, 0, b.length);
}

public int read(byte b[], int off, int len) throws IOException{
 //为了不使篇幅过长,方法体大家可自行查看 jdk 源码
}

这两个方法本质上是一样的,第一个方法是第二个方法的特殊形态,它允许传入一个字节数组,并要求程序将文件中读到的字节从数组索引位置 0 开始填充,供填充数组长度个字节数。

而第二个方法更加宽泛一点,它允许你指定起始位置和字节总数。

InputStream 中还有其他几个方法,基本都没怎么具体实现,留待子类实现,我们简单看看。

  • public long skip(long n):跳过 n 个字节,返回实际跳过的字节数
  • public void close():关闭流并释放对应的资源
  • public synchronized void mark(int readlimit)
  • public synchronized void reset()
  • public boolean markSupported()

mark 方法会在当前流读取位置打上一个标志,reset 方法即重置读取指针到该标志处。

事实上,文件读取是不可能重置回头读取的,而一般都是将标志位置到重置点之间所有的字节临时保存了,当调用 reset 方法时,其实是从保存的临时字节集合进行重复读取,所以 readlimit 用于限制最大缓存容量。

而 markSupported 方法则用于确定当前流是否支持这种「回退式」读取操作。

OutputStream 和 InputStream 是类似的,只不过一个是写一个是读,此处我们不再赘述了。

文件字节流 FileInput/OutputStream

我们依然着重点于 FileInputStream,而 FileOutputStream 是类似的。

首先 FileInputStream 有以下几种构造器实例化一个对象:

public FileInputStream(String name) throws FileNotFoundException {
 this(name != null ? new File(name) : null);
}
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);
}

这两个构造器本质上也是一样的,前者是后者的特殊形态。其实你别看后者的方法体一大堆代码,大部分都只是在做安全校验,核心的就是一个 open 方法,用于打开一个文件。

主要是这两种构造器,如果文件不存在或者文件路径和名称不合法,都将抛出 FileNotFoundException 异常。

记得我们说过,基类 InputStream 中有一个抽象方法 read 要求所有子类进行实现,而 FileInputStream 使用本地方法进行了实现:

public int read() throws IOException {
 return read0();
}

private native int read0() throws IOException;

这个 read0 的具体实现我们暂时无从探究,但是你必须明确的是,这个 read 方法的作用,它用于返回流中下一个字节,返回 -1 说明读取到文件末尾,已无字节可读。

除此之外,FileInputStream 中还有一些其他的读取相关方法,但大多采用了本地方法进行了实现,此处我们简单看看:

  • public int read(byte b[]):读取 b.length() 个长度的字节到数组中
  • public int read(byte b[], int off, int len):读取指定长度的字节数到数组中
  • public native long skip(long n):跳过 n 的字节进行读取
  • public void close():释放流资源

FileInputStream 的内部方法基本就这么些,还有一些高级的复杂的,我们暂时用不到,以后再进行学习,下面我们简单看一个文件读取的例子:

public static void main(String[] args) throws IOException {
 FileInputStream input = new FileInputStream("C:\\Users\\yanga\\Desktop\\test.txt");
 byte[] buffer = new byte[1024];
 int len = input.read(buffer);
 String str = new String(buffer);
 System.out.println(str);
 System.out.println(len);
 input.close();
}

输出结果很简单,会打印出我们 test 文件中的内容和实际读出的字节数,但细心的同学就会发现了,你怎么就能保证 test 文件中内容不会超过 1024 个字节呢?

为了能够完整的读出文件中的内容,一种解决办法是:将 buffer 定义的足够大,以期望尽可能的能够存储下文件中的所有内容。

这种方法显然是不可取的,因为我们根本不可能实现知道待读文件的实际大小,一味的创建过大的字节数组其本身也是一种很差劲的方案。

第二种方式就是使用我们的动态字节数组流,它可以动态调整内部字节数组的大小,保证适当的容量,这一点我们后文中将详细介绍。

关于 FileOutputStream,还需要强调一点的是它的构造器,其中有以下两个构造器:

public FileOutputStream(String name, boolean append)

public FileOutputStream(File file, boolean append)

参数 append 指明了,此流的写入操作是覆盖还是追加,true 表示追加,false 表示覆盖。

字节数组流 ByteArrayInput/OutputStream

所谓的「字节数组流」就是围绕一个字节数组运作的流,它并不像其他流一样,针对文件进行流的读写操作。

字节数组流虽然并不是基于文件的流,但却依然是一个很重要的流,因为它内部封装的字节数组并不是固定的,而是动态可扩容的,往往基于某些场景下,非常合适。

ByteArrayInputStream 是读字节数组流,可以通过以下构造函数被实例化:

protected byte buf[];
protected int pos;
protected int count;

public ByteArrayInputStream(byte buf[]) {
 this.buf = buf;
 this.pos = 0;
 this.count = buf.length;
}

public ByteArrayInputStream(byte buf[], int offset, int length)

buf 就是被封装在 ByteArrayInputStream 内部的一个字节数组,ByteArrayInputStream 的所有读操作都是围绕着它进行的。

所以,实例化一个 ByteArrayInputStream 对象的时候,至少传入一个目标字节数组的。

pos 属性用于记录当前流读取的位置,count 记录了目标字节数组最后一个有效字节索引的后一个位置。

理解了这一点,有关它各种的 read 方法就不难了:

//读取下一个字节
public synchronized int read() {
 return (pos < count) ? (buf[pos++] & 0xff) : -1;
}
//读取 len 个字节放到字节数组 b 中
public synchronized int read(byte b[], int off, int len){
 //同样的,方法体较长,大家查看自己的 jdk
}

除此之外,ByteArrayInputStream 还非常简单的实现了「重复读取」操作。

public void mark(int readAheadLimit) {
 mark = pos;
}

public synchronized void reset() {
 pos = mark;
}

因为 ByteArrayInputStream 是基于字节数组的,所有重复读取操作的实现就比较容易了,基于索引实现就可以了。

ByteArrayOutputStream 是写的字节数组流,很多实现还是很有自己的特点的,我们一起来看看。

首先,这两个属性是必须的:

protected byte buf[];

//这里的 count 表示的是 buf 中有效字节个个数
protected int count;

构造器:

public ByteArrayOutputStream() {
 this(32);
}

public ByteArrayOutputStream(int size) {
 if (size < 0) {
 throw new IllegalArgumentException("Negative initial size: "+ size);
 }
 buf = new byte[size];
}

构造器的核心任务是,初始化内部的字节数组 buf,允许你传入 size 显式限制初始化的字节数组大小,否则将默认长度 32 。

从外部向 ByteArrayOutputStream 写内容:

public synchronized void write(int b) {
 ensureCapacity(count + 1);
 buf[count] = (byte) b;
 count += 1;
}

public synchronized void write(byte b[], int off, int len){
 if ((off < 0) || (off > b.length) || (len < 0) ||
 ((off + len) - b.length > 0)) {
 throw new IndexOutOfBoundsException();
 }
 ensureCapacity(count + len);
 System.arraycopy(b, off, buf, count, len);
 count += len;
}

看到没有,所有写操作的第一步都是 ensureCapacity 方法的调用,目的是为了确保当前流内的字节数组能容纳本次写操作。

而这个方法也很有意思了,如果计算后发现,内部的 buf 不能够支持本次写操作,则会调用 grow 方法做一次扩容。扩容的原理和 ArrayList 的实现是类似的,扩大为原来的两倍容量。

除此之外,ByteArrayOutputStream 还有一个 writeTo 方法:

public synchronized void writeTo(OutputStream out) throws IOException {
 out.write(buf, 0, count);
}

将我们内部封装的字节数组写到某个输出流当中。

剩余的一些方法也很常用:

  • public synchronized byte toByteArray()[]:返回内部封装的字节数组
  • public synchronized int size():返回 buf 的有效字节数
  • public synchronized String toString():返回该数组对应的字符串形式

注意到,这两个流虽然被称作「流」,但是它们本质上并没有像真正的流一样去分配一些资源,所以我们无需调用它的 close 方法,调了也没用(人家官方说了,has no effect)。

测试的案例就不放出来了,等会我会上传本篇文章用到的所有代码案例,大家自行选择下载即可。

为了控制篇幅,余下流的学习,放在下篇文章。

文章中的所有代码、图片、文件都云存储在我的 GitHub 上:

(https://github.com/SingleYam/overview_java)

大家也可以选择通过本地下载。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • Java中的字节流文件读取教程(二)

    接着上篇文章,我们继续来学习 Java 中的字节流操作. 装饰者缓冲流 BufferedInput/OutputStream 装饰者流其实是基于一种设计模式「装饰者模式」而实现的一种文件 IO 流,而我们的缓冲流只是其中的一种,我们一起来看看. 在这之前,我们使用的文件读写流 FileInputStream 和 FileOutputStream 都是一个字节一个字节的从磁盘读取或写入,非常耗时. 而我们的缓冲流可以预先从磁盘一次性读出指定容量的字节数到内存中,之后的读取操作将直接从内存中读取,提

  • Java文件(io)编程_文件字节流的使用方法

    案例1: 演示FileInputStream类的使用(用FileInputStream的对象把文件读入到内存) 首先要在E盘新建一个文本文件,命名为test.txt,输入若干字符 public class Demo_2 { public static void main(String[] args) { File f=new File("e:\\test.txt"); //得到一个文件对象f,指向e:\\test.txt FileInputStream fis=null; try {

  • 详解Java中IO字节流基本操作(复制文件)并测试性能

    此次案例将以复制文件的形式来演示IO字节流的基本操作,复制一个mp3文件,文件信息如下图: main方法测试 public static void main(String[] args) throws Exception { //源文件 String srcFile = "src/a.mp3"; //目的文件 String destFile = "src/b.mp3"; long start = System.currentTimeMillis(); ... 复制文

  • Java字符流和字节流对文件操作的区别

    记得当初自己刚开始学习Java的时候,对Java的IO流这一块特别不明白,所以写了这篇随笔希望能对刚开始学习Java的人有所帮助,也方便以后自己查询.Java的IO流分为字符流(Reader,Writer)和字节流(InputStream,OutputStream),字节流顾名思义字节流就是将文件的内容读取到字节数组,然后再输出到另一个文件中.而字符流操作的最小单位则是字符.可以先看一下IO流的概述: 下面首先是通过字符流对文件进行读取和写入: package lib; import java.

  • Java字节流 从文件输入输出到文件过程解析

    假如需要复制一张图片,一份word,一个rar包.可以以字节流的方式,读取文件,然后输出到目标文件夹. 以复制一张4M的图片举例. 每次读一个字节: ch = (char)System.in.read(); //读入一个字符,返回读到的字节的int表示方式,读到末尾返回-1 复制时候一个字节一个字节的读取.写入,这样是很慢的.设置一个用来缓冲的字符数组,会让复制的过程快很多(每次读入的字节变多). 方便阅读,类的名称用中文描述 import java.io.*; public class 字节流

  • Java使用字节流复制文件的方法

    其实用java程序复制文件并不难,具体内容如下 import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.LinkedList; import java.util.List; //文件复制 //E:/3.jpg ---> D:/1.jpg public class CopyFileByIo { public static void ma

  • Java中的字节流文件读取教程(一)

    前言 上篇文章我们介绍了抽象化磁盘文件的 File 类型,它仅仅用于抽象化描述一个磁盘文件或目录,却不具备访问和修改一个文件内容的能力. Java 的 IO 流就是用于读写文件内容的一种设计,它能完成将磁盘文件内容输出到内存或者是将内存数据输出到磁盘文件的数据传输工作. Java IO 流的设计并不是完美的,设计了大量的类,增加了我们对于 IO 流的理解,但无外乎为两大类,一类是针对二进制文件的字节流,另一类是针对文本文件的字符流.而本篇我们就先来学习有关字节流的相关类型的原理以及使用场景等细节

  • Java中IO流文件读取、写入和复制的实例

    //构造文件File类 File f=new File(fileName); //判断是否为目录 f.isDirectory(); //获取目录下的文件名 String[] fileName=f.list(); //获取目录下的文件 File[] files=f.listFiles(); 1.Java怎么读取文件 package com.yyb.file; import java.io.File; import java.io.FileInputStream; import java.io.In

  • Java中创建ZIP文件的方法

    java创建zip文件的代码如下如下: import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; public cla

  • 如何在Java中调用python文件执行详解

    目录 一.Java内置Jpython库(不推荐) 1.1 下载与使用 1.2 缺陷 二.使用Runtime.getRuntime()执行脚本⽂件 2.1 使用 2.2 缺陷 三.利用cmd调用python文件 3.1 使用 3.2 优化 总结 一.Java内置Jpython库(不推荐) 1.1 下载与使用 可以在官网下载jar包,官网:http://ftp.cuhk.edu.hk/pub/packages/apache.org/ 或者使用maven进行jar包下载 <dependency> &

  • Java中I/O流读取数据不完整的问题解决

    目录 一·问题描述: 二·问题原因: 三·解决办法: 四·测试结果:成功 一·问题描述: 1.利用Java的转换流去读取一个json文件数据,获取的数据无法被解析为json格式数据(格式总是报错),且获取的数据末尾缺少一部分数据. (1)Java源代码如图 (2)原json文件如图 (3)解析获取的数据如图:转换为json格式数据报错 二·问题原因: 1.最后一次缓存数组里面的数据,没有拼接到最终字符串数据里面 2.stringBuffer.append(buffer)拼接数据的方法,内部可能会

  • java中struts2实现文件上传下载功能实例解析

    本文实例讲述了java中struts2实现文件上传下载功能实现方法.分享给大家供大家参考.具体分析如下: 1.文件上传 首先是jsp页面的代码 在jsp页面中定义一个上传标签 复制代码 代码如下: <tr>      <td align="right" bgcolor="#F5F8F9"><b>附件:</b></td>      <td bgcolor="#FFFFFF">

  • python中readline判断文件读取结束的方法

    本文实例讲述了python中readline判断文件读取结束的方法.分享给大家供大家参考.具体分析如下: 大家知道,python中按行读取文件可以使用readline函数,下面现介绍一个按行遍历读取文件的方法,通过这个方法,展开我们要讨论的问题: 复制代码 代码如下: filename = raw_input('Enter your file name')  #输入要遍历读取的文件路径及文件名 file = open(filename,'r') done = 0 while not  done:

  • 详解Java目录操作与文件操作教程

    目录 目录操作 创建目录 判断这个文件或目录是否存在 判断是否是目录 读取目录 删除目录 文件操作 创建文件 删除文件 File对象常用函数 目录操作 创建目录 File类中有两个方法可以用来创建文件夹: mkdir( )方法创建一个文件夹,成功则返回true,失败则返回false.失败表明File对象指定的路径已经存在,或者由于整个路径还不存在,该文件夹不能被创建. mkdirs()方法创建一个文件夹和它的所有父文件夹. 创建目录AAA路径为D:AAA public class Mk { pu

随机推荐