深入理解JAVA I/O系列二:字节流详解
流的概念
JAVA程序通过流来完成输入/输出。流是生产或消费信息的抽象,流通过JAVA的输入输出与物理设备链接,尽管与它们链接的物理设备不尽相同,所有流的行为具有相同的方式。这样就意味一个输入流能够抽象多种不同类型的输入:从磁盘文件、从键盘或从网络套接字;同样,一个输出流可以输出到控制台、磁盘文件或相连的网络。
在我们平时接触的输入/输出流中,有这样一个概念必须要弄明白,何谓输入、何谓输出?讨论这个问题的前提是要知道以什么为参考物,这个参考物就是程序或者内存。输入:就是从磁盘文件或者网络等外部的数据流向程序或者内存。输出就是相反的过程。其中这个“外部”可以是很多种介质:
1、本地磁盘文件、远程磁盘文件。
2、数据库链接
3、TCP、UDP、HTTP网络通信
4、进程间通信
5、硬件设备(键盘、串口)
流的分类
1、从功能上:输入流、输出流
2、从结构上:字节流、字符流
3、从来源上:节点流、过滤流
其中InputStream/OutputStream是为字节流而设计的,Reader/Writer是为字符流而设计的。处理字节或者二进制对象使用字节流,处理字符或者字符串使用字符流。
在最底层,所有的输入/输出都是字节形式的,基于字符的流只在处理字符的时候提供方便有效的方法。
节点流是从特定的地方读写的流,例如磁盘或者内存空间,也就是这种流是直接对接目标的。
过滤流是使用节点流作为输入输出的,就是在节点流的基础上进行包装,新增一些特定的功能。
InputStream
其中有底色标注的为节点流,无底色标注的为过滤流,其中FilterInputStream在JDK中的定义为:包含其他一些输入流,它将这些流用作其基本数据源,可以直接传输数据或提供一些额外的功能,这个类本身并不经常被我们使用,常用的是它的子类。
定义了字节输入模式的抽象类,该类提供了三个重载的read方法:
我们可以看到,三个read方法中,其中有一个是抽象的。那在这里思考这样一个问题:为什么只有第一个是抽象的, 其他两个是具体的?
因为后面两个方法内部最终会去调用第一个方法,所以在InputStream派生类中只需要重写第一个方法就可以了。在这里可以看到第一个read方法是与具体的I/O设备相关的,需要子类去实现。
读写数据的逻辑步骤为:
open a stream
while more information
read/write information
close the stream
一、FileInputStream
1 public static void main(String[] args) throws IOException 2 { 3 InputStream is = new FileInputStream("c:/test.txt"); 4 int length = 0; 5 byte[] buffer = new byte[20]; 6 while(-1 != (length = is.read(buffer,0,20))) 7 { 8 String str = new String(buffer,0,length); 9 System.out.print(str); 10 } 11 is.close(); 12 }
执行结果:
中国移动基地咪咕阅读 中国移动基地咪咕音乐 a
1、首先我们看下读取文件test的内容:
2、在这里我们使用的是输入流,读取的是我本机C盘中名为test文件中的内容。
3、每次读取的内容存放在buffer这个字节数组中,然后转换成String字符串打印在控制台中。
4、我们来看下第6行代码:因为一个文件内容有多少我们事先并不知道,所以在这里只能分批次读取,每次最多读取20个字节(即buffer数组定义的长度)。
5、length的作用是表示在最后一次读取的时候,读取的长度小于等于buffer数组的长度,while循环体执行结束后,下一次再来读取已经没有内容了,read方法在这个时候会返 回-1,然后跳出循环。
6、对于流的操作,最后一步永远是调用close方法,释放资源。
总结:文本中内容故意定义为41个字节长度,然后打印的时候用了换行。从执行结果可以看到,前两次读取的长度都是20个字节,第三次是1个字节,最后一次没有内容返回-1,然后就跳出循环体了。这个DEMO虽然很简单,但是也是IO种最基本最需要掌握的一部分。
二、FileOutputStream
这个类与FileInputStream是成对的,用法也类似:
1 public static void main(String[] args) throws IOException 2 { 3 OutputStream os = new FileOutputStream("c:/test1.txt"); 4 String str = "中国移动手机阅读"; 5 byte[] b = str.getBytes(); 6 os.write(b); 7 os.close(); 8 }
执行结果:
1、这个DEMO主要是将字符串写入磁盘文件中。
2、在第3行的构造函数处要注意下,这个方法中如果指定的文件不存在,则会创建一个新的;如果指定的文件存在,在后面的写入操作会覆盖原有的内容。
这个大家会有这样一个疑问,如果我不想覆盖原有的内容,只想在后面追加内容呢?
1 public static void main(String[] args) throws IOException 2 { 3 OutputStream os = new FileOutputStream("c:/test.txt",true); 4 String str = "注册用户6亿"; 5 byte[] b = str.getBytes(); 6 os.write(b); 7 os.close(); 8 }
执行结果:
1、从执行结果看,这个是在原有内容后面进行追加;程序唯一的区别就在于第三行的构造函数。
OutputStream os = new FileOutputStream("c:/test.txt",true);
如果第二个参数为 true
,则将字节写入文件末尾处,而不是写入文件开始处。
三、BufferedOutputStream
在前面介绍的FileOutputStream,是在程序里面读取一个字节,就往外写一个字节。在这种情况下,频繁的跟IO设备打交道,I/O的处理速度跟CPU的速度根本就不在一个量级上(I/O是一种物理设备),在信息量很多的时候,就比较消耗性能。基于这种问题,JAVA提供了缓冲字节流,通过这种流,应用程序就可以将各个字节写入底层输出流中,而不必针对每次字节写入调用底层系统。
1 public static void main(String[] args) throws IOException 2 { 3 OutputStream os = new FileOutputStream("c:/test.txt"); 4 OutputStream bs = new BufferedOutputStream(os); 5 byte[] buffer = "中国移动阅读基地".getBytes(); 6 bs.write(buffer); 7 bs.close(); 8 os.close(); 9 }
执行结果:
缓冲输出流在输出的时候,不是直接一个字节一个字节的操作,而是先写入内存的缓冲区内。直到缓冲区满了或者我们调用close方法或flush方法,该缓冲区的内容才会写入目标。才会从内存中转移到磁盘上。下面来看看不调用close方法会出现什么情况:
1 public static void main(String[] args) throws IOException 2 { 3 OutputStream os = new FileOutputStream("c:/test1.txt"); 4 OutputStream bs = new BufferedOutputStream(os); 5 byte[] buffer = "中国移动阅读基地".getBytes(); 6 bs.write(buffer); 7 // bs.close(); 8 // os.close(); 9 }
执行结果:
在这里没有调用close方法,相当于内容还在内存的缓冲区中。BufferedOutputStream本身没有close方法,调用的是父类FilterOutputStream的close方法:
1 public void close() throws IOException { 2 try { 3 flush(); 4 } catch (IOException ignored) { 5 } 6 out.close(); 7 }
在这里可以看到这个方法的本质是在在关闭资源之前,调用的flush方法。
而flush在JDK中的定义为:刷新此缓冲的输出流。这迫使所有缓冲的输出字节被写出到底层输出流中
四、DataOutputStream
数据输出流允许应用程序以适当方式将基本 Java 数据类型写入输出流中。然后,应用程序可以使用数据输入流将数据读入。
1 public static void main(String[] args) throws IOException 2 { 3 DataOutputStream dos = new DataOutputStream(new BufferedOutputStream( 4 new FileOutputStream("c:/data.txt"))); 5 byte b = 4; 6 char c = 'c'; 7 int i = 12; 8 float f = 3.3f; 9 dos.writeByte(b); 10 dos.writeChar(c); 11 dos.writeInt(i); 12 dos.writeFloat(f); 13 dos.close();
执行结果:
打开之后,里面是乱码,程序写入之后是一个二进制文件。我们的程序中是将java的基本数据类型写入文本,注意这里不是字符串,而是基本数据类型。我们这样写入是没有意义的,下面我们用同样的方式去读取。
1 DataOutputStream dos = new DataOutputStream(new BufferedOutputStream( 2 new FileOutputStream("c:/data.txt"))); 3 byte b = 4; 4 char c = 'c'; 5 int i = 12; 6 float f = 3.3f; 7 dos.writeByte(b); 8 dos.writeChar(c); 9 dos.writeInt(i); 10 dos.writeFloat(f); 11 dos.close(); 12 13 DataInputStream dis = new DataInputStream(new BufferedInputStream( 14 new FileInputStream("c:/data.txt"))); 15 System.out.println(dis.readByte()); 16 System.out.println(dis.readChar()); 17 System.out.println(dis.readInt()); 18 System.out.println(dis.readFloat()); 19 dis.close(); 20
执行结果:
4 c 12 3.3
这里看到,我们的数据输入流允许应用程序以与机器无关方式从底层输入流中读取基本JAVA数据类型。
这里特别注意的地方是:读取数据类型的顺序与当初写入的数据类型的顺序要一致,否则会出现乱码或者读取的信息不准确。
这个原因我们可以这样来理解:写入的时候是不同类型的基本数据,不同的基本数据类型的字节长度不一样,如果读取时候顺序不一致,会导致字节的组合混乱,导致乱码或者走义。