字节流每次输入或输出一个字节的数据,下面的表12-4展示了java.io包下定义的字节流。
表12-4字节流类
流类 | 用途 |
InputStream | 字节输入流的父类 |
OutputStream | 字节输出流的父类 |
BufferedInputStream | 缓冲输入流 |
BufferedOutputStream | 缓冲输出流 |
ByteArrayInputStream | 从字节数组读取的输入流 |
ByteArrayOutputStream | 向字节数组写入的输出流 |
DataInputStream | 包含读取Java标准数据类型方法的输入流 |
DataOutputStream | 包含编写Java 标准数据类型方法的输出流 |
FileInputStream | 读取文件的输入流 |
FileOutputStream | 写文件的输出流 |
PipedInputStream | 输入管道 |
PipedOutputStream | 输出管道 |
PrintStream | 包含print() 和 println()的输出流 |
PushbackInputStream | 支持向输入流返回一个字节的单字节的“unget”的输入流 |
RandomAccessFile | 支持随机文件输入/输出 |
SequenceInputStream | 两个或两个以上顺序读取的输入流组成的输入流 |
本小节将讲解Java语言中常用字节流的使用方式
12.3.1 InputStream类与OutputStream类
InputStream类是所有输入字节流的父类,它是一个抽象类,因此无法创建对象,但这个类中定义的诸多方法都成为了其他字节输入流读取数据的标准操作方式。下面的表12-5展示了InputStream类所定义的各种方法。
表12-5InputStream类的方法
方法名 | 功能 |
int available() | 返回还有多少个字节的数据没有读取 |
void close() | 关闭此输入流并释放与该流关联的所有系统资源。 |
void mark(int readlimit) | 在此输入流中标记当前的位置。readlimit表示读取多少个数据之后,该标记失效 |
boolean markSupported() | 测试此输入流是否支持 mark 和 reset 方法。 |
abstract int read() | 读取一个字节的数据,当读到流的结尾时,返回-1. |
int read(byte[] b) | 读取一定数量的字节并将其存储在数组 b 中。 |
int read(byte[] b, int off, int len) | 将输入流中最多len个数据字节读入字节数组。 |
void reset() | 将指针重新定位到对最后调用mark()方法时的位置。 |
long skip(long n) | 跳过和放弃数据源中的n个数据字节。 |
对于初学者而言,表12-5中所列出的很多方法并不是很好理解,下面针对表中的方法逐一讲解。read()方法是输入流最核心的方法,它用来读取数据,这个方法的返回值就是读取到的数据。虽然每次读取的数据是一个字节,但read()方法的返回值类型是int而不是byte。使用read()方法读取字节数据时,当读到输入源的末尾会返回-1,程序员就是通过返回值是否是-1来判断数据源中的数据是否全部被读取完。
每次读数据操作只能读取一个字节,为了能够增加单次读取数据的量,read()方法又提供了两个以字符数组为参数的版本,这两个版本都能够把读取到的数据存入字节数组中,只是第一个版本是把所读到的数据存入整个数组,而第二个版本是把读到的数据存入以下标off为起点,长度为len的部分数组。
当完成了对一个流的读取或者写入后,要用close()方法将这个流关闭,这样可以释放流占用的操作系统资源。因为这些资源很有限,如果一个应用程序打开了太多的流而没有将其关闭,那么系统资源将被逐步耗尽。
read()方法和close()方法都声明了IOException,因此在调用这些方法时要对异常做出处理。字节流每次读取一个字节的数据,如果希望知道数据源中还有多少字节的数据没有被读取,可以调用available()方法进行计算,该方法的返回值就是数据源中还没有被读取的字节数量。
在读取数据的过程中,可以调用mark()方法在指针所指向的位置做一个标记,在指针向后移动离开标志位置后,可以调用reset()方法使指针回到标记的位置。如果在调用reset()方法时没有使用mark()方法做过标记,那么reset()方法将会把指针重置于数据源的开头位置。并不是每一种流都能支持做标记的操作,因此InputStream类提供了markSupported()方法来判断某个流是否支持mark()方法。此处需要说明:某些流即使支持mark()方法,但实际执行这个方法时并不能达到的预期的效果,这是因为InputStream类中所定义的mark()方法是一个没有实现过程的空方法(不是抽象方法),某些子类继承了这个方法之后并没有对其进行重写,所以方法的执行不会达到预期效果。
读取数据的过程中,还可以跳过一部分数据,直接读取后面的数据,调用skip()方法就能完成跳过部分数据的操作。skip()方法有一个long型的参数,这个参数指定了跳过的字节数。
OutputStream是字节输出流的父类,它也是一个抽象类,无法创建对象,但OutputStream类所定义的诸多方法也是其他字节输出流向目标数据源输出数据的标准操作方式。下面的表12-6展示了OutputStream类中所定义的各种方法。
表12-6 OutputStream类的方法
方法 | 功能 |
abstract void write(int b) | 将指定的字节写入此输出流。 |
void write(byte[] b) | 将b.length个字节从指定的字节数组写入此输出流。 |
void write(byte[] b, int off,int len) | 将指定字节数组中从偏移量off 始的len个字节写入此输出流。 |
void close() | 关闭此输出流并释放与此流有关的所有系统资源。 |
void flush() | 刷新此输出流并强制写出所有缓冲的输出字节。 |
在表12-6所列出的这些方法中,最核心的就是输出数据的write()方法,这个方法有三个版本,其中第一个版本的方法的参数类型是int,它用来把一个字节的数据输出到目标数据源,第二个版本的参数类型是byte型数组,它用来把一个字节数组输出到目标数据源,第三个版本的write()方法有三个参数,它可以把byte型数组b从off下标开始,长度为len的这一部分数据输出到目标数据源。每当输出数据结束之后要调用close()方法关闭流以释放资源。
由于InputStream类与OutputStream类都无法创建对象,所以无法直接演示它们的使用方式,从下一小节开始,将为各位读者演示它们的子类如何对数据进行读写操作。
12.3.2 ByteArrayInputStream与ByteArrayOutputStream
ByteArrayInputStream用来读取byte型数组,它有两个构造方法,每个构造方法都需要一个byte型数组作为数据源,下面的表12-7展示了ByteArrayInputStream类的构造方法。
表12-7ByteArrayInputStream类的构造方法
构造方法 | 功能 |
ByteArrayInputStream(byte src[]) | 创建读取src数组全部数据的对象 |
ByteArrayInputStream(byte src[],int start,int len) | 创建读取src数组部分数据的对象 |
表12-6所列出的两个构造方法读取数据的范围并不相同,第一个构造方法所创建的对象能够读取src数组的全部数据,而第二个构造方法所创建的对象只能读取src数组中从下标start开始,长度为len的那一部分数据。ByteArrayInputStream类虽然在实际开发过程中使用的频率并不高,但这个类能够完美的体展出字节输入流所设计的各种功能,下面的【例12_03】展示了ByteArrayInputStream类如何读取字节数组。
【例12_03 ByteArrayInputStream类的使用】
Exam12_03.java
import java.io.*;
public class Exam12_03 {
public static void main(String[] args) {
String s = "Welcome to Java world!";
byte b[] = s.getBytes();//把字符串转为字节数组
ByteArrayInputStream bais1 = new ByteArrayInputStream(b);//①
ByteArrayInputStream bais2 = new ByteArrayInputStream(b, 0, 7);//②
try {
//计算两个流对象还有多少字节没有读
System.out.println("bais1未读字节数:" + bais1.available());//③
System.out.println("bais2未读字节数:" + bais2.available());//④
bais1.skip(8);//跳过8个字节
int r;//r用于存储读到的数据
//用循环的方式读取数据,每次读取一个字节都判断是否到达数据源末尾
System.out.print("bais1跳过8字节后所读到的数据:");
while ((r = bais1.read()) != -1) {
System.out.print((char) r);//把读取到的数据转换为字符后输出到控制台
}
System.out.println();//换行
System.out.print("bais2所读到的数据:");
while ((r = bais2.read()) != -1) {
System.out.print((char) r);
}
System.out.println();//换行
//此时bais1的指针已经指向末尾
System.out.print("使用reset()方法把指针重置到数据源开头位置后再次读取数据:");
bais1.reset();
int j = 0;
//再次读取数据并在读取过程中使用mark()方法把指针重置于数组下标为20的位置
while ((r = bais1.read()) != -1) {
j++;
if (j == 20) {
bais1.mark(j);
}
System.out.print((char) r);
}
System.out.println();//换行
System.out.print("用reset()方法把指针重置到下标为20的位置后再次读取数据:");
bais1.reset();
while ((r = bais1.read()) != -1) {//重置后再次读取数据
System.out.print((char) r);
}
} catch (Exception e) {
e.printStackTrace();
}finally {//在finally块中关闭流
if(bais1!=null){
try {
bais1.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(bais2!=null){
try {
bais2.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
【例12_03】中创建了两个ByteArrayInputStream类对象bais1和bais2读取字节数组中的数据。其中bais1读取字节数组中的全部数据,bais2读取字节数组中从下标为0开始,长度为7的那一部分数据。bais1第一次读取数据时先跳过了8个字节,并且在读完数据后两次使用reset()方法重置指针的位置。需要提醒各位读者:关闭流的操作一定要放到finally块中执行,如果放到try块中执行,那么程序运行如果出现异常将无法关闭流。【例12_03】的运行结果如图12-6所示。
图12-6【例12_03】运行结果
从图12-6可以看出:ByteArrayInputStream类能够正确的读取字节数组中的数据,但必须注意,如果字符串中包含汉字,则读取数据的结果会出现乱码,这是因为汉字占据两个字节,而ByteArrayInputStream类只能读取一个字节的数据,也就是汉字编码的一半。
ByteArrayOutputStream类的作用与ByteArrayInputStream类相反,它用来把数据输出到字节数组。ByteArrayOutputStream类有两个构造方法,如下面的表12-8所示。
表12-8 ByteArrayOutputStream类的构造方法
构造方法 | 功能 |
ByteArrayOutputStream() | 创建缓冲区为32字节的对象 |
ByteArrayOutputStream(int numBytes) | 创建对象,缓冲区大小由参数指定 |
第一种形式的构造方法能够创建一个32字节缓冲区的对象,而第二中形式的构造方法所创建的对象的缓冲区大小由numBytes参数所指定。下面的【例12_04】展示了如何使用ByteArrayOutputStream类的对象向字节数组输出数据。
【例12_04 ByteArrayOutputStream类的使用】
Exam12_04.java
import java.io.*;
public class Exam12_04 {
public static void main(String[] args) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
String str = "Hello world!";
byte[] data = str.getBytes();
int len = data.length;
for (int i=0;i<len;i++){//用循环的形式把数据写入数组
stream.write(data[i]);
}
byte[] dest = stream.toByteArray();//①获取目标数组
System.out.println("被输出到数组中的数据是:");
for(int i=0;i<dest.length;i++){
System.out.print((char)dest[i]);
}
System.out.println();//换行
System.out.println("data与dest是否为同一数组:"+(data==dest));
try {
stream.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
【例12_04】用一个ByteArrayOutputStream类的对象stream把一些数据输出到了目标数组中,语句①通过toByteArray()方法获得了这个目标数组,之后打印数组中的数据。ByteArrayOutputStream类的write()方法没有声明异常,所以不用把调用write()方法的语句放到try块中,但关闭流的close()方法必须用try...catch块处理异常。【例12_04】的运行结果如图12-7所示。
图12-7【例12_04】运行结果
从图12-7可以看出:数据已经被正确的输出到了目标数组中,并且这个目标数组dest与原先存放数据的data数组并不是同一个数组,这说明在输出过程中stream对象自动新建了一个字节数组。
12.3.3 FileInputStream类和FileOutputStream类
如果希望读写磁盘文件中的数据,就必须用到FileInputStream类和FileOutputStream类。从这两个类的名称不难看出,它们一个用于读取文件数据,另一个用于把数据输出到文件中。这两个类的主要方法也是read()、write()和close(),但是需要提醒各位读者注意:这两个类的构造方法都声明了表示找不到文件的FileNotFoundException,因此创建对象的语句必须放在try块中。
通过FileInputStream类可以读取任何类型文件中的数据,例如图片、音乐文件等,但这些文件必须配合相应的解码方式才能正确的展示,因此如果使用FileInputStream类对象读取一个png图片中的数据并且直接把这些数据打印到控制台上时显示的都是乱码。
在实际开发过程中,经常使用FileInputStream类和FileOutputStream类相互配合来完成文件复制的操作。文件复制的原理非常简单,只需要用FileInputStream连续读取某个文件中的数据,并且把每次读取到的数据原封不动的输出到另一个文件中即可。下面的【例12_05】展示了如何使用这两个类来完成文件复制的操作。为正确运行程序,需要先在计算机D盘根目录下新建一个名为12_05.docx的word文档,并随意在文档中输入一些文字或插入一些图片。
【例12_05 文件的复制】
Exam12_05.java
import java.io.*;
public class Exam12_05 {
public static void main(String[] args) {
FileInputStream fis = null;//①
FileOutputStream fos = null;//②
try {
fis = new FileInputStream("D:/12_05.docx");
fos = new FileOutputStream("E:/12_05.docx");
int r ;//r用于保存读取到的数据
while ((r=fis.read())!=-1){//使用循环的方式读取数据
fos.write(r);//把读取到的数据输出到E盘下的12_05.docx中
}
}catch (FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if(fis!=null){
try {
fis.close();//关闭输入流
} catch (IOException e) {
e.printStackTrace();
}
}
if(fos!=null){
try {
fos.close();//关闭输出流
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
【例12_05】中,在try块内部创建了对象,这是因为这两个类的构造方法都声明了无法找到文件的异常。但语句①和②在try块之外先用null初始化对象,这是因为如果在try内部直接创建对象,那么这两个对象的作用域都仅限于try块内,从而在finally块中无法对其进行关闭,因此要在try块之外声明对象的存在,而在try块之内创建对象。如果程序运行前D盘上不存在12_05.docx,那么程序运行就会抛出FileNotFoundException。如果D盘上已经存在12_05.docx,那么运行程序后,可以发现在E盘的根目录中出现了一个12_05.docx,它与D盘上的12_05.docx完全一样。很多读者不理解为什么E盘不存在12_05.docx的情况下也能成功复制文件,并没有抛出FileNotFoundException,这是因为这个异常是在目录不存在的情况下才抛出的,例如计算机只有C、D两个磁盘分区,那么把文件复制到E盘上时就会出现异常。
12.3.4 DataInputStream类与DataOutputStream 类
12.3.2和12.3.3这两个小节所介绍的流每次都只能读写一个字节的数据。有时候程序员需要完成对更多类型数据的读写操作,例如把一个long型的数据写入文件,或者把写入文件中的long型数据读入到程序中,在这种情况下,需要使用DataInputStream和DataOutputStream来完成操作。
DataInputStream和DataOutputStream都属于处理流,因此它们不能直接连接到数据源,而是通过所包装的节点流连接到数据源,这两个类提供了读写多种数据类型的方法,下面的表12-9展示了DataOutputStream类向数据源写入数据的各种方法。
表12-9 DataOutputStream类写入数据的方法
方法名 | 功能 |
void write(byte[] b, int off, int len) | 将指定字节数组中从偏移量off开始的len个字节写入数据源 |
void write(int b) | 将指定字节写入数据源 |
void writeBoolean(boolean v) | 将一个boolean值写入数据源 |
void writeByte(int v) | 将一个byte值写入数据源 |
void writeBytes(String s) | 将字符串按字节顺序写入数据源 |
void writeChar(int v) | 将一个char值写入数据源 |
void writeChars(String s) | 将字符串按字符顺序写入数据源 |
void writeDouble(double v) | 将一个double值写入数据源 |
void writeFloat(float v) | 将一个float值写入数据源 |
void writeInt(int v) | 将一个int值写入数据源 |
void writeLong(long v) | 将一个long值写入数据源 |
void writeShort(int v) | 将一个short值写入数据源 |
void writeUTF(String str) | 以与机器无关方式使用UTF-8修改版编码将一个字符串写入数据源 |
从表12-9可以看出:DataOutputStream可以把各种类型的数据写入数据源,这些数据还能被按照原来的形式读出,不需要任何转换操作,读出数据由DataInputStream完成,下面的表12-10展示了DataInputStream类读数据的方法。
表12-10 DataInputStream类读数据的方法
方法名 | 功能 |
int read(byte[] b) | 读取一定数量的字节,并将它们存储到字节数组b中 |
int read(byte[] b, int off, int len) | 读取byte型数据,并把读取到的数据存入b数组的以off开头,长度为len的部分 |
boolean readBoolean() | 读取boolean型数据 |
byte readByte() | 读取byte型数据 |
char readChar() | 读取char型数据 |
double readDouble() | 读取double型数据 |
float readFloat() | 读取float型数据 |
int readInt() | 读取int型数据 |
long readLong() | 读取long型数据 |
short readShort() | 读取short型数据 |
int readUnsignedByte() | 读取一个字节,将它左侧补0转变为int类型,结果范围是0~255 |
int readUnsignedShort() | 读取两个字节,左侧补0转变为int类型,结果范围0~65535 |
String readUTF() | 读入一个已使用 UTF-8 修改版格式编码的字符串 |
对比表12-9和12-10可以发现,DataOutputStream和DataInputStream所定义的读写数据的方法大多数都是对应的,例如它们分别定义了writeDouble()和readDouble()方法,这样就能够使得写入数据源的数据都能原封不动的被读取出来,但也有些方法在在另一个类中找不到对应的方法,例如DataOutputStream定义了writeChars()方法和writeBytes()方法,但DataInputStream类却没有定义readChars()方法和readBytes()方法,这是因为writeChars()方法实际上是把一组字符以字符串写入数据源的方法,而writeBytes()方法则是把一组byte数据以字符串的形式写入数据源的方法,因此用这两个方法写入的数据要调用readChar()和readByte()方法读取。读取数据的方法一定要与写入数据的方法配合使用才能正确读取数据,例如用writeInt()写入的int型数据就不能用readLong()方法读取,否则会出现异常。下面的【例12_06】展示了如何用DataInputStream类与DataOutputStream类读写不同类型的数据,为正确读写数据,需要先在D盘根目录上新建一个12_06.txt文件。
【例12_06读写多种类型数据】
Exam12_06.java
import java.io.*;
public class Exam12_06 {
public static void main(String[] args) {
DataInputStream dis = null;
DataOutputStream dos = null;
FileInputStream fis = null;
FileOutputStream fos = null;
try{
fis = new FileInputStream("D:/12_06.txt");
dis = new DataInputStream(fis);
fos = new FileOutputStream("D:/ 12_06.txt");
dos = new DataOutputStream(fos);
String[] course = { "语文","数学","政治","历史","生物"};//课程名称
double[] score = { 80.5, 66, 89, 76,77.5};//成绩
int[] index = { 5, 10, 4, 29, 18}; //名次
int len = score.length;//数组的长度
//写入数据
for (int i = 0; i <len; i++)
{
dos.writeUTF(course[i]);
dos.writeDouble(score[i]);
dos.writeInt(index[i]);
}
System.out.println("课程 成绩 名次");
//读取数据
for(int i=0;i<len;i++)
{
System.out.print(""+dis.readUTF());
System.out.print(" "+dis.readDouble());
System.out.println(" "+dis.readInt());
}
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}finally {
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(dis!=null){
try {
dis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fos!=null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(dos!=null){
try {
dos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
【例12_06】中创建了一个DataInputStream对象和一个DataOutputStream对象用于读写多种类型的数据,可以看出:创建这两个对象时以FileInputStream类对象和FileOutputStream类对象作为构造方法的参数,这就是处理流创建对象的特征:构造方法不以数据源为参数,而是以另一个流对象作为参数,从而完成对这个流的包装,处理流的所操作的数据源就是由它包装的流对象指定。在对流对象进行关闭时,要首先关闭内层的节点流,再关闭外层的处理流。【例12_06】的运行结果如图12-8所示。
图12-8【例12_06】运行结果
当程序运行完毕后,读者可以打开12_06.txt,可以发现文件中的内容都是乱码。这是文件中保存的多种类型的数据,而记事本却把这些数据都当作字符串进行解码,因此出现乱码。