面向字节的输入输出流

本文最后更新于:2024年3月18日 凌晨

面向字节的输入输出流

面向字节的输入流

类InputStream介绍

  • 面向字节的输入流类都是类InputStream的子类,如下图所示,类InputStream是一个抽象类,定义了如下方法:
    • public int read():读一个字节,返回读到字节的int表示方式(0~255),读到流的末尾时返回-1
    • public int read(byte b[]):读多个字节到字节数组,返回结果为读到的实际字节个数,当输入流中无数据可读时返回-1
    • public int read(byte[] b,int off,int len):从输入流读制定长度的数据到字节数组,数据从字节数组的off处开始存放,当输入流中无数据可读时返回-1
    • public long skip(long n):指针跳过n个字节,定义输入位置指针的方法。
    • public void mark():在当前位置指针处做一标记。
    • public void reset():将位置指针返回标记处。
    • public void close():关闭流。
  • 数据的读取通常按照顺序逐个字节进行访问,在某些特殊情况下,要重复处理某个字节可通过mark()加标记,以后用reset()返回该标记处再处理。

[例13-1]:将由键盘输入的一个十六进制数转换为十进制输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import java.io.IOException;

public class 进制转换 {// 将由键盘输入的一个十六进制数转换为十进制输出。

public static void main(String[] args) {
try {
char ch;// 存放输入的字符。
int x = 0;// 存放转换后的数字。
long d = 0;// 存放转换的十进制数。
System.out.println("输入一个十六进制数");
ch = (char) System.in.read();// 读一个字符,字节强制转换为字符。
while (ch != '\n') {
switch (ch) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
x = (ch - '0');
break;
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
x = (ch - 'A') + 10;
break;
case 'a':
case 'b':
case 'c':
case 'd':
case 'e':
case 'f':
x = (ch - 'a') + 10;
break;
default:
System.out.println("非法符号");
System.exit(0);
}
d = d * 16 + x;// 转换结果拼接。
ch = (char) System.in.read();
}
System.out.println("十进制=" + d);
} catch (IOException e) {
}
}
}

输入一个十六进制数。
2a
十进制=42
  • 说明:任何一个数字字符与0字符之差就是其数字值,而a,b等字符则分别代表十进制10,11等数字,字符串拼接时注意是按十六进位的,所以拼接时将前面数字串的结果乘16再加上本位的结果。
  • 思考:本程序是用逐个字符判别进行计算拼接的办法,实际上,有一种简单的办法,利用Ineger.parseInt(String,16)方法可将十六进制字符串准换为十进制的整数。

类InputStream的子类的使用

  • InputStream的主要子类及功能如下表所示:
类名 构造方法的主要参数 功能描述
ByteArrayInputStream 字节数组 以程序中的一个字节数组作为输入源,通常用于对字节数组中的数据进行转换
FileInputStream 类File的对象或字符串表示的文件名 以文件作为数据源,用于实现对磁盘文件中数据的读取
PipedInputStream PipedOutputStream的对象 与另一输出管道相连,读取写入到输出管道中的数据,用于程序中线程间通信
FilterInputStream InputStream的对象 用于修饰另一输出流以提供对输入数据的附加处理功能,其子类如下表所示
SequenceInputStream 一系列InputStream的对象 将两个其他流首尾相连,合并为一个完整的输入流
ObjectInputStream InputStream的对象 用于从输入流读取串行化对象,可实现轻量级对象持久性
  • 其中过滤输入流类FilterInputStream是一个抽象类,没有提供实质的过滤功能,其子类中定义了具体的过滤功能,如下表所示:
类名 功能描述
BufferedInputStream 为所装饰的输入流提供缓冲区的功能,以提高输入数据的效率
DataInputStream 为所装饰的输入流提供数据转换的功能,可从数据源读取各种基本类型的数据
LineNumberInputStream 为文本文件输入流附加行号
PushbackInputStream 提供回压数据的功能,可以多次读同样数据
  • 以下结合数据操作访问单位的特点介绍若干流的使用。

以字节为单位读取数据

  • 以文件访问操作为例,可利用文件输入流(FileInputStream)的方法从文件读取数据,注意,读到文件结尾时read()方法访问-1,编程时可以利用该特点来组织循环,从文件的第一个字节一直读到最后一个字节。

[例13-2]:在屏幕上显示文件内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class 在屏幕上显示文件内容 {
public static void main(String[] args) {
try{
FileInputStream inifile = new FileInputStream(args[0]);
int byteRead = inifile.read();
while (byteRead!=-1){ // 判断是否读到文件的末尾。
System.out.println((char)byteRead);// 将字节转换为字符显示。
byteRead=inifile.read();
}
}
catch (ArrayIndexOutOfBoundsException e){
System.out.println("要一个文件名作为命令行参数!");
}
catch (FileNotFoundException e){
System.out.println("文件不存在");
}
catch (IOException e){}
}
finally(){
inifile.close();
}
}
  • 说明:从命令行参数获取要显示的文件的文件名,利用FileInputStream的构造方法建立对文件进行操作的输入流,利用循环从文件逐个字节读取数据。将读到的数据转换为字符在屏幕上显示,运行程序不难发现,本程序可查看文本文件的内容,但如果输入的文件是二进制文件(如Java程序的class文件等)则看到的是乱码,因为那些文件中的数据不是字符,强制转换为字符是没有意义的。

以数据类型为单位读取数据

  • DataInputStream实现了DataInput接口,DataInput接口规定了基本类型数据的读取方法,如readByte(),readBoolean(),readShort(),readChar(),readInt(),readLong(),readFloat(),readDouble()以及读取字符串的readUTF()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class 在屏幕上显示文件内容 {
public static void main(String[] args) {
try{
FileInputStream inifile = new FileInputStream(args[0]);
int byteRead = inifile.readChar();
while (byteRead!=-1){ // 判断是否读到文件的末尾。
System.out.println(byteRead);
byteRead=inifile.readChar();
}
}
catch (ArrayIndexOutOfBoundsException e){
System.out.println("要一个文件名作为命令行参数!");
}
catch (FileNotFoundException e){
System.out.println("文件不存在");
}
catch (IOException e){}
}
finally(){
inifile.close();
}
}

缓冲

  • 在读取流的时候,一次读取一个字节并不是最高效的方法,很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多,InputStream提供了两个重载方法来支持读取多个字节:
  • int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数。
  • int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数。
  • 利用上述方法一次读取多个字节时,需要先定义一个byte[]数组作为缓冲区,read()方法会尽可能多地读取字节到缓冲区,但不会超过缓冲区的大小,read()方法的返回值不再是字节的int值,而是返回实际读取了多少个字节,如果返回-1,表示没有更多的数据了。
  • 利用缓冲区一次读取多个字节的代码如下:
1
2
3
4
5
6
7
8
9
10
public void readFile() throws IOException {
try (InputStream input = new FileInputStream("src/readme.txt")) {
// 定义1000个字节大小的缓冲区:
byte[] buffer = new byte[1000];
int n;
while ((n = input.read(buffer)) != -1) { // 读取到缓冲区。
System.out.println("read " + n + " bytes.");
}
}
}

面向字节的输出流

  • 面向字节的输入流都是类OutputStream的后代类,如下图所示,类OutputStream是一个抽象类,含一套所有输出流均需要的方法。
    • public void write(int b):将参数b的低字节写入输出流。
    • public void write(byte[] b):将字节数组全部写入输出流。
    • public void flush():强制将缓冲区数据写入输出流对应的外设。
    • public void close():关闭输出流。
  • 其中,PrintStream提供了常用的print(),println(),printf()等方法。

以字节为单位的数据写入

[例13-3]:将一个大文件分拆为若干小文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class 将一个大文件分拆为若干小文件 {
public static void main(String[] args) {
FileInputStream infile;// 大文件。
FileOutputStream outfile;// 小文件。
int number = 0;
final int size = Integer.parseInt(args[1]);// 小文件大小。
byte[] b = new byte[size];// 创建一个字节数组存放读取的数据。
try {
infile = new FileInputStream(args[0]);// 大文件。
while (true) {
int byteRead = infile.read(b);// 从文件读数据给字节数组。
if (byteRead == -1)// 在文件尾,无数据可读。
break;
outfile = new FileOutputStream("src/Study/流式输入输出与文件处理/面向字节的输入输出流/面向字节的输出流/以字节为单位的数据写入/file" + number);// 创建小文件。
number++;
outfile.write(b,0,byteRead);
outfile.close();
}
} catch (IOException e) {
}
}
}
  • 说明:运行程序需要两个参数,一个是要分拆的大文件名,另一个是小文件的大小,分拆的小文件命名为file0,file1,…
  • 注意:将数据写入文件用write(b,0,byteRead)方法是保证将当前读到的数据写入文件,不能直接用write(b),因为最后读的子文件通常会更少。

以数据类型为单位写入数据

  • 类DataOutputStream实现各种基本类型数据的输出处理,它实现了DataOutput接口,在该接口中定义了基本类型数据的输出方法,如writeByte(int),writeBytes(String),writeBoolean(boolean),writeChars(String),writeInt(int),writeLong(long),writeFloat(float),writeDouble(double),writeUTF(String)等。
  • 以下结合一个文件写入的例子演示基本类型数据的读写访问处理。

[例13-4]:找出10~100之间的所有姐妹素数,写入到文件中,所谓姐妹素数是指相邻两个奇数均为素数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class 素数判断 {
/*判断一个数是否为素数,是返回true,否则返回false*/
public static boolean isPrime(int n) {
for (int k = 2; k <= Math.sqrt(n); k++) {
if (n % k == 0)
return false;
}
return true;
}

public static void main(String[] args) {
try {
// 创建一个FileOutputStream文件输出流,如果对应名称的文件不存在,系统会自动新建文件。
FileOutputStream file = new FileOutputStream("./src/Study/流式输入输出与文件处理/面向字节的输入输出流/面向字节的输出流/以数据类型为单位的数据写入/x.dat");
// 对FileOutputStream进行包装,创建了一个DataOutputStream流,可以利用该流给文件写入各种基本类型的数据。
DataOutputStream out = new DataOutputStream(file);
for (int n = 11; n < 100; n += 2) {
if (isPrime(n) && isPrime(n + 2)) {// 两个相邻奇数是否为素数。
// 利用DataOutputStream的writeInt()方法将找到的素数写入文件。
out.writeInt(n);// 将素数写入文件。
out.writeInt(n + 2);
}
}
out.close();
} catch (IOException e) { }
}
}
  • 注意:用记事本查看文件将显示乱码,原因在于该文件中的数据不是文本格式的数据,要读取其中的数据需要以输入流的方式访问文件,用DataInputStreamreadInt()方法读取对应数据,以下为程序代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class 文本读取 {
public static void main(String[] args) {
try {
FileInputStream file = new FileInputStream("./src/Study/流式输入输出与文件处理/面向字节的输入输出流/面向字节的输出流/以数据类型为单位的数据写入/x.dat");
DataInputStream in = new DataInputStream(file);
while (true) {
int n1 = in.readInt();// 从文件读取整数。
int n2 = in.readInt();
System.out.println(n1 + "," + n2);// 输出相邻的两个素数。
}
} catch (EOFException e) {
} catch (IOException e) {
}
}
}

注意

  • 本程序在处理文件访问中利用了异常处理机制,在try块中用无限循环来读取访问文件,如果遇到文件结束将抛出EOFException异常。
  • 从上面的例子可以看出,各种过滤流实际上是对数据进行特殊的包装处理,在读写字节的基础上提供更高级的功能,从而更方便地访问数据。